Source code for schrodinger.ui.sequencealignment.undo

"""
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")