#!/usr/bin/python3

#
# Revelation - a password manager for GNOME 2
# http://oss.codepoet.no/revelation/
# $Id$
#
# Copyright (c) 2003-2006 Erik Grinaker
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, Gio, GLib
import gettext, os, pwd, sys, urllib.parse, locale

from revelation import config, data, datahandler, dialog, entry, io, ui, util

_ = gettext.gettext

class Revelation(ui.App):
    "The Revelation application"

    def __init__(self):
        sys.excepthook = self.__cb_exception
        os.umask(0o077)

        gettext.bindtextdomain(config.PACKAGE, config.DIR_LOCALE)
        gettext.textdomain(config.PACKAGE)

        # Gtk.Builder uses the C lib's locale API, accessible with the locale module
        locale.bindtextdomain(config.PACKAGE, config.DIR_LOCALE)
        locale.bind_textdomain_codeset(config.PACKAGE, "UTF-8")

        ui.App.__init__(self, config.APPNAME)
        self.window = None

        resource_path = os.path.join(config.DIR_UI, 'revelation.gresource')
        resource = Gio.Resource.load(resource_path)
        resource._register()

    def do_startup(self):
        Gtk.Application.do_startup(self)
        if not self.window:
            self.window = ui.AppWindow(application=self, title="Revelation")
            self.main_vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL,5)
            self.window.add(self.main_vbox)
        self.add_window(self.window)

    def do_activate(self):

        self.builder = Gtk.Builder()
        self.builder.add_from_resource('/info/olasagasti/revelation/ui/menubar.ui')
        self.menubar = self.builder.get_object("menubar")

        self.popupbuilder = Gtk.Builder()
        self.popupbuilder.add_from_resource('/info/olasagasti/revelation/ui/popup-tree.ui')
        self.popupmenu = self.popupbuilder.get_object("popup-tree")

        self.window.connect("delete-event", self.__cb_quit)

        try:
            self.__init_config()
            self.__init_actions()
            self.__init_facilities()
            self.__init_ui()
            self.__init_states()
            self.__init_dbus()

        except IOError:
            dialog.Error(self.window, _('Missing data files'), _('Some of Revelations system files could not be found, please reinstall Revelation.')).run()
            sys.exit(1)

        except config.ConfigError:
            dialog.Error(self.window, _('Missing configuration data'), _('Revelation could not find its configuration data, please reinstall Revelation.')).run()
            sys.exit(1)

        except ui.DataError:
            dialog.Error(self.window, _('Invalid data files'), _('Some of Revelations system files contain invalid data, please reinstall Revelation.')).run()
            sys.exit(1)


        if len(sys.argv) > 1:
            file = sys.argv[1]

        elif self.config.get_boolean("file-autoload") == True:
            file = self.config.get_string("file-autoload-file")

        else:
            file = ""

        if file != "":
            self.file_open(io.file_normpath(urllib.parse.unquote(file)))


    def __init_config(self):
        "Get configuration schema"

        schema_source = Gio.SettingsSchemaSource.get_default()
        rvl_schema = schema_source.lookup('org.revelation', recursive=True)

        if not rvl_schema:
            schema_source = Gio.SettingsSchemaSource.new_from_directory(config.DIR_GSCHEMAS, schema_source, False)
            rvl_schema=schema_source.lookup('org.revelation', recursive=True)

        if not rvl_schema:
            raise config.ConfigError

        rvl_settings = Gio.Settings.new_full(rvl_schema, None, None)
        self.config = rvl_settings


    def __init_actions(self):
        "Sets up actions"

        # set up placeholders
        group = Gio.SimpleActionGroup()
        self.window.insert_action_group("placeholder", group)

        action_menu_edit  = Gio.SimpleAction.new("menu-edit",  None)
        action_menu_entry = Gio.SimpleAction.new("menu-entry", None)
        action_menu_file  = Gio.SimpleAction.new("menu-file",  None)
        action_menu_help  = Gio.SimpleAction.new("menu-help",  None)
        action_menu_view  = Gio.SimpleAction.new("menu-view",  None)
        action_popup_tree = Gio.SimpleAction.new("popup-tree", None)
        group.add_action(action_menu_edit)
        group.add_action(action_menu_entry)
        group.add_action(action_menu_file)
        group.add_action(action_menu_help)
        group.add_action(action_menu_view)
        group.add_action(action_popup_tree)

        # set up dynamic actions
        group = Gio.SimpleActionGroup()
        self.window.insert_action_group("dynamic", group)

        action = Gio.SimpleAction.new("clip-paste", None)
        action.connect("activate",      self.__cb_clip_paste)
        group.add_action(action)

        action = Gio.SimpleAction.new("entry-goto", None)
        action.connect("activate",      lambda w,k: self.entry_goto(self.tree.get_selected()))
        group.add_action(action)

        action = Gio.SimpleAction.new("redo", None)
        action.connect("activate",      lambda w,k: self.redo())
        group.add_action(action)

        action = Gio.SimpleAction.new("undo", None)
        action.connect("activate",      lambda w,k: self.undo())
        group.add_action(action)

        # set up group for multiple entries
        group = Gio.SimpleActionGroup()
        self.window.insert_action_group("entry-multiple", group)

        action = Gio.SimpleAction.new("clip-copy", None)
        action.connect("activate",      self.__cb_clip_copy)
        group.add_action(action)

        action = Gio.SimpleAction.new("clip-chain", None)
        action.connect("activate",      lambda w,k: self.clip_chain(self.entrystore.get_entry(self.tree.get_active())))
        group.add_action(action)

        action = Gio.SimpleAction.new("clip-cut", None)
        action.connect("activate",      self.__cb_clip_cut)
        group.add_action(action)

        action = Gio.SimpleAction.new("entry-remove", None)
        action.connect("activate",      lambda w,k: self.entry_remove(self.tree.get_selected()))
        group.add_action(action)

        # action group for "optional" entries
        group = Gio.SimpleActionGroup()
        self.window.insert_action_group("entry-optional", group)

        action = Gio.SimpleAction.new("entry-add", None)
        action.connect("activate",      lambda w,k: self.entry_add(None, self.tree.get_active()))
        group.add_action(action)

        action = Gio.SimpleAction.new("entry-folder", None)
        action.connect("activate",      lambda w,k: self.entry_folder(None, self.tree.get_active()))
        group.add_action(action)

        # action group for single entries
        group = Gio.SimpleActionGroup()
        self.window.insert_action_group("entry-single", group)

        action = Gio.SimpleAction.new("entry-edit", None)
        action.connect("activate",      lambda w,k: self.entry_edit(self.tree.get_active()))
        group.add_action(action)

        # action group for existing file
        group = Gio.SimpleActionGroup()
        self.window.insert_action_group("file-exists", group)

        action = Gio.SimpleAction.new("file-lock", None)
        action.connect("activate",      lambda w,k: self.file_lock())
        group.add_action(action)

        # action group for searching
        group = Gio.SimpleActionGroup()
        self.window.insert_action_group("find", group)

        action = Gio.SimpleAction.new("find-next", None)
        action.connect("activate",      lambda w,k: self.__entry_find(self, self.searchbar.entry.get_text(), self.searchbar.dropdown.get_active_type(), data.SEARCH_NEXT))
        group.add_action(action)

        action = Gio.SimpleAction.new("find-previous", None)
        action.connect("activate",      lambda w,k: self.__entry_find(self, self.searchbar.entry.get_text(), self.searchbar.dropdown.get_active_type(), data.SEARCH_PREVIOUS))
        group.add_action(action)

        # global action group
        group = Gio.SimpleActionGroup()
        self.window.insert_action_group("file", group)

        action = Gio.SimpleAction.new("file-change-password", None)
        action.connect("activate",      lambda w,k: self.file_change_password())
        group.add_action(action)

        action = Gio.SimpleAction.new("file-close", None)
        action.connect("activate",      self.__cb_close)
        group.add_action(action)

        action = Gio.SimpleAction.new("file-export", None)
        action.connect("activate",      lambda w,k: self.file_export())
        group.add_action(action)

        action = Gio.SimpleAction.new("file-import", None)
        action.connect("activate",      lambda w,k: self.file_import())
        group.add_action(action)

        action = Gio.SimpleAction.new("file-new", None)
        action.connect("activate",      lambda w,k: self.file_new())
        group.add_action(action)

        action = Gio.SimpleAction.new("file-open", None)
        action.connect("activate",      lambda w,k: self.file_open())
        group.add_action(action)

        action = Gio.SimpleAction.new("file-save", None)
        action.connect("activate",      lambda w,k: self.file_save(self.datafile.get_file(), self.datafile.get_password()))
        group.add_action(action)

        action = Gio.SimpleAction.new("file-save-as", None)
        action.connect("activate",      lambda w,k: self.file_save(None, None))
        group.add_action(action)

        action = Gio.SimpleAction.new("find", None)
        action.connect("activate",      lambda w,k: self.entry_find())
        group.add_action(action)

        action = Gio.SimpleAction.new("help-about", None)
        action.connect("activate",      lambda w,k: self.about())
        group.add_action(action)

        action = Gio.SimpleAction.new("prefs", None)
        action.connect("activate",      lambda w,k: self.prefs())
        group.add_action(action)

        action = Gio.SimpleAction.new("pwchecker", None)
        action.connect("activate",      lambda w,k: self.pwcheck())
        group.add_action(action)

        action = Gio.SimpleAction.new("pwgenerator", None)
        action.connect("activate",      lambda w,k: self.pwgen())
        group.add_action(action)

        action = Gio.SimpleAction.new("quit", None)
        action.connect("activate",      self.__cb_quit)
        group.add_action(action)

        action = Gio.SimpleAction.new("select-all", None)
        action.connect("activate",      lambda w,k: self.tree.select_all())
        group.add_action(action)

        action = Gio.SimpleAction.new("select-none", None)
        action.connect("activate",      lambda w,k: self.tree.unselect_all())
        group.add_action(action)

        action_vp = Gio.SimpleAction.new_stateful("view-passwords", None, GLib.Variant.new_boolean(self.config.get_boolean("view-passwords")))
        action_vp.connect("activate", lambda w, k: action_vp.set_state(GLib.Variant.new_boolean(not action_vp.get_state())))
        action_vp.connect("activate", lambda w, k: self.config.set_boolean("view-passwords", action_vp.get_state()))
        self.config.connect("changed::view-passwords", lambda w, k: action_vp.set_state(GLib.Variant.new_boolean(w.get_boolean(k))))
        group.add_action(action_vp)

        action_vs = Gio.SimpleAction.new_stateful("view-searchbar", None, GLib.Variant.new_boolean(True))
        action_vs.connect("activate", lambda w, k: action_vs.set_state(GLib.Variant.new_boolean(not action_vs.get_state())))
        action_vs.connect("activate", lambda w, k: self.config.set_boolean("view-searchbar", action_vs.get_state()))
        action_vs.connect("activate", lambda w, k: self.searchbar.set_visible(GLib.Variant.new_boolean(action_vs.get_state())))
        self.config.connect("changed::view-searchbar", lambda w, k: action_vs.set_state(GLib.Variant.new_boolean(w.get_boolean(k))))
        self.config.connect("changed::view-searchbar", lambda w, k: self.searchbar.set_visible(GLib.Variant.new_boolean(w.get_boolean(k))))
        group.add_action(action_vs)

        action_va = Gio.SimpleAction.new_stateful("view-statusbar", None, GLib.Variant.new_boolean(True))
        action_va.connect("activate", lambda w, k: action_va.set_state(GLib.Variant.new_boolean(not action_va.get_state())))
        action_va.connect("activate", lambda w, k: self.config.set_boolean("view-statusbar", action_va.get_state()))
        action_va.connect("activate", lambda w, k: self.statusbar.set_visible(GLib.Variant.new_boolean(action_va.get_state())))
        self.config.connect("changed::view-statusbar", lambda w, k: action_va.set_state(GLib.Variant.new_boolean(w.get_boolean(k))))
        self.config.connect("changed::view-statusbar", lambda w, k: self.statusbar.set_visible(GLib.Variant.new_boolean(w.get_boolean(k))))
        group.add_action(action_va)

        action_vt = Gio.SimpleAction.new_stateful("view-toolbar", None, GLib.Variant.new_boolean(True))
        action_vt.connect("activate", lambda w, k: action_vt.set_state(GLib.Variant.new_boolean(not action_vt.get_state())))
        action_vt.connect("activate", lambda w, k: self.config.set_boolean("view-toolbar", action_vt.get_state()))
        action_vt.connect("activate", lambda w, k: self.toolbar.set_visible(GLib.Variant.new_boolean(action_vt.get_state())))
        self.config.connect("changed::view-toolbar", lambda w, k: action_vt.set_state(GLib.Variant.new_boolean(w.get_boolean(k))))
        self.config.connect("changed::view-toolbar", lambda w, k: self.toolbar.set_visible(GLib.Variant.new_boolean(w.get_boolean(k))))
        self.config.connect("changed::view-toolbar-style", lambda w, k: self.__cb_config_toolbar_style(w, w.get_string(k)))
        group.add_action(action_vt)


    def __init_facilities(self):
        "Sets up various facilities"

        self.clipboard      = data.Clipboard()
        self.datafile       = io.DataFile(datahandler.Revelation2)
        self.entryclipboard = data.EntryClipboard()
        self.entrystore     = data.EntryStore()
        self.entrysearch    = data.EntrySearch(self.entrystore)
        self.items          = Gtk.IconTheme.get_default()
        self.locktimer      = data.Timer()
        self.undoqueue      = data.UndoQueue()

        self.datafile.connect("changed", lambda w,f: self.__state_file(f))
        self.datafile.connect("content-changed", self.__cb_file_content_changed)
        self.entryclipboard.connect("content-toggled", lambda w,d: self.__state_clipboard(d))
        self.locktimer.connect("ring", self.__cb_file_autolock)
        self.undoqueue.connect("changed", lambda w: self.__state_undo(self.undoqueue.get_undo_action(), self.undoqueue.get_redo_action()))

        self.config.connect("changed::file-autolock-timeout", lambda w, k: self.locktimer.start(60 * w.get_int(k)))
        if self.config.get_boolean("file-autolock"):
            self.locktimer.start(60 * self.config.get_int("file-autolock-timeout"))

        dialog.EVENT_FILTER = self.__cb_event_filter


    def __init_states(self):
        "Sets the initial application state"

        # set window states
        self.window.set_default_size(
            self.config.get_int("view-window-width"),
            self.config.get_int("view-window-height")
        )

        self.window.move(
            self.config.get_int("view-window-position-x"),
            self.config.get_int("view-window-position-y")
        )

        self.hpaned.set_position(
            self.config.get_int("view-pane-position")
        )

        # bind ui widgets to config keys
        bind = {
            "view-passwords"    : "/menubar/menu-view/view-passwords",
            "view-searchbar"    : "/menubar/menu-view/view-searchbar",
            "view-statusbar"    : "/menubar/menu-view/view-statusbar",
            "view-toolbar"      : "/menubar/menu-view/view-toolbar"
        }

        for key in bind.keys():
            self.window.get_action_group("file").lookup_action(key).set_state(self.config.get_value(key))

        self.window.show_all()

        # use some events to restart lock timer
        Gdk.event_handler_set(self.__cb_event_filter)
        self.file_locked = False


        # set some variables
        self.entrysearch.string = ''
        self.entrysearch.type   = None

        # set ui widget states
        self.__state_clipboard(self.entryclipboard.has_contents())
        self.__state_entry([])
        self.__state_file(None)
        self.__state_find(self.searchbar.entry.get_text())
        self.__state_undo(None, None)

        # set states from config
        self.searchbar.set_visible(self.config.get_boolean("view-searchbar"))
        self.statusbar.set_visible(self.config.get_boolean("view-statusbar"))
        self.toolbar.set_visible(self.config.get_boolean("view-toolbar"))
        self.__cb_config_toolbar_style(self.config, self.config.get_string("view-toolbar-style"))

        # give focus to searchbar entry if shown
        if self.searchbar.get_property("visible") == True:
            self.searchbar.entry.grab_focus()


    def __init_ui(self):
        "Sets up the UI"

        # add custom icons path
        _icon_theme = Gtk.IconTheme.get_default()
        if _icon_theme is not None:
            _icon_theme.append_search_path(config.DIR_ICONS)

        # set window icons
        pixbufs = [ self.items.load_icon("info.olasagasti.revelation", size, 0) for size in ( 128, 48, 32, 24, 16) ]
        pixbufs = [ pixbuf for pixbuf in pixbufs if pixbuf != None ]

        if len(pixbufs) > 0:
            Gtk.Window.set_default_icon_list(pixbufs)

        # set up toolbar and menus
        self.set_menubar(self.menubar)

        toolbar = Gtk.Toolbar.new()
        toolbar.set_style(Gtk.ToolbarStyle.BOTH_HORIZ)

        open_item = Gtk.ToolButton.new(Gtk.Image.new_from_icon_name('document-open', Gtk.IconSize.LARGE_TOOLBAR), _('_Open'))
        open_item.connect('clicked', lambda k: self.window.get_action_group("file").lookup_action("file-open").activate())
        open_item.set_tooltip_text(_('Open a file'))
        open_item.set_use_underline(True)
        toolbar.insert(open_item, -1)

        save_item = Gtk.ToolButton.new(Gtk.Image.new_from_icon_name('document-save', Gtk.IconSize.LARGE_TOOLBAR), _('_Save'))
        save_item.connect('clicked', lambda k: self.window.get_action_group("file").lookup_action("file-save").activate())
        save_item.set_tooltip_text(_('Save data to a file'))
        save_item.set_property('is-important', True)
        save_item.set_use_underline(True)
        toolbar.insert(save_item, -1)

        toolbar.insert(Gtk.SeparatorToolItem.new(), -1)

        addentry_item = Gtk.ToolButton.new(Gtk.Image.new_from_icon_name('list-add', Gtk.IconSize.LARGE_TOOLBAR), _('Add Entry'))
        addentry_item.connect('clicked', lambda k: self.window.get_action_group("entry-optional").lookup_action("entry-add").activate())
        addentry_item.set_tooltip_text(_('Create a new entry'))
        addentry_item.set_property('is-important', True)
        addentry_item.set_use_underline(True)
        toolbar.insert(addentry_item, -1)

        addfolder_item = Gtk.ToolButton.new(Gtk.Image.new_from_icon_name('folder-new', Gtk.IconSize.LARGE_TOOLBAR), _('Add folder'))
        addfolder_item.connect('clicked', lambda k: self.window.get_action_group("entry-optional").lookup_action("entry-folder").activate())
        addfolder_item.set_tooltip_text(_('Create a new folder'))
        toolbar.insert(addfolder_item, -1)

        toolbar.insert(Gtk.SeparatorToolItem.new(), -1)

        gotoentry_item = Gtk.ToolButton.new(Gtk.Image.new_from_icon_name('go-jump', Gtk.IconSize.LARGE_TOOLBAR), _('_Go to'))
        gotoentry_item.connect('clicked', lambda k: self.window.get_action_group("dynamic").lookup_action("entry-goto").activate())
        gotoentry_item.set_tooltip_text(_('Go to the selected entries'))
        gotoentry_item.set_property('is-important', True)
        gotoentry_item.set_use_underline(True)
        toolbar.insert(gotoentry_item, -1)

        editentry_item = Gtk.ToolButton.new(Gtk.Image.new_from_icon_name('document-edit', Gtk.IconSize.LARGE_TOOLBAR), _('_Edit'))
        editentry_item.connect('clicked', lambda k: self.window.get_action_group("entry-single").lookup_action("entry-edit").activate())
        editentry_item.set_tooltip_text(_('Edit the selected entry'))
        editentry_item.set_use_underline(True)
        toolbar.insert(editentry_item, -1)

        removeentry_item = Gtk.ToolButton.new(Gtk.Image.new_from_icon_name('edit-delete', Gtk.IconSize.LARGE_TOOLBAR), _('Re_move'))
        removeentry_item.connect('clicked', lambda k: self.window.get_action_group("entry-multiple").lookup_action("entry-remove").activate())
        removeentry_item.set_tooltip_text(_('Remove the selected entries'))
        removeentry_item.set_use_underline(True)
        toolbar.insert(removeentry_item, -1)


        self.toolbar=toolbar
        self.toolbar.connect("popup-context-menu", lambda w,x,y,b: True)
        self.add_toolbar(toolbar, "toolbar", 1)

        self.statusbar = ui.Statusbar()
        self.main_vbox.pack_end(self.statusbar, False, True, 0)

        self.searchbar = ui.Searchbar()
        self.add_toolbar(self.searchbar, "searchbar", 2)

        # set up main application widgets
        self.tree = ui.EntryTree(self.entrystore)
        self.scrolledwindow = ui.ScrolledWindow(self.tree)

        self.entryview = ui.EntryView(self.config, self.clipboard)
        self.entryview.set_halign(Gtk.Align.CENTER)
        self.entryview.set_valign(Gtk.Align.CENTER)
        self.entryview.set_hexpand(True)

        self.hpaned = Gtk.Paned.new(Gtk.Orientation.HORIZONTAL)
        self.hpaned.pack1(self.scrolledwindow, True, True)
        self.hpaned.pack2(self.entryview, True, True)
        self.hpaned.set_border_width(6)
        self.set_contents(self.hpaned)

        # set up drag-and-drop
        uritarget = Gtk.TargetEntry.new("text/uri-list", 0, 0)
        self.window.drag_dest_set(Gtk.DestDefaults.ALL, [uritarget], Gdk.DragAction.COPY | Gdk.DragAction.MOVE | Gdk.DragAction.LINK )
        self.window.connect("drag_data_received", self.__cb_drag_dest)

        self.tree.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, ( ( "revelation/treerow", Gtk.TargetFlags.SAME_APP | Gtk.TargetFlags.SAME_WIDGET, 0), ), Gdk.DragAction.MOVE)
        self.tree.enable_model_drag_dest(( ( "revelation/treerow", Gtk.TargetFlags.SAME_APP | Gtk.TargetFlags.SAME_WIDGET, 0), ), Gdk.DragAction.MOVE)
        self.tree.connect("drag_data_received", self.__cb_tree_drag_received)

        # set up callbacks
        self.searchbar.entry.connect("key-press-event", self.__cb_searchbar_key_press)
        self.searchbar.button_next.connect("clicked", self.__cb_searchbar_button_clicked, data.SEARCH_NEXT)
        self.searchbar.button_prev.connect("clicked", self.__cb_searchbar_button_clicked, data.SEARCH_PREVIOUS)
        self.searchbar.entry.connect("changed", lambda w: self.__state_find(self.searchbar.entry.get_text()))

        self.tree.connect("popup", lambda w,d: self.popup(self.popupmenu, d.button, d.time))
        self.tree.connect("doubleclick", self.__cb_tree_doubleclick)
        self.tree.connect("key-press-event", self.__cb_tree_keypress)
        self.tree.selection.connect("changed", lambda w: self.entryview.display_entry(self.entrystore.get_entry(self.tree.get_active())))
        self.tree.selection.connect("changed", lambda w: self.__state_entry(self.tree.get_selected()))

    def __init_dbus(self):
        app = Gio.Application.get_default
        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)

    ##### STATE HANDLERS #####

    def __save_state(self):
        "Saves the current application state"

        width, height = self.window.get_size()
        self.config.set_int("view-window-width", width)
        self.config.set_int("view-window-height", height)

        x, y = self.window.get_position()
        self.config.set_int("view-window-position-x", x)
        self.config.set_int("view-window-position-y", y)

        self.config.set_int("view-pane-position", self.hpaned.get_position())
        self.config.sync()


    def __state_clipboard(self, has_contents):
        "Sets states based on the clipboard contents"

        self.window.get_action_group("dynamic").lookup_action("clip-paste").set_enabled(has_contents)

    def __state_entry(self, iters):
        "Sets states for entry-dependant ui items"

        # widget sensitivity based on number of entries
        for action in self.window.get_action_group("entry-multiple").list_actions():
            self.window.get_action_group("entry-multiple").lookup_action(action).set_enabled(len(iters) > 0)
        for action in self.window.get_action_group("entry-single").list_actions():
            self.window.get_action_group("entry-single").lookup_action(action).set_enabled(len(iters) == 1)
        for action in self.window.get_action_group("entry-optional").list_actions():
            self.window.get_action_group("entry-optional").lookup_action(action).set_enabled(len(iters) < 2)


        # copy password sensitivity
        s = False

        for iter in iters:
            e = self.entrystore.get_entry(iter)

            for f in e.fields:
                if f.datatype == entry.DATATYPE_PASSWORD and f.value != "":
                    s = True

        self.window.get_action_group("entry-multiple").lookup_action("clip-chain").set_enabled(s)


        # goto sensitivity
        try:
            for iter in iters:
                e = self.entrystore.get_entry(iter)

                if (e.id == "folder"):
                    continue

                if self.config.get_string("launcher-%s" % e.id) not in ( "", None ):
                    s = True
                    break

            else:
                s = False

        except config.ConfigError:
            s = False

        self.window.get_action_group("dynamic").lookup_action("entry-goto").set_enabled(s)


    def __state_file(self, file):
        "Sets states based on file"

        for action in self.window.get_action_group("file-exists").list_actions():
            self.window.get_action_group("file-exists").lookup_action(action).set_enabled(file is not None)

        if file is not None:
            self.window.set_title(os.path.basename(file))

            if io.file_is_local(file):
                os.chdir(os.path.dirname(file))

        else:
            self.window.set_title('[' + _('New file') + ']')


    def __state_find(self, string):
        "Sets states based on the current search string"

        for action in self.window.get_action_group("find").list_actions():
            self.window.get_action_group("find").lookup_action(action).set_enabled(string != "")


    def __state_undo(self, undoaction, redoaction):
        "Sets states based on undoqueue actions"

        if undoaction is None:
            s, l = False, _('_Undo')

        else:
            s, l = True, _('_Undo %s') % undoaction[1].lower()

        action = self.window.get_action_group("dynamic").lookup_action("undo")
        action.set_enabled(s)
        # TODO action.set_property("label", l)


        if redoaction is None:
            s, l = False, _('_Redo')

        else:
            s, l = True, _('_Redo %s') % redoaction[1].lower()

        action = self.window.get_action_group("dynamic").lookup_action("redo")
        action.set_enabled(s)
        # TODO action.set_property("label", l)




    ##### MISC CALLBACKS #####

    def __cb_clip_copy(self, widget, data = None):
        "Handles copying to the clipboard"

        focuswidget = self.window.get_focus()

        if focuswidget is self.tree:
            self.clip_copy(self.tree.get_selected())

        elif isinstance(focuswidget, Gtk.Label) or isinstance(focuswidget, Gtk.Entry):
            focuswidget.emit("copy-clipboard")


    def __cb_clip_cut(self, widget, data = None):
        "Handles cutting to clipboard"

        focuswidget = self.window.get_focus()

        if focuswidget is self.tree:
            self.clip_cut(self.tree.get_selected())

        elif isinstance(focuswidget, Gtk.Entry):
            focuswidget.emit("cut-clipboard")


    def __cb_clip_paste(self, widget, data = None):
        "Handles pasting from clipboard"

        focuswidget = self.window.get_focus()

        if focuswidget is self.tree:
            self.clip_paste(self.entryclipboard.get(), self.tree.get_active())

        elif isinstance(focuswidget, Gtk.Entry):
            focuswidget.emit("paste-clipboard")


    def __cb_drag_dest(self, widget, context, x, y, seldata, info, time, userdata = None):
        "Handles file drops"

        if seldata.data is None:
            return

        files = [ file.strip() for file in seldata.data.split("\n") if file.strip() != "" ]

        if len(files) > 0:
            self.file_open(files[0])


    def __cb_event_filter(self, event):
        "Event filter for gdk window"

        if event.type in (Gdk.EventType.KEY_PRESS, Gdk.EventType.BUTTON_PRESS, Gdk.EventType.MOTION_NOTIFY):
            self.locktimer.reset()

        Gtk.main_do_event(event)
        return Gdk.FilterReturn.CONTINUE


    def __cb_exception(self, type, value, trace):
        "Callback for unhandled exceptions"

        if type == KeyboardInterrupt:
            sys.exit(1)

        traceback = util.trace_exception(type, value, trace)
        sys.stderr.write(traceback)

        if dialog.Exception(self.window, traceback).run() == True:
            Gtk.main()

        else:
            sys.exit(1)


    def __cb_file_content_changed(self, widget, file):
        "Callback for changed file"

        try:
            if dialog.FileChanged(self.window, file).run() == True:
                self.file_open(self.datafile.get_file(), self.datafile.get_password())

        except dialog.CancelError:
            self.statusbar.set_status(_('Open cancelled'))


    def __cb_file_autolock(self, widget, data = None):
        "Callback for locking the file"

        if self.config.get_boolean("file-autolock") == True:
            self.file_lock()


    def __cb_screensaver_lock(self, connection, unique_name, object_path, interface, signal, state):
        if state[0] is True and self.config.get_boolean("file-autolock") == True:
            self.file_lock()

    def __cb_quit(self, widget, data = None):
        "Callback for quit"

        if self.quit() == False:
            return True

        else:
            return False

    def __cb_close(self, widget, data = None):
        "Callback for Close"

        if self.file_close() == False:
            return True

        else:
            return False

    def __cb_searchbar_button_clicked(self, widget, direction = data.SEARCH_NEXT):
        "Callback for searchbar button clicks"

        self.__entry_find(self, self.searchbar.entry.get_text(), self.searchbar.dropdown.get_active_type(), direction)
        self.searchbar.entry.select_region(0, -1)


    def __cb_searchbar_key_press(self, widget, data):
        "Callback for searchbar key presses"

        # escape
        if data.keyval == Gdk.KEY_Escape:
            context = widget.get_style_context()
            context.remove_class(Gtk.STYLE_CLASS_ERROR)
            self.config.set_boolean("view-searchbar", False)


    def __cb_tree_doubleclick(self, widget, iter):
        "Handles doubleclicks on the tree"

        if self.config.get_string("behavior-doubleclick") == "edit":
            self.entry_edit(iter)

        elif self.config.get_string("behavior-doubleclick") == "copy":
            self.clip_chain(self.entrystore.get_entry(iter))

        else:
            self.entry_goto((iter,))


    def __cb_tree_drag_received(self, tree, context, x, y, seldata, info, time):
        "Callback for drag drops on the treeview"

        # get source and destination data
        sourceiters = self.entrystore.filter_parents(self.tree.get_selected())
        destrow = self.tree.get_dest_row_at_pos(x, y)

        if destrow is None:
            destpath = ( self.entrystore.iter_n_children(None) - 1, )
            pos = Gtk.TreeViewDropPosition.AFTER

        else:
            destpath, pos = destrow

        destiter = self.entrystore.get_iter(destpath)
        destpath = self.entrystore.get_path(destiter)

        # avoid drops to current iter or descentants
        for sourceiter in sourceiters:
            sourcepath = self.entrystore.get_path(sourceiter)

            if self.entrystore.is_ancestor(sourceiter, destiter) == True or sourcepath == destpath:
                context.finish(False, False, time)
                return

            elif pos == Gtk.TreeViewDropPosition.BEFORE and sourcepath[:-1] == destpath[:-1] and sourcepath[-1] == destpath[-1] - 1:
                context.finish(False, False, time)
                return

            elif pos == Gtk.TreeViewDropPosition.AFTER and sourcepath[:-1] == destpath[:-1] and sourcepath[-1] == destpath[-1] + 1:
                context.finish(False, False, time)
                return


        # move the entries
        if pos in ( Gtk.TreeViewDropPosition.INTO_OR_BEFORE, Gtk.TreeViewDropPosition.INTO_OR_AFTER):
            parent = destiter
            sibling = None

        elif pos == Gtk.TreeViewDropPosition.BEFORE:
            parent = self.entrystore.iter_parent(destiter)
            sibling = destiter

        elif pos == Gtk.TreeViewDropPosition.AFTER:
            parent = self.entrystore.iter_parent(destiter)

            sibpath = list(destpath)
            sibpath[-1] += 1
            sibling = self.entrystore.get_iter(sibpath)

        self.entry_move(sourceiters, parent, sibling)

        context.finish(False, False, time)


    def __cb_tree_keypress(self, widget, data = None):
        "Handles key presses for the tree"

        # return
        if data.keyval == Gdk.KEY_Return:
            self.entry_edit(self.tree.get_active())

        # insert
        elif data.keyval == Gdk.KEY_Insert:
            self.entry_add(None, self.tree.get_active())

        # delete
        elif data.keyval == Gdk.KEY_Delete:
            self.entry_remove(self.tree.get_selected())



    ##### CONFIG CALLBACKS #####

    def __cb_config_toolbar_style(self, config, value, data = None):
        "Config callback for setting toolbar style"

        if value == "both":
            self.toolbar.set_style(Gtk.ToolbarStyle.BOTH)

        elif value == "both-horiz":
            self.toolbar.set_style(Gtk.ToolbarStyle.BOTH_HORIZ)

        elif value == "icons":
            self.toolbar.set_style(Gtk.ToolbarStyle.ICONS)

        elif value == "text":
            self.toolbar.set_style(Gtk.ToolbarStyle.TEXT)

        else:
            self.toolbar.unset_style()


    #### UNDO / REDO CALLBACKS #####

    def __cb_redo_add(self, name, actiondata):
        "Redoes an add action"

        path, e = actiondata
        parent = self.entrystore.get_iter(path[:-1])
        sibling = self.entrystore.get_iter(path)

        iter = self.entrystore.add_entry(e, parent, sibling)
        self.tree.select(iter)


    def __cb_redo_edit(self, name, actiondata):
        "Redoes an edit action"

        path, preentry, postentry = actiondata
        iter = self.entrystore.get_iter(path)

        self.entrystore.update_entry(iter, postentry)
        self.tree.select(iter)


    def __cb_redo_import(self, name, actiondata):
        "Redoes an import action"

        paths, entrystore = actiondata
        self.entrystore.import_entry(entrystore, None)


    def __cb_redo_move(self, name, actiondata):
        "Redoes a move action"

        newiters = []

        for prepath, postpath in actiondata:
            prepath, postpath = list(prepath), list(postpath)

            # adjust path if necessary
            if len(prepath) <= len(postpath):
                if prepath[:-1] == postpath[:len(prepath) - 1]:
                    if prepath[-1] <= postpath[len(prepath) - 1]:
                        postpath[len(prepath) - 1] += 1

            newiter = self.entrystore.move_entry(
                self.entrystore.get_iter(prepath),
                self.entrystore.get_iter(postpath[:-1]),
                self.entrystore.get_iter(postpath)
            )

            newiters.append(newiter)

        if len(newiters) > 0:
            self.tree.select(newiters[0])


    def __cb_redo_paste(self, name, actiondata):
        "Redoes a paste action"

        entrystore, parentpath, paths = actiondata
        iters = self.entrystore.import_entry(entrystore, None, self.entrystore.get_iter(parentpath))

        if len(iters) > 0:
            self.tree.select(iters[0])


    def __cb_redo_remove(self, name, actiondata):
        "Redoes a remove action"

        iters = []
        for path, entrystore in actiondata:
            iters.append(self.entrystore.get_iter(path))

        for iter in iters:
            self.entrystore.remove_entry(iter)

        self.tree.unselect_all()


    def __cb_undo_add(self, name, actiondata):
        "Undoes an add action"

        path, e = actiondata

        self.entrystore.remove_entry(self.entrystore.get_iter(path))
        self.tree.unselect_all()


    def __cb_undo_edit(self, name, actiondata):
        "Undoes an edit action"

        path, preentry, postentry = actiondata
        iter = self.entrystore.get_iter(path)

        self.entrystore.update_entry(iter, preentry)
        self.tree.select(iter)


    def __cb_undo_import(self, name, actiondata):
        "Undoes an import action"

        paths, entrystore = actiondata
        iters = [ self.entrystore.get_iter(path) for path in paths ]

        for iter in iters:
            self.entrystore.remove_entry(iter)

        self.tree.unselect_all()


    def __cb_undo_move(self, name, actiondata):
        "Undoes a move action"

        actiondata = actiondata[:]
        actiondata.reverse()

        newiters = []

        for prepath, postpath in actiondata:
            prepath, postpath = list(prepath), list(postpath)

            # adjust path if necessary
            if len(postpath) <= len(prepath):
                if postpath[:-1] == prepath[:len(postpath) - 1]:
                    if postpath[-1] <= prepath[len(postpath) - 1]:
                        prepath[len(postpath) - 1] += 1

            newiter = self.entrystore.move_entry(
                self.entrystore.get_iter(postpath),
                self.entrystore.get_iter(prepath[:-1]),
                self.entrystore.get_iter(prepath)
            )

            newiters.append(newiter)

        if len(newiters) > 0:
            self.tree.select(newiters[-1])


    def __cb_undo_paste(self, name, actiondata):
        "Undoes a paste action"

        entrystore, parentpath, paths = actiondata
        iters = [ self.entrystore.get_iter(path) for path in paths ]

        for iter in iters:
            self.entrystore.remove_entry(iter)

        self.tree.unselect_all()


    def __cb_undo_remove(self, name, actiondata):
        "Undoes a remove action"

        iters = []
        for path, entrystore in actiondata:
            parent = self.entrystore.get_iter(path[:-1])
            sibling = self.entrystore.get_iter(path)

            iter = self.entrystore.import_entry(entrystore, entrystore.iter_nth_child(None, 0), parent, sibling)
            iters.append(iter)

        self.tree.select(iters[0])



    ##### PRIVATE METHODS #####

    def __entry_find(self, parent, string, entrytype, direction = data.SEARCH_NEXT):
        "Searches for an entry"

        match = self.entrysearch.find(string, entrytype, self.tree.get_active(), direction)
        context = self.searchbar.entry.get_style_context()

        if match != None:
            self.tree.select(match)
            self.statusbar.set_status(_('Match found for “%s”') % string)
            context.remove_class(Gtk.STYLE_CLASS_ERROR)

        else:
            self.statusbar.set_status(_('No match found for “%s”') % string)
            context.add_class(Gtk.STYLE_CLASS_ERROR)


    def __file_autosave(self):
        "Autosaves the current file if needed"

        try:
            if self.datafile.get_file() is None or self.datafile.get_password() is None:
                return

            if self.config.get_boolean("file-autosave") == False:
                return

            self.datafile.save(self.entrystore, self.datafile.get_file(), self.datafile.get_password())
            self.entrystore.changed = False

        except IOError:
            pass


    def __file_load(self, file, password, datafile = None):
        "Loads data from a data file into an entrystore"

        # We may need to change the datahandler
        old_handler = None
        result = None

        try:
            if datafile is None:
                datafile = self.datafile

                # Because there are two fileversion we need to check if we are really dealing
                # with version two. If we aren't chances are high, that we are
                # dealing with version one. In this case we use the version one
                # handler and save the file as version two if it is changed, to
                # allow seemless upgrades.
                if datafile.get_handler().detect(io.file_read(file)) != True:
                    # Store the datahandler to be reset later on
                    old_handler = datafile.get_handler()
                    # Load the revelation fileversion one handler
                    datafile.set_handler(datahandler.Revelation)
                    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()

            while True:
                try:
                    result = datafile.load(file, password, lambda: dialog.PasswordOpen(self.window, os.path.basename(file)).run())
                    break

                except datahandler.PasswordError:
                    dialog.Error(self.window, _('Incorrect password'), _('The password you entered for the file \'%s\' was not correct.') % file).run()

        except datahandler.FormatError:
            self.statusbar.set_status(_('Open failed'))
            dialog.Error(self.window, _('Invalid file format'), _('The file \'%s\' contains invalid data.') % file).run()

        except ( datahandler.DataError, entry.EntryTypeError, entry.EntryFieldError ):
            self.statusbar.set_status(_('Open failed'))
            dialog.Error(self.window, _('Unknown data'), _('The file \'%s\' contains unknown data. It may have been created by a newer version of Revelation.') % file).run()

        except datahandler.VersionError:
            self.statusbar.set_status(_('Open failed'))
            dialog.Error(self.window, _('Unknown data version'), _('The file \'%s\' has a future version number, please upgrade Revelation to open it.') % file).run()

        except datahandler.DetectError:
            self.statusbar.set_status(_('Open failed'))
            dialog.Error(self.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()

        except IOError:
            self.statusbar.set_status(_('Open failed'))
            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()

        # If we switched the datahandlers before we need to switch back to the
        # version2 handler here, to ensure a seemless version upgrade on save
        if old_handler != None:
            datafile.set_handler(old_handler.__class__)

        return result


    def __get_common_usernames(self, e = None):
        "Returns a list of possibly relevant usernames"

        ulist = []

        if e is not None and e.has_field(entry.UsernameField):
            ulist.append(e[entry.UsernameField])

        ulist.append(pwd.getpwuid(os.getuid())[0])
        ulist.extend(self.entrystore.get_popular_values(entry.UsernameField, 3))

        ulist = list({}.fromkeys(ulist).keys())
        ulist.sort()

        return ulist



    ##### PUBLIC METHODS #####

    def about(self):
        "Displays the about dialog"

        dialog.run_unique(dialog.About, self)


    def clip_chain(self, e):
        "Copies all passwords from an entry as a chain"

        if e is None:
            return

        secrets = [ field.value for field in e.fields if field.datatype == entry.DATATYPE_PASSWORD and field.value != "" ]

        if self.config.get_boolean("clipboard-chain-username") == True and len(secrets) > 0 and e.has_field(entry.UsernameField) and e[entry.UsernameField] != "":
            secrets.insert(0, e[entry.UsernameField])

        if len(secrets) == 0:
            self.statusbar.set_status(_('Entry has no password to copy'))

        else:
            self.clipboard.set(secrets, True)
            self.statusbar.set_status(_('Password copied to clipboard'))


    def clip_copy(self, iters):
        "Copies entries to the clipboard"

        self.entryclipboard.set(self.entrystore, iters)
        self.statusbar.set_status(_('Entries copied'))


    def clip_cut(self, iters):
        "Cuts entries to the clipboard"

        iters = self.entrystore.filter_parents(iters)
        self.entryclipboard.set(self.entrystore, iters)

        # store undo data (need paths)
        undoactions = []
        for iter in iters:
            undostore = data.EntryStore()
            undostore.import_entry(self.entrystore, iter)
            path = self.entrystore.get_path(iter)
            undoactions.append( ( path, undostore ) )

        # remove data
        for iter in iters:
            self.entrystore.remove_entry(iter)

        self.undoqueue.add_action(
            _('Cut entries'), self.__cb_undo_remove, self.__cb_redo_remove,
            undoactions
        )

        self.__file_autosave()

        self.tree.unselect_all()
        self.statusbar.set_status(_('Entries cut'))


    def clip_paste(self, entrystore, parent):
        "Pastes entries from the clipboard"

        if entrystore is None:
            return

        parent = self.tree.get_active()
        iters = self.entrystore.import_entry(entrystore, None, parent)

        paths = [ self.entrystore.get_path(iter) for iter in iters ]

        self.undoqueue.add_action(
            _('Paste entries'), self.__cb_undo_paste, self.__cb_redo_paste,
            ( entrystore, self.entrystore.get_path(parent), paths )
        )

        if len(iters) > 0:
            self.tree.select(iters[0])

        self.statusbar.set_status(_('Entries pasted'))


    def entry_add(self, e = None, parent = None, sibling = None):
        "Adds an entry"

        try:
            if e is None:
                d = dialog.EntryEdit(self.window, _('Add Entry'), None, self.config, self.clipboard)
                d.set_fieldwidget_data(entry.UsernameField, self.__get_common_usernames())
                e = d.run()

            iter = self.entrystore.add_entry(e, parent, sibling)

            self.undoqueue.add_action(
                _('Add entry'), self.__cb_undo_add, self.__cb_redo_add,
                ( self.entrystore.get_path(iter), e.copy() )
            )

            self.__file_autosave()
            self.tree.select(iter)
            self.statusbar.set_status(_('Entry added'))

        except dialog.CancelError:
            self.statusbar.set_status(_('Add entry cancelled'))


    def entry_edit(self, iter):
        "Edits an entry"

        try:
            if iter is None:
                return

            e = self.entrystore.get_entry(iter)

            if type(e) == entry.FolderEntry:
                d = dialog.FolderEdit(self.window, _('Edit Folder'), e)

            else:
                d = dialog.EntryEdit(self.window, _('Edit Entry'), e, self.config, self.clipboard)
                d.set_fieldwidget_data(entry.UsernameField, self.__get_common_usernames(e))


            n = d.run()
            self.entrystore.update_entry(iter, n)
            self.tree.select(iter)

            self.undoqueue.add_action(
                _('Update entry'), self.__cb_undo_edit, self.__cb_redo_edit,
                ( self.entrystore.get_path(iter), e.copy(), n.copy() )
            )

            self.__file_autosave()
            self.statusbar.set_status(_('Entry updated'))

        except dialog.CancelError:
            self.statusbar.set_status(_('Edit entry cancelled'))


    def entry_find(self):
        "Searches for an entry"

        self.config.set_boolean("view-searchbar", True)
        self.searchbar.entry.select_region(0, -1)
        self.searchbar.entry.grab_focus()


    def entry_folder(self, e = None, parent = None, sibling = None):
        "Adds a folder"

        try:
            if e is None:
                e = dialog.FolderEdit(self.window, _('Add folder')).run()

            iter = self.entrystore.add_entry(e, parent, sibling)

            self.undoqueue.add_action(
                _('Add folder'), self.__cb_undo_add, self.__cb_redo_add,
                ( self.entrystore.get_path(iter), e.copy() )
            )

            self.__file_autosave()
            self.tree.select(iter)
            self.statusbar.set_status(_('Folder added'))

        except dialog.CancelError:
            self.statusbar.set_status(_('Add folder cancelled'))


    def entry_goto(self, iters):
        "Goes to an entry"

        for iter in iters:
            try:

                # get goto data for entry
                e = self.entrystore.get_entry(iter)
                command = self.config.get_string("launcher-%s" % e.id)

                if command in ( "", None ):
                    self.statusbar.set_status(_('No goto command found for %s entries') % e.typename)
                    return

                subst = {}
                for field in e.fields:
                    subst[field.symbol] = field.value

                # copy passwords to clipboard
                chain = []

                for field in e.fields:
                    if field.datatype == entry.DATATYPE_PASSWORD and field.value != "":
                        chain.append(field.value)

                if self.config.get_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:
                    chain.insert(0, e[entry.UsernameField])

                self.clipboard.set(chain, True)

                # generate and run goto command
                command = util.parse_subst(command, subst)
                util.execute_child(command)

                self.statusbar.set_status(_('Entry opened'))

            except ( util.SubstFormatError, config.ConfigError ):
                dialog.Error(self.window, _('Invalid goto command format'), _('The goto command for \'%s\' entries is invalid, please correct it in the preferences.') % e.typename).run()

            except util.SubstValueError:
                dialog.Error(self.window, _('Missing entry data'), _('The entry \'%s\' does not have all the data required to open it.') % e.name).run()


    def entry_move(self, sourceiters, parent = None, sibling = None):
        "Moves a set of entries"

        if type(sourceiters) != list:
            sourceiters = [ sourceiters ]

        newiters = []
        undoactions = []

        for sourceiter in sourceiters:
            sourcepath = self.entrystore.get_path(sourceiter)
            newiter = self.entrystore.move_entry(sourceiter, parent, sibling)
            newpath = self.entrystore.get_path(newiter)

            undoactions.append( ( sourcepath, newpath ) )
            newiters.append(newiter)

        self.undoqueue.add_action(
            _('Move entry'), self.__cb_undo_move, self.__cb_redo_move,
            undoactions
        )

        if len(newiters) > 0:
            self.tree.select(newiters[0])

        self.__file_autosave()
        self.statusbar.set_status(_('Entries moved'))


    def entry_remove(self, iters):
        "Removes the selected entries"

        try:
            if len(iters) == 0:
                return

            entries = [ self.entrystore.get_entry(iter) for iter in iters ]
            dialog.EntryRemove(self.window, entries).run()
            iters = self.entrystore.filter_parents(iters)

            # store undo data (need paths)
            undoactions = []
            for iter in iters:
                undostore = data.EntryStore()
                undostore.import_entry(self.entrystore, iter)
                path = self.entrystore.get_path(iter)
                undoactions.append( ( path, undostore ) )

            # remove data
            for iter in iters:
                self.entrystore.remove_entry(iter)

            self.undoqueue.add_action(
                _('Remove entry'), self.__cb_undo_remove, self.__cb_redo_remove,
                undoactions
            )

            self.tree.unselect_all()
            self.__file_autosave()
            self.statusbar.set_status(_('Entries removed'))

        except dialog.CancelError:
            self.statusbar.set_status(_('Entry removal cancelled'))


    def file_change_password(self, password = None):
        "Changes the password of the current data file"

        try:
            if password is None:
                password = dialog.PasswordChange(self.window, self.datafile.get_password()).run()

            self.datafile.set_password(password)
            self.entrystore.changed = True

            self.__file_autosave()
            self.statusbar.set_status(_('Password changed'))

        except dialog.CancelError:
            self.statusbar.set_status(_('Password change cancelled'))

    def file_close(self):
        "Closes the current file"

        try:
            if self.entrystore.changed == True and dialog.FileChangesClose(self.window).run() == True:
                if self.file_save(self.datafile.get_file(), self.datafile.get_password()) == False:
                    raise dialog.CancelError

            self.clipboard.clear()
            self.entryclipboard.clear()
            self.entrystore.clear()
            self.undoqueue.clear()
            self.statusbar.set_status(_('Closed file %s') % self.datafile.get_file())
            self.datafile.close()

            return True

        except dialog.CancelError:
            self.statusbar.set_status(_('Close file cancelled'))
            return False

    def file_export(self):
        "Exports data to a foreign file format"

        try:
            file, handler = dialog.ExportFileSelector(self.window).run()
            datafile = io.DataFile(handler)

            if datafile.get_handler().encryption == True:
                password = dialog.PasswordSave(self.window, file).run()

            else:
                dialog.FileSaveInsecure(self.window).run()
                password = None

            datafile.save(self.entrystore, file, password)
            self.statusbar.set_status(_('Data exported to %s') % datafile.get_file())

        except dialog.CancelError:
            self.statusbar.set_status(_('Export cancelled'))

        except IOError:
            dialog.Error(self.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()
            self.statusbar.set_status(_('Export failed'))


    def file_import(self):
        "Imports data from a foreign file"

        try:
            file, handler = dialog.ImportFileSelector(self.window).run()
            datafile = io.DataFile(handler)
            entrystore = self.__file_load(file, None, datafile)

            if entrystore is not None:
                newiters = self.entrystore.import_entry(entrystore, None)
                paths = [ self.entrystore.get_path(iter) for iter in newiters ]

                self.undoqueue.add_action(
                    _('Import data'), self.__cb_undo_import, self.__cb_redo_import,
                    ( paths, entrystore )
                )

                self.statusbar.set_status(_('Data imported from %s') % datafile.get_file())

            self.__file_autosave()

        except dialog.CancelError:
            self.statusbar.set_status(_('Import cancelled'))


    def file_lock(self):
        "Locks the current file"

        if self.datafile.get_file() is None:
            return

        if self.file_locked is True:
            return

        password = self.datafile.get_password()

        if password is None:
            return

        self.locktimer.stop()
        app = Gio.Application.get_default
        app().get_dbus_connection().signal_unsubscribe(self.dbus_subscription_id)


        # TODO can this be done more elegantly?
        transients = [ window for window in Gtk.Window.list_toplevels() if window.get_transient_for() == self ]

        # store current state
        activeiter = self.tree.get_active()
        oldtitle = self.get_title()

        # clear application contents
        self.tree.set_model(None)
        self.entryview.clear()
        self.window.set_title('[' + _('Locked') + ']')
        self.statusbar.set_status(_('File locked'))
        self.file_locked = True;

        # hide any dialogs
        for window in transients:
            window.hide()

        # lock file
        try:
            d = dialog.PasswordLock(self.window, password)

            if self.entrystore.changed == True:
                l = ui.ImageLabel(_('Quit disabled due to unsaved changes'), "dialog-warning")
                d.contents.pack_start(l, True, True, 0)
                d.get_widget_for_response(Gtk.ResponseType.CANCEL).set_sensitive(False)

            d.run()

        except dialog.CancelError:
            self.quit()

        # unlock the file and restore state
        self.tree.set_model(self.entrystore)
        self.tree.select(activeiter)
        self.window.set_title(oldtitle)
        self.statusbar.set_status(_('File unlocked'))
        self.file_locked = False;

        for window in transients:
            window.show()

        self.locktimer.start(self.config.get_int("file-autolock-timeout") * 60)
        app = Gio.Application.get_default
        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)



    def file_new(self):
        "Opens a new file"

        try:
            if self.entrystore.changed == True and dialog.FileChangesNew(self.window).run() == True:
                if self.file_save(self.datafile.get_file(), self.datafile.get_password()) == False:
                    raise dialog.CancelError

            self.entrystore.clear()
            self.datafile.close()
            self.undoqueue.clear()
            self.statusbar.set_status(_('New file created'))

        except dialog.CancelError:
            self.statusbar.set_status(_('New file cancelled'))


    def file_open(self, file = None, password = None):
        "Opens a data file"

        try:
            if self.entrystore.changed == True and dialog.FileChangesOpen(self.window).run() == True:
                if self.file_save(self.datafile.get_file(), self.datafile.get_password()) == False:
                    raise dialog.CancelError

            if file is None:
                file = dialog.OpenFileSelector(self.window).run()

            entrystore = self.__file_load(file, password)

            if entrystore is None:
                return

            self.entrystore.clear()
            self.entrystore.import_entry(entrystore, None)
            self.entrystore.changed = False
            self.undoqueue.clear()

            self.file_locked = False;
            self.locktimer.start(60 * self.config.get_int("file-autolock-timeout"))
            self.statusbar.set_status(_('Opened file %s') % self.datafile.get_file())

        except dialog.CancelError:
            self.statusbar.set_status(_('Open cancelled'))


    def file_save(self, file = None, password = None):
        "Saves data to a file"

        try:
            if file is None:
                file = dialog.SaveFileSelector(self.window).run()

            if password is None:
                password = dialog.PasswordSave(self.window, file).run()

            self.datafile.save(self.entrystore, file, password)
            self.entrystore.changed = False
            self.statusbar.set_status(_('Data saved to file %s') % file)

            return True

        except dialog.CancelError:
            self.statusbar.set_status(_('Save cancelled'))
            return False

        except IOError:
            dialog.Error(self.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()
            self.statusbar.set_status(_('Save failed'))
            return False


    def prefs(self):
        "Displays the application preferences"

        dialog.run_unique(Preferences, self.window, self.config)


    def pwcheck(self):
        "Displays the password checking dialog"

        dialog.run_unique(dialog.PasswordChecker, self.window, self.config, self.clipboard)


    def pwgen(self):
        "Displays the password generator dialog"

        dialog.run_unique(dialog.PasswordGenerator, self.window, self.config, self.clipboard)


    def quit(self):
        "Quits the application"

        try:
            if self.entrystore.changed == True and dialog.FileChangesQuit(self.window).run() == True:
                if self.file_save(self.datafile.get_file(), self.datafile.get_password()) == False:
                    raise dialog.CancelError

            self.clipboard.clear()
            self.entryclipboard.clear()

            self.__save_state()

            Gtk.Application.quit(self)
            if sys.exc_info()[1]:
                # avoid raising an additional exception
                os._exit(0)
            else:
                sys.exit(0)
            return True

        except dialog.CancelError:
            self.statusbar.set_status(_('Quit cancelled'))
            return False


    def redo(self):
        "Redoes the previous action"

        action = self.undoqueue.get_redo_action()

        if action is None:
            return

        self.undoqueue.redo()
        self.statusbar.set_status(_('%s redone') % action[1])
        self.__file_autosave()


    def undo(self):
        "Undoes the previous action"

        action = self.undoqueue.get_undo_action()

        if action is None:
            return

        self.undoqueue.undo()
        self.statusbar.set_status(_('%s undone') % action[1])
        self.__file_autosave()



class Preferences(dialog.Utility):
    "A preference dialog"

    def __init__(self, parent, cfg=None):
        dialog.Utility.__init__(self, parent, _('Preferences'))
        self.add_button(Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE)
        self.set_default_response(Gtk.ResponseType.CLOSE)
        self.config = cfg
        self.set_modal(False)

        self.notebook = ui.Notebook()
        self.vbox.pack_start(self.notebook, True, True, 0)

        self.page_general = self.notebook.create_page(_('General'))
        self.__init_section_files(self.page_general)
        self.__init_section_password(self.page_general)

        self.page_interface = self.notebook.create_page(_('Interface'))
        self.__init_section_doubleclick(self.page_interface)
        self.__init_section_toolbar(self.page_interface)

        self.page_gotocmd = self.notebook.create_page(_('Goto Commands'))
        self.notebook.get_tab_label(self.page_gotocmd).set_hexpand(True)
        self.__init_section_gotocmd(self.page_gotocmd)

        self.connect("response", lambda w,d: self.destroy())


    def __init_section_doubleclick(self, page):
        "Sets up the doubleclick section"

        self.section_doubleclick = page.add_section(_('Doubleclick Action'))

        # radio-button for go to
        self.radio_doubleclick_goto = Gtk.RadioButton.new_with_label_from_widget(None, _('Go to account, if possible'))
        self.radio_doubleclick_goto.connect("toggled", lambda w: w.get_active() and self.config.set_string("behavior-doubleclick", "goto"))

        self.radio_doubleclick_goto.set_tooltip_text(_('Go to the account (open in external application) on doubleclick, if required data is filled in'))
        self.section_doubleclick.append_widget(None, self.radio_doubleclick_goto)

        # radio-button for edit
        self.radio_doubleclick_edit = Gtk.RadioButton.new_with_label_from_widget(self.radio_doubleclick_goto, label=_('Edit account'))
        self.radio_doubleclick_edit.connect("toggled", lambda w: w.get_active() and self.config.set_string("behavior-doubleclick", "edit"))

        self.radio_doubleclick_edit.set_tooltip_text(_('Edit the account on doubleclick'))
        self.section_doubleclick.append_widget(None, self.radio_doubleclick_edit)

        # radio-button for copy
        self.radio_doubleclick_copy = Gtk.RadioButton.new_with_label_from_widget(self.radio_doubleclick_goto, label=_('Copy password to clipboard'))
        self.radio_doubleclick_copy.connect("toggled", lambda w: w.get_active() and self.config.set_string("behavior-doubleclick", "copy"))

        self.radio_doubleclick_copy.set_tooltip_text(_('Copy the account password to clipboard on doubleclick'))
        self.section_doubleclick.append_widget(None, self.radio_doubleclick_copy)

        {"goto":self.radio_doubleclick_goto,
         "edit":self.radio_doubleclick_edit,
         "copy":self.radio_doubleclick_copy}[self.config.get_string("behavior-doubleclick")].set_active(True)


    def __init_section_files(self, page):
        "Sets up the files section"

        self.section_files = page.add_section(_('Files'))

        # checkbutton and file button for autoloading a file
        self.check_autoload = Gtk.CheckButton(label=_('Open file on startup:'))
        self.config.bind("file-autoload", self.check_autoload, "active", Gio.SettingsBindFlags.DEFAULT)

        self.check_autoload.connect("toggled", lambda w: self.button_autoload_file.set_sensitive(w.get_active()))
        self.check_autoload.set_tooltip_text(_('When enabled, this file will be opened when the program is started'))

        self.button_autoload_file = Gtk.FileChooserButton(title=_('Select File to Automatically Open'))
        if self.config.get_boolean("file-autoload"):
            self.button_autoload_file.set_filename(self.config.get_string("file-autoload-file"))
        self.button_autoload_file.connect('file-set', lambda w: self.config.set_string("file-autoload-file", w.get_filename()))
        self.config.connect("changed::autoload-file", lambda w, fname: self.button_autoload_file.set_filename(w.get_string(fname)))
        self.button_autoload_file.set_sensitive(self.check_autoload.get_active())

        filter = Gtk.FileFilter()
        filter.set_name(_('Revelation files'))
        filter.add_mime_type("application/x-revelation")
        self.button_autoload_file.add_filter(filter)

        filter = Gtk.FileFilter()
        filter.set_name(_('All files'))
        filter.add_pattern("*")
        self.button_autoload_file.add_filter(filter)

        eventbox = ui.EventBox(self.button_autoload_file)
        eventbox.set_tooltip_text(_('File to open when Revelation is started'))

        hbox = ui.HBox()
        hbox.pack_start(self.check_autoload, False, False, 0)
        hbox.pack_start(eventbox, True, True, 0)
        self.section_files.append_widget(None, hbox)

        # check-button for autosave
        self.check_autosave = Gtk.CheckButton(label=_('Automatically save data when changed'))
        self.config.bind("file-autosave", self.check_autosave, "active", Gio.SettingsBindFlags.DEFAULT)

        self.check_autosave.set_tooltip_text(_('Automatically save the data file when an entry is added, modified or removed'))
        self.section_files.append_widget(None, self.check_autosave)

        # autolock file
        self.check_autolock = Gtk.CheckButton(label=_('Lock file when inactive for'))
        self.config.bind("file-autolock", self.check_autolock, "active", Gio.SettingsBindFlags.DEFAULT)
        self.check_autolock.connect("toggled", lambda w: self.spin_autolock_timeout.set_sensitive(w.get_active()))
        self.check_autolock.set_tooltip_text(_('Automatically lock the data file after a period of inactivity'))

        self.spin_autolock_timeout = ui.SpinEntry()
        self.spin_autolock_timeout.set_range(1, 120)
        self.spin_autolock_timeout.set_sensitive(self.check_autolock.get_active())
        self.config.bind("file-autolock-timeout", self.spin_autolock_timeout, "value", Gio.SettingsBindFlags.DEFAULT)
        self.spin_autolock_timeout.set_tooltip_text(_('The period of inactivity before locking the file, in minutes'))

        hbox = ui.HBox()
        hbox.set_spacing(3)
        hbox.pack_start(self.check_autolock, False, False, 0)
        hbox.pack_start(self.spin_autolock_timeout, False, False, 0)
        hbox.pack_start(ui.Label(_('minutes')), True, True, 0)
        self.section_files.append_widget(None, hbox)


    def __init_section_gotocmd(self, page):
        "Sets up the goto command section"

        self.section_gotocmd = page.add_section(_('Goto Commands'))

        for entrytype in entry.ENTRYLIST:
            if entrytype == entry.FolderEntry:
                continue

            e = entrytype()

            widget = ui.Entry()
            self.config.bind("launcher-"+e.id, widget, "text", Gio.SettingsBindFlags.DEFAULT)

            tooltip = _('Goto command for %s accounts. The following expansion variables can be used:') % e.typename + "\n\n"

            for field in e.fields:
                tooltip += "%%%s: %s\n" % ( field.symbol, field.name )

            tooltip += "\n"
            tooltip += _('%%: a "%" sign') + "\n"
            tooltip += _('%?x: optional expansion variable') + "\n"
            tooltip += _('%(...%): optional substring expansion')

            widget.set_tooltip_text(tooltip)
            self.section_gotocmd.append_widget(e.typename, widget)


    def __init_section_password(self, page):
        "Sets up the password section"

        self.section_password = page.add_section(_('Passwords'))

        # show passwords checkbutton
        self.check_show_passwords = Gtk.CheckButton(label=_('Display passwords and other secrets'))
        self.config.bind("view-passwords", self.check_show_passwords, "active", Gio.SettingsBindFlags.DEFAULT)

        self.check_show_passwords.set_tooltip_text(_('Display passwords and other secrets, such as PIN codes (otherwise, hide with ******)'))
        self.section_password.append_widget(None, self.check_show_passwords)

        # chain username checkbutton
        self.check_chain_username = Gtk.CheckButton(label=_('Also copy username when copying password'))
        self.config.bind("clipboard-chain-username", self.check_chain_username, "active", Gio.SettingsBindFlags.DEFAULT)

        self.check_chain_username.set_tooltip_text(_('When the password is copied to clipboard, put the username before the password as a clipboard "chain"'))
        self.section_password.append_widget(None, self.check_chain_username)

        # use punctuation chars checkbutton
        self.check_punctuation_chars = Gtk.CheckButton(label=_('Use punctuation characters for passwords'))
        self.config.bind("passwordgen-punctuation", self.check_punctuation_chars, "active", Gio.SettingsBindFlags.DEFAULT)

        self.check_punctuation_chars.set_tooltip_text(_('When passwords are generated, use punctuation characters like %, =, { or .'))
        self.section_password.append_widget(None, self.check_punctuation_chars)

        # password length spinbutton
        self.spin_pwlen = ui.SpinEntry()
        self.spin_pwlen.set_range(4, 32)
        self.config.bind("passwordgen-length", self.spin_pwlen, "value", Gio.SettingsBindFlags.DEFAULT)

        self.spin_pwlen.set_tooltip_text(_('The number of characters in generated passwords - 8 or more are recommended'))
        self.section_password.append_widget(_('Length of generated passwords'), self.spin_pwlen)


    def __init_section_toolbar(self, page):
        "Sets up the toolbar section"

        self.section_toolbar = page.add_section(_('Toolbar Style'))

        # radio-button for desktop default
        self.radio_toolbar_desktop = Gtk.RadioButton.new_with_label_from_widget(None, _('Use desktop default'))
        self.radio_toolbar_desktop.connect("toggled", lambda w: w.get_active() and self.config.set_string("view-toolbar-style", "desktop"))

        self.radio_toolbar_desktop.set_tooltip_text(_('Show toolbar items with default style'))
        self.section_toolbar.append_widget(None, self.radio_toolbar_desktop)

        # radio-button for icons and text
        self.radio_toolbar_both = Gtk.RadioButton.new_with_label_from_widget(self.radio_toolbar_desktop, _('Show icons and text'))
        self.radio_toolbar_both.connect("toggled", lambda w: w.get_active() and self.config.set_string("view-toolbar-style", "both"))

        self.radio_toolbar_both.set_tooltip_text(_('Show toolbar items with both icons and text'))
        self.section_toolbar.append_widget(None, self.radio_toolbar_both)

        # radio-button for icons and important text
        self.radio_toolbar_bothhoriz = Gtk.RadioButton.new_with_label_from_widget(self.radio_toolbar_desktop, _('Show icons and important text'))
        self.radio_toolbar_bothhoriz.connect("toggled", lambda w: w.get_active() and self.config.set_string("view-toolbar-style", "both-horiz"))

        self.radio_toolbar_bothhoriz.set_tooltip_text(_('Show toolbar items with text beside important icons'))
        self.section_toolbar.append_widget(None, self.radio_toolbar_bothhoriz)

        # radio-button for icons only
        self.radio_toolbar_icons = Gtk.RadioButton.new_with_label_from_widget(self.radio_toolbar_desktop, _('Show icons only'))
        self.radio_toolbar_icons.connect("toggled", lambda w: w.get_active() and self.config.set_string("view-toolbar-style", "icons"))

        self.radio_toolbar_icons.set_tooltip_text(_('Show toolbar items with icons only'))
        self.section_toolbar.append_widget(None, self.radio_toolbar_icons)

        # radio-button for text only
        self.radio_toolbar_text = Gtk.RadioButton.new_with_label_from_widget(self.radio_toolbar_desktop, _('Show text only'))
        self.radio_toolbar_text.connect("toggled", lambda w: w.get_active() and self.config.set_string("view-toolbar-style", "text"))

        self.radio_toolbar_text.set_tooltip_text(_('Show toolbar items with text only'))
        self.section_toolbar.append_widget(None, self.radio_toolbar_text)

        {"desktop":    self.radio_toolbar_desktop,
         "both":       self.radio_toolbar_both,
         "both-horiz": self.radio_toolbar_bothhoriz,
         "icons":      self.radio_toolbar_icons,
         "text":       self.radio_toolbar_text
        }[self.config.get_string("view-toolbar-style")].set_active(True)


    def run(self):
        "Runs the preference dialog"

        self.show_all()

        # for some reason, Gtk crashes on close-by-escape unless we do this
        self.get_widget_for_response(Gtk.ResponseType.CLOSE).grab_focus()
        self.notebook.grab_focus()



if __name__ == "__main__":
    app = Revelation()
    app.set_flags(Gio.ApplicationFlags.NON_UNIQUE)
    app.run()

