Skip to content

Commit

Permalink
Copy Undo over from apptools (#813)
Browse files Browse the repository at this point in the history
* copy Undo over from apptools

* copy over undo examples from apptools

* copy over undo documentation from apptools

* update copyright headers

* follow convention of instantiating traits for trait definitions

* add comment about circular dependency workaround

* add #: to trait definitions comments so traits come up with autogenerated api docs

* remove unneeded redundant portion of docs mentioning API overview (this is all available in auto generated api docs).  Also remove broken links

* getting the undo example working

* undoing an unneeded change

* adding more tests

* add some simple tests for undo.actions

* Apply suggestions from code review

Co-authored-by: Poruri Sai Rahul <[email protected]>

* another suggestion from code review

Co-authored-by: Poruri Sai Rahul <[email protected]>
  • Loading branch information
aaronayres35 and Poruri Sai Rahul authored Dec 1, 2020
1 parent 3443b3e commit 75709d0
Show file tree
Hide file tree
Showing 30 changed files with 2,263 additions and 6 deletions.
1 change: 1 addition & 0 deletions docs/source/submodules.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ Submodules
Data View <data_view>
Fields <fields>
Timers <timer>
Undo <undo>
72 changes: 72 additions & 0 deletions docs/source/undo.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
Undo Framework
==============

The Undo Framework is a component of the Enthought Tool Suite that provides
developers with an API that implements the standard pattern for do/undo/redo
commands.

The framework is completely configurable. Alternate implementations of all
major components can be provided if necessary.


Framework Concepts
------------------

The following are the concepts supported by the framework.

- Command

A command is an application defined operation that can be done (i.e.
executed), undone (i.e. reverted) and redone (i.e. repeated).

A command operates on some data and maintains sufficient state to allow it to
revert or repeat a change to the data.

Commands may be merged so that potentially long sequences of similar
commands (e.g. to add a character to some text) can be collapsed into a
single command (e.g. to add a word to some text).

- Macro

A macro is a sequence of commands that is treated as a single command when
being undone or redone.

- Command Stack

A command is done by pushing it onto a command stack. The last command can
be undone and redone by calling appropriate command stack methods. It is
also possible to move the stack's position to any point and the command stack
will ensure that commands are undone or redone as required.

A command stack maintains a *clean* state which is updated as commands are
done and undone. It may be explicitly set, for example when the data being
manipulated by the commands is saved to disk.

PyFace actions are provided as wrappers around command stack methods
to implement common menu items.

- Undo Manager

An undo manager is responsible for one or more command stacks and maintains
a reference to the currently active stack. It provides convenience undo and
redo methods that operate on the currently active stack.

An undo manager ensures that each command execution is allocated a unique
sequence number, irrespective of which command stack it is pushed to. Using
this it is possible to synchronise multiple command stacks and restore them
to a particular point in time.

An undo manager will generate an event whenever the clean state of the active
stack changes. This can be used to maintain some sort of GUI status
indicator to tell the user that their data has been modified since it was
last saved.

Typically an application will have one undo manager and one undo stack for
each data type that can be edited. However this is not a requirement: how the
command stack's in particular are organised and linked (with the user
manager's sequence number) can need careful thought so as not to confuse the
user - particularly in a plugin based application that may have many editors.

To support this typical usage the PyFace ``Workbench`` class has an
``undo_manager`` trait and the PyFace ``Editor`` class has a ``command_stack``
trait. Both are lazy loaded so can be completely ignored if they are not used.
213 changes: 213 additions & 0 deletions examples/undo/commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
# (C) Copyright 2007-2020 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!

# -----------------------------------------------------------------------------
# Copyright (c) 2007, Riverbank Computing Limited
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in enthought/LICENSE.txt and may be redistributed only
# under the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
# Thanks for using Enthought open source!
#
# Author: Riverbank Computing Limited
# Description: <Enthought undo package component>
# -----------------------------------------------------------------------------


# Enthought library imports.
from traits.api import Instance, Int, Str
from pyface.undo.api import AbstractCommand

# Local imports.
from model import Label


class LabelIncrementSizeCommand(AbstractCommand):
""" The LabelIncrementSizeCommand class is a command that increases the
size of a label's text. This command will merge multiple increments
togther.
"""

#### 'ICommand' interface #################################################

# The data being operated on.
data = Instance(Label)

# The name of the command.
name = Str("&Increment size")

#### Private interface ####################################################

_incremented_by = Int()

###########################################################################
# 'ICommand' interface.
###########################################################################

def do(self):
self.data.increment_size(1)
self._incremented_by = 1

def merge(self, other):
# We can merge if the other command is the same type (or a sub-type).
if isinstance(other, type(self)):
self._incremented_by += 1
merged = True
else:
merged = False

return merged

def redo(self):
self.data.increment_size(self._incremented_by)

def undo(self):
self.data.decrement_size(self._incremented_by)


class LabelDecrementSizeCommand(AbstractCommand):
""" The LabelDecrementSizeCommand class is a command that decreases the
size of a label's text. This command will merge multiple decrements
togther.
"""

#### 'ICommand' interface #################################################

# The data being operated on.
data = Instance(Label)

# The name of the command.
name = Str("&Decrement size")

#### Private interface ####################################################

_decremented_by = Int()

###########################################################################
# 'ICommand' interface.
###########################################################################

def do(self):
self.data.decrement_size(1)
self._decremented_by = 1

def merge(self, other):
# We can merge if the other command is the same type (or a sub-type).
if isinstance(other, type(self)):
self._decremented_by += 1
merged = True
else:
merged = False

return merged

def redo(self):
self.data.decrement_size(self._decremented_by)

def undo(self):
self.data.increment_size(self._decremented_by)


class LabelNormalFontCommand(AbstractCommand):
""" The LabelNormalFontCommand class is a command that sets a normal font
for a label's text.
"""

#### 'ICommand' interface #################################################

# The data being operated on.
data = Instance(Label)

# The name of the command.
name = Str("&Normal font")

###########################################################################
# 'ICommand' interface.
###########################################################################

def do(self):
# Save the old value.
self._saved = self.data.style

# Calling redo() is a convenient way to update the model now that the
# old value is saved.
self.redo()

def redo(self):
self.data.style = 'normal'

def undo(self):
self.data.style = self._saved


class LabelBoldFontCommand(AbstractCommand):
""" The LabelNormalFontCommand class is a command that sets a bold font for
a label's text.
"""

#### 'ICommand' interface #############################################

# The data being operated on.
data = Instance(Label)

# The name of the command.
name = Str("&Bold font")

###########################################################################
# 'ICommand' interface.
###########################################################################

def do(self):
# Save the old value.
self._saved = self.data.style

# Calling redo() is a convenient way to update the model now that the
# old value is saved.
self.redo()

def redo(self):
self.data.style = 'bold'

def undo(self):
self.data.style = self._saved


class LabelItalicFontCommand(AbstractCommand):
""" The LabelNormalFontCommand class is a command that sets an italic font
for a label's text.
"""

#### 'ICommand' interface #################################################

# The data being operated on.
data = Instance(Label)

# The name of the command.
name = Str("&Italic font")

###########################################################################
# 'ICommand' interface.
###########################################################################

def do(self):
# Save the old value.
self._saved = self.data.style

# Calling redo() is a convenient way to update the model now that the
# old value is saved.
self.redo()

def redo(self):
self.data.style = 'italic'

def undo(self):
self.data.style = self._saved
96 changes: 96 additions & 0 deletions examples/undo/example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# (C) Copyright 2008-2020 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!

# -----------------------------------------------------------------------------
# Copyright (c) 2008, Riverbank Computing Limited
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in enthought/LICENSE.txt and may be redistributed only
# under the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
# Thanks for using Enthought open source!
#
# Author: Riverbank Computing Limited
# Description: <Enthought undo package component>
# -----------------------------------------------------------------------------


# Standard library imports.
import logging

# Enthought library imports.
from pyface.api import GUI, YES
from pyface.workbench.api import Workbench
from pyface.undo.api import UndoManager

# Local imports.
from example_undo_window import ExampleUndoWindow
from model import Label


# Log to stderr.
logging.getLogger().addHandler(logging.StreamHandler())
logging.getLogger().setLevel(logging.DEBUG)


class ExampleUndo(Workbench):
""" The ExampleUndo class is a workbench that creates ExampleUndoWindow
windows.
"""

#### 'Workbench' interface ################################################

# The factory (in this case simply a class) that is used to create
# workbench windows.
window_factory = ExampleUndoWindow

###########################################################################
# Private interface.
###########################################################################

def _exiting_changed(self, event):
""" Called when the workbench is exiting. """

if self.active_window.confirm('Ok to exit?') != YES:
event.veto = True

return


def main(argv):
""" A simple example of using the the undo framework in a workbench. """

# Create the GUI.
gui = GUI()

# Create the workbench.
workbench = ExampleUndo(state_location=gui.state_location)

window = workbench.create_window(position=(300, 300), size=(400, 300))
window.open()

# Create some objects to edit.
label = Label(text="Label")
label2 = Label(text="Label2")

# Edit the objects.
window.edit(label)
window.edit(label2)

# Start the GUI event loop.
gui.start_event_loop()

return


if __name__ == '__main__':
import sys
main(sys.argv)
Loading

0 comments on commit 75709d0

Please sign in to comment.