#!/usr/bin/python

#
# Revelation 0.2.1 - a password manager for GNOME 2
# http://oss.wired-networks.net/revelation/
#
# Copyright (c) 2003-2004 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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#


import pygtk
pygtk.require("2.0")
import gtk, gnome, gnome.ui, revelation, os, os.path, sys, gobject, gc

# main application
class Revelation(gnome.ui.App):

	def __init__(self):

		# set up application
		gnome.ui.App.__init__(self, revelation.APPNAME, revelation.APPNAME)
		self.connect("delete_event", self.__cb_item_quit)
		self.set_default_size(600, 400)
		self.set_icon_list(
			gtk.gdk.pixbuf_new_from_file(revelation.DATADIR + "/pixmaps/revelation.png"),
			gtk.gdk.pixbuf_new_from_file(revelation.DATADIR + "/pixmaps/revelation-16x16.png")
		)

		# set up various subsystems and ui components
		self.__init_facilities()
		self.__init_menu()
		self.__init_toolbar()
		self.__init_statusbar()
		self.__init_mainarea()

		# set some initial states
		self.password = None

		self.__file_change(None)
		self.dataview.display_info()
		self.tree.select(None)
		self.finder.set_string("")

		self.if_menu.get_widget("<main>/Edit/Undo").set_sensitive(self.undoqueue.can_undo())
		self.if_menu.get_widget("<main>/Edit/Redo").set_sensitive(self.undoqueue.can_undo(revelation.data.REDO))
		self.if_menu.get_widget("<main>/View/Toolbar").set_active(gtk.TRUE)
		self.if_menu.get_widget("<main>/View/Statusbar").set_active(gtk.TRUE)


	def __init_facilities(self):

		self.icons = revelation.ui.IconFactory(self)
		self.entrytypes = revelation.data.EntryTypes()
		self.data = revelation.data.DataStore(self.entrytypes)
		self.clipboard = revelation.data.EntryClipboard()
		self.undoqueue = revelation.data.UndoQueue()
		self.finder = revelation.data.EntrySearch(self.data)

		self.clipboard.connect("copy", self.__cb_state_clipboard)
		self.clipboard.connect("cut", self.__cb_state_clipboard)

		self.undoqueue.connect("undo", self.__cb_undo, revelation.data.UNDO)
		self.undoqueue.connect("redo", self.__cb_undo, revelation.data.REDO)
		self.undoqueue.connect("can-undo", self.__cb_state_undo, revelation.data.UNDO)
		self.undoqueue.connect("can-redo", self.__cb_state_undo, revelation.data.REDO)

		self.finder.connect("string_changed", self.__cb_state_find)


	def __init_mainarea(self):

		# main hpaned
		hpaned = gtk.HPaned()
		hpaned.set_border_width(5)
		hpaned.set_position(300)
		self.set_contents(hpaned)

		# tree
		self.tree = revelation.ui.Tree(self.data)
		self.tree.get_selection().connect("changed", self.__cb_entry_display)
		self.tree.get_selection().connect("changed", self.__cb_state_entryitems)
		self.tree.connect("button_press_event", self.__cb_popup)

		scrolledwindow = gtk.ScrolledWindow()
		scrolledwindow.add(self.tree)
		hpaned.pack1(scrolledwindow, gtk.TRUE)

		# dataview
		self.dataview = revelation.ui.DataView(self.entrytypes)
		alignment = gtk.Alignment(0.5, 0.4, 0, 0)
		alignment.add(self.dataview)
		hpaned.pack2(alignment)


	def __init_menu(self):

		menuitems = (
			("/_File",		None,			None,					None,				0,	"<Branch>"),
			("/File/_New",		"<Control>N",		"Create a new file",			self.__cb_item_file_new,	0,	"<StockItem>",	gtk.STOCK_NEW),
			("/File/_Open...",	"<Control>O",		"Open a file",				self.__cb_item_file_open,	0,	"<StockItem>",	gtk.STOCK_OPEN),
			("/File/sep1",		None,			None,					None,				0,	"<Separator>"),
			("/File/_Save",		"<Control>S",		"Save data to file",			self.__cb_item_file_save,	0,	"<StockItem>",	gtk.STOCK_SAVE),
			("/File/Save _As...",	"<Shift><Control>S",	"Save data to different file",		self.__cb_item_file_save_as,	0,	"<StockItem>",	gtk.STOCK_SAVE_AS),
			("/File/_Revert",	None,			"Revert to the saved copy of the file",	self.__cb_item_file_revert,	0,	"<StockItem>",	gtk.STOCK_REVERT_TO_SAVED),
			("/File/sep2",		None,			None,					None,				0,	"<Separator>"),
			("/File/Change _Password...",	None,		"Change password of current file",	self.__cb_item_change_password,	0,	"<StockItem>",	revelation.ui.STOCK_PASSWORD),
			("/File/sep3",		None,			None,					None,				0,	"<Separator>"),
			("/File/_Import...",	None,			"Import data from a foreign file",	self.__cb_item_file_import,	0,	"<StockItem>",	revelation.ui.STOCK_IMPORT),
			("/File/_Export...",	None,			"Export data to a different format",	self.__cb_item_file_export,	0,	"<StockItem>",	revelation.ui.STOCK_EXPORT),
			("/File/sep4",		None,			None,					None,				0,	"<Separator>"),
			("/File/_Quit",		"<Control>Q",		"Quit the application",			self.__cb_item_quit,		0, 	"<StockItem>",	gtk.STOCK_QUIT),

			("/_Edit",		None,			None,					None,				0,	"<Branch>"),
			("/Edit/_Add Entry...",	"Insert",		"Create a new entry",			self.__cb_item_entry_add,	0,	"<StockItem>",	revelation.ui.STOCK_ADD),
			("/Edit/_Edit",		"Return",		"Edit the selected entry",		self.__cb_item_entry_edit,	0,	"<StockItem>",	revelation.ui.STOCK_EDIT),
			("/Edit/Re_move",	"Delete",		"Remove the selected entry",		self.__cb_item_entry_remove,	0,	"<StockItem>",	revelation.ui.STOCK_REMOVE),
			("/Edit/sep1",		None,			None,					None,				0,	"<Separator>"),
			("/Edit/_Undo",		"<Control>Z",		"Undo the last action",			self.__cb_item_undo,		0,	"<StockItem>",	gtk.STOCK_UNDO),
			("/Edit/_Redo",		"<Shift><Control>Z",	"Redo the previously undone action",	self.__cb_item_redo,		0,	"<StockItem>",	gtk.STOCK_REDO),
			("/Edit/sep2",		None,			None,					None,				0,	"<Separator>"),
			("/Edit/Cu_t",		"<Control>X",		"Cut the entry to the clipboard",	self.__cb_item_cut,		0,	"<StockItem>",	gtk.STOCK_CUT),
			("/Edit/_Copy",		"<Control>C",		"Copy the entry to the clipboard",	self.__cb_item_copy,		0,	"<StockItem>",	gtk.STOCK_COPY),
			("/Edit/_Paste",	"<Control>V",		"Paste entry from clipboard",		self.__cb_item_paste,		0,	"<StockItem>",	gtk.STOCK_PASTE),
			("/Edit/sep3",		None,			None,					None,				0,	"<Separator>"),
			("/Edit/_Find...",	"<Control>F",		"Search for an entry",			self.__cb_item_find,		0,	"<StockItem>",	gtk.STOCK_FIND),
			("/Edit/Find Ne_xt",	"<Control>G",		"Find the next search match",			self.__cb_item_find_next,	0,	"<Item>"),
			("/Edit/Find Pre_vious",	"<Shift><Control>G",		"Find the previous search match",	self.__cb_item_find_previous,	0,	"<Item>"),
			("/Edit/sep4",		None,			None,					None,				0,	"<Separator>"),
			("/Edit/_Select All",	"<Control>A",		"Select all entries",			self.__cb_item_select_all,	0,	"<Item>"),
			("/Edit/_Deselect All",	"<Shift><Control>A",	"Deselect all entries",			self.__cb_item_deselect_all,	0,	"<Item>"),

			("/_View",		None,			None,					None,				0,	"<Branch>"),
			("/View/_Toolbar",	None,			"Toggle display of the toolbar",	self.__cb_item_toolbar,		0,	"<CheckItem>"),
			("/View/_Statusbar",	None,			"Toggle display of the statusbar",	self.__cb_item_statusbar,	0,	"<CheckItem>"),

			("/_Help",		None,			None,					None,				0,	"<Branch>"),
			("/Help/_Homepage",	None,			"Visit the Revelation homepage",	lambda object, data: gnome.url_show(revelation.URL),	0,	"<StockItem>", gtk.STOCK_HOME),
			("/Help/_About",	None,			"Show info about this application",	lambda object, data: revelation.dialog.About().run(),	0,	"<StockItem>", "gnome-stock-about")
		)

		self.accelgroup = gtk.AccelGroup()
		self.add_accel_group(self.accelgroup)

		self.if_menu = self.__create_itemfactory(gtk.MenuBar, self.accelgroup, menuitems)
		self.set_menus(self.if_menu.get_widget("<main>"))


	def __init_statusbar(self):

		self.statusbar = gnome.ui.AppBar(gtk.FALSE, gtk.TRUE, gnome.ui.PREFERENCES_USER)
		self.set_statusbar(self.statusbar)


	def __init_toolbar(self):

		self.toolbar = gtk.Toolbar()
		self.set_toolbar(self.toolbar)

		self.toolbar.button_new = self.toolbar.insert_stock(gtk.STOCK_NEW, "New file", None, self.__cb_item_file_new, "", -1)
		self.toolbar.button_open = self.toolbar.insert_stock(gtk.STOCK_OPEN, "Open file", None, self.__cb_item_file_open, "", -1)
		self.toolbar.button_save = self.toolbar.insert_stock(gtk.STOCK_SAVE, "Save file", None, self.__cb_item_file_save, "", -1)

		self.toolbar.append_space()

		self.toolbar.button_entry_add = self.toolbar.insert_stock(revelation.ui.STOCK_ADD, "Add a new entry", None, self.__cb_item_entry_add, "", -1)
		self.toolbar.button_entry_edit = self.toolbar.insert_stock(revelation.ui.STOCK_EDIT, "Edit the selected entry", None, self.__cb_item_entry_edit, "", -1)
		self.toolbar.button_entry_remove = self.toolbar.insert_stock(revelation.ui.STOCK_REMOVE, "Remove the selected entry", None, self.__cb_item_entry_remove, "", -1)



	# private utility functions
	def __create_itemfactory(self, widget, accelgroup, items):

		itemfactory = revelation.ui.MenuFactory(widget, accelgroup)
		itemfactory.create_items(items)
		itemfactory.connect("item-selected", self.__cb_state_menudesc, gtk.TRUE)
		itemfactory.connect("item-deselected", self.__cb_state_menudesc, gtk.FALSE)

		return itemfactory


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

		if parent == None:
			parent = self

		self.finder.offset = self.tree.get_active()

		try:
			if direction == revelation.data.SEARCH_PREVIOUS:
				match = self.finder.find_previous()
			else:
				match = self.finder.find_next()

		except revelation.data.SearchError:
			revelation.dialog.Error(parent, "No match found", "The string you searched for did not match any entries. Try using a different search-phrase.").run()

		else:
			self.tree.select(match)
			self.finder.offset = match


	def __entry_find_next(self, parent = None):
		"""Searches for the next match"""

		self.__entry_find(parent, revelation.data.SEARCH_NEXT)


	def __entry_find_previous(self, parent = None):
		"""Searches for the previous match"""

		self.__entry_find(parent, revelation.data.SEARCH_PREVIOUS)


	def __file_change(self, file, password = None):
		"""Keeps track of the current file and password"""

		self.file = file

		if file == None:
			self.set_title("[New file] - " + revelation.APPNAME)
			self.if_menu.get_widget("<main>/File/Revert").set_sensitive(gtk.FALSE)

			self.filepassword = None
			self.password = None
		else:
			self.set_title(os.path.basename(file) + " - " + revelation.APPNAME)
			self.if_menu.get_widget("<main>/File/Revert").set_sensitive(gtk.TRUE)
			os.chdir(os.path.dirname(file))

			self.filepassword = password
			self.password = password



	def __file_export(self, datafile):
		"""Exports a file"""

		try:
			datafile.save(self.data)

		except IOError:
			revelation.dialog.Error(self, "Unable to write to file", "The file '" + datafile.get_file() + "' could not be opened for writing. Make sure that you have the proper permissions to write to it.").run()
			raise revelation.FileError


	def __file_import(self, datafile):
		"""Loads a file, returns an entrystore"""

		try:
			data = datafile.load()

		except IOError:
			revelation.dialog.Error(self, "Unable to open file", "The file '" + datafile.get_file() + "' could not be opened. Make sure that the file exists, and that you have the proper permissions to open it.").run()
			raise revelation.FileError

		except revelation.datafile.FormatError:
			revelation.dialog.Error(self, "Invalid file format", "The file '" + datafile.get_file() + "' is not a valid data file.").run()
			raise revelation.FileError

		except (revelation.datafile.EntryTypeError, revelation.datafile.EntryFieldError):
			revelation.dialog.Error(self, "Unknown data", "The file '" + datafile.get_file() + "' contained unknown data items. It may have been created by a future version of Revelation, try upgrading.").run()
			raise revelation.FileError

		except revelation.datafile.PasswordError:
			revelation.dialog.Error(self, "Wrong password", "You entered a wrong password for the file '" + datafile.get_file() + "'").run()
			raise revelation.FileError

		except revelation.datafile.VersionError:
			revelation.dialog.Error(self, "Unknown data version", "The file '" + datafile.get_file() + "' has a future version number - upgrade Revelation to a more recent version to open it.").run()
			raise revelation.FileError

		return data


	def __ui_menu_popup(self, data):

		# get the selected entries
		path = self.tree.get_path_at_pos(int(data.x), int(data.y))

		if path == None:
			self.tree.unselect_all()
		elif self.tree.get_selection().iter_is_selected(self.data.get_iter(path[0])) == gtk.FALSE:
			self.tree.set_cursor(path[0], path[1], gtk.FALSE)

		iters = self.tree.get_selected()


		# set up entry menu
		entrymenu = []

		if len(iters) < 2:
			entrymenu.append(("/Add Entry...", None, "Create a new entry", self.__cb_item_entry_add, 0, "<StockItem>", revelation.ui.STOCK_ADD))

		if len(iters) == 1:
			entrymenu.append(("/Edit", None, "Edit the selected entry", self.__cb_item_entry_edit, 0, "<StockItem>", revelation.ui.STOCK_EDIT))

		if len(iters) > 0:
			entrymenu.append(("/Remove", None, "Remove the selected entry", self.__cb_item_entry_remove, 0, "<StockItem>", revelation.ui.STOCK_REMOVE))

		# set up clipboard menu
		clipboardmenu = []

		if len(iters) > 0:
			clipboardmenu.append(("/Cut", "", "Cut the selected entry to the clipboard", self.__cb_item_cut, 0, "<StockItem>", gtk.STOCK_CUT))
			clipboardmenu.append(("/Copy", "", "Copy the selected entry to the keyboard", self.__cb_item_copy, 0, "<StockItem>", gtk.STOCK_COPY))

		if len(iters) < 2 and self.clipboard.has_contents():
			clipboardmenu.append(("/Paste", "", "Paste entry from clipboard", self.__cb_item_paste, 0, "<StockItem>", gtk.STOCK_PASTE))

		# create the menu
		menuitems = entrymenu

		if len(clipboardmenu) > 0:
			menuitems.append(("/sep1", None, None, None, 0, "<Separator>"))
			menuitems.extend(clipboardmenu)

		itemfactory = self.__create_itemfactory(gtk.Menu, gtk.AccelGroup(), menuitems)
		itemfactory.popup(int(data.x_root), int(data.y_root), data.button, data.get_time())


	def __ui_save_changes(self, pritext, sectext):
		"""Checks for unsaved changes, and asks to save them"""

		if self.data.changed == gtk.TRUE:

			response = revelation.dialog.Hig(
				self, pritext, sectext, gtk.STOCK_DIALOG_WARNING,
				[ [ revelation.ui.STOCK_DISCARD, gtk.RESPONSE_CLOSE ], [ gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL ], [ gtk.STOCK_SAVE, gtk.RESPONSE_OK ] ], 2
			).run()

			if response == gtk.RESPONSE_OK and self.file_save() == gtk.FALSE:
				return gtk.FALSE
			elif response == gtk.RESPONSE_CANCEL:
				return gtk.FALSE
			elif response == gtk.RESPONSE_CLOSE:
				pass

		return gtk.TRUE



	# callbacks for items (buttons, menu items, etc)
	def __cb_item_change_password(self, object, data):
		self.change_password()

	def __cb_item_copy(self, object, data):
		self.clip_copy(self.tree.get_selected())

	def __cb_item_cut(self, object, data):
		self.clip_cut(self.tree.get_selected())

	def __cb_item_deselect_all(self, object, data):
		self.tree.unselect_all()

	def __cb_item_entry_add(self, object, data):
		self.entry_add(self.tree.get_active())

	def __cb_item_entry_edit(self, object, data):
		self.entry_edit(self.tree.get_active())

	def __cb_item_entry_remove(self, object, data):
		self.entry_remove(self.tree.get_selected())

	def __cb_item_file_export(self, object, data):
		self.file_export()

	def __cb_item_file_import(self, object, data):
		self.file_import()

	def __cb_item_file_new(self, object, data):
		self.file_new()

	def __cb_item_file_open(self, object, data):
		self.file_open()

	def __cb_item_file_revert(self, object, data):
		self.file_revert()

	def __cb_item_file_save(self, object, data):
		self.file_save(self.file, self.password)

	def __cb_item_file_save_as(self, object, data):
		self.file_save(None, None)

	def __cb_item_find(self, object, data):
		self.entry_find()

	def __cb_item_find_next(self, object, data):
		self.__entry_find_next()

	def __cb_item_find_previous(self, object, data):
		self.__entry_find_previous()

	def __cb_item_paste(self, object, data):
		self.clip_paste(self.tree.get_active())

	def __cb_item_quit(self, object, data):
		return gtk.TRUE ^ self.quit()

	def __cb_item_redo(self, object, data):
		self.undoqueue.redo()

	def __cb_item_select_all(self, object, data):
		self.tree.select_all()

	def __cb_item_statusbar(self, object, data):
		if self.if_menu.get_widget("<main>/View/Statusbar").get_active():
			self.statusbar.show()
		else:
			self.statusbar.hide()

	def __cb_item_toolbar(self, object, data):
		dock = self.get_dock_item_by_name("Toolbar")
		if self.if_menu.get_widget("<main>/View/Toolbar").get_active():
			dock.show()
		else:
			dock.hide()

	def __cb_item_undo(self, object, data):
		self.undoqueue.undo()



	# callbacks for updating states
	def __cb_state_clipboard(self, object):
		self.if_menu.get_widget("<main>/Edit/Paste").set_sensitive(self.clipboard.has_contents())


	def __cb_state_menudesc(self, object, item, show):
		if show == gtk.TRUE:
			self.statusbar.push(item.get_data("description"))
		else:
			self.statusbar.pop()


	def __cb_state_undo(self, object, state, method):
		if method == revelation.data.UNDO:
			widget = self.if_menu.get_widget("<main>/Edit/Undo")
			action = "_Undo"
		elif method == revelation.data.REDO:
			widget = self.if_menu.get_widget("<main>/Edit/Redo")
			action = "_Redo"

		widget.set_sensitive(state)
		item = widget.get_children()[0]

		if state == gtk.TRUE:
			item.set_label(action + " " + self.undoqueue.get_data(method)["name"])
		else:
			item.set_label(action)


	def __cb_state_entryitems(self, object):
		selcount = len(self.tree.get_selected())

		self.toolbar.button_entry_add.set_sensitive(selcount < 2)
		self.if_menu.get_widget("<main>/Edit/Add Entry...").set_sensitive(selcount < 2)
		self.if_menu.get_widget("<main>/Edit/Paste").set_sensitive(selcount < 2 and self.clipboard.has_contents())

		self.toolbar.button_entry_edit.set_sensitive(selcount == 1)
		self.if_menu.get_widget("<main>/Edit/Edit").set_sensitive(selcount == 1)

		self.toolbar.button_entry_remove.set_sensitive(selcount > 0)
		self.if_menu.get_widget("<main>/Edit/Remove").set_sensitive(selcount > 0)
		self.if_menu.get_widget("<main>/Edit/Cut").set_sensitive(selcount > 0)
		self.if_menu.get_widget("<main>/Edit/Copy").set_sensitive(selcount > 0)


	def __cb_state_find(self, widget, string):
		if string == "":
			self.if_menu.get_widget("<main>/Edit/Find Next").set_sensitive(gtk.FALSE)
			self.if_menu.get_widget("<main>/Edit/Find Previous").set_sensitive(gtk.FALSE)
		else:
			self.if_menu.get_widget("<main>/Edit/Find Next").set_sensitive(gtk.TRUE)
			self.if_menu.get_widget("<main>/Edit/Find Previous").set_sensitive(gtk.TRUE)


	# misc callbacks
	def __cb_entry_display(self, object):
		self.entry_display(self.tree.get_active())

	def __cb_popup(self, object, data):
		if data.button != 3:
			return

		self.__ui_menu_popup(data)
		return gtk.TRUE

	def __cb_undo(self, object, data, method):
		self.undo(data["name"], data["action"], data["data"], method)



	# main application methods
	def change_password(self):
		"""Changes the password of the current file"""

		try:
			password = revelation.dialog.Password(
				self, "Enter new password",
				"Enter a new password for the current data file. The file must be saved before the new password is applied.",
				gtk.TRUE, self.password
			).run()

		except revelation.CancelError:
			self.statusbar.set_status("Password change cancelled")

		else:
			self.password = password
			self.data.changed = gtk.TRUE
			self.statusbar.set_status("Password changed")


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

		iters = self.data.filter_parents(iters)
		self.clipboard.copy(self.data, iters)


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

		iters = self.data.filter_parents(iters)
		self.undo_add_action(self.clip_cut, iters)
		self.clipboard.cut(self.data, iters)
		self.tree.unselect_all()


	def clip_paste(self, parent, sibling = None):
		"""Pastes data from the clipboard"""

		if self.clipboard.is_empty():
			return

		if self.data.get_entry_type(parent) not in [ revelation.data.ENTRY_FOLDER, None]:
			sibling = parent
			parent = self.data.iter_parent(parent)

		iters = self.clipboard.paste(self.data, parent, sibling)
		self.undo_add_action(self.clip_paste, iters)

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


	def entry_add(self, parent):
		"""Adds an entry"""

		try:
			data = revelation.dialog.EditEntry(self, "Add entry", self.data.entrytypes).run()

			iter = self.data.add_entry(parent, data)
			self.undo_add_action(self.entry_add, iter)
			self.tree.select(iter)
			self.statusbar.set_status("Added entry '" + data["name"] + "'")

		except revelation.CancelError:
			pass


	def entry_display(self, iter):
		"""Displays an entry"""

		self.dataview.display_entry(self.data.get_entry(iter))


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

		if iter == None:
			return

		try:
			data = self.data.get_entry(iter)
			dialog = revelation.dialog.EditEntry(self, "Edit entry", self.data.entrytypes, data)

			if data["type"] == revelation.data.ENTRY_FOLDER and self.data.iter_n_children(iter) > 0:
				dialog.set_typechange_allowed(gtk.FALSE)

			newdata = dialog.run()
			self.data.update_entry(iter, newdata)
			self.undo_add_action(self.entry_edit, iter, data)
			self.tree.select(iter)
			self.statusbar.set_status("Updated entry '" + newdata["name"] + "'")

		except revelation.CancelError:
			pass


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

		dialog = revelation.dialog.Find(self)

		dialog.entry_phrase.set_text(self.finder.get_string())
		dialog.check_casesens.set_active(self.finder.get_option_case_sensitive())
		dialog.check_folders.set_active(self.finder.get_option_folders())
		dialog.check_namedesc.set_active(self.finder.get_option_name_description())
		dialog.set_type(self.finder.get_type())

		while 1:
			response = dialog.run()

			self.finder.set_string(dialog.entry_phrase.get_text())
			self.finder.set_type(dialog.get_type())

			self.finder.set_option_case_sensitive(dialog.check_casesens.get_active())
			self.finder.set_option_folders(dialog.check_folders.get_active())
			self.finder.set_option_name_description(dialog.check_namedesc.get_active())

			# close dialog when close button is pressed
			if response == gtk.RESPONSE_CLOSE:
				dialog.destroy()
				break


			if response == revelation.ui.RESPONSE_NEXT:
				match = self.__entry_find_next(dialog)

			elif response == revelation.ui.RESPONSE_PREVIOUS:
				match = self.__entry_find_previous(dialog)


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

		if not isinstance(iters, list) and iters != None:
			iters = [iters]

		if len(iters) == 0:
			return

		elif len(iters) == 1:
			data = self.data.get_entry(iters[0])
			statustext = "Removed entry '" + data["name"] + "'"

		else:
			data = len(iters)
			statustext = "Removed " + str(data) + " entries"

		# run the dialog and perform the removal
		if revelation.dialog.RemoveEntry(self, data).run() == gtk.TRUE:
			iters = self.data.filter_parents(iters)
			self.undo_add_action(self.entry_remove, iters)

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

			self.tree.unselect_all()
			self.statusbar.set_status(statustext)


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

		try:

			# disable garbage collection, to work around pygtk bug 122569
			gc.disable()

			druid = revelation.druid.ExportFile(self)
			datafile = druid.run()

			gc.enable()

			self.__file_export(datafile)

		except revelation.CancelError:
			self.statusbar.set_status("Export cancelled")

		else:
			self.statusbar.set_status("Data exported to " + datafile.get_file())



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

		try:

			# disable garbage collection, to work around pygtk bug 122569
			gc.disable()

			druid = revelation.druid.ImportFile(self)
			datafile = druid.run()

			gc.enable()

			entrystore = self.__file_import(datafile)

		except revelation.CancelError:
			self.statusbar.set_status("Import cancelled")

		except revelation.FileError:
			self.statusbar.set_status("Import failed")

		else:
			iters = self.data.import_entrystore(entrystore)
			self.undo_add_action(self.file_import, iters)
			self.statusbar.set_status("Data imported from " + datafile.get_file())


	def file_new(self):
		"""Clears all data"""

		if self.__ui_save_changes("Save changes to current file?", "You have made changes which have not been saved. If you create a new file without saving, then these changes will be discarded.") == gtk.FALSE:
			self.statusbar.set_status("New file cancelled")
			return gtk.FALSE

		self.data.clear()
		self.dataview.clear()
		self.undoqueue.clear()

		self.__file_change(None)
		self.statusbar.set_status("New file created")


	def file_open(self, file = None, password = None, ignorechanges = gtk.FALSE):
		"""Loads data from a file"""

		try:
			
			# check if data has changed
			if ignorechanges == gtk.FALSE and self.__ui_save_changes("Save changes before opening?", "You have made changes which have not been saved. If you open another file without saving, then these changes will be discarded.") == gtk.FALSE:
				raise revelation.CancelError

			if file == None:
				file = revelation.dialog.FileSelector("Select file to open").run()

			datafile = revelation.datafile.DataFile(file, revelation.datafile.TYPE_REVELATION)
			datafile.check_file()
			datafile.check_format()

			if password == None:
				password = revelation.dialog.Password(self, "Enter file password", "This file is encrypted. Please enter the file password to open it.").run()

			datafile.set_option(revelation.datafile.OPTION_PASSWORD, password)
			entrystore = self.__file_import(datafile)

		except revelation.CancelError:
			self.statusbar.set_status("Open cancelled")
			return gtk.FALSE

		except revelation.FileError:
			self.statusbar.set_status("Open failed")
			return gtk.FALSE

		except revelation.datafile.FormatError:
			self.statusbar.set_status("Open failed")
			revelation.dialog.Error(self, "Invalid file format", "The file '" + file + "' is not a valid data file.").run()
			return gtk.FALSE

		except revelation.datafile.VersionError:
			self.statusbar.set_status("Open failed")
			revelation.dialog.Error(self, "Unknown data version", "The file '" + file + "' has a future version number - upgrade Revelation to a more recent version to open it.").run()
			return gtk.FALSE

		except IOError:
			self.statusbar.set_status("Open failed")
			revelation.dialog.Error(self, "Unable to open file", "The file '" + file + "' could not be opened. Make sure that the file exists, and that you have the proper permissions to open it.").run()
			return gtk.FALSE

		else:
			self.data.clear()
			self.undoqueue.clear()
			self.data.import_entrystore(entrystore)

			self.__file_change(file, password)
			self.password = password
			self.data.changed = gtk.FALSE
			self.statusbar.set_status("Opened file " + file)

			return gtk.TRUE


	def file_revert(self):
		"""Reverts to the saved copy of the file"""

		if self.data.changed == gtk.TRUE and revelation.dialog.Hig(
			self, "Ignore unsaved changes?", "You have made changes which have not yet been saved. If you revert to the saved file then these changes will be lost.",
			gtk.STOCK_DIALOG_WARNING, [ [ gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL ], [ gtk.STOCK_REVERT_TO_SAVED, gtk.RESPONSE_OK ] ]
		).run() != gtk.RESPONSE_OK:
			return gtk.FALSE

		return self.file_open(self.file, self.filepassword, gtk.TRUE)


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

		# write data to file
		try:
			if file == None:
				file = revelation.dialog.FileSelector("Select file to save data to").run()

			# ask for overwrite confirmation
			if file != self.file and os.access(file, os.F_OK) == 1 and revelation.dialog.FileOverwrite(self, file).run() == gtk.FALSE:
				raise revelation.CancelError

			if password == None:
				password = revelation.dialog.Password(
					self, "Enter file password",
					"Please enter a password which will be used to encrypt the file. You will need this password to open the file at a later time.",
					gtk.TRUE
				).run()

			datafile = revelation.datafile.DataFile(file, revelation.datafile.TYPE_REVELATION)
			datafile.set_option(revelation.datafile.OPTION_PASSWORD, password)
			self.__file_export(datafile)

		except revelation.CancelError:
			self.statusbar.set_status("Save cancelled")
			return gtk.FALSE

		except revelation.FileError:
			self.statusbar.set_status("Save failed")
			return gtk.FALSE

		else:
			self.__file_change(file, password)
			self.password = password
			self.data.changed = gtk.FALSE
			self.statusbar.set_status("Data saved to file " + file)

			return gtk.TRUE


	def quit(self):
		"""Quits the application"""

		if self.__ui_save_changes("Save changes before quitting?", "You have made changes which have not been saved. If you quit without saving, then these changes will be discarded.") == gtk.FALSE:
			return gtk.FALSE

		gtk.mainquit()
		return gtk.TRUE


	def undo(self, name, action, data, method = revelation.data.UNDO):
		"""Undoes the previous action"""

		iters = []

		# handle add, paste and import actions
		if action == self.entry_add or action == self.clip_paste or action == self.file_import:

			if method == revelation.data.UNDO:
				for item in data:
					item["iter"] = self.data.get_iter(item["path"])

				for item in data:
					self.data.remove_entry(item["iter"])

			elif method == revelation.data.REDO:
				for item in data:
					newiters = self.data.import_entrystore_before(item["data"], self.data.get_iter(item["parent"]), self.data.get_iter(item["path"]))
					iters.extend(newiters)


		# handle remove and cut actions
		elif action == self.entry_remove or action == self.clip_cut:

			if method == revelation.data.UNDO:
				for item in data:
					newiters = self.data.import_entrystore_before(item["data"], self.data.get_iter(item["parent"]), self.data.get_iter(item["path"]))
					iters.extend(newiters)


			elif method == revelation.data.REDO:
				for item in data:
					item["iter"] = self.data.get_iter(item["path"])

				for item in data:
					self.data.remove_entry(item["iter"])


		# handle edit action
		elif action == self.entry_edit:

			iter = self.data.get_iter(data[0]["path"])
			iters.append(iter)

			if method == revelation.data.UNDO:
				self.data.update_entry(iter, data[0]["predata"])

			elif method == revelation.data.REDO:
				self.data.update_entry(iter, data[0]["data"])


		# update status
		if method == revelation.data.UNDO:
			self.statusbar.set_status(name.capitalize() + " undone")
		elif method == revelation.data.REDO:
			self.statusbar.set_status(name.capitalize() + " redone")

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


	def undo_add_action(self, action, iters, extradata = None):
		"""Adds action data to the undo queue"""

		if not isinstance(iters, list) and iters != None:
			iters = [iters]

		data = []
		name = {
			self.entry_add		: "Add Entry",
			self.entry_remove	: "Remove Entry",
			self.entry_edit		: "Edit Entry",
			self.file_import	: "File Import",
			self.clip_cut		: "Cut",
			self.clip_paste		: "Paste"
		}[action]


		for iter in iters:

			item = ({
				"path"		: self.data.get_path(iter),
				"parent"	: self.data.get_path(self.data.iter_parent(iter))
			})

			if action in [ self.entry_add, self.entry_remove, self.clip_cut, self.clip_paste, self.file_import ]:
				item["data"] = self.data.export_entrystore([iter])

			elif action == self.entry_edit:
				item["data"] = self.data.get_entry(iter)
				item["predata"] = extradata

			data.append(item)

		self.undoqueue.add_action(name, action, data)



if __name__ == "__main__":

	os.umask(0077)

	program = gnome.init(revelation.APPNAME, revelation.VERSION)
	app = Revelation()
	app.show_all()

	if len(sys.argv) > 1:
		app.file_open(os.path.abspath(sys.argv[1]))

	gtk.main()

