Skip to content

Commit

Permalink
Initial commit showing apptools/enable integration for undo/redo.
Browse files Browse the repository at this point in the history
  • Loading branch information
corranwebster committed Jan 12, 2015
1 parent 70c1460 commit a2fda92
Show file tree
Hide file tree
Showing 6 changed files with 435 additions and 0 deletions.
59 changes: 59 additions & 0 deletions enable/example_application.py
Original file line number Diff line number Diff line change
@@ -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)
Empty file.
56 changes: 56 additions & 0 deletions enable/tools/apptools/command_tool.py
Original file line number Diff line number Diff line change
@@ -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()
158 changes: 158 additions & 0 deletions enable/tools/apptools/commands.py
Original file line number Diff line number Diff line change
@@ -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
70 changes: 70 additions & 0 deletions enable/tools/apptools/move_command_tool.py
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit a2fda92

Please sign in to comment.