Added UndoableBuffer for notes that allows undo (control+z) and redo (control+shift+z)
svn: r15751
This commit is contained in:
parent
14a5f9a1b8
commit
6a26f7b873
@ -22,6 +22,7 @@ pkgdata_PYTHON = \
|
||||
styledtextbuffer.py \
|
||||
styledtexteditor.py \
|
||||
toolcomboentry.py \
|
||||
undoablebuffer.py \
|
||||
validatedcomboentry.py \
|
||||
validatedmaskedentry.py \
|
||||
valueaction.py \
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
248
src/gui/widgets/undoablebuffer.py
Normal file
248
src/gui/widgets/undoablebuffer.py
Normal 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
|
Loading…
Reference in New Issue
Block a user