# HG changeset patch # User Erik Grinaker # Date 1089392767 0 # Node ID 268984dc47cf3aff7fe21e291e645cfca3afcc3d # Parent 4787d15f40a47ecd4912e428c6d04ddd0dcb28e6 cleaned up the undo/redo code diff -r 4787d15f40a47ecd4912e428c6d04ddd0dcb28e6 -r 268984dc47cf3aff7fe21e291e645cfca3afcc3d ChangeLog --- a/ChangeLog Thu Jul 08 21:11:32 2004 +0000 +++ b/ChangeLog Fri Jul 09 17:06:07 2004 +0000 @@ -2,6 +2,11 @@ ---------------[ xxxx-xx-xx : 0.3.1 ]--------------- +2004-07-09 Erik Grinaker + + * rewrote the Undo/Redo code, and moved most of it into the + UndoQueue class + 2004-07-08 Erik Grinaker * the OK button in the password dialogs is inactive when it's diff -r 4787d15f40a47ecd4912e428c6d04ddd0dcb28e6 -r 268984dc47cf3aff7fe21e291e645cfca3afcc3d TODO --- a/TODO Thu Jul 08 21:11:32 2004 +0000 +++ b/TODO Fri Jul 09 17:06:07 2004 +0000 @@ -5,7 +5,8 @@ - add a password generator dialog (under Tools/Password Generator menu) - add info about file as cmdline-arg in --help text - string cleanups -- program-launchers for misc account types +- program-launchers for misc account types (announce on rvl-list + when done in svn) - use xpath for xml handling instead of plain dom? - add autosaving of files (needs a preference) - add docstrings to all objects, methods and functions diff -r 4787d15f40a47ecd4912e428c6d04ddd0dcb28e6 -r 268984dc47cf3aff7fe21e291e645cfca3afcc3d src/lib/data.py --- a/src/lib/data.py Thu Jul 08 21:11:32 2004 +0000 +++ b/src/lib/data.py Fri Jul 09 17:06:07 2004 +0000 @@ -26,18 +26,29 @@ import gtk, revelation, gobject -ENTRYSTORE_COL_NAME = 0 -ENTRYSTORE_COL_ICON = 1 -ENTRYSTORE_COL_TYPE = 2 -ENTRYSTORE_COL_DESC = 3 -ENTRYSTORE_COL_UPDATED = 4 -ENTRYSTORE_COL_FIELDS = 5 +ENTRYSTORE_COL_NAME = 0 +ENTRYSTORE_COL_ICON = 1 +ENTRYSTORE_COL_TYPE = 2 +ENTRYSTORE_COL_DESC = 3 +ENTRYSTORE_COL_UPDATED = 4 +ENTRYSTORE_COL_FIELDS = 5 -UNDO = "undo" -REDO = "redo" +UNDO_ACTION_ADD = "add" +UNDO_ACTION_CUT = "cut" +UNDO_ACTION_EDIT = "edit" +UNDO_ACTION_IMPORT = "import" +UNDO_ACTION_PASTE = "paste" +UNDO_ACTION_REMOVE = "remove" -SEARCH_NEXT = "next" -SEARCH_PREV = "prev" +UNDO_ACTIONTYPE_ADD = "add" +UNDO_ACTIONTYPE_EDIT = "edit" +UNDO_ACTIONTYPE_REMOVE = "remove" + +UNDO = "undo" +REDO = "redo" + +SEARCH_NEXT = "next" +SEARCH_PREV = "prev" class EntrySearch(gobject.GObject): @@ -111,7 +122,7 @@ class EntryStore(revelation.widget.TreeStore): - "A basic class structure for handling a tree of entries" + "A basic class for handling a tree of entries" def __init__(self): revelation.widget.TreeStore.__init__(self, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_INT, gobject.TYPE_PYOBJECT) @@ -244,19 +255,21 @@ def copy(self, entrystore, iters): - "Copies a group of nodes from an entrystore" + "Copies a group of entries from an entrystore" - if len(iters) > 0 and None not in iters: - self.clear() + if len(iters) == 0 or None in iters: + return - for iter in iters: - entrystore.export_entry(iter, self) + self.clear() - self.emit("copy") + for iter in iters: + entrystore.export_entry(iter, self) + + self.emit("copy") def cut(self, entrystore, iters): - "Cuts a group of nodes from an entrystore" + "Cuts a group of entries from an entrystore" self.copy(entrystore, iters) @@ -281,73 +294,189 @@ +class UndoAction(gobject.GObject): + "A class containing data about an action that can be undone/redone" + + def __init__(self, action): + gobject.GObject.__init__(self) + + self.set_action(action) + self.data = [] + + + def add_data(self, path, parent, data): + "Adds a piece of data to the action" + + self.data.append({ + "path" : path, + "parent" : parent, + "data" : data + }) + + + def set_action(self, action): + "Sets the type of action" + + self.action = action + + if self.action == UNDO_ACTION_ADD: + self.name = "Add Entry" + self.actiontype = UNDO_ACTIONTYPE_ADD + + elif self.action == UNDO_ACTION_CUT: + self.name = "Cut" + self.actiontype = UNDO_ACTIONTYPE_REMOVE + + elif self.action == UNDO_ACTION_EDIT: + self.name = "Edit Entry" + self.actiontype = UNDO_ACTIONTYPE_EDIT + + elif self.action == UNDO_ACTION_IMPORT: + self.name = "Import" + self.actiontype = UNDO_ACTIONTYPE_ADD + + elif self.action == UNDO_ACTION_PASTE: + self.name = "Paste" + self.actiontype = UNDO_ACTIONTYPE_ADD + + elif self.action == UNDO_ACTION_REMOVE: + self.name = "Remove Entry" + self.actiontype = UNDO_ACTIONTYPE_REMOVE + + + class UndoQueue(gobject.GObject): + "Handles undo/redo for an entrystore" - def __init__(self): + def __init__(self, entrystore): gobject.GObject.__init__(self) + + self.entrystore = entrystore + self.queue = [] + self.actionptr = 0 + + + def add_action(self, action, iters, extradata = None): + "Adds an action to the undo queue" + + if not isinstance(iters, list) and iters != None: + iters = [iters] + + # get data about the action + actionitem = UndoAction(action) + + for iter in iters: + path = self.entrystore.get_path(iter) + parent = self.entrystore.get_path(self.entrystore.iter_parent(iter)) + + if action == UNDO_ACTION_EDIT: + data = (self.entrystore.get_entry(iter), extradata) + + else: + data = EntryStore() + self.entrystore.export_entry(iter, data) + + actionitem.add_data(path, parent, data) + + + # remove any items later in the queue and add the action + del self.queue[self.actionptr:] + + self.queue.append(actionitem) + self.actionptr = len(self.queue) + + self.emit("changed") + + + def can_redo(self): + "Checks if a redo action is possible" + + return self.actionptr < len(self.queue) + + + def can_undo(self, method = UNDO): + "Checks if an undo action is possible" + + return self.actionptr > 0 + + + def clear(self): + "Clears the undo queue" + self.queue = [] self.actionptr = 0 + self.emit("changed") - def add_action(self, name, action, data): + def execute(self, action, method = UNDO): + "Executes and undo or redo action" - # remove any items later in the queue - del self.queue[self.actionptr:] + iters = [] - self.queue.append({ "name" : name, "action" : action, "data" : data }) - self.actionptr = len(self.queue) + # undo add, or redo remove (same operation) + if (method == UNDO and action.actiontype == UNDO_ACTIONTYPE_ADD) or (method == REDO and action.actiontype == UNDO_ACTIONTYPE_REMOVE): + for item in action.data: + item["iter"] = self.entrystore.get_iter(item["path"]) - self.emit("can-undo", self.can_undo()) - self.emit("can-redo", self.can_undo(REDO)) + for item in action.data: + self.entrystore.remove_entry(item["iter"]) + # undo remove, or redo add (same operation) + elif (method == UNDO and action.actiontype == UNDO_ACTIONTYPE_REMOVE) or (method == REDO and action.actiontype == UNDO_ACTIONTYPE_ADD): + for item in action.data: + newiters = self.entrystore.import_entrystore(item["data"], self.entrystore.get_iter(item["parent"]), self.entrystore.get_iter(item["path"])) + iters.extend(newiters) - def can_undo(self, method = UNDO): - if method == UNDO: - return self.actionptr > 0 - else: - return self.actionptr < len(self.queue) + # handle edit actions + elif action.actiontype == UNDO_ACTIONTYPE_EDIT: + iter = self.entrystore.get_iter(action.data[0]["path"]) + iters.append(iter) + if method == UNDO: + self.entrystore.update_entry(iter, data[0]["predata"]) - def clear(self): - self.queue = [] - self.actionptr = 0 + elif method == REDO: + self.entrystore.update_entry(iter, data[0]["data"]) - self.emit("can-undo", gtk.FALSE) - self.emit("can-redo", gtk.FALSE) + return iters - def get_data(self, method = UNDO): - if method == UNDO: - ptr = self.actionptr - 1 - else: - ptr = self.actionptr + def get_action(self, method = UNDO): + "Fetches the current UndoAction object for an operation" - if self.can_undo(method): - return self.queue[ptr] - else: - return None + if method == UNDO and self.can_undo(): + return self.queue[self.actionptr - 1] + + elif method == REDO and self.can_redo(): + return self.queue[self.actionptr] def redo(self): - if self.can_undo(REDO): - self.emit("redo", self.get_data(REDO)) - self.actionptr = self.actionptr + 1 + "Executes a redo operation" - self.emit("can-undo", self.can_undo()) - self.emit("can-redo", self.can_undo(REDO)) + if not self.can_redo(): + return + + iters = self.execute(self.get_action(REDO), REDO) + self.actionptr += 1 + self.emit("changed") + + return iters def undo(self): - if self.can_undo(): - self.emit("undo", self.get_data()) - self.actionptr = self.actionptr - 1 + "Executes an undo operation" - self.emit("can-undo", self.can_undo()) - self.emit("can-redo", self.can_undo(REDO)) + if not self.can_undo(): + return + iters = self.execute(self.get_action(), UNDO) + self.actionptr -= 1 + self.emit("changed") -gobject.signal_new("undo", UndoQueue, gobject.SIGNAL_ACTION, gobject.TYPE_BOOLEAN, (gobject.TYPE_PYOBJECT, )) -gobject.signal_new("redo", UndoQueue, gobject.SIGNAL_ACTION, gobject.TYPE_BOOLEAN, (gobject.TYPE_PYOBJECT, )) -gobject.signal_new("can-undo", UndoQueue, gobject.SIGNAL_ACTION, gobject.TYPE_BOOLEAN, (gobject.TYPE_BOOLEAN, )) -gobject.signal_new("can-redo", UndoQueue, gobject.SIGNAL_ACTION, gobject.TYPE_BOOLEAN, (gobject.TYPE_BOOLEAN, )) + return iters + +gobject.type_register(UndoQueue) +gobject.signal_new("changed", UndoQueue, gobject.SIGNAL_ACTION, gobject.TYPE_BOOLEAN, ()) + diff -r 4787d15f40a47ecd4912e428c6d04ddd0dcb28e6 -r 268984dc47cf3aff7fe21e291e645cfca3afcc3d src/lib/ui.py --- a/src/lib/ui.py Thu Jul 08 21:11:32 2004 +0000 +++ b/src/lib/ui.py Fri Jul 09 17:06:07 2004 +0000 @@ -85,9 +85,8 @@ self.clipboard.connect("copy", self.__cb_state_clipboard) self.clipboard.connect("cut", self.__cb_state_clipboard) - self.undoqueue = revelation.data.UndoQueue() - 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.undoqueue = revelation.data.UndoQueue(self.data) + self.undoqueue.connect("changed", self.__cb_state_undo) self.finder = revelation.data.EntrySearch(self.data) self.finder.connect("string_changed", self.__cb_state_find) @@ -258,21 +257,29 @@ self.gconf.set_bool("/apps/revelation/view/toolbar", active) - def __cb_state_undo(self, object, state, method): - if method == revelation.data.UNDO: - widget = self.if_menu.get_widget("
/Edit/Undo") - action = "_Undo" - elif method == revelation.data.REDO: - widget = self.if_menu.get_widget("
/Edit/Redo") - action = "_Redo" + def __cb_state_undo(self, object, data = None): - widget.set_sensitive(state) - item = widget.get_children()[0] + # update undo widgets + widget = self.if_menu.get_widget("
/Edit/Undo") + action = "_Undo" - if state == gtk.TRUE: - item.set_label(action + " " + self.undoqueue.get_data(method)["name"]) + if self.undoqueue.can_undo(): + widget.get_children()[0].set_label(action + " " + self.undoqueue.get_action(revelation.data.UNDO).name) + widget.set_sensitive(gtk.TRUE) else: - item.set_label(action) + widget.get_children()[0].set_label(action) + widget.set_sensitive(gtk.FALSE) + + # update redo widgets + widget = self.if_menu.get_widget("
/Edit/Redo") + action = "_Redo" + + if self.undoqueue.can_redo(): + widget.get_children()[0].set_label(action + " " + self.undoqueue.get_action(revelation.data.REDO).name) + widget.set_sensitive(gtk.TRUE) + else: + widget.get_children()[0].set_label(action) + widget.set_sensitive(gtk.FALSE) # misc other callbacks diff -r 4787d15f40a47ecd4912e428c6d04ddd0dcb28e6 -r 268984dc47cf3aff7fe21e291e645cfca3afcc3d src/revelation --- a/src/revelation Thu Jul 08 21:11:32 2004 +0000 +++ b/src/revelation Fri Jul 09 17:06:07 2004 +0000 @@ -57,8 +57,8 @@ "Edit/Add Entry..." : lambda w: self.entry_add(), "Edit/Edit" : lambda w: self.entry_edit(), "Edit/Remove" : lambda w: self.entry_remove(), - "Edit/Undo" : lambda w: self.undoqueue.undo(), - "Edit/Redo" : lambda w: self.undoqueue.redo(), + "Edit/Undo" : lambda w: self.undo(), + "Edit/Redo" : lambda w: self.redo(), "Edit/Cut" : lambda w: self.clip_cut(), "Edit/Copy" : lambda w: self.clip_copy(), "Edit/Paste" : lambda w: self.clip_paste(), @@ -73,8 +73,6 @@ self.connect("delete_event", self.__cb_quit) self.connect("tree-popup", self.__cb_popup_tree) - self.undoqueue.connect("undo", self.__cb_undo, revelation.data.UNDO) - self.undoqueue.connect("redo", self.__cb_undo, revelation.data.REDO) self.tree.connect("doubleclick", self.__cb_doubleclick_tree) @@ -113,10 +111,6 @@ return gtk.TRUE ^ self.quit() - def __cb_undo(self, object, data, method): - self.undo(data["name"], data["action"], data["data"], method) - - def __entry_find(self, parent, direction = revelation.data.SEARCH_NEXT): self.finder.folders = self.gconf.get_bool("/apps/revelation/search/folders") self.finder.casesens = self.gconf.get_bool("/apps/revelation/search/casesens") @@ -163,7 +157,7 @@ def clip_cut(self): iters = self.data.filter_parents(self.tree.get_selected()) - self.undo_add_action(self.clip_cut, iters) + self.undoqueue.add_action(revelation.data.UNDO_ACTION_CUT, iters) self.clipboard.cut(self.data, iters) self.tree.unselect_all() @@ -173,7 +167,7 @@ return iters = self.clipboard.paste(self.data, self.tree.get_active()) - self.undo_add_action(self.clip_paste, iters) + self.undoqueue.add_action(revelation.data.UNDO_ACTION_PASTE, iters) self.tree.select(iters[0]) @@ -181,7 +175,7 @@ try: entry = revelation.dialog.EditEntry(self, "Add entry").run() iter = self.data.add_entry(self.tree.get_active(), entry) - self.undo_add_action(self.entry_add, iter) + self.undoqueue.add_action(revelation.data.UNDO_ACTION_ADD, iter) self.tree.select(iter) self.statusbar.set_status("Added entry '" + entry.name + "'") @@ -204,7 +198,7 @@ newentry = dialog.run() self.data.update_entry(iter, newentry) - self.undo_add_action(self.entry_edit, iter, entry) + self.undoqueue.add_action(revelation.data.UNDO_ACTION_EDIT, iter, entry) self.tree.select(iter) self.statusbar.set_status("Updated entry '" + newentry.name + "'") @@ -260,7 +254,7 @@ if revelation.dialog.RemoveEntry(self, pritext, sectext).run() == gtk.TRUE: iters = self.data.filter_parents(iters) - self.undo_add_action(self.entry_remove, iters) + self.undoqueue.add_action(revelation.data.UNDO_ACTION_REMOVE, iters) for iter in iters: self.data.remove_entry(iter) @@ -330,7 +324,7 @@ else: iters = self.data.import_entrystore(entrystore) - self.undo_add_action(self.file_import, iters) + self.undoqueue.add_action(revelation.data.UNDO_ACTION_IMPORT, iters) self.statusbar.set_status("Data imported from " + datafile.file) @@ -527,6 +521,23 @@ return gtk.TRUE + def redo(self): + "Redo the previously undone operation" + + if not self.undoqueue.can_redo(): + return + + action = self.undoqueue.get_action(revelation.data.REDO) + iters = self.undoqueue.redo() + + if len(iters) > 0: + self.tree.select(iters[0]) + else: + self.tree.unselect_all() + + self.statusbar.set_status(action.name.capitalize() + " redone") + + def run(self, file = None): if file != None: self.file_open(file) @@ -536,100 +547,21 @@ gtk.main() - def undo(self, name, action, data, method = revelation.data.UNDO): - iters = [] + def undo(self): + "Attempts to undo the previous operation" - # handle add, paste and import actions - if action == self.entry_add or action == self.clip_paste or action == self.file_import: + if not self.undoqueue.can_undo(): + return - 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(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(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") + action = self.undoqueue.get_action() + iters = self.undoqueue.undo() if len(iters) > 0: self.tree.select(iters[0]) else: self.tree.unselect_all() - - def undo_add_action(self, action, iters, extradata = None): - 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"] = revelation.data.EntryStore() - self.data.export_entry(iter, item["data"]) - - 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) + self.statusbar.set_status(action.name.capitalize() + " undone")