#!/usr/bin/env python # # Revelation - a password manager for GNOME 2 # http://oss.codepoet.no/revelation/ # $Id$ # # Copyright (c) 2003-2006 Erik Grinaker # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # import gettext, gnome, gobject, gtk, gtk.gdk, os, pwd, sys if "@pyexecdir@" not in sys.path: sys.path.insert(0, "@pyexecdir@") from revelation import config, data, datahandler, dialog, entry, io, ui, util tran = gettext.translation(config.PACKAGE,config.DIR_LOCALE) _ = tran.ugettext class Revelation(ui.App): "The Revelation application" def __init__(self): sys.excepthook = self.__cb_exception os.umask(0077) gettext.bindtextdomain(config.PACKAGE, config.DIR_LOCALE) gettext.bind_textdomain_codeset(config.PACKAGE, "UTF-8") gettext.textdomain(config.PACKAGE) self.program = gnome.init( config.APPNAME, config.APPNAME, gnome.libgnome_module_info_get(), sys.argv, [] ) gnome.ui.authentication_manager_init() ui.App.__init__(self, config.APPNAME) self.connect("delete-event", self.__cb_quit) try: self.__init_actions() self.__init_facilities() self.__init_ui() self.__init_states() except IOError: dialog.Error(self, _('Missing data files'), _('Some of Revelations system files could not be found, please reinstall Revelation.')).run() sys.exit(1) except config.ConfigError: dialog.Error(self, _('Missing configuration data'), _('Revelation could not find its configuration data, please reinstall Revelation.')).run() sys.exit(1) except ui.DataError: dialog.Error(self, _('Invalid data files'), _('Some of Revelations system files contain invalid data, please reinstall Revelation.')).run() sys.exit(1) def __init_actions(self): "Sets up actions" # set up placeholders group = ui.ActionGroup("placeholder") self.uimanager.append_action_group(group) group.add_action(ui.Action("menu-edit", _('_Edit'))) group.add_action(ui.Action("menu-entry", _('E_ntry'))) group.add_action(ui.Action("menu-file", _('_File'))) group.add_action(ui.Action("menu-help", _('_Help'))) group.add_action(ui.Action("menu-view", _('_View'))) group.add_action(ui.Action("popup-tree")) # set up dynamic actions group = ui.ActionGroup("dynamic") self.uimanager.append_action_group(group) action = ui.Action("clip-paste", _('_Paste'), _('Paste entry from clipboard'), "gtk-paste") action.connect("activate", self.__cb_clip_paste) group.add_action(action, "V") action = ui.Action("entry-goto", _('_Go to'), _('Go to the selected entries'), "revelation-goto", True) action.connect("activate", lambda w: self.entry_goto(self.tree.get_selected())) group.add_action(action, "Return") action = ui.Action("redo", _('_Redo'), _('Redo the previously undone action'), "gtk-redo") action.connect("activate", lambda w: self.redo()) group.add_action(action, "Z") action = ui.Action("undo", _('_Undo'), _('Undo the last action'), "gtk-undo") action.connect("activate", lambda w: self.undo()) group.add_action(action, "Z") # set up group for multiple entries group = ui.ActionGroup("entry-multiple") self.uimanager.append_action_group(group) action = ui.Action("clip-copy", _('_Copy'), _('Copy selected entries to the clipboard'), "gtk-copy") action.connect("activate", self.__cb_clip_copy) group.add_action(action, "C") action = ui.Action("clip-chain", _('_Copy Pass_word'), _('Copy password to the clipboard')) action.connect("activate", lambda w: self.clip_chain(self.entrystore.get_entry(self.tree.get_active()))) group.add_action(action, "C") action = ui.Action("clip-cut", _('Cu_t'), _('Cut selected entries to the clipboard'), "gtk-cut") action.connect("activate", self.__cb_clip_cut) group.add_action(action, "X") action = ui.Action("entry-remove", _('Re_move'), _('Remove the selected entries'), "revelation-remove") action.connect("activate", lambda w: self.entry_remove(self.tree.get_selected())) group.add_action(action, "Delete") # action group for "optional" entries group = ui.ActionGroup("entry-optional") self.uimanager.append_action_group(group) action = ui.Action("entry-add", _('_Add Entry...'), _('Create a new entry'), "revelation-new-entry", True) action.connect("activate", lambda w: self.entry_add(None, self.tree.get_active())) group.add_action(action, "Insert") action = ui.Action("entry-folder", _('Add _Folder...'), _('Create a new folder'), "revelation-new-folder") action.connect("activate", lambda w: self.entry_folder(None, self.tree.get_active())) group.add_action(action, "Insert") # action group for single entries group = ui.ActionGroup("entry-single") self.uimanager.append_action_group(group) action = ui.Action("entry-edit", _('_Edit...'), _('Edit the selected entry'), "revelation-edit") action.connect("activate", lambda w: self.entry_edit(self.tree.get_active())) group.add_action(action, "Return") # action group for existing file group = ui.ActionGroup("file-exists") self.uimanager.append_action_group(group) action = ui.Action("file-lock", _('_Lock'), _('Lock the current data file'), "revelation-lock") action.connect("activate", lambda w: self.file_lock()) group.add_action(action, "L") # action group for searching group = ui.ActionGroup("find") self.uimanager.append_action_group(group) action = ui.Action("find-next", _('Find Ne_xt'), _('Find the next search match'), "find-next") action.connect("activate", lambda w: self.__entry_find(self, self.searchbar.entry.get_text(), self.searchbar.dropdown.get_active_type(), data.SEARCH_NEXT)) group.add_action(action, "G") action = ui.Action("find-previous", _('Find Pre_vious'), _('Find the previous search match'), "find-previous") action.connect("activate", lambda w: self.__entry_find(self, self.searchbar.entry.get_text(), self.searchbar.dropdown.get_active_type(), data.SEARCH_PREVIOUS)) group.add_action(action, "G") # global action group group = ui.ActionGroup("find") self.uimanager.append_action_group(group) action = ui.Action("file-change-password", _('Change _Password...'), _('Change password of current file'), "revelation-password-change") action.connect("activate", lambda w: self.file_change_password()) group.add_action(action) action = ui.Action("file-close", _('_Close'), _('Close the application'), "gtk-close") action.connect("activate", self.__cb_quit) group.add_action(action, "W") action = ui.Action("file-export", _('_Export...'), _('Export data to a different file format'), "revelation-export") action.connect("activate", lambda w: self.file_export()) group.add_action(action) action = ui.Action("file-import", _('_Import...'), _('Import data from a foreign file'), "revelation-import") action.connect("activate", lambda w: self.file_import()) group.add_action(action) action = ui.Action("file-new", _('_New'), _('Create a new file'), "gtk-new") action.connect("activate", lambda w: self.file_new()) group.add_action(action, "N") action = ui.Action("file-open", _('_Open'), _('Open a file'), "gtk-open") action.connect("activate", lambda w: self.file_open()) group.add_action(action, "O") action = ui.Action("file-save", _('_Save'), _('Save data to a file'), "gtk-save", True) action.connect("activate", lambda w: self.file_save(self.datafile.get_file(), self.datafile.get_password())) group.add_action(action, "S") action = ui.Action("file-save-as", _('Save _as...'), _('Save data to a different file'), "gtk-save-as") action.connect("activate", lambda w: self.file_save(None, None)) group.add_action(action, "S") action = ui.Action("find", _('_Find...'), _('Search for an entry'), "gtk-find") action.connect("activate", lambda w: self.entry_find()) group.add_action(action, "F") action = ui.Action("help-about", _('_About'), _('About this application'), "gnome-stock-about") action.connect("activate", lambda w: self.about()) group.add_action(action) action = ui.Action("prefs", _('Prefere_nces'), _('Edit preferences'), "gtk-preferences") action.connect("activate", lambda w: self.prefs()) group.add_action(action) action = ui.Action("pwchecker", _('Password _Checker'), _('Opens a password checker'), "revelation-password-check") action.connect("activate", lambda w: self.pwcheck()) group.add_action(action) action = ui.Action("pwgenerator", _('Password _Generator'), _('Opens a password generator'), "revelation-generate") action.connect("activate", lambda w: self.pwgen()) group.add_action(action) action = ui.Action("quit", _('_Quit'), _('Quit the application'), "gtk-quit") action.connect("activate", self.__cb_quit) group.add_action(action, "Q") action = ui.Action("select-all", _('_Select All'), _('Selects all entries')) action.connect("activate", lambda w: self.tree.select_all()) group.add_action(action, "A") action = ui.Action("select-none", _('_Deselect All'), _('Deselects all entries')) action.connect("activate", lambda w: self.tree.unselect_all()) group.add_action(action, "A") action = ui.ToggleAction("view-passwords", _('Show _Passwords'), _('Toggle display of passwords')) group.add_action(action, "P") action = ui.ToggleAction("view-searchbar", _('S_earch Toolbar'), _('Toggle the search toolbar')) group.add_action(action) action = ui.ToggleAction("view-statusbar", _('_Statusbar'), _('Toggle the statusbar')) group.add_action(action) action = ui.ToggleAction("view-toolbar", _('_Main Toolbar'), _('Toggle the main toolbar')) group.add_action(action) def __init_facilities(self): "Sets up various facilities" self.clipboard = data.Clipboard() self.config = config.Config() self.datafile = io.DataFile(datahandler.Revelation) self.entryclipboard = data.EntryClipboard() self.entrystore = data.EntryStore() self.entrysearch = data.EntrySearch(self.entrystore) self.items = ui.ItemFactory(self) self.locktimer = data.Timer() self.sessionclient = gnome.ui.master_client() self.undoqueue = data.UndoQueue() self.datafile.connect("changed", lambda w,f: self.__state_file(f)) self.datafile.connect("content-changed", self.__cb_file_content_changed) self.entryclipboard.connect("content-toggled", lambda w,d: self.__state_clipboard(d)) self.locktimer.connect("ring", self.__cb_file_autolock) self.sessionclient.connect("die", lambda w: self.quit()) self.sessionclient.connect("save-yourself", self.__cb_session_save) self.undoqueue.connect("changed", lambda w: self.__state_undo(self.undoqueue.get_undo_action(), self.undoqueue.get_redo_action())) # check if configuration is updated, install schema if not if self.__check_config() == False: if config.install_schema("%s/revelation.schemas" % config.DIR_GCONFSCHEMAS) == False: raise config.ConfigError self.config.client.clear_cache() if self.__check_config() == False: raise config.ConfigError self.config.monitor("file/autolock_timeout", lambda k,v,d: self.locktimer.start(v * 60)) dialog.EVENT_FILTER = self.__cb_event_filter def __init_states(self): "Sets the initial application state" # set window states self.set_default_size( self.config.get("view/window-width"), self.config.get("view/window-height") ) self.move( self.config.get("view/window-position-x"), self.config.get("view/window-position-y") ) self.hpaned.set_position( self.config.get("view/pane-position") ) # bind ui widgets to config keys bind = { "view/passwords" : "/menubar/menu-view/view-passwords", "view/searchbar" : "/menubar/menu-view/view-searchbar", "view/statusbar" : "/menubar/menu-view/view-statusbar", "view/toolbar" : "/menubar/menu-view/view-toolbar" } for key, path in bind.items(): ui.config_bind(self.config, key, self.uimanager.get_widget(path)) self.show_all() self.window.add_filter(self.__cb_event_filter) # set some variables self.entrysearch.string = '' self.entrysearch.type = None # set ui widget states self.__state_clipboard(self.entryclipboard.has_contents()) self.__state_entry([]) self.__state_file(None) self.__state_find(self.searchbar.entry.get_text()) self.__state_undo(None, None) # set states from config self.config.monitor("view/searchbar", self.__cb_config_toolbar, self.searchbar) self.config.monitor("view/statusbar", self.__cb_config_toolbar, self.statusbar) self.config.monitor("view/toolbar", self.__cb_config_toolbar, self.toolbar) self.config.monitor("view/toolbar_style", self.__cb_config_toolbar_style) # give focus to searchbar entry if shown if self.searchbar.get_property("visible") == True: self.searchbar.entry.grab_focus() def __init_ui(self): "Sets up the UI" gtk.about_dialog_set_url_hook(lambda d,l: gnome.url_show(l)) gtk.about_dialog_set_email_hook(lambda d,l: gnome.url_show("mailto:" + l)) # set window icons pixbufs = [ self.items.get_pixbuf("revelation", size) for size in ( 48, 32, 24, 16) ] pixbufs = [ pixbuf for pixbuf in pixbufs if pixbuf != None ] if len(pixbufs) > 0: gtk.window_set_default_icon_list(*pixbufs) # load UI definitions self.uimanager.add_ui_from_file(config.DIR_UI + "/menubar.xml") self.uimanager.add_ui_from_file(config.DIR_UI + "/popup-tree.xml") self.uimanager.add_ui_from_file(config.DIR_UI + "/toolbar.xml") # set up toolbar and menus self.set_menus(self.uimanager.get_widget("/menubar")) self.toolbar = self.uimanager.get_widget("/toolbar") self.toolbar.connect("popup-context-menu", lambda w,x,y,b: True) self.set_toolbar(self.toolbar) try: detachable = self.config.get("/desktop/gnome/interface/toolbar_detachable") except config.ConfigError: detachable = False self.searchbar = ui.Searchbar() self.add_toolbar(self.searchbar, "searchbar", 2, detachable) # set up main application widgets self.tree = ui.EntryTree(self.entrystore) self.scrolledwindow = ui.ScrolledWindow(self.tree) self.entryview = ui.EntryView(self.config, self.clipboard) alignment = ui.Alignment(self.entryview, 0.5, 0.5, 1, 0) self.hpaned = ui.HPaned(self.scrolledwindow, alignment) self.set_contents(self.hpaned) # set up drag-and-drop self.drag_dest_set(gtk.DEST_DEFAULT_ALL, ( ( "text/uri-list", 0, 0 ), ), gtk.gdk.ACTION_COPY | gtk.gdk.ACTION_MOVE | gtk.gdk.ACTION_LINK ) self.connect("drag_data_received", self.__cb_drag_dest) self.tree.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, ( ( "revelation/treerow", gtk.TARGET_SAME_APP | gtk.TARGET_SAME_WIDGET, 0), ), gtk.gdk.ACTION_MOVE) self.tree.enable_model_drag_dest(( ( "revelation/treerow", gtk.TARGET_SAME_APP | gtk.TARGET_SAME_WIDGET, 0), ), gtk.gdk.ACTION_MOVE) self.tree.connect("drag_data_received", self.__cb_tree_drag_received) # set up callbacks self.searchbar.connect("key-press-event", self.__cb_searchbar_key_press) self.searchbar.button_next.connect("clicked", self.__cb_searchbar_button_clicked, data.SEARCH_NEXT) self.searchbar.button_prev.connect("clicked", self.__cb_searchbar_button_clicked, data.SEARCH_PREVIOUS) self.searchbar.entry.connect("changed", lambda w: self.__state_find(self.searchbar.entry.get_text())) self.tree.connect("popup", lambda w,d: self.popup(self.uimanager.get_widget("/popup-tree"), d.button, d.time)) self.tree.connect("doubleclick", self.__cb_tree_doubleclick) self.tree.connect("key-press-event", self.__cb_tree_keypress) self.tree.selection.connect("changed", lambda w: self.entryview.display_entry(self.entrystore.get_entry(self.tree.get_active()))) self.tree.selection.connect("changed", lambda w: self.__state_entry(self.tree.get_selected())) ##### STATE HANDLERS ##### def __save_state(self): "Saves the current application state" width, height = self.get_size() self.config.set("view/window-width", width) self.config.set("view/window-height", height) x, y = self.get_position() self.config.set("view/window-position-x", x) self.config.set("view/window-position-y", y) self.config.set("view/pane-position", self.hpaned.get_position()) def __state_clipboard(self, has_contents): "Sets states based on the clipboard contents" self.uimanager.get_action("clip-paste").set_property("sensitive", has_contents) def __state_entry(self, iters): "Sets states for entry-dependant ui items" # widget sensitivity based on number of entries self.uimanager.get_action_group("entry-multiple").set_sensitive(len(iters) > 0) self.uimanager.get_action_group("entry-single").set_sensitive(len(iters) == 1) self.uimanager.get_action_group("entry-optional").set_sensitive(len(iters) < 2) # copy password sensitivity s = False for iter in iters: e = self.entrystore.get_entry(iter) for f in e.fields: if f.datatype == entry.DATATYPE_PASSWORD and f.value != "": s = True self.uimanager.get_action("clip-chain").set_property("sensitive", s) # goto sensitivity try: for iter in iters: e = self.entrystore.get_entry(iter) if self.config.get("launcher/%s" % e.id) not in ( "", None ): s = True break else: s = False except config.ConfigError: s = False self.uimanager.get_action("entry-goto").set_sensitive(s) def __state_file(self, file): "Sets states based on file" self.uimanager.get_action_group("file-exists").set_sensitive(file is not None) if file is not None: self.set_title(os.path.basename(file)) if io.file_is_local(file): os.chdir(os.path.dirname(file)) else: self.set_title('[' + _('New file') + ']') def __state_find(self, string): "Sets states based on the current search string" self.uimanager.get_action_group("find").set_sensitive(string != "") def __state_undo(self, undoaction, redoaction): "Sets states based on undoqueue actions" if undoaction is None: s, l = False, _('_Undo') else: s, l = True, _('_Undo %s') % undoaction[1].lower() action = self.uimanager.get_action("undo") action.set_property("sensitive", s) action.set_property("label", l) if redoaction is None: s, l = False, _('_Redo') else: s, l = True, _('_Redo %s') % redoaction[1].lower() action = self.uimanager.get_action("redo") action.set_property("sensitive", s) action.set_property("label", l) ##### MISC CALLBACKS ##### def __cb_clip_copy(self, widget, data = None): "Handles copying to the clipboard" focuswidget = self.get_focus() if focuswidget is self.tree: self.clip_copy(self.tree.get_selected()) elif isinstance(focuswidget, gtk.Label) or isinstance(focuswidget, gtk.Entry): focuswidget.emit("copy-clipboard") def __cb_clip_cut(self, widget, data = None): "Handles cutting to clipboard" focuswidget = self.get_focus() if focuswidget is self.tree: self.clip_cut(self.tree.get_selected()) elif isinstance(focuswidget, gtk.Entry): focuswidget.emit("cut-clipboard") def __cb_clip_paste(self, widget, data = None): "Handles pasting from clipboard" focuswidget = self.get_focus() if focuswidget is self.tree: self.clip_paste(self.entryclipboard.get(), self.tree.get_active()) elif isinstance(focuswidget, gtk.Entry): focuswidget.emit("paste-clipboard") def __cb_drag_dest(self, widget, context, x, y, seldata, info, time, userdata = None): "Handles file drops" if seldata.data is None: return files = [ file.strip() for file in seldata.data.split("\n") if file.strip() != "" ] if len(files) > 0: self.file_open(files[0]) def __cb_event_filter(self, event): "Event filter for gdk window" self.locktimer.reset() return gtk.gdk.FILTER_CONTINUE def __cb_exception(self, type, value, trace): "Callback for unhandled exceptions" if type == KeyboardInterrupt: sys.exit(1) traceback = util.trace_exception(type, value, trace) sys.stderr.write(traceback) if dialog.Exception(self, traceback).run() == True: gtk.main() else: sys.exit(1) def __cb_file_content_changed(self, widget, file): "Callback for changed file" try: if dialog.FileChanged(self, file).run() == True: self.file_open(self.datafile.get_file(), self.datafile.get_password()) except dialog.CancelError: self.statusbar.set_status(_('Open cancelled')) def __cb_file_autolock(self, widget, data = None): "Callback for locking the file" if self.config.get("file/autolock") == True: self.file_lock() def __cb_quit(self, widget, data = None): "Callback for quit" if self.quit() == False: return True else: return False def __cb_searchbar_button_clicked(self, widget, direction = data.SEARCH_NEXT): "Callback for searchbar button clicks" self.__entry_find(self, self.searchbar.entry.get_text(), self.searchbar.dropdown.get_active_type(), direction) self.searchbar.entry.select_region(0, -1) def __cb_searchbar_key_press(self, widget, data): "Callback for searchbar key presses" # escape if data.keyval == 65307: self.config.set("view/searchbar", False) def __cb_session_save(self, widget, phase, what, end, interaction, fast, data = None): "Handles session saves" self.sessionclient.set_current_directory(os.getcwd()) self.sessionclient.set_clone_command(sys.argv[0]) if self.datafile.get_file() == None: self.sessionclient.set_restart_command(1, [ sys.argv[0] ]) else: self.sessionclient.set_restart_command(2, [ sys.argv[0], self.datafile.get_file() ] ) self.__save_state() return True def __cb_tree_doubleclick(self, widget, iter): "Handles doubleclicks on the tree" if self.config.get("behavior/doubleclick") == "edit": self.entry_edit(iter) elif self.config.get("behavior/doubleclick") == "copy": self.clip_chain(self.entrystore.get_entry(iter)) else: self.entry_goto((iter,)) def __cb_tree_drag_received(self, tree, context, x, y, seldata, info, time): "Callback for drag drops on the treeview" # get source and destination data sourceiters = self.entrystore.filter_parents(self.tree.get_selected()) destrow = self.tree.get_dest_row_at_pos(x, y) if destrow is None: destpath = ( self.entrystore.iter_n_children(None) - 1, ) pos = gtk.TREE_VIEW_DROP_AFTER else: destpath, pos = destrow destiter = self.entrystore.get_iter(destpath) destpath = self.entrystore.get_path(destiter) # avoid drops to current iter or descentants for sourceiter in sourceiters: sourcepath = self.entrystore.get_path(sourceiter) if self.entrystore.is_ancestor(sourceiter, destiter) == True or sourcepath == destpath: context.finish(False, False, long(time)) return elif pos == gtk.TREE_VIEW_DROP_BEFORE and sourcepath[:-1] == destpath[:-1] and sourcepath[-1] == destpath[-1] - 1: context.finish(False, False, long(time)) return elif pos == gtk.TREE_VIEW_DROP_AFTER and sourcepath[:-1] == destpath[:-1] and sourcepath[-1] == destpath[-1] + 1: context.finish(False, False, long(time)) return # move the entries if pos in ( gtk.TREE_VIEW_DROP_INTO_OR_BEFORE, gtk.TREE_VIEW_DROP_INTO_OR_AFTER): parent = destiter sibling = None elif pos == gtk.TREE_VIEW_DROP_BEFORE: parent = self.entrystore.iter_parent(destiter) sibling = destiter elif pos == gtk.TREE_VIEW_DROP_AFTER: parent = self.entrystore.iter_parent(destiter) sibpath = list(destpath) sibpath[-1] += 1 sibling = self.entrystore.get_iter(sibpath) self.entry_move(sourceiters, parent, sibling) context.finish(False, False, long(time)) def __cb_tree_keypress(self, widget, data = None): "Handles key presses for the tree" # return if data.keyval == 65293: self.entry_edit(self.tree.get_active()) # insert elif data.keyval == 65379: self.entry_add(None, self.tree.get_active()) # delete elif data.keyval == 65535: self.entry_remove(self.tree.get_selected()) ##### CONFIG CALLBACKS ##### def __cb_config_toolbar(self, config, value, toolbar): "Config callback for showing toolbars" if value == True: toolbar.show() else: toolbar.hide() def __cb_config_toolbar_style(self, config, value, data = None): "Config callback for setting toolbar style" if value == "both": self.toolbar.set_style(gtk.TOOLBAR_BOTH) elif value == "both-horiz": self.toolbar.set_style(gtk.TOOLBAR_BOTH_HORIZ) elif value == "icons": self.toolbar.set_style(gtk.TOOLBAR_ICONS) elif value == "text": self.toolbar.set_style(gtk.TOOLBAR_TEXT) else: self.toolbar.unset_style() #### UNDO / REDO CALLBACKS ##### def __cb_redo_add(self, name, actiondata): "Redoes an add action" path, e = actiondata parent = self.entrystore.get_iter(path[:-1]) sibling = self.entrystore.get_iter(path) iter = self.entrystore.add_entry(e, parent, sibling) self.tree.select(iter) def __cb_redo_edit(self, name, actiondata): "Redoes an edit action" path, preentry, postentry = actiondata iter = self.entrystore.get_iter(path) self.entrystore.update_entry(iter, postentry) self.tree.select(iter) def __cb_redo_import(self, name, actiondata): "Redoes an import action" paths, entrystore = actiondata self.entrystore.import_entry(entrystore, None) def __cb_redo_move(self, name, actiondata): "Redoes a move action" newiters = [] for prepath, postpath in actiondata: prepath, postpath = list(prepath), list(postpath) # adjust path if necessary if len(prepath) <= len(postpath): if prepath[:-1] == postpath[:len(prepath) - 1]: if prepath[-1] <= postpath[len(prepath) - 1]: postpath[len(prepath) - 1] += 1 newiter = self.entrystore.move_entry( self.entrystore.get_iter(prepath), self.entrystore.get_iter(postpath[:-1]), self.entrystore.get_iter(postpath) ) newiters.append(newiter) if len(newiters) > 0: self.tree.select(newiters[0]) def __cb_redo_paste(self, name, actiondata): "Redoes a paste action" entrystore, parentpath, paths = actiondata iters = self.entrystore.import_entry(entrystore, None, self.entrystore.get_iter(parentpath)) if len(iters) > 0: self.tree.select(iters[0]) def __cb_redo_remove(self, name, actiondata): "Redoes a remove action" iters = [] for path, entrystore in actiondata: iters.append(self.entrystore.get_iter(path)) for iter in iters: self.entrystore.remove_entry(iter) self.tree.unselect_all() def __cb_undo_add(self, name, actiondata): "Undoes an add action" path, e = actiondata self.entrystore.remove_entry(self.entrystore.get_iter(path)) self.tree.unselect_all() def __cb_undo_edit(self, name, actiondata): "Undoes an edit action" path, preentry, postentry = actiondata iter = self.entrystore.get_iter(path) self.entrystore.update_entry(iter, preentry) self.tree.select(iter) def __cb_undo_import(self, name, actiondata): "Undoes an import action" paths, entrystore = actiondata iters = [ self.entrystore.get_iter(path) for path in paths ] for iter in iters: self.entrystore.remove_entry(iter) self.tree.unselect_all() def __cb_undo_move(self, name, actiondata): "Undoes a move action" actiondata = actiondata[:] actiondata.reverse() newiters = [] for prepath, postpath in actiondata: prepath, postpath = list(prepath), list(postpath) # adjust path if necessary if len(postpath) <= len(prepath): if postpath[:-1] == prepath[:len(postpath) - 1]: if postpath[-1] <= prepath[len(postpath) - 1]: prepath[len(postpath) - 1] += 1 newiter = self.entrystore.move_entry( self.entrystore.get_iter(postpath), self.entrystore.get_iter(prepath[:-1]), self.entrystore.get_iter(prepath) ) newiters.append(newiter) if len(newiters) > 0: self.tree.select(newiters[-1]) def __cb_undo_paste(self, name, actiondata): "Undoes a paste action" entrystore, parentpath, paths = actiondata iters = [ self.entrystore.get_iter(path) for path in paths ] for iter in iters: self.entrystore.remove_entry(iter) self.tree.unselect_all() def __cb_undo_remove(self, name, actiondata): "Undoes a remove action" iters = [] for path, entrystore in actiondata: parent = self.entrystore.get_iter(path[:-1]) sibling = self.entrystore.get_iter(path) iter = self.entrystore.import_entry(entrystore, entrystore.iter_nth_child(None, 0), parent, sibling) iters.append(iter) self.tree.select(iters[0]) ##### PRIVATE METHODS ##### def __check_config(self): "Checks if the configuration is correct" try: self.config.get("launcher/website") self.config.get("view/searchbar") self.config.get("clipboard/chain_username") self.config.get("behavior/doubleclick") self.config.get("view/toolbar_style") return True except config.ConfigError: return False def __entry_find(self, parent, string, entrytype, direction = data.SEARCH_NEXT): "Searches for an entry" match = self.entrysearch.find(string, entrytype, self.tree.get_active(), direction) if match != None: self.tree.select(match) self.statusbar.set_status(_('Match found for \'%s\'') % string) else: self.statusbar.set_status(_('No match found for \'%s\'') % string) dialog.Error(parent, _('No match found'), _('The string \'%s\' does not match any entries. Try searching for a different phrase.') % string).run() def __file_autosave(self): "Autosaves the current file if needed" try: if self.datafile.get_file() is None or self.datafile.get_password() is None: return if self.config.get("file/autosave") == False: return self.datafile.save(self.entrystore, self.datafile.get_file(), self.datafile.get_password()) self.entrystore.changed = False except IOError: pass def __file_load(self, file, password, datafile = None): "Loads data from a data file into an entrystore" try: if datafile is None: datafile = self.datafile while 1: try: return datafile.load(file, password, lambda: dialog.PasswordOpen(self, os.path.basename(file)).run()) except datahandler.PasswordError: dialog.Error(self, _('Incorrect password'), _('The password you entered for the file \'%s\' was not correct.') % file).run() except datahandler.FormatError: self.statusbar.set_status(_('Open failed')) dialog.Error(self, _('Invalid file format'), _('The file \'%s\' contains invalid data.') % file).run() except ( datahandler.DataError, entry.EntryTypeError, entry.EntryFieldError ): self.statusbar.set_status(_('Open failed')) dialog.Error(self, _('Unknown data'), _('The file \'%s\' contains unknown data. It may have been created by a newer version of Revelation.') % file).run() except datahandler.VersionError: self.statusbar.set_status(_('Open failed')) dialog.Error(self, _('Unknown data version'), _('The file \'%s\' has a future version number, please upgrade Revelation to open it.') % file).run() except datahandler.DetectError: self.statusbar.set_status(_('Open failed')) dialog.Error(self, _('Unable to detect filetype'), _('The file type of the file \'%s\' could not be automatically detected. Try specifying the file type manually.')% file).run() except IOError: self.statusbar.set_status(_('Open failed')) dialog.Error(self, _('Unable to open file'), _('The file \'%s\' could not be opened. Make sure that the file exists, and that you have permissions to open it.') % file).run() def __get_common_usernames(self, e = None): "Returns a list of possibly relevant usernames" list = [] if e is not None and e.has_field(entry.UsernameField): list.append(e[entry.UsernameField]) list.append(pwd.getpwuid(os.getuid())[0]) list.extend(self.entrystore.get_popular_values(entry.UsernameField, 3)) list = {}.fromkeys(list).keys() list.sort() return list ##### PUBLIC METHODS ##### def about(self): "Displays the about dialog" dialog.run_unique(dialog.About, self) def clip_chain(self, e): "Copies all passwords from an entry as a chain" if e == None: return secrets = [ field.value for field in e.fields if field.datatype == entry.DATATYPE_PASSWORD and field.value != "" ] if self.config.get("clipboard/chain_username") == True and len(secrets) > 0 and e.has_field(entry.UsernameField) and e[entry.UsernameField] != "": secrets.insert(0, e[entry.UsernameField]) if len(secrets) == 0: self.statusbar.set_status(_('Entry has no password to copy')) else: self.clipboard.set(secrets, True) self.statusbar.set_status(_('Password copied to clipboard')) def clip_copy(self, iters): "Copies entries to the clipboard" self.entryclipboard.set(self.entrystore, iters) self.statusbar.set_status(_('Entries copied')) def clip_cut(self, iters): "Cuts entries to the clipboard" iters = self.entrystore.filter_parents(iters) self.entryclipboard.set(self.entrystore, iters) # store undo data (need paths) undoactions = [] for iter in iters: undostore = data.EntryStore() undostore.import_entry(self.entrystore, iter) path = self.entrystore.get_path(iter) undoactions.append( ( path, undostore ) ) # remove data for iter in iters: self.entrystore.remove_entry(iter) self.undoqueue.add_action( _('Cut entries'), self.__cb_undo_remove, self.__cb_redo_remove, undoactions ) self.__file_autosave() self.tree.unselect_all() self.statusbar.set_status(_('Entries cut')) def clip_paste(self, entrystore, parent): "Pastes entries from the clipboard" if entrystore == None: return parent = self.tree.get_active() iters = self.entrystore.import_entry(entrystore, None, parent) paths = [ self.entrystore.get_path(iter) for iter in iters ] self.undoqueue.add_action( _('Paste entries'), self.__cb_undo_paste, self.__cb_redo_paste, ( entrystore, self.entrystore.get_path(parent), paths ) ) if len(iters) > 0: self.tree.select(iters[0]) self.statusbar.set_status(_('Entries pasted')) def entry_add(self, e = None, parent = None, sibling = None): "Adds an entry" try: if e == None: d = dialog.EntryEdit(self, _('Add Entry'), None, self.config, self.clipboard) d.set_fieldwidget_data(entry.UsernameField, self.__get_common_usernames()) e = d.run() iter = self.entrystore.add_entry(e, parent, sibling) self.undoqueue.add_action( _('Add entry'), self.__cb_undo_add, self.__cb_redo_add, ( self.entrystore.get_path(iter), e.copy() ) ) self.__file_autosave() self.tree.select(iter) self.statusbar.set_status(_('Entry added')) except dialog.CancelError: self.statusbar.set_status(_('Add entry cancelled')) def entry_edit(self, iter): "Edits an entry" try: if iter == None: return e = self.entrystore.get_entry(iter) if type(e) == entry.FolderEntry: d = dialog.FolderEdit(self, _('Edit Folder'), e) else: d = dialog.EntryEdit(self, _('Edit Entry'), e, self.config, self.clipboard) d.set_fieldwidget_data(entry.UsernameField, self.__get_common_usernames(e)) n = d.run() self.entrystore.update_entry(iter, n) self.tree.select(iter) self.undoqueue.add_action( _('Update entry'), self.__cb_undo_edit, self.__cb_redo_edit, ( self.entrystore.get_path(iter), e.copy(), n.copy() ) ) self.__file_autosave() self.statusbar.set_status(_('Entry updated')) except dialog.CancelError: self.statusbar.set_status(_('Edit entry cancelled')) def entry_find(self): "Searches for an entry" self.config.set("view/searchbar", True) self.searchbar.entry.select_region(0, -1) self.searchbar.entry.grab_focus() def entry_folder(self, e = None, parent = None, sibling = None): "Adds a folder" try: if e == None: e = dialog.FolderEdit(self, _('Add Folder')).run() iter = self.entrystore.add_entry(e, parent, sibling) self.undoqueue.add_action( _('Add folder'), self.__cb_undo_add, self.__cb_redo_add, ( self.entrystore.get_path(iter), e.copy() ) ) self.__file_autosave() self.tree.select(iter) self.statusbar.set_status(_('Folder added')) except dialog.CancelError: self.statusbar.set_status(_('Add folder cancelled')) def entry_goto(self, iters): "Goes to an entry" for iter in iters: try: # get goto data for entry e = self.entrystore.get_entry(iter) command = self.config.get("launcher/%s" % e.id) if command in ( "", None ): self.statusbar.set_status(_('No goto command found for %s entries') % e.typename) return subst = {} for field in e.fields: subst[field.symbol] = field.value # copy passwords to clipboard chain = [] for field in e.fields: if field.datatype == entry.DATATYPE_PASSWORD and field.value != "": chain.append(field.value) if self.config.get("clipboard/chain_username") == True and len(chain) > 0 and e.has_field(entry.UsernameField) == True and e[entry.UsernameField] != "" and "%" + entry.UsernameField.symbol not in command: chain.insert(0, e[entry.UsernameField]) self.clipboard.set(chain, True) # generate and run goto command command = util.parse_subst(command, subst) util.execute_child(command) self.statusbar.set_status(_('Entry opened')) except ( util.SubstFormatError, config.ConfigError ): dialog.Error(self, _('Invalid goto command format'), _('The goto command for \'%s\' entries is invalid, please correct it in the preferences.') % e.typename).run() except util.SubstValueError: dialog.Error(self, _('Missing entry data'), _('The entry \'%s\' does not have all the data required to open it.') % e.name).run() def entry_move(self, sourceiters, parent = None, sibling = None): "Moves a set of entries" if type(sourceiters) != list: sourceiters = [ sourceiters ] newiters = [] undoactions = [] for sourceiter in sourceiters: sourcepath = self.entrystore.get_path(sourceiter) newiter = self.entrystore.move_entry(sourceiter, parent, sibling) newpath = self.entrystore.get_path(newiter) undoactions.append( ( sourcepath, newpath ) ) newiters.append(newiter) self.undoqueue.add_action( _('Move entry'), self.__cb_undo_move, self.__cb_redo_move, undoactions ) if len(newiters) > 0: self.tree.select(newiters[0]) self.__file_autosave() self.statusbar.set_status(_('Entries moved')) def entry_remove(self, iters): "Removes the selected entries" try: if len(iters) == 0: return entries = [ self.entrystore.get_entry(iter) for iter in iters ] dialog.EntryRemove(self, entries).run() iters = self.entrystore.filter_parents(iters) # store undo data (need paths) undoactions = [] for iter in iters: undostore = data.EntryStore() undostore.import_entry(self.entrystore, iter) path = self.entrystore.get_path(iter) undoactions.append( ( path, undostore ) ) # remove data for iter in iters: self.entrystore.remove_entry(iter) self.undoqueue.add_action( _('Remove entry'), self.__cb_undo_remove, self.__cb_redo_remove, undoactions ) self.tree.unselect_all() self.__file_autosave() self.statusbar.set_status(_('Entries removed')) except dialog.CancelError: self.statusbar.set_status(_('Entry removal cancelled')) def file_change_password(self, password = None): "Changes the password of the current data file" try: if password == None: password = dialog.PasswordChange(self, self.datafile.get_password()).run() self.datafile.set_password(password) self.entrystore.changed = True self.__file_autosave() self.statusbar.set_status(_('Password changed')) except dialog.CancelError: self.statusbar.set_status(_('Password change cancelled')) def file_export(self): "Exports data to a foreign file format" try: file, handler = dialog.ExportFileSelector(self).run() datafile = io.DataFile(handler) if datafile.get_handler().encryption == True: password = dialog.PasswordSave(self, file).run() else: dialog.FileSaveInsecure(self).run() password = None datafile.save(self.entrystore, file, password) self.statusbar.set_status(_('Data exported to %s') % datafile.get_file()) except dialog.CancelError: self.statusbar.set_status(_('Export cancelled')) except IOError: dialog.Error(self, _('Unable to write to file'), _('The file \'%s\' could not be opened for writing. Make sure that you have the proper permissions to write to it.') % file).run() self.statusbar.set_status(_('Export failed')) def file_import(self): "Imports data from a foreign file" try: file, handler = dialog.ImportFileSelector(self).run() datafile = io.DataFile(handler) entrystore = self.__file_load(file, None, datafile) if entrystore is not None: newiters = self.entrystore.import_entry(entrystore, None) paths = [ self.entrystore.get_path(iter) for iter in newiters ] self.undoqueue.add_action( _('Import data'), self.__cb_undo_import, self.__cb_redo_import, ( paths, entrystore ) ) self.statusbar.set_status(_('Data imported from %s') % datafile.get_file()) self.__file_autosave() except dialog.CancelError: self.statusbar.set_status(_('Import cancelled')) def file_lock(self): "Locks the current file" password = self.datafile.get_password() if password is None: return self.locktimer.stop() # TODO can this be done more elegantly? transients = [ window for window in gtk.window_list_toplevels() if window.get_transient_for() == self ] # store current state activeiter = self.tree.get_active() oldtitle = self.get_title() # clear application contents self.tree.set_model(None) self.entryview.clear() self.set_title('[' + _('Locked') + ']') self.statusbar.set_status(_('File locked')) # hide any dialogs for window in transients: window.hide() # lock file try: d = dialog.PasswordLock(self, password) if self.entrystore.changed == True: l = ui.ImageLabel(_('Quit disabled due to unsaved changes'), ui.STOCK_WARNING) d.contents.pack_start(l) d.get_button(1).set_sensitive(False) d.run() except dialog.CancelError: self.quit() # unlock the file and restore state self.tree.set_model(self.entrystore) self.tree.select(activeiter) self.set_title(oldtitle) self.statusbar.set_status(_('File unlocked')) for window in transients: window.show() self.locktimer.start(self.config.get("file/autolock_timeout") * 60) def file_new(self): "Opens a new file" try: if self.entrystore.changed == True and dialog.FileChangesNew(self).run() == True: if self.file_save(self.datafile.get_file(), self.datafile.get_password()) == False: raise dialog.CancelError self.entrystore.clear() self.datafile.close() self.undoqueue.clear() self.statusbar.set_status(_('New file created')) except dialog.CancelError: self.statusbar.set_status(_('New file cancelled')) def file_open(self, file = None, password = None): "Opens a data file" try: if self.entrystore.changed == True and dialog.FileChangesOpen(self).run() == True: if self.file_save(self.datafile.get_file(), self.datafile.get_password()) == False: raise dialog.CancelError if file is None: file = dialog.OpenFileSelector(self).run() entrystore = self.__file_load(file, password) if entrystore is None: return self.entrystore.clear() self.entrystore.import_entry(entrystore, None) self.entrystore.changed = False self.undoqueue.clear() self.statusbar.set_status(_('Opened file %s') % self.datafile.get_file()) except dialog.CancelError: self.statusbar.set_status(_('Open cancelled')) def file_save(self, file = None, password = None): "Saves data to a file" try: if file is None: file = dialog.SaveFileSelector(self).run() if password == None: password = dialog.PasswordSave(self, file).run() self.datafile.save(self.entrystore, file, password) self.entrystore.changed = False self.statusbar.set_status(_('Data saved to file %s') % file) return True except dialog.CancelError: self.statusbar.set_status(_('Save cancelled')) return False except IOError: dialog.Error(self, _('Unable to save file'), _('The file \'%s\' could not be opened for writing. Make sure that you have the proper permissions to write to it.') % file).run() self.statusbar.set_status(_('Save failed')) return False def prefs(self): "Displays the application preferences" dialog.run_unique(Preferences, self, self.config) def pwcheck(self): "Displays the password checking dialog" dialog.run_unique(dialog.PasswordChecker, self, self.config, self.clipboard) def pwgen(self): "Displays the password generator dialog" dialog.run_unique(dialog.PasswordGenerator, self, self.config, self.clipboard) def quit(self): "Quits the application" try: if self.entrystore.changed == True and dialog.FileChangesQuit(self).run() == True: if self.file_save(self.datafile.get_file(), self.datafile.get_password()) == False: raise dialog.CancelError self.clipboard.clear() self.entryclipboard.clear() self.__save_state() gtk.main_quit() sys.exit(0) return True except dialog.CancelError: self.statusbar.set_status(_('Quit cancelled')) return False def redo(self): "Redoes the previous action" action = self.undoqueue.get_redo_action() if action is None: return self.undoqueue.redo() self.statusbar.set_status(_('%s redone') % action[1]) self.__file_autosave() def run(self): "Runs the application" args, argdict = self.program.get_popt_args() if len(args) > 0: file = args[0] elif self.config.get("file/autoload") == True: file = self.config.get("file/autoload_file") else: file = "" if file != "": self.file_open(io.file_normpath(file)) gtk.main() def undo(self): "Undoes the previous action" action = self.undoqueue.get_undo_action() if action is None: return self.undoqueue.undo() self.statusbar.set_status(_('%s undone') % action[1]) self.__file_autosave() class Preferences(dialog.Utility): "A preference dialog" def __init__(self, parent, cfg): dialog.Utility.__init__(self, parent, _('Preferences')) self.config = cfg self.set_modal(False) self.notebook = ui.Notebook() self.vbox.pack_start(self.notebook) self.page_general = self.notebook.create_page(_('General')) self.__init_section_files(self.page_general) self.__init_section_password(self.page_general) self.page_interface = self.notebook.create_page(_('Interface')) self.__init_section_doubleclick(self.page_interface) self.__init_section_toolbar(self.page_interface) self.page_gotocmd = self.notebook.create_page(_('Goto Commands')) self.__init_section_gotocmd(self.page_gotocmd) self.connect("response", lambda w,d: self.destroy()) def __init_section_doubleclick(self, page): "Sets up the doubleclick section" self.section_doubleclick = page.add_section(_('Doubleclick Action')) # radio-button for go to self.radio_doubleclick_goto = ui.RadioButton(None, _('Go to account, if possible')) ui.config_bind(self.config, "behavior/doubleclick", self.radio_doubleclick_goto, "goto") self.tooltips.set_tip(self.radio_doubleclick_goto, _('Go to the account (open in external application) on doubleclick, if required data is filled in')) self.section_doubleclick.append_widget(None, self.radio_doubleclick_goto) # radio-button for edit self.radio_doubleclick_edit = ui.RadioButton(self.radio_doubleclick_goto, _('Edit account')) ui.config_bind(self.config, "behavior/doubleclick", self.radio_doubleclick_edit, "edit") self.tooltips.set_tip(self.radio_doubleclick_edit, _('Edit the account on doubleclick')) self.section_doubleclick.append_widget(None, self.radio_doubleclick_edit) # radio-button for copy self.radio_doubleclick_copy = ui.RadioButton(self.radio_doubleclick_goto, _('Copy password to clipboard')) ui.config_bind(self.config, "behavior/doubleclick", self.radio_doubleclick_copy, "copy") self.tooltips.set_tip(self.radio_doubleclick_copy, _('Copy the account password to clipboard on doubleclick')) self.section_doubleclick.append_widget(None, self.radio_doubleclick_copy) def __init_section_files(self, page): "Sets up the files section" self.section_files = page.add_section(_('Files')) # checkbutton and file button for autoloading a file self.check_autoload = ui.CheckButton(_('Open file on startup:')) ui.config_bind(self.config, "file/autoload", self.check_autoload) self.check_autoload.connect("toggled", lambda w: self.button_autoload_file.set_sensitive(w.get_active())) self.tooltips.set_tip(self.check_autoload, _('When enabled, this file will be opened when the program is started')) self.button_autoload_file = ui.FileButton(_('Select File to Automatically Open')) ui.config_bind(self.config, "file/autoload_file", self.button_autoload_file) self.button_autoload_file.set_sensitive(self.check_autoload.get_active()) eventbox = ui.EventBox(self.button_autoload_file) self.tooltips.set_tip(eventbox, _('File to open when Revelation is started')) hbox = ui.HBox() hbox.pack_start(self.check_autoload, False, False) hbox.pack_start(eventbox) self.section_files.append_widget(None, hbox) # check-button for autosave self.check_autosave = ui.CheckButton(_('Automatically save data when changed')) ui.config_bind(self.config, "file/autosave", self.check_autosave) self.tooltips.set_tip(self.check_autosave, _('Automatically save the data file when an entry is added, modified or removed')) self.section_files.append_widget(None, self.check_autosave) # autolock file self.check_autolock = ui.CheckButton(_('Lock file when inactive for')) ui.config_bind(self.config, "file/autolock", self.check_autolock) self.check_autolock.connect("toggled", lambda w: self.spin_autolock_timeout.set_sensitive(w.get_active())) self.tooltips.set_tip(self.check_autolock, _('Automatically lock the data file after a period of inactivity')) self.spin_autolock_timeout = ui.SpinEntry() self.spin_autolock_timeout.set_range(1, 120) self.spin_autolock_timeout.set_sensitive(self.check_autolock.get_active()) ui.config_bind(self.config, "file/autolock_timeout", self.spin_autolock_timeout) self.tooltips.set_tip(self.spin_autolock_timeout, _('The period of inactivity before locking the file, in minutes')) hbox = ui.HBox() hbox.set_spacing(3) hbox.pack_start(self.check_autolock, False, False) hbox.pack_start(self.spin_autolock_timeout, False, False) hbox.pack_start(ui.Label(_('minutes'))) self.section_files.append_widget(None, hbox) def __init_section_gotocmd(self, page): "Sets up the goto command section" self.section_gotocmd = page.add_section(_('Goto Commands')) for entrytype in entry.ENTRYLIST: if entrytype == entry.FolderEntry: continue e = entrytype() widget = ui.Entry() ui.config_bind(self.config, "launcher/%s" % e.id, widget) tooltip = _('Goto command for %s accounts. The following expansion variables can be used:') % e.typename + "\n\n" for field in e.fields: tooltip += "%%%s: %s\n" % ( field.symbol, field.name ) tooltip += "\n" tooltip += _('%%: a % sign') + "\n" tooltip += _('%?x: optional expansion variable') + "\n" tooltip += _('%(...%): optional substring expansion') self.tooltips.set_tip(widget, tooltip) self.section_gotocmd.append_widget(e.typename, widget) def __init_section_password(self, page): "Sets up the password section" self.section_password = page.add_section(_('Passwords')) # show passwords checkbutton self.check_show_passwords = ui.CheckButton(_('Display passwords and other secrets')) ui.config_bind(self.config, "view/passwords", self.check_show_passwords) self.tooltips.set_tip(self.check_show_passwords, _('Display passwords and other secrets, such as PIN codes (otherwise, hide with ******)')) self.section_password.append_widget(None, self.check_show_passwords) # chain username checkbutton self.check_chain_username = ui.CheckButton(_('Also copy username when copying password')) ui.config_bind(self.config, "clipboard/chain_username", self.check_chain_username) self.tooltips.set_tip(self.check_chain_username, _('When the password is copied to clipboard, put the username before the password as a clipboard "chain"')) self.section_password.append_widget(None, self.check_chain_username) # password length spinbutton self.spin_pwlen = ui.SpinEntry() self.spin_pwlen.set_range(4, 32) ui.config_bind(self.config, "passwordgen/length", self.spin_pwlen) self.tooltips.set_tip(self.spin_pwlen, _('The number of characters in generated passwords - 8 or more are recommended')) self.section_password.append_widget(_('Length of generated passwords'), self.spin_pwlen) def __init_section_toolbar(self, page): "Sets up the toolbar section" self.section_toolbar = page.add_section(_('Toolbar Style')) # radio-button for desktop default self.radio_toolbar_desktop = ui.RadioButton(None, _('Use desktop default')) ui.config_bind(self.config, "view/toolbar_style", self.radio_toolbar_desktop, "desktop") self.tooltips.set_tip(self.radio_toolbar_desktop, _('Show toolbar items with default style')) self.section_toolbar.append_widget(None, self.radio_toolbar_desktop) # radio-button for icons and text self.radio_toolbar_both = ui.RadioButton(self.radio_toolbar_desktop, _('Show icons and text')) ui.config_bind(self.config, "view/toolbar_style", self.radio_toolbar_both, "both") self.tooltips.set_tip(self.radio_toolbar_both, _('Show toolbar items with both icons and text')) self.section_toolbar.append_widget(None, self.radio_toolbar_both) # radio-button for icons and important text self.radio_toolbar_bothhoriz = ui.RadioButton(self.radio_toolbar_desktop, _('Show icons and important text')) ui.config_bind(self.config, "view/toolbar_style", self.radio_toolbar_bothhoriz, "both-horiz") self.tooltips.set_tip(self.radio_toolbar_bothhoriz, _('Show toolbar items with text beside important icons')) self.section_toolbar.append_widget(None, self.radio_toolbar_bothhoriz) # radio-button for icons only self.radio_toolbar_icons = ui.RadioButton(self.radio_toolbar_desktop, _('Show icons only')) ui.config_bind(self.config, "view/toolbar_style", self.radio_toolbar_icons, "icons") self.tooltips.set_tip(self.radio_toolbar_icons, _('Show toolbar items with icons only')) self.section_toolbar.append_widget(None, self.radio_toolbar_icons) # radio-button for text only self.radio_toolbar_text = ui.RadioButton(self.radio_toolbar_desktop, _('Show text only')) ui.config_bind(self.config, "view/toolbar_style", self.radio_toolbar_text, "text") self.tooltips.set_tip(self.radio_toolbar_text, _('Show toolbar items with text only')) self.section_toolbar.append_widget(None, self.radio_toolbar_text) def run(self): "Runs the preference dialog" self.show_all() if dialog.EVENT_FILTER != None: self.window.add_filter(dialog.EVENT_FILTER) # for some reason, gtk crashes on close-by-escape unless we do this self.get_button(0).grab_focus() self.notebook.grab_focus() if __name__ == "__main__": app = Revelation() app.run()