diff --git a/ChangeLog.md b/ChangeLog.md index 9108e63..4e5c2de 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -12,6 +12,8 @@ the zoom level. ([#14](https://github.com/davep/complexitty/issues/14)) - Added `CopyCommandLineToClipboard`. ([#20](https://github.com/davep/complexitty/pull/20)) +- Added the ability to undo a change in the plot. + ([#21](https://github.com/davep/complexitty/pull/21)) ## v0.2.0 diff --git a/pyproject.toml b/pyproject.toml index 7e6a580..8976f90 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ authors = [ ] dependencies = [ "textual>=3.1.0", - "textual-enhanced>=0.11.0", + "textual-enhanced>=0.13.0", "textual-canvas>=0.3.0", "xdg-base-dirs>=6.0.2", ] diff --git a/requirements-dev.lock b/requirements-dev.lock index fa8a4e5..2835ec9 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -12,7 +12,7 @@ -e file:. aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.11.16 +aiohttp==3.11.18 # via aiohttp-jinja2 # via textual-dev # via textual-serve @@ -42,12 +42,12 @@ distlib==0.3.9 # via virtualenv filelock==3.18.0 # via virtualenv -frozenlist==1.5.0 +frozenlist==1.6.0 # via aiohttp # via aiosignal ghp-import==2.1.0 # via mkdocs -identify==2.6.9 +identify==2.6.10 # via pre-commit idna==3.10 # via requests @@ -91,11 +91,11 @@ multidict==6.4.3 # via aiohttp # via yarl mypy==1.15.0 -mypy-extensions==1.0.0 +mypy-extensions==1.1.0 # via mypy nodeenv==1.9.1 # via pre-commit -packaging==24.2 +packaging==25.0 # via mkdocs paginate==0.5.7 # via mkdocs-material @@ -132,7 +132,7 @@ rich==14.0.0 # via textual-serve six==1.17.0 # via python-dateutil -textual==3.1.0 +textual==3.1.1 # via complexitty # via textual-canvas # via textual-dev @@ -141,7 +141,7 @@ textual==3.1.0 textual-canvas==0.4.0 # via complexitty textual-dev==1.7.0 -textual-enhanced==0.12.0 +textual-enhanced==0.13.0 # via complexitty textual-serve==1.1.2 # via textual-dev diff --git a/requirements.lock b/requirements.lock index b688cdd..692fa78 100644 --- a/requirements.lock +++ b/requirements.lock @@ -26,13 +26,13 @@ pygments==2.19.1 # via rich rich==14.0.0 # via textual -textual==3.1.0 +textual==3.1.1 # via complexitty # via textual-canvas # via textual-enhanced textual-canvas==0.4.0 # via complexitty -textual-enhanced==0.12.0 +textual-enhanced==0.13.0 # via complexitty typing-extensions==4.13.2 # via textual diff --git a/src/complexitty/commands/__init__.py b/src/complexitty/commands/__init__.py index ae3f5b4..7feafb3 100644 --- a/src/complexitty/commands/__init__.py +++ b/src/complexitty/commands/__init__.py @@ -11,7 +11,7 @@ SetColourToShadesOfGreen, SetColourToShadesOfRed, ) -from .main import CopyCommandLineToClipboard, Quit +from .main import CopyCommandLineToClipboard, Quit, Undo from .navigation import ( GoMiddle, GoTo, @@ -68,6 +68,7 @@ "SetColourToShadesOfBlue", "SetColourToShadesOfGreen", "SetColourToShadesOfRed", + "Undo", "ZeroZero", "ZoomIn", "ZoomInFaster", diff --git a/src/complexitty/commands/main.py b/src/complexitty/commands/main.py index a022354..abe490b 100644 --- a/src/complexitty/commands/main.py +++ b/src/complexitty/commands/main.py @@ -21,4 +21,11 @@ class CopyCommandLineToClipboard(Command): BINDING_KEY = "c" +############################################################################## +class Undo(Command): + """Undo the latest change""" + + BINDING_KEY = "backspace" + + ### main.py ends here diff --git a/src/complexitty/providers/main.py b/src/complexitty/providers/main.py index 2153a86..3f45082 100644 --- a/src/complexitty/providers/main.py +++ b/src/complexitty/providers/main.py @@ -38,6 +38,7 @@ SetColourToShadesOfBlue, SetColourToShadesOfGreen, SetColourToShadesOfRed, + Undo, ZeroZero, ZoomIn, ZoomInFaster, @@ -84,6 +85,7 @@ def commands(self) -> CommandHits: yield SetColourToShadesOfBlue() yield SetColourToShadesOfGreen() yield SetColourToShadesOfRed() + yield Undo() yield ZeroZero() yield ZoomIn() yield ZoomInFaster() diff --git a/src/complexitty/screens/main.py b/src/complexitty/screens/main.py index cbaab4f..9d72a87 100644 --- a/src/complexitty/screens/main.py +++ b/src/complexitty/screens/main.py @@ -5,7 +5,7 @@ from argparse import Namespace from math import floor, log10 from re import Pattern, compile -from typing import Final +from typing import Final, NamedTuple, TypeAlias ############################################################################## # Textual imports. @@ -18,6 +18,7 @@ from textual_enhanced.commands import ChangeTheme, Command, Help from textual_enhanced.dialogs import ModalInput from textual_enhanced.screen import EnhancedScreen +from textual_enhanced.tools import History ############################################################################## # Local imports. @@ -48,6 +49,7 @@ SetColourToShadesOfBlue, SetColourToShadesOfGreen, SetColourToShadesOfRed, + Undo, ZeroZero, ZoomIn, ZoomInFaster, @@ -58,6 +60,27 @@ from ..providers import MainCommands +############################################################################## +class Situation(NamedTuple): + """A class to hold a particular situation we can undo to.""" + + x_position: float + """The X position in the plot.""" + y_position: float + """The Y position in the plot.""" + zoom: float + """The zoom level.""" + max_iteration: int + """The maximum iteration.""" + multibrot: float + """The multibrot setting.""" + + +############################################################################## +PlotHistory: TypeAlias = History[Situation] +"""Type of the plot history.""" + + ############################################################################## class Main(EnhancedScreen[None]): """The main screen for the application.""" @@ -101,6 +124,7 @@ class Main(EnhancedScreen[None]): SetColourToShadesOfBlue, SetColourToShadesOfGreen, SetColourToShadesOfRed, + Undo, ZeroZero, ZoomIn, ZoomInFaster, @@ -120,6 +144,8 @@ def __init__(self, arguments: Namespace) -> None: """ self._arguments = arguments """The command line arguments passed to the application.""" + self._history = PlotHistory() + """The plot situation history.""" super().__init__() def compose(self) -> ComposeResult: @@ -140,6 +166,7 @@ def on_mount(self) -> None: if self._arguments.colour_map is None else get_colour_map(self._arguments.colour_map), ) + self._remember() @on(Mandelbrot.Plotted) def _update_situation(self, message: Mandelbrot.Plotted) -> None: @@ -168,6 +195,19 @@ def _update_situation(self, message: Mandelbrot.Plotted) -> None: f"{message.elapsed:0.4f} seconds" ) + def _remember(self) -> None: + """Remember the current situation.""" + plot = self.query_one(Mandelbrot) + self._history.add( + Situation( + plot.x_position, + plot.y_position, + plot.zoom, + plot.max_iteration, + plot.multibrot, + ) + ) + def action_zoom(self, change: float) -> None: """Change the zoom value. @@ -175,6 +215,7 @@ def action_zoom(self, change: float) -> None: change: The amount to change the zoom by. """ self.query_one(Mandelbrot).zoom *= change + self._remember() def action_move(self, x: int, y: int) -> None: """Move the plot in the indicated direction. @@ -184,6 +225,7 @@ def action_move(self, x: int, y: int) -> None: y: The number of pixels to move in the Y direction. """ self.query_one(Mandelbrot).move(x, y) + self._remember() def action_iterate(self, change: int) -> None: """Change the maximum iteration. @@ -192,6 +234,7 @@ def action_iterate(self, change: int) -> None: change: The change to make to the maximum iterations. """ self.query_one(Mandelbrot).max_iteration += change + self._remember() def action_set_colour(self, colour_map: str) -> None: """Set the colour map for the plot. @@ -208,6 +251,7 @@ def action_multibrot(self, change: int) -> None: change: The change to make to the 'multibrot' value. """ self.query_one(Mandelbrot).multibrot += change + self._remember() def action_goto(self, x: int, y: int) -> None: """Go to a specific location. @@ -217,10 +261,12 @@ def action_goto(self, x: int, y: int) -> None: y: The Y location to go to. """ self.query_one(Mandelbrot).goto(x, y) + self._remember() def action_reset_command(self) -> None: """Reset the plot to its default values.""" self.query_one(Mandelbrot).reset() + self._remember() _VALID_LOCATION: Final[Pattern[str]] = compile( r"(?P[^, ]+) *[, ] *(?P[^, ]+)" @@ -270,5 +316,19 @@ def action_copy_command_line_to_clipboard_command(self) -> None: self.app.copy_to_clipboard(command) self.notify(command, title="Copied") + def action_undo_command(self) -> None: + """Undo through the history.""" + if ( + self._history.backward() + and (situation := self._history.current_item) is not None + ): + self.query_one(Mandelbrot).set( + x_position=situation.x_position, + y_position=situation.y_position, + zoom=situation.zoom, + max_iteration=situation.max_iteration, + multibrot=situation.multibrot, + ).plot() + ### main.py ends here