Another implementation of undo/redo

This commit is contained in:
2025-07-31 22:54:09 +02:00
parent 72f5f30da6
commit 37c91d0d5d
13 changed files with 228 additions and 94 deletions

View File

@@ -1,30 +1,24 @@
import logging
from abc import ABC, abstractmethod
from dataclasses import dataclass
from fasthtml.components import *
from components.BaseComponent import BaseComponentSingleton
from components.undo_redo.assets.icons import icon_redo, icon_undo
from components.undo_redo.commands import UndoRedoCommandManager
from components.undo_redo.constants import UNDO_REDO_INSTANCE_ID
from components.undo_redo.constants import UNDO_REDO_INSTANCE_ID, UndoRedoAttrs
from components_helpers import mk_icon, mk_tooltip
logger = logging.getLogger("UndoRedoApp")
class CommandHistory(ABC):
def __init__(self, name, desc, owner):
self.name = name
self.desc = desc
self.owner = owner
@abstractmethod
def undo(self):
pass
@abstractmethod
def redo(self):
pass
@dataclass
class CommandHistory:
attrs: UndoRedoAttrs
digest: str | None # digest to remember
entry: str # digest to remember
key: str # key
path: str # path within the key if only on subitem needs to be updated
class UndoRedo(BaseComponentSingleton):
@@ -35,35 +29,73 @@ class UndoRedo(BaseComponentSingleton):
self.index = -1
self.history = []
self._commands = UndoRedoCommandManager(self)
self._db_engine = settings_manager.get_db_engine()
def push(self, command: CommandHistory):
self.history = self.history[:self.index + 1]
def snapshot(self, undo_redo_attrs: UndoRedoAttrs, entry, key, path=None):
digest = self._settings_manager.get_digest(self._session, entry) # get the current digest (the last one)
# init the history if this is the first call
if len(self.history) == 0:
digest_history = self._settings_manager.history(self._session, entry, digest, 2)
command = CommandHistory(undo_redo_attrs,
digest_history[1] if len(digest_history) > 1 else None,
entry,
key,
path)
self.history.append(command)
self.index = 0
command = CommandHistory(undo_redo_attrs, digest, entry, key, path)
self.history = self.history[:self.index + 1] #
self.history.append(command)
self.index += 1
self.index = len(self.history) - 1
def undo(self):
logger.debug(f"Undo command")
if self.index < 0 :
if self.index < 1:
logger.debug(f" No command to undo.")
return self
command = self.history[self.index]
logger.debug(f" Undoing command {command.name} ({command.desc})")
res = command.undo()
current = self.history[self.index]
current_state = self._settings_manager.load(self._session, None, digest=current.digest)
previous = self.history[self.index - 1]
previous_state = self._settings_manager.load(self._session, None, digest=previous.digest)
# reapply the state
current_state[current.key] = previous_state[current.key]
self._settings_manager.save(self._session, current.entry, current_state)
self.index -= 1
return self, res
if current.attrs.on_undo is not None:
return self, current.attrs.on_undo()
else:
return self
def redo(self):
logger.debug("Redo command")
if self.index == len(self.history) - 1:
logger.debug(f" No command to redo.")
logger.debug(f"Redo command")
if self.index >= len(self.history) - 1:
logger.debug(f" No command to undo.")
return self
current = self.history[self.index]
current_state = self._settings_manager.load(self._session, None, digest=current.digest)
next_ = self.history[self.index + 1]
next_state = self._settings_manager.load(self._session, None, digest=next_.digest)
# reapply the state
current_state[current.key] = next_state[current.key]
self._settings_manager.save(self._session, current.entry, current_state)
self.index += 1
command = self.history[self.index]
logger.debug(f" Redoing command {command.name} ({command.desc})")
res = command.redo()
return self, res
if current.attrs.on_undo is not None:
return self, current.attrs.on_redo()
else:
return self
def refresh(self):
return self.__ft__(oob=True)
@@ -83,7 +115,7 @@ class UndoRedo(BaseComponentSingleton):
return mk_tooltip(mk_icon(icon_undo,
size=24,
**self._commands.undo()),
f"Undo '{command.name}'.")
f"Undo '{command.attrs.name}'.")
else:
return mk_tooltip(mk_icon(icon_undo,
size=24,
@@ -97,7 +129,7 @@ class UndoRedo(BaseComponentSingleton):
return mk_tooltip(mk_icon(icon_redo,
size=24,
**self._commands.redo()),
f"Redo '{command.name}'.")
f"Redo '{command.attrs.name}'.")
else:
return mk_tooltip(mk_icon(icon_redo,
size=24,
@@ -106,7 +138,7 @@ class UndoRedo(BaseComponentSingleton):
"Nothing to redo.")
def _can_undo(self):
return self.index >= 0
return self.index >= 1
def _can_redo(self):
return self.index < len(self.history) - 1

View File

@@ -1,3 +1,6 @@
from dataclasses import dataclass
from typing import Callable
UNDO_REDO_INSTANCE_ID = "__UndoRedo__"
ROUTE_ROOT = "/undo"
@@ -5,4 +8,16 @@ ROUTE_ROOT = "/undo"
class Routes:
Undo = "/undo"
Redo = "/redo"
Redo = "/redo"
@dataclass
class UndoRedoAttrs:
name: str
desc: str = None
on_undo: Callable = None
on_redo: Callable = None
def __post_init__(self):
if self.on_redo is None:
self.on_redo = self.on_undo