Commits

Mikel Olasagasti Uranga committed 7ec0ae7

Rename to .py so intltool-update can extract gettext strings

  • Participants
  • Parent commits 0f1deb8

Comments (0)

Files changed (3)

File po/POTFILES.in

-src/revelation.in
-src/revelation-applet.in
+src/revelation.py
+src/revelation-applet.py
 src/lib/config.py.in
 src/lib/dialog.py
 src/lib/entry.py

File src/revelation-applet.py

+#!/usr/bin/env python
+
+#
+# Revelation - a password manager for GNOME 2
+# http://oss.codepoet.no/revelation/
+# $Id$
+#
+# Applet for account lookup
+#
+#
+# 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 gettext, gnome, gnomeapplet, gobject, gtk, os, sys
+
+if "@pyexecdir@" not in sys.path:
+	sys.path.insert(0, "@pyexecdir@")
+
+from revelation import config, data, datahandler, dialog, entry, io, ui, util
+
+_ = gettext.gettext
+
+
+class RevelationApplet(object):
+	"Revelation applet"
+
+	def __init__(self, applet, iid):
+		self.applet	= applet
+		self.iid	= iid
+
+		sys.excepthook	= self.__cb_exception
+
+		gettext.bindtextdomain(config.PACKAGE, config.DIR_LOCALE)
+		gettext.bind_textdomain_codeset(config.PACKAGE, "UTF-8")
+		gettext.textdomain(config.PACKAGE)
+
+		try:
+			self.__init_config()
+			self.__init_facilities()
+			self.__init_ui()
+			self.__init_states()
+
+		except config.ConfigError:
+			dialog.Error(None, _('Missing configuration data'), _('The applet could not find its configuration data, please reinstall Revelation.')).run()
+			sys.exit(1)
+
+
+	def __init_config(self):
+		"Sets up configuration"
+
+		self.applet.add_preferences("/schemas/apps/revelation-applet/prefs")
+		self.config = config.Config(self.applet.get_preferences_key())
+
+		# set up defaults
+		# TODO this shouldn't really be necessary, the schema should
+		# be used for defaults - is this even possible with the current
+		# applet api?
+		defaults = {
+			"autolock"		: True,
+			"autolock_timeout"	: 10,
+			"chain_username"	: False,
+			"file"			: "",
+			"menuaction"		: "show",
+			"show_passwords"	: True,
+			"show_searchentry"	: True
+		}
+
+		for key, value in defaults.items():
+			try:
+				self.config.get(key)
+
+			except config.ConfigError:
+				self.config.set_force(key, value)
+
+		# make sure the launchers have been set up, otherwise
+		# install the Revelation schema
+		def check_launchers():
+			try:
+				for entrytype in [ et() for et in entry.ENTRYLIST if et != entry.FolderEntry ]:
+					self.config.get("/apps/revelation/launcher/%s" % entrytype.id)
+
+			except config.ConfigError:
+				return False
+
+			else:
+				return True
+
+
+		if check_launchers() == False:
+			if config.install_schema("%s/revelation.schemas" % config.DIR_GCONFSCHEMAS) == False:
+				raise config.ConfigError
+
+			if check_launchers() == False:
+				raise config.ConfigError
+
+
+	def __init_facilities(self):
+		"Sets up facilities"
+
+		self.clipboard		= data.Clipboard()
+		self.datafile		= io.DataFile(datahandler.Revelation)
+		self.entrystore		= data.EntryStore()
+		self.entrysearch	= data.EntrySearch(self.entrystore)
+		self.items		= ui.ItemFactory(self.applet)
+		self.locktimer		= data.Timer()
+
+		self.config.monitor("autolock_timeout", lambda k,v,d: self.locktimer.start(v * 60))
+		self.config.monitor("file", self.__cb_config_file)
+
+		self.datafile.connect("changed", self.__cb_file_changed)
+		self.datafile.connect("content-changed", self.__cb_file_content_changed)
+		self.locktimer.connect("ring", self.__cb_file_autolock)
+
+		self.entrysearch.folders = False
+
+
+	def __init_states(self):
+		"Sets up the initial states"
+
+		self.datafile.emit("changed", self.datafile.get_file())
+		os.chdir(os.path.expanduser("~/"))
+
+		self.config.monitor("show_searchentry", self.__cb_config_show_searchentry)
+
+
+	def __init_ui(self):
+		"Sets up the main ui"
+
+		gtk.about_dialog_set_url_hook(lambda d,l: gtk.show_uri(None, l, gtk.get_current_event_time()))
+		gtk.about_dialog_set_email_hook(lambda d,l: gtk.show_uri(None, "mailto:" + l, gtk.get_current_event_time()))
+
+		# set up applet
+		self.applet.set_flags(gnomeapplet.EXPAND_MINOR)
+
+		# set up window icons
+		pixbufs = [ self.items.get_pixbuf("revelation", size) for size in ( 48, 32, 24, 16) ]
+		pixbufs = [ pixbuf for pixbuf in pixbufs if pixbuf != None ]
+
+		if len(pixbufs) > 0:
+			gtk.window_set_default_icon_list(*pixbufs)
+
+		# set up popup menu
+		self.applet.setup_menu("""
+			<popup name="button3">
+				<menuitem name="file-unlock"	verb="file-unlock"	label=\"""" + _('Unlock File') + """\"		pixtype="stock" pixname="revelation-unlock" />
+				<menuitem name="file-lock"	verb="file-lock"	label=\"""" + _('Lock File') + """\"		pixtype="stock" pixname="revelation-lock" />
+				<menuitem name="file-reload"	verb="file-reload"	label=\"""" + _('Reload File') + """\"		pixtype="stock" pixname="revelation-reload" />
+				<separator />
+				<menuitem name="revelation"	verb="revelation"	label=\"""" + _('Start Revelation') + """\"	pixtype="stock" pixname="revelation-revelation" />
+				<menuitem name="prefs"		verb="prefs"		label=\"""" + _('Preferences') + """\"		pixtype="stock"	pixname="gtk-properties" />
+				<menuitem name="about"		verb="about"		label=\"""" + _('About') + """\"		pixtype="stock"	pixname="gnome-stock-about" />
+			</popup>
+		""", (
+			( "about",		lambda w,d=None: self.about() ),
+			( "file-lock",		lambda w,d=None: self.file_close() ),
+			( "file-reload",	lambda w,d=None: self.file_reload() ),
+			( "file-unlock",	lambda w,d=None: self.file_open(self.config.get("file")) ),
+			( "prefs",		lambda w,d=None: self.prefs() ),
+			( "revelation",		lambda w,d=None: util.execute_child("@bindir@/revelation") ),
+		), None)
+
+		# set up ui items
+		self.entry = ui.Entry()
+		self.entry.set_width_chars(14)
+		self.entry.connect("activate", self.__cb_entry_activate)
+		self.entry.connect("button_press_event", self.__cb_entry_buttonpress)
+		self.entry.connect("key_press_event", lambda w,d=None: self.locktimer.reset())
+
+		self.icon = ui.Image()
+		self.eventbox = ui.EventBox(self.icon)
+		self.eventbox.connect("button_press_event", self.__cb_icon_buttonpress)
+
+		self.hbox = ui.HBox(self.eventbox, self.entry)
+		self.applet.add(self.hbox)
+		
+		# handle Gnome Panel background
+		self.applet.connect("change-background",self.panel_bg)
+
+		self.applet.show_all()
+
+		# set up various ui element holders
+		self.popup_entryview	= None
+		self.popup_entrylist	= None
+
+		self.entrymenu		= None
+
+
+
+	##### CALLBACKS #####
+
+	def __cb_config_file(self, key, value, data):
+		"Config callback for file key changes"
+
+		self.file_close()
+		self.applet.get_popup_component().set_prop("/commands/file-unlock", "sensitive", self.config.get("file") != "" and "1" or "0")
+
+
+	def __cb_config_show_searchentry(self, key, value, data):
+		"Config callback for show searchentry setting"
+
+		if value == True:
+				self.entry.show()
+
+		else:
+				self.entry.hide()
+
+
+	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(None, traceback).run() == True:
+			gtk.main()
+
+		else:
+			sys.exit(1)
+
+
+	def __cb_entry_activate(self, widget, data = None):
+		"Callback for entry activation (pressing enter etc)"
+
+		self.entry_search(self.entry.get_text(), True)
+
+
+	def __cb_entry_buttonpress(self, widget, data = None):
+		"Callback for entry button presses"
+
+		self.locktimer.reset()
+
+		if data.button == 1:
+			self.applet.request_focus(data.time)
+
+
+	def __cb_file_autolock(self, widget, data = None):
+		"Callback for autolocking the file"
+
+		if self.config.get("autolock") == True:
+			self.file_close()
+
+
+	def __cb_file_content_changed(self, widget, data = None):
+		"Callback for changed file content"
+
+		try:
+			self.__file_load(self.datafile.get_file(), self.datafile.get_password())
+
+		except dialog.CancelError:
+			pass
+
+		except datahandler.PasswordError:
+			self.file_close()
+
+		except datahandler.Error:
+			pass
+
+
+	def __cb_file_changed(self, widget, data = None):
+		"Callback for changed data file"
+
+		popup = self.applet.get_popup_component()
+
+		if self.datafile.get_file() == None:
+			self.entry.set_text("")
+
+			popup.set_prop("/commands/file-unlock", "sensitive", self.config.get("file") != "" and "1" or "0")
+			popup.set_prop("/commands/file-lock", "sensitive", "0")
+			popup.set_prop("/commands/file-reload", "sensitive", "0")
+
+			self.icon.set_from_stock(ui.STOCK_REVELATION_LOCKED, ui.ICON_SIZE_APPLET)
+
+		else:
+			popup.set_prop("/commands/file-unlock", "sensitive", "0")
+			popup.set_prop("/commands/file-lock", "sensitive", "1")
+			popup.set_prop("/commands/file-reload", "sensitive", "1")
+
+			self.icon.set_from_stock(ui.STOCK_REVELATION, ui.ICON_SIZE_APPLET)
+
+
+	def __cb_icon_buttonpress(self, widget, data = None):
+		"Callback for buttonpress on button"
+
+		if data.button != 1:
+			return False
+
+		self.entry_menu(data.time)
+
+		return True
+
+
+	def __cb_popup_activate(self, widget, data = None):
+		"Takes appropriate action when a menu item is activated"
+
+		self.locktimer.reset()
+
+		action = self.config.get("menuaction")
+
+		if action == "show":
+			self.entry_show(data)
+
+		elif action == "copy":
+			self.entry_copychain(data)
+
+		elif self.__launcher_valid(data):
+			self.entry_goto(data)
+
+		else:
+			self.entry_show(data)
+
+    # Handle Gnome Panel background
+	def panel_bg(self, applet, bg_type, color, pixmap):
+		# Reset styles
+		rc_style = gtk.RcStyle()
+		self.applet.set_style(None)
+		self.eventbox.set_style(None)
+		self.entry.set_style(None)
+		
+		self.applet.modify_style(rc_style)
+		self.eventbox.modify_style(rc_style)
+		self.entry.modify_style(rc_style)
+		
+		if bg_type == gnomeapplet.PIXMAP_BACKGROUND:
+			style = self.applet.get_style()
+			style.bg_pixmap[gtk.STATE_NORMAL] = pixmap
+			self.applet.set_style(style)
+			self.eventbox.set_style(style)
+			self.entry.set_style(style)
+		if bg_type == gnomeapplet.COLOR_BACKGROUND:
+			self.applet.modify_bg(gtk.STATE_NORMAL, color)
+			self.eventbox.modify_bg(gtk.STATE_NORMAL, color)
+			self.entry.modify_bg(gtk.STATE_NORMAL, color)
+
+
+
+	##### PRIVATE METHODS #####
+
+	def __close_popups(self):
+		"Closes any open popups"
+
+		self.locktimer.reset()
+
+		if hasattr(self, "popup_entryview") == True and self.popup_entryview != None:
+			self.popup_entryview.destroy()
+
+		if hasattr(self, "popup_entrylist") == True and self.popup_entrylist != None:
+			self.popup_entrylist.destroy()
+
+		if hasattr(self, "entrymenu") == True and self.entrymenu != None:
+			self.entrymenu.hide()
+
+
+	def __file_load(self, file, password = None):
+		"Loads a data file"
+
+		if file in ( "", None ):
+			return False
+
+		if dialog.present_unique(dialog.PasswordOpen) == True:
+			return False
+
+		entrystore = self.datafile.load(file, password, lambda: dialog.run_unique(dialog.PasswordOpen, None, os.path.basename(file)))
+
+		self.entrystore.clear()
+		self.entrystore.import_entry(entrystore, None)
+
+		self.entrymenu = self.__generate_entrymenu(self.entrystore)
+		self.locktimer.start(self.config.get("autolock_timeout") * 60)
+
+		self.__close_popups()
+
+		return True
+
+
+	def __flash_entry(self, color = "#ffbaba", duration = 500):
+		"Flashes the entry with a color"
+
+		color_normal	= ui.Entry().rc_get_style().base[gtk.STATE_NORMAL]
+		color_new	= gtk.gdk.color_parse(color)
+
+		self.entry.modify_base(gtk.STATE_NORMAL, color_new)
+		gobject.timeout_add(duration, lambda: self.entry.modify_base(gtk.STATE_NORMAL, color_normal))
+
+
+	def __focus_entry(self):
+		"Gives focus to the entry"
+
+		self.applet.request_focus(long(0))
+
+
+	def __generate_entrymenu(self, entrystore, parent = None):
+		"Generates an entry menu tree"
+
+		menu = gtk.Menu()
+
+		for i in range(entrystore.iter_n_children(parent)):
+			iter = entrystore.iter_nth_child(parent, i)
+
+			e = entrystore.get_entry(iter)
+			item = ui.ImageMenuItem(type(e) == entry.FolderEntry and ui.STOCK_FOLDER or e.icon, e.name)
+			item.connect("select", lambda w,d=None: self.locktimer.reset())
+
+			if type(e) == entry.FolderEntry:
+				item.set_submenu(self.__generate_entrymenu(entrystore, iter))
+
+			else:
+				item.connect("activate", self.__cb_popup_activate, e)
+
+			menu.append(item)
+
+		return menu
+
+
+	def __get_launcher(self, e):
+		"Returns a launcher command for an entry, if possible"
+
+		command = self.config.get("/apps/revelation/launcher/%s" % e.id)
+
+		if command in ( "", None ):
+			return None
+
+		subst = {}
+		for field in e.fields:
+			subst[field.symbol] = field.value
+
+		command = util.parse_subst(command, subst)
+
+		return command
+
+
+	def __get_popup_offset(self, popup):
+		"Returns a tuple of x and y offset coords for popups"
+
+		screen	= self.applet.get_screen()
+		a	= self.applet.get_allocation()
+		rw, rh	= popup.size_request()
+
+		x, y	= self.applet.window.get_origin()
+		x	+= a.x
+		y	+= a.y
+
+
+		# TODO use constants ORIENT_UP etc here, if available
+		if self.applet.get_orient() in ( 0, 1 ):
+			x = min(x, screen.get_width() - rw)
+
+			if (y > screen.get_height() / 2):
+				y -= rh
+
+			else:
+				y += a.height
+
+		else:
+			y = min(y, screen.get_height() - rh)
+
+			if (x > screen.get_width() / 2):
+				x -= rw
+
+			else:
+				x += a.width
+
+		return x, y
+
+
+	def __launcher_valid(self, e):
+		"Checks if a launcher is valid"
+
+		try:
+			command = self.__get_launcher(e)
+
+			return command != None
+
+		except ( util.SubstFormatError ):
+			return True
+
+		except ( util.SubstValueError, config.ConfigError ):
+			return False
+
+
+	def __require_file(self):
+		"Checks if a datafile is loaded, or alerts the user"
+
+		if self.datafile.get_file() != None:
+			return True
+
+		if self.config.get("file") != "":
+			return self.file_open(self.config.get("file"))
+
+		d = dialog.Info(
+			None, _('File not selected'),
+			_('You must select a Revelation data file to use - this can be done in the applet preferences.'),
+			( ( gtk.STOCK_PREFERENCES, gtk.RESPONSE_ACCEPT ), ( gtk.STOCK_OK, gtk.RESPONSE_OK ) )
+		)
+
+
+		if d.run() == gtk.RESPONSE_ACCEPT:
+			self.prefs()
+
+		return False
+
+
+	##### PUBLIC METHODS #####
+
+	def about(self):
+		"Displays an about dialog"
+
+		dialog.run_unique(About, self.applet)
+
+
+	def entry_copychain(self, e, launcher = ""):
+		"Copies all passwords from an entry as a chain"
+
+		if e == None:
+			return
+
+		secrets = [ field.value for field in e.fields if field.datatype == entry.DATATYPE_PASSWORD and field.value != "" ]
+
+		if self.config.get("chain_username") == True and len(secrets) > 0:
+			if e.has_field(entry.UsernameField) and e[entry.UsernameField] != "":
+				if "%" + entry.UsernameField.symbol not in launcher:
+					secrets.insert(0, e[entry.UsernameField])
+
+		self.clipboard.set(secrets)
+
+
+	def entry_goto(self, e):
+		"Goes to an entry"
+
+		try:
+			command = self.__get_launcher(e)
+
+			if command == None:
+				return
+
+			self.entry_copychain(e)
+
+			util.execute_child(command)
+
+		except ( util.SubstFormatError, config.ConfigError ):
+			dialog.Error(None, _('Invalid goto command format'), _('The goto command for '" + e.typename + "' entries is invalid, please correct this in the preferences.')).run()
+
+		except util.SubstValueError:
+			self.entry_show(e)
+
+
+	def entry_menu(self, time = None):
+		"Displays the entry menu"
+
+		self.__close_popups()
+
+		if self.__require_file() == False:
+			return
+
+		if self.entrymenu == None:
+			return
+
+		x, y = self.__get_popup_offset(self.entrymenu)
+
+		self.entrymenu.show_all()
+		self.entrymenu.popup(None, None, lambda d: (x, y, False), 1, time)
+
+
+	def entry_search(self, term, focusafter = False):
+		"Searches for an entry"
+
+		self.__close_popups()
+
+		if term.strip() == "":
+			return
+
+		if self.__require_file() == False:
+			return
+
+
+		matches = [ self.entrystore.get_entry(iter) for iter in self.entrysearch.find_all(term) ]
+
+		if len(matches) == 0:
+			self.__focus_entry()
+			self.__flash_entry()
+			self.entry.select_region(0, -1)
+
+		elif len(matches) == 1:
+			self.entry_show(matches[0], True)
+
+		else:
+			self.popup_entrylist = EntryListPopup(matches)
+			self.popup_entrylist.connect("entry-chosen", lambda w,e: self.entry_show(e, focusafter))
+
+			if focusafter == True:
+				self.popup_entrylist.connect("closed", lambda w: self.__focus_entry())
+
+			self.popup_entrylist.realize()
+			x, y = self.__get_popup_offset(self.popup_entrylist)
+			self.popup_entrylist.show(x, y)
+
+
+	def entry_show(self, e, focusafter = False):
+		"Shows an entry"
+
+		self.__close_popups()
+
+		self.popup_entryview = EntryViewPopup(e, self.config, self.clipboard)
+
+		if focusafter == True:
+			self.popup_entryview.connect("closed", lambda w: self.__focus_entry())
+
+		def cb_goto(widget):
+			if self.__launcher_valid(e):
+				self.entry_goto(e)
+
+			self.popup_entryview.close()
+
+		self.popup_entryview.button_goto.connect("clicked", cb_goto)
+		self.popup_entryview.button_goto.set_sensitive(self.__launcher_valid(e))
+
+		self.popup_entryview.realize()
+		x, y = self.__get_popup_offset(self.popup_entryview)
+		self.popup_entryview.show(x, y)
+
+
+	def file_close(self):
+		"Closes the current data file"
+
+		self.__close_popups()
+		self.locktimer.stop()
+
+		self.datafile.close()
+		self.entrystore.clear()
+		self.entrymenu = None
+
+
+	def file_open(self, file, password = None):
+		"Opens a data file"
+
+		try:
+			return self.__file_load(file, password)
+
+		except dialog.CancelError:
+			pass
+
+		except datahandler.FormatError:
+			dialog.Error(None, _('Invalid file format'), _('The file \'%s\' contains invalid data.') % file).run()
+
+		except ( datahandler.DataError, entry.EntryTypeError, entry.EntryFieldError ):
+			dialog.Error(None, _('Unknown data'), _('The file \'%s\' contains unknown data. It may have been created by a more recent version of Revelation.') % file).run()
+
+		except datahandler.PasswordError:
+			dialog.Error(None, _('Incorrect password'), _('You entered an incorrect password for the file \'%s\', please try again.') % file).run()
+			self.file_open(file, None)
+
+		except datahandler.VersionError:
+			dialog.Error(None, _('Unknown data version'), _('The file \'%s\' has a future version number, please upgrade Revelation to open it.') % file).run()
+
+		except IOError:
+			dialog.Error(None, _('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()
+
+		return False
+
+
+	def file_reload(self):
+		"Reloads the current data file"
+
+		if self.datafile.get_file() == None:
+			return
+
+		self.file_open(self.datafile.get_file(), self.datafile.get_password())
+
+
+	def prefs(self):
+		"Displays the preference dialog"
+
+		dialog.run_unique(Preferences, None, self.config)
+
+
+
+class About(dialog.About):
+	"About dialog"
+
+	def __init__(self, parent):
+		dialog.About.__init__(self, parent)
+
+		self.set_name(_('Revelation Account Search'))
+		self.set_comments(_('"%s"\n\nAn applet for searching and browsing a Revelation account database') % config.RELNAME)
+
+
+
+class EntryListPopup(dialog.Popup):
+	"A popup for displaying a list of entries"
+
+	def __init__(self, entries):
+		dialog.Popup.__init__(self)
+		self.set_default_size(225, 200)
+
+		self.entrystore = data.EntryStore()
+		self.entrystore.set_sort_column_id(0, gtk.SORT_ASCENDING)
+
+		for e in entries:
+			self.entrystore.add_entry(e)
+
+		self.treeview = ui.EntryTree(self.entrystore)
+		self.treeview.set_cursor((0,))
+		self.treeview.connect("row-activated", self.__cb_row_activated)
+
+		self.scrolledwindow = ui.ScrolledWindow(self.treeview)
+		self.scrolledwindow.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+		self.add(self.scrolledwindow)
+
+
+	def __cb_row_activated(self, widget, path, data = None):
+		"Callback for tree row activation"
+
+		iter = self.entrystore.get_iter(path)
+		e = self.entrystore.get_entry(iter)
+
+		self.emit("entry-chosen", e)
+		self.close()
+
+
+gobject.signal_new("entry-chosen", EntryListPopup, gobject.SIGNAL_ACTION, gobject.TYPE_BOOLEAN, ( gobject.TYPE_PYOBJECT, ))
+
+
+
+class EntryViewPopup(dialog.Popup):
+	"A popup for displaying an entry"
+
+	def __init__(self, e, cfg = None, clipboard = None):
+		dialog.Popup.__init__(self)
+		self.set_title(e.name)
+
+		self.entryview = ui.EntryView(cfg, clipboard)
+		self.entryview.set_border_width(0)
+		self.entryview.display_entry(e)
+
+		self.button_close = ui.Button(gtk.STOCK_CLOSE, lambda w: self.close())
+		self.button_goto = ui.Button(ui.STOCK_GOTO)
+		self.buttonbox = ui.HButtonBox(self.button_goto, self.button_close)
+
+		self.vbox = ui.VBox(self.entryview, self.buttonbox)
+		self.vbox.set_border_width(12)
+		self.vbox.set_spacing(15)
+
+		self.add(self.vbox)
+
+		self.connect("show", lambda w: self.button_close.grab_focus())
+
+
+
+class Preferences(dialog.Utility):
+	"A preference dialog"
+
+	def __init__(self, parent, cfg):
+		dialog.Utility.__init__(self, parent, "Preferences")
+		self.config = cfg
+		self.set_modal(False)
+
+		self.notebook = ui.Notebook()
+		self.vbox.pack_start(self.notebook)
+
+		self.page_general = self.notebook.create_page(_('General'))
+		self.__init_section_file(self.page_general)
+		self.__init_section_menuaction(self.page_general)
+		self.__init_section_misc(self.page_general)
+
+		self.page_goto = self.notebook.create_page(_('Goto Commands'))
+		self.__init_section_gotocmd(self.page_goto)
+
+		self.connect("response", lambda w,d: self.destroy())
+
+
+	def __init_section_file(self, page):
+		"Sets up a file section in a page"
+
+		self.section_file = page.add_section(_('File Handling'))
+
+		# entry for file
+		self.button_file = ui.FileButton(_('Select File to Use'))
+		ui.config_bind(self.config, "file", self.button_file)
+
+		eventbox = ui.EventBox(self.button_file)
+		eventbox.set_tooltip_text(_('The data file to search for accounts in'))
+		self.section_file.append_widget(_('File to use'), eventbox)
+
+		# check-button for autolock
+		self.check_autolock = ui.CheckButton(_('Lock file when inactive for'))
+		ui.config_bind(self.config, "autolock", self.check_autolock)
+		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 file after a period of inactivity'))
+
+		# spin-entry for autolock-timeout
+		self.spin_autolock_timeout = ui.SpinEntry()
+		self.spin_autolock_timeout.set_range(1, 120)
+		self.spin_autolock_timeout.set_sensitive(self.check_autolock.get_active())
+		ui.config_bind(self.config, "autolock_timeout", self.spin_autolock_timeout)
+		self.spin_autolock_timeout.set_tooltip_text(_('The period of inactivity before locking the file, in minutes'))
+
+		# container for autolock-widgets
+		hbox = ui.HBox()
+		hbox.set_spacing(3)
+		hbox.pack_start(self.check_autolock)
+		hbox.pack_start(self.spin_autolock_timeout)
+		hbox.pack_start(ui.Label(_('minutes')))
+		self.section_file.append_widget(None, hbox)
+
+
+	def __init_section_gotocmd(self, page):
+		"Sets up the goto command section"
+
+		self.section_goto = page.add_section(_('Goto Commands'))
+
+		for entrytype in entry.ENTRYLIST:
+			if entrytype == entry.FolderEntry:
+				continue
+
+			e = entrytype()
+
+			widget = ui.Entry()
+			ui.config_bind(self.config, "/apps/revelation/launcher/%s" % e.id, widget)
+
+			tooltip = _('Goto command for %s accounts. The following expansion variables can be used:\n\n') % e.typename
+
+			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_goto.append_widget(e.typename, widget)
+
+
+	def __init_section_menuaction(self, page):
+		"Sets up a menuaction section in a page"
+
+		self.section_menuaction = page.add_section(_('Menu Action'))
+
+		# radio-button for show
+		self.radio_show = ui.RadioButton(None, _('Display account info'))
+		ui.config_bind(self.config, "menuaction", self.radio_show, "show")
+
+		self.radio_show.set_tooltip_text(_('Display the account information'))
+		self.section_menuaction.append_widget(None, self.radio_show)
+
+		# radio-button for goto
+		self.radio_goto = ui.RadioButton(self.radio_show, _('Go to account, if possible'))
+		ui.config_bind(self.config, "menuaction", self.radio_goto, "goto")
+
+		self.radio_goto.set_tooltip_text(_('Open the account in an external application if possible, otherwise display it'))
+		self.section_menuaction.append_widget(None, self.radio_goto)
+
+		# radio-button for copy username/password
+		self.radio_copy = ui.RadioButton(self.radio_show, _('Copy password to clipboard'))
+		ui.config_bind(self.config, "menuaction", self.radio_copy, "copy")
+
+		self.radio_copy.set_tooltip_text(_('Copy the account password to the clipboard'))
+		self.section_menuaction.append_widget(None, self.radio_copy)
+
+
+	def __init_section_misc(self, page):
+		"Sets up the misc section"
+
+		self.section_misc = page.add_section(_('Miscellaneous'))
+
+		# show searchentry checkbutton
+		self.check_show_searchentry = ui.CheckButton(_('Show search entry'))
+		ui.config_bind(self.config, "show_searchentry", self.check_show_searchentry)
+
+		self.check_show_searchentry.set_tooltip_text(_('Display an entry box in the applet for searching'))
+		self.section_misc.append_widget(None, self.check_show_searchentry)
+
+		# show passwords checkbutton
+		self.check_show_passwords = ui.CheckButton(_('Show passwords and other secrets'))
+		ui.config_bind(self.config, "show_passwords", self.check_show_passwords)
+
+		self.check_show_passwords.set_tooltip_text(_('Display passwords and other secrets, such as PIN codes (otherwise, hide with ******)'))
+		self.section_misc.append_widget(None, self.check_show_passwords)
+
+		# check-button for username
+		self.check_chain_username = ui.CheckButton(_('Also copy username when copying password'))
+		ui.config_bind(self.config, "chain_username", self.check_chain_username)
+
+		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_misc.append_widget(None, self.check_chain_username)
+
+
+	def run(self):
+		"Runs the dialog"
+
+		self.show_all()
+
+
+
+def factory(applet, iid):
+	"Applet factory function"
+
+	RevelationApplet(applet, iid)
+
+	return True
+
+
+
+if __name__ == "__main__":
+	gnome.init(config.APPNAME, config.VERSION)
+
+	gnomeapplet.bonobo_factory(
+		"OAFIID:GNOME_RevelationApplet_Factory",
+		gnomeapplet.Applet.__gtype__,
+		config.APPNAME, config.VERSION, factory
+	)
+

File src/revelation.py

+#!/usr/bin/env python
+
+#
+# Revelation - a password manager for GNOME 2
+# http://oss.codepoet.no/revelation/
+# $Id$
+#
+# Copyright (c) 2003-2006 Erik Grinaker
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+
+import gettext, gobject, gtk, gtk.gdk, os, pwd, sys, dbus
+from dbus.mainloop.glib import DBusGMainLoop
+
+if "@pyexecdir@" not in sys.path:
+	sys.path.insert(0, "@pyexecdir@")
+
+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(0077)
+
+		gettext.bindtextdomain(config.PACKAGE, config.DIR_LOCALE)
+		gettext.bind_textdomain_codeset(config.PACKAGE, "UTF-8")
+		gettext.textdomain(config.PACKAGE)
+
+		ui.App.__init__(self, config.APPNAME)
+
+		self.connect("delete-event", self.__cb_quit)
+
+		try:
+			self.__init_actions()
+			self.__init_facilities()
+			self.__init_ui()
+			self.__init_states()
+			self.__init_dbus()
+
+		except IOError:
+			dialog.Error(self, _('Missing data files'), _('Some of Revelations system files could not be found, please reinstall Revelation.')).run()
+			sys.exit(1)
+
+		except config.ConfigError:
+			dialog.Error(self, _('Missing configuration data'), _('Revelation could not find its configuration data, please reinstall Revelation.')).run()
+			sys.exit(1)
+
+		except ui.DataError:
+			dialog.Error(self, _('Invalid data files'), _('Some of Revelations system files contain invalid data, please reinstall Revelation.')).run()
+			sys.exit(1)
+
+
+	def __init_actions(self):
+		"Sets up actions"
+
+		# set up placeholders
+		group	= ui.ActionGroup("placeholder")
+		self.uimanager.append_action_group(group)
+
+		group.add_action(ui.Action("menu-edit",		_('_Edit')))
+		group.add_action(ui.Action("menu-entry",	_('E_ntry')))
+		group.add_action(ui.Action("menu-file",		_('_File')))
+		group.add_action(ui.Action("menu-help",		_('_Help')))
+		group.add_action(ui.Action("menu-view",		_('_View')))
+		group.add_action(ui.Action("popup-tree"))
+
+		# set up dynamic actions
+		group	= ui.ActionGroup("dynamic")
+		self.uimanager.append_action_group(group)
+
+		action	= ui.Action("clip-paste",	_('_Paste'),		_('Paste entry from clipboard'),		"gtk-paste")
+		action.connect("activate",		self.__cb_clip_paste)
+		group.add_action(action, "<Control>V")
+
+		action	= ui.Action("entry-goto",	_('_Go to'),		_('Go to the selected entries'),		"revelation-goto",	True)
+		action.connect("activate",		lambda w: self.entry_goto(self.tree.get_selected()))
+		group.add_action(action, "<Shift><Control>Return")
+
+		action	= ui.Action("redo",		_('_Redo'),		_('Redo the previously undone action'),		"gtk-redo")
+		action.connect("activate",		lambda w: self.redo())
+		group.add_action(action, "<Shift><Control>Z")
+
+		action	= ui.Action("undo",		_('_Undo'),		_('Undo the last action'),			"gtk-undo")
+		action.connect("activate",		lambda w: self.undo())
+		group.add_action(action, "<Control>Z")
+
+		# set up group for multiple entries
+		group	= ui.ActionGroup("entry-multiple")
+		self.uimanager.append_action_group(group)
+
+		action	= ui.Action("clip-copy",	_('_Copy'),		_('Copy selected entries to the clipboard'),	"gtk-copy")
+		action.connect("activate",		self.__cb_clip_copy)
+		group.add_action(action, "<Control>C")
+
+		action	= ui.Action("clip-chain",	_('_Copy Pass_word'),	_('Copy password to the clipboard'))
+		action.connect("activate",		lambda w: self.clip_chain(self.entrystore.get_entry(self.tree.get_active())))
+		group.add_action(action, "<Shift><Control>C")
+
+		action	= ui.Action("clip-cut",		_('Cu_t'),		_('Cut selected entries to the clipboard'),	"gtk-cut")
+		action.connect("activate",		self.__cb_clip_cut)
+		group.add_action(action, "<Control>X")
+
+		action	= ui.Action("entry-remove",	_('Re_move'),		_('Remove the selected entries'),		"revelation-remove")
+		action.connect("activate",		lambda w: self.entry_remove(self.tree.get_selected()))
+		group.add_action(action, "<Control>Delete")
+
+		# action group for "optional" entries
+		group	= ui.ActionGroup("entry-optional")
+		self.uimanager.append_action_group(group)
+
+		action	= ui.Action("entry-add",	_('_Add Entry...'),	_('Create a new entry'),			"revelation-new-entry",		True)
+		action.connect("activate",		lambda w: self.entry_add(None, self.tree.get_active()))
+		group.add_action(action, "<Control>Insert")
+
+		action	= ui.Action("entry-folder",	_('Add _Folder...'),	_('Create a new folder'),			"revelation-new-folder")
+		action.connect("activate",		lambda w: self.entry_folder(None, self.tree.get_active()))
+		group.add_action(action, "<Shift><Control>Insert")
+
+		# action group for single entries
+		group	= ui.ActionGroup("entry-single")
+		self.uimanager.append_action_group(group)
+
+		action	= ui.Action("entry-edit",	_('_Edit...'),		_('Edit the selected entry'),			"revelation-edit")
+		action.connect("activate",		lambda w: self.entry_edit(self.tree.get_active()))
+		group.add_action(action, "<Control>Return")
+
+		# action group for existing file
+		group	= ui.ActionGroup("file-exists")
+		self.uimanager.append_action_group(group)
+
+		action	= ui.Action("file-lock",	_('_Lock'),		_('Lock the current data file'),		"revelation-lock")
+		action.connect("activate",		lambda w: self.file_lock())
+		group.add_action(action, "<Control>L")
+
+		# action group for searching
+		group	= ui.ActionGroup("find")
+		self.uimanager.append_action_group(group)
+
+		action	= ui.Action("find-next",	_('Find Ne_xt'),	_('Find the next search match'),		"find-next")
+		action.connect("activate",		lambda w: self.__entry_find(self, self.searchbar.entry.get_text(), self.searchbar.dropdown.get_active_type(), data.SEARCH_NEXT))
+		group.add_action(action, "<Control>G")
+
+		action	= ui.Action("find-previous",	_('Find Pre_vious'),	_('Find the previous search match'),		"find-previous")
+		action.connect("activate",		lambda w: self.__entry_find(self, self.searchbar.entry.get_text(), self.searchbar.dropdown.get_active_type(), data.SEARCH_PREVIOUS))
+		group.add_action(action, "<Shift><Control>G")
+
+		# global action group
+		group	= ui.ActionGroup("file")
+		self.uimanager.append_action_group(group)
+
+		action	= ui.Action("file-change-password",	_('Change _Password...'),	_('Change password of current file'),		"revelation-password-change")
+		action.connect("activate",		lambda w: self.file_change_password())
+		group.add_action(action)
+
+		action	= ui.Action("file-close",	_('_Close'),		_('Close the application'),			"gtk-close")
+		action.connect("activate",		self.__cb_quit)
+		group.add_action(action, "<Control>W")
+
+		action	= ui.Action("file-export",	_('_Export...'),	_('Export data to a different file format'),	"revelation-export")
+		action.connect("activate",		lambda w: self.file_export())
+		group.add_action(action)
+
+		action	= ui.Action("file-import",	_('_Import...'),	_('Import data from a foreign file'),		"revelation-import")
+		action.connect("activate",		lambda w: self.file_import())
+		group.add_action(action)
+
+		action	= ui.Action("file-new",		_('_New'),		_('Create a new file'),				"gtk-new")
+		action.connect("activate",		lambda w: self.file_new())
+		group.add_action(action, "<Control>N")
+
+		action	= ui.Action("file-open",	_('_Open'),		_('Open a file'),				"gtk-open")
+		action.connect("activate",		lambda w: self.file_open())
+		group.add_action(action, "<Control>O")
+
+		action	= ui.Action("file-save",	_('_Save'),		_('Save data to a file'),			"gtk-save",		True)
+		action.connect("activate",		lambda w: self.file_save(self.datafile.get_file(), self.datafile.get_password()))
+		group.add_action(action, "<Control>S")
+
+		action	= ui.Action("file-save-as",	_('Save _as...'),	_('Save data to a different file'),		"gtk-save-as")
+		action.connect("activate",		lambda w: self.file_save(None, None))
+		group.add_action(action, "<Shift><Control>S")
+
+		action	= ui.Action("find",		_('_Find...'),		_('Search for an entry'),			"gtk-find")
+		action.connect("activate",		lambda w: self.entry_find())
+		group.add_action(action, "<Control>F")
+
+		action	= ui.Action("help-about",	_('_About'),		_('About this application'),			"gnome-stock-about")
+		action.connect("activate",		lambda w: self.about())
+		group.add_action(action)
+
+		action	= ui.Action("prefs",		_('Prefere_nces'),	_('Edit preferences'),				"gtk-preferences")
+		action.connect("activate",		lambda w: self.prefs())
+		group.add_action(action)
+
+		action	= ui.Action("pwchecker",	_('Password _Checker'),	_('Opens a password checker'),			"revelation-password-check")
+		action.connect("activate",		lambda w: self.pwcheck())
+		group.add_action(action)
+
+		action	= ui.Action("pwgenerator",	_('Password _Generator'),	_('Opens a password generator'),	"revelation-generate")
+		action.connect("activate",		lambda w: self.pwgen())
+		group.add_action(action)
+
+		action	= ui.Action("quit",		_('_Quit'),		_('Quit the application'),			"gtk-quit")
+		action.connect("activate",		self.__cb_quit)
+		group.add_action(action, "<Control>Q")
+
+		action	= ui.Action("select-all",	_('_Select All'),	_('Selects all entries'))
+		action.connect("activate",		lambda w: self.tree.select_all())
+		group.add_action(action, "<Control>A")
+
+		action	= ui.Action("select-none",	_('_Deselect All'),	_('Deselects all entries'))
+		action.connect("activate",		lambda w: self.tree.unselect_all())
+		group.add_action(action, "<Shift><Control>A")
+
+		action	= ui.ToggleAction("view-passwords",	_('Show _Passwords'),	_('Toggle display of passwords'))
+		group.add_action(action, "<Control>P")
+
+		action	= ui.ToggleAction("view-searchbar",	_('S_earch Toolbar'),	_('Toggle the search toolbar'))
+		group.add_action(action)
+
+		action	= ui.ToggleAction("view-statusbar",	_('_Statusbar'),	_('Toggle the statusbar'))
+		group.add_action(action)
+
+		action	= ui.ToggleAction("view-toolbar",	_('_Main Toolbar'),	_('Toggle the main toolbar'))
+		group.add_action(action)
+
+
+	def __init_facilities(self):
+		"Sets up various facilities"
+
+		self.clipboard		= data.Clipboard()
+		self.config		= config.Config()
+		self.datafile		= io.DataFile(datahandler.Revelation)
+		self.entryclipboard	= data.EntryClipboard()
+		self.entrystore		= data.EntryStore()
+		self.entrysearch	= data.EntrySearch(self.entrystore)
+		self.items		= ui.ItemFactory(self)
+		self.locktimer		= data.Timer()
+		self.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()))
+
+		# check if configuration is updated, install schema if not
+		if self.__check_config() == False:
+
+			if config.install_schema("%s/revelation.schemas" % config.DIR_GCONFSCHEMAS) == False:
+				raise config.ConfigError
+
+			self.config.client.clear_cache()
+
+			if self.__check_config() == False:
+				raise config.ConfigError
+
+		self.config.monitor("file/autolock_timeout",	lambda k,v,d: self.locktimer.start(v * 60))
+
+		dialog.EVENT_FILTER = self.__cb_event_filter
+
+
+	def __init_states(self):
+		"Sets the initial application state"
+
+		# set window states
+		self.set_default_size(
+			self.config.get("view/window-width"),
+			self.config.get("view/window-height")
+		)
+
+		self.move(
+			self.config.get("view/window-position-x"),
+			self.config.get("view/window-position-y")
+		)
+
+		self.hpaned.set_position(
+			self.config.get("view/pane-position")
+		)
+
+		# bind ui widgets to config keys
+		bind = {
+			"view/passwords"	: "/menubar/menu-view/view-passwords",
+			"view/searchbar"	: "/menubar/menu-view/view-searchbar",
+			"view/statusbar"	: "/menubar/menu-view/view-statusbar",
+			"view/toolbar"		: "/menubar/menu-view/view-toolbar"
+		}
+
+		for key, path in bind.items():
+			ui.config_bind(self.config, key, self.uimanager.get_widget(path))
+
+		self.show_all()
+
+		self.window.add_filter(self.__cb_event_filter)
+
+
+		# set some variables
+		self.entrysearch.string	= ''
+		self.entrysearch.type	= None
+
+		# set ui widget states
+		self.__state_clipboard(self.entryclipboard.has_contents())
+		self.__state_entry([])
+		self.__state_file(None)
+		self.__state_find(self.searchbar.entry.get_text())
+		self.__state_undo(None, None)
+
+		# set states from config
+		self.config.monitor("view/searchbar", self.__cb_config_toolbar, self.searchbar)
+		self.config.monitor("view/statusbar", self.__cb_config_toolbar, self.statusbar)
+		self.config.monitor("view/toolbar", self.__cb_config_toolbar, self.toolbar)
+		self.config.monitor("view/toolbar_style", self.__cb_config_toolbar_style)
+
+		# give focus to searchbar entry if shown
+		if self.searchbar.get_property("visible") == True:
+			self.searchbar.entry.grab_focus()
+
+
+	def __init_ui(self):
+		"Sets up the UI"
+
+		gtk.about_dialog_set_url_hook(lambda d,l: gtk.show_uri(None, l, gtk.get_current_event_time()))
+		gtk.about_dialog_set_email_hook(lambda d,l: gtk.show_uri(None, "mailto:" + l, gtk.get_current_event_time()))
+
+		# set window icons
+		pixbufs = [ self.items.get_pixbuf("revelation", size) for size in ( 48, 32, 24, 16) ]
+		pixbufs = [ pixbuf for pixbuf in pixbufs if pixbuf != None ]
+
+		if len(pixbufs) > 0:
+			gtk.window_set_default_icon_list(*pixbufs)
+
+		# load UI definitions
+		self.uimanager.add_ui_from_file(config.DIR_UI + "/menubar.xml")
+		self.uimanager.add_ui_from_file(config.DIR_UI + "/popup-tree.xml")
+		self.uimanager.add_ui_from_file(config.DIR_UI + "/toolbar.xml")
+
+		# set up toolbar and menus
+		self.set_menus(self.uimanager.get_widget("/menubar"))
+
+		self.toolbar = self.uimanager.get_widget("/toolbar")
+		self.toolbar.connect("popup-context-menu", lambda w,x,y,b: True)
+		self.set_toolbar(self.toolbar)
+
+		try:
+			detachable = self.config.get("/desktop/gnome/interface/toolbar_detachable")
+
+		except config.ConfigError:
+			detachable = False
+
+		self.searchbar = ui.Searchbar()
+		self.add_toolbar(self.searchbar, "searchbar", 2, detachable)
+
+		# set up main application widgets
+		self.tree = ui.EntryTree(self.entrystore)
+		self.scrolledwindow = ui.ScrolledWindow(self.tree)
+
+		self.entryview = ui.EntryView(self.config, self.clipboard)
+		alignment = ui.Alignment(self.entryview, 0.5, 0.5, 1, 0)
+
+		self.hpaned = ui.HPaned(self.scrolledwindow, alignment)
+		self.set_contents(self.hpaned)
+
+		# set up drag-and-drop
+		self.drag_dest_set(gtk.DEST_DEFAULT_ALL, ( ( "text/uri-list", 0, 0 ), ), gtk.gdk.ACTION_COPY | gtk.gdk.ACTION_MOVE | gtk.gdk.ACTION_LINK )
+		self.connect("drag_data_received", self.__cb_drag_dest)
+
+		self.tree.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, ( ( "revelation/treerow", gtk.TARGET_SAME_APP | gtk.TARGET_SAME_WIDGET, 0), ), gtk.gdk.ACTION_MOVE)
+		self.tree.enable_model_drag_dest(( ( "revelation/treerow", gtk.TARGET_SAME_APP | gtk.TARGET_SAME_WIDGET, 0), ), gtk.gdk.ACTION_MOVE)
+		self.tree.connect("drag_data_received", self.__cb_tree_drag_received)
+
+		# set up callbacks
+		self.searchbar.connect("key-press-event", self.__cb_searchbar_key_press)
+		self.searchbar.button_next.connect("clicked", self.__cb_searchbar_button_clicked, data.SEARCH_NEXT)
+		self.searchbar.button_prev.connect("clicked", self.__cb_searchbar_button_clicked, data.SEARCH_PREVIOUS)
+		self.searchbar.entry.connect("changed", lambda w: self.__state_find(self.searchbar.entry.get_text()))
+
+		self.tree.connect("popup", lambda w,d: self.popup(self.uimanager.get_widget("/popup-tree"), d.button, d.time))
+		self.tree.connect("doubleclick", self.__cb_tree_doubleclick)
+		self.tree.connect("key-press-event", self.__cb_tree_keypress)
+		self.tree.selection.connect("changed", lambda w: self.entryview.display_entry(self.entrystore.get_entry(self.tree.get_active())))
+		self.tree.selection.connect("changed", lambda w: self.__state_entry(self.tree.get_selected()))
+
+	def __init_dbus(self):
+		loop = DBusGMainLoop()
+		bus = dbus.SessionBus(mainloop=loop)
+		bus.add_signal_receiver(self.__cb_screensaver_lock, signal_name='ActiveChanged', dbus_interface='org.gnome.ScreenSaver')
+		bus.add_signal_receiver(self.__cb_screensaver_lock, signal_name='ActiveChanged', dbus_interface='org.freedesktop.ScreenSaver')
+
+	##### STATE HANDLERS #####
+
+	def __save_state(self):
+		"Saves the current application state"
+
+		width, height = self.get_size()
+		self.config.set("view/window-width", width)
+		self.config.set("view/window-height", height)
+
+		x, y = self.get_position()
+		self.config.set("view/window-position-x", x)
+		self.config.set("view/window-position-y", y)
+
+		self.config.set("view/pane-position", self.hpaned.get_position())
+
+
+	def __state_clipboard(self, has_contents):
+		"Sets states based on the clipboard contents"
+
+		self.uimanager.get_action("clip-paste").set_property("sensitive", has_contents)
+
+
+	def __state_entry(self, iters):
+		"Sets states for entry-dependant ui items"
+
+		# widget sensitivity based on number of entries
+		self.uimanager.get_action_group("entry-multiple").set_sensitive(len(iters) > 0)
+		self.uimanager.get_action_group("entry-single").set_sensitive(len(iters) == 1)
+		self.uimanager.get_action_group("entry-optional").set_sensitive(len(iters) < 2)
+
+
+		# copy password sensitivity
+		s = False
+
+		for iter in iters:
+			e = self.entrystore.get_entry(iter)
+
+			for f in e.fields:
+				if f.datatype == entry.DATATYPE_PASSWORD and f.value != "":
+					s = True
+
+		self.uimanager.get_action("clip-chain").set_property("sensitive", s)
+
+
+		# goto sensitivity
+		try:
+			for iter in iters:
+				e = self.entrystore.get_entry(iter)
+
+				if self.config.get("launcher/%s" % e.id) not in ( "", None ):
+					s = True
+					break
+
+			else:
+				s = False
+
+		except config.ConfigError:
+			s = False
+
+		self.uimanager.get_action("entry-goto").set_sensitive(s)
+
+
+	def __state_file(self, file):
+		"Sets states based on file"
+
+		self.uimanager.get_action_group("file-exists").set_sensitive(file is not None)
+
+		if file is not None:
+			self.set_title(os.path.basename(file))
+
+			if io.file_is_local(file):
+				os.chdir(os.path.dirname(file))
+
+		else:
+			self.set_title('[' + _('New file') + ']')
+
+
+	def __state_find(self, string):
+		"Sets states based on the current search string"
+
+		self.uimanager.get_action_group("find").set_sensitive(string != "")
+
+
+	def __state_undo(self, undoaction, redoaction):
+		"Sets states based on undoqueue actions"
+
+		if undoaction is None:
+			s, l = False, _('_Undo')
+
+		else:
+			s, l = True, _('_Undo %s') % undoaction[1].lower()
+
+		action = self.uimanager.get_action("undo")
+		action.set_property("sensitive", s)
+		action.set_property("label", l)
+
+
+		if redoaction is None:
+			s, l = False, _('_Redo')
+
+		else:
+			s, l = True, _('_Redo %s') % redoaction[1].lower()
+
+		action = self.uimanager.get_action("redo")
+		action.set_property("sensitive", s)
+		action.set_property("label", l)
+
+
+
+
+	##### MISC CALLBACKS #####
+
+	def __cb_clip_copy(self, widget, data = None):
+		"Handles copying to the clipboard"
+
+		focuswidget = self.get_focus()
+
+		if focuswidget is self.tree:
+			self.clip_copy(self.tree.get_selected())
+
+		elif isinstance(focuswidget, gtk.Label) or isinstance(focuswidget, gtk.Entry):
+			focuswidget.emit("copy-clipboard")
+
+
+	def __cb_clip_cut(self, widget, data = None):
+		"Handles cutting to clipboard"
+
+		focuswidget = self.get_focus()
+
+		if focuswidget is self.tree:
+			self.clip_cut(self.tree.get_selected())
+
+		elif isinstance(focuswidget, gtk.Entry):
+			focuswidget.emit("cut-clipboard")
+
+
+	def __cb_clip_paste(self, widget, data = None):
+		"Handles pasting from clipboard"
+
+		focuswidget = self.get_focus()
+
+		if focuswidget is self.tree:
+			self.clip_paste(self.entryclipboard.get(), self.tree.get_active())
+
+		elif isinstance(focuswidget, gtk.Entry):
+			focuswidget.emit("paste-clipboard")
+
+
+	def __cb_drag_dest(self, widget, context, x, y, seldata, info, time, userdata = None):
+		"Handles file drops"
+
+		if seldata.data is None:
+			return
+
+		files = [ file.strip() for file in seldata.data.split("\n") if file.strip() != "" ]
+
+		if len(files) > 0:
+			self.file_open(files[0])
+
+
+	def __cb_event_filter(self, event):
+		"Event filter for gdk window"
+
+		self.locktimer.reset()
+		return gtk.gdk.FILTER_CONTINUE
+
+
+	def __cb_exception(self, type, value, trace):
+		"Callback for unhandled exceptions"
+
+		if type == KeyboardInterrupt:
+			sys.exit(1)
+
+		traceback = util.trace_exception(type, value, trace)
+		sys.stderr.write(traceback)
+
+		if dialog.Exception(self, traceback).run() == True:
+			gtk.main()
+
+		else:
+			sys.exit(1)
+
+
+	def __cb_file_content_changed(self, widget, file):
+		"Callback for changed file"
+
+		try:
+			if dialog.FileChanged(self, file).run() == True:
+				self.file_open(self.datafile.get_file(), self.datafile.get_password())
+
+		except dialog.CancelError:
+			self.statusbar.set_status(_('Open cancelled'))
+
+
+	def __cb_file_autolock(self, widget, data = None):
+		"Callback for locking the file"
+
+		if self.config.get("file/autolock") == True:
+			self.file_lock()
+
+
+	def __cb_screensaver_lock(self, screensaver_active):
+		if screensaver_active and self.config.get("file/autolock") == True:
+			self.file_lock()
+
+	def __cb_quit(self, widget, data = None):
+		"Callback for quit"
+
+		if self.quit() == False:
+			return True
+
+		else:
+			return False
+
+
+	def __cb_searchbar_button_clicked(self, widget, direction = data.SEARCH_NEXT):
+		"Callback for searchbar button clicks"
+
+		self.__entry_find(self, self.searchbar.entry.get_text(), self.searchbar.dropdown.get_active_type(), direction)
+		self.searchbar.entry.select_region(0, -1)
+
+
+	def __cb_searchbar_key_press(self, widget, data):
+		"Callback for searchbar key presses"
+
+		# escape
+		if data.keyval == 65307:
+			self.config.set("view/searchbar", False)
+
+
+	def __cb_tree_doubleclick(self, widget, iter):
+		"Handles doubleclicks on the tree"
+
+		if self.config.get("behavior/doubleclick") == "edit":
+			self.entry_edit(iter)
+
+		elif self.config.get("behavior/doubleclick") == "copy":
+			self.clip_chain(self.entrystore.get_entry(iter))
+
+		else:
+			self.entry_goto((iter,))
+
+
+	def __cb_tree_drag_received(self, tree, context, x, y, seldata, info, time):
+		"Callback for drag drops on the treeview"
+
+		# get source and destination data
+		sourceiters = self.entrystore.filter_parents(self.tree.get_selected())
+		destrow = self.tree.get_dest_row_at_pos(x, y)
+
+		if destrow is None:
+			destpath = ( self.entrystore.iter_n_children(None) - 1, )
+			pos = gtk.TREE_VIEW_DROP_AFTER
+
+		else:
+			destpath, pos = destrow
+
+		destiter = self.entrystore.get_iter(destpath)
+		destpath = self.entrystore.get_path(destiter)
+
+		# avoid drops to current iter or descentants
+		for sourceiter in sourceiters:
+			sourcepath = self.entrystore.get_path(sourceiter)
+
+			if self.entrystore.is_ancestor(sourceiter, destiter) == True or sourcepath == destpath:
+				context.finish(False, False, long(time))
+				return
+
+			elif pos == gtk.TREE_VIEW_DROP_BEFORE and sourcepath[:-1] == destpath[:-1] and sourcepath[-1] == destpath[-1] - 1:
+				context.finish(False, False, long(time))
+				return
+
+			elif pos == gtk.TREE_VIEW_DROP_AFTER and sourcepath[:-1] == destpath[:-1] and sourcepath[-1] == destpath[-1] + 1:
+				context.finish(False, False, long(time))
+				return
+
+
+		# move the entries
+		if pos in ( gtk.TREE_VIEW_DROP_INTO_OR_BEFORE, gtk.TREE_VIEW_DROP_INTO_OR_AFTER):
+			parent = destiter
+			sibling = None
+
+		elif pos == gtk.TREE_VIEW_DROP_BEFORE:
+			parent = self.entrystore.iter_parent(destiter)
+			sibling = destiter
+
+		elif pos == gtk.TREE_VIEW_DROP_AFTER:
+			parent = self.entrystore.iter_parent(destiter)
+
+			sibpath = list(destpath)
+			sibpath[-1] += 1
+			sibling = self.entrystore.get_iter(sibpath)
+
+		self.entry_move(sourceiters, parent, sibling)
+
+		context.finish(False, False, long(time))
+
+
+	def __cb_tree_keypress(self, widget, data = None):
+		"Handles key presses for the tree"
+
+		# return
+		if data.keyval == 65293:
+			self.entry_edit(self.tree.get_active())
+
+		# insert
+		elif data.keyval == 65379:
+			self.entry_add(None, self.tree.get_active())
+
+		# delete
+		elif data.keyval == 65535:
+			self.entry_remove(self.tree.get_selected())
+
+
+
+	##### CONFIG CALLBACKS #####
+
+	def __cb_config_toolbar(self, config, value, toolbar):
+		"Config callback for showing toolbars"
+
+		if value == True:
+			toolbar.show()
+
+		else:
+			toolbar.hide()
+
+
+	def __cb_config_toolbar_style(self, config, value, data = None):
+		"Config callback for setting toolbar style"
+
+		if value == "both":
+			self.toolbar.set_style(gtk.TOOLBAR_BOTH)
+
+		elif value == "both-horiz":
+			self.toolbar.set_style(gtk.TOOLBAR_BOTH_HORIZ)
+
+		elif value == "icons":
+			self.toolbar.set_style(gtk.TOOLBAR_ICONS)
+
+		elif value == "text":
+			self.toolbar.set_style(gtk.TOOLBAR_TEXT)
+
+		else:
+			self.toolbar.unset_style()
+
+
+	#### UNDO / REDO CALLBACKS #####
+
+	def __cb_redo_add(self, name, actiondata):
+		"Redoes an add action"
+
+		path, e = actiondata
+		parent = self.entrystore.get_iter(path[:-1])
+		sibling = self.entrystore.get_iter(path)
+
+		iter = self.entrystore.add_entry(e, parent, sibling)
+		self.tree.select(iter)
+
+
+	def __cb_redo_edit(self, name, actiondata):
+		"Redoes an edit action"
+
+		path, preentry, postentry = actiondata
+		iter = self.entrystore.get_iter(path)
+
+		self.entrystore.update_entry(iter, postentry)
+		self.tree.select(iter)
+
+
+	def __cb_redo_import(self, name, actiondata):
+		"Redoes an import action"
+
+		paths, entrystore = actiondata
+		self.entrystore.import_entry(entrystore, None)
+
+
+	def __cb_redo_move(self, name, actiondata):
+		"Redoes a move action"
+
+		newiters = []
+
+		for prepath, postpath in actiondata:
+			prepath, postpath = list(prepath), list(postpath)
+
+			# adjust path if necessary
+			if len(prepath) <= len(postpath):
+				if prepath[:-1] == postpath[:len(prepath) - 1]:
+					if prepath[-1] <= postpath[len(prepath) - 1]:
+						postpath[len(prepath) - 1] += 1
+
+			newiter = self.entrystore.move_entry(
+				self.entrystore.get_iter(prepath),
+				self.entrystore.get_iter(postpath[:-1]),
+				self.entrystore.get_iter(postpath)
+			)
+
+			newiters.append(newiter)
+
+		if len(newiters) > 0:
+			self.tree.select(newiters[0])
+
+
+	def __cb_redo_paste(self, name, actiondata):
+		"Redoes a paste action"
+
+		entrystore, parentpath, paths = actiondata
+		iters = self.entrystore.import_entry(entrystore, None, self.entrystore.get_iter(parentpath))
+
+		if len(iters) > 0:
+			self.tree.select(iters[0])
+
+
+	def __cb_redo_remove(self, name, actiondata):
+		"Redoes a remove action"
+
+		iters = []
+		for path, entrystore in actiondata:
+			iters.append(self.entrystore.get_iter(path))
+
+		for iter in iters:
+			self.entrystore.remove_entry(iter)
+
+		self.tree.unselect_all()
+
+
+	def __cb_undo_add(self, name, actiondata):
+		"Undoes an add action"
+
+		path, e = actiondata
+
+		self.entrystore.remove_entry(self.entrystore.get_iter(path))
+		self.tree.unselect_all()
+
+
+	def __cb_undo_edit(self, name, actiondata):
+		"Undoes an edit action"
+
+		path, preentry, postentry = actiondata
+		iter = self.entrystore.get_iter(path)
+
+		self.entrystore.update_entry(iter, preentry)
+		self.tree.select(iter)
+
+
+	def __cb_undo_import(self, name, actiondata):
+		"Undoes an import action"
+
+		paths, entrystore = actiondata
+		iters = [ self.entrystore.get_iter(path) for path in paths ]
+
+		for iter in iters:
+			self.entrystore.remove_entry(iter)
+
+		self.tree.unselect_all()
+
+
+	def __cb_undo_move(self, name, actiondata):
+		"Undoes a move action"
+
+		actiondata = actiondata[:]
+		actiondata.reverse()
+
+		newiters = []
+
+		for prepath, postpath in actiondata:
+			prepath, postpath = list(prepath), list(postpath)
+
+			# adjust path if necessary
+			if len(postpath) <= len(prepath):
+				if postpath[:-1] == prepath[:len(postpath) - 1]:
+					if postpath[-1] <= prepath[len(postpath) - 1]:
+						prepath[len(postpath) - 1] += 1
+
+			newiter = self.entrystore.move_entry(
+				self.entrystore.get_iter(postpath),
+				self.entrystore.get_iter(prepath[:-1]),
+				self.entrystore.get_iter(prepath)
+			)
+
+			newiters.append(newiter)
+
+		if len(newiters) > 0:
+			self.tree.select(newiters[-1])
+
+
+	def __cb_undo_paste(self, name, actiondata):
+		"Undoes a paste action"
+
+		entrystore, parentpath, paths = actiondata
+		iters = [ self.entrystore.get_iter(path) for path in paths ]
+
+		for iter in iters:
+			self.entrystore.remove_entry(iter)
+
+		self.tree.unselect_all()
+
+
+	def __cb_undo_remove(self, name, actiondata):
+		"Undoes a remove action"
+
+		iters = []
+		for path, entrystore in actiondata:
+			parent = self.entrystore.get_iter(path[:-1])
+			sibling = self.entrystore.get_iter(path)
+
+			iter = self.entrystore.import_entry(entrystore, entrystore.iter_nth_child(None, 0), parent, sibling)
+			iters.append(iter)
+
+		self.tree.select(iters[0])
+
+
+
+	##### PRIVATE METHODS #####
+
+	def __check_config(self):
+		"Checks if the configuration is correct"
+
+		try:
+			self.config.get("launcher/website")
+			self.config.get("view/searchbar")
+			self.config.get("clipboard/chain_username")
+			self.config.get("behavior/doubleclick")
+			self.config.get("view/toolbar_style")
+
+			return True
+
+		except config.ConfigError:
+			return False
+
+
+	def __entry_find(self, parent, string, entrytype, direction = data.SEARCH_NEXT):
+		"Searches for an entry"
+
+		match = self.entrysearch.find(string, entrytype, self.tree.get_active(), direction)
+
+		if match != None:
+			self.tree.select(match)
+			self.statusbar.set_status(_('Match found for \'%s\'') % string)
+
+		else:
+			self.statusbar.set_status(_('No match found for \'%s\'') % string)
+			dialog.Error(parent, _('No match found'), _('The string \'%s\' does not match any entries. Try searching for a different phrase.') % string).run()
+
+
+	def __file_autosave(self):
+		"Autosaves the current file if needed"
+
+		try:
+			if self.datafile.get_file() is None or self.datafile.get_password() is None:
+				return
+
+			if self.config.get("file/autosave") == False:
+				return
+
+			self.datafile.save(self.entrystore, self.datafile.get_file(), self.datafile.get_password())
+			self.entrystore.changed = False
+
+		except IOError:
+			pass
+
+
+	def __file_load(self, file, password, datafile = None):
+		"Loads data from a data file into an entrystore"
+
+		try:
+			if datafile is None:
+				datafile = self.datafile
+
+			while 1:
+				try:
+					return datafile.load(file, password, lambda: dialog.PasswordOpen(self, os.path.basename(file)).run())
+
+				except datahandler.PasswordError:
+					dialog.Error(self, _('Incorrect password'), _('The password you entered for the file \'%s\' was not correct.') % file).run()
+
+		except datahandler.FormatError:
+			self.statusbar.set_status(_('Open failed'))
+			dialog.Error(self, _('Invalid file format'), _('The file \'%s\' contains invalid data.') % file).run()
+
+		except ( datahandler.DataError, entry.EntryTypeError, entry.EntryFieldError ):
+			self.statusbar.set_status(_('Open failed'))
+			dialog.Error(self, _('Unknown data'), _('The file \'%s\' contains unknown data. It may have been created by a newer version of Revelation.') % file).run()
+
+		except datahandler.VersionError:
+			self.statusbar.set_status(_('Open failed'))
+			dialog.Error(self, _('Unknown data version'), _('The file \'%s\' has a future version number, please upgrade Revelation to open it.') % file).run()
+
+		except datahandler.DetectError:
+			self.statusbar.set_status(_('Open failed'))
+			dialog.Error(self, _('Unable to detect filetype'), _('The file type of the file \'%s\' could not be automatically detected. Try specifying the file type manually.')% file).run()
+
+		except IOError:
+			self.statusbar.set_status(_('Open failed'))
+			dialog.Error(self, _('Unable to open file'), _('The file \'%s\' could not be opened. Make sure that the file exists, and that you have permissions to open it.') % file).run()
+
+
+	def __get_common_usernames(self, e = None):
+		"Returns a list of possibly relevant usernames"
+
+		list = []
+
+		if e is not None and e.has_field(entry.UsernameField):
+			list.append(e[entry.UsernameField])
+
+		list.append(pwd.getpwuid(os.getuid())[0])
+		list.extend(self.entrystore.get_popular_values(entry.UsernameField, 3))
+
+		list = {}.fromkeys(list).keys()
+		list.sort()
+
+		return list
+
+
+
+	##### PUBLIC METHODS #####
+
+	def about(self):
+		"Displays the about dialog"
+
+		dialog.run_unique(dialog.About, self)
+
+
+	def clip_chain(self, e):
+		"Copies all passwords from an entry as a chain"
+
+		if e == None:
+			return
+
+		secrets = [ field.value for field in e.fields if field.datatype == entry.DATATYPE_PASSWORD and field.value != "" ]
+
+		if self.config.get("clipboard/chain_username") == True and len(secrets) > 0 and e.has_field(entry.UsernameField) and e[entry.UsernameField] != "":
+			secrets.insert(0, e[entry.UsernameField])
+
+		if len(secrets) == 0:
+			self.statusbar.set_status(_('Entry has no password to copy'))
+
+		else:
+			self.clipboard.set(secrets, True)
+			self.statusbar.set_status(_('Password copied to clipboard'))
+
+
+	def clip_copy(self, iters):
+		"Copies entries to the clipboard"
+
+		self.entryclipboard.set(self.entrystore, iters)
+		self.statusbar.set_status(_('Entries copied'))
+
+
+	def clip_cut(self, iters):
+		"Cuts entries to the clipboard"
+
+		iters = self.entrystore.filter_parents(iters)
+		self.entryclipboard.set(self.entrystore, iters)
+
+		# store undo data (need paths)
+		undoactions = []
+		for iter in iters:
+			undostore = data.EntryStore()
+			undostore.import_entry(self.entrystore, iter)
+			path = self.entrystore.get_path(iter)
+			undoactions.append( ( path, undostore ) )
+
+		# remove data
+		for iter in iters:
+			self.entrystore.remove_entry(iter)
+
+		self.undoqueue.add_action(
+			_('Cut entries'), self.__cb_undo_remove, self.__cb_redo_remove,
+			undoactions
+		)
+
+		self.__file_autosave()
+
+		self.tree.unselect_all()
+		self.statusbar.set_status(_('Entries cut'))
+
+
+	def clip_paste(self, entrystore, parent):
+		"Pastes entries from the clipboard"
+
+		if entrystore == None:
+			return
+
+		parent = self.tree.get_active()
+		iters = self.entrystore.import_entry(entrystore, None, parent)
+
+		paths = [ self.entrystore.get_path(iter) for iter in iters ]
+
+		self.undoqueue.add_action(
+			_('Paste entries'), self.__cb_undo_paste, self.__cb_redo_paste,
+			( entrystore, self.entrystore.get_path(parent), paths )
+		)
+
+		if len(iters) > 0:
+			self.tree.select(iters[0])
+
+		self.statusbar.set_status(_('Entries pasted'))
+
+
+	def entry_add(self, e = None, parent = None, sibling = None):
+		"Adds an entry"
+
+		try:
+			if e == None:
+				d = dialog.EntryEdit(self, _('Add Entry'), None, self.config, self.clipboard)
+				d.set_fieldwidget_data(entry.UsernameField, self.__get_common_usernames())
+				e = d.run()
+
+			iter = self.entrystore.add_entry(e, parent, sibling)
+
+			self.undoqueue.add_action(
+				_('Add entry'), self.__cb_undo_add, self.__cb_redo_add,
+				( self.entrystore.get_path(iter), e.copy() )
+			)
+
+			self.__file_autosave()
+			self.tree.select(iter)
+			self.statusbar.set_status(_('Entry added'))
+
+		except dialog.CancelError:
+			self.statusbar.set_status(_('Add entry cancelled'))
+
+
+	def entry_edit(self, iter):
+		"Edits an entry"
+
+		try:
+			if iter == None:
+				return
+
+			e = self.entrystore.get_entry(iter)
+
+			if type(e) == entry.FolderEntry:
+				d = dialog.FolderEdit(self, _('Edit Folder'), e)
+
+			else:
+				d = dialog.EntryEdit(self, _('Edit Entry'), e, self.config, self.clipboard)
+				d.set_fieldwidget_data(entry.UsernameField, self.__get_common_usernames(e))
+
+
+			n = d.run()
+			self.entrystore.update_entry(iter, n)
+			self.tree.select(iter)
+
+			self.undoqueue.add_action(
+				_('Update entry'), self.__cb_undo_edit, self.__cb_redo_edit,
+				( self.entrystore.get_path(iter), e.copy(), n.copy() )
+			)
+
+			self.__file_autosave()
+			self.statusbar.set_status(_('Entry updated'))
+
+		except dialog.CancelError:
+			self.statusbar.set_status(_('Edit entry cancelled'))
+
+
+	def entry_find(self):
+		"Searches for an entry"
+
+		self.config.set("view/searchbar", True)
+		self.searchbar.entry.select_region(0, -1)
+		self.searchbar.entry.grab_focus()
+
+
+	def entry_folder(self, e = None, parent = None, sibling = None):
+		"Adds a folder"
+
+		try:
+			if e == None:
+				e = dialog.FolderEdit(self, _('Add Folder')).run()
+
+			iter = self.entrystore.add_entry(e, parent, sibling)
+
+			self.undoqueue.add_action(
+				_('Add folder'), self.__cb_undo_add, self.__cb_redo_add,
+				( self.entrystore.get_path(iter), e.copy() )
+			)
+
+			self.__file_autosave()
+			self.tree.select(iter)
+			self.statusbar.set_status(_('Folder added'))
+
+		except dialog.CancelError:
+			self.statusbar.set_status(_('Add folder cancelled'))
+
+
+	def entry_goto(self, iters):
+		"Goes to an entry"
+
+		for iter in iters:
+			try:
+
+				# get goto data for entry
+				e = self.entrystore.get_entry(iter)
+				command = self.config.get("launcher/%s" % e.id)
+
+				if command in ( "", None ):
+					self.statusbar.set_status(_('No goto command found for %s entries') % e.typename)
+					return
+
+				subst = {}
+				for field in e.fields:
+					subst[field.symbol] = field.value
+
+				# copy passwords to clipboard
+				chain = []
+
+				for field in e.fields:
+					if field.datatype == entry.DATATYPE_PASSWORD and field.value != "":
+						chain.append(field.value)
+
+				if self.config.get("clipboard/chain_username") == True and len(chain) > 0 and e.has_field(entry.UsernameField) == True and e[entry.UsernameField] != "" and "%" + entry.UsernameField.symbol not in command:
+					chain.insert(0, e[entry.UsernameField])
+
+				self.clipboard.set(chain, True)
+
+				# generate and run goto command
+				command = util.parse_subst(command, subst)
+				util.execute_child(command)
+
+				self.statusbar.set_status(_('Entry opened'))
+
+			except ( util.SubstFormatError, config.ConfigError ):
+				dialog.Error(self, _('Invalid goto command format'), _('The goto command for \'%s\' entries is invalid, please correct it in the preferences.') % e.typename).run()
+
+			except util.SubstValueError:
+				dialog.Error(self, _('Missing entry data'), _('The entry \'%s\' does not have all the data required to open it.') % e.name).run()
+
+