Skip to content
Snippets Groups Projects
Commit 632636ef authored by Paul McCarthy's avatar Paul McCarthy
Browse files

Re-factoring actions module. The Action class may now be used as a

decorator. New sub-class called ToggleAction, which does some
wx.MenuItem-specific stuff. ActionProvider class no longer derives
from SyncableHasProperties - sub-classes will have to explicitly inherit
from both. All the docs are wrong, and all the things are broken.
parent ada55f9a
No related branches found
No related tags found
No related merge requests found
......@@ -18,7 +18,6 @@ Some 'global' actions are also provided in this package:
.. autosummary::
~fsl.fsleyes.actions.copyoverlay
~fsl.fsleyes.actions.loadcolourmap
~fsl.fsleyes.actions.openfile
~fsl.fsleyes.actions.openstandard
~fsl.fsleyes.actions.saveoverlay
......@@ -32,7 +31,7 @@ one or more actions. As the :class:`.FSLEyesPanel` class derives from
import logging
import collections
import inspect
import props
......@@ -58,10 +57,12 @@ def listGlobalActions():
saveoverlay .SaveOverlayAction]
class ActionButton(props.Button):
"""Extends the :class:`props.Button` class to encapsulate an
:class:`Action` instance.
"""
def __init__(self, actionName, classType=None, **kwargs):
"""Create an ``ActionButton``.
......@@ -99,51 +100,70 @@ class ActionButton(props.Button):
def __onButton(self, instance, *a):
"""Called when the button is pushed. Runs the action."""
instance.run(self.name)
instance.getAction(self.name)()
class ActionDisabledError(Exception):
pass
class Action(props.HasProperties):
"""Class which represents an action of some sort.
The actual action which is performed may be specified either by
specifying it it during initialisation (the ``action`` parameter to
:meth:`__init__`), or by subclasses overriding the :meth:`doAction`
method. The former method will take precedence over the latter.
class Action(props.HasProperties):
"""
https://wiki.python.org/moin/PythonDecoratorLibrary#Class_method_decorator_using_instance
"""
enabled = props.Boolean(default=True)
"""Controls whether the action is currently enabled or disabled.
When this property is ``False`` calls to :meth:`doAction` will
result in a ``RuntimeError``.
When this property is ``False`` calls to the action will
result in a :exc:`ActionDisabledError`.
"""
def __init__(self, overlayList, displayCtx, action=None):
def __init__(self, func):
"""Create an ``Action``.
:arg overlayList: An :class:`.OverlayList` instance
containing the list of overlays being displayed.
:arg displayCtx: A :class:`.DisplayContext` instance defining how
the overlays are to be displayed.
:arg action: The action function. If not provided, assumes that
the :meth:`doAction` method has been overridden.
:arg func: The action function.
"""
self._overlayList = overlayList
self._displayCtx = displayCtx
self._boundWidgets = []
self._name = '{}_{}'.format(self.__class__.__name__, id(self))
if action is not None:
self.doAction = action
self.__enabledDoAction = self.doAction
self.__instance = None
self.__func = func
self.__name = func.__name__
self.__boundWidgets = []
self.addListener('enabled',
'Action_{}_internal'.format(id(self)),
self._enabledChanged)
self.__enabledChanged)
def __get__(self, instance, cls):
if self.__instance is None and instance is not None:
self.__instance = instance
return self
def __call__(self, *args, **kwargs):
if not self.enabled:
raise ActionDisabledError('Action {} is disabled'.format(
self.__name))
log.debug('Action {} called'.format(self.__name))
if self.__instance is not None:
args = [self.__instance] + list(args)
return self.__func(*args, **kwargs)
def destroy(self):
self.unbindAllWidgets()
self.__func = None
self.__instance = None
def bindToWidget(self, parent, evType, widget):
......@@ -157,17 +177,11 @@ class Action(props.HasProperties):
"""
def wrappedAction(ev):
self.doAction()
self()
parent.Bind(evType, wrappedAction, widget)
widget.Enable(self.enabled)
self._boundWidgets.append((parent, evType, widget))
def destroy(self):
self.unbindAllWidgets()
self.__enabledDoAction = None
self.__disabledDoAction = None
self.__boundWidgets.append((parent, evType, widget))
def unbindAllWidgets(self):
......@@ -176,7 +190,7 @@ class Action(props.HasProperties):
import wx
for parent, evType, widget in self._boundWidgets:
for parent, evType, widget in self.__boundWidgets:
# Only attempt to unbind if the parent
# and widget have not been destroyed
......@@ -185,132 +199,103 @@ class Action(props.HasProperties):
except wx.PyDeadObjectError:
pass
self._boundWidgets = []
self.__boundWidgets = []
def getBoundWidgets(self):
"""
"""
return [w for _, _, w in self.__boundWidgets]
def _enabledChanged(self, *args):
def __enabledChanged(self, *args):
"""Internal method which is called when the :attr:`enabled` property
changes. Enables/disables the action, and any bound widgets.
changes. Enables/disables any bound widgets.
"""
if self.enabled: self.doAction = self.__enabledDoAction
else: self.doAction = self.__disabledDoAction
for _, _, widget in self._boundWidgets:
for _, _, widget in self.__boundWidgets:
widget.Enable(self.enabled)
def __disabledDoAction(self, *args):
"""This method gets called when the action is disabled."""
raise RuntimeError('{} is disabled'.format(self.__class__.__name__))
class ToggleAction(Action):
def __enabledDoAction(self, *args):
"""This method is set in :meth:`__init__`; it gets called when the
action is enabled."""
pass
def doAction(self, *args):
"""This method must be overridden by subclasses.
toggled = props.Boolean(default=False)
It performs the action, or raises a ``RuntimeError`` if the action
is disabled.
"""
raise NotImplementedError('Action object must implement '
'the doAction method')
def __init__(self, func, initState=False):
Action.__init__(self, func)
self.toggled = initState
self.addListener('toggled',
'ToggleAction_{}_internal'.format(id(self)),
self.__toggledChanged)
class ActionProvider(props.SyncableHasProperties):
"""An :class:`ActionProvider` is some entity which can perform actions.
def __call__(self, *args, **kwargs):
"""
"""
Said entity is also a :class:`~props.HasProperties` instance, so can
optionally define some properties which, along with any defined actions,
will ultimately be exposed to the user.
"""
result = Action.__call__(self, *args, **kwargs)
self.toggled = not self.toggled
return result
def __init__(self, overlayList, displayCtx, **kwargs):
"""Create an :class:`ActionProvider` instance.
:arg overlayList: An :class:`.OverlayList` instance containing the
list of overlays being displayed.
def bindToWidget(self, parent, evType, widget):
:arg displayCtx: A :class:`.DisplayContext` instance defining how
the overlays are to be displayed.
import wx
Action.bindToWidget(self, parent, evType, widget)
:arg actions: A dictionary containing ``{name -> function}``
mappings, where each function is an action that
should be made available to the user.
if isinstance(widget, wx.MenuItem):
widget.Check(self.toggled)
:arg kwargs: Passed to the :class:`.SyncableHasProperties`
constructor.
"""
def __toggledChanged(self, *a):
import wx
for widget in self.getBoundWidgets():
actions = kwargs.pop('actions', None)
if isinstance(widget, wx.MenuItem):
widget.Check(self.toggled)
props.SyncableHasProperties.__init__(self, **kwargs)
if actions is None:
actions = {}
self.__actions = collections.OrderedDict()
class ActionProvider(object):
for name, func in actions.items():
act = Action(overlayList, displayCtx, action=func)
self.__actions[name] = act
def destroy(self):
"""This method should be called when this ``ActionProvider`` is
about to be destroyed. It ensures that all ``Action`` instances
are cleared.
"""
for n, act in self.__actions.items():
act.destroy()
self.__actions = None
for name, action in self.getActions():
action.destroy()
def addActionToggleListener(self, name, listenerName, func):
"""Add a listener function which will be called when the named action
is enabled or disabled.
"""
self.__actions[name].addListener('enabled', listenerName, func)
def getAction(self, name):
return getattr(self, name)
def getAction(self, name):
"""Return the :class:`Action` object of the given name. """
return self.__actions[name]
def enableAction(self, name, enable=True):
self.getAction(name).enable = enable
def disableAction(self, name):
self.enableAction(name, False)
def getActions(self):
"""Return a dictionary containing ``{name -> Action}`` mappings for
all defined actions.
"""
return collections.OrderedDict(self.__actions)
def isEnabled(self, name):
"""Return ``True`` if the named action is enabled, ``False`` otherwise.
Sub-classes may override this method to enforce a specific ordering
of their actions.
"""
return self.__actions[name].enabled
def enable(self, name, enable=True):
"""Enables/disables the named action. """
self.__actions[name].enabled = enable
acts = []
def disable(self, name):
"""Disables the named action. """
self.__actions[name].enabled = False
def toggle(self, name):
"""Toggles the state of the named action. """
self.__actions[name].enabled = not self.__actions[name].enabled
def run(self, name, *args):
"""Performs the named action."""
self.__actions[name].doAction(*args)
for name, attr in inspect.getmembers(self):
if isinstance(attr, Action):
acts.append((name, attr))
return acts
......@@ -25,21 +25,34 @@ class CopyOverlayAction(actions.Action):
"""
def __init__(self, *args, **kwargs):
"""Create a ``CopyOverlayAction``. All arguments are passed through
to the :class:`.Action` constructor.
def __init__(self, overlayList, displayCtx):
"""Create a ``CopyOverlayAction``.
"""
actions.Action.__init__(self, *args, **kwargs)
actions.Action.__init__(self, self.__copyOverlay)
self._displayCtx .addListener('selectedOverlay',
self._name,
self.__selectedOverlayChanged)
self._overlayList.addListener('overlays',
self._name,
self.__selectedOverlayChanged)
self.__overlayList = overlayList
self.__displayCtx = displayCtx
self.__name = '{}_{}'.format(type(self).__name__, id(self))
displayCtx .addListener('selectedOverlay',
self.__name,
self.__selectedOverlayChanged)
overlayList.addListener('overlays',
self.__name,
self.__selectedOverlayChanged)
self.__selectedOverlayChanged()
def destroy(self):
"""Removes listeners from the :class:`.DisplayContext` and
:class:`.OverlayList`, and calls :meth:`.Action.destroy`.
"""
self.__displayCtx .removeListener('selectedOverlay', self.__name)
self.__overlayList.removeListener('overlays', self.__name)
actions.Action.destroy(self)
def __selectedOverlayChanged(self, *a):
"""Called when the selected overlay, or overlay list, changes.
......@@ -47,29 +60,19 @@ class CopyOverlayAction(actions.Action):
Enables/disables this action depending on the nature of the selected
overlay.
"""
overlay = self._displayCtx.getSelectedOverlay()
overlay = self.__displayCtx.getSelectedOverlay()
self.enabled = (overlay is not None) and \
isinstance(overlay, fslimage.Image)
def destroy(self):
"""Removes listeners from the :class:`.DisplayContext` and
:class:`.OverlayList`, and calls :meth:`.Action.destroy`.
"""
self._displayCtx .removeListener('selectedOverlay', self._name)
self._overlayList.removeListener('overlays', self._name)
actions.Action.destroy(self)
def doAction(self):
def __copyOverlay(self):
"""Creates a copy of the currently selected overlay, and inserts it
into the :class:`.OverlayList`.
"""
ovlIdx = self._displayCtx.selectedOverlay
overlay = self._overlayList[ovlIdx]
ovlIdx = self.__displayCtx.selectedOverlay
overlay = self.__overlayList[ovlIdx]
if overlay is None:
return
......@@ -86,4 +89,4 @@ class CopyOverlayAction(actions.Action):
# TODO copy display properties
self._overlayList.insert(ovlIdx + 1, copy)
self.__overlayList.insert(ovlIdx + 1, copy)
......@@ -23,12 +23,18 @@ class OpenFileAction(actions.Action):
:meth:`.OverlayList.addOverlays` method.
"""
def doAction(self):
def __init__(self, overlayList, displayCtx):
actions.Action.__init__(self, self.__openFile)
self.__overlayList = overlayList
self.__displayCtx = displayCtx
def __openFile(self):
"""Calls :meth:`.OverlayList.addOverlays` method. If overlays were added,
updates the :attr:`.DisplayContext.selectedOverlay` accordingly.
"""
if self._overlayList.addOverlays():
self._displayCtx.selectedOverlay = \
self._displayCtx.overlayOrder[-1]
if self.__overlayList.addOverlays():
self.__displayCtx.selectedOverlay = \
self.__displayCtx.overlayOrder[-1]
......@@ -26,7 +26,10 @@ class OpenStandardAction(actions.Action):
overlays, using ``$FSLDIR/data/standard/`` as the default directory.
"""
def __init__(self, overlayList, displayCtx):
actions.Action.__init__(self, overlayList, displayCtx)
actions.Action.__init__(self, self.__openStandard)
self.__overlayList = overlayList
self.__displayCtx = displayCtx
# disable this action
# if $FSLDIR is not set
......@@ -39,10 +42,11 @@ class OpenStandardAction(actions.Action):
self.enabled = False
def doAction(self):
def __openStandard(self):
"""Calls the :meth:`.OverlayList.addOverlays` method. If the user
added some overlays, updates the
:attr:`.DisplayContext.selectedOverlay` accordingly.
"""
if self._overlayList.addOverlays(self.__stddir, addToEnd=False):
self._displayCtx.selectedOverlay = self._displayCtx.overlayOrder[0]
if self.__overlayList.addOverlays(self.__stddir, addToEnd=False):
self.__displayCtx.selectedOverlay = \
self.__displayCtx.overlayOrder[0]
......@@ -24,18 +24,22 @@ class SaveOverlayAction(actions.Action):
"""
def __init__(self, *args, **kwargs):
def __init__(self, overlayList, displayCtx):
"""Create a ``SaveOverlayAction``. All arguments are passed through
to the :class:`.Action` constructor.
"""
actions.Action.__init__(self, *args, **kwargs)
actions.Action.__init__(self, self.__saveOverlay)
self._displayCtx .addListener('selectedOverlay',
self._name,
self.__selectedOverlayChanged)
self._overlayList.addListener('overlays',
self._name,
self.__selectedOverlayChanged)
self.__overlayList = overlayList
self.__displayCtx = displayCtx
self.__name = '{}_{}'.format(type(self).__name__, id(self))
displayCtx .addListener('selectedOverlay',
self.__name,
self.__selectedOverlayChanged)
overlayList.addListener('overlays',
self.__name,
self.__selectedOverlayChanged)
self.__selectedOverlayChanged()
......@@ -45,8 +49,8 @@ class SaveOverlayAction(actions.Action):
:class:`.OverlayList`, and calls :meth:`.Action.destroy`.
"""
self._displayCtx .removeListener('selectedOverlay', self._name)
self._overlayList.removeListener('overlays', self._name)
self.__displayCtx .removeListener('selectedOverlay', self.__name)
self.__overlayList.removeListener('overlays', self.__name)
actions.Action.destroy(self)
......@@ -57,7 +61,7 @@ class SaveOverlayAction(actions.Action):
this action is enabled; otherwise it is disabled.
"""
overlay = self._displayCtx.getSelectedOverlay()
overlay = self.__displayCtx.getSelectedOverlay()
# TODO Support for other overlay types
......@@ -65,11 +69,11 @@ class SaveOverlayAction(actions.Action):
isinstance(overlay, fslimage.Image) and
(not overlay.saved))
for ovl in self._overlayList:
for ovl in self.__overlayList:
if not isinstance(ovl, fslimage.Image):
continue
ovl.removeListener('saved', self._name)
ovl.removeListener('saved', self.__name)
# Register a listener on the saved property
# of the currently selected image, so we can
......@@ -77,7 +81,7 @@ class SaveOverlayAction(actions.Action):
# becomes 'unsaved', and vice versa.
if ovl is overlay:
ovl.addListener('saved',
self._name,
self.__name,
self.__overlaySaveStateChanged)
......@@ -90,7 +94,7 @@ class SaveOverlayAction(actions.Action):
see the :meth:`__selectedOverlayChanged` method.
"""
overlay = self._displayCtx.getSelectedOverlay()
overlay = self.__displayCtx.getSelectedOverlay()
if overlay is None:
self.enabled = False
......@@ -101,12 +105,12 @@ class SaveOverlayAction(actions.Action):
self.enabled = not overlay.saved
def doAction(self):
def __saveOverlay(self):
"""Saves the currently selected overlay (only if it is a
:class:`.Image`), by a call to :meth:`.Image.save`.
"""
overlay = self._displayCtx.getSelectedOverlay()
overlay = self.__displayCtx.getSelectedOverlay()
if overlay is None:
return
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment