* 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/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
|
||||
|
@ -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'
|
||||
|
||||
|
@ -6,6 +6,10 @@
|
||||
name="ValidatableMaskedEntry"
|
||||
title="Validatable Masked Entry"
|
||||
generic-name="valid_mask"/>
|
||||
<glade-widget-class
|
||||
name="UndoableEntry"
|
||||
title="Undoable Entry"
|
||||
generic-name="undo_entry"/>
|
||||
<glade-widget-class
|
||||
name="StyledTextEditor"
|
||||
title="Styled Text Editor"
|
||||
@ -13,6 +17,7 @@
|
||||
</glade-widget-classes>
|
||||
<glade-widget-group name="GrampsWidgets" title="Gramps Widgets">
|
||||
<glade-widget-class-ref name="ValidatableMaskedEntry"/>
|
||||
<glade-widget-class-ref name="UndoableEntry"/>
|
||||
<glade-widget-class-ref name="StyledTextEditor"/>
|
||||
</glade-widget-group>
|
||||
</glade-catalog>
|
||||
|
@ -1,6 +1,7 @@
|
||||
<?xml version="1.0"?>
|
||||
<interface>
|
||||
<!-- interface-requires gtk+ 2.12 -->
|
||||
<!-- interface-requires grampswidgets 0.0 -->
|
||||
<!-- interface-naming-policy toplevel-contextual -->
|
||||
<object class="GtkDialog" id="editperson">
|
||||
<property name="has_focus">True</property>
|
||||
@ -149,7 +150,7 @@
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="surname">
|
||||
<object class="UndoableEntry" id="surname">
|
||||
<property name="visible">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>
|
||||
|
@ -24,6 +24,8 @@ pkgdata_PYTHON = \
|
||||
tageditor.py \
|
||||
toolcomboentry.py \
|
||||
undoablebuffer.py \
|
||||
undoableentry.py \
|
||||
undoablestyledbuffer.py \
|
||||
validatedcomboentry.py \
|
||||
validatedmaskedentry.py \
|
||||
valueaction.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 *
|
||||
|
@ -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"]
|
||||
|
@ -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
|
||||
|
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
|
||||
"""
|
||||
|
||||
# Originally LGLP from:
|
||||
# http://bitbucket.org/tiax/gtk-textbuffer-with-undo/
|
||||
# Please send bugfixes and comments upstream to Florian
|
||||
__all__ = ["UndoableStyledBuffer"]
|
||||
|
||||
import gtk
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user