Source code for pyqtcmd.history

#!/usr/bin/env python3

"""
Base history mechanism
"""

import collections
import contextlib
import logging

try:
    from PyQt5 import QtCore
except ModuleNotFoundError: # pragma: no cover
    from PyQt6 import QtCore


[docs]class HistoryVersion(collections.namedtuple('HistoryVersion', ['major', 'minor'])): """ History version. Internal use only. """ def __str__(self): return '.'.join(map(str, self))
[docs]class ConsistencyError(Exception): """ Thrown when something bad happens (trying to undo with an empty past list for instance). """
[docs]class History(QtCore.QObject): """ Maintains lists of past and future commands as they are done and undone. The `changed` signal is emitted whenever the history state changes. This class also maintains a pointer to a 'saved' state and can tell if its current state is the saved one or not (see is_modified()). """ changed = QtCore.pyqtSignal() logger = logging.getLogger('pyqtcmd.History') def __init__(self): self.__past = [] self.__future = [] self.__version = HistoryVersion(0, 0) self.__saved_version = self.__version super().__init__()
[docs] @contextlib.contextmanager def freeze(self): # pylint: disable=R0201 """ This context manager will wrap calls to Command.(undo|redo|do). You can override it to provide some sort of batch updating/signalling in your own project. """ yield
[docs] def check(self): """ This just emits the changed signal; use it when you think UICommand states should be updated, when add_check_signal is not enough. """ self.changed.emit()
[docs] def reset(self, is_new=True): """ Resets the history state. This is typically called when a new document has been loaded or created. If `is_new` is False, the saved state is not reset, so is_modified() will still return True. This is intended for session management, when loading a document that hasn't actually been saved yet. """ self.__version = self.__version._replace(major=self.__version.major + 1) self.__past = [] self.__future = [] if is_new: self.__saved_version = self.__version self.logger.debug('Reset to version %s (saved %s)', self.__version, self.__saved_version) self.changed.emit()
[docs] def is_modified(self): """ Returns True if the history state is different from what it was when save_point() was last called (or when constructed, or reset with is_new=False). """ return self.__version != self.__saved_version
[docs] def save_point(self): """ Sets the 'saved' state as the current one. Call this after saving a document to disk for instance. """ self.__saved_version = self.__version self.logger.debug('Saved (version is now %s/%s)', self.__version, self.__saved_version) self.changed.emit()
[docs] def run(self, command): """ Run a command and put it in the past. In order to keep things consistent, the future list is deleted. """ with self.freeze(): self.logger.debug('Will run command "%s"', command) command.do() self.logger.debug('Ran command "%s"', command) self.__past.append(command) self.__future = [] self.__version = self.__version._replace(minor=self.__version.minor + 1) self.logger.debug('Version is now %s/%s', self.__version, self.__saved_version) self.changed.emit()
[docs] def can_undo(self): """Returns True if there is at least one past command""" return bool(self.__past)
[docs] def undo_label(self): """The text for the current undo command, or None""" if self.__past: return self.__past[-1].label() return ''
[docs] def can_redo(self): """Returns True if there is at least one future command""" return bool(self.__future)
[docs] def redo_label(self): """The text for the current redo command, or None""" if self.__future: return self.__future[-1].label() return ''
[docs] def undo(self): """ Undo the latest command that was run. Throws ConsistencyError if there are no commands to undo. """ if not self.__past: raise ConsistencyError('No command to undo') cmd = self.__past.pop() with self.freeze(): self.logger.debug('Will undo "%s"', cmd) cmd.undo() self.logger.debug('Undid "%s"', cmd) self.__future.append(cmd) self.__version = self.__version._replace(minor=self.__version.minor - 1) self.logger.debug('Version is now %s/%s', self.__version, self.__saved_version) self.changed.emit()
[docs] def redo(self): """ Redo the latest command that was undone. Throws ConsistencyError if there are no command to redo. """ if not self.__future: raise ConsistencyError('No command to redo') cmd = self.__future.pop() with self.freeze(): self.logger.debug('Will redo "%s"', cmd) cmd.redo() self.logger.debug('Redid "%s"', cmd) self.__past.append(cmd) self.__version = self.__version._replace(minor=self.__version.minor + 1) self.logger.debug('Version is now %s/%s', self.__version, self.__saved_version) self.changed.emit()
[docs] def past(self): """ Return a copy of past Command objects. This may be useful in situation where, for instance, a modal dialog has a 'local' history, and you want to encapsulate everything that has been done in this dialog in a single CompositeCommand when the dialog is closed, and add it to the 'main' history. """ return self.__past[:]