From a2fda92f495f31d7774724ba79ccce0389d2e643 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Mon, 12 Jan 2015 11:31:48 +0000 Subject: [PATCH] Initial commit showing apptools/enable integration for undo/redo. --- enable/example_application.py | 59 +++++++ enable/tools/apptools/__init__.py | 0 enable/tools/apptools/command_tool.py | 56 +++++++ enable/tools/apptools/commands.py | 158 ++++++++++++++++++ enable/tools/apptools/move_command_tool.py | 70 ++++++++ .../tools/apptools/undoable_move_tool.py | 92 ++++++++++ 6 files changed, 435 insertions(+) create mode 100644 enable/example_application.py create mode 100644 enable/tools/apptools/__init__.py create mode 100644 enable/tools/apptools/command_tool.py create mode 100644 enable/tools/apptools/commands.py create mode 100644 enable/tools/apptools/move_command_tool.py create mode 100644 examples/enable/tools/apptools/undoable_move_tool.py diff --git a/enable/example_application.py b/enable/example_application.py new file mode 100644 index 000000000..98b88ca85 --- /dev/null +++ b/enable/example_application.py @@ -0,0 +1,59 @@ +from __future__ import (division, absolute_import, print_function, + unicode_literals) + +from pyface.api import ApplicationWindow, GUI + +class DemoApplication(ApplicationWindow): + """ Simple Pyface application displaying a component. + + This application has the same interface as the DemoFrames from the + example_support module, but the window is embedded in a full Pyface + application. This means that subclasses have the opportunity of + adding Menus, Toolbars, and other similar features to the demo, where + needed. + + """ + + def _create_contents(self, parent): + self.enable_win = self._create_window() + return self.enable_win.control + + def _create_window(self): + "Subclasses should override this method and return an enable.Window" + raise NotImplementedError() + + @classmethod + def demo_main(cls, **traits): + """ Create the demo application and start the mainloop, if needed + + This should be called with appropriate arguments which will be passed to + the class' constructor. + + """ + # get the Pyface GUI + gui = GUI() + + # create the application's main window + window = cls(**traits) + window.open() + + # start the application + # if there isn't already a running mainloop, this will block + gui.start_event_loop() + + # if there is already a running mainloop (eg. in an IPython session), + # return a reference to the window so that our caller can hold on to it + return window + + +def demo_main(cls, **traits): + """ Create the demo application and start the mainloop, if needed. + + This is a simple wrapper around `cls.demo_main` for compatibility with the + `DemoFrame` implementation. + + This should be called with appropriate arguments which will be passed to + the class' constructor. + + """ + cls.demo_main(**traits) diff --git a/enable/tools/apptools/__init__.py b/enable/tools/apptools/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/enable/tools/apptools/command_tool.py b/enable/tools/apptools/command_tool.py new file mode 100644 index 000000000..0c88273be --- /dev/null +++ b/enable/tools/apptools/command_tool.py @@ -0,0 +1,56 @@ +# +# (C) Copyright 2015 Enthought, Inc., Austin, TX +# All right reserved. +# +# This file is open source software distributed according to the terms in +# LICENSE.txt +# + +""" +Command Tools +============= + +This module provides classes for tools that work with Apptools' Undo/Redo +Command stack. + +""" + +from __future__ import (division, absolute_import, print_function, + unicode_literals) + +from apptools.undo.api import ICommandStack, IUndoManager +from traits.api import Instance + +from enable.base_tool import BaseTool + + +class BaseCommandTool(BaseTool): + """ A tool which can push commands onto a command stack + + This is a base class for all tools that want to be able to issue + undoable commands. + + """ + + # The command stack to push to. + command_stack = Instance(ICommandStack) + + +class BaseUndoTool(BaseCommandTool): + """ A tool with access to an UndoManager + + This is a base class for all tools that want to be able to access undo and + redo functionality. + + """ + + # The undo manager + undo_manager = Instance(IUndoManager) + + def undo(self): + """ Call undo on the UndoManager """ + self.undo_manager.undo() + + def redo(self): + """ Call redo on the UndoManager """ + self.undo_manager.redo() diff --git a/enable/tools/apptools/commands.py b/enable/tools/apptools/commands.py new file mode 100644 index 000000000..d55bbdcf2 --- /dev/null +++ b/enable/tools/apptools/commands.py @@ -0,0 +1,158 @@ +# +# (C) Copyright 2015 Enthought, Inc., Austin, TX +# All right reserved. +# +# This file is open source software distributed according to the terms in +# LICENSE.txt +# + +""" +Enable Commands +=============== + +This module provides :py:class:`apptools.undo.abstract_command.AbstractCommand` +subclasses for common component manipulations, such as moving, resizing and +setting attribute values. + +""" + +from __future__ import (division, absolute_import, print_function, + unicode_literals) + +from apptools.undo.api import AbstractCommand +from traits.api import Bool, Instance, Tuple, Unicode +from traits.util.camel_case import camel_case_to_words + +from enable.component import Component + +class ComponentCommand(AbstractCommand): + """ Abstract command which operates on a Component """ + + #: The component the command is being performed on. + component = Instance(Component) + + #: An appropriate name for the component that can be used by the command. + #: The default is the class name, split into words. + component_name = Unicode + + #------------------------------------------------------------------------- + # traits handlers + #------------------------------------------------------------------------- + + def _component_name_default(self): + if self.component is not None: + return camel_case_to_words(self.component.__class__.__name__) + return '' + + +class MoveCommand(ComponentCommand): + """ A command that moves a component + + This handles some of the logic of moving a component and merging successive + moves. Subclasses should call `_change_position()` when they wish to move + the object, and should override the implementation of `_merge_data()` if + they wish to be able to merge non-finalized moves. + + """ + + #: whether the move is finished, or if additional moves can be merged. + final = Bool + + #------------------------------------------------------------------------- + # AbstractCommand interface + #------------------------------------------------------------------------- + + def merge(self, other): + if not self.final and isinstance(other, self.__class__) and \ + other.component == self.component: + return self._merge_data(other) + return super(MoveCommand, self).merge(other) + + #------------------------------------------------------------------------- + # Private interface + #------------------------------------------------------------------------- + + def _change_position(self, position): + self.component.position = list(position) + self.component._layout_needed = True + self.component.request_redraw() + + def _merge_data(self, other): + return False + + #------------------------------------------------------------------------- + # traits handlers + #------------------------------------------------------------------------- + + def _name_default(self): + return "Move "+self.component_name + + +class MoveDeltaCommand(MoveCommand): + """ Command that records fine-grained movement of an object + + This is suitable for being used for building up a Command from many + incremental steps. + + """ + + #: The change in position of the component as a tuple (dx, dy). + data = Tuple + + #------------------------------------------------------------------------- + # AbstractCommand interface + #------------------------------------------------------------------------- + + def do(self): + self.redo() + + def redo(self): + x = self.component.position[0] + self.delta[0] + y = self.component.position[1] + self.delta[1] + self._change_position((x, y)) + + def undo(self): + x = self.component.position[0] - self.delta[0] + y = self.component.position[1] - self.delta[1] + self._change_position((x, y)) + + #------------------------------------------------------------------------- + # Private interface + #------------------------------------------------------------------------- + + def _merge_data(self, other): + x = self.data[0] + other.data[0] + y = self.data[1] + other.data[1] + self.data = (x, y) + + +class MovePositionCommand(MoveCommand): + """ Command that records gross movement of an object """ + + #: The new position of the component as a tuple (x, y). + data = Tuple + + #: The old position of the component as a tuple (x, y). + previous_position = Tuple + + #------------------------------------------------------------------------- + # AbstractCommand interface + #------------------------------------------------------------------------- + + def do(self): + if self.previous_position == (): + self.previous_position = tuple(self.component.position) + self.redo() + + def redo(self): + self._change_position(self.data) + + def undo(self): + self._change_position(self.previous_position) + + #------------------------------------------------------------------------- + # Private interface + #------------------------------------------------------------------------- + + def _merge_data(self, other): + self.data = other.data diff --git a/enable/tools/apptools/move_command_tool.py b/enable/tools/apptools/move_command_tool.py new file mode 100644 index 000000000..7e84e4189 --- /dev/null +++ b/enable/tools/apptools/move_command_tool.py @@ -0,0 +1,70 @@ +# +# (C) Copyright 2015 Enthought, Inc., Austin, TX +# All right reserved. +# +# This file is open source software distributed according to the terms in +# LICENSE.txt +# +""" +MoveCommandTool +=============== + +A MoveTool that uses AppTools' undo/redo infrastructure to create undoable move +commands. + +""" + +from __future__ import (division, absolute_import, print_function, + unicode_literals) + +from traits.api import Tuple + +from enable.tools.move_tool import MoveTool + +from .command_tool import BaseCommandTool +from .commands import MovePositionCommand + + +class MoveCommandTool(MoveTool, BaseCommandTool): + """ Move tool which pushes MovePositionCommands onto a CommandStack + + This tool pushes a single MovePositionCommand onto its CommandStack at + the end of the drag operation. If the drag is cancelled, then no command + is issued, and no commands are issued during the drag operation. + + """ + + #: The initial component position. + _initial_position = Tuple(0, 0) + + def drag_start(self, event): + if self.component: + # we need to save the initial position to give to the Command + self._initial_position = tuple(self.component.position) + return super(MoveCommandTool, self).drag_start(event) + + def drag_end(self, event): + """ End the drag operation, issuing a MovePositionCommand """ + if self.component: + command = MovePositionCommand( + component=self.component, + data=tuple(self.component.position), + previous_position=self._initial_position, + final=True) + self.command_stack.push(command) + event.handled = True + return + + def drag_cancel(self, event): + """ Restore the component's position if the drag is cancelled. + + A drag is usually cancelled by receiving a mouse_leave event when + `end_drag_on_leave` is True, or by the user pressing any of the + `cancel_keys`. + + """ + if self.component: + self.component.position = list(self._initial_position) + + event.handled = True + return diff --git a/examples/enable/tools/apptools/undoable_move_tool.py b/examples/enable/tools/apptools/undoable_move_tool.py new file mode 100644 index 000000000..d856ee044 --- /dev/null +++ b/examples/enable/tools/apptools/undoable_move_tool.py @@ -0,0 +1,92 @@ +# +# (C) Copyright 2015 Enthought, Inc., Austin, TX +# All right reserved. +# +# This file is open source software distributed according to the terms in +# LICENSE.txt +# + +""" +Undoable Move Tool +================== + +This example shows how to integrate a simple component move tool with apptools +undo/redo infrastructure. + +""" +from __future__ import (division, absolute_import, print_function, + unicode_literals) + +from apptools.undo.api import (CommandStack, ICommandStack, IUndoManager, + UndoManager) +from apptools.undo.action.api import UndoAction, RedoAction +from pyface.api import ApplicationWindow, GUI +from pyface.action.api import Action, Group, MenuBarManager, MenuManager +from traits.api import Instance + +from enable.api import Container, Window +from enable.example_application import DemoApplication, demo_main +from enable.primitives.api import Box +from enable.tools.apptools.move_command_tool import MoveCommandTool + + +class UndoableMoveApplication(DemoApplication): + """ Example of using a MoveCommandTool with undo/redo support. """ + + #: The apptools undo manager the application uses. + undo_manager = Instance(IUndoManager) + + #: The command stack that the MoveCommandTool will use. + command_stack = Instance(ICommandStack) + + #------------------------------------------------------------------------- + # DemoApplication interface + #------------------------------------------------------------------------- + + def _create_window(self): + box = Box(bounds=[100,100], position=[50,50], color='red') + tool = MoveCommandTool(component=box, + command_stack=self.command_stack) + box.tools.append(tool) + + container = Container(bounds=[600, 600]) + container.add(box) + return Window(self.control, -1, component=container) + + #------------------------------------------------------------------------- + # Traits handlers + #------------------------------------------------------------------------- + + def _menu_bar_manager_default(self): + # Create an action that exits the application. + exit_action = Action(name='E&xit', on_perform=self.close) + self.exit_action = exit_action + file_menu = MenuManager(name='&File') + file_menu.append(Group(exit_action)) + + self.undo = UndoAction(undo_manager=self.undo_manager, + accelerator='Ctrl+Z') + self.redo = RedoAction(undo_manager=self.undo_manager, + accelerator='Ctrl+Shift+Z') + menu_bar_manager = MenuBarManager( + file_menu, + MenuManager( + self.undo, + self.redo, + name='&Edit') + ) + return menu_bar_manager + + def _undo_manager_default(self): + return UndoManager() + + def _command_stack_default(self): + stack = CommandStack(undo_manager=self.undo_manager) + self.undo_manager.active_stack = stack + return stack + + +if __name__ == "__main__": + # Save demo so that it doesn't get garbage collected when run within + # existing event loop (i.e. from ipython). + demo_main(UndoableMoveApplication, size=(600, 600))