Skip to content

Commit

Permalink
🪲 Fix history lost by manual history creation
Browse files Browse the repository at this point in the history
  • Loading branch information
FabienRoger committed Jan 8, 2022
1 parent 4b592c6 commit ebd26aa
Show file tree
Hide file tree
Showing 5 changed files with 203 additions and 45 deletions.
68 changes: 68 additions & 0 deletions pyflow/core/history.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Pyflow an open-source tool for modular visual programing in python
# Copyright (C) 2021-2022 Bycelium <https://www.gnu.org/licenses/>

""" Module for the handling an history of operations. """

from typing import Any, List


class History:
"""Helper object to handle undo/redo operations.
Args:
max_stack: Maximum size of the history stack (number of available undo).
"""

def __init__(self, max_stack: int = 50):
self.history_stack: List[Any] = []
self.current: int = -1
self.max_stack: int = max_stack

def undo(self) -> None:
"""Undo the last action by moving the current stamp backward and restoring."""
if len(self.history_stack) > 0 and self.current > 0:
self.current -= 1
self.restore()

def redo(self) -> None:
"""Redo the last undone action by moving the current stamp forward and restoring."""
if len(self.history_stack) > 0 and self.current + 1 < len(self.history_stack):
self.current += 1
self.restore()

def store(self, data: Any) -> None:
"""Store new data in the history stack, updating current checkpoint.
Remove data that would be forward in the history stack.
Args:
data: Data to store in the history stack.
"""
if self.current + 1 < len(self.history_stack):
self.history_stack = self.history_stack[0 : self.current + 1]

self.history_stack.append(data)

if len(self.history_stack) > self.max_stack:
self.history_stack.pop(0)

self.current = min(self.current + 1, len(self.history_stack) - 1)

def restored_data(self) -> Any:
"""
Return the snapshot pointed by current in the history stack.
Return None if there is nothing pointed to.
"""
if len(self.history_stack) >= 0 and self.current >= 0:
data = self.history_stack[self.current]
return data
return None

def restore(self) -> None:
"""
Empty function to be overriden
Contains the behavior to be adopted when a state is restored
"""
# data: Any = self.restored_data()
return
6 changes: 6 additions & 0 deletions pyflow/core/pyeditor/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Pyflow an open-source tool for modular visual programing in python
# Copyright (C) 2021-2022 Bycelium <https://www.gnu.org/licenses/>

""" Module for the Python Editor. """

from pyflow.core.pyeditor.pyeditor import PythonEditor, POINT_SIZE
75 changes: 75 additions & 0 deletions pyflow/core/pyeditor/history.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Pyflow an open-source tool for modular visual programing in python
# Copyright (C) 2021-2022 Bycelium <https://www.gnu.org/licenses/>

""" Module for the handling a PythonEditor history. """

from typing import TYPE_CHECKING, Optional, OrderedDict, Tuple

from pyflow.core.history import History

if TYPE_CHECKING:
from pyflow.core.pyeditor import PythonEditor


class EditorHistory(History):
"""
Helper object to handle undo/redo operations on a PythonEditor.
Args:
editor: PythonEditor reference.
max_stack: Maximum size of the history stack (number of available undo).
"""

def __init__(self, editor: "PythonEditor", max_stack: int = 50):
self.editor = editor
self.is_writing = False
super().__init__(max_stack)

def start_sequence(self):
"""
Start a new writing sequence if it was not already the case, and save the current state.
"""
if not self.is_writing:
self.is_writing = True
self.checkpoint()

def end_sequence(self):
"""
End the writing sequence if it was not already the case.
Do not save at this point because the writing parameters to be saved (cursor pos, etc)
are the one of the beginning of the next sequence.
"""
self.is_writing = False

def checkpoint(self):
"""
Store a snapshot of the editor's text and parameters in the history stack
(only if the text has changed).
"""
text: str = self.editor.text()
old_data = self.restored_data()
if old_data is not None and old_data["text"] == text:
return

cursor_pos: Tuple[int, int] = self.editor.getCursorPosition()
scroll_pos: int = self.editor.verticalScrollBar().value()
self.store(
{
"text": text,
"cursor_pos": cursor_pos,
"scroll_pos": scroll_pos,
}
)

def restore(self):
"""
Restore the editor's text and parameters
using the snapshot pointed by current in the history stack.
"""
data: Optional[OrderedDict] = self.restored_data()

if data is not None:
self.editor.setText(data["text"])
self.editor.setCursorPosition(*data["cursor_pos"])
self.editor.verticalScrollBar().setValue(data["scroll_pos"])
42 changes: 38 additions & 4 deletions pyflow/core/pyeditor.py → pyflow/core/pyeditor/pyeditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
QFont,
QFontMetrics,
QColor,
QKeyEvent,
QMouseEvent,
QWheelEvent,
)
from PyQt5.Qsci import QsciScintilla, QsciLexerPython
from pyflow.core.pyeditor.history import EditorHistory
from pyflow.graphics.theme_manager import theme_manager

from pyflow.blocks.block import Block
Expand All @@ -39,6 +41,10 @@ def __init__(self, block: Block):
self._mode = "NOOP"
self.block = block

self.history = EditorHistory(self)
self.pressingControl = False
# self.startOfSequencePos

