# # Gramps - a GTK+/GNOME based genealogy program # # Copyright (C) 2009 Florian Heinle # Copyright (C) 2010 Doug Blank # # 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 # # $Id: $ """ gtk textbuffer with undo functionality """ # Originally LGLP from: # http://bitbucket.org/tiax/gtk-textbuffer-with-undo/ # Please send bugfixes and comments upstream to Florian import gtk class UndoableInsert(object): """something that has been inserted into our textbuffer""" def __init__(self, text_iter, text, length): self.offset = text_iter.get_offset() # FIXME: GRAMPS change: force to use string rather than # StyledText self.text = str(text) self.length = length if self.length > 1 or self.text in ("\r", "\n", " "): self.mergeable = False else: self.mergeable = True class UndoableDelete(object): """something that has ben deleted from our textbuffer""" def __init__(self, text_buffer, start_iter, end_iter): # FIXME: GRAMPS change: force to use string rather than # StyledText self.text = str(text_buffer.get_text(start_iter, end_iter)) self.start = start_iter.get_offset() self.end = end_iter.get_offset() # need to find out if backspace or delete key has been used # so we don't mess up during redo insert_iter = text_buffer.get_iter_at_mark(text_buffer.get_insert()) if insert_iter.get_offset() <= self.start: self.delete_key_used = True else: self.delete_key_used = False if self.end - self.start > 1 or self.text in ("\r", "\n", " "): self.mergeable = False else: self.mergeable = True class UndoableBuffer(gtk.TextBuffer): """text buffer with added undo capabilities designed as a drop-in replacement for gtksourceview, at least as far as undo is concerned""" def __init__(self): """ we'll need empty stacks for undo/redo and some state keeping """ gtk.TextBuffer.__init__(self) self.undo_stack = [] self.redo_stack = [] self.not_undoable_action = False self.undo_in_progress = False self.connect('insert-text', self.on_insert_text_undoable) self.connect('delete-range', self.on_delete_range_undoable) @property def can_undo(self): return bool(self.undo_stack) @property def can_redo(self): return bool(self.redo_stack) def on_insert_text_undoable(self, textbuffer, text_iter, text, length): def can_be_merged(prev, cur): """see if we can merge multiple inserts here will try to merge words or whitespace can't merge if prev and cur are not mergeable in the first place can't merge when user set the input bar somewhere else can't merge across word boundaries""" WHITESPACE = (' ', '\t') if not cur.mergeable or not prev.mergeable: return False elif cur.offset != (prev.offset + prev.length): return False elif cur.text in WHITESPACE and not prev.text in WHITESPACE: return False elif prev.text in WHITESPACE and not cur.text in WHITESPACE: return False return True if not self.undo_in_progress: self.redo_stack = [] if self.not_undoable_action: return undo_action = UndoableInsert(text_iter, text, length) try: prev_insert = self.undo_stack.pop() except IndexError: self.undo_stack.append(undo_action) return if not isinstance(prev_insert, UndoableInsert): self.undo_stack.append(prev_insert) self.undo_stack.append(undo_action) return if can_be_merged(prev_insert, undo_action): prev_insert.length += undo_action.length prev_insert.text += undo_action.text self.undo_stack.append(prev_insert) else: self.undo_stack.append(prev_insert) self.undo_stack.append(undo_action) def on_delete_range_undoable(self, text_buffer, start_iter, end_iter): def can_be_merged(prev, cur): """see if we can merge multiple deletions here will try to merge words or whitespace can't merge if prev and cur are not mergeable in the first place can't merge if delete and backspace key were both used can't merge across word boundaries""" WHITESPACE = (' ', '\t') if not cur.mergeable or not prev.mergeable: return False elif prev.delete_key_used != cur.delete_key_used: return False elif prev.start != cur.start and prev.start != cur.end: return False elif cur.text not in WHITESPACE and \ prev.text in WHITESPACE: return False elif cur.text in WHITESPACE and \ prev.text not in WHITESPACE: return False return True if not self.undo_in_progress: self.redo_stack = [] if self.not_undoable_action: return undo_action = UndoableDelete(text_buffer, start_iter, end_iter) try: prev_delete = self.undo_stack.pop() except IndexError: self.undo_stack.append(undo_action) return if not isinstance(prev_delete, UndoableDelete): self.undo_stack.append(prev_delete) self.undo_stack.append(undo_action) return if can_be_merged(prev_delete, undo_action): if prev_delete.start == undo_action.start: # delete key used prev_delete.text += undo_action.text prev_delete.end += (undo_action.end - undo_action.start) else: # Backspace used prev_delete.text = "%s%s" % (undo_action.text, prev_delete.text) prev_delete.start = undo_action.start self.undo_stack.append(prev_delete) else: self.undo_stack.append(prev_delete) self.undo_stack.append(undo_action) def begin_not_undoable_action(self): """don't record the next actions toggles self.not_undoable_action""" self.not_undoable_action = True def end_not_undoable_action(self): """record next actions toggles self.not_undoable_action""" self.not_undoable_action = False def undo(self): """undo inserts or deletions undone actions are being moved to redo stack""" if not self.undo_stack: return self.begin_not_undoable_action() self.undo_in_progress = True undo_action = self.undo_stack.pop() self.redo_stack.append(undo_action) if isinstance(undo_action, UndoableInsert): start = self.get_iter_at_offset(undo_action.offset) stop = self.get_iter_at_offset( undo_action.offset + undo_action.length ) self.delete(start, stop) self.place_cursor(start) else: start = self.get_iter_at_offset(undo_action.start) self.insert(start, undo_action.text) stop = self.get_iter_at_offset(undo_action.end) if undo_action.delete_key_used: self.place_cursor(start) else: self.place_cursor(stop) self.end_not_undoable_action() self.undo_in_progress = False def redo(self): """redo inserts or deletions redone actions are moved to undo stack""" if not self.redo_stack: return self.begin_not_undoable_action() self.undo_in_progress = True redo_action = self.redo_stack.pop() self.undo_stack.append(redo_action) if isinstance(redo_action, UndoableInsert): start = self.get_iter_at_offset(redo_action.offset) self.insert(start, redo_action.text) new_cursor_pos = self.get_iter_at_offset( redo_action.offset + redo_action.length ) self.place_cursor(new_cursor_pos) else: start = self.get_iter_at_offset(redo_action.start) stop = self.get_iter_at_offset(redo_action.end) self.delete(start, stop) self.place_cursor(start) self.end_not_undoable_action() self.undo_in_progress = False