forked from Mirrors/apostrophe
205 lines
7.9 KiB
Python
205 lines
7.9 KiB
Python
class UndoableInsert:
|
|
"""Something has been inserted into text_buffer"""
|
|
|
|
def __init__(self, text_iter, text, length):
|
|
self.offset = text_iter.get_offset()
|
|
self.text = text
|
|
self.length = length
|
|
self.mergeable = not bool(self.length > 1 or self.text in ("\r", "\n", " "))
|
|
|
|
|
|
class UndoableDelete:
|
|
"""Something has been deleted from text_buffer"""
|
|
|
|
def __init__(self, text_buffer, start_iter, end_iter):
|
|
self.text = text_buffer.get_text(start_iter, end_iter, False)
|
|
self.start = start_iter.get_offset()
|
|
self.end = end_iter.get_offset()
|
|
# Find out if backspace or delete were used to not mess up redo
|
|
insert_iter = text_buffer.get_iter_at_mark(text_buffer.get_insert())
|
|
self.delete_key_used = bool(insert_iter.get_offset() <= self.start)
|
|
self.mergeable = not bool(self.end - self.start > 1 or self.text in ("\r", "\n", " "))
|
|
|
|
|
|
class UndoRedoHandler:
|
|
"""Manages undo/redo for a given text_buffer.
|
|
|
|
Methods can be called directly, as well as be used as signal callbacks."""
|
|
|
|
def __init__(self):
|
|
self.undo_stack = []
|
|
self.redo_stack = []
|
|
self.not_undoable_action = False
|
|
self.undo_in_progress = False
|
|
|
|
def undo(self, text_view, _data=None):
|
|
"""Undo insertions or deletions. Undone actions are moved to redo stack.
|
|
|
|
This method can be registered to a custom undo signal, or used independently."""
|
|
|
|
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)
|
|
text_buffer = text_view.get_buffer()
|
|
if isinstance(undo_action, UndoableInsert):
|
|
offset = undo_action.offset
|
|
start = text_buffer.get_iter_at_offset(offset)
|
|
stop = text_buffer.get_iter_at_offset(
|
|
offset + undo_action.length
|
|
)
|
|
text_buffer.place_cursor(start)
|
|
text_buffer.delete(start, stop)
|
|
else:
|
|
start = text_buffer.get_iter_at_offset(undo_action.start)
|
|
text_buffer.insert(start, undo_action.text)
|
|
if undo_action.delete_key_used:
|
|
text_buffer.place_cursor(start)
|
|
else:
|
|
stop = text_buffer.get_iter_at_offset(undo_action.end)
|
|
text_buffer.place_cursor(stop)
|
|
self.__end_not_undoable_action()
|
|
self.undo_in_progress = False
|
|
|
|
def redo(self, text_view, _data=None):
|
|
"""Redo insertions or deletions. Redone actions are moved to undo stack
|
|
|
|
This method can be registered to a custom redo signal, or used independently."""
|
|
|
|
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)
|
|
text_buffer = text_view.get_buffer()
|
|
if isinstance(redo_action, UndoableInsert):
|
|
start = text_buffer.get_iter_at_offset(redo_action.offset)
|
|
text_buffer.insert(start, redo_action.text)
|
|
new_cursor_pos = text_buffer.get_iter_at_offset(
|
|
redo_action.offset + redo_action.length)
|
|
text_buffer.place_cursor(new_cursor_pos)
|
|
else:
|
|
start = text_buffer.get_iter_at_offset(redo_action.start)
|
|
stop = text_buffer.get_iter_at_offset(redo_action.end)
|
|
text_buffer.delete(start, stop)
|
|
text_buffer.place_cursor(start)
|
|
self.__end_not_undoable_action()
|
|
self.undo_in_progress = False
|
|
|
|
def clear(self):
|
|
self.undo_stack = []
|
|
self.redo_stack = []
|
|
|
|
def on_insert_text(self, _text_buffer, text_iter, text, _length):
|
|
"""Registers a text insert. Refer to TextBuffer's "insert-text" signal.
|
|
|
|
This method must be registered to TextBuffer's "insert-text" signal, or called manually."""
|
|
|
|
def can_be_merged(prev, cur):
|
|
"""Check if multiple insertions can be merged
|
|
|
|
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
|
|
if cur.offset != (prev.offset + prev.length):
|
|
return False
|
|
if cur.text in whitespace and prev.text not in whitespace:
|
|
return False
|
|
if prev.text in whitespace and cur.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 = UndoableInsert(text_iter, text, len(text))
|
|
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):
|
|
"""Registers a range deletion. Refer to TextBuffer's "delete-range" signal.
|
|
|
|
This method must be registered to TextBuffer's "delete-range" signal, or called manually."""
|
|
|
|
def can_be_merged(prev, cur):
|
|
"""Check if multiple deletions can be merged
|
|
|
|
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
|
|
if prev.delete_key_used != cur.delete_key_used:
|
|
return False
|
|
if prev.start != cur.start and prev.start != cur.end:
|
|
return False
|
|
if cur.text not in whitespace and \
|
|
prev.text in whitespace:
|
|
return False
|
|
if 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):
|
|
"""Toggle to stop recording actions"""
|
|
|
|
self.not_undoable_action = True
|
|
|
|
def __end_not_undoable_action(self):
|
|
"""Toggle to start recording actions"""
|
|
|
|
self.not_undoable_action = False
|