"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