# -*- coding: utf-8 -*-
"""
A module containing all code for working with History (Undo/Redo)
"""
from node_editor.utils import dumpException
DEBUG = False
DEBUG_SELECTION = False
[docs]class SceneHistory():
"""Class contains all the code for undo/redo operations"""
def __init__(self, scene: 'Scene'):
"""
:param scene: Reference to the :class:`~node_editor.node_scene.Scene`
:type scene: :class:`~nodeeditor.node_scene.Scene`
:Instance Attributes:
- **scene** - reference to the :class:`~node_editor.node_scene.Scene`
- **history_limit** - number of history steps that can be stored
"""
self.scene = scene
self.clear()
self.history_limit = 32
self.undo_selection_has_changed = False
# listeners
self._history_modified_listeners = []
self._history_stored_listeners = []
self._history_restored_listeners = []
[docs] def clear(self):
"""Reset the history stack"""
self.history_stack = []
self.history_current_step = -1
[docs] def storeInitialHistoryStamp(self):
"""Helper function usually used when new or open file requested"""
self.storeHistory("Initial History Stamp")
[docs] def addHistoryModifiedListener(self, callback: 'function'):
"""
Register callback for `HistoryModified` event
:param callback: callback function
"""
self._history_modified_listeners.append(callback)
[docs] def addHistoryStoredListener(self, callback: 'function'):
"""
Register callback for `HistoryStored` event
:param callback: callback function
"""
self._history_stored_listeners.append(callback)
[docs] def addHistoryRestoredListener(self, callback: 'function'):
"""
Register callback for `HistoryRestored` event
:param callback: callback function
"""
self._history_restored_listeners.append(callback)
[docs] def canUndo(self) -> bool:
"""Return ``True`` if Undo is available for current `History Stack`
:rtype: ``bool``
"""
return self.history_current_step > 0
[docs] def canRedo(self) -> bool:
"""
Return ``True`` if Redo is available for current `History Stack`
:rtype: ``bool``
"""
return self.history_current_step + 1 < len(self.history_stack)
[docs] def undo(self):
"""Undo operation"""
if DEBUG: print("UNDO")
if self.canUndo():
self.history_current_step -= 1
self.restoreHistory()
self.scene.has_been_modified = True
[docs] def redo(self):
"""Redo operation"""
if DEBUG: print("REDO")
if self.canRedo():
self.history_current_step += 1
self.restoreHistory()
self.scene.has_been_modified = True
[docs] def restoreHistory(self):
"""
Restore `History Stamp` from `History stack`.
Triggers:
- `History Modified` event
- `History Restored` event
"""
if DEBUG: print("Restoring history",
".... current_step: @%d" % self.history_current_step,
"(%d)" % len(self.history_stack))
self.restoreHistoryStamp(self.history_stack[self.history_current_step])
for callback in self._history_modified_listeners: callback()
for callback in self._history_restored_listeners: callback()
[docs] def storeHistory(self, desc: str, setModified: bool=False):
"""
Store History Stamp into History Stack
:param desc: Description of current History Stamp
:type desc: ``str``
:param setModified: if ``True`` marks :class:`~node_editor.node_scene.Scene` with `has_been_modified`
:type setModified: ``bool``
Triggers:
- `History Modified`
- `History Stored`
"""
if setModified:
self.scene.has_been_modified = True
if DEBUG: print("Storing history", '"%s"' % desc,
".... current_step: @%d" % self.history_current_step,
"(%d)" % len(self.history_stack))
# if the pointer (history_current_step) is not at the end of history_stack
if self.history_current_step+1 < len(self.history_stack):
self.history_stack = self.history_stack[0:self.history_current_step+1]
# history is outside of the limits
if self.history_current_step+1 >= self.history_limit:
self.history_stack = self.history_stack[1:]
self.history_current_step -= 1
hs = self.createHistoryStamp(desc)
self.history_stack.append(hs)
self.history_current_step += 1
if DEBUG: print(" -- setting step to:", self.history_current_step)
# always trigger history modified (for i.e. updateEditMenu)
for callback in self._history_modified_listeners: callback()
for callback in self._history_stored_listeners: callback()
[docs] def captureCurrentSelection(self) -> dict:
"""
Create dictionary with a list of selected nodes and a list of selected edges
:return: ``dict`` 'nodes' - list of selected nodes, 'edges' - list of selected edges
:rtype: ``dict``
"""
sel_obj = {
'nodes': [],
'edges': [],
}
for item in self.scene.grScene.selectedItems():
if hasattr(item, 'node'): sel_obj['nodes'].append(item.node.id)
elif hasattr(item, 'edge'): sel_obj['edges'].append(item.edge.id)
return sel_obj
[docs] def createHistoryStamp(self, desc: str) -> dict:
"""
Create History Stamp. Internally serialize whole scene and the current selection
:param desc: Descriptive label for the History Stamp
:return: History stamp serializing state of `Scene` and current selection
:rtype: ``dict``
"""
history_stamp = {
'desc': desc,
'snapshot': self.scene.serialize(),
'selection': self.captureCurrentSelection(),
}
return history_stamp
[docs] def restoreHistoryStamp(self, history_stamp: dict):
"""
Restore History Stamp to current `Scene` with selection of items included
:param history_stamp: History Stamp to restore
:type history_stamp: ``dict``
"""
if DEBUG: print("RHS: ", history_stamp['desc'])
try:
self.undo_selection_has_changed = False
previous_selection = self.captureCurrentSelection()
if DEBUG_SELECTION: print("selected nodes before restore:", previous_selection['nodes'])
self.scene.deserialize(history_stamp['snapshot'])
# restore selection
# first clear all selection on edges
for edge in self.scene.edges: edge.grEdge.setSelected(False)
# now restore selected edges from history_stamp
for edge_id in history_stamp['selection']['edges']:
for edge in self.scene.edges:
if edge.id == edge_id:
edge.grEdge.setSelected(True)
break
# first clear all selection on nodes
for node in self.scene.nodes: node.grNode.setSelected(False)
# now restore selected nodes from history_stamp
for node_id in history_stamp['selection']['nodes']:
for node in self.scene.nodes:
if node.id == node_id:
node.grNode.setSelected(True)
break
current_selection = self.captureCurrentSelection()
if DEBUG_SELECTION: print("selected nodes after restore:", current_selection['nodes'])
# reset the last_selected_items - since we're comparing change to the last_selected state
self.scene._last_selected_items = self.scene.getSelectedItems()
# if the selection of nodes differ before and after restoration, set flag
if current_selection['nodes'] != previous_selection['nodes'] or current_selection['edges'] != previous_selection['edges']:
if DEBUG_SELECTION: print("\nSCENE: Selection has changed")
self.undo_selection_has_changed = True
except Exception as e: dumpException(e)