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 \
|
styledtextbuffer.py \
|
||||||
styledtexteditor.py \
|
styledtexteditor.py \
|
||||||
toolcomboentry.py \
|
toolcomboentry.py \
|
||||||
|
undoablebuffer.py \
|
||||||
validatedcomboentry.py \
|
validatedcomboentry.py \
|
||||||
validatedmaskedentry.py \
|
validatedmaskedentry.py \
|
||||||
valueaction.py \
|
valueaction.py \
|
||||||
|
@ -43,6 +43,7 @@ _LOG = logging.getLogger(".widgets.styledtextbuffer")
|
|||||||
#-------------------------------------------------------------------------
|
#-------------------------------------------------------------------------
|
||||||
import gobject
|
import gobject
|
||||||
import gtk
|
import gtk
|
||||||
|
from gui.widgets.undoablebuffer import UndoableBuffer
|
||||||
from pango import WEIGHT_BOLD, STYLE_ITALIC, UNDERLINE_SINGLE
|
from pango import WEIGHT_BOLD, STYLE_ITALIC, UNDERLINE_SINGLE
|
||||||
|
|
||||||
#-------------------------------------------------------------------------
|
#-------------------------------------------------------------------------
|
||||||
@ -225,7 +226,7 @@ class GtkSpellState(object):
|
|||||||
# StyledTextBuffer class
|
# StyledTextBuffer class
|
||||||
#
|
#
|
||||||
#-------------------------------------------------------------------------
|
#-------------------------------------------------------------------------
|
||||||
class StyledTextBuffer(gtk.TextBuffer):
|
class StyledTextBuffer(UndoableBuffer):
|
||||||
"""An extended TextBuffer for handling StyledText strings.
|
"""An extended TextBuffer for handling StyledText strings.
|
||||||
|
|
||||||
StyledTextBuffer is an interface between GRAMPS' L{StyledText} format
|
StyledTextBuffer is an interface between GRAMPS' L{StyledText} format
|
||||||
@ -256,7 +257,7 @@ class StyledTextBuffer(gtk.TextBuffer):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
gtk.TextBuffer.__init__(self)
|
super(StyledTextBuffer, self).__init__()
|
||||||
|
|
||||||
# Create fix tags.
|
# Create fix tags.
|
||||||
# Other tags (e.g. color) have to be created on the fly
|
# Other tags (e.g. color) have to be created on the fly
|
||||||
@ -327,7 +328,7 @@ class StyledTextBuffer(gtk.TextBuffer):
|
|||||||
def do_changed(self):
|
def do_changed(self):
|
||||||
"""Parse for patterns in the text."""
|
"""Parse for patterns in the text."""
|
||||||
self.matches = []
|
self.matches = []
|
||||||
text = unicode(gtk.TextBuffer.get_text(self,
|
text = unicode(super(StyledTextBuffer, self).get_text(
|
||||||
self.get_start_iter(),
|
self.get_start_iter(),
|
||||||
self.get_end_iter()))
|
self.get_end_iter()))
|
||||||
for regex, flavor in self.patterns:
|
for regex, flavor in self.patterns:
|
||||||
@ -547,7 +548,7 @@ class StyledTextBuffer(gtk.TextBuffer):
|
|||||||
@note: 's_' prefix means StyledText*, while 'g_' prefix means gtk.*.
|
@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()
|
s_tags = s_text.get_tags()
|
||||||
for s_tag in s_tags:
|
for s_tag in s_tags:
|
||||||
@ -574,7 +575,7 @@ class StyledTextBuffer(gtk.TextBuffer):
|
|||||||
if end is None:
|
if end is None:
|
||||||
end = self.get_end_iter()
|
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)
|
txt = unicode(txt)
|
||||||
|
|
||||||
# extract tags out of the buffer
|
# extract tags out of the buffer
|
||||||
|
@ -412,6 +412,8 @@ class StyledTextEditor(gtk.TextView):
|
|||||||
_('Bold'), self._on_toggle_action_activate),
|
_('Bold'), self._on_toggle_action_activate),
|
||||||
(str(StyledTextTagType.UNDERLINE), gtk.STOCK_UNDERLINE, None, None,
|
(str(StyledTextTagType.UNDERLINE), gtk.STOCK_UNDERLINE, None, None,
|
||||||
_('Underline'), self._on_toggle_action_activate),
|
_('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]
|
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>i': str(StyledTextTagType.ITALIC),
|
||||||
'<Control>b': str(StyledTextTagType.BOLD),
|
'<Control>b': str(StyledTextTagType.BOLD),
|
||||||
'<Control>u': str(StyledTextTagType.UNDERLINE),
|
'<Control>u': str(StyledTextTagType.UNDERLINE),
|
||||||
|
'<Control>z' : "Undo",
|
||||||
|
'<Control><Shift>z': "Redo",
|
||||||
}
|
}
|
||||||
|
|
||||||
# create the action group and insert all the actions
|
# create the action group and insert all the actions
|
||||||
@ -731,6 +735,12 @@ class StyledTextEditor(gtk.TextView):
|
|||||||
"""
|
"""
|
||||||
return self.toolbar
|
return self.toolbar
|
||||||
|
|
||||||
|
def undo(self, obj):
|
||||||
|
self.textbuffer.undo()
|
||||||
|
|
||||||
|
def redo(self, obj):
|
||||||
|
self.textbuffer.redo()
|
||||||
|
|
||||||
def uri_dialog(self, uri, callback):
|
def uri_dialog(self, uri, callback):
|
||||||
"""
|
"""
|
||||||
Function to spawn the link editor.
|
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…
x
Reference in New Issue
Block a user