"Fossies" - the Fresh Open Source Software Archive

Member "revelation-0.5.4/src/lib/dialog.py" (4 Oct 2020, 37727 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 "dialog.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 #
    2 # Revelation - a password manager for GNOME 2
    3 # http://oss.codepoet.no/revelation/
    4 # $Id$
    5 #
    6 # Module with dialogs
    7 #
    8 #
    9 # Copyright (c) 2003-2006 Erik Grinaker
   10 #
   11 # This program is free software; you can redistribute it and/or
   12 # modify it under the terms of the GNU General Public License
   13 # as published by the Free Software Foundation; either version 2
   14 # of the License, or (at your option) any later version.
   15 #
   16 # This program is distributed in the hope that it will be useful,
   17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
   18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   19 # GNU General Public License for more details.
   20 #
   21 # You should have received a copy of the GNU General Public License
   22 # along with this program; if not, write to the Free Software
   23 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
   24 #
   25 
   26 from . import config, datahandler, entry, io, ui, util
   27 
   28 import gi
   29 gi.require_version('Gtk', '3.0')
   30 from gi.repository import GObject, Gtk, Gio, Gdk
   31 import gettext, urllib.parse
   32 
   33 _ = gettext.gettext
   34 
   35 
   36 EVENT_FILTER        = None
   37 UNIQUE_DIALOGS      = {}
   38 
   39 
   40 Gtk.rc_parse_string("""
   41     style "hig" {
   42         GtkDialog::content-area-border  = 0
   43         GtkDialog::action-area-border   = 0
   44     }
   45 
   46     class "GtkDialog" style "hig"
   47 """)
   48 
   49 
   50 ##### EXCEPTIONS #####
   51 
   52 class CancelError(Exception):
   53     "Exception for dialog cancellations"
   54     pass
   55 
   56 
   57 
   58 ##### BASE DIALOGS #####
   59 
   60 class Dialog(Gtk.Dialog):
   61     "Base class for dialogs"
   62 
   63     def __init__(self, parent, title):
   64         Gtk.Dialog.__init__(
   65             self, title, parent,
   66             Gtk.DialogFlags.DESTROY_WITH_PARENT
   67         )
   68 
   69         self.set_border_width(12)
   70         self.vbox.set_spacing(12)
   71         self.set_resizable(False)
   72         self.set_modal(True)
   73 
   74         self.connect("key-press-event", self.__cb_keypress)
   75 
   76 
   77     def __cb_keypress(self, widget, data):
   78         "Callback for handling key presses"
   79 
   80         # close the dialog on escape
   81         if data.keyval == Gdk.KEY_Escape:
   82             self.response(Gtk.ResponseType.CLOSE)
   83             return True
   84 
   85 
   86     def run(self):
   87         "Runs the dialog"
   88 
   89         self.show_all()
   90 
   91         while True:
   92             response = Gtk.Dialog.run(self)
   93 
   94             if response == Gtk.ResponseType.NONE:
   95                 continue
   96 
   97             return response
   98 
   99 
  100 
  101 class Popup(Gtk.Window):
  102     "Base class for popup (frameless) dialogs"
  103 
  104     def __init__(self, widget = None):
  105         Gtk.Window.__init__(self)
  106         self.set_decorated(False)
  107 
  108         self.border = Gtk.Frame()
  109         self.border.set_shadow_type(Gtk.ShadowType.OUT)
  110         Gtk.Window.add(self, self.border)
  111 
  112         if widget != None:
  113             self.add(widget)
  114 
  115         self.connect("key-press-event", self.__cb_keypress)
  116 
  117 
  118     def __cb_keypress(self, widget, data):
  119         "Callback for key presses"
  120 
  121         if data.keyval == Gdk.KEY_Escape:
  122             self.close()
  123 
  124 
  125     def add(self, widget):
  126         "Adds a widget to the window"
  127 
  128         self.border.add(widget)
  129 
  130 
  131     def close(self):
  132         "Closes the dialog"
  133 
  134         self.hide()
  135         self.emit("closed")
  136         self.destroy()
  137 
  138 
  139     def realize(self):
  140         "Realizes the popup and displays children"
  141 
  142         Gtk.Window.realize(self)
  143 
  144         for child in self.get_children():
  145             child.show_all()
  146 
  147 
  148     def show(self, x = None, y = None):
  149         "Show the dialog"
  150 
  151         if x != None and y != None:
  152             self.move(x, y)
  153 
  154         self.show_all()
  155 
  156 
  157 GObject.signal_new("closed", Popup, GObject.SignalFlags.ACTION,
  158                    GObject.TYPE_BOOLEAN, ())
  159 
  160 
  161 
  162 class Utility(Dialog):
  163     "A utility dialog"
  164 
  165     def __init__(self, parent, title):
  166         Dialog.__init__(self, parent, title)
  167 
  168         self.set_border_width(12)
  169         self.vbox.set_spacing(18)
  170 
  171         self.sizegroup = Gtk.SizeGroup(mode=Gtk.SizeGroupMode.HORIZONTAL)
  172 
  173 
  174     def add_section(self, title, description = None):
  175         "Adds an input section to the dialog"
  176 
  177         section = ui.InputSection(title, description, self.sizegroup)
  178         self.vbox.pack_start(section, True, True, 0)
  179 
  180         return section
  181 
  182 
  183 
  184 class Message(Dialog):
  185     "A message dialog"
  186 
  187     def __init__(self, parent, title, text, stockimage):
  188         Dialog.__init__(self, parent, "")
  189 
  190         # hbox with image and contents
  191         hbox = ui.HBox()
  192         hbox.set_spacing(12)
  193         self.vbox.pack_start(hbox, True, True, 0)
  194         self.vbox.set_spacing(24)
  195 
  196         # set up image
  197         if stockimage != None:
  198             image = ui.Image(stockimage, Gtk.IconSize.DIALOG)
  199             image.set_alignment(0.5, 0)
  200             hbox.pack_start(image, False, False, 0)
  201 
  202         # set up message
  203         self.contents = ui.VBox()
  204         self.contents.set_spacing(10)
  205         hbox.pack_start(self.contents, True, True, 0)
  206 
  207         label = ui.Label("<span size=\"larger\" weight=\"bold\">%s</span>\n\n%s" % ( util.escape_markup(title), text))
  208         label.set_alignment(0, 0)
  209         label.set_selectable(True)
  210         label.set_max_width_chars(45)
  211         self.contents.pack_start(label, True, True, 0)
  212 
  213 
  214     def run(self):
  215         "Displays the dialog"
  216 
  217         self.show_all()
  218         response = Dialog.run(self)
  219         self.destroy()
  220 
  221         return response
  222 
  223 
  224 
  225 class Error(Message):
  226     "Displays an error message"
  227 
  228     def __init__(self, parent, title, text):
  229         Message.__init__(self, parent, title, text, "dialog-error")
  230         self.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK)
  231 
  232 
  233 
  234 class Info(Message):
  235     "Displays an info message"
  236 
  237     def __init__(self, parent, title, text):
  238         Message.__init__(self, parent, title, text, "dialog-information")
  239         self.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK)
  240 
  241 
  242 
  243 class Question(Message):
  244     "Displays a question"
  245 
  246     def __init__(self, parent, title, text):
  247         Message.__init__(self, parent, title, text, "dialog-question")
  248         self.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK)
  249 
  250 
  251 
  252 class Warning(Message):
  253     "Displays a warning message"
  254 
  255     def __init__(self, parent, title, text):
  256         Message.__init__(self, parent, title, text, "dialog-warning")
  257 
  258 
  259 
  260 ##### QUESTION DIALOGS #####
  261 
  262 class FileChanged(Warning):
  263     "Notifies about changed file"
  264 
  265     def __init__(self, parent, filename):
  266         Warning.__init__(
  267             self, parent, _('File has changed'), _('The current file \'%s\' has changed. Do you want to reload it?') % filename
  268         )
  269 
  270         self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
  271         self.add_button(ui.STOCK_RELOAD, Gtk.ResponseType.OK)
  272         self.set_default_response(Gtk.ResponseType.OK)
  273 
  274 
  275     def run(self):
  276         "Displays the dialog"
  277 
  278         if Warning.run(self) == Gtk.ResponseType.OK:
  279             return True
  280 
  281         else:
  282             raise CancelError
  283 
  284 
  285 class FileChanges(Warning):
  286     "Asks to save changes before proceeding"
  287 
  288     def __init__(self, parent, title, text):
  289         Warning.__init__(self, parent, title, text)
  290 
  291         self.add_button(Gtk.STOCK_DISCARD, Gtk.ResponseType.ACCEPT)
  292         self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
  293         self.add_button(Gtk.STOCK_SAVE, Gtk.ResponseType.OK)
  294         self.set_default_response(Gtk.ResponseType.OK)
  295 
  296 
  297     def run(self):
  298         "Displays the dialog"
  299 
  300         response = Warning.run(self)
  301 
  302         if response == Gtk.ResponseType.OK:
  303             return True
  304 
  305         elif response == Gtk.ResponseType.ACCEPT:
  306             return False
  307 
  308         elif response in ( Gtk.ResponseType.CANCEL, Gtk.ResponseType.CLOSE ):
  309             raise CancelError
  310 
  311 
  312 
  313 class FileChangesNew(FileChanges):
  314     "Asks the user to save changes when creating a new file"
  315 
  316     def __init__(self, parent):
  317         FileChanges.__init__(
  318             self, parent, _('Save changes to current file?'),
  319             _('You have made changes which have not been saved. If you create a new file without saving then these changes will be lost.')
  320         )
  321 
  322 
  323 
  324 class FileChangesOpen(FileChanges):
  325     "Asks the user to save changes when opening a different file"
  326 
  327     def __init__(self, parent):
  328         FileChanges.__init__(
  329             self, parent, _('Save changes before opening?'),
  330             _('You have made changes which have not been saved. If you open a different file without saving then these changes will be lost.')
  331         )
  332 
  333 
  334 
  335 class FileChangesQuit(FileChanges):
  336     "Asks the user to save changes when quitting"
  337 
  338     def __init__(self, parent):
  339         FileChanges.__init__(
  340             self, parent, _('Save changes before quitting?'),
  341             _('You have made changes which have not been saved. If you quit without saving, then these changes will be lost.')
  342         )
  343 
  344 class FileChangesClose(FileChanges):
  345     "Asks the user to save changes when closing"
  346 
  347     def __init__(self, parent):
  348         FileChanges.__init__(
  349             self, parent, _('Save changes before closing?'),
  350             _('You have made changes which have not been saved. If you close without saving, then these changes will be lost.')
  351         )
  352 
  353 
  354 class FileReplace(Warning):
  355     "Asks for confirmation when replacing a file"
  356 
  357     def __init__(self, parent, file):
  358         Warning.__init__(
  359             self, parent, _('Replace existing file?'),
  360             _('The file \'%s\' already exists - do you wish to replace this file?') % file
  361         )
  362 
  363         self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
  364         self.add_button(ui.STOCK_REPLACE, Gtk.ResponseType.OK)
  365         self.set_default_response(Gtk.ResponseType.CANCEL)
  366 
  367     def run(self):
  368         "Displays the dialog"
  369 
  370         if Warning.run(self) == Gtk.ResponseType.OK:
  371             return True
  372 
  373         else:
  374             raise CancelError
  375 
  376 
  377 
  378 class FileSaveInsecure(Warning):
  379     "Asks for confirmation when exporting to insecure file"
  380 
  381     def __init__(self, parent):
  382         Warning.__init__(
  383             self, parent, _('Save to insecure file?'),
  384             _('You have chosen to save your passwords to an insecure (unencrypted) file format - if anyone has access to this file, they will be able to see your passwords.'),
  385         )
  386 
  387         self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
  388         self.add_button(Gtk.STOCK_SAVE, Gtk.ResponseType.OK)
  389         self.set_default_response(Gtk.ResponseType.CANCEL)
  390 
  391 
  392     def run(self):
  393         "Runs the dialog"
  394 
  395         if Warning.run(self) == Gtk.ResponseType.OK:
  396             return True
  397 
  398         else:
  399             raise CancelError
  400 
  401 
  402 
  403 ##### FILE SELECTION DIALOGS #####
  404 
  405 class FileSelector(Gtk.FileChooserNative):
  406     "A normal file selector"
  407 
  408     def __init__(self, parent, title=None,
  409                  action=Gtk.FileChooserAction.OPEN):
  410 
  411         Gtk.FileChooserNative.__init__(self, title=title, action=action)
  412         self.set_transient_for(parent)
  413         self.set_local_only(False)
  414         self.inputsection = None
  415 
  416 
  417     def add_widget(self, title, widget):
  418         "Adds a widget to the file selection"
  419 
  420         if self.inputsection is None:
  421             self.inputsection = ui.InputSection()
  422             self.set_extra_widget(self.inputsection)
  423 
  424         self.inputsection.append_widget(title, widget)
  425 
  426 
  427     def get_filename(self):
  428         "Returns the file URI"
  429 
  430         uri = self.get_uri()
  431 
  432         if uri is None:
  433             return None
  434 
  435         else:
  436             return io.file_normpath(urllib.parse.unquote(uri))
  437 
  438 
  439     def run(self):
  440         "Displays and runs the file selector, returns the filename"
  441 
  442         response = Gtk.FileChooserNative.run(self)
  443         filename = self.get_filename()
  444         self.destroy()
  445 
  446         if response == Gtk.ResponseType.ACCEPT:
  447             return filename
  448         else:
  449             raise CancelError
  450 
  451 
  452 
  453 class ExportFileSelector(FileSelector):
  454     "A file selector for exporting files"
  455 
  456     def __init__(self, parent):
  457         FileSelector.__init__(
  458             self, parent, _('Select File to Export to'),
  459             Gtk.FileChooserAction.SAVE
  460         )
  461 
  462         # set up filetype dropdown
  463         self.dropdown = ui.DropDown()
  464         self.add_widget(_('Filetype'), self.dropdown)
  465 
  466         for handler in datahandler.get_export_handlers():
  467             self.dropdown.append_item(handler.name, None, handler)
  468 
  469         for index in range(self.dropdown.get_num_items()):
  470             if self.dropdown.get_item(index)[2] == datahandler.RevelationXML:
  471                 self.dropdown.set_active(index)
  472 
  473 
  474     def run(self):
  475         "Displays the dialog"
  476 
  477         self.inputsection.show_all()
  478 
  479         if Gtk.FileChooserNative.run(self) == Gtk.ResponseType.ACCEPT:
  480             filename = self.get_filename()
  481             handler = self.dropdown.get_active_item()[2]
  482             self.destroy()
  483 
  484             return filename, handler
  485         else:
  486             self.destroy()
  487             raise CancelError
  488 
  489 
  490 
  491 class ImportFileSelector(FileSelector):
  492     "A file selector for importing files"
  493 
  494     def __init__(self, parent):
  495         FileSelector.__init__(
  496             self, parent, _('Select File to Import'),
  497             Gtk.FileChooserAction.OPEN
  498         )
  499 
  500         # set up filetype dropdown
  501         self.dropdown = ui.DropDown()
  502         self.add_widget(_('Filetype'), self.dropdown)
  503 
  504         self.dropdown.append_item(_('Automatically detect'))
  505 
  506         for handler in datahandler.get_import_handlers():
  507             self.dropdown.append_item(handler.name, None, handler)
  508 
  509 
  510     def run(self):
  511         "Displays the dialog"
  512 
  513         self.inputsection.show_all()
  514 
  515         if Gtk.FileChooserNative.run(self) == Gtk.ResponseType.ACCEPT:
  516             filename = self.get_filename()
  517             handler = self.dropdown.get_active_item()[2]
  518             self.destroy()
  519 
  520             return filename, handler
  521         else:
  522             self.destroy()
  523             raise CancelError
  524 
  525 
  526 
  527 class OpenFileSelector(FileSelector):
  528     "A file selector for opening files"
  529 
  530     def __init__(self, parent):
  531         FileSelector.__init__(
  532             self, parent, _('Select File to Open'),
  533             Gtk.FileChooserAction.OPEN
  534         )
  535 
  536         filter = Gtk.FileFilter()
  537         filter.set_name(_('Revelation files'))
  538         filter.add_mime_type("application/x-revelation")
  539         self.add_filter(filter)
  540 
  541         filter = Gtk.FileFilter()
  542         filter.set_name(_('All files'))
  543         filter.add_pattern("*")
  544         self.add_filter(filter)
  545 
  546 
  547 
  548 class SaveFileSelector(FileSelector):
  549     "A file selector for saving files"
  550 
  551     def __init__(self, parent):
  552         FileSelector.__init__(
  553             self, parent, _('Select File to Save to'),
  554             Gtk.FileChooserAction.SAVE
  555         )
  556 
  557         filter = Gtk.FileFilter()
  558         filter.set_name(_('Revelation files'))
  559         filter.add_mime_type("application/x-revelation")
  560         self.add_filter(filter)
  561 
  562         filter = Gtk.FileFilter()
  563         filter.set_name(_('All files'))
  564         filter.add_pattern("*")
  565         self.add_filter(filter)
  566 
  567         self.set_do_overwrite_confirmation(True)
  568         self.connect("confirm-overwrite", self.__cb_confirm_overwrite)
  569 
  570 
  571     def __cb_confirm_overwrite(self, widget, data = None):
  572         "Handles confirm-overwrite signals"
  573 
  574         try:
  575             FileReplace(self, io.file_normpath(self.get_uri())).run()
  576 
  577         except CancelError:
  578             return Gtk.FileChooserConfirmation.SELECT_AGAIN
  579 
  580         else:
  581             return Gtk.FileChooserConfirmation.ACCEPT_FILENAME
  582 
  583 
  584 
  585 ##### PASSWORD DIALOGS #####
  586 
  587 class Password(Message):
  588     "A base dialog for asking for passwords"
  589 
  590     def __init__(self, parent, title, text, stock = Gtk.STOCK_OK):
  591         Message.__init__(self, parent, title, text, "dialog-password")
  592 
  593         self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
  594         self.add_button(stock, Gtk.ResponseType.OK)
  595         self.set_default_response(Gtk.ResponseType.OK)
  596 
  597         self.entries = []
  598 
  599         self.sect_passwords = ui.InputSection()
  600         self.contents.pack_start(self.sect_passwords, True, True, 0)
  601 
  602 
  603     def add_entry(self, name, entry = None):
  604         "Adds a password entry to the dialog"
  605 
  606         if entry is None:
  607             entry = ui.Entry()
  608             entry.set_visibility(False)
  609 
  610         self.sect_passwords.append_widget(name, entry)
  611         self.entries.append(entry)
  612 
  613         return entry
  614 
  615 
  616     def run(self):
  617         "Displays the dialog"
  618 
  619         self.show_all()
  620 
  621         if len(self.entries) > 0:
  622             self.entries[0].grab_focus()
  623 
  624         return Gtk.Dialog.run(self)
  625 
  626 
  627 
  628 class PasswordChange(Password):
  629     "A dialog for changing the password"
  630 
  631     def __init__(self, parent, password = None):
  632         Password.__init__(
  633             self, parent, _('Enter new password'),
  634             _('Enter a new password for the current data file. The file must be saved before the new password is applied.'),
  635             ui.STOCK_PASSWORD_CHANGE
  636         )
  637 
  638         self.password = password
  639 
  640         if password is not None:
  641             self.entry_current = self.add_entry(_('Current password'))
  642 
  643         self.entry_new      = self.add_entry(_('New password'), ui.PasswordEntry())
  644         self.entry_confirm  = self.add_entry(_('Confirm password'))
  645         self.entry_confirm.autocheck = False
  646 
  647 
  648     def run(self):
  649         "Displays the dialog"
  650 
  651         while True:
  652             if Password.run(self) != Gtk.ResponseType.OK:
  653                 self.destroy()
  654                 raise CancelError
  655 
  656             elif self.password is not None and self.entry_current.get_text() != self.password:
  657                 Error(self, _('Incorrect password'), _('The password you entered as the current file password is incorrect.')).run()
  658 
  659             elif self.entry_new.get_text() != self.entry_confirm.get_text():
  660                 Error(self, _('Passwords don\'t match'), _('The password and password confirmation you entered does not match.')).run()
  661 
  662             else:
  663                 password = self.entry_new.get_text()
  664 
  665                 try:
  666                     util.check_password(password)
  667 
  668                 except ValueError as res:
  669                     WarnInsecure = Warning(
  670                         self, _('Use insecure password?'),
  671                         _('The password you entered is not secure; %s. Are you sure you want to use it?') % str(res).lower()
  672                     )
  673 
  674                     WarnInsecure.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
  675                     WarnInsecure.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK )
  676                     WarnInsecure.set_default_response(Gtk.ResponseType.CANCEL)
  677 
  678                     response = WarnInsecure.run()
  679                     if response != Gtk.ResponseType.OK:
  680                         continue
  681 
  682                 self.destroy()
  683                 return password
  684 
  685 
  686 
  687 class PasswordLock(Password):
  688     "Asks for a password when the file is locked"
  689 
  690     def __init__(self, parent, password):
  691         Password.__init__(
  692             self, parent, _('Enter password to unlock file'),
  693             _('The current file has been locked, please enter the file password to unlock it.'),
  694             ui.STOCK_UNLOCK
  695         )
  696 
  697         self.get_widget_for_response(Gtk.ResponseType.CANCEL).set_label(Gtk.STOCK_QUIT)
  698         self.set_default_response(Gtk.ResponseType.OK)
  699 
  700         self.password = password
  701         self.entry_password = self.add_entry(_('Password'))
  702 
  703 
  704     def run(self):
  705         "Displays the dialog"
  706 
  707         while True:
  708             try:
  709                 response = Password.run(self)
  710 
  711                 if response == Gtk.ResponseType.CANCEL:
  712                     raise CancelError
  713 
  714                 elif response != Gtk.ResponseType.OK:
  715                     continue
  716 
  717                 elif self.entry_password.get_text() == self.password:
  718                     break
  719 
  720                 else:
  721                     Error(self, _('Incorrect password'), _('The password you entered was not correct, please try again.')).run()
  722 
  723             except CancelError:
  724                 if self.get_widget_for_response(Gtk.ResponseType.CANCEL).get_property("sensitive") == True:
  725                     self.destroy()
  726                     raise
  727 
  728                 else:
  729                     continue
  730 
  731         self.destroy()
  732 
  733 
  734 
  735 class PasswordOpen(Password):
  736     "Password dialog for opening files"
  737 
  738     def __init__(self, parent, filename):
  739         Password.__init__(
  740             self, parent, _('Enter file password'),
  741             _('The file \'%s\' is encrypted. Please enter the file password to open it.') % filename,
  742             Gtk.STOCK_OPEN
  743         )
  744 
  745         self.set_default_response(Gtk.ResponseType.OK)
  746         self.entry_password = self.add_entry(_('Password'))
  747 
  748 
  749     def run(self):
  750         "Displays the dialog"
  751 
  752         response = Password.run(self)
  753         password = self.entry_password.get_text()
  754         self.destroy()
  755 
  756         if response == Gtk.ResponseType.OK:
  757             return password
  758 
  759         else:
  760             raise CancelError
  761 
  762 
  763 
  764 class PasswordSave(Password):
  765     "Password dialog for saving data"
  766 
  767     def __init__(self, parent, filename):
  768         Password.__init__(
  769             self, parent, _('Enter password for file'),
  770             _('Please enter a password for the file \'%s\'. You will need this password to open the file at a later time.') % filename,
  771             Gtk.STOCK_SAVE
  772         )
  773 
  774         self.entry_new      = self.add_entry(_('New password'), ui.PasswordEntry())
  775         self.entry_confirm  = self.add_entry(_('Confirm password'))
  776         self.entry_confirm.autocheck = False
  777 
  778 
  779     def run(self):
  780         "Displays the dialog"
  781 
  782         while True:
  783             if Password.run(self) != Gtk.ResponseType.OK:
  784                 self.destroy()
  785                 raise CancelError
  786 
  787             elif self.entry_new.get_text() != self.entry_confirm.get_text():
  788                 Error(self, _('Passwords don\'t match'), _('The passwords you entered does not match.')).run()
  789 
  790             elif len(self.entry_new.get_text()) == 0:
  791                 Error(self, _('No password entered'), _('You must enter a password for the new data file.')).run()
  792 
  793             else:
  794                 password = self.entry_new.get_text()
  795 
  796                 try:
  797                     util.check_password(password)
  798 
  799                 except ValueError as res:
  800                     res = str(res).lower()
  801 
  802                     WarnInsecure = Warning(
  803                         self, _('Use insecure password?'),
  804                         _('The password you entered is not secure; %s. Are you sure you want to use it?') % res
  805                     )
  806 
  807                     WarnInsecure.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
  808                     WarnInsecure.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK )
  809                     WarnInsecure.set_default_response(Gtk.ResponseType.CANCEL)
  810 
  811                     response = WarnInsecure.run()
  812                     if response != Gtk.ResponseType.OK:
  813                         continue
  814 
  815                 self.destroy()
  816                 return password
  817 
  818 
  819 
  820 ##### ENTRY DIALOGS #####
  821 
  822 class EntryEdit(Utility):
  823     "A dialog for editing entries"
  824 
  825     def __init__(self, parent, title, e = None, cfg = None, clipboard = None):
  826         Utility.__init__(self, parent, title)
  827 
  828         self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
  829         if not e:
  830             self.add_button(ui.STOCK_NEW_ENTRY, Gtk.ResponseType.OK)
  831         else:
  832             self.add_button(ui.STOCK_UPDATE, Gtk.ResponseType.OK)
  833         self.set_default_response(Gtk.ResponseType.OK)
  834 
  835         self.config      = cfg
  836         self.clipboard   = clipboard
  837         self.entry_field = {}
  838         self.fielddata   = {}
  839         self.widgetdata  = {}
  840 
  841         # set up the ui
  842         self.sect_meta      = self.add_section(title)
  843         self.sect_fields    = self.add_section(_('Account Data'))
  844         self.sect_notes     = self.add_section(_('Notes'))
  845 
  846         self.entry_name = ui.Entry()
  847         self.entry_name.set_width_chars(50)
  848         self.entry_name.set_tooltip_text(_('The name of the entry'))
  849         self.sect_meta.append_widget(_('Name'), self.entry_name)
  850 
  851         self.entry_desc = ui.Entry()
  852         self.entry_desc.set_tooltip_text(_('A description of the entry'))
  853         self.sect_meta.append_widget(_('Description'), self.entry_desc)
  854 
  855         self.dropdown = ui.EntryDropDown()
  856         self.dropdown.connect("changed", lambda w: self.__setup_fieldsect(self.dropdown.get_active_type()().fields))
  857         eventbox = ui.EventBox(self.dropdown)
  858         eventbox.set_tooltip_text(_('The type of entry'))
  859         self.sect_meta.append_widget(_('Type'), eventbox)
  860 
  861         self.entry_notes = ui.EditableTextView()
  862         self.entry_notes.set_tooltip_text(_('Notes for the entry'))
  863         self.sect_notes.append_widget(None, self.entry_notes)
  864 
  865         # populate the dialog with data
  866         self.set_entry(e)
  867 
  868 
  869     def __setup_fieldsect(self, fields):
  870         "Generates a field section based on a field list"
  871 
  872         # store current field values
  873         for fieldtype, fieldentry in self.entry_field.items():
  874             self.fielddata[fieldtype] = fieldentry.get_text()
  875 
  876         self.entry_field = {}
  877         self.sect_fields.clear()
  878 
  879         # generate field entries
  880         for field in fields:
  881 
  882             if type(field) in self.widgetdata:
  883                 userdata = self.widgetdata[type(field)]
  884 
  885             elif field.datatype == entry.DATATYPE_PASSWORD:
  886                 userdata = self.clipboard
  887 
  888             else:
  889                 userdata = None
  890 
  891             fieldentry = ui.generate_field_edit_widget(field, self.config, userdata)
  892             self.entry_field[type(field)] = fieldentry
  893 
  894             if type(field) in self.fielddata:
  895                 fieldentry.set_text(self.fielddata[type(field)])
  896 
  897             if hasattr(fieldentry, "set_tooltip_text"):
  898                 fieldentry.set_tooltip_text(field.description)
  899 
  900             elif hasattr(fieldentry, "entry") == True:
  901                 fieldentry.entry.set_tooltip_text(field.description)
  902 
  903             self.sect_fields.append_widget(field.name, fieldentry)
  904 
  905 
  906         # show widgets
  907         self.sect_fields.show_all()
  908 
  909 
  910     def get_entry(self):
  911         "Generates an entry from the dialog contents"
  912 
  913         e = self.dropdown.get_active_type()()
  914 
  915         e.name = self.entry_name.get_text()
  916         e.description = self.entry_desc.get_text()
  917         e.notes = self.entry_notes.get_text()
  918 
  919         for field in e.fields:
  920             field.value = self.entry_field[type(field)].get_text()
  921 
  922         return e
  923 
  924 
  925     def run(self):
  926         "Displays the dialog"
  927 
  928         while True:
  929             self.show_all()
  930 
  931             if Utility.run(self) == Gtk.ResponseType.OK:
  932                 e = self.get_entry()
  933 
  934                 if e.name == "":
  935                     Error(self, _('Name not entered'), _('You must enter a name for the account')).run()
  936                     continue
  937 
  938                 self.destroy()
  939                 return e
  940 
  941             else:
  942                 self.destroy()
  943                 raise CancelError
  944 
  945 
  946     def set_entry(self, e):
  947         "Sets an entry for the dialog"
  948 
  949         e = e is not None and e.copy() or entry.GenericEntry()
  950 
  951         self.entry_name.set_text(e.name)
  952         self.entry_desc.set_text(e.description)
  953         self.entry_notes.set_text(e.notes)
  954         self.dropdown.set_active_type(type(e))
  955 
  956         for field in e.fields:
  957             self.entry_field[type(field)].set_text(field.value or "")
  958 
  959 
  960     def set_fieldwidget_data(self, fieldtype, userdata):
  961         "Sets user data for fieldwidget"
  962 
  963         self.widgetdata[fieldtype] = userdata
  964 
  965         if fieldtype == entry.UsernameField and entry.UsernameField in self.entry_field:
  966             self.entry_field[entry.UsernameField].set_values(userdata)
  967 
  968 
  969 
  970 class EntryRemove(Warning):
  971     "Asks for confirmation when removing entries"
  972 
  973     def __init__(self, parent, entries):
  974 
  975         if len(entries) > 1:
  976             title   = _('Really remove the %i selected entries?') % len(entries)
  977             text    = _('By removing these entries you will also remove any entries they may contain.')
  978 
  979         elif type(entries[0]) == entry.FolderEntry:
  980             title   = _('Really remove folder \'%s\'?') % entries[0].name
  981             text    = _('By removing this folder you will also remove all accounts and folders it contains.')
  982 
  983         else:
  984             title   = _('Really remove account \'%s\'?') % entries[0].name
  985             text    = _('Please confirm that you wish to remove this account.')
  986 
  987 
  988         Warning.__init__(self, parent, title, text)
  989 
  990         self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
  991         self.add_button(Gtk.STOCK_REMOVE, Gtk.ResponseType.OK)
  992         self.set_default_response(Gtk.ResponseType.CANCEL)
  993 
  994 
  995     def run(self):
  996         "Displays the dialog"
  997 
  998         if Warning.run(self) == Gtk.ResponseType.OK:
  999             return True
 1000 
 1001         else:
 1002             raise CancelError
 1003 
 1004 
 1005 
 1006 class FolderEdit(Utility):
 1007     "Dialog for editing a folder"
 1008 
 1009     def __init__(self, parent, title, e = None):
 1010         Utility.__init__(self, parent, title)
 1011 
 1012         self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
 1013         if not e:
 1014             self.add_button(ui.STOCK_NEW_FOLDER, Gtk.ResponseType.OK)
 1015         else:
 1016             self.add_button(ui.STOCK_UPDATE, Gtk.ResponseType.OK)
 1017         self.set_default_response(Gtk.ResponseType.OK)
 1018 
 1019         # set up the ui
 1020         self.sect_folder    = self.add_section(title)
 1021 
 1022         self.entry_name = ui.Entry()
 1023         self.entry_name.set_width_chars(25)
 1024         self.entry_name.set_tooltip_text(_('The name of the folder'))
 1025         self.sect_folder.append_widget(_('Name'), self.entry_name)
 1026 
 1027         self.entry_desc = ui.Entry()
 1028         self.entry_desc.set_tooltip_text(_('A description of the folder'))
 1029         self.sect_folder.append_widget(_('Description'), self.entry_desc)
 1030 
 1031         # populate the dialog with data
 1032         self.set_entry(e)
 1033 
 1034 
 1035     def get_entry(self):
 1036         "Generates an entry from the dialog contents"
 1037 
 1038         e = entry.FolderEntry()
 1039         e.name = self.entry_name.get_text()
 1040         e.description = self.entry_desc.get_text()
 1041 
 1042         return e
 1043 
 1044 
 1045     def run(self):
 1046         "Displays the dialog"
 1047 
 1048         while True:
 1049             self.show_all()
 1050 
 1051             if Utility.run(self) == Gtk.ResponseType.OK:
 1052                 e = self.get_entry()
 1053 
 1054                 if e.name == "":
 1055                     Error(self, _('Name not entered'), _('You must enter a name for the folder')).run()
 1056                     continue
 1057 
 1058                 self.destroy()
 1059                 return e
 1060 
 1061             else:
 1062                 self.destroy()
 1063                 raise CancelError
 1064 
 1065 
 1066     def set_entry(self, e):
 1067         "Sets an entry for the dialog"
 1068 
 1069         if e is None:
 1070             return
 1071 
 1072         self.entry_name.set_text(e.name)
 1073         self.entry_desc.set_text(e.description)
 1074 
 1075 
 1076 
 1077 ##### MISCELLANEOUS DIALOGS #####
 1078 
 1079 class About(Gtk.AboutDialog):
 1080     "About dialog"
 1081 
 1082     def __init__(self, parent):
 1083         Gtk.AboutDialog.__init__(self)
 1084 
 1085         if isinstance(parent, Gtk.Window):
 1086             self.set_transient_for(parent)
 1087 
 1088         self.set_name(config.APPNAME)
 1089         self.set_version(config.VERSION)
 1090         self.set_copyright(config.COPYRIGHT)
 1091         self.set_comments(('"%s"\n\n' + _('A password manager for the GNOME desktop')) % config.RELNAME)
 1092         self.set_license(config.LICENSE)
 1093         self.set_website(config.URL)
 1094         self.set_authors(config.AUTHORS)
 1095         self.set_artists(config.ARTISTS)
 1096 
 1097 
 1098     def run(self):
 1099         "Displays the dialog"
 1100 
 1101         self.show_all()
 1102         Gtk.AboutDialog.run(self)
 1103 
 1104         self.destroy()
 1105 
 1106 
 1107 
 1108 class Exception(Error):
 1109     "Displays a traceback for an unhandled exception"
 1110 
 1111     def __init__(self, parent, traceback):
 1112         Error.__init__(
 1113             self, parent, _('Unknown error'),
 1114             _('An unknown error occured. Please report the text below to the Revelation developers, along with what you were doing that may have caused the error. You may attempt to continue running Revelation, but it may behave unexpectedly.')
 1115         )
 1116 
 1117         self.add_button(Gtk.STOCK_QUIT, Gtk.ResponseType.CANCEL )
 1118         self.add_button(ui.STOCK_CONTINUE, Gtk.ResponseType.OK )
 1119 
 1120         textview = ui.TextView(None, traceback)
 1121         scrolledwindow = ui.ScrolledWindow(textview)
 1122         scrolledwindow.set_size_request(-1, 120)
 1123 
 1124         self.contents.pack_start(scrolledwindow, True, True, 0)
 1125 
 1126 
 1127     def run(self):
 1128         "Runs the dialog"
 1129 
 1130         return Error.run(self) == Gtk.ResponseType.OK
 1131 
 1132 
 1133 
 1134 class PasswordChecker(Utility):
 1135     "A password checker dialog"
 1136 
 1137     def __init__(self, parent, cfg = None, clipboard = None):
 1138         Utility.__init__(self, parent, _('Password Checker'))
 1139 
 1140         self.add_button(Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE)
 1141         self.set_default_response(Gtk.ResponseType.CLOSE)
 1142 
 1143         self.cfg = cfg
 1144         self.set_modal(False)
 1145         self.set_size_request(300, -1)
 1146 
 1147         self.section = self.add_section(_('Password Checker'))
 1148 
 1149         self.entry = ui.PasswordEntry(None, cfg, clipboard)
 1150         self.entry.autocheck = False
 1151         self.entry.set_width_chars(40)
 1152         self.entry.connect("changed", self.__cb_changed)
 1153         self.entry.set_tooltip_text(_('Enter a password to check'))
 1154         self.section.append_widget(_('Password'), self.entry)
 1155 
 1156         self.result = ui.ImageLabel(_('Enter a password to check'), ui.STOCK_UNKNOWN, ui.ICON_SIZE_HEADLINE)
 1157         self.result.set_tooltip_text(_('The result of the check'))
 1158         self.section.append_widget(None, self.result)
 1159 
 1160         self.connect("response", self.__cb_response)
 1161 
 1162 
 1163     def __cb_changed(self, widget, data = None):
 1164         "Callback for entry changes"
 1165 
 1166         password = self.entry.get_text()
 1167 
 1168         try:
 1169             if len(password) == 0:
 1170                 icon    = ui.STOCK_UNKNOWN
 1171                 result  = _('Enter a password to check')
 1172 
 1173             else:
 1174                 util.check_password(password)
 1175                 icon    = ui.STOCK_PASSWORD_STRONG
 1176                 result  = _('The password seems good')
 1177 
 1178         except ValueError as reason:
 1179             icon    = ui.STOCK_PASSWORD_WEAK
 1180             result = _('The password %s') % str(reason)
 1181 
 1182         self.result.set_text(result)
 1183         self.result.set_stock(icon, ui.ICON_SIZE_HEADLINE)
 1184 
 1185 
 1186     def __cb_response(self, widget, response):
 1187         "Callback for response"
 1188 
 1189         self.destroy()
 1190 
 1191 
 1192     def run(self):
 1193         "Displays the dialog"
 1194 
 1195         self.show_all()
 1196 
 1197         # for some reason, Gtk crashes on close-by-escape
 1198         # if we don't do this
 1199         self.get_widget_for_response(Gtk.ResponseType.CLOSE).grab_focus()
 1200         self.entry.grab_focus()
 1201 
 1202 
 1203 
 1204 class PasswordGenerator(Utility):
 1205     "A password generator dialog"
 1206 
 1207     def __init__(self, parent, cfg, clipboard = None):
 1208         Utility.__init__(self, parent, _('Password Generator'))
 1209 
 1210         self.add_button(Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE)
 1211         self.add_button(ui.STOCK_GENERATE, Gtk.ResponseType.OK)
 1212 
 1213         self.config = cfg
 1214         self.set_modal(False)
 1215 
 1216         self.section = self.add_section(_('Password Generator'))
 1217 
 1218         self.entry = ui.PasswordEntry(None, cfg, clipboard)
 1219         self.entry.autocheck = False
 1220         self.entry.set_editable(False)
 1221         self.entry.set_tooltip_text(_('The generated password'))
 1222         self.section.append_widget(_('Password'), self.entry)
 1223 
 1224         self.spin_pwlen = ui.SpinEntry()
 1225         self.spin_pwlen.set_range(4, 256)
 1226         self.config.bind("passwordgen-length", self.spin_pwlen, "value", Gio.SettingsBindFlags.DEFAULT)
 1227         self.spin_pwlen.set_tooltip_text(_('The number of characters in generated passwords - 8 or more are recommended'))
 1228         self.section.append_widget(_('Length'), self.spin_pwlen)
 1229 
 1230         self.check_punctuation_chars = ui.CheckButton(_('Use punctuation characters for passwords'))
 1231         if self.config.get_int("passwordgen-length"):
 1232             self.check_punctuation_chars.set_sensitive(True)
 1233         self.config.bind("passwordgen-punctuation", self.check_punctuation_chars, "active", Gio.SettingsBindFlags.DEFAULT)
 1234         self.check_punctuation_chars.set_tooltip_text(_('When passwords are generated, use punctuation characters like %, =, { or .'))
 1235         self.section.append_widget(None, self.check_punctuation_chars)
 1236 
 1237         self.connect("response", self.__cb_response)
 1238 
 1239 
 1240     def __cb_response(self, widget, response):
 1241         "Callback for dialog responses"
 1242 
 1243         if response == Gtk.ResponseType.OK:
 1244             self.entry.set_text(util.generate_password(self.spin_pwlen.get_value(), self.check_punctuation_chars.get_active()))
 1245 
 1246         else:
 1247             self.destroy()
 1248 
 1249 
 1250     def run(self):
 1251         "Displays the dialog"
 1252 
 1253         self.show_all()
 1254         self.get_widget_for_response(Gtk.ResponseType.OK).grab_focus()
 1255 
 1256 
 1257 
 1258 ##### FUNCTIONS #####
 1259 
 1260 def create_unique(dialog, *args):
 1261     "Creates a unique dialog"
 1262 
 1263     if present_unique(dialog) == True:
 1264         return get_unique(dialog)
 1265 
 1266     else:
 1267         UNIQUE_DIALOGS[dialog] = dialog(*args)
 1268         UNIQUE_DIALOGS[dialog].connect("destroy", lambda w: remove_unique(dialog))
 1269 
 1270         return UNIQUE_DIALOGS[dialog]
 1271 
 1272 
 1273 def get_unique(dialog):
 1274     "Returns a unique dialog"
 1275 
 1276     if unique_exists(dialog) == True:
 1277         return UNIQUE_DIALOGS[dialog]
 1278 
 1279     else:
 1280         return None
 1281 
 1282 
 1283 def present_unique(dialog):
 1284     "Presents a unique dialog, if it exists"
 1285 
 1286     if unique_exists(dialog) == True:
 1287         get_unique(dialog).present()
 1288 
 1289         return True
 1290 
 1291     else:
 1292         return False
 1293 
 1294 
 1295 def remove_unique(dialog):
 1296     "Removes a unique dialog"
 1297 
 1298     if unique_exists(dialog):
 1299         UNIQUE_DIALOGS[dialog] = None
 1300 
 1301 
 1302 def run_unique(dialog, *args):
 1303     "Runs a unique dialog"
 1304 
 1305     if present_unique(dialog) == True:
 1306         return None
 1307 
 1308     else:
 1309         d = create_unique(dialog, *args)
 1310 
 1311         return d.run()
 1312 
 1313 
 1314 def unique_exists(dialog):
 1315     "Checks if a unique dialog exists"
 1316 
 1317     return dialog in UNIQUE_DIALOGS and UNIQUE_DIALOGS[dialog] != None
 1318