"Fossies" - the Fresh Open Source Software Archive

Member "revelation-0.5.4/src/revelation.py" (4 Oct 2020, 76910 Bytes) of package /linux/privat/revelation-0.5.4.tar.xz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "revelation.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 0.5.3_vs_0.5.4.

    1 #!/usr/bin/python3
    2 
    3 #
    4 # Revelation - a password manager for GNOME 2
    5 # http://oss.codepoet.no/revelation/
    6 # $Id$
    7 #
    8 # Copyright (c) 2003-2006 Erik Grinaker
    9 #
   10 # This program is free software; you can redistribute it and/or
   11 # modify it under the terms of the GNU General Public License
   12 # as published by the Free Software Foundation; either version 2
   13 # of the License, or (at your option) any later version.
   14 #
   15 # This program is distributed in the hope that it will be useful,
   16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
   17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   18 # GNU General Public License for more details.
   19 #
   20 # You should have received a copy of the GNU General Public License
   21 # along with this program; if not, write to the Free Software
   22 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
   23 #
   24 
   25 import gi
   26 gi.require_version('Gtk', '3.0')
   27 from gi.repository import Gtk, Gdk, Gio, GLib
   28 import gettext, os, pwd, sys, urllib.parse, locale
   29 
   30 from revelation import config, data, datahandler, dialog, entry, io, ui, util
   31 
   32 _ = gettext.gettext
   33 
   34 class Revelation(ui.App):
   35     "The Revelation application"
   36 
   37     def __init__(self):
   38         sys.excepthook = self.__cb_exception
   39         os.umask(0o077)
   40 
   41         gettext.bindtextdomain(config.PACKAGE, config.DIR_LOCALE)
   42         gettext.textdomain(config.PACKAGE)
   43 
   44         # Gtk.Builder uses the C lib's locale API, accessible with the locale module
   45         locale.bindtextdomain(config.PACKAGE, config.DIR_LOCALE)
   46         locale.bind_textdomain_codeset(config.PACKAGE, "UTF-8")
   47 
   48         ui.App.__init__(self, config.APPNAME)
   49         self.window = None
   50 
   51         resource_path = os.path.join(config.DIR_UI, 'revelation.gresource')
   52         resource = Gio.Resource.load(resource_path)
   53         resource._register()
   54 
   55     def do_startup(self):
   56         Gtk.Application.do_startup(self)
   57         if not self.window:
   58             self.window = ui.AppWindow(application=self, title="Revelation")
   59             self.main_vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL,5)
   60             self.window.add(self.main_vbox)
   61         self.add_window(self.window)
   62 
   63     def do_activate(self):
   64 
   65         self.builder = Gtk.Builder()
   66         self.builder.add_from_resource('/info/olasagasti/revelation/ui/menubar.ui')
   67         self.menubar = self.builder.get_object("menubar")
   68 
   69         self.popupbuilder = Gtk.Builder()
   70         self.popupbuilder.add_from_resource('/info/olasagasti/revelation/ui/popup-tree.ui')
   71         self.popupmenu = self.popupbuilder.get_object("popup-tree")
   72 
   73         self.window.connect("delete-event", self.__cb_quit)
   74 
   75         try:
   76             self.__init_config()
   77             self.__init_actions()
   78             self.__init_facilities()
   79             self.__init_ui()
   80             self.__init_states()
   81             self.__init_dbus()
   82 
   83         except IOError:
   84             dialog.Error(self.window, _('Missing data files'), _('Some of Revelations system files could not be found, please reinstall Revelation.')).run()
   85             sys.exit(1)
   86 
   87         except config.ConfigError:
   88             dialog.Error(self.window, _('Missing configuration data'), _('Revelation could not find its configuration data, please reinstall Revelation.')).run()
   89             sys.exit(1)
   90 
   91         except ui.DataError:
   92             dialog.Error(self.window, _('Invalid data files'), _('Some of Revelations system files contain invalid data, please reinstall Revelation.')).run()
   93             sys.exit(1)
   94 
   95 
   96         if len(sys.argv) > 1:
   97             file = sys.argv[1]
   98 
   99         elif self.config.get_boolean("file-autoload") == True:
  100             file = self.config.get_string("file-autoload-file")
  101 
  102         else:
  103             file = ""
  104 
  105         if file != "":
  106             self.file_open(io.file_normpath(urllib.parse.unquote(file)))
  107 
  108 
  109     def __init_config(self):
  110         "Get configuration schema"
  111 
  112         schema_source = Gio.SettingsSchemaSource.get_default()
  113         rvl_schema = schema_source.lookup('org.revelation', recursive=True)
  114 
  115         if not rvl_schema:
  116             schema_source = Gio.SettingsSchemaSource.new_from_directory(config.DIR_GSCHEMAS, schema_source, False)
  117             rvl_schema=schema_source.lookup('org.revelation', recursive=True)
  118 
  119         if not rvl_schema:
  120             raise config.ConfigError
  121 
  122         rvl_settings = Gio.Settings.new_full(rvl_schema, None, None)
  123         self.config = rvl_settings
  124 
  125 
  126     def __init_actions(self):
  127         "Sets up actions"
  128 
  129         # set up placeholders
  130         group = Gio.SimpleActionGroup()
  131         self.window.insert_action_group("placeholder", group)
  132 
  133         action_menu_edit  = Gio.SimpleAction.new("menu-edit",  None)
  134         action_menu_entry = Gio.SimpleAction.new("menu-entry", None)
  135         action_menu_file  = Gio.SimpleAction.new("menu-file",  None)
  136         action_menu_help  = Gio.SimpleAction.new("menu-help",  None)
  137         action_menu_view  = Gio.SimpleAction.new("menu-view",  None)
  138         action_popup_tree = Gio.SimpleAction.new("popup-tree", None)
  139         group.add_action(action_menu_edit)
  140         group.add_action(action_menu_entry)
  141         group.add_action(action_menu_file)
  142         group.add_action(action_menu_help)
  143         group.add_action(action_menu_view)
  144         group.add_action(action_popup_tree)
  145 
  146         # set up dynamic actions
  147         group = Gio.SimpleActionGroup()
  148         self.window.insert_action_group("dynamic", group)
  149 
  150         action = Gio.SimpleAction.new("clip-paste", None)
  151         action.connect("activate",      self.__cb_clip_paste)
  152         group.add_action(action)
  153 
  154         action = Gio.SimpleAction.new("entry-goto", None)
  155         action.connect("activate",      lambda w,k: self.entry_goto(self.tree.get_selected()))
  156         group.add_action(action)
  157 
  158         action = Gio.SimpleAction.new("redo", None)
  159         action.connect("activate",      lambda w,k: self.redo())
  160         group.add_action(action)
  161 
  162         action = Gio.SimpleAction.new("undo", None)
  163         action.connect("activate",      lambda w,k: self.undo())
  164         group.add_action(action)
  165 
  166         # set up group for multiple entries
  167         group = Gio.SimpleActionGroup()
  168         self.window.insert_action_group("entry-multiple", group)
  169 
  170         action = Gio.SimpleAction.new("clip-copy", None)
  171         action.connect("activate",      self.__cb_clip_copy)
  172         group.add_action(action)
  173 
  174         action = Gio.SimpleAction.new("clip-chain", None)
  175         action.connect("activate",      lambda w,k: self.clip_chain(self.entrystore.get_entry(self.tree.get_active())))
  176         group.add_action(action)
  177 
  178         action = Gio.SimpleAction.new("clip-cut", None)
  179         action.connect("activate",      self.__cb_clip_cut)
  180         group.add_action(action)
  181 
  182         action = Gio.SimpleAction.new("entry-remove", None)
  183         action.connect("activate",      lambda w,k: self.entry_remove(self.tree.get_selected()))
  184         group.add_action(action)
  185 
  186         # action group for "optional" entries
  187         group = Gio.SimpleActionGroup()
  188         self.window.insert_action_group("entry-optional", group)
  189 
  190         action = Gio.SimpleAction.new("entry-add", None)
  191         action.connect("activate",      lambda w,k: self.entry_add(None, self.tree.get_active()))
  192         group.add_action(action)
  193 
  194         action = Gio.SimpleAction.new("entry-folder", None)
  195         action.connect("activate",      lambda w,k: self.entry_folder(None, self.tree.get_active()))
  196         group.add_action(action)
  197 
  198         # action group for single entries
  199         group = Gio.SimpleActionGroup()
  200         self.window.insert_action_group("entry-single", group)
  201 
  202         action = Gio.SimpleAction.new("entry-edit", None)
  203         action.connect("activate",      lambda w,k: self.entry_edit(self.tree.get_active()))
  204         group.add_action(action)
  205 
  206         # action group for existing file
  207         group = Gio.SimpleActionGroup()
  208         self.window.insert_action_group("file-exists", group)
  209 
  210         action = Gio.SimpleAction.new("file-lock", None)
  211         action.connect("activate",      lambda w,k: self.file_lock())
  212         group.add_action(action)
  213 
  214         # action group for searching
  215         group = Gio.SimpleActionGroup()
  216         self.window.insert_action_group("find", group)
  217 
  218         action = Gio.SimpleAction.new("find-next", None)
  219         action.connect("activate",      lambda w,k: self.__entry_find(self, self.searchbar.entry.get_text(), self.searchbar.dropdown.get_active_type(), data.SEARCH_NEXT))
  220         group.add_action(action)
  221 
  222         action = Gio.SimpleAction.new("find-previous", None)
  223         action.connect("activate",      lambda w,k: self.__entry_find(self, self.searchbar.entry.get_text(), self.searchbar.dropdown.get_active_type(), data.SEARCH_PREVIOUS))
  224         group.add_action(action)
  225 
  226         # global action group
  227         group = Gio.SimpleActionGroup()
  228         self.window.insert_action_group("file", group)
  229 
  230         action = Gio.SimpleAction.new("file-change-password", None)
  231         action.connect("activate",      lambda w,k: self.file_change_password())
  232         group.add_action(action)
  233 
  234         action = Gio.SimpleAction.new("file-close", None)
  235         action.connect("activate",      self.__cb_close)
  236         group.add_action(action)
  237 
  238         action = Gio.SimpleAction.new("file-export", None)
  239         action.connect("activate",      lambda w,k: self.file_export())
  240         group.add_action(action)
  241 
  242         action = Gio.SimpleAction.new("file-import", None)
  243         action.connect("activate",      lambda w,k: self.file_import())
  244         group.add_action(action)
  245 
  246         action = Gio.SimpleAction.new("file-new", None)
  247         action.connect("activate",      lambda w,k: self.file_new())
  248         group.add_action(action)
  249 
  250         action = Gio.SimpleAction.new("file-open", None)
  251         action.connect("activate",      lambda w,k: self.file_open())
  252         group.add_action(action)
  253 
  254         action = Gio.SimpleAction.new("file-save", None)
  255         action.connect("activate",      lambda w,k: self.file_save(self.datafile.get_file(), self.datafile.get_password()))
  256         group.add_action(action)
  257 
  258         action = Gio.SimpleAction.new("file-save-as", None)
  259         action.connect("activate",      lambda w,k: self.file_save(None, None))
  260         group.add_action(action)
  261 
  262         action = Gio.SimpleAction.new("find", None)
  263         action.connect("activate",      lambda w,k: self.entry_find())
  264         group.add_action(action)
  265 
  266         action = Gio.SimpleAction.new("help-about", None)
  267         action.connect("activate",      lambda w,k: self.about())
  268         group.add_action(action)
  269 
  270         action = Gio.SimpleAction.new("prefs", None)
  271         action.connect("activate",      lambda w,k: self.prefs())
  272         group.add_action(action)
  273 
  274         action = Gio.SimpleAction.new("pwchecker", None)
  275         action.connect("activate",      lambda w,k: self.pwcheck())
  276         group.add_action(action)
  277 
  278         action = Gio.SimpleAction.new("pwgenerator", None)
  279         action.connect("activate",      lambda w,k: self.pwgen())
  280         group.add_action(action)
  281 
  282         action = Gio.SimpleAction.new("quit", None)
  283         action.connect("activate",      self.__cb_quit)
  284         group.add_action(action)
  285 
  286         action = Gio.SimpleAction.new("select-all", None)
  287         action.connect("activate",      lambda w,k: self.tree.select_all())
  288         group.add_action(action)
  289 
  290         action = Gio.SimpleAction.new("select-none", None)
  291         action.connect("activate",      lambda w,k: self.tree.unselect_all())
  292         group.add_action(action)
  293 
  294         action_vp = Gio.SimpleAction.new_stateful("view-passwords", None, GLib.Variant.new_boolean(self.config.get_boolean("view-passwords")))
  295         action_vp.connect("activate", lambda w, k: action_vp.set_state(GLib.Variant.new_boolean(not action_vp.get_state())))
  296         action_vp.connect("activate", lambda w, k: self.config.set_boolean("view-passwords", action_vp.get_state()))
  297         self.config.connect("changed::view-passwords", lambda w, k: action_vp.set_state(GLib.Variant.new_boolean(w.get_boolean(k))))
  298         group.add_action(action_vp)
  299 
  300         action_vs = Gio.SimpleAction.new_stateful("view-searchbar", None, GLib.Variant.new_boolean(True))
  301         action_vs.connect("activate", lambda w, k: action_vs.set_state(GLib.Variant.new_boolean(not action_vs.get_state())))
  302         action_vs.connect("activate", lambda w, k: self.config.set_boolean("view-searchbar", action_vs.get_state()))
  303         action_vs.connect("activate", lambda w, k: self.searchbar.set_visible(GLib.Variant.new_boolean(action_vs.get_state())))
  304         self.config.connect("changed::view-searchbar", lambda w, k: action_vs.set_state(GLib.Variant.new_boolean(w.get_boolean(k))))
  305         self.config.connect("changed::view-searchbar", lambda w, k: self.searchbar.set_visible(GLib.Variant.new_boolean(w.get_boolean(k))))
  306         group.add_action(action_vs)
  307 
  308         action_va = Gio.SimpleAction.new_stateful("view-statusbar", None, GLib.Variant.new_boolean(True))
  309         action_va.connect("activate", lambda w, k: action_va.set_state(GLib.Variant.new_boolean(not action_va.get_state())))
  310         action_va.connect("activate", lambda w, k: self.config.set_boolean("view-statusbar", action_va.get_state()))
  311         action_va.connect("activate", lambda w, k: self.statusbar.set_visible(GLib.Variant.new_boolean(action_va.get_state())))
  312         self.config.connect("changed::view-statusbar", lambda w, k: action_va.set_state(GLib.Variant.new_boolean(w.get_boolean(k))))
  313         self.config.connect("changed::view-statusbar", lambda w, k: self.statusbar.set_visible(GLib.Variant.new_boolean(w.get_boolean(k))))
  314         group.add_action(action_va)
  315 
  316         action_vt = Gio.SimpleAction.new_stateful("view-toolbar", None, GLib.Variant.new_boolean(True))
  317         action_vt.connect("activate", lambda w, k: action_vt.set_state(GLib.Variant.new_boolean(not action_vt.get_state())))
  318         action_vt.connect("activate", lambda w, k: self.config.set_boolean("view-toolbar", action_vt.get_state()))
  319         action_vt.connect("activate", lambda w, k: self.toolbar.set_visible(GLib.Variant.new_boolean(action_vt.get_state())))
  320         self.config.connect("changed::view-toolbar", lambda w, k: action_vt.set_state(GLib.Variant.new_boolean(w.get_boolean(k))))
  321         self.config.connect("changed::view-toolbar", lambda w, k: self.toolbar.set_visible(GLib.Variant.new_boolean(w.get_boolean(k))))
  322         self.config.connect("changed::view-toolbar-style", lambda w, k: self.__cb_config_toolbar_style(w, w.get_string(k)))
  323         group.add_action(action_vt)
  324 
  325 
  326     def __init_facilities(self):
  327         "Sets up various facilities"
  328 
  329         self.clipboard      = data.Clipboard()
  330         self.datafile       = io.DataFile(datahandler.Revelation2)
  331         self.entryclipboard = data.EntryClipboard()
  332         self.entrystore     = data.EntryStore()
  333         self.entrysearch    = data.EntrySearch(self.entrystore)
  334         self.items          = Gtk.IconTheme.get_default()
  335         self.locktimer      = data.Timer()
  336         self.undoqueue      = data.UndoQueue()
  337 
  338         self.datafile.connect("changed", lambda w,f: self.__state_file(f))
  339         self.datafile.connect("content-changed", self.__cb_file_content_changed)
  340         self.entryclipboard.connect("content-toggled", lambda w,d: self.__state_clipboard(d))
  341         self.locktimer.connect("ring", self.__cb_file_autolock)
  342         self.undoqueue.connect("changed", lambda w: self.__state_undo(self.undoqueue.get_undo_action(), self.undoqueue.get_redo_action()))
  343 
  344         self.config.connect("changed::file-autolock-timeout", lambda w, k: self.locktimer.start(60 * w.get_int(k)))
  345         if self.config.get_boolean("file-autolock"):
  346             self.locktimer.start(60 * self.config.get_int("file-autolock-timeout"))
  347 
  348         dialog.EVENT_FILTER = self.__cb_event_filter
  349 
  350 
  351     def __init_states(self):
  352         "Sets the initial application state"
  353 
  354         # set window states
  355         self.window.set_default_size(
  356             self.config.get_int("view-window-width"),
  357             self.config.get_int("view-window-height")
  358         )
  359 
  360         self.window.move(
  361             self.config.get_int("view-window-position-x"),
  362             self.config.get_int("view-window-position-y")
  363         )
  364 
  365         self.hpaned.set_position(
  366             self.config.get_int("view-pane-position")
  367         )
  368 
  369         # bind ui widgets to config keys
  370         bind = {
  371             "view-passwords"    : "/menubar/menu-view/view-passwords",
  372             "view-searchbar"    : "/menubar/menu-view/view-searchbar",
  373             "view-statusbar"    : "/menubar/menu-view/view-statusbar",
  374             "view-toolbar"      : "/menubar/menu-view/view-toolbar"
  375         }
  376 
  377         for key in bind.keys():
  378             self.window.get_action_group("file").lookup_action(key).set_state(self.config.get_value(key))
  379 
  380         self.window.show_all()
  381 
  382         # use some events to restart lock timer
  383         Gdk.event_handler_set(self.__cb_event_filter)
  384         self.file_locked = False
  385 
  386 
  387         # set some variables
  388         self.entrysearch.string = ''
  389         self.entrysearch.type   = None
  390 
  391         # set ui widget states
  392         self.__state_clipboard(self.entryclipboard.has_contents())
  393         self.__state_entry([])
  394         self.__state_file(None)
  395         self.__state_find(self.searchbar.entry.get_text())
  396         self.__state_undo(None, None)
  397 
  398         # set states from config
  399         self.searchbar.set_visible(self.config.get_boolean("view-searchbar"))
  400         self.statusbar.set_visible(self.config.get_boolean("view-statusbar"))
  401         self.toolbar.set_visible(self.config.get_boolean("view-toolbar"))
  402         self.__cb_config_toolbar_style(self.config, self.config.get_string("view-toolbar-style"))
  403 
  404         # give focus to searchbar entry if shown
  405         if self.searchbar.get_property("visible") == True:
  406             self.searchbar.entry.grab_focus()
  407 
  408 
  409     def __init_ui(self):
  410         "Sets up the UI"
  411 
  412         # add custom icons path
  413         _icon_theme = Gtk.IconTheme.get_default()
  414         if _icon_theme is not None:
  415             _icon_theme.append_search_path(config.DIR_ICONS)
  416 
  417         # set window icons
  418         pixbufs = [ self.items.load_icon("info.olasagasti.revelation", size, 0) for size in ( 128, 48, 32, 24, 16) ]
  419         pixbufs = [ pixbuf for pixbuf in pixbufs if pixbuf != None ]
  420 
  421         if len(pixbufs) > 0:
  422             Gtk.Window.set_default_icon_list(pixbufs)
  423 
  424         # set up toolbar and menus
  425         self.set_menubar(self.menubar)
  426 
  427         toolbar = Gtk.Toolbar.new()
  428         toolbar.set_style(Gtk.ToolbarStyle.BOTH_HORIZ)
  429 
  430         open_item = Gtk.ToolButton.new(Gtk.Image.new_from_icon_name('document-open', Gtk.IconSize.LARGE_TOOLBAR), _('_Open'))
  431         open_item.connect('clicked', lambda k: self.window.get_action_group("file").lookup_action("file-open").activate())
  432         open_item.set_tooltip_text(_('Open a file'))
  433         open_item.set_use_underline(True)
  434         toolbar.insert(open_item, -1)
  435 
  436         save_item = Gtk.ToolButton.new(Gtk.Image.new_from_icon_name('document-save', Gtk.IconSize.LARGE_TOOLBAR), _('_Save'))
  437         save_item.connect('clicked', lambda k: self.window.get_action_group("file").lookup_action("file-save").activate())
  438         save_item.set_tooltip_text(_('Save data to a file'))
  439         save_item.set_property('is-important', True)
  440         save_item.set_use_underline(True)
  441         toolbar.insert(save_item, -1)
  442 
  443         toolbar.insert(Gtk.SeparatorToolItem.new(), -1)
  444 
  445         addentry_item = Gtk.ToolButton.new(Gtk.Image.new_from_icon_name('list-add', Gtk.IconSize.LARGE_TOOLBAR), _('Add Entry'))
  446         addentry_item.connect('clicked', lambda k: self.window.get_action_group("entry-optional").lookup_action("entry-add").activate())
  447         addentry_item.set_tooltip_text(_('Create a new entry'))
  448         addentry_item.set_property('is-important', True)
  449         addentry_item.set_use_underline(True)
  450         toolbar.insert(addentry_item, -1)
  451 
  452         addfolder_item = Gtk.ToolButton.new(Gtk.Image.new_from_icon_name('folder-new', Gtk.IconSize.LARGE_TOOLBAR), _('Add folder'))
  453         addfolder_item.connect('clicked', lambda k: self.window.get_action_group("entry-optional").lookup_action("entry-folder").activate())
  454         addfolder_item.set_tooltip_text(_('Create a new folder'))
  455         toolbar.insert(addfolder_item, -1)
  456 
  457         toolbar.insert(Gtk.SeparatorToolItem.new(), -1)
  458 
  459         gotoentry_item = Gtk.ToolButton.new(Gtk.Image.new_from_icon_name('go-jump', Gtk.IconSize.LARGE_TOOLBAR), _('_Go to'))
  460         gotoentry_item.connect('clicked', lambda k: self.window.get_action_group("dynamic").lookup_action("entry-goto").activate())
  461         gotoentry_item.set_tooltip_text(_('Go to the selected entries'))
  462         gotoentry_item.set_property('is-important', True)
  463         gotoentry_item.set_use_underline(True)
  464         toolbar.insert(gotoentry_item, -1)
  465 
  466         editentry_item = Gtk.ToolButton.new(Gtk.Image.new_from_icon_name('document-edit', Gtk.IconSize.LARGE_TOOLBAR), _('_Edit'))
  467         editentry_item.connect('clicked', lambda k: self.window.get_action_group("entry-single").lookup_action("entry-edit").activate())
  468         editentry_item.set_tooltip_text(_('Edit the selected entry'))
  469         editentry_item.set_use_underline(True)
  470         toolbar.insert(editentry_item, -1)
  471 
  472         removeentry_item = Gtk.ToolButton.new(Gtk.Image.new_from_icon_name('edit-delete', Gtk.IconSize.LARGE_TOOLBAR), _('Re_move'))
  473         removeentry_item.connect('clicked', lambda k: self.window.get_action_group("entry-multiple").lookup_action("entry-remove").activate())
  474         removeentry_item.set_tooltip_text(_('Remove the selected entries'))
  475         removeentry_item.set_use_underline(True)
  476         toolbar.insert(removeentry_item, -1)
  477 
  478 
  479         self.toolbar=toolbar
  480         self.toolbar.connect("popup-context-menu", lambda w,x,y,b: True)
  481         self.add_toolbar(toolbar, "toolbar", 1)
  482 
  483         self.statusbar = ui.Statusbar()
  484         self.main_vbox.pack_end(self.statusbar, False, True, 0)
  485 
  486         self.searchbar = ui.Searchbar()
  487         self.add_toolbar(self.searchbar, "searchbar", 2)
  488 
  489         # set up main application widgets
  490         self.tree = ui.EntryTree(self.entrystore)
  491         self.scrolledwindow = ui.ScrolledWindow(self.tree)
  492 
  493         self.entryview = ui.EntryView(self.config, self.clipboard)
  494         self.entryview.set_halign(Gtk.Align.CENTER)
  495         self.entryview.set_valign(Gtk.Align.CENTER)
  496         self.entryview.set_hexpand(True)
  497 
  498         self.hpaned = Gtk.Paned.new(Gtk.Orientation.HORIZONTAL)
  499         self.hpaned.pack1(self.scrolledwindow, True, True)
  500         self.hpaned.pack2(self.entryview, True, True)
  501         self.hpaned.set_border_width(6)
  502         self.set_contents(self.hpaned)
  503 
  504         # set up drag-and-drop
  505         uritarget = Gtk.TargetEntry.new("text/uri-list", 0, 0)
  506         self.window.drag_dest_set(Gtk.DestDefaults.ALL, [uritarget], Gdk.DragAction.COPY | Gdk.DragAction.MOVE | Gdk.DragAction.LINK )
  507         self.window.connect("drag_data_received", self.__cb_drag_dest)
  508 
  509         self.tree.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, ( ( "revelation/treerow", Gtk.TargetFlags.SAME_APP | Gtk.TargetFlags.SAME_WIDGET, 0), ), Gdk.DragAction.MOVE)
  510         self.tree.enable_model_drag_dest(( ( "revelation/treerow", Gtk.TargetFlags.SAME_APP | Gtk.TargetFlags.SAME_WIDGET, 0), ), Gdk.DragAction.MOVE)
  511         self.tree.connect("drag_data_received", self.__cb_tree_drag_received)
  512 
  513         # set up callbacks
  514         self.searchbar.entry.connect("key-press-event", self.__cb_searchbar_key_press)
  515         self.searchbar.button_next.connect("clicked", self.__cb_searchbar_button_clicked, data.SEARCH_NEXT)
  516         self.searchbar.button_prev.connect("clicked", self.__cb_searchbar_button_clicked, data.SEARCH_PREVIOUS)
  517         self.searchbar.entry.connect("changed", lambda w: self.__state_find(self.searchbar.entry.get_text()))
  518 
  519         self.tree.connect("popup", lambda w,d: self.popup(self.popupmenu, d.button, d.time))
  520         self.tree.connect("doubleclick", self.__cb_tree_doubleclick)
  521         self.tree.connect("key-press-event", self.__cb_tree_keypress)
  522         self.tree.selection.connect("changed", lambda w: self.entryview.display_entry(self.entrystore.get_entry(self.tree.get_active())))
  523         self.tree.selection.connect("changed", lambda w: self.__state_entry(self.tree.get_selected()))
  524 
  525     def __init_dbus(self):
  526         app = Gio.Application.get_default
  527         self.dbus_subscription_id = app().get_dbus_connection().signal_subscribe(None, "org.gnome.ScreenSaver", "ActiveChanged", "/org/gnome/ScreenSaver", None, Gio.DBusSignalFlags.NONE, self.__cb_screensaver_lock)
  528 
  529     ##### STATE HANDLERS #####
  530 
  531     def __save_state(self):
  532         "Saves the current application state"
  533 
  534         width, height = self.window.get_size()
  535         self.config.set_int("view-window-width", width)
  536         self.config.set_int("view-window-height", height)
  537 
  538         x, y = self.window.get_position()
  539         self.config.set_int("view-window-position-x", x)
  540         self.config.set_int("view-window-position-y", y)
  541 
  542         self.config.set_int("view-pane-position", self.hpaned.get_position())
  543         self.config.sync()
  544 
  545 
  546     def __state_clipboard(self, has_contents):
  547         "Sets states based on the clipboard contents"
  548 
  549         self.window.get_action_group("dynamic").lookup_action("clip-paste").set_enabled(has_contents)
  550 
  551     def __state_entry(self, iters):
  552         "Sets states for entry-dependant ui items"
  553 
  554         # widget sensitivity based on number of entries
  555         for action in self.window.get_action_group("entry-multiple").list_actions():
  556             self.window.get_action_group("entry-multiple").lookup_action(action).set_enabled(len(iters) > 0)
  557         for action in self.window.get_action_group("entry-single").list_actions():
  558             self.window.get_action_group("entry-single").lookup_action(action).set_enabled(len(iters) == 1)
  559         for action in self.window.get_action_group("entry-optional").list_actions():
  560             self.window.get_action_group("entry-optional").lookup_action(action).set_enabled(len(iters) < 2)
  561 
  562 
  563         # copy password sensitivity
  564         s = False
  565 
  566         for iter in iters:
  567             e = self.entrystore.get_entry(iter)
  568 
  569             for f in e.fields:
  570                 if f.datatype == entry.DATATYPE_PASSWORD and f.value != "":
  571                     s = True
  572 
  573         self.window.get_action_group("entry-multiple").lookup_action("clip-chain").set_enabled(s)
  574 
  575 
  576         # goto sensitivity
  577         try:
  578             for iter in iters:
  579                 e = self.entrystore.get_entry(iter)
  580 
  581                 if (e.id == "folder"):
  582                     continue
  583 
  584                 if self.config.get_string("launcher-%s" % e.id) not in ( "", None ):
  585                     s = True
  586                     break
  587 
  588             else:
  589                 s = False
  590 
  591         except config.ConfigError:
  592             s = False
  593 
  594         self.window.get_action_group("dynamic").lookup_action("entry-goto").set_enabled(s)
  595 
  596 
  597     def __state_file(self, file):
  598         "Sets states based on file"
  599 
  600         for action in self.window.get_action_group("file-exists").list_actions():
  601             self.window.get_action_group("file-exists").lookup_action(action).set_enabled(file is not None)
  602 
  603         if file is not None:
  604             self.window.set_title(os.path.basename(file))
  605 
  606             if io.file_is_local(file):
  607                 os.chdir(os.path.dirname(file))
  608 
  609         else:
  610             self.window.set_title('[' + _('New file') + ']')
  611 
  612 
  613     def __state_find(self, string):
  614         "Sets states based on the current search string"
  615 
  616         for action in self.window.get_action_group("find").list_actions():
  617             self.window.get_action_group("find").lookup_action(action).set_enabled(string != "")
  618 
  619 
  620     def __state_undo(self, undoaction, redoaction):
  621         "Sets states based on undoqueue actions"
  622 
  623         if undoaction is None:
  624             s, l = False, _('_Undo')
  625 
  626         else:
  627             s, l = True, _('_Undo %s') % undoaction[1].lower()
  628 
  629         action = self.window.get_action_group("dynamic").lookup_action("undo")
  630         action.set_enabled(s)
  631         # TODO action.set_property("label", l)
  632 
  633 
  634         if redoaction is None:
  635             s, l = False, _('_Redo')
  636 
  637         else:
  638             s, l = True, _('_Redo %s') % redoaction[1].lower()
  639 
  640         action = self.window.get_action_group("dynamic").lookup_action("redo")
  641         action.set_enabled(s)
  642         # TODO action.set_property("label", l)
  643 
  644 
  645 
  646 
  647     ##### MISC CALLBACKS #####
  648 
  649     def __cb_clip_copy(self, widget, data = None):
  650         "Handles copying to the clipboard"
  651 
  652         focuswidget = self.window.get_focus()
  653 
  654         if focuswidget is self.tree:
  655             self.clip_copy(self.tree.get_selected())
  656 
  657         elif isinstance(focuswidget, Gtk.Label) or isinstance(focuswidget, Gtk.Entry):
  658             focuswidget.emit("copy-clipboard")
  659 
  660 
  661     def __cb_clip_cut(self, widget, data = None):
  662         "Handles cutting to clipboard"
  663 
  664         focuswidget = self.window.get_focus()
  665 
  666         if focuswidget is self.tree:
  667             self.clip_cut(self.tree.get_selected())
  668 
  669         elif isinstance(focuswidget, Gtk.Entry):
  670             focuswidget.emit("cut-clipboard")
  671 
  672 
  673     def __cb_clip_paste(self, widget, data = None):
  674         "Handles pasting from clipboard"
  675 
  676         focuswidget = self.window.get_focus()
  677 
  678         if focuswidget is self.tree:
  679             self.clip_paste(self.entryclipboard.get(), self.tree.get_active())
  680 
  681         elif isinstance(focuswidget, Gtk.Entry):
  682             focuswidget.emit("paste-clipboard")
  683 
  684 
  685     def __cb_drag_dest(self, widget, context, x, y, seldata, info, time, userdata = None):
  686         "Handles file drops"
  687 
  688         if seldata.data is None:
  689             return
  690 
  691         files = [ file.strip() for file in seldata.data.split("\n") if file.strip() != "" ]
  692 
  693         if len(files) > 0:
  694             self.file_open(files[0])
  695 
  696 
  697     def __cb_event_filter(self, event):
  698         "Event filter for gdk window"
  699 
  700         if event.type in (Gdk.EventType.KEY_PRESS, Gdk.EventType.BUTTON_PRESS, Gdk.EventType.MOTION_NOTIFY):
  701             self.locktimer.reset()
  702 
  703         Gtk.main_do_event(event)
  704         return Gdk.FilterReturn.CONTINUE
  705 
  706 
  707     def __cb_exception(self, type, value, trace):
  708         "Callback for unhandled exceptions"
  709 
  710         if type == KeyboardInterrupt:
  711             sys.exit(1)
  712 
  713         traceback = util.trace_exception(type, value, trace)
  714         sys.stderr.write(traceback)
  715 
  716         if dialog.Exception(self.window, traceback).run() == True:
  717             Gtk.main()
  718 
  719         else:
  720             sys.exit(1)
  721 
  722 
  723     def __cb_file_content_changed(self, widget, file):
  724         "Callback for changed file"
  725 
  726         try:
  727             if dialog.FileChanged(self.window, file).run() == True:
  728                 self.file_open(self.datafile.get_file(), self.datafile.get_password())
  729 
  730         except dialog.CancelError:
  731             self.statusbar.set_status(_('Open cancelled'))
  732 
  733 
  734     def __cb_file_autolock(self, widget, data = None):
  735         "Callback for locking the file"
  736 
  737         if self.config.get_boolean("file-autolock") == True:
  738             self.file_lock()
  739 
  740 
  741     def __cb_screensaver_lock(self, connection, unique_name, object_path, interface, signal, state):
  742         if state[0] is True and self.config.get_boolean("file-autolock") == True:
  743             self.file_lock()
  744 
  745     def __cb_quit(self, widget, data = None):
  746         "Callback for quit"
  747 
  748         if self.quit() == False:
  749             return True
  750 
  751         else:
  752             return False
  753 
  754     def __cb_close(self, widget, data = None):
  755         "Callback for Close"
  756 
  757         if self.file_close() == False:
  758             return True
  759 
  760         else:
  761             return False
  762 
  763     def __cb_searchbar_button_clicked(self, widget, direction = data.SEARCH_NEXT):
  764         "Callback for searchbar button clicks"
  765 
  766         self.__entry_find(self, self.searchbar.entry.get_text(), self.searchbar.dropdown.get_active_type(), direction)
  767         self.searchbar.entry.select_region(0, -1)
  768 
  769 
  770     def __cb_searchbar_key_press(self, widget, data):
  771         "Callback for searchbar key presses"
  772 
  773         # escape
  774         if data.keyval == Gdk.KEY_Escape:
  775             context = widget.get_style_context()
  776             context.remove_class(Gtk.STYLE_CLASS_ERROR)
  777             self.config.set_boolean("view-searchbar", False)
  778 
  779 
  780     def __cb_tree_doubleclick(self, widget, iter):
  781         "Handles doubleclicks on the tree"
  782 
  783         if self.config.get_string("behavior-doubleclick") == "edit":
  784             self.entry_edit(iter)
  785 
  786         elif self.config.get_string("behavior-doubleclick") == "copy":
  787             self.clip_chain(self.entrystore.get_entry(iter))
  788 
  789         else:
  790             self.entry_goto((iter,))
  791 
  792 
  793     def __cb_tree_drag_received(self, tree, context, x, y, seldata, info, time):
  794         "Callback for drag drops on the treeview"
  795 
  796         # get source and destination data
  797         sourceiters = self.entrystore.filter_parents(self.tree.get_selected())
  798         destrow = self.tree.get_dest_row_at_pos(x, y)
  799 
  800         if destrow is None:
  801             destpath = ( self.entrystore.iter_n_children(None) - 1, )
  802             pos = Gtk.TreeViewDropPosition.AFTER
  803 
  804         else:
  805             destpath, pos = destrow
  806 
  807         destiter = self.entrystore.get_iter(destpath)
  808         destpath = self.entrystore.get_path(destiter)
  809 
  810         # avoid drops to current iter or descentants
  811         for sourceiter in sourceiters:
  812             sourcepath = self.entrystore.get_path(sourceiter)
  813 
  814             if self.entrystore.is_ancestor(sourceiter, destiter) == True or sourcepath == destpath:
  815                 context.finish(False, False, time)
  816                 return
  817 
  818             elif pos == Gtk.TreeViewDropPosition.BEFORE and sourcepath[:-1] == destpath[:-1] and sourcepath[-1] == destpath[-1] - 1:
  819                 context.finish(False, False, time)
  820                 return
  821 
  822             elif pos == Gtk.TreeViewDropPosition.AFTER and sourcepath[:-1] == destpath[:-1] and sourcepath[-1] == destpath[-1] + 1:
  823                 context.finish(False, False, time)
  824                 return
  825 
  826 
  827         # move the entries
  828         if pos in ( Gtk.TreeViewDropPosition.INTO_OR_BEFORE, Gtk.TreeViewDropPosition.INTO_OR_AFTER):
  829             parent = destiter
  830             sibling = None
  831 
  832         elif pos == Gtk.TreeViewDropPosition.BEFORE:
  833             parent = self.entrystore.iter_parent(destiter)
  834             sibling = destiter
  835 
  836         elif pos == Gtk.TreeViewDropPosition.AFTER:
  837             parent = self.entrystore.iter_parent(destiter)
  838 
  839             sibpath = list(destpath)
  840             sibpath[-1] += 1
  841             sibling = self.entrystore.get_iter(sibpath)
  842 
  843         self.entry_move(sourceiters, parent, sibling)
  844 
  845         context.finish(False, False, time)
  846 
  847 
  848     def __cb_tree_keypress(self, widget, data = None):
  849         "Handles key presses for the tree"
  850 
  851         # return
  852         if data.keyval == Gdk.KEY_Return:
  853             self.entry_edit(self.tree.get_active())
  854 
  855         # insert
  856         elif data.keyval == Gdk.KEY_Insert:
  857             self.entry_add(None, self.tree.get_active())
  858 
  859         # delete
  860         elif data.keyval == Gdk.KEY_Delete:
  861             self.entry_remove(self.tree.get_selected())
  862 
  863 
  864 
  865     ##### CONFIG CALLBACKS #####
  866 
  867     def __cb_config_toolbar_style(self, config, value, data = None):
  868         "Config callback for setting toolbar style"
  869 
  870         if value == "both":
  871             self.toolbar.set_style(Gtk.ToolbarStyle.BOTH)
  872 
  873         elif value == "both-horiz":
  874             self.toolbar.set_style(Gtk.ToolbarStyle.BOTH_HORIZ)
  875 
  876         elif value == "icons":
  877             self.toolbar.set_style(Gtk.ToolbarStyle.ICONS)
  878 
  879         elif value == "text":
  880             self.toolbar.set_style(Gtk.ToolbarStyle.TEXT)
  881 
  882         else:
  883             self.toolbar.unset_style()
  884 
  885 
  886     #### UNDO / REDO CALLBACKS #####
  887 
  888     def __cb_redo_add(self, name, actiondata):
  889         "Redoes an add action"
  890 
  891         path, e = actiondata
  892         parent = self.entrystore.get_iter(path[:-1])
  893         sibling = self.entrystore.get_iter(path)
  894 
  895         iter = self.entrystore.add_entry(e, parent, sibling)
  896         self.tree.select(iter)
  897 
  898 
  899     def __cb_redo_edit(self, name, actiondata):
  900         "Redoes an edit action"
  901 
  902         path, preentry, postentry = actiondata
  903         iter = self.entrystore.get_iter(path)
  904 
  905         self.entrystore.update_entry(iter, postentry)
  906         self.tree.select(iter)
  907 
  908 
  909     def __cb_redo_import(self, name, actiondata):
  910         "Redoes an import action"
  911 
  912         paths, entrystore = actiondata
  913         self.entrystore.import_entry(entrystore, None)
  914 
  915 
  916     def __cb_redo_move(self, name, actiondata):
  917         "Redoes a move action"
  918 
  919         newiters = []
  920 
  921         for prepath, postpath in actiondata:
  922             prepath, postpath = list(prepath), list(postpath)
  923 
  924             # adjust path if necessary
  925             if len(prepath) <= len(postpath):
  926                 if prepath[:-1] == postpath[:len(prepath) - 1]:
  927                     if prepath[-1] <= postpath[len(prepath) - 1]:
  928                         postpath[len(prepath) - 1] += 1
  929 
  930             newiter = self.entrystore.move_entry(
  931                 self.entrystore.get_iter(prepath),
  932                 self.entrystore.get_iter(postpath[:-1]),
  933                 self.entrystore.get_iter(postpath)
  934             )
  935 
  936             newiters.append(newiter)
  937 
  938         if len(newiters) > 0:
  939             self.tree.select(newiters[0])
  940 
  941 
  942     def __cb_redo_paste(self, name, actiondata):
  943         "Redoes a paste action"
  944 
  945         entrystore, parentpath, paths = actiondata
  946         iters = self.entrystore.import_entry(entrystore, None, self.entrystore.get_iter(parentpath))
  947 
  948         if len(iters) > 0:
  949             self.tree.select(iters[0])
  950 
  951 
  952     def __cb_redo_remove(self, name, actiondata):
  953         "Redoes a remove action"
  954 
  955         iters = []
  956         for path, entrystore in actiondata:
  957             iters.append(self.entrystore.get_iter(path))
  958 
  959         for iter in iters:
  960             self.entrystore.remove_entry(iter)
  961 
  962         self.tree.unselect_all()
  963 
  964 
  965     def __cb_undo_add(self, name, actiondata):
  966         "Undoes an add action"
  967 
  968         path, e = actiondata
  969 
  970         self.entrystore.remove_entry(self.entrystore.get_iter(path))
  971         self.tree.unselect_all()
  972 
  973 
  974     def __cb_undo_edit(self, name, actiondata):
  975         "Undoes an edit action"
  976 
  977         path, preentry, postentry = actiondata
  978         iter = self.entrystore.get_iter(path)
  979 
  980         self.entrystore.update_entry(iter, preentry)
  981         self.tree.select(iter)
  982 
  983 
  984     def __cb_undo_import(self, name, actiondata):
  985         "Undoes an import action"
  986 
  987         paths, entrystore = actiondata
  988         iters = [ self.entrystore.get_iter(path) for path in paths ]
  989 
  990         for iter in iters:
  991             self.entrystore.remove_entry(iter)
  992 
  993         self.tree.unselect_all()
  994 
  995 
  996     def __cb_undo_move(self, name, actiondata):
  997         "Undoes a move action"
  998 
  999         actiondata = actiondata[:]
 1000         actiondata.reverse()
 1001 
 1002         newiters = []
 1003 
 1004         for prepath, postpath in actiondata:
 1005             prepath, postpath = list(prepath), list(postpath)
 1006 
 1007             # adjust path if necessary
 1008             if len(postpath) <= len(prepath):
 1009                 if postpath[:-1] == prepath[:len(postpath) - 1]:
 1010                     if postpath[-1] <= prepath[len(postpath) - 1]:
 1011                         prepath[len(postpath) - 1] += 1
 1012 
 1013             newiter = self.entrystore.move_entry(
 1014                 self.entrystore.get_iter(postpath),
 1015                 self.entrystore.get_iter(prepath[:-1]),
 1016                 self.entrystore.get_iter(prepath)
 1017             )
 1018 
 1019             newiters.append(newiter)
 1020 
 1021         if len(newiters) > 0:
 1022             self.tree.select(newiters[-1])
 1023 
 1024 
 1025     def __cb_undo_paste(self, name, actiondata):
 1026         "Undoes a paste action"
 1027 
 1028         entrystore, parentpath, paths = actiondata
 1029         iters = [ self.entrystore.get_iter(path) for path in paths ]
 1030 
 1031         for iter in iters:
 1032             self.entrystore.remove_entry(iter)
 1033 
 1034         self.tree.unselect_all()
 1035 
 1036 
 1037     def __cb_undo_remove(self, name, actiondata):
 1038         "Undoes a remove action"
 1039 
 1040         iters = []
 1041         for path, entrystore in actiondata:
 1042             parent = self.entrystore.get_iter(path[:-1])
 1043             sibling = self.entrystore.get_iter(path)
 1044 
 1045             iter = self.entrystore.import_entry(entrystore, entrystore.iter_nth_child(None, 0), parent, sibling)
 1046             iters.append(iter)
 1047 
 1048         self.tree.select(iters[0])
 1049 
 1050 
 1051 
 1052     ##### PRIVATE METHODS #####
 1053 
 1054     def __entry_find(self, parent, string, entrytype, direction = data.SEARCH_NEXT):
 1055         "Searches for an entry"
 1056 
 1057         match = self.entrysearch.find(string, entrytype, self.tree.get_active(), direction)
 1058         context = self.searchbar.entry.get_style_context()
 1059 
 1060         if match != None:
 1061             self.tree.select(match)
 1062             self.statusbar.set_status(_('Match found for “%s”') % string)
 1063             context.remove_class(Gtk.STYLE_CLASS_ERROR)
 1064 
 1065         else:
 1066             self.statusbar.set_status(_('No match found for “%s”') % string)
 1067             context.add_class(Gtk.STYLE_CLASS_ERROR)
 1068 
 1069 
 1070     def __file_autosave(self):
 1071         "Autosaves the current file if needed"
 1072 
 1073         try:
 1074             if self.datafile.get_file() is None or self.datafile.get_password() is None:
 1075                 return
 1076 
 1077             if self.config.get_boolean("file-autosave") == False:
 1078                 return
 1079 
 1080             self.datafile.save(self.entrystore, self.datafile.get_file(), self.datafile.get_password())
 1081             self.entrystore.changed = False
 1082 
 1083         except IOError:
 1084             pass
 1085 
 1086 
 1087     def __file_load(self, file, password, datafile = None):
 1088         "Loads data from a data file into an entrystore"
 1089 
 1090         # We may need to change the datahandler
 1091         old_handler = None
 1092         result = None
 1093 
 1094         try:
 1095             if datafile is None:
 1096                 datafile = self.datafile
 1097 
 1098                 # Because there are two fileversion we need to check if we are really dealing
 1099                 # with version two. If we aren't chances are high, that we are
 1100                 # dealing with version one. In this case we use the version one
 1101                 # handler and save the file as version two if it is changed, to
 1102                 # allow seemless upgrades.
 1103                 if datafile.get_handler().detect(io.file_read(file)) != True:
 1104                     # Store the datahandler to be reset later on
 1105                     old_handler = datafile.get_handler()
 1106                     # Load the revelation fileversion one handler
 1107                     datafile.set_handler(datahandler.Revelation)
 1108                     dialog.Info(self.window,_('Old file format'), _('Revelation detected that \'%s\' file has the old and actually non-secure file format. It is strongly recommended to save this file with the new format. Revelation will do it automatically if you press save after opening the file.') % file).run()
 1109 
 1110             while True:
 1111                 try:
 1112                     result = datafile.load(file, password, lambda: dialog.PasswordOpen(self.window, os.path.basename(file)).run())
 1113                     break
 1114 
 1115                 except datahandler.PasswordError:
 1116                     dialog.Error(self.window, _('Incorrect password'), _('The password you entered for the file \'%s\' was not correct.') % file).run()
 1117 
 1118         except datahandler.FormatError:
 1119             self.statusbar.set_status(_('Open failed'))
 1120             dialog.Error(self.window, _('Invalid file format'), _('The file \'%s\' contains invalid data.') % file).run()
 1121 
 1122         except ( datahandler.DataError, entry.EntryTypeError, entry.EntryFieldError ):
 1123             self.statusbar.set_status(_('Open failed'))
 1124             dialog.Error(self.window, _('Unknown data'), _('The file \'%s\' contains unknown data. It may have been created by a newer version of Revelation.') % file).run()
 1125 
 1126         except datahandler.VersionError:
 1127             self.statusbar.set_status(_('Open failed'))
 1128             dialog.Error(self.window, _('Unknown data version'), _('The file \'%s\' has a future version number, please upgrade Revelation to open it.') % file).run()
 1129 
 1130         except datahandler.DetectError:
 1131             self.statusbar.set_status(_('Open failed'))
 1132             dialog.Error(self.window, _('Unable to detect filetype'), _('The file type of the file \'%s\' could not be automatically detected. Try specifying the file type manually.')% file).run()
 1133 
 1134         except IOError:
 1135             self.statusbar.set_status(_('Open failed'))
 1136             dialog.Error(self.window, _('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()
 1137 
 1138         # If we switched the datahandlers before we need to switch back to the
 1139         # version2 handler here, to ensure a seemless version upgrade on save
 1140         if old_handler != None:
 1141             datafile.set_handler(old_handler.__class__)
 1142 
 1143         return result
 1144 
 1145 
 1146     def __get_common_usernames(self, e = None):
 1147         "Returns a list of possibly relevant usernames"
 1148 
 1149         ulist = []
 1150 
 1151         if e is not None and e.has_field(entry.UsernameField):
 1152             ulist.append(e[entry.UsernameField])
 1153 
 1154         ulist.append(pwd.getpwuid(os.getuid())[0])
 1155         ulist.extend(self.entrystore.get_popular_values(entry.UsernameField, 3))
 1156 
 1157         ulist = list({}.fromkeys(ulist).keys())
 1158         ulist.sort()
 1159 
 1160         return ulist
 1161 
 1162 
 1163 
 1164     ##### PUBLIC METHODS #####
 1165 
 1166     def about(self):
 1167         "Displays the about dialog"
 1168 
 1169         dialog.run_unique(dialog.About, self)
 1170 
 1171 
 1172     def clip_chain(self, e):
 1173         "Copies all passwords from an entry as a chain"
 1174 
 1175         if e is None:
 1176             return
 1177 
 1178         secrets = [ field.value for field in e.fields if field.datatype == entry.DATATYPE_PASSWORD and field.value != "" ]
 1179 
 1180         if self.config.get_boolean("clipboard-chain-username") == True and len(secrets) > 0 and e.has_field(entry.UsernameField) and e[entry.UsernameField] != "":
 1181             secrets.insert(0, e[entry.UsernameField])
 1182 
 1183         if len(secrets) == 0:
 1184             self.statusbar.set_status(_('Entry has no password to copy'))
 1185 
 1186         else:
 1187             self.clipboard.set(secrets, True)
 1188             self.statusbar.set_status(_('Password copied to clipboard'))
 1189 
 1190 
 1191     def clip_copy(self, iters):
 1192         "Copies entries to the clipboard"
 1193 
 1194         self.entryclipboard.set(self.entrystore, iters)
 1195         self.statusbar.set_status(_('Entries copied'))
 1196 
 1197 
 1198     def clip_cut(self, iters):
 1199         "Cuts entries to the clipboard"
 1200 
 1201         iters = self.entrystore.filter_parents(iters)
 1202         self.entryclipboard.set(self.entrystore, iters)
 1203 
 1204         # store undo data (need paths)
 1205         undoactions = []
 1206         for iter in iters:
 1207             undostore = data.EntryStore()
 1208             undostore.import_entry(self.entrystore, iter)
 1209             path = self.entrystore.get_path(iter)
 1210             undoactions.append( ( path, undostore ) )
 1211 
 1212         # remove data
 1213         for iter in iters:
 1214             self.entrystore.remove_entry(iter)
 1215 
 1216         self.undoqueue.add_action(
 1217             _('Cut entries'), self.__cb_undo_remove, self.__cb_redo_remove,
 1218             undoactions
 1219         )
 1220 
 1221         self.__file_autosave()
 1222 
 1223         self.tree.unselect_all()
 1224         self.statusbar.set_status(_('Entries cut'))
 1225 
 1226 
 1227     def clip_paste(self, entrystore, parent):
 1228         "Pastes entries from the clipboard"
 1229 
 1230         if entrystore is None:
 1231             return
 1232 
 1233         parent = self.tree.get_active()
 1234         iters = self.entrystore.import_entry(entrystore, None, parent)
 1235 
 1236         paths = [ self.entrystore.get_path(iter) for iter in iters ]
 1237 
 1238         self.undoqueue.add_action(
 1239             _('Paste entries'), self.__cb_undo_paste, self.__cb_redo_paste,
 1240             ( entrystore, self.entrystore.get_path(parent), paths )
 1241         )
 1242 
 1243         if len(iters) > 0:
 1244             self.tree.select(iters[0])
 1245 
 1246         self.statusbar.set_status(_('Entries pasted'))
 1247 
 1248 
 1249     def entry_add(self, e = None, parent = None, sibling = None):
 1250         "Adds an entry"
 1251 
 1252         try:
 1253             if e is None:
 1254                 d = dialog.EntryEdit(self.window, _('Add Entry'), None, self.config, self.clipboard)
 1255                 d.set_fieldwidget_data(entry.UsernameField, self.__get_common_usernames())
 1256                 e = d.run()
 1257 
 1258             iter = self.entrystore.add_entry(e, parent, sibling)
 1259 
 1260             self.undoqueue.add_action(
 1261                 _('Add entry'), self.__cb_undo_add, self.__cb_redo_add,
 1262                 ( self.entrystore.get_path(iter), e.copy() )
 1263             )
 1264 
 1265             self.__file_autosave()
 1266             self.tree.select(iter)
 1267             self.statusbar.set_status(_('Entry added'))
 1268 
 1269         except dialog.CancelError:
 1270             self.statusbar.set_status(_('Add entry cancelled'))
 1271 
 1272 
 1273     def entry_edit(self, iter):
 1274         "Edits an entry"
 1275 
 1276         try:
 1277             if iter is None:
 1278                 return
 1279 
 1280             e = self.entrystore.get_entry(iter)
 1281 
 1282             if type(e) == entry.FolderEntry:
 1283                 d = dialog.FolderEdit(self.window, _('Edit Folder'), e)
 1284 
 1285             else:
 1286                 d = dialog.EntryEdit(self.window, _('Edit Entry'), e, self.config, self.clipboard)
 1287                 d.set_fieldwidget_data(entry.UsernameField, self.__get_common_usernames(e))
 1288 
 1289 
 1290             n = d.run()
 1291             self.entrystore.update_entry(iter, n)
 1292             self.tree.select(iter)
 1293 
 1294             self.undoqueue.add_action(
 1295                 _('Update entry'), self.__cb_undo_edit, self.__cb_redo_edit,
 1296                 ( self.entrystore.get_path(iter), e.copy(), n.copy() )
 1297             )
 1298 
 1299             self.__file_autosave()
 1300             self.statusbar.set_status(_('Entry updated'))
 1301 
 1302         except dialog.CancelError:
 1303             self.statusbar.set_status(_('Edit entry cancelled'))
 1304 
 1305 
 1306     def entry_find(self):
 1307         "Searches for an entry"
 1308 
 1309         self.config.set_boolean("view-searchbar", True)
 1310         self.searchbar.entry.select_region(0, -1)
 1311         self.searchbar.entry.grab_focus()
 1312 
 1313 
 1314     def entry_folder(self, e = None, parent = None, sibling = None):
 1315         "Adds a folder"
 1316 
 1317         try:
 1318             if e is None:
 1319                 e = dialog.FolderEdit(self.window, _('Add folder')).run()
 1320 
 1321             iter = self.entrystore.add_entry(e, parent, sibling)
 1322 
 1323             self.undoqueue.add_action(
 1324                 _('Add folder'), self.__cb_undo_add, self.__cb_redo_add,
 1325                 ( self.entrystore.get_path(iter), e.copy() )
 1326             )
 1327 
 1328             self.__file_autosave()
 1329             self.tree.select(iter)
 1330             self.statusbar.set_status(_('Folder added'))
 1331 
 1332         except dialog.CancelError:
 1333             self.statusbar.set_status(_('Add folder cancelled'))
 1334 
 1335 
 1336     def entry_goto(self, iters):
 1337         "Goes to an entry"
 1338 
 1339         for iter in iters:
 1340             try:
 1341 
 1342                 # get goto data for entry
 1343                 e = self.entrystore.get_entry(iter)
 1344                 command = self.config.get_string("launcher-%s" % e.id)
 1345 
 1346                 if command in ( "", None ):
 1347                     self.statusbar.set_status(_('No goto command found for %s entries') % e.typename)
 1348                     return
 1349 
 1350                 subst = {}
 1351                 for field in e.fields:
 1352                     subst[field.symbol] = field.value
 1353 
 1354                 # copy passwords to clipboard
 1355                 chain = []
 1356 
 1357                 for field in e.fields:
 1358                     if field.datatype == entry.DATATYPE_PASSWORD and field.value != "":
 1359                         chain.append(field.value)
 1360 
 1361                 if self.config.get_boolean("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:
 1362                     chain.insert(0, e[entry.UsernameField])
 1363 
 1364                 self.clipboard.set(chain, True)
 1365 
 1366                 # generate and run goto command
 1367                 command = util.parse_subst(command, subst)
 1368                 util.execute_child(command)
 1369 
 1370                 self.statusbar.set_status(_('Entry opened'))
 1371 
 1372             except ( util.SubstFormatError, config.ConfigError ):
 1373                 dialog.Error(self.window, _('Invalid goto command format'), _('The goto command for \'%s\' entries is invalid, please correct it in the preferences.') % e.typename).run()
 1374 
 1375             except util.SubstValueError:
 1376                 dialog.Error(self.window, _('Missing entry data'), _('The entry \'%s\' does not have all the data required to open it.') % e.name).run()
 1377 
 1378 
 1379     def entry_move(self, sourceiters, parent = None, sibling = None):
 1380         "Moves a set of entries"
 1381 
 1382         if type(sourceiters) != list:
 1383             sourceiters = [ sourceiters ]
 1384 
 1385         newiters = []
 1386         undoactions = []
 1387 
 1388         for sourceiter in sourceiters:
 1389             sourcepath = self.entrystore.get_path(sourceiter)
 1390             newiter = self.entrystore.move_entry(sourceiter, parent, sibling)
 1391             newpath = self.entrystore.get_path(newiter)
 1392 
 1393             undoactions.append( ( sourcepath, newpath ) )
 1394             newiters.append(newiter)
 1395 
 1396         self.undoqueue.add_action(
 1397             _('Move entry'), self.__cb_undo_move, self.__cb_redo_move,
 1398             undoactions
 1399         )
 1400 
 1401         if len(newiters) > 0:
 1402             self.tree.select(newiters[0])
 1403 
 1404         self.__file_autosave()
 1405         self.statusbar.set_status(_('Entries moved'))
 1406 
 1407 
 1408     def entry_remove(self, iters):
 1409         "Removes the selected entries"
 1410 
 1411         try:
 1412             if len(iters) == 0:
 1413                 return
 1414 
 1415             entries = [ self.entrystore.get_entry(iter) for iter in iters ]
 1416             dialog.EntryRemove(self.window, entries).run()
 1417             iters = self.entrystore.filter_parents(iters)
 1418 
 1419             # store undo data (need paths)
 1420             undoactions = []
 1421             for iter in iters:
 1422                 undostore = data.EntryStore()
 1423                 undostore.import_entry(self.entrystore, iter)
 1424                 path = self.entrystore.get_path(iter)
 1425                 undoactions.append( ( path, undostore ) )
 1426 
 1427             # remove data
 1428             for iter in iters:
 1429                 self.entrystore.remove_entry(iter)
 1430 
 1431             self.undoqueue.add_action(
 1432                 _('Remove entry'), self.__cb_undo_remove, self.__cb_redo_remove,
 1433                 undoactions
 1434             )
 1435 
 1436             self.tree.unselect_all()
 1437             self.__file_autosave()
 1438             self.statusbar.set_status(_('Entries removed'))
 1439 
 1440         except dialog.CancelError:
 1441             self.statusbar.set_status(_('Entry removal cancelled'))
 1442 
 1443 
 1444     def file_change_password(self, password = None):
 1445         "Changes the password of the current data file"
 1446 
 1447         try:
 1448             if password is None:
 1449                 password = dialog.PasswordChange(self.window, self.datafile.get_password()).run()
 1450 
 1451             self.datafile.set_password(password)
 1452             self.entrystore.changed = True
 1453 
 1454             self.__file_autosave()
 1455             self.statusbar.set_status(_('Password changed'))
 1456 
 1457         except dialog.CancelError:
 1458             self.statusbar.set_status(_('Password change cancelled'))
 1459 
 1460     def file_close(self):
 1461         "Closes the current file"
 1462 
 1463         try:
 1464             if self.entrystore.changed == True and dialog.FileChangesClose(self.window).run() == True:
 1465                 if self.file_save(self.datafile.get_file(), self.datafile.get_password()) == False:
 1466                     raise dialog.CancelError
 1467 
 1468             self.clipboard.clear()
 1469             self.entryclipboard.clear()
 1470             self.entrystore.clear()
 1471             self.undoqueue.clear()
 1472             self.statusbar.set_status(_('Closed file %s') % self.datafile.get_file())
 1473             self.datafile.close()
 1474 
 1475             return True
 1476 
 1477         except dialog.CancelError:
 1478             self.statusbar.set_status(_('Close file cancelled'))
 1479             return False
 1480 
 1481     def file_export(self):
 1482         "Exports data to a foreign file format"
 1483 
 1484         try:
 1485             file, handler = dialog.ExportFileSelector(self.window).run()
 1486             datafile = io.DataFile(handler)
 1487 
 1488             if datafile.get_handler().encryption == True:
 1489                 password = dialog.PasswordSave(self.window, file).run()
 1490 
 1491             else:
 1492                 dialog.FileSaveInsecure(self.window).run()
 1493                 password = None
 1494 
 1495             datafile.save(self.entrystore, file, password)
 1496             self.statusbar.set_status(_('Data exported to %s') % datafile.get_file())
 1497 
 1498         except dialog.CancelError:
 1499             self.statusbar.set_status(_('Export cancelled'))
 1500 
 1501         except IOError:
 1502             dialog.Error(self.window, _('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()
 1503             self.statusbar.set_status(_('Export failed'))
 1504 
 1505 
 1506     def file_import(self):
 1507         "Imports data from a foreign file"
 1508 
 1509         try:
 1510             file, handler = dialog.ImportFileSelector(self.window).run()
 1511             datafile = io.DataFile(handler)
 1512             entrystore = self.__file_load(file, None, datafile)
 1513 
 1514             if entrystore is not None:
 1515                 newiters = self.entrystore.import_entry(entrystore, None)
 1516                 paths = [ self.entrystore.get_path(iter) for iter in newiters ]
 1517 
 1518                 self.undoqueue.add_action(
 1519                     _('Import data'), self.__cb_undo_import, self.__cb_redo_import,
 1520                     ( paths, entrystore )
 1521                 )
 1522 
 1523                 self.statusbar.set_status(_('Data imported from %s') % datafile.get_file())
 1524 
 1525             self.__file_autosave()
 1526 
 1527         except dialog.CancelError:
 1528             self.statusbar.set_status(_('Import cancelled'))
 1529 
 1530 
 1531     def file_lock(self):
 1532         "Locks the current file"
 1533 
 1534         if self.datafile.get_file() is None:
 1535             return
 1536 
 1537         if self.file_locked is True:
 1538             return
 1539 
 1540         password = self.datafile.get_password()
 1541 
 1542         if password is None:
 1543             return
 1544 
 1545         self.locktimer.stop()
 1546         app = Gio.Application.get_default
 1547         app().get_dbus_connection().signal_unsubscribe(self.dbus_subscription_id)
 1548 
 1549 
 1550         # TODO can this be done more elegantly?
 1551         transients = [ window for window in Gtk.Window.list_toplevels() if window.get_transient_for() == self ]
 1552 
 1553         # store current state
 1554         activeiter = self.tree.get_active()
 1555         oldtitle = self.get_title()
 1556 
 1557         # clear application contents
 1558         self.tree.set_model(None)
 1559         self.entryview.clear()
 1560         self.window.set_title('[' + _('Locked') + ']')
 1561         self.statusbar.set_status(_('File locked'))
 1562         self.file_locked = True;
 1563 
 1564         # hide any dialogs
 1565         for window in transients:
 1566             window.hide()
 1567 
 1568         # lock file
 1569         try:
 1570             d = dialog.PasswordLock(self.window, password)
 1571 
 1572             if self.entrystore.changed == True:
 1573                 l = ui.ImageLabel(_('Quit disabled due to unsaved changes'), "dialog-warning")
 1574                 d.contents.pack_start(l, True, True, 0)
 1575                 d.get_widget_for_response(Gtk.ResponseType.CANCEL).set_sensitive(False)
 1576 
 1577             d.run()
 1578 
 1579         except dialog.CancelError:
 1580             self.quit()
 1581 
 1582         # unlock the file and restore state
 1583         self.tree.set_model(self.entrystore)
 1584         self.tree.select(activeiter)
 1585         self.window.set_title(oldtitle)
 1586         self.statusbar.set_status(_('File unlocked'))
 1587         self.file_locked = False;
 1588 
 1589         for window in transients:
 1590             window.show()
 1591 
 1592         self.locktimer.start(self.config.get_int("file-autolock-timeout") * 60)
 1593         app = Gio.Application.get_default
 1594         self.dbus_subscription_id = app().get_dbus_connection().signal_subscribe(None, "org.gnome.ScreenSaver", "ActiveChanged", "/org/gnome/ScreenSaver", None, Gio.DBusSignalFlags.NONE, self.__cb_screensaver_lock)
 1595 
 1596 
 1597 
 1598     def file_new(self):
 1599         "Opens a new file"
 1600 
 1601         try:
 1602             if self.entrystore.changed == True and dialog.FileChangesNew(self.window).run() == True:
 1603                 if self.file_save(self.datafile.get_file(), self.datafile.get_password()) == False:
 1604                     raise dialog.CancelError
 1605 
 1606             self.entrystore.clear()
 1607             self.datafile.close()
 1608             self.undoqueue.clear()
 1609             self.statusbar.set_status(_('New file created'))
 1610 
 1611         except dialog.CancelError:
 1612             self.statusbar.set_status(_('New file cancelled'))
 1613 
 1614 
 1615     def file_open(self, file = None, password = None):
 1616         "Opens a data file"
 1617 
 1618         try:
 1619             if self.entrystore.changed == True and dialog.FileChangesOpen(self.window).run() == True:
 1620                 if self.file_save(self.datafile.get_file(), self.datafile.get_password()) == False:
 1621                     raise dialog.CancelError
 1622 
 1623             if file is None:
 1624                 file = dialog.OpenFileSelector(self.window).run()
 1625 
 1626             entrystore = self.__file_load(file, password)
 1627 
 1628             if entrystore is None:
 1629                 return
 1630 
 1631             self.entrystore.clear()
 1632             self.entrystore.import_entry(entrystore, None)
 1633             self.entrystore.changed = False
 1634             self.undoqueue.clear()
 1635 
 1636             self.file_locked = False;
 1637             self.locktimer.start(60 * self.config.get_int("file-autolock-timeout"))
 1638             self.statusbar.set_status(_('Opened file %s') % self.datafile.get_file())
 1639 
 1640         except dialog.CancelError:
 1641             self.statusbar.set_status(_('Open cancelled'))
 1642 
 1643 
 1644     def file_save(self, file = None, password = None):
 1645         "Saves data to a file"
 1646 
 1647         try:
 1648             if file is None:
 1649                 file = dialog.SaveFileSelector(self.window).run()
 1650 
 1651             if password is None:
 1652                 password = dialog.PasswordSave(self.window, file).run()
 1653 
 1654             self.datafile.save(self.entrystore, file, password)
 1655             self.entrystore.changed = False
 1656             self.statusbar.set_status(_('Data saved to file %s') % file)
 1657 
 1658             return True
 1659 
 1660         except dialog.CancelError:
 1661             self.statusbar.set_status(_('Save cancelled'))
 1662             return False
 1663 
 1664         except IOError:
 1665             dialog.Error(self.window, _('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()
 1666             self.statusbar.set_status(_('Save failed'))
 1667             return False
 1668 
 1669 
 1670     def prefs(self):
 1671         "Displays the application preferences"
 1672 
 1673         dialog.run_unique(Preferences, self.window, self.config)
 1674 
 1675 
 1676     def pwcheck(self):
 1677         "Displays the password checking dialog"
 1678 
 1679         dialog.run_unique(dialog.PasswordChecker, self.window, self.config, self.clipboard)
 1680 
 1681 
 1682     def pwgen(self):
 1683         "Displays the password generator dialog"
 1684 
 1685         dialog.run_unique(dialog.PasswordGenerator, self.window, self.config, self.clipboard)
 1686 
 1687 
 1688     def quit(self):
 1689         "Quits the application"
 1690 
 1691         try:
 1692             if self.entrystore.changed == True and dialog.FileChangesQuit(self.window).run() == True:
 1693                 if self.file_save(self.datafile.get_file(), self.datafile.get_password()) == False:
 1694                     raise dialog.CancelError
 1695 
 1696             self.clipboard.clear()
 1697             self.entryclipboard.clear()
 1698 
 1699             self.__save_state()
 1700 
 1701             Gtk.Application.quit(self)
 1702             if sys.exc_info()[1]:
 1703                 # avoid raising an additional exception
 1704                 os._exit(0)
 1705             else:
 1706                 sys.exit(0)
 1707             return True
 1708 
 1709         except dialog.CancelError:
 1710             self.statusbar.set_status(_('Quit cancelled'))
 1711             return False
 1712 
 1713 
 1714     def redo(self):
 1715         "Redoes the previous action"
 1716 
 1717         action = self.undoqueue.get_redo_action()
 1718 
 1719         if action is None:
 1720             return
 1721 
 1722         self.undoqueue.redo()
 1723         self.statusbar.set_status(_('%s redone') % action[1])
 1724         self.__file_autosave()
 1725 
 1726 
 1727     def undo(self):
 1728         "Undoes the previous action"
 1729 
 1730         action = self.undoqueue.get_undo_action()
 1731 
 1732         if action is None:
 1733             return
 1734 
 1735         self.undoqueue.undo()
 1736         self.statusbar.set_status(_('%s undone') % action[1])
 1737         self.__file_autosave()
 1738 
 1739 
 1740 
 1741 class Preferences(dialog.Utility):
 1742     "A preference dialog"
 1743 
 1744     def __init__(self, parent, cfg=None):
 1745         dialog.Utility.__init__(self, parent, _('Preferences'))
 1746         self.add_button(Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE)
 1747         self.set_default_response(Gtk.ResponseType.CLOSE)
 1748         self.config = cfg
 1749         self.set_modal(False)
 1750 
 1751         self.notebook = ui.Notebook()
 1752         self.vbox.pack_start(self.notebook, True, True, 0)
 1753 
 1754         self.page_general = self.notebook.create_page(_('General'))
 1755         self.__init_section_files(self.page_general)
 1756         self.__init_section_password(self.page_general)
 1757 
 1758         self.page_interface = self.notebook.create_page(_('Interface'))
 1759         self.__init_section_doubleclick(self.page_interface)
 1760         self.__init_section_toolbar(self.page_interface)
 1761 
 1762         self.page_gotocmd = self.notebook.create_page(_('Goto Commands'))
 1763         self.notebook.get_tab_label(self.page_gotocmd).set_hexpand(True)
 1764         self.__init_section_gotocmd(self.page_gotocmd)
 1765 
 1766         self.connect("response", lambda w,d: self.destroy())
 1767 
 1768 
 1769     def __init_section_doubleclick(self, page):
 1770         "Sets up the doubleclick section"
 1771 
 1772         self.section_doubleclick = page.add_section(_('Doubleclick Action'))
 1773 
 1774         # radio-button for go to
 1775         self.radio_doubleclick_goto = Gtk.RadioButton.new_with_label_from_widget(None, _('Go to account, if possible'))
 1776         self.radio_doubleclick_goto.connect("toggled", lambda w: w.get_active() and self.config.set_string("behavior-doubleclick", "goto"))
 1777 
 1778         self.radio_doubleclick_goto.set_tooltip_text(_('Go to the account (open in external application) on doubleclick, if required data is filled in'))
 1779         self.section_doubleclick.append_widget(None, self.radio_doubleclick_goto)
 1780 
 1781         # radio-button for edit
 1782         self.radio_doubleclick_edit = Gtk.RadioButton.new_with_label_from_widget(self.radio_doubleclick_goto, label=_('Edit account'))
 1783         self.radio_doubleclick_edit.connect("toggled", lambda w: w.get_active() and self.config.set_string("behavior-doubleclick", "edit"))
 1784 
 1785         self.radio_doubleclick_edit.set_tooltip_text(_('Edit the account on doubleclick'))
 1786         self.section_doubleclick.append_widget(None, self.radio_doubleclick_edit)
 1787 
 1788         # radio-button for copy
 1789         self.radio_doubleclick_copy = Gtk.RadioButton.new_with_label_from_widget(self.radio_doubleclick_goto, label=_('Copy password to clipboard'))
 1790         self.radio_doubleclick_copy.connect("toggled", lambda w: w.get_active() and self.config.set_string("behavior-doubleclick", "copy"))
 1791 
 1792         self.radio_doubleclick_copy.set_tooltip_text(_('Copy the account password to clipboard on doubleclick'))
 1793         self.section_doubleclick.append_widget(None, self.radio_doubleclick_copy)
 1794 
 1795         {"goto":self.radio_doubleclick_goto,
 1796          "edit":self.radio_doubleclick_edit,
 1797          "copy":self.radio_doubleclick_copy}[self.config.get_string("behavior-doubleclick")].set_active(True)
 1798 
 1799 
 1800     def __init_section_files(self, page):
 1801         "Sets up the files section"
 1802 
 1803         self.section_files = page.add_section(_('Files'))
 1804 
 1805         # checkbutton and file button for autoloading a file
 1806         self.check_autoload = Gtk.CheckButton(label=_('Open file on startup:'))
 1807         self.config.bind("file-autoload", self.check_autoload, "active", Gio.SettingsBindFlags.DEFAULT)
 1808 
 1809         self.check_autoload.connect("toggled", lambda w: self.button_autoload_file.set_sensitive(w.get_active()))
 1810         self.check_autoload.set_tooltip_text(_('When enabled, this file will be opened when the program is started'))
 1811 
 1812         self.button_autoload_file = Gtk.FileChooserButton(title=_('Select File to Automatically Open'))
 1813         if self.config.get_boolean("file-autoload"):
 1814             self.button_autoload_file.set_filename(self.config.get_string("file-autoload-file"))
 1815         self.button_autoload_file.connect('file-set', lambda w: self.config.set_string("file-autoload-file", w.get_filename()))
 1816         self.config.connect("changed::autoload-file", lambda w, fname: self.button_autoload_file.set_filename(w.get_string(fname)))
 1817         self.button_autoload_file.set_sensitive(self.check_autoload.get_active())
 1818 
 1819         filter = Gtk.FileFilter()
 1820         filter.set_name(_('Revelation files'))
 1821         filter.add_mime_type("application/x-revelation")
 1822         self.button_autoload_file.add_filter(filter)
 1823 
 1824         filter = Gtk.FileFilter()
 1825         filter.set_name(_('All files'))
 1826         filter.add_pattern("*")
 1827         self.button_autoload_file.add_filter(filter)
 1828 
 1829         eventbox = ui.EventBox(self.button_autoload_file)
 1830         eventbox.set_tooltip_text(_('File to open when Revelation is started'))
 1831 
 1832         hbox = ui.HBox()
 1833         hbox.pack_start(self.check_autoload, False, False, 0)
 1834         hbox.pack_start(eventbox, True, True, 0)
 1835         self.section_files.append_widget(None, hbox)
 1836 
 1837         # check-button for autosave
 1838         self.check_autosave = Gtk.CheckButton(label=_('Automatically save data when changed'))
 1839         self.config.bind("file-autosave", self.check_autosave, "active", Gio.SettingsBindFlags.DEFAULT)
 1840 
 1841         self.check_autosave.set_tooltip_text(_('Automatically save the data file when an entry is added, modified or removed'))
 1842         self.section_files.append_widget(None, self.check_autosave)
 1843 
 1844         # autolock file
 1845         self.check_autolock = Gtk.CheckButton(label=_('Lock file when inactive for'))
 1846         self.config.bind("file-autolock", self.check_autolock, "active", Gio.SettingsBindFlags.DEFAULT)
 1847         self.check_autolock.connect("toggled", lambda w: self.spin_autolock_timeout.set_sensitive(w.get_active()))
 1848         self.check_autolock.set_tooltip_text(_('Automatically lock the data file after a period of inactivity'))
 1849 
 1850         self.spin_autolock_timeout = ui.SpinEntry()
 1851         self.spin_autolock_timeout.set_range(1, 120)
 1852         self.spin_autolock_timeout.set_sensitive(self.check_autolock.get_active())
 1853         self.config.bind("file-autolock-timeout", self.spin_autolock_timeout, "value", Gio.SettingsBindFlags.DEFAULT)
 1854         self.spin_autolock_timeout.set_tooltip_text(_('The period of inactivity before locking the file, in minutes'))
 1855 
 1856         hbox = ui.HBox()
 1857         hbox.set_spacing(3)
 1858         hbox.pack_start(self.check_autolock, False, False, 0)
 1859         hbox.pack_start(self.spin_autolock_timeout, False, False, 0)
 1860         hbox.pack_start(ui.Label(_('minutes')), True, True, 0)
 1861         self.section_files.append_widget(None, hbox)
 1862 
 1863 
 1864     def __init_section_gotocmd(self, page):
 1865         "Sets up the goto command section"
 1866 
 1867         self.section_gotocmd = page.add_section(_('Goto Commands'))
 1868 
 1869         for entrytype in entry.ENTRYLIST:
 1870             if entrytype == entry.FolderEntry:
 1871                 continue
 1872 
 1873             e = entrytype()
 1874 
 1875             widget = ui.Entry()
 1876             self.config.bind("launcher-"+e.id, widget, "text", Gio.SettingsBindFlags.DEFAULT)
 1877 
 1878             tooltip = _('Goto command for %s accounts. The following expansion variables can be used:') % e.typename + "\n\n"
 1879 
 1880             for field in e.fields:
 1881                 tooltip += "%%%s: %s\n" % ( field.symbol, field.name )
 1882 
 1883             tooltip += "\n"
 1884             tooltip += _('%%: a "%" sign') + "\n"
 1885             tooltip += _('%?x: optional expansion variable') + "\n"
 1886             tooltip += _('%(...%): optional substring expansion')
 1887 
 1888             widget.set_tooltip_text(tooltip)
 1889             self.section_gotocmd.append_widget(e.typename, widget)
 1890 
 1891 
 1892     def __init_section_password(self, page):
 1893         "Sets up the password section"
 1894 
 1895         self.section_password = page.add_section(_('Passwords'))
 1896 
 1897         # show passwords checkbutton
 1898         self.check_show_passwords = Gtk.CheckButton(label=_('Display passwords and other secrets'))
 1899         self.config.bind("view-passwords", self.check_show_passwords, "active", Gio.SettingsBindFlags.DEFAULT)
 1900 
 1901         self.check_show_passwords.set_tooltip_text(_('Display passwords and other secrets, such as PIN codes (otherwise, hide with ******)'))
 1902         self.section_password.append_widget(None, self.check_show_passwords)
 1903 
 1904         # chain username checkbutton
 1905         self.check_chain_username = Gtk.CheckButton(label=_('Also copy username when copying password'))
 1906         self.config.bind("clipboard-chain-username", self.check_chain_username, "active", Gio.SettingsBindFlags.DEFAULT)
 1907 
 1908         self.check_chain_username.set_tooltip_text(_('When the password is copied to clipboard, put the username before the password as a clipboard "chain"'))
 1909         self.section_password.append_widget(None, self.check_chain_username)
 1910 
 1911         # use punctuation chars checkbutton
 1912         self.check_punctuation_chars = Gtk.CheckButton(label=_('Use punctuation characters for passwords'))
 1913         self.config.bind("passwordgen-punctuation", self.check_punctuation_chars, "active", Gio.SettingsBindFlags.DEFAULT)
 1914 
 1915         self.check_punctuation_chars.set_tooltip_text(_('When passwords are generated, use punctuation characters like %, =, { or .'))
 1916         self.section_password.append_widget(None, self.check_punctuation_chars)
 1917 
 1918         # password length spinbutton
 1919         self.spin_pwlen = ui.SpinEntry()
 1920         self.spin_pwlen.set_range(4, 32)
 1921         self.config.bind("passwordgen-length", self.spin_pwlen, "value", Gio.SettingsBindFlags.DEFAULT)
 1922 
 1923         self.spin_pwlen.set_tooltip_text(_('The number of characters in generated passwords - 8 or more are recommended'))
 1924         self.section_password.append_widget(_('Length of generated passwords'), self.spin_pwlen)
 1925 
 1926 
 1927     def __init_section_toolbar(self, page):
 1928         "Sets up the toolbar section"
 1929 
 1930         self.section_toolbar = page.add_section(_('Toolbar Style'))
 1931 
 1932         # radio-button for desktop default
 1933         self.radio_toolbar_desktop = Gtk.RadioButton.new_with_label_from_widget(None, _('Use desktop default'))
 1934         self.radio_toolbar_desktop.connect("toggled", lambda w: w.get_active() and self.config.set_string("view-toolbar-style", "desktop"))
 1935 
 1936         self.radio_toolbar_desktop.set_tooltip_text(_('Show toolbar items with default style'))
 1937         self.section_toolbar.append_widget(None, self.radio_toolbar_desktop)
 1938 
 1939         # radio-button for icons and text
 1940         self.radio_toolbar_both = Gtk.RadioButton.new_with_label_from_widget(self.radio_toolbar_desktop, _('Show icons and text'))
 1941         self.radio_toolbar_both.connect("toggled", lambda w: w.get_active() and self.config.set_string("view-toolbar-style", "both"))
 1942 
 1943         self.radio_toolbar_both.set_tooltip_text(_('Show toolbar items with both icons and text'))
 1944         self.section_toolbar.append_widget(None, self.radio_toolbar_both)
 1945 
 1946         # radio-button for icons and important text
 1947         self.radio_toolbar_bothhoriz = Gtk.RadioButton.new_with_label_from_widget(self.radio_toolbar_desktop, _('Show icons and important text'))
 1948         self.radio_toolbar_bothhoriz.connect("toggled", lambda w: w.get_active() and self.config.set_string("view-toolbar-style", "both-horiz"))
 1949 
 1950         self.radio_toolbar_bothhoriz.set_tooltip_text(_('Show toolbar items with text beside important icons'))
 1951         self.section_toolbar.append_widget(None, self.radio_toolbar_bothhoriz)
 1952 
 1953         # radio-button for icons only
 1954         self.radio_toolbar_icons = Gtk.RadioButton.new_with_label_from_widget(self.radio_toolbar_desktop, _('Show icons only'))
 1955         self.radio_toolbar_icons.connect("toggled", lambda w: w.get_active() and self.config.set_string("view-toolbar-style", "icons"))
 1956 
 1957         self.radio_toolbar_icons.set_tooltip_text(_('Show toolbar items with icons only'))
 1958         self.section_toolbar.append_widget(None, self.radio_toolbar_icons)
 1959 
 1960         # radio-button for text only
 1961         self.radio_toolbar_text = Gtk.RadioButton.new_with_label_from_widget(self.radio_toolbar_desktop, _('Show text only'))
 1962         self.radio_toolbar_text.connect("toggled", lambda w: w.get_active() and self.config.set_string("view-toolbar-style", "text"))
 1963 
 1964         self.radio_toolbar_text.set_tooltip_text(_('Show toolbar items with text only'))
 1965         self.section_toolbar.append_widget(None, self.radio_toolbar_text)
 1966 
 1967         {"desktop":    self.radio_toolbar_desktop,
 1968          "both":       self.radio_toolbar_both,
 1969          "both-horiz": self.radio_toolbar_bothhoriz,
 1970          "icons":      self.radio_toolbar_icons,
 1971          "text":       self.radio_toolbar_text
 1972         }[self.config.get_string("view-toolbar-style")].set_active(True)
 1973 
 1974 
 1975     def run(self):
 1976         "Runs the preference dialog"
 1977 
 1978         self.show_all()
 1979 
 1980         # for some reason, Gtk crashes on close-by-escape unless we do this
 1981         self.get_widget_for_response(Gtk.ResponseType.CLOSE).grab_focus()
 1982         self.notebook.grab_focus()
 1983 
 1984 
 1985 
 1986 if __name__ == "__main__":
 1987     app = Revelation()
 1988     app.set_flags(Gio.ApplicationFlags.NON_UNIQUE)
 1989     app.run()
 1990