Added UndoableBuffer for notes that allows undo (control+z) and redo (control+shift+z)

svn: r15751
This commit is contained in:
Doug Blank 2010-08-16 11:44:26 +00:00
parent 14a5f9a1b8
commit 6a26f7b873
4 changed files with 265 additions and 5 deletions

View File

@ -22,6 +22,7 @@ pkgdata_PYTHON = \
styledtextbuffer.py \
styledtexteditor.py \
toolcomboentry.py \
undoablebuffer.py \
validatedcomboentry.py \
validatedmaskedentry.py \
valueaction.py \

View File

@ -43,6 +43,7 @@ _LOG = logging.getLogger(".widgets.styledtextbuffer")
#-------------------------------------------------------------------------
import gobject
import gtk
from gui.widgets.undoablebuffer import UndoableBuffer
from pango import WEIGHT_BOLD, STYLE_ITALIC, UNDERLINE_SINGLE
#-------------------------------------------------------------------------
@ -225,7 +226,7 @@ class GtkSpellState(object):
# StyledTextBuffer class
#
#-------------------------------------------------------------------------
class StyledTextBuffer(gtk.TextBuffer):
class StyledTextBuffer(UndoableBuffer):
"""An extended TextBuffer for handling StyledText strings.
StyledTextBuffer is an interface between GRAMPS' L{StyledText} format
@ -256,7 +257,7 @@ class StyledTextBuffer(gtk.TextBuffer):
}
def __init__(self):
gtk.TextBuffer.__init__(self)
super(StyledTextBuffer, self).__init__()
# Create fix tags.
# Other tags (e.g. color) have to be created on the fly
@ -327,7 +328,7 @@ class StyledTextBuffer(gtk.TextBuffer):
def do_changed(self):
"""Parse for patterns in the text."""
self.matches = []
text = unicode(gtk.TextBuffer.get_text(self,
text = unicode(super(StyledTextBuffer, self).get_text(
self.get_start_iter(),
self.get_end_iter()))
for regex, flavor in self.patterns:
@ -547,7 +548,7 @@ class StyledTextBuffer(gtk.TextBuffer):
@note: 's_' prefix means StyledText*, while 'g_' prefix means gtk.*.
"""
gtk.TextBuffer.set_text(self, str(s_text))
super(StyledTextBuffer, self).set_text(str(s_text))
s_tags = s_text.get_tags()
for s_tag in s_tags:
@ -574,7 +575,7 @@ class StyledTextBuffer(gtk.TextBuffer):
if end is None:
end = self.get_end_iter()
txt = gtk.TextBuffer.get_text(self, start, end, include_hidden_chars)
txt = super(StyledTextBuffer, self).get_text(start, end, include_hidden_chars)
txt = unicode(txt)
# extract tags out of the buffer

View File

@ -412,6 +412,8 @@ class StyledTextEditor(gtk.TextView):
_('Bold'), self._on_toggle_action_activate),
(str(StyledTextTagType.UNDERLINE), gtk.STOCK_UNDERLINE, None, None,
_('Underline'), self._on_toggle_action_activate),
("Undo", gtk.STOCK_UNDO, None, None, _('Undo'), self.undo),
("Redo", gtk.STOCK_REDO, None, None, _('Redo'), self.redo),
]
self.toggle_actions = [action[0] for action in format_toggle_actions]
@ -461,6 +463,8 @@ class StyledTextEditor(gtk.TextView):
'<Control>i': str(StyledTextTagType.ITALIC),
'<Control>b': str(StyledTextTagType.BOLD),
'<Control>u': str(StyledTextTagType.UNDERLINE),
'<Control>z' : "Undo",
'<Control><Shift>z': "Redo",
}
# create the action group and insert all the actions
@ -731,6 +735,12 @@ class StyledTextEditor(gtk.TextView):
"""
return self.toolbar
def undo(self, obj):
self.textbuffer.undo()
def redo(self, obj):
self.textbuffer.redo()
def uri_dialog(self, uri, callback):
"""
Function to spawn the link editor.

View File

@ -0,0 +1,248 @@
#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2009 Florian Heinle
# Copyright (C) 2010 Doug Blank <doug.blank@gmail.com>
#
# 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)
self.connect('delete-range', self.on_delete_range)
@property
def can_undo(self):
return bool(self.undo_stack)
@property
def can_redo(self):
return bool(self.redo_stack)
def on_insert_text(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(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