Changeset 0e1796d258f2…
Parent d66e6d23b735…
by
Changes to 2 files · Browse files at 0e1796d258f2 Showing diff from parent d66e6d23b735 Diff from another changeset...
@@ -31,6 +31,8 @@ except ImportError:
config_nofork = None
+from tortoisehg.hgtk import textview
+
try:
import win32con
openflags = win32con.CREATE_NO_WINDOW
@@ -56,6 +58,9 @@for sig in ('thg-close', 'thg-new'):
gobject.signal_new(sig, gtk.Notebook,
gobject.SIGNAL_ACTION, gobject.TYPE_NONE, ())
+for sig in ('thg-undo', 'thg-redo'):
+ gobject.signal_new(sig, textview.UndoableTextView,
+ gobject.SIGNAL_ACTION, gobject.TYPE_NONE, ())
gtkmainalive = False
def dispatch(args):
|
|
|
@@ -0,0 +1,249 @@ + # textview.py - TextView/TextBuffer with undo/redo functionality
+#
+# Copyright 2009 Florian Heinle
+# Copyright 2010 Yuki KODAMA <endflow.net@gmail.com>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2, incorporated herein by reference.
+
+import gtk
+
+from tortoisehg.hgtk import gtklib
+
+class UndoableInsert(object):
+ """something that has been inserted into our textbuffer"""
+ def __init__(self, text_iter, text, length):
+ self.offset = text_iter.get_offset()
+ self.text = 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):
+ self.text = 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 UndoableTextBuffer(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 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
+
+class UndoableTextView(gtk.TextView):
+ def __init__(self, buffer=None, accelgroup=None):
+ if buffer is None:
+ buffer = UndoableTextBuffer()
+ gtk.TextView.__init__(self, buffer)
+
+ if accelgroup:
+ mod = gtklib.get_thg_modifier()
+ key, modifier = gtk.accelerator_parse(mod+'z')
+ self.add_accelerator('thg-undo', accelgroup, key,
+ modifier, gtk.ACCEL_VISIBLE)
+ def do_undo(view):
+ buffer = self.get_buffer()
+ if hasattr(buffer, 'undo'):
+ buffer.undo()
+ self.connect('thg-undo', do_undo)
+
+ key, modifier = gtk.accelerator_parse(mod+'y')
+ self.add_accelerator('thg-redo', accelgroup, key,
+ modifier, gtk.ACCEL_VISIBLE)
+ def do_redo(view):
+ buffer = self.get_buffer()
+ if hasattr(buffer, 'redo'):
+ buffer.redo()
+ self.connect('thg-redo', do_redo)
|
Loading...