From c2dc10471c0b531a5dec8e96e52829b4c7779213 Mon Sep 17 00:00:00 2001 From: Benny Malengier Date: Mon, 30 Aug 2010 23:00:28 +0000 Subject: [PATCH] * missing installation stuff for undo/redo * create a first undo gtk entry field: family name in person editor svn: r15837 --- po/POTFILES.in | 3 + src/glade/catalog/grampswidgets.py | 3 + src/glade/catalog/grampswidgets.xml | 5 + src/glade/editperson.glade | 3 +- src/gui/widgets/Makefile.am | 2 + src/gui/widgets/__init__.py | 3 + src/gui/widgets/styledtextbuffer.py | 2 +- src/gui/widgets/undoablebuffer.py | 2 + src/gui/widgets/undoableentry.py | 311 ++++++++++++++++++++++++ src/gui/widgets/undoablestyledbuffer.py | 4 +- 10 files changed, 333 insertions(+), 5 deletions(-) create mode 100644 src/gui/widgets/undoableentry.py diff --git a/po/POTFILES.in b/po/POTFILES.in index 559514e1c..232b0cd99 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -326,6 +326,9 @@ src/gui/widgets/labels.py src/gui/widgets/menutoolbuttonaction.py src/gui/widgets/progressdialog.py src/gui/widgets/styledtexteditor.py +src/gui/widgets/undoablebuffer.py +src/gui/widgets/undoableentry.py +src/gui/widgets/undoablestyledbuffer.py src/gui/widgets/validatedmaskedentry.py # Simple API diff --git a/src/glade/catalog/grampswidgets.py b/src/glade/catalog/grampswidgets.py index 7601dc605..6c973459c 100644 --- a/src/glade/catalog/grampswidgets.py +++ b/src/glade/catalog/grampswidgets.py @@ -3,6 +3,9 @@ import gtk class ValidatableMaskedEntry(gtk.Entry): __gtype_name__ = 'ValidatableMaskedEntry' +class UndoableEntry(gtk.Entry): + __gtype_name__ = 'UndoableEntry' + class StyledTextEditor(gtk.TextView): __gtype_name__ = 'StyledTextEditor' diff --git a/src/glade/catalog/grampswidgets.xml b/src/glade/catalog/grampswidgets.xml index 00849fc0a..ad977560a 100644 --- a/src/glade/catalog/grampswidgets.xml +++ b/src/glade/catalog/grampswidgets.xml @@ -6,6 +6,10 @@ name="ValidatableMaskedEntry" title="Validatable Masked Entry" generic-name="valid_mask"/> + + diff --git a/src/glade/editperson.glade b/src/glade/editperson.glade index c078b60a6..493bb83a6 100644 --- a/src/glade/editperson.glade +++ b/src/glade/editperson.glade @@ -1,6 +1,7 @@ + True @@ -149,7 +150,7 @@ - + True True part of a person's name indicating the family to which the person belongs diff --git a/src/gui/widgets/Makefile.am b/src/gui/widgets/Makefile.am index 059dd5245..af450ad6c 100644 --- a/src/gui/widgets/Makefile.am +++ b/src/gui/widgets/Makefile.am @@ -24,6 +24,8 @@ pkgdata_PYTHON = \ tageditor.py \ toolcomboentry.py \ undoablebuffer.py \ + undoableentry.py \ + undoablestyledbuffer.py \ validatedcomboentry.py \ validatedmaskedentry.py \ valueaction.py \ diff --git a/src/gui/widgets/__init__.py b/src/gui/widgets/__init__.py index bb3f95922..c17218c5a 100644 --- a/src/gui/widgets/__init__.py +++ b/src/gui/widgets/__init__.py @@ -34,6 +34,9 @@ from statusbar import Statusbar from styledtextbuffer import * from styledtexteditor import * from toolcomboentry import * +from undoablebuffer import * +from undoableentry import * +from undoablestyledbuffer import * from validatedcomboentry import * from validatedmaskedentry import * from valueaction import * diff --git a/src/gui/widgets/styledtextbuffer.py b/src/gui/widgets/styledtextbuffer.py index fa62c04cd..0855aaa0e 100644 --- a/src/gui/widgets/styledtextbuffer.py +++ b/src/gui/widgets/styledtextbuffer.py @@ -20,7 +20,7 @@ # $Id$ -"Text buffer subclassed from gtk.TextBuffer handling L{StyledText}." +"""Text buffer subclassed from gtk.TextBuffer handling L{StyledText}.""" __all__ = ["ALLOWED_STYLES", "MATCH_START", "MATCH_END", "MATCH_FLAVOR", "MATCH_STRING", "StyledTextBuffer"] diff --git a/src/gui/widgets/undoablebuffer.py b/src/gui/widgets/undoablebuffer.py index c8343056a..1e032ff52 100644 --- a/src/gui/widgets/undoablebuffer.py +++ b/src/gui/widgets/undoablebuffer.py @@ -26,6 +26,8 @@ gtk textbuffer with undo functionality """ +__all__ = ["UndoableBuffer"] + # Originally LGLP from: # http://bitbucket.org/tiax/gtk-textbuffer-with-undo/ # Please send bugfixes and comments upstream to Florian diff --git a/src/gui/widgets/undoableentry.py b/src/gui/widgets/undoableentry.py new file mode 100644 index 000000000..176d8d609 --- /dev/null +++ b/src/gui/widgets/undoableentry.py @@ -0,0 +1,311 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2010 Benny Malengier +# +# based on undoablebuffer Copyright (C) 2009 Florian Heinle +# +# 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: validatedmaskedentry.py 14091 2010-01-18 04:42:17Z pez4brian $ + +__all__ = ["UndoableEntry"] + +#------------------------------------------------------------------------- +# +# Standard python modules +# +#------------------------------------------------------------------------- +from gen.ggettext import gettext as _ + +import logging +_LOG = logging.getLogger(".widgets.undoableentry") + +#------------------------------------------------------------------------- +# +# GTK/Gnome modules +# +#------------------------------------------------------------------------- +import gobject +import gtk + +#------------------------------------------------------------------------- +# +# Gramps modules +# +#------------------------------------------------------------------------- +from undoablebuffer import Stack + +class UndoableInsertEntry(object): + """something that has been inserted into our gtk.editable""" + def __init__(self, text, length, position, editable): + self.offset = position + 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 UndoableDeleteEntry(object): + """something that has been deleted from our textbuffer""" + def __init__(self, editable, start, end): + self.text = str(editable.get_chars(start, end)) + self.start = start + self.end = end + # need to find out if backspace or delete key has been used + # so we don't mess up during redo + insert = editable.get_position() + if insert <= 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 UndoableEntry(gtk.Entry): + """ + The UndoableEntry is an Entry subclass with additional features. + + Additional features: + - Undo and Redo on CTRL-Z/CTRL-SHIFT-Z + """ + __gtype_name__ = 'UndoableEntry' + + insertclass = UndoableInsertEntry + deleteclass = UndoableDeleteEntry + + #how many undo's are remembered + undo_stack_size = 50 + + def __init__(self): + gtk.Entry.__init__(self) + self.undo_stack = Stack(self.undo_stack_size) + self.redo_stack = [] + self.not_undoable_action = False + self.undo_in_progress = False + + self.connect('insert-text', self._on_insert_text) + self.connect('delete-text', self._on_delete_text) + self.connect('key-press-event', self._on_key_press_event) + + def set_text(self, text): + gtk.Entry.set_text(self, text) + self.reset() + + def _on_key_press_event(self, widget, event): + """Signal handler. + Handle formatting undo/redo key press. + + """ + if ((gtk.gdk.keyval_name(event.keyval) == 'Z') and + (event.state & gtk.gdk.CONTROL_MASK) and + (event.state & gtk.gdk.SHIFT_MASK)): + self.redo() + return True + elif ((gtk.gdk.keyval_name(event.keyval) == 'z') and + (event.state & gtk.gdk.CONTROL_MASK)): + self.undo() + return True + + return False + + def __empty_redo_stack(self): + self.redo_stack = [] + + def _on_insert_text(self, editable, text, length, positionptr): + 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.__empty_redo_stack() + if self.not_undoable_action: + return + undo_action = self.insertclass(text, length, editable.get_position(), + editable) + try: + prev_insert = self.undo_stack.pop() + except IndexError: + self.undo_stack.append(undo_action) + return + if not isinstance(prev_insert, self.insertclass): + 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_text(self, editable, start, end): + 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.__empty_redo_stack() + if self.not_undoable_action: + return + undo_action = self.deleteclass(editable, start, end) + try: + prev_delete = self.undo_stack.pop() + except IndexError: + self.undo_stack.append(undo_action) + return + if not isinstance(prev_delete, self.deleteclass): + 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 reset(self): + """ + Resets buffer to initial state. + """ + self.undo_stack = Stack(self.undo_stack_size) + self.redo_stack[:] = [] + self.not_undoable_action = False + self.undo_in_progress = 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, self.insertclass): + self._undo_insert(undo_action) + elif isinstance(undo_action, self.deleteclass): + self._undo_delete(undo_action) + else: + self._handle_undo(undo_action) + self.end_not_undoable_action() + self.undo_in_progress = False + + def _undo_insert(self, undo_action): + start = undo_action.offset + stop = undo_action.offset + undo_action.length + self.delete_text(start, stop) + self.set_position(undo_action.offset) + + def _undo_delete(self, undo_action): + self.insert_text(undo_action.text, undo_action.start) + if undo_action.delete_key_used: + self.set_position(undo_action.start) + else: + self.set_position(undo_action.end) + + def _handle_undo(self, undo_action): + raise NotImplementedError + + 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, self.insertclass): + self._redo_insert(redo_action) + elif isinstance(redo_action, self.deleteclass): + self._redo_delete(redo_action) + else: + self._handle_redo(redo_action) + self.end_not_undoable_action() + self.undo_in_progress = False + + def _redo_insert(self, redo_action): + self.insert_text(redo_action.text, redo_action.offset) + new_cursor_pos = redo_action.offset + redo_action.length + self.set_position(new_cursor_pos) + + def _redo_delete(self, redo_action): + start = redo_action.start + stop = redo_action.end + self.delete_text(start, stop) + self.set_position(redo_action.start) + + def _handle_redo(self, redo_action): + raise NotImplementedError diff --git a/src/gui/widgets/undoablestyledbuffer.py b/src/gui/widgets/undoablestyledbuffer.py index 53a177c10..9488ccdde 100644 --- a/src/gui/widgets/undoablestyledbuffer.py +++ b/src/gui/widgets/undoablestyledbuffer.py @@ -26,9 +26,7 @@ gtk textbuffer with undo functionality """ -# Originally LGLP from: -# http://bitbucket.org/tiax/gtk-textbuffer-with-undo/ -# Please send bugfixes and comments upstream to Florian +__all__ = ["UndoableStyledBuffer"] import gtk