#
# __init__.py - Superclasses for objects which perform 'actions'.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This package provides a collection of actions, classes - the
:class:`~fsleyes.actions.base.Action` class and the :class:`.ActionProvider`
class, and the :func:`action` and :func:`toggleAction` decorators.
The :class:`.Action` class represents some sort of action which may be
performed, enabled and disabled, and may be bound to a GUI menu item or
button. The :class:`ActionProvider` class represents some entity which can
perform one or more actions. As the :class:`.FSLeyesPanel` class derives from
:class:`ActionProvider` pretty much everything in FSLeyes is an
:class:`ActionProvider`.
Many of the modules in this package also contain standalone functions for doing
various things, such as the :func:`.screenshot.screenshot` function, and the
:func:`.loadoverlay.loadImage` function.
The :func:`action` and :func:`toggleAction` functions are intended to be used
as decorators upon the methods of a class which derives from
:class:`ActionProvider`. For example::
>>> import fsleyes.actions as actions
>>> class Thing(actions.ActionProvider):
@actions.action
def doFirstThing(self):
print 'First thing done'
@actions.action
def doSecondThing(self):
print 'Second thing done'
@actions.toggleAction
def toggleOtherThing(self):
print 'Other thing toggled'
In this example, when an instance of ``Thing`` is defined, each of the methods
that are defined as actions will be available through the methods defined in
the :class:`ActionProvder`. For example::
>>> t = Thing()
>>> print t.getActions()
[('doFirstThing', Action(doFirstThing)),
('doSecondThing', Action(doSecondThing)),
('toggleOtherThing', ToggleAction(toggleOtherThing))]
You can enable/disable actions through the :meth:`ActionProvider.enableAction`
and :meth:`ActionProvider.disableAction` methods::
>>> t.disableAction('doFirstThing')
>>> t.doFirstThing()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/.../fsleyes/actions/__init__.py", line 139, in __call__
self.__name))
fsleyes.actions.ActionDisabledError: Action doFirstThing is disabled
It is useful to know that each method on the ``t`` instance has actually been
replaced with an :class:`.Action` instance, which encapsulates the method.
Using this knowledge, you can access the ``Action`` instances directly::
>>> t.doFirstThing.enabled = True
>>> t.doFirstThing()
First thing done
The :meth:`.Action.bindToWidget` method allows a widget to be bound to an
:class:`.Action`. For example::
# We're assuming here that a wx.App, and
# a parent window, has been created
>>> button = wx.Button(parent, label='Do the first thing!')
>>> t.doSecondThing.bindToWidget(parent, button, wx.EVT_BUTTON)
All bound widgets of an ``Action`` can be accessed through the
:meth:`.Action.getBoundWidgets` method, and can be unbound via the
:meth:`.Action.unbindAllWidgets` method.
This module also provides two classes which allow a widget to be automatically
created for, and bound to an ``Action`` or ``ToggleAction`` (through the
:mod:`props.build` package):
.. autosummary::
:nosignatures:
ActionButton
ToggleActionButton
"""
import logging
import types
import inspect
import warnings
import functools
import fsleyes_props as props
import fsleyes.strings as strings
from . import base
from . import togglecontrolpanel
Action = base.Action
ActionDisabledError = base.ActionDisabledError
ToggleAction = base.ToggleAction
ToggleControlPanelAction = togglecontrolpanel.ToggleControlPanelAction
log = logging.getLogger(__name__)
[docs]def action(*args, **kwargs):
"""A decorator which identifies a class method as an action. """
return ActionFactory(Action, *args, **kwargs)
[docs]def toggleAction(*args, **kwargs):
"""A decorator which identifies a class method as a toggle action. """
return ActionFactory(ToggleAction, *args, **kwargs)
[docs]def toggleControlAction(*args, **kwargs):
"""A decorator which identifies a class method as a
:class:`.ToggleControlPanelAction`.
"""
return ActionFactory(ToggleControlPanelAction, *args, **kwargs)
[docs]class ActionProvider(object):
"""The ``ActionProvider`` class is intended to be used as a base class for
classes which contain actions. The :func:`action` and :func:`toggleAction`
functions can be used as decorators on class methods, to denote them as
actions.
"""
[docs] def __init__(self, overlayList, displayCtx):
"""Create an ``ActionProvider``.
:arg overlayList: The :class:`.OverlayList`.
:arg displayCtx: The :class:`.DisplayContext`.
"""
self.__overlayList = overlayList
self.__displayCtx = displayCtx
@property
def overlayList(self):
"""Return a reference to the :class:`.OverlayList`. """
return self.__overlayList
@property
def displayCtx(self):
"""Return a reference to the :class:`.DisplayContext`. """
return self.__displayCtx
[docs] def destroy(self):
"""Must be called when this ``ActionProvider`` is no longer needed.
Calls the :meth:`Action.destroy` method of all ``Action`` instances.
"""
self.__overlayList = None
self.__displayCtx = None
for name, actionz in ActionProvider.getActions(self):
# Entries in getActions may be (None, None)
if actionz is None:
continue
# Or may be ('groupName', [('actionName', action), ...])
elif isinstance(actionz, list):
actionz = [a[1] for a in actionz]
# Or may be ('actionName', action)
else:
actionz = [actionz]
for a in actionz:
a.destroy()
[docs] def getAction(self, name):
"""Return the :class:`Action` instance with the specified name. """
return getattr(self, name)
[docs] def hasAction(self, name):
"""Return ``True`` if this ``ActionProvider`` has an action with the
given name, ``False`` otherwise
"""
return getattr(self, name, None) is not None
[docs] def enableAction(self, name, enable=True):
"""Enable/disable the named :class:`Action`. """
self.getAction(name).enabled = enable
[docs] def disableAction(self, name):
"""Disable the named :class:`Action`. """
self.enableAction(name, False)
[docs] def getActions(self):
"""Return a list containing the ``(name, Action)`` of all
:class:`Action` instances in this ``ActionProvider``.
Sub-classes may wish to override this method to enforce a specific
ordering of their actions.
.. note:: The list returned by this method may contain:
- Entries equal to ``(None, None)``. This is used as a
hint for GUIs which display action widgets/menu items to
indicate that a separator should be inserted.
- Entries equal to ``(name, [(name, Action), ...])``.
This is used as a hint for GUIs that the nested list
of actions should appear as a sub-menu with the
specified ``name``.
"""
acts = []
with warnings.catch_warnings():
warnings.simplefilter('ignore')
for name, attr in inspect.getmembers(self):
if isinstance(attr, Action):
acts.append((name, attr))
return acts
[docs]class ActionFactory(object):
"""The ``ActionFactory`` is used by the :func:`action` and
:func:`toggleAction` decorators. Its job is to create :class:`Action`
instances for :class:`ActionProvider` instances.
.. warning:: This class contains difficult-to-understand code. Read up
on decorators and descriptors before proceeding.
.. note:: This class has no use outside of this module, except for use
with custom :class:`.Action`/:class:`.ToggleAction` sub-classes
and corresponding decorators (along the lines of :func:`.action`
and :func:`.toggleAction`). A custom decorator simply needs
to return ``ActionFactory(CustomActionClass, *args, **kwargs)``,
where the ``*args`` and ``**kwargs`` are the arguments passed to
the :class:`Action` sub-class.
See the :func:`toggleControlAction` decorator for an example.
*Boring technical details*
Consider the following class::
class MyThing(ActionProvider):
@action
def myAction(self):
# do things here
The ``MyClass.myAction`` method has been marked as an action, using the
:func:`action` decorator. However, the :func:`action` decorator cannot
create an :class:`Action` instance at the point of class definition,
because this would lead to a single ``Action`` instance being shared by
multiple ``MyThing`` instances. We need to be able to create an ``Action``
instance for every ``MyThing`` instance, whilst still allowing the action
decorator to be used on class methods.
So when the :func:`action` or :func:`toggleAction` is used in a class
definition, an ``ActionFactory`` is created, and used as the decorator
of the unbound class method.
Later on, when the ``ActionFactory`` detects that it being is accessed
through an instance of the class (a ``MyThing`` instance in the example
above), it creates an :class:`Action` instance, and then replaces itself
with this ``Action`` instance - the ``Action`` instance becomes the
decorator of the bound method. This is possible because the
``ActionFactory`` is a descriptor - it uses the :meth:`__get__` method
so it can differentiate between class-level and instance-level accesses
of the decorated method.
The ``ActionFactory`` supports class-method decorators both with and
without arguments. While neither the :class:`.Action`, nor the
:class:`.ToggleAction` classes accept any optional arguments, this may be
useful for custom sub-classes, i.e.::
class MyThing(ActionProvider):
@action
def myAction(self):
# do things here
@customAction()
def myAction2(self):
# do things here
@otherCustomAction(arg1=8)
def myAction3(self):
# do things here
.. todo:: Merge/replace this class with the :class:`.memoize.Instanceify`
decorator.
"""
[docs] def __init__(self, actionType, *args, **kwargs):
"""Create an ``ActionFactory``.
:arg actionType: The action type (e.g. :class:`Action` or
:class:`ToggleAction`).
The remaining arguments may comprise a single callable object, (an
``@action`` style decorator was used) or a collection of arguments
passed to the decorator (an ``@action(...)`` style decorator was
used).
"""
self.__actionType = actionType
self.__args = args
self.__kwargs = kwargs
self.__func = None
# A no-brackets style
# decorator was used
if len(kwargs) == 0 and \
len(args) == 1 and \
isinstance(args[0], (types.FunctionType,
types.MethodType)):
self.__func = args[0]
self.__args = self.__args[1:]
[docs] def __call__(self, func=None):
"""If this ``ActionFactory`` was instantiated through a brackets-style
decorator (e.g. ``@action(arg1=1, arg2=2)``), this method is called
immediately after :meth:`__init__`, with a reference to the decorated
function. Otherwise, (an ``@action`` style decorator was used), this
method should never be called.
"""
if self.__func is not None:
log.warn('ActionFactory.__call__ was called, but function is '
'alreday set ({})! I\'m really confused.'.format(
self.__func.__name__))
self.__func = func
return self
[docs] def __get__(self, instance, cls):
"""When this ``ActionFactory`` is accessed through an instance,
an :class:`Action` instance is created. This ``ActionFactory`` is
then replaced by the ``Action`` instance.
If this ``ActionFactory`` is accessed through a class, the
encapsulated function is returned.
"""
# Class-level access
if instance is None:
return self.__func
else:
# Create an Action for the instance
action = self.__actionType(
instance.overlayList,
instance.displayCtx,
self.__func,
instance,
*self.__args,
**self.__kwargs)
# and replace this ActionFactory
# with the Action on the instance.
setattr(instance, self.__func.__name__, action)
return functools.update_wrapper(action, self.__func)