* missing installation stuff for undo/redo
* create a first undo gtk entry field: family name in person editor svn: r15837
This commit is contained in:
parent
fe311affb2
commit
c2dc10471c
@ -326,6 +326,9 @@ src/gui/widgets/labels.py
|
|||||||
src/gui/widgets/menutoolbuttonaction.py
|
src/gui/widgets/menutoolbuttonaction.py
|
||||||
src/gui/widgets/progressdialog.py
|
src/gui/widgets/progressdialog.py
|
||||||
src/gui/widgets/styledtexteditor.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
|
src/gui/widgets/validatedmaskedentry.py
|
||||||
|
|
||||||
# Simple API
|
# Simple API
|
||||||
|
@ -3,6 +3,9 @@ import gtk
|
|||||||
class ValidatableMaskedEntry(gtk.Entry):
|
class ValidatableMaskedEntry(gtk.Entry):
|
||||||
__gtype_name__ = 'ValidatableMaskedEntry'
|
__gtype_name__ = 'ValidatableMaskedEntry'
|
||||||
|
|
||||||
|
class UndoableEntry(gtk.Entry):
|
||||||
|
__gtype_name__ = 'UndoableEntry'
|
||||||
|
|
||||||
class StyledTextEditor(gtk.TextView):
|
class StyledTextEditor(gtk.TextView):
|
||||||
__gtype_name__ = 'StyledTextEditor'
|
__gtype_name__ = 'StyledTextEditor'
|
||||||
|
|
||||||
|
@ -6,6 +6,10 @@
|
|||||||
name="ValidatableMaskedEntry"
|
name="ValidatableMaskedEntry"
|
||||||
title="Validatable Masked Entry"
|
title="Validatable Masked Entry"
|
||||||
generic-name="valid_mask"/>
|
generic-name="valid_mask"/>
|
||||||
|
<glade-widget-class
|
||||||
|
name="UndoableEntry"
|
||||||
|
title="Undoable Entry"
|
||||||
|
generic-name="undo_entry"/>
|
||||||
<glade-widget-class
|
<glade-widget-class
|
||||||
name="StyledTextEditor"
|
name="StyledTextEditor"
|
||||||
title="Styled Text Editor"
|
title="Styled Text Editor"
|
||||||
@ -13,6 +17,7 @@
|
|||||||
</glade-widget-classes>
|
</glade-widget-classes>
|
||||||
<glade-widget-group name="GrampsWidgets" title="Gramps Widgets">
|
<glade-widget-group name="GrampsWidgets" title="Gramps Widgets">
|
||||||
<glade-widget-class-ref name="ValidatableMaskedEntry"/>
|
<glade-widget-class-ref name="ValidatableMaskedEntry"/>
|
||||||
|
<glade-widget-class-ref name="UndoableEntry"/>
|
||||||
<glade-widget-class-ref name="StyledTextEditor"/>
|
<glade-widget-class-ref name="StyledTextEditor"/>
|
||||||
</glade-widget-group>
|
</glade-widget-group>
|
||||||
</glade-catalog>
|
</glade-catalog>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<?xml version="1.0"?>
|
<?xml version="1.0"?>
|
||||||
<interface>
|
<interface>
|
||||||
<!-- interface-requires gtk+ 2.12 -->
|
<!-- interface-requires gtk+ 2.12 -->
|
||||||
|
<!-- interface-requires grampswidgets 0.0 -->
|
||||||
<!-- interface-naming-policy toplevel-contextual -->
|
<!-- interface-naming-policy toplevel-contextual -->
|
||||||
<object class="GtkDialog" id="editperson">
|
<object class="GtkDialog" id="editperson">
|
||||||
<property name="has_focus">True</property>
|
<property name="has_focus">True</property>
|
||||||
@ -149,7 +150,7 @@
|
|||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkEntry" id="surname">
|
<object class="UndoableEntry" id="surname">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="tooltip_text" translatable="yes">part of a person's name indicating the family to which the person belongs</property>
|
<property name="tooltip_text" translatable="yes">part of a person's name indicating the family to which the person belongs</property>
|
||||||
|
@ -24,6 +24,8 @@ pkgdata_PYTHON = \
|
|||||||
tageditor.py \
|
tageditor.py \
|
||||||
toolcomboentry.py \
|
toolcomboentry.py \
|
||||||
undoablebuffer.py \
|
undoablebuffer.py \
|
||||||
|
undoableentry.py \
|
||||||
|
undoablestyledbuffer.py \
|
||||||
validatedcomboentry.py \
|
validatedcomboentry.py \
|
||||||
validatedmaskedentry.py \
|
validatedmaskedentry.py \
|
||||||
valueaction.py \
|
valueaction.py \
|
||||||
|
@ -34,6 +34,9 @@ from statusbar import Statusbar
|
|||||||
from styledtextbuffer import *
|
from styledtextbuffer import *
|
||||||
from styledtexteditor import *
|
from styledtexteditor import *
|
||||||
from toolcomboentry import *
|
from toolcomboentry import *
|
||||||
|
from undoablebuffer import *
|
||||||
|
from undoableentry import *
|
||||||
|
from undoablestyledbuffer import *
|
||||||
from validatedcomboentry import *
|
from validatedcomboentry import *
|
||||||
from validatedmaskedentry import *
|
from validatedmaskedentry import *
|
||||||
from valueaction import *
|
from valueaction import *
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
# $Id$
|
# $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",
|
__all__ = ["ALLOWED_STYLES", "MATCH_START", "MATCH_END", "MATCH_FLAVOR",
|
||||||
"MATCH_STRING", "StyledTextBuffer"]
|
"MATCH_STRING", "StyledTextBuffer"]
|
||||||
|
@ -26,6 +26,8 @@
|
|||||||
gtk textbuffer with undo functionality
|
gtk textbuffer with undo functionality
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
__all__ = ["UndoableBuffer"]
|
||||||
|
|
||||||
# Originally LGLP from:
|
# Originally LGLP from:
|
||||||
# http://bitbucket.org/tiax/gtk-textbuffer-with-undo/
|
# http://bitbucket.org/tiax/gtk-textbuffer-with-undo/
|
||||||
# Please send bugfixes and comments upstream to Florian
|
# Please send bugfixes and comments upstream to Florian
|
||||||
|
311
src/gui/widgets/undoableentry.py
Normal file
311
src/gui/widgets/undoableentry.py
Normal file
@ -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
|
@ -26,9 +26,7 @@
|
|||||||
gtk textbuffer with undo functionality
|
gtk textbuffer with undo functionality
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Originally LGLP from:
|
__all__ = ["UndoableStyledBuffer"]
|
||||||
# http://bitbucket.org/tiax/gtk-textbuffer-with-undo/
|
|
||||||
# Please send bugfixes and comments upstream to Florian
|
|
||||||
|
|
||||||
import gtk
|
import gtk
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user