-
Notifications
You must be signed in to change notification settings - Fork 44
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial commit showing apptools/enable integration for undo/redo.
- Loading branch information
1 parent
70c1460
commit a2fda92
Showing
6 changed files
with
435 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.