#
# base.py - The Action and ToggleAction classes.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides the :class:`Action`, :class:`NeedOverlayAction`, and
:class:`ToggleAction` classes. See the :mod:`.actions` package documentation
for more details.
"""
import logging
import fsl.data.image as fslimage
import fsleyes_props as props
import fsleyes_widgets as fwidgets
log = logging.getLogger(__name__)
[docs]class ActionDisabledError(Exception):
"""Exception raised when an attempt is made to call a disabled
:class:`Action`.
"""
[docs]class Action(props.HasProperties):
"""Represents an action of some sort. """
enabled = props.Boolean(default=True)
"""Controls whether the action is currently enabled or disabled.
When this property is ``False`` calls to the action will
result in a :exc:`ActionDisabledError`.
"""
[docs] def __init__(self,
overlayList,
displayCtx,
func,
instance=None,
name=None):
"""Create an ``Action``.
:arg overlayList: The :class:`.OverlayList`.
:arg displayCtx: The :class:`.DisplayContext` associated with this
``Action``; note that this is not necessarily the
master :class:`.DisplayContext`.
:arg func: The action function.
:arg instance: Object associated with the function, if this
``Action`` is encapsulating an instance method.
:arg name: Action name. Defaults to ``func.__name__``.
.. note:: If an ``Action`` encapsulates a method of an
:class:`.ActionProvider` instance, it is assumed that the
``name`` is the name of the method on the instance.
"""
if name is None:
name = func.__name__
self.__overlayList = overlayList
self.__displayCtx = displayCtx
self.__instance = instance
self.__func = func
self.__name = name
self.__boundWidgets = []
self.addListener('enabled',
'Action_{}_internal'.format(id(self)),
self.__enabledChanged)
[docs] def __str__(self):
"""Returns a string representation of this ``Action``. """
return '{}({})'.format(type(self).__name__, self.__name)
[docs] def __repr__(self):
"""Returns a string representation of this ``Action``. """
return self.__str__()
[docs] def name(self):
"""Returns the name of this ``Action``. """
return self.__name
@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
@property
def instance(self):
"""Return the instance which owns this ``Action``, if relevant.
Returns ``None`` otherwise.
"""
return self.__instance
[docs] def __call__(self, *args, **kwargs):
"""Calls this action. An :exc:`ActionDisabledError` will be raised
if :attr:`enabled` is ``False``.
"""
if not self.enabled:
raise ActionDisabledError('Action {} is disabled'.format(
self.__name))
log.debug('Action {}.{} called'.format(
type(self.__instance).__name__,
self.__name))
if self.__instance is not None:
args = [self.__instance] + list(args)
return self.__func(*args, **kwargs)
[docs] def destroy(self):
"""Must be called when this ``Action`` is no longer needed. """
self.unbindAllWidgets()
self.__overlayList = None
self.__displayCtx = None
self.__func = None
self.__instance = None
def __unbindWidget(self, index):
"""Unbinds the widget at the specified index into the
``__boundWidgets`` list. Does not remove it from the list.
"""
bw = self.__boundWidgets[index]
# Only attempt to unbind if the parent
# and widget have not been destroyed
if bw.isAlive():
bw.parent.Unbind(bw.evType, source=bw.widget)
def __enabledChanged(self, *args):
"""Internal method which is called when the :attr:`enabled` property
changes. Enables/disables any bound widgets.
"""
for bw in self.__boundWidgets:
# The widget may have been destroyed,
# so check before trying to access it
if bw.isAlive(): bw.widget.Enable(self.enabled)
else: self.unbindWidget(bw.widget)
[docs]class ToggleAction(Action):
"""A ``ToggleAction`` an ``Action`` which is intended to encapsulate
actions that toggle some sort of state. For example, a ``ToggleAction``
could be used to encapsulate an action which opens and/or closes a dialog
window.
"""
toggled = props.Boolean(default=False)
"""Boolean which tracks the current state of the ``ToggleAction``. """
[docs] def __init__(self, *args, **kwargs):
"""Create a ``ToggleAction``. All arguments are passed to
:meth:`Action.__init__`.
"""
Action.__init__(self, *args, **kwargs)
self.addListener('toggled',
'ToggleAction_{}_internal'.format(id(self)),
self.__toggledChanged)
[docs] def __call__(self, *args, **kwargs):
"""Call this ``ToggleAction``. The value of the :attr:`toggled` property
is flipped.
"""
# Copy the toggled value before running
# the action, in case it gets inadvertently
# changed
toggled = self.toggled
result = Action.__call__(self, *args, **kwargs)
self.toggled = not toggled
return result
def __setState(self, widget):
"""Sets the toggled state of the given widget to the current value of
:attr:`toggled`.
"""
import wx
import fsleyes_widgets.bitmaptoggle as bmptoggle
if isinstance(widget, wx.MenuItem):
widget.Check(self.toggled)
elif isinstance(widget, (wx.CheckBox,
wx.ToggleButton,
bmptoggle.BitmapToggleButton)):
widget.SetValue(self.toggled)
def __toggledChanged(self, *a):
"""Internal method called when :attr:`toggled` changes. Updates the
state of any bound widgets.
"""
for bw in list(self.getBoundWidgets()):
# An error will be raised if a widget
# has been destroyed, so we'll unbind
# any widgets which no longer exist.
try:
if not bw.isAlive():
raise Exception()
self.__setState(bw.widget)
except Exception:
self.unbindWidget(bw.widget)
[docs]class NeedOverlayAction(Action):
"""The ``NeedOverlayAction`` is a convenience base class for actions
which can only be executed when an overlay of a specific type is selected.
It enables/disables itself based on the type of the currently selected
overlay.
"""
[docs] def __init__(self,
overlayList,
displayCtx,
func=None,
overlayType=fslimage.Image):
"""Create a ``NeedOverlayAction``.
:arg overlayList: The :class:`.OverlayList`.
:arg displayCtx: The :class:`.DisplayContext`.
:arg func: The action function
:arg overlayType: The required overlay type (defaults to :class:`.Image`)
"""
Action.__init__(self, overlayList, displayCtx, func)
self.__overlayType = overlayType
self.__name = 'NeedOverlayAction_{}_{}'.format(
type(self).__name__, id(self))
displayCtx .addListener('selectedOverlay',
self.__name,
self.__selectedOverlayChanged)
overlayList.addListener('overlays',
self.__name,
self.__selectedOverlayChanged)
self.__selectedOverlayChanged()
[docs] 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)
Action.destroy(self)
def __selectedOverlayChanged(self, *a):
"""Called when the selected overlay, or overlay list, changes.
Enables/disables this action depending on the nature of the selected
overlay.
"""
ovl = self.displayCtx.getSelectedOverlay()
ovlType = self.__overlayType
self.enabled = (ovl is not None) and isinstance(ovl, ovlType)