You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
149 lines
6.2 KiB
149 lines
6.2 KiB
#! /usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Copyright 2020, Nils Hilbricht, Germany ( https://www.hilbricht.net )
|
|
|
|
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),
|
|
|
|
This application is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
"""
|
|
|
|
import logging; logger = logging.getLogger(__name__); logger.info("import")
|
|
|
|
|
|
from contextlib import contextmanager
|
|
|
|
class History(object):
|
|
"""Keep track of changes in the session. From file load to quit.
|
|
|
|
All functions that need undo and redo follow the same idiom:
|
|
|
|
A fuzzy and vague user function starts the process. It may depend
|
|
on the context, like the cursor pitch or the current key signature.
|
|
|
|
This function itself, e.g. Note.sharpen, saves its exact old values
|
|
and instances and returns a lambda: undo function with these exact
|
|
values already inserted.
|
|
|
|
The undo function must be able to undo itself.
|
|
Redo is undo for undo, it uses the same system. Each undo function
|
|
needs to register an undo itself.
|
|
|
|
In other words: The user function, like "sharpen", is only called
|
|
once. After that an internal setter function, without context and
|
|
state, handles undo and redo by using exact values on the exact
|
|
instances (for example note instances).
|
|
|
|
We still set and reset the cursor position in undo, but that doesn't
|
|
mean getting the right context or track-state back.
|
|
It is only to enable the user to edit from the same place again
|
|
and also to find the right track to instruct the callbacks to update.
|
|
|
|
|
|
The history does not know, and subsequently does not differentiate,
|
|
between apply-to-selection or single-item commands. Each history
|
|
function does a set of operations. The history itself is agnostic
|
|
about them.
|
|
"""
|
|
def __init__(self):
|
|
self.commandStack = [] # elements are (description, listOfFunctions). listOfFunctions is a real python list.
|
|
self.redoStack = [] #same as self.commandStack
|
|
self.doNotRegisterRightNow = False #prevents self.undo from going into an infinite loop. For example if you undo api.delete it will register insertItem again (which has a register itself with delete as undo function, which will...). There is only one history in the session so this attribute is shared by all undo functions.
|
|
self.duringRedo = False
|
|
|
|
self.apiCallback_historySequenceStarted = None
|
|
self.apiCallback_historySequenceStopped = None
|
|
|
|
@contextmanager
|
|
def sequence(self, descriptionString):
|
|
"""Convenience function. All registrations from now on will be undone at once.
|
|
call stopSequence when done.
|
|
This is not meant for high level scripts and not for user-interaction.
|
|
|
|
This context can also be used to force a custom name for the undo description, even if it is
|
|
only one step.
|
|
"""
|
|
|
|
self.apiCallback_historySequenceStarted()
|
|
|
|
|
|
def sequenceRegister(registeredUndoFunction, descriptionString, listOfFunctions):
|
|
"""Modifies a list in place"""
|
|
listOfFunctions.append(registeredUndoFunction)
|
|
|
|
listOfFunctions = []
|
|
originalRegister = self.register
|
|
self.register = lambda registeredUndoFunction, descriptionString, l=listOfFunctions: sequenceRegister(registeredUndoFunction, descriptionString, l)
|
|
|
|
yield
|
|
|
|
self._register(descriptionString, listOfFunctions)
|
|
self.register = originalRegister
|
|
self.apiCallback_historySequenceStopped()
|
|
|
|
|
|
def register(self, registeredUndoFunction, descriptionString):
|
|
"""Register a single undo function but use the same syntax as many functions"""
|
|
self._register(descriptionString, [registeredUndoFunction,])
|
|
|
|
def _register(self, descriptionString, listOfFunctions):
|
|
assert type(listOfFunctions) is list, (type(listOfFunctions), listOfFunctions)
|
|
if self.doNotRegisterRightNow:
|
|
self.redoStack.append((descriptionString, listOfFunctions))
|
|
else:
|
|
self.commandStack.append((descriptionString, listOfFunctions))
|
|
if not self.duringRedo:
|
|
self.redoStack = [] #no redo after new commands.
|
|
|
|
def clear(self):
|
|
"""User-scripts which do not implement their own undo must
|
|
call this to not disturb the history. Better no history
|
|
than a wrong history."""
|
|
self.commandStack = []
|
|
self.redoStack = []
|
|
|
|
def undo(self):
|
|
if self.commandStack:
|
|
self.doNotRegisterRightNow = True
|
|
|
|
descriptionString, listOfFunctions = self.commandStack.pop()
|
|
|
|
with self.sequence(descriptionString):
|
|
for registeredUndoFunction in reversed(listOfFunctions):
|
|
registeredUndoFunction()
|
|
|
|
self.doNotRegisterRightNow = False
|
|
#print ("pop", len(self.commandStack))
|
|
|
|
def redo(self):
|
|
if self.redoStack:
|
|
self.duringRedo = True
|
|
|
|
descriptionString, listOfFunctions = self.redoStack.pop()
|
|
with self.sequence(descriptionString):
|
|
for registeredUndoFunction in reversed(listOfFunctions):
|
|
registeredUndoFunction()
|
|
|
|
self.duringRedo = False
|
|
|
|
def asList(self):
|
|
return [descr for descr, listOfFunctions in self.commandStack], [descr for descr, listOfFunctions in self.redoStack]
|
|
|
|
|
|
def setterWithUndo(self, object, attribute, value, descriptionString, callback):
|
|
oldValue = getattr(object, attribute)
|
|
setattr(object, attribute, value)
|
|
undoFunction = lambda oldValue=oldValue: self.setterWithUndo(object, attribute, oldValue, descriptionString, callback)
|
|
self.register(undoFunction, descriptionString = descriptionString)
|
|
callback()
|
|
|