"""
Implementation of multiple sequence viewer undo/redo mechanism.
Copyright Schrodinger, LLC. All rights reserved.
"""
# Contributors: Piotr Rotkiewicz
import pickle
# Available state types.
UNDO_STATE_GROUP = 1
UNDO_STATE_GROUP_DEEP = 2
UNDO_STATE_SEQUENCE_EDIT = 3
# 256 MB as a total undo stack size
MAX_UNDO_STACK_SIZE = 256 * 1024 * 1024 * 1024
TOTAL_UNDO_STACK_SIZE = 0
[docs]class UndoStack:
"""
This undo implementation uses either Python copy methods, or cPickle module
to store undo states.
"""
[docs] def __init__(self):
"""
Class constructor.
"""
self.undo_stack = [] #Undo states stack
self.redo_stack = [] #Redo states stack
self.undo_action = None #Associated undo menu action.
self.redo_action = None #Associated redo menu action.
[docs] def clearRedoStack(self):
"""
Clears redo stack and disables redo action. This function is called
by storeState methods. Whenever state changes and is stored for undo,
the redo operation is not valid anymore.
"""
self.redo_stack = []
if self.redo_action:
self.redo_action.setText(self.redo_action.tr("Redo"))
self.redo_action.setEnabled(False)
[docs] def setActions(self, undo_action, redo_action):
"""
Sets Qt undo/redo actions, so that the undo/redo mechanism can change
the corresponding menu items appropriately.
:type undo_action: QAction
:param undo_action: Qt action for undo operation.
:type redo_action: QAction
:param redo_action: Qt action for redo operation.
"""
self.undo_action = undo_action
self.redo_action = redo_action
[docs] def storeStateSequence(self,
sequence_group,
index,
from_redo=False,
from_undo=False,
label=None):
"""
Stores undo state of an individual sequence.
:type sequence_group: `SequenceGroup`
:param sequence_group: sequence group
:type index: int
:parame index: sequence index in the sequence group
:type from_redo: bool
:param from_redo: set to True if calling from redo function
:type from_undo: bool
:param from_undo: set to True if calling from undo function
:type label: string
:param label: undo menu item label
"""
sequence_copy = sequence_group.sequences[index].copyForUndo()
size = 0
state = (UNDO_STATE_SEQUENCE_EDIT, (index, sequence_copy), label, size)
if from_undo:
self.redo_stack.append(state)
else:
self.undo_stack.append(state)
if self.undo_action:
self.undo_action.setEnabled(True)
if label:
self.undo_action.setText(
self.undo_action.tr("Undo " + label))
if not from_redo and not from_undo:
self.clearRedoStack()
[docs] def storeStateGroup(self,
sequence_group,
from_redo=False,
from_undo=False,
label=None):
"""
Stores undo state of entire sequence group.
:type sequence_group: `SequenceGroup`
:param sequence_group: sequence group
:type from_redo: bool
:param from_redo: set to True if calling from redo function
:type from_undo: bool
:param from_undo: set to True if calling from undo function
:type label: string
:param label: undo menu item label
"""
group_copy = sequence_group.copyForUndo()
size = 0
state = (UNDO_STATE_GROUP, group_copy, label, size)
if from_undo:
self.redo_stack.append(state)
else:
self.undo_stack.append(state)
if self.undo_action:
self.undo_action.setEnabled(True)
if label:
self.undo_action.setText(
self.undo_action.tr("Undo " + label))
if not from_redo and not from_undo:
self.clearRedoStack()
[docs] def storeStateGroupDeep(self,
sequence_group,
from_redo=False,
from_undo=False,
label=None):
"""
Stores undo state of entire sequence group and all sequences
(makes a deep copy of the sequence group).
:type sequence_group: `SequenceGroup`
:param sequence_group: sequence group
:type from_redo: bool
:param from_redo: set to True if calling from redo function
:type from_undo: bool
:param from_undo: set to True if calling from undo function
:type label: string
:param label: undo menu item label
"""
global TOTAL_UNDO_STACK_SIZE
try:
pickled_sequences = pickle.dumps(sequence_group.sequences, -1)
pickled_profile = pickle.dumps(sequence_group.profile, -1)
pickled_tree = pickle.dumps(sequence_group.tree, -1)
except:
return False
size = len(pickled_sequences) + len(pickled_profile) + len(pickled_tree)
group_copy = (pickled_sequences, pickled_profile, pickled_tree)
state = (UNDO_STATE_GROUP_DEEP, group_copy, label, size)
TOTAL_UNDO_STACK_SIZE += size
while TOTAL_UNDO_STACK_SIZE > MAX_UNDO_STACK_SIZE and self.undo_stack:
t, g, l, s = self.undo_stack[0]
TOTAL_UNDO_STACK_SIZE -= s
self.undo_stack = self.undo_stack[1:]
if from_undo:
self.redo_stack.append(state)
else:
self.undo_stack.append(state)
if self.undo_action:
self.undo_action.setEnabled(True)
if label:
self.undo_action.setText(
self.undo_action.tr("Undo " + label))
if not from_redo and not from_undo:
self.clearRedoStack()
[docs] def undo(self, viewer):
"""
This function undoes the last operation.
"""
if len(self.undo_stack) > 0:
type, data, label, size = self.undo_stack.pop()
if type == UNDO_STATE_SEQUENCE_EDIT:
index, sequence_copy = data
self.storeStateSequence(viewer.sequence_group,
index,
from_undo=True,
label=label)
viewer.sequence_group.sequences[index] = sequence_copy
elif type == UNDO_STATE_GROUP:
group_copy = data
self.storeStateGroup(viewer.sequence_group,
from_undo=True,
label=label)
if viewer.sequence_group in viewer.sequence_group_list:
index = viewer.sequence_group_list.index(
viewer.sequence_group)
viewer.sequence_group_list[index] = group_copy
viewer.sequence_group = group_copy
viewer.sequence_group.unselectAllSequences()
elif type == UNDO_STATE_GROUP_DEEP:
pickled_sequences, pickled_profile, pickled_tree = data
self.storeStateGroupDeep(viewer.sequence_group,
from_undo=True,
label=label)
viewer.sequence_group.sequences = pickle.loads(
pickled_sequences)
viewer.sequence_group.profile = pickle.loads(pickled_profile)
viewer.sequence_group.tree = pickle.loads(pickled_tree)
viewer.sequence_group.unselectAllSequences()
self.updateActions()
return (len(self.undo_stack) > 0, len(self.redo_stack) > 0)
[docs] def redo(self, viewer):
"""
This function redoes the operation that was undone.
"""
if len(self.redo_stack) > 0:
state = self.redo_stack.pop()
type, data, label, size = state
if type == UNDO_STATE_SEQUENCE_EDIT:
index, sequence_copy = data
self.storeStateSequence(viewer.sequence_group,
index,
from_redo=True,
label=label)
viewer.sequence_group.sequences[index] = sequence_copy
elif type == UNDO_STATE_GROUP:
group_copy = data
self.storeStateGroup(viewer.sequence_group,
from_redo=True,
label=label)
if viewer.sequence_group in viewer.sequence_group_list:
index = viewer.sequence_group_list.index(
viewer.sequence_group)
viewer.sequence_group_list[index] = group_copy
viewer.sequence_group = group_copy
elif type == UNDO_STATE_GROUP_DEEP:
pickled_sequences, pickled_profile, pickled_tree = data
self.storeStateGroupDeep(viewer.sequence_group,
from_redo=True,
label=label)
viewer.sequence_group.sequences = pickle.loads(
pickled_sequences)
viewer.sequence_group.profile = pickle.loads(pickled_profile)
viewer.sequence_group.tree = pickle.loads(pickled_tree)
self.updateActions()
return (len(self.undo_stack) > 0, len(self.redo_stack) > 0)
[docs] def updateActions(self):
"""
Updates Qt actions text and enabled state based on undo/redo
stacks contents.
"""
if self.undo_action:
if len(self.undo_stack) > 0:
self.undo_action.setEnabled(True)
type, data, label, size = self.undo_stack[-1]
if label:
self.undo_action.setText(
self.undo_action.tr("Undo " + label))
else:
self.undo_action.setEnabled(False)
self.undo_action.setText("Undo")
if self.redo_action:
if len(self.redo_stack) > 0:
self.redo_action.setEnabled(True)
type, data, label, size = self.redo_stack[-1]
if label:
self.redo_action.setText(
self.redo_action.tr("Redo " + label))
else:
self.redo_action.setEnabled(False)
self.redo_action.setText("Redo")