self.update_theme()
theme_manager().themeChanged.connect(self.update_theme)

Expand Down Expand Up @@ -67,7 +73,7 @@ def __init__(self, block: Block):
self.setWindowFlags(Qt.WindowType.FramelessWindowHint)

def update_theme(self):
"""Change the font and colors of the editor to match the current theme."""
"""Change the font and colors of the editor to match the current theme"""
font = QFont()
font.setFamily(theme_manager().recommended_font_family)
font.setFixedPitch(True)
Expand All @@ -94,14 +100,14 @@ def views(self) -> List["View"]:
return self.block.scene().views()

def wheelEvent(self, event: QWheelEvent) -> None:
"""How PythonEditor handles wheel events."""
"""How PythonEditor handles wheel events"""
if self.mode == "EDITING" and event.angleDelta().x() == 0:
event.accept()
return super().wheelEvent(event)

@property
def mode(self) -> int:
"""PythonEditor current mode."""
"""PythonEditor current mode"""
return self._mode

@mode.setter
Expand All @@ -114,10 +120,38 @@ def mousePressEvent(self, event: QMouseEvent) -> None:
"""PythonEditor reaction to PyQt mousePressEvent events."""
if event.buttons() & Qt.MouseButton.LeftButton:
self.mode = "EDITING"
return super().mousePressEvent(event)

super().mousePressEvent(event)
self.history.end_sequence()

def focusOutEvent(self, event: QFocusEvent):
"""PythonEditor reaction to PyQt focusOut events."""
self.history.end_sequence()
self.mode = "NOOP"
self.block.source = self.text()
return super().focusOutEvent(event)

def keyPressEvent(self, e: QKeyEvent) -> None:
"""PythonEditor reaction to PyQt keyPressed events."""

# Disable QsciScintilla undo
self.SendScintilla(QsciScintilla.SCI_EMPTYUNDOBUFFER, 1)

# Manualy check if Ctrl+Z or Ctrl+Y is pressed
if self.pressingControl and e.key() == Qt.Key.Key_Z:
# The sequence ends and a new one starts when pressing Ctrl+Z
self.history.end_sequence()
self.history.start_sequence()
self.history.undo()
elif self.pressingControl and e.key() == Qt.Key.Key_Y:
self.history.redo()
elif e.key() == Qt.Key.Key_Control:
self.pressingControl = True
else:
self.pressingControl = False
self.history.start_sequence()

if e.key() in {Qt.Key.Key_Return, Qt.Key.Key_Enter}:
self.history.end_sequence()

super().keyPressEvent(e)
57 changes: 16 additions & 41 deletions pyflow/scene/history.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
# Pyflow an open-source tool for modular visual programing in python
# Copyright (C) 2021-2022 Bycelium <https://www.gnu.org/licenses/>
# OpenCodeBlock an open-source tool for modular visual programing in python
# Copyright (C) 2021 Mathïs FEDERICO <https://www.gnu.org/licenses/>

""" Module for the handling a scene history. """
""" Module for the handling an OCBScene history. """

from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING

from pyflow.core.history import History

if TYPE_CHECKING:
from pyflow.scene import Scene


class SceneHistory:
class SceneHistory(History):
"""Helper object to handle undo/redo operations on an Scene.
Args:
Expand All @@ -20,21 +22,7 @@ class SceneHistory:

def __init__(self, scene: "Scene", max_stack: int = 50):
self.scene = scene
self.history_stack = []
self.current = -1
self.max_stack = max_stack

def undo(self):
"""Undo the last action by moving the current stamp backward and restoring."""
if len(self.history_stack) > 0 and self.current > 0:
self.current -= 1
self.restore()

def redo(self):
"""Redo the last undone action by moving the current stamp forward and restoring."""
if len(self.history_stack) > 0 and self.current + 1 < len(self.history_stack):
self.current += 1
self.restore()
super().__init__(max_stack)

def checkpoint(self, description: str, set_modified=True):
"""Store a snapshot of the scene in the history stack.
Expand All @@ -44,32 +32,19 @@ def checkpoint(self, description: str, set_modified=True):
set_modified: Whether the scene should be considered modified.
"""
history_stamp = {"description": description, "snapshot": self.scene.serialize()}
history_stamp = {
"description": description,
"snapshot": self.scene.serialize(),
}
self.store(history_stamp)
if set_modified:
self.scene.has_been_modified = True

def store(self, data: Any):
"""Store new data in the history stack, updating current checkpoint.
Remove data that would be forward in the history stack.
Args:
data: Data to store in the history stack.
"""
if self.current + 1 < len(self.history_stack):
self.history_stack = self.history_stack[0 : self.current + 1]

self.history_stack.append(data)

if len(self.history_stack) > self.max_stack:
self.history_stack.pop(0)

self.current = min(self.current + 1, len(self.history_stack) - 1)

def restore(self):
"""Restore the scene using the snapshot pointed by current in the history stack."""
if len(self.history_stack) >= 0 and self.current >= 0:
stamp = self.history_stack[self.current]

stamp = self.restored_data()

if stamp is not None:
snapshot = stamp["snapshot"]
self.scene.deserialize(snapshot)

0 comments on commit ebd26aa

Please sign in to comment.