From df52583501b80dd4b7ab89e314ec73a0bdd7b50a Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauld.mccarthy@gmail.com>
Date: Tue, 17 Nov 2015 13:58:39 +0000
Subject: [PATCH] All action-related code (I think) updated to work with new
 Action decorator design. Need to iron out some class-hierarchy related bugs.

---
 fsl/data/strings.py                           |   6 +-
 fsl/fsleyes/actions/__init__.py               |  13 +-
 fsl/fsleyes/actions/loadcolourmap.py          |  10 +-
 fsl/fsleyes/controls/lightboxtoolbar.py       |  31 ++---
 fsl/fsleyes/controls/orthotoolbar.py          |  49 ++++----
 fsl/fsleyes/controls/overlaydisplaypanel.py   |   3 +-
 fsl/fsleyes/controls/overlaydisplaytoolbar.py |   9 +-
 fsl/fsleyes/displaycontext/display.py         |  12 +-
 fsl/fsleyes/displaycontext/volumeopts.py      |   4 +-
 fsl/fsleyes/frame.py                          |  18 +--
 fsl/fsleyes/panel.py                          |  20 ++--
 fsl/fsleyes/profiles/__init__.py              |  13 +-
 fsl/fsleyes/profiles/orthoeditprofile.py      |  49 ++++----
 fsl/fsleyes/profiles/orthoviewprofile.py      |  29 ++---
 fsl/fsleyes/toolbar.py                        |   5 +-
 fsl/fsleyes/tooltips.py                       |  10 +-
 fsl/fsleyes/views/canvaspanel.py              | 112 +++++++++---------
 fsl/fsleyes/views/histogrampanel.py           |  31 ++---
 fsl/fsleyes/views/lightboxpanel.py            |  30 +++--
 fsl/fsleyes/views/orthopanel.py               |  31 +++--
 fsl/fsleyes/views/plotpanel.py                |  20 ++--
 fsl/fsleyes/views/powerspectrumpanel.py       |  32 ++---
 fsl/fsleyes/views/timeseriespanel.py          |  33 +++---
 fsl/fsleyes/views/viewpanel.py                |   5 +-
 24 files changed, 279 insertions(+), 296 deletions(-)

diff --git a/fsl/data/strings.py b/fsl/data/strings.py
index 04bdea3a8..f095c5f3f 100644
--- a/fsl/data/strings.py
+++ b/fsl/data/strings.py
@@ -219,9 +219,9 @@ actions = TypeDict({
     'OrthoPanel.toggleOrthoToolBar'     : 'View properties',
     'OrthoPanel.toggleEditToolBar'      : 'Edit toolbar',
 
-    'OrthoToolBar.more'           : 'More settings',
-    'LightBoxToolBar.more'        : 'More settings',
-    'OverlayDisplayToolBar.more'  : 'More settings',
+    'OrthoToolBar.showMoreSettings'          : 'More settings',
+    'LightBoxToolBar.showMoreSettings'       : 'More settings',
+    'OverlayDisplayToolBar.showMoreSettings' : 'More settings',
     
     'LightBoxPanel.toggleLightBoxToolBar' : 'View properties',
 
diff --git a/fsl/fsleyes/actions/__init__.py b/fsl/fsleyes/actions/__init__.py
index 57fb9f7aa..e739a542f 100644
--- a/fsl/fsleyes/actions/__init__.py
+++ b/fsl/fsleyes/actions/__init__.py
@@ -18,6 +18,7 @@ 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
@@ -57,7 +58,6 @@ def listGlobalActions():
             saveoverlay .SaveOverlayAction]
 
 
-
 class ActionButton(props.Button):
     """Extends the :class:`props.Button` class to encapsulate an
     :class:`Action` instance.
@@ -73,7 +73,7 @@ class ActionButton(props.Button):
         :arg kwargs:     Passed to the :class:`props.Button` constructor.
         """
 
-        self.name = actionName
+        self.__name = actionName
 
         if classType is not None:
             text = strings.actions.get((classType, actionName), actionName)
@@ -94,13 +94,14 @@ class ActionButton(props.Button):
         ``Action`` instance.
         """
         import wx
-        instance.getAction(self.name).bindToWidget(
+        instance.getAction(self.__name).bindToWidget(
             parent, wx.EVT_BUTTON, widget)
 
         
     def __onButton(self, instance, *a):
         """Called when the button is pushed. Runs the action."""
-        instance.getAction(self.name)()
+        instance.getAction(self.__name)()
+
 
 
 class ActionDisabledError(Exception):
@@ -152,7 +153,9 @@ class Action(props.HasProperties):
             raise ActionDisabledError('Action {} is disabled'.format(
                 self.__name))
 
-        log.debug('Action {} called'.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)
diff --git a/fsl/fsleyes/actions/loadcolourmap.py b/fsl/fsleyes/actions/loadcolourmap.py
index 9b5800c97..1a389baad 100644
--- a/fsl/fsleyes/actions/loadcolourmap.py
+++ b/fsl/fsleyes/actions/loadcolourmap.py
@@ -23,15 +23,19 @@ _stringID = 'actions.loadcolourmap.'
 
 
 class LoadColourMapAction(actions.Action):
-    """The ``LoadColourMapAction`` allows the yser to select a colour
+    """The ``LoadColourMapAction`` allows the user to select a colour
     map file and give it a name.
 
     The loaded colour map is then registered with the :mod:`.colourmaps`
     module. The user is also given the option to permanently save the
     loaded colour map in *FSLeyes*.
     """
-    
-    def doAction(self):
+
+    def __init__(self):
+        actions.Action.__init__(self, self.__loadColourMap)
+
+        
+    def __loadColourMap(self):
         """This method does the following:
 
         1. Prompts the user to select a colour map file
diff --git a/fsl/fsleyes/controls/lightboxtoolbar.py b/fsl/fsleyes/controls/lightboxtoolbar.py
index fc1d5e3b4..fe27deea0 100644
--- a/fsl/fsleyes/controls/lightboxtoolbar.py
+++ b/fsl/fsleyes/controls/lightboxtoolbar.py
@@ -47,18 +47,17 @@ class LightBoxToolBar(fsltoolbar.FSLEyesToolBar):
         :arg lb:          The :class:`.LightBoxPanel` instance.
         """
 
-        actionz = {'more' : self.showMoreSettings}
-        
         fsltoolbar.FSLEyesToolBar.__init__(
-            self, parent, overlayList, displayCtx, 24, actionz)
+            self, parent, overlayList, displayCtx, 24)
+        
         self.lightBoxPanel = lb
 
         lbOpts = lb.getSceneOptions()
         
         icons = {
-            'screenshot'  : fslicons.findImageFile('camera24'),
-            'movieMode'   : fslicons.findImageFile('movie24'),
-            'more'        : fslicons.findImageFile('gear24'),
+            'screenshot'       : fslicons.findImageFile('camera24'),
+            'movieMode'        : fslicons.findImageFile('movie24'),
+            'showMoreSettings' : fslicons.findImageFile('gear24'),
 
             'zax' : {
                 0 : fslicons.findImageFile('sagittalSlice24'),
@@ -68,8 +67,7 @@ class LightBoxToolBar(fsltoolbar.FSLEyesToolBar):
         }
 
         tooltips = {
-
-            'more'         : fsltooltips.actions[   self,    'more'],
+            
             'screenshot'   : fsltooltips.actions[   lb,      'screenshot'],
             'movieMode'    : fsltooltips.properties[lb,      'movieMode'],
             'zax'          : fsltooltips.properties[lbOpts,  'zax'],
@@ -77,7 +75,9 @@ class LightBoxToolBar(fsltoolbar.FSLEyesToolBar):
             'zrange'       : fsltooltips.properties[lbOpts,  'zrange'],
             'zoom'         : fsltooltips.properties[lbOpts,  'zoom'],
             'displaySpace' : fsltooltips.properties[displayCtx,
-                                                    'displaySpace']
+                                                    'displaySpace'],
+            
+            'showMoreSettings' : fsltooltips.actions[self, 'showMoreSettings'],
         }
 
         def displaySpaceOptionName(opt):
@@ -89,10 +89,10 @@ class LightBoxToolBar(fsltoolbar.FSLEyesToolBar):
         
         specs = {
             
-            'more'       : actions.ActionButton(
-                'more',
-                icon=icons['more'],
-                tooltip=tooltips['more']),
+            'showMoreSettings' : actions.ActionButton(
+                'showMoreSettings',
+                icon=icons['showMoreSettings'],
+                tooltip=tooltips['showMoreSettings']),
             'screenshot' : actions.ActionButton(
                 'screenshot',
                 icon=icons['screenshot'],
@@ -139,7 +139,7 @@ class LightBoxToolBar(fsltoolbar.FSLEyesToolBar):
         sizer = wx.FlexGridSizer(2, 2)
         panel.SetSizer(sizer)
 
-        more         = props.buildGUI(self,  self,       specs['more'])
+        more         = props.buildGUI(self,  self, specs['showMoreSettings'])
         screenshot   = props.buildGUI(self,  lb,         specs['screenshot'])
         movieMode    = props.buildGUI(self,  lb,         specs['movieMode'])
         zax          = props.buildGUI(self,  lbOpts,     specs['zax'])
@@ -166,7 +166,8 @@ class LightBoxToolBar(fsltoolbar.FSLEyesToolBar):
         
         self.SetTools(tools) 
 
-        
+
+    @actions.ToggleAction
     def showMoreSettings(self, *a):
         """Opens a :class:`.CanvasSettingsPanel` for the
         :class:`.LightBoxPanel` that owns this ``LightBoxToolBar``.
diff --git a/fsl/fsleyes/controls/orthotoolbar.py b/fsl/fsleyes/controls/orthotoolbar.py
index 769193953..9ca8e7732 100644
--- a/fsl/fsleyes/controls/orthotoolbar.py
+++ b/fsl/fsleyes/controls/orthotoolbar.py
@@ -55,10 +55,8 @@ class OrthoToolBar(fsltoolbar.FSLEyesToolBar):
         :arg ortho:       The :class:`.OrthoPanel` instance.
         """ 
 
-        actionz = {'more' : self.showMoreSettings}
-        
         fsltoolbar.FSLEyesToolBar.__init__(
-            self, parent, overlayList, displayCtx, 24, actionz)
+            self, parent, overlayList, displayCtx, 24)
         
         self.orthoPanel = ortho
 
@@ -86,12 +84,12 @@ class OrthoToolBar(fsltoolbar.FSLEyesToolBar):
         profile   = ortho.getCurrentProfile()
 
         icons = {
-            'screenshot'  : fslicons.findImageFile('camera24'),
-            'movieMode'   : fslicons.findImageFile('movie24'),
-            'showXCanvas' : fslicons.findImageFile('sagittalSlice24'),
-            'showYCanvas' : fslicons.findImageFile('coronalSlice24'),
-            'showZCanvas' : fslicons.findImageFile('axialSlice24'),
-            'more'        : fslicons.findImageFile('gear24'),
+            'screenshot'       : fslicons.findImageFile('camera24'),
+            'movieMode'        : fslicons.findImageFile('movie24'),
+            'showXCanvas'      : fslicons.findImageFile('sagittalSlice24'),
+            'showYCanvas'      : fslicons.findImageFile('coronalSlice24'),
+            'showZCanvas'      : fslicons.findImageFile('axialSlice24'),
+            'showMoreSettings' : fslicons.findImageFile('gear24'),
 
             'resetZoom'    : fslicons.findImageFile('resetZoom24'),
             'centreCursor' : fslicons.findImageFile('centre24'),
@@ -114,21 +112,21 @@ class OrthoToolBar(fsltoolbar.FSLEyesToolBar):
             'displaySpace' : fsltooltips.properties[dctx,      'displaySpace'],
             'resetZoom'    : fsltooltips.actions[   profile,   'resetZoom'],
             'centreCursor' : fsltooltips.actions[   profile,   'centreCursor'],
-            'more'         : fsltooltips.actions[   self,      'more'],
+            'showMoreSettings' : fsltooltips.actions[self, 'showMoreSettings'],
             
         }
         
-        targets    = {'screenshot'   : ortho,
-                      'movieMode'    : ortho,
-                      'zoom'         : orthoOpts,
-                      'layout'       : orthoOpts,
-                      'showXCanvas'  : orthoOpts,
-                      'showYCanvas'  : orthoOpts,
-                      'showZCanvas'  : orthoOpts,
-                      'displaySpace' : dctx,
-                      'resetZoom'    : profile,
-                      'centreCursor' : profile,
-                      'more'         : self}
+        targets    = {'screenshot'       : ortho,
+                      'movieMode'        : ortho,
+                      'zoom'             : orthoOpts,
+                      'layout'           : orthoOpts,
+                      'showXCanvas'      : orthoOpts,
+                      'showYCanvas'      : orthoOpts,
+                      'showZCanvas'      : orthoOpts,
+                      'displaySpace'     : dctx,
+                      'resetZoom'        : profile,
+                      'centreCursor'     : profile,
+                      'showMoreSettings' : self}
 
         def displaySpaceOptionName(opt):
 
@@ -140,9 +138,9 @@ class OrthoToolBar(fsltoolbar.FSLEyesToolBar):
 
         toolSpecs = [
 
-            actions.ActionButton('more',
-                                 icon=icons['more'],
-                                 tooltip=tooltips['more']),
+            actions.ActionButton('showMoreSettings',
+                                 icon=icons['showMoreSettings'],
+                                 tooltip=tooltips['showMoreSettings']),
             actions.ActionButton('screenshot',
                                  icon=icons['screenshot'],
                                  tooltip=tooltips['screenshot']),
@@ -192,7 +190,8 @@ class OrthoToolBar(fsltoolbar.FSLEyesToolBar):
 
         self.SetTools(tools, destroy=True) 
 
-    
+
+    @actions.ToggleAction
     def showMoreSettings(self, *a):
         """Opens a :class:`.CanvasSettingsPanel` for the
         :class:`.OrthoPanel` that owns this ``OrthoToolBar``.
diff --git a/fsl/fsleyes/controls/overlaydisplaypanel.py b/fsl/fsleyes/controls/overlaydisplaypanel.py
index ff0cf0d2f..804b78c20 100644
--- a/fsl/fsleyes/controls/overlaydisplaypanel.py
+++ b/fsl/fsleyes/controls/overlaydisplaypanel.py
@@ -244,8 +244,7 @@ class OverlayDisplayPanel(fslpanel.FSLEyesPanel):
         overlays with a :attr:`.Display.overlayType`  of ``'volume'``.
         """
 
-        action = loadcmap.LoadColourMapAction(self._overlayList,
-                                              self._displayCtx)
+        action = loadcmap.LoadColourMapAction()
 
         button = wx.Button(self.__widgets)
         button.SetLabel(strings.labels[self, 'loadCmap'])
diff --git a/fsl/fsleyes/controls/overlaydisplaytoolbar.py b/fsl/fsleyes/controls/overlaydisplaytoolbar.py
index a0d57cd92..a5d5ec1de 100644
--- a/fsl/fsleyes/controls/overlaydisplaytoolbar.py
+++ b/fsl/fsleyes/controls/overlaydisplaytoolbar.py
@@ -78,11 +78,9 @@ class OverlayDisplayToolBar(fsltoolbar.FSLEyesToolBar):
         :arg viewPanel:   The :class:`.ViewPanel` which this
                           ``OverlayDisplayToolBar`` is owned by.
         """
-
-        actionz = {'more' : self.showMoreSettings}
         
         fsltoolbar.FSLEyesToolBar.__init__(
-            self, parent, overlayList, displayCtx, 24, actionz)
+            self, parent, overlayList, displayCtx, 24)
 
         self.__viewPanel      = viewPanel
         self.__currentOverlay = None
@@ -121,6 +119,7 @@ class OverlayDisplayToolBar(fsltoolbar.FSLEyesToolBar):
         fsltoolbar.FSLEyesToolBar.destroy(self)
 
 
+    @actions.ToggleAction
     def showMoreSettings(self, *a):
         """Shows/hides a :class:`.OverlayDisplayPanel` dialog. """
         self.__viewPanel.togglePanel(overlaydisplay.OverlayDisplayPanel,
@@ -164,9 +163,9 @@ class OverlayDisplayToolBar(fsltoolbar.FSLEyesToolBar):
             self,
             self,
             view=actions.ActionButton(
-                'more',
+                'showMoreSettings',
                 icon=icons.findImageFile('gear24'),
-                tooltip=fsltooltips.actions[self, 'more']))
+                tooltip=fsltooltips.actions[self, 'showMoreSettings']))
 
         tools.insert(0, more)
 
diff --git a/fsl/fsleyes/displaycontext/display.py b/fsl/fsleyes/displaycontext/display.py
index 35d418ef7..a2472c675 100644
--- a/fsl/fsleyes/displaycontext/display.py
+++ b/fsl/fsleyes/displaycontext/display.py
@@ -326,7 +326,7 @@ class Display(props.SyncableHasProperties):
         self.__restoreOldDisplayOpts()
 
 
-class DisplayOpts(actions.ActionProvider):
+class DisplayOpts(props.SyncableHasProperties, actions.ActionProvider):
     """The ``DisplayOpts`` class contains overlay type specific display
     settings. ``DisplayOpts`` instances are managed by :class:`Display`
     instances.
@@ -391,20 +391,14 @@ class DisplayOpts(actions.ActionProvider):
         
         :arg displayCtx:  A :class:`.DisplayContext` instance describing
                           how the overlays are to be displayed.
-        
-        :arg kwargs:      Passed through to the
-                          :class:`.ActionProvider` constructor.
         """
 
         nounbind = kwargs.get('nounbind', [])
         nounbind.append('bounds')
         kwargs['nounbind'] = nounbind
 
-        actions.ActionProvider.__init__(
-            self,
-            overlayList,
-            displayCtx,
-            **kwargs)
+        props.SyncableHasProperties.__init__(self, **kwargs)
+        actions.ActionProvider     .__init__(self)
         
         self.overlay     = overlay
         self.display     = display
diff --git a/fsl/fsleyes/displaycontext/volumeopts.py b/fsl/fsleyes/displaycontext/volumeopts.py
index 97813021c..729b23e61 100644
--- a/fsl/fsleyes/displaycontext/volumeopts.py
+++ b/fsl/fsleyes/displaycontext/volumeopts.py
@@ -78,6 +78,7 @@ import props
 
 import fsl.utils.transform    as transform
 import fsl.fsleyes.colourmaps as fslcm
+import fsl.fsleyes.actions    as actions
 import display                as fsldisplay
 
 
@@ -546,13 +547,11 @@ class VolumeOpts(ImageOpts):
         
         self.setConstraint('displayRange', 'minDistance', dMinDistance)
 
-        actionz = {'resetDisplayRange' : self.resetDisplayRange}
         ImageOpts.__init__(self,
                            overlay,
                            display,
                            overlayList,
                            displayCtx,
-                           actions=actionz,
                            **kwargs)
 
         # The displayRange property of every child VolumeOpts
@@ -580,6 +579,7 @@ class VolumeOpts(ImageOpts):
                            display.getSyncPropertyName('contrast'))
 
 
+    @actions.Action
     def resetDisplayRange(self):
         """Resets the display range to the data range."""
         self.displayRange.x = [self.dataMin, self.dataMax]
diff --git a/fsl/fsleyes/frame.py b/fsl/fsleyes/frame.py
index 4f51e483f..cc7259cae 100644
--- a/fsl/fsleyes/frame.py
+++ b/fsl/fsleyes/frame.py
@@ -254,11 +254,18 @@ class FSLEyesFrame(wx.Frame):
 
         self.__viewPanelMenus[panel] = submenu
 
-        for actionName, actionObj in actionz.items():
+        for actionName, actionObj in actionz:
+
+            if isinstance(actionObj, actions.ToggleAction):
+                kind = wx.ITEM_CHECK
+            else:
+                kind = wx.ITEM_NORMAL
             
             menuItem = menu.Append(
                 wx.ID_ANY,
-                strings.actions[panel, actionName])
+                strings.actions[panel, actionName],
+                kind=kind)
+            
             actionObj.bindToWidget(self, wx.EVT_MENU, menuItem)
 
         # Add a 'Close' action to
@@ -511,11 +518,8 @@ class FSLEyesFrame(wx.Frame):
             # Set up a default for ortho views
             # layout (this will hopefully eventually
             # be restored from a saved state)
-            import fsl.fsleyes.controls.overlaylistpanel as olp
-            import fsl.fsleyes.controls.locationpanel    as lop
-
-            viewPanel.togglePanel(olp.OverlayListPanel)
-            viewPanel.togglePanel(lop.LocationPanel)
+            viewPanel.toggleOverlayList()
+            viewPanel.toggleLocationPanel()
 
             
     def __makeMenuBar(self):
diff --git a/fsl/fsleyes/panel.py b/fsl/fsleyes/panel.py
index 2df231720..df9c3ee37 100644
--- a/fsl/fsleyes/panel.py
+++ b/fsl/fsleyes/panel.py
@@ -42,6 +42,8 @@ import logging
 
 import wx
 
+import props
+
 import actions
 import displaycontext
 
@@ -49,7 +51,7 @@ import displaycontext
 log = logging.getLogger(__name__)
 
 
-class _FSLEyesPanel(actions.ActionProvider):
+class _FSLEyesPanel(actions.ActionProvider, props.SyncableHasProperties):
     """The ``_FSLEyesPanel`` is the base class for the :class:`.FSLEyesPanel`
     and the :class:`.FSLEyesToolBar`.
 
@@ -72,22 +74,16 @@ class _FSLEyesPanel(actions.ActionProvider):
     """ 
 
     
-    def __init__(self,
-                 overlayList,
-                 displayCtx,
-                 actionz=None):
+    def __init__(self, overlayList, displayCtx):
         """Create a :class:`ViewPanel`.
 
         :arg overlayList: A :class:`.OverlayList` instance.
         
         :arg displayCtx:  A :class:`.DisplayContext` instance.
-
-        :arg actionz:     A dictionary containing ``{name -> function}``
-                          actions (see :class:`.ActionProvider`).
         """
         
-        actions.ActionProvider.__init__(
-            self, overlayList, displayCtx, actions=actionz)
+        actions.ActionProvider     .__init__(self)
+        props.SyncableHasProperties.__init__(self)
 
         if not isinstance(displayCtx, displaycontext.DisplayContext):
             raise TypeError(
@@ -151,6 +147,6 @@ class FSLEyesPanel(_FSLEyesPanel, wx.PyPanel):
     *FSLeyes*. See the :mod:`~fsl.fsleyes` documentation for more details.
     """
     
-    def __init__(self, parent, overlayList, displayCtx, actionz=None):
+    def __init__(self, parent, overlayList, displayCtx):
         wx.PyPanel.__init__(self, parent)
-        _FSLEyesPanel.__init__(self, overlayList, displayCtx, actionz)
+        _FSLEyesPanel.__init__(self, overlayList, displayCtx)
diff --git a/fsl/fsleyes/profiles/__init__.py b/fsl/fsleyes/profiles/__init__.py
index c72ab3f6e..c0e8cbc36 100644
--- a/fsl/fsleyes/profiles/__init__.py
+++ b/fsl/fsleyes/profiles/__init__.py
@@ -135,7 +135,7 @@ class ProfileManager(object):
         self.__currentProfile.register()
 
 
-class Profile(actions.ActionProvider):
+class Profile(props.SyncableHasProperties, actions.ActionProvider):
     """A :class:`Profile` class implements keyboard/mouse interaction behaviour
     for a :class:`.ViewPanel` instance.
 
@@ -234,8 +234,7 @@ class Profile(actions.ActionProvider):
                  viewPanel,
                  overlayList,
                  displayCtx,
-                 modes=None,
-                 actionz=None):
+                 modes=None):
         """Create a ``Profile`` instance.
 
         :arg viewPanel:   The :class:`.ViewPanel` instance for which this
@@ -251,14 +250,10 @@ class Profile(actions.ActionProvider):
         :arg modes:       A sequence of strings, containing the mode
                           identifiers for this profile. These are added as
                           options on the :attr:`mode` property.
-
-        :arg actionz:     A dictionary of ``{name : function}`` mappings 
-                          defining any actions provided by this instance; 
-                          see the :class:`.ActionProvider` class.
         """
 
-        actions.ActionProvider.__init__(
-            self, overlayList, displayCtx, actions=actionz)
+        actions.ActionProvider     .__init__(self)
+        props.SyncableHasProperties.__init__(self)
         
         self._viewPanel   = viewPanel
         self._overlayList = overlayList
diff --git a/fsl/fsleyes/profiles/orthoeditprofile.py b/fsl/fsleyes/profiles/orthoeditprofile.py
index 98463154c..817501fc1 100644
--- a/fsl/fsleyes/profiles/orthoeditprofile.py
+++ b/fsl/fsleyes/profiles/orthoeditprofile.py
@@ -17,6 +17,7 @@ import numpy                        as np
 import                                 props
 import fsl.data.image               as fslimage
 import fsl.data.strings             as strings
+import fsl.fsleyes.actions          as actions
 import fsl.fsleyes.editor.editor    as fsleditor
 import fsl.fsleyes.gl.annotations   as annotations
 
@@ -178,22 +179,14 @@ class OrthoEditProfile(orthoviewprofile.OrthoViewProfile):
         self.__selecting         = False
         self.__lastDist          = None
         self.__currentOverlay    = None
-        
-        actions = {
-            'undo'                    : self.undo,
-            'redo'                    : self.redo,
-            'fillSelection'           : self.fillSelection,
-            'clearSelection'          : self.clearSelection,
-            'createMaskFromSelection' : self.createMaskFromSelection,
-            'createROIFromSelection'  : self.createROIFromSelection}
+
 
         orthoviewprofile.OrthoViewProfile.__init__(
             self,
             viewPanel,
             overlayList,
             displayCtx,
-            ['sel', 'desel', 'selint'],
-            actions)
+            ['sel', 'desel', 'selint'])
 
         self.mode = 'sel'
 
@@ -283,8 +276,9 @@ class OrthoEditProfile(orthoviewprofile.OrthoViewProfile):
             
         orthoviewprofile.OrthoViewProfile.deregister(self)
 
-    
-    def clearSelection(self, *a):
+
+    @actions.Action
+    def clearSelection(self):
         """Clears the current selection. See :meth:`.Editor.clearSelection`.
         """
         
@@ -298,7 +292,8 @@ class OrthoEditProfile(orthoviewprofile.OrthoViewProfile):
         self._viewPanel.Refresh()
 
 
-    def fillSelection(self, *a):
+    @actions.Action
+    def fillSelection(self):
         """Fills the current selection with the :attr:`fillValue`. See
         :meth:`.Editor.fillSelection`.
         """
@@ -310,8 +305,9 @@ class OrthoEditProfile(orthoviewprofile.OrthoViewProfile):
         editor.fillSelection(self.fillValue)
         editor.getSelection().clearSelection()
 
-        
-    def createMaskFromSelection(self, *a):
+
+    @actions.Action
+    def createMaskFromSelection(self):
         """Creates a new mask :class:`.Image` from the current selection.
         See :meth:`.Editor.createMaskFromSelection`.
         """
@@ -320,8 +316,9 @@ class OrthoEditProfile(orthoviewprofile.OrthoViewProfile):
 
         self.__editors[self.__currentOverlay].createMaskFromSelection()
 
-    
-    def createROIFromSelection(self, *a):
+
+    @actions.Action
+    def createROIFromSelection(self):
         """Creates a new ROI :class:`.Image` from the current selection.
         See :meth:`.Editor.createROIFromSelection`.
         """ 
@@ -331,7 +328,8 @@ class OrthoEditProfile(orthoviewprofile.OrthoViewProfile):
         self.__editors[self.__currentOverlay].createROIFromSelection() 
 
 
-    def undo(self, *a):
+    @actions.Action
+    def undo(self):
         """Un-does the most recent change to the selection or to the
         :class:`.Image` data. See :meth:`.Editor.undo`.
         """
@@ -360,7 +358,8 @@ class OrthoEditProfile(orthoviewprofile.OrthoViewProfile):
         self._viewPanel.Refresh()
 
 
-    def redo(self, *a):
+    @actions.Action
+    def redo(self):
         """Re-does the most recent undone change to the selection or to the
         :class:`.Image` data. See :meth:`.Editor.redo`.
         """
@@ -395,8 +394,8 @@ class OrthoEditProfile(orthoviewprofile.OrthoViewProfile):
                       editor.canUndo,
                       editor.canRedo))
         
-        self.enable('undo', editor.canUndo)
-        self.enable('redo', editor.canRedo)
+        self.undo.enable = editor.canUndo
+        self.redo.enable = editor.canRedo
 
 
     def __selectionColoursChanged(self, *a):
@@ -604,10 +603,10 @@ class OrthoEditProfile(orthoviewprofile.OrthoViewProfile):
         #      start/end of a mouse drag?
         selSize   = selection.getSelectionSize()
 
-        self.enable('createMaskFromSelection', selSize > 0)
-        self.enable('createROIFromSelection',  selSize > 0)
-        self.enable('clearSelection',          selSize > 0)
-        self.enable('fillSelection',           selSize > 0)
+        self.createMaskFromSelection.enable = selSize > 0
+        self.createROIFromSelection .enable = selSize > 0
+        self.clearSelection         .enable = selSize > 0
+        self.fillSelection          .enable = selSize > 0
 
     
     def __getVoxelLocation(self, canvasPos):
diff --git a/fsl/fsleyes/profiles/orthoviewprofile.py b/fsl/fsleyes/profiles/orthoviewprofile.py
index 8554f2259..249a9b0eb 100644
--- a/fsl/fsleyes/profiles/orthoviewprofile.py
+++ b/fsl/fsleyes/profiles/orthoviewprofile.py
@@ -13,6 +13,7 @@ import logging
 import wx
 
 import fsl.fsleyes.profiles as profiles
+import fsl.fsleyes.actions  as actions
 
 
 log = logging.getLogger(__name__)
@@ -62,8 +63,7 @@ class OrthoViewProfile(profiles.Profile):
                  viewPanel,
                  overlayList,
                  displayCtx,
-                 extraModes=None,
-                 extraActions=None):
+                 extraModes=None):
         """Creates an :class:`OrthoViewProfile`, which can be registered
         with the given ``viewPanel``.
 
@@ -81,29 +81,18 @@ class OrthoViewProfile(profiles.Profile):
         
         :arg extraModes:   Extra modes to pass through to the
                            :class:`.Profile` constructor.
-        
-        :arg extraActions: Extra actions to pass through to the
-                           :class:`.Profile` constructor.
         """
 
-        if extraModes   is None: extraModes   = []
-        if extraActions is None: extraActions = {}
-
-        modes   = ['nav', 'pan', 'zoom', 'bricon']
-        actionz = {
-            'resetZoom'    : self.resetZoom,
-            'centreCursor' : self.centreCursor,
-        }
+        if extraModes is None:
+            extraModes = []
 
-        modes   = modes + extraModes
-        actionz = dict(actionz.items() + extraActions.items())
+        modes = ['nav', 'pan', 'zoom', 'bricon'] + extraModes
 
         profiles.Profile.__init__(self,
                                   viewPanel,
                                   overlayList,
                                   displayCtx,
-                                  modes,
-                                  actionz)
+                                  modes)
 
         self.__xcanvas = viewPanel.getXCanvas()
         self.__ycanvas = viewPanel.getYCanvas()
@@ -124,7 +113,8 @@ class OrthoViewProfile(profiles.Profile):
         return [self.__xcanvas, self.__ycanvas, self.__zcanvas]
 
 
-    def resetZoom(self, *a):
+    @actions.Action
+    def resetZoom(self):
         """Sets the :class:`.SceneOpts.zoom`, :class:`.OrthoOpts.xzoom`,
         :class:`.OrthoOpts.yzoom`,  and :class:`.OrthoOpts.zzoom` properties
         to 100%.
@@ -138,7 +128,8 @@ class OrthoViewProfile(profiles.Profile):
         opts.zzoom = 100
 
 
-    def centreCursor(self, *a):
+    @actions.Action
+    def centreCursor(self):
         """Sets the :attr:`.DisplayContext.location` to the centre of the
         :attr:`.DisplayContext.bounds`.
         """
diff --git a/fsl/fsleyes/toolbar.py b/fsl/fsleyes/toolbar.py
index a17da46ec..0bf20631d 100644
--- a/fsl/fsleyes/toolbar.py
+++ b/fsl/fsleyes/toolbar.py
@@ -58,8 +58,7 @@ class FSLEyesToolBar(fslpanel._FSLEyesPanel, wx.PyPanel):
                  parent,
                  overlayList,
                  displayCtx,
-                 height=32,
-                 actionz=None):
+                 height=32):
         """Create a ``FSLEyesToolBar``.
 
         :arg parent:      The :mod:`wx` parent object.
@@ -79,7 +78,7 @@ class FSLEyesToolBar(fslpanel._FSLEyesPanel, wx.PyPanel):
         """
         
         wx.PyPanel.__init__(self, parent)
-        fslpanel._FSLEyesPanel.__init__(self, overlayList, displayCtx, actionz)
+        fslpanel._FSLEyesPanel.__init__(self, overlayList, displayCtx)
 
         self.__tools      = []
         self.__index      = 0
diff --git a/fsl/fsleyes/tooltips.py b/fsl/fsleyes/tooltips.py
index 4f1d22e0b..ef34a2a5f 100644
--- a/fsl/fsleyes/tooltips.py
+++ b/fsl/fsleyes/tooltips.py
@@ -397,8 +397,12 @@ properties = TypeDict({
 actions = TypeDict({
     'CanvasPanel.screenshot'        : 'Take a screenshot of the current scene',
     
-    'OrthoToolBar.more'             : 'Show more view control settings',
-    'LightBoxToolBar.more'          : 'Show more view control settings',
+    'OrthoToolBar.showMoreSettings'          : 'Show more view '
+                                               'control settings',
+    'LightBoxToolBar.showMoreSettings'       : 'Show more view '
+                                               'control settings',
+    'OverlayDisplayToolBar.showMoreSettings' : 'Show more overlay '
+                                               'display settings',
 
     'OrthoViewProfile.resetZoom'    : 'Reset zoom level to 100%',
     'OrthoViewProfile.centreCursor' : 'Reset location to centre of scene',
@@ -416,8 +420,6 @@ actions = TypeDict({
 
     'VolumeOpts.resetDisplayRange' : 'Reset the display range '
                                      'to the data range.',
-    
-    'OverlayDisplayToolBar.more' : 'Show more overlay display settings.',
 })
 
 
diff --git a/fsl/fsleyes/views/canvaspanel.py b/fsl/fsleyes/views/canvaspanel.py
index 3822151e7..43515ca5e 100644
--- a/fsl/fsleyes/views/canvaspanel.py
+++ b/fsl/fsleyes/views/canvaspanel.py
@@ -12,7 +12,6 @@ class for all panels which display overlays using ``OpenGL``.
 import os
 import os.path as op
 import logging
-import collections
 
 import wx
 
@@ -26,6 +25,7 @@ import fsl.utils.dialog                                as fsldlg
 import fsl.utils.settings                              as fslsettings
 import fsl.data.image                                  as fslimage
 import fsl.data.strings                                as strings
+import fsl.fsleyes.actions                             as actions
 import fsl.fsleyes.displaycontext                      as displayctx
 import fsl.fsleyes.controls.overlaylistpanel           as overlaylistpanel
 import fsl.fsleyes.controls.overlayinfopanel           as overlayinfopanel
@@ -222,12 +222,7 @@ class CanvasPanel(viewpanel.ViewPanel):
     """
     
 
-    def __init__(self,
-                 parent,
-                 overlayList,
-                 displayCtx,
-                 sceneOpts,
-                 extraActions=None):
+    def __init__(self, parent, overlayList, displayCtx, sceneOpts):
         """Create a ``CanvasPanel``.
 
         :arg parent:       The :mod:`wx` parent object.
@@ -239,54 +234,9 @@ class CanvasPanel(viewpanel.ViewPanel):
         :arg sceneOpts:    A :class:`.SceneOpts` instance for this
                            ``CanvasPanel`` - must be created by
                            sub-classes.
-        
-        :arg extraActions: A dictionary of ``{name : function}`` mappings,
-                           containing extra actions defined by sub-classes.
         """
 
-        if extraActions is None:
-            extraActions = {}
-
-        actionz = [
-            ('screenshot',                self.screenshot),
-            ('showCommandLineArgs',       self.showCommandLineArgs),
-            ('toggleOverlayList',         lambda *a: self.togglePanel(
-                overlaylistpanel.OverlayListPanel,
-                location=wx.BOTTOM)),
-            ('toggleOverlayInfo',         lambda *a: self.togglePanel(
-                overlayinfopanel.OverlayInfoPanel,
-                location=wx.LEFT)), 
-            ('toggleAtlasPanel',          lambda *a: self.togglePanel(
-                atlaspanel.AtlasPanel,
-                location=wx.BOTTOM)),
-            ('toggleDisplayProperties',   lambda *a: self.togglePanel(
-                overlaydisplaytoolbar.OverlayDisplayToolBar,
-                viewPanel=self)),
-            ('toggleLocationPanel',       lambda *a: self.togglePanel(
-                locationpanel.LocationPanel,
-                location=wx.BOTTOM)),
-            ('toggleClusterPanel',        lambda *a: self.togglePanel(
-                clusterpanel.ClusterPanel,
-                location=wx.TOP)), 
-            ('toggleLookupTablePanel',    lambda *a: self.togglePanel(
-                lookuptablepanel.LookupTablePanel,
-                location=wx.TOP)),
-            ('toggleClassificationPanel', lambda *a: self.togglePanel(
-                melclasspanel.MelodicClassificationPanel,
-                location=wx.RIGHT))]
-
-        actionz += extraActions.items()
-
-        actionz += [
-            ('toggleShell', lambda *a: self.togglePanel(
-                shellpanel.ShellPanel,
-                self.getSceneOptions(),
-                location=wx.BOTTOM))]
-        
-        actionz = collections.OrderedDict(actionz)
-        
-        viewpanel.ViewPanel.__init__(
-            self, parent, overlayList, displayCtx, actionz)
+        viewpanel.ViewPanel.__init__(self, parent, overlayList, displayCtx)
 
         self.__opts = sceneOpts
         
@@ -353,21 +303,71 @@ class CanvasPanel(viewpanel.ViewPanel):
             
         viewpanel.ViewPanel.destroy(self)
 
-    
-    def screenshot(self, *a):
+
+    @actions.Action
+    def screenshot(self):
         """Takes a screenshot of the currently displayed scene on this
         ``CanvasPanel``. See the :func:`_screenshot` function.
         """
         _screenshot(self._overlayList, self._displayCtx, self)
 
 
-    def showCommandLineArgs(self, *a):
+    @actions.Action
+    def showCommandLineArgs(self):
         """Shows the command line arguments which can be used to re-create
         the currently displayed scene. See the :func:`_showCommandLineArgs`
         function.
         """
         _showCommandLineArgs(self._overlayList, self._displayCtx, self)
 
+        
+    @actions.ToggleAction
+    def toggleOverlayList(self):
+        self.togglePanel(overlaylistpanel.OverlayListPanel, location=wx.BOTTOM)
+
+    
+    @actions.ToggleAction
+    def toggleOverlayInfo(self):
+        self.togglePanel(overlayinfopanel.OverlayInfoPanel, location=wx.LEFT) 
+    
+
+    @actions.ToggleAction
+    def toggleAtlasPanel(self):
+        self.togglePanel(atlaspanel.AtlasPanel, location=wx.BOTTOM) 
+
+
+    @actions.ToggleAction
+    def toggleDisplayProperties(self):
+        self.togglePanel(overlaydisplaytoolbar.OverlayDisplayToolBar,
+                         viewPanel=self)
+        
+
+    @actions.ToggleAction
+    def toggleLocationPanel(self):
+        self.togglePanel(locationpanel.LocationPanel, location=wx.BOTTOM) 
+
+
+    @actions.ToggleAction
+    def toggleClusterPanel(self):
+        self.togglePanel(clusterpanel.ClusterPanel, location=wx.TOP) 
+
+
+    @actions.ToggleAction
+    def toggleLookupTablePanel(self):
+        self.togglePanel(lookuptablepanel.LookupTablePanel, location=wx.TOP)
+
+    @actions.ToggleAction
+    def toggleClassificationPanel(self):
+        self.togglePanel(melclasspanel.MelodicClassificationPanel,
+                         location=wx.RIGHT)
+
+
+    @actions.ToggleAction
+    def toggleShell(self):
+        self.togglePanel(shellpanel.ShellPanel,
+                         self.getSceneOptions(),
+                         location=wx.BOTTOM) 
+
 
     def getSceneOptions(self):
         """Returns the :class:`.SceneOpts` instance used by this
diff --git a/fsl/fsleyes/views/histogrampanel.py b/fsl/fsleyes/views/histogrampanel.py
index fcc9d63cc..051943283 100644
--- a/fsl/fsleyes/views/histogrampanel.py
+++ b/fsl/fsleyes/views/histogrampanel.py
@@ -20,6 +20,7 @@ import props
 import fsl.data.image                             as fslimage
 import fsl.data.strings                           as strings
 import fsl.utils.dialog                           as fsldlg
+import fsl.fsleyes.actions                        as actions
 import fsl.fsleyes.plotting.histogramseries       as histogramseries
 import fsl.fsleyes.controls.histogramcontrolpanel as histogramcontrolpanel
 import fsl.fsleyes.controls.plotlistpanel         as plotlistpanel
@@ -85,25 +86,14 @@ class HistogramPanel(plotpanel.OverlayPlotPanel):
         :arg displayCtx:  The :class:`.DisplayContext` instance.
         """
 
-        actionz = {
-            'toggleHistogramList'    : lambda *a: self.togglePanel(
-                plotlistpanel.PlotListPanel,
-                self,
-                location=wx.TOP),
-            'toggleHistogramControl' : lambda *a: self.togglePanel(
-                histogramcontrolpanel.HistogramControlPanel,
-                self,
-                location=wx.TOP) 
-        }
-
         plotpanel.OverlayPlotPanel.__init__(
-            self, parent, overlayList, displayCtx, actionz)
+            self, parent, overlayList, displayCtx)
 
         self.addListener('histType', self._name, self.draw)
 
         def addPanels():
-            self.run('toggleHistogramControl') 
-            self.run('toggleHistogramList')
+            self.toggleHistogramControl()
+            self.toggleHistogramList()
 
         wx.CallAfter(addPanels) 
 
@@ -117,7 +107,20 @@ class HistogramPanel(plotpanel.OverlayPlotPanel):
         
         plotpanel.OverlayPlotPanel.destroy(self)
 
+        
+    @actions.ToggleAction
+    def toggleHistogramList(self):
+        self.togglePanel(plotlistpanel.PlotListPanel, self, location=wx.TOP)
 
+        
+    @actions.ToggleAction
+    def toggleHistogramControl(self):
+        self.togglePanel(
+            histogramcontrolpanel.HistogramControlPanel,
+            self,
+            location=wx.TOP) 
+
+        
     def draw(self, *a):
         """Overrides :meth:`.PlotPanel.draw`. Passes some
         :class:`.HistogramSeries` instances to the
diff --git a/fsl/fsleyes/views/lightboxpanel.py b/fsl/fsleyes/views/lightboxpanel.py
index ebb620f3c..a86a390a0 100644
--- a/fsl/fsleyes/views/lightboxpanel.py
+++ b/fsl/fsleyes/views/lightboxpanel.py
@@ -15,12 +15,12 @@ import wx
 
 import numpy as np
 
-import fsl.utils.layout                           as fsllayout
-import fsl.fsleyes.gl.wxgllightboxcanvas          as lightboxcanvas
-import fsl.fsleyes.controls.lightboxtoolbar       as lightboxtoolbar
-import fsl.fsleyes.controls.overlaydisplaytoolbar as overlaydisplaytoolbar
-import fsl.fsleyes.displaycontext.lightboxopts    as lightboxopts
-import                                               canvaspanel
+import fsl.utils.layout                        as fsllayout
+import fsl.fsleyes.actions                     as actions
+import fsl.fsleyes.gl.wxgllightboxcanvas       as lightboxcanvas
+import fsl.fsleyes.controls.lightboxtoolbar    as lightboxtoolbar
+import fsl.fsleyes.displaycontext.lightboxopts as lightboxopts
+import                                            canvaspanel
 
 
 log = logging.getLogger(__name__)
@@ -73,17 +73,11 @@ class LightBoxPanel(canvaspanel.CanvasPanel):
 
         sceneOpts = lightboxopts.LightBoxOpts()
 
-        actionz = {
-            'toggleLightBoxToolBar' : lambda *a: self.togglePanel(
-                lightboxtoolbar.LightBoxToolBar, lb=self)
-        }
-
         canvaspanel.CanvasPanel.__init__(self,
                                          parent,
                                          overlayList,
                                          displayCtx,
-                                         sceneOpts,
-                                         actionz)
+                                         sceneOpts)
 
         self.__scrollbar = wx.ScrollBar(
             self.getCentrePanel(),
@@ -169,9 +163,8 @@ class LightBoxPanel(canvaspanel.CanvasPanel):
         # struggle if we add these toolbars
         # immediately, so we'll do it asynchronously
         def addToolbars():
-            self.togglePanel(overlaydisplaytoolbar.OverlayDisplayToolBar,
-                             viewPanel=self)
-            self.togglePanel(lightboxtoolbar.LightBoxToolBar, lb=self)
+            self.toggleDisplayProperties()
+            self.toggleLightBoxToolBar()
 
         wx.CallAfter(addToolbars)
 
@@ -192,6 +185,11 @@ class LightBoxPanel(canvaspanel.CanvasPanel):
         canvaspanel.CanvasPanel.destroy(self)
 
 
+    @actions.ToggleAction
+    def toggleLightBoxToolBar(self):
+        self.togglePanel(lightboxtoolbar.LightBoxToolBar, lb=self)
+
+
     def getGLCanvases(self):
         """Returns a list containing the :class:`.LightBoxCanvas` contained
         within this ``LightBoxPanel``.
diff --git a/fsl/fsleyes/views/orthopanel.py b/fsl/fsleyes/views/orthopanel.py
index 6d5aae394..d35276811 100644
--- a/fsl/fsleyes/views/orthopanel.py
+++ b/fsl/fsleyes/views/orthopanel.py
@@ -25,9 +25,9 @@ import fsl.data.strings                           as strings
 import fsl.data.constants                         as constants
 import fsl.utils.layout                           as fsllayout
 import fsl.fsleyes.gl                             as fslgl
+import fsl.fsleyes.actions                        as actions
 import fsl.fsleyes.colourmaps                     as colourmaps
 import fsl.fsleyes.gl.wxglslicecanvas             as slicecanvas
-import fsl.fsleyes.controls.overlaydisplaytoolbar as overlaydisplaytoolbar
 import fsl.fsleyes.controls.orthotoolbar          as orthotoolbar
 import fsl.fsleyes.controls.orthoedittoolbar      as orthoedittoolbar
 import fsl.fsleyes.displaycontext.orthoopts       as orthoopts
@@ -134,19 +134,11 @@ class OrthoPanel(canvaspanel.CanvasPanel):
 
         sceneOpts = orthoopts.OrthoOpts()
 
-        actionz = {
-            'toggleOrthoToolBar' : lambda *a: self.togglePanel(
-                orthotoolbar.OrthoToolBar, ortho=self),
-            'toggleEditToolBar' : lambda *a: self.togglePanel(
-                orthoedittoolbar.OrthoEditToolBar, ortho=self), 
-        }
-
         canvaspanel.CanvasPanel.__init__(self,
                                          parent,
                                          overlayList,
                                          displayCtx,
-                                         sceneOpts,
-                                         actionz)
+                                         sceneOpts)
 
         contentPanel = self.getContentPanel()
 
@@ -246,12 +238,9 @@ class OrthoPanel(canvaspanel.CanvasPanel):
         # struggle if we add these toolbars
         # immediately, so we'll do it asynchronously 
         def _addToolbars():
-            self.togglePanel(overlaydisplaytoolbar.OverlayDisplayToolBar,
-                             viewPanel=self)
-            self.togglePanel(orthotoolbar.OrthoToolBar,
-                             ortho=self) 
-            self.togglePanel(orthoedittoolbar.OrthoEditToolBar,
-                             ortho=self) 
+            self.toggleDisplayProperties()
+            self.toggleOrthoToolBar()
+            self.toggleEditToolBar()
 
         if addToolbars:
             wx.CallAfter(_addToolbars)
@@ -283,6 +272,16 @@ class OrthoPanel(canvaspanel.CanvasPanel):
 
         canvaspanel.CanvasPanel.destroy(self)
 
+
+    @actions.ToggleAction
+    def toggleOrthoToolBar(self):
+        self.togglePanel(orthotoolbar.OrthoToolBar, ortho=self)
+
+
+    @actions.ToggleAction
+    def toggleEditToolBar(self):
+        self.togglePanel(orthoedittoolbar.OrthoEditToolBar, ortho=self) 
+
             
     def getGLCanvases(self):
         """Returns all of the :class:`.SliceCanvas` instances contained
diff --git a/fsl/fsleyes/views/plotpanel.py b/fsl/fsleyes/views/plotpanel.py
index b3beeb332..11fcd5881 100644
--- a/fsl/fsleyes/views/plotpanel.py
+++ b/fsl/fsleyes/views/plotpanel.py
@@ -32,6 +32,7 @@ from matplotlib.backends.backend_wx    import NavigationToolbar2Wx
 import                           props
 import                           viewpanel
 import fsl.data.strings       as strings
+import fsl.fsleyes.actions    as actions
 import fsl.fsleyes.colourmaps as fslcm
 
 
@@ -166,7 +167,6 @@ class PlotPanel(viewpanel.ViewPanel):
                  parent,
                  overlayList,
                  displayCtx,
-                 actionz=None,
                  interactive=True):
         """Create a ``PlotPanel``.
 
@@ -176,21 +176,12 @@ class PlotPanel(viewpanel.ViewPanel):
         
         :arg displayCtx:  A :class:`.DisplayContext` instance.
         
-        :arg actionz:     A dictionary of ``{ name : function }``
-                          mappings, containing actions implemented by
-                          the sub-class.
-
         :arg interactive: If ``True`` (the default), the canvas is configured
                           so the user can pan/zoom the plot with the mouse.
         """
-        
-        if actionz is None:
-            actionz = {}
-
-        actionz = dict([('screenshot', self.screenshot)] + actionz.items())
-        
+         
         viewpanel.ViewPanel.__init__(
-            self, parent, overlayList, displayCtx, actionz)
+            self, parent, overlayList, displayCtx)
 
         figure = plt.Figure()
         axis   = figure.add_subplot(111)
@@ -302,7 +293,8 @@ class PlotPanel(viewpanel.ViewPanel):
             
         viewpanel.ViewPanel.destroy(self)
 
-    
+
+    @actions.Action
     def screenshot(self, *a):
         """Prompts the user  to select a file name, then saves a screenshot
         of the current plot.
@@ -333,11 +325,13 @@ class PlotPanel(viewpanel.ViewPanel):
                 wx.ICON_ERROR)
 
 
+    # @actions.Action
     def importDataSeries(self, *a):
         """Not implemented yet. Imports data series from a text file."""
         pass
 
 
+    # @actions.Action
     def exportDataSeries(self, *a):
         """Not implemented yet. Exports displayed data series to a text file.
         """
diff --git a/fsl/fsleyes/views/powerspectrumpanel.py b/fsl/fsleyes/views/powerspectrumpanel.py
index c9c618483..c7b5aba88 100644
--- a/fsl/fsleyes/views/powerspectrumpanel.py
+++ b/fsl/fsleyes/views/powerspectrumpanel.py
@@ -18,6 +18,7 @@ import numpy as np
 import props
 
 import                                                   plotpanel
+import fsl.fsleyes.actions                            as actions
 import fsl.fsleyes.plotting.powerspectrumseries       as psseries
 import fsl.fsleyes.controls.powerspectrumcontrolpanel as pscontrol
 import fsl.fsleyes.controls.plotlistpanel             as plotlistpanel
@@ -88,23 +89,11 @@ class PowerSpectrumPanel(plotpanel.OverlayPlotPanel):
         :arg overlayList: The :class:`.OverlayList`.
         :arg displayCtx:  The :class:`.DisplayContext`.
         """
-
-        actionz = {
-            'togglePowerSpectrumControl' : lambda *a: self.togglePanel(
-                pscontrol.PowerSpectrumControlPanel,
-                self,
-                location=wx.TOP),
-            'togglePowerSpectrumList' : lambda *a: self.togglePanel(
-                plotlistpanel.PlotListPanel,
-                self,
-                location=wx.TOP) 
-        }
         
         plotpanel.OverlayPlotPanel.__init__(self,
                                             parent,
                                             overlayList,
-                                            displayCtx,
-                                            actionz=actionz)
+                                            displayCtx)
 
 
         self.addListener('plotFrequencies', self._name, self.draw)
@@ -113,8 +102,8 @@ class PowerSpectrumPanel(plotpanel.OverlayPlotPanel):
                                 self.__plotMelodicICsChanged)
 
         def addPanels():
-            self.run('togglePowerSpectrumControl') 
-            self.run('togglePowerSpectrumList') 
+            self.togglePowerSpectrumControl()
+            self.togglePowerSpectrumList() 
 
         wx.CallAfter(addPanels)
         self.draw()
@@ -131,6 +120,19 @@ class PowerSpectrumPanel(plotpanel.OverlayPlotPanel):
         plotpanel.OverlayPlotPanel.destroy(self)
 
 
+    @actions.ToggleAction
+    def togglePowerSpectrumControl(self):
+        self.togglePanel(
+            pscontrol.PowerSpectrumControlPanel,
+            self,
+            location=wx.TOP)
+
+        
+    @actions.ToggleAction
+    def togglePowerSpectrumList(self):
+        self.togglePanel(plotlistpanel.PlotListPanel, self, location=wx.TOP) 
+
+
     def draw(self, *a):
         """Overrides :meth:`.PlotPanel.draw`. Draws some
         :class:`.PowerSpectrumSeries` using the
diff --git a/fsl/fsleyes/views/timeseriespanel.py b/fsl/fsleyes/views/timeseriespanel.py
index cc89346c1..d32776366 100644
--- a/fsl/fsleyes/views/timeseriespanel.py
+++ b/fsl/fsleyes/views/timeseriespanel.py
@@ -19,6 +19,7 @@ import                                                plotpanel
 import fsl.data.featimage                          as fslfeatimage
 import fsl.data.melodicimage                       as fslmelimage
 import fsl.data.image                              as fslimage
+import fsl.fsleyes.actions                         as actions
 import fsl.fsleyes.plotting                        as plotting
 import fsl.fsleyes.controls.timeseriescontrolpanel as timeseriescontrolpanel
 import fsl.fsleyes.controls.plotlistpanel          as plotlistpanel
@@ -136,19 +137,8 @@ class TimeSeriesPanel(plotpanel.OverlayPlotPanel):
         :arg displayCtx:  A :class:`.DisplayContext` instance.
         """
 
-        actionz = {
-            'toggleTimeSeriesList'    : lambda *a: self.togglePanel(
-                plotlistpanel.PlotListPanel,
-                self,
-                location=wx.TOP),
-            'toggleTimeSeriesControl' : lambda *a: self.togglePanel(
-                timeseriescontrolpanel.TimeSeriesControlPanel,
-                self,
-                location=wx.TOP) 
-        }
-
         plotpanel.OverlayPlotPanel.__init__(
-            self, parent, overlayList, displayCtx, actionz=actionz)
+            self, parent, overlayList, displayCtx)
 
         self.addListener('plotMode',  self._name, self.draw)
         self.addListener('usePixdim', self._name, self.draw)
@@ -157,8 +147,8 @@ class TimeSeriesPanel(plotpanel.OverlayPlotPanel):
                          self.__plotMelodicICsChanged)
 
         def addPanels():
-            self.run('toggleTimeSeriesControl') 
-            self.run('toggleTimeSeriesList') 
+            self.toggleTimeSeriesControl()
+            self.toggleTimeSeriesList()
 
         wx.CallAfter(addPanels)
         self.draw()
@@ -173,7 +163,20 @@ class TimeSeriesPanel(plotpanel.OverlayPlotPanel):
         self.removeListener('usePixdim',      self._name)
         self.removeListener('plotMelodicICs', self._name)
         
-        plotpanel.OverlayPlotPanel.destroy(self) 
+        plotpanel.OverlayPlotPanel.destroy(self)
+
+
+    @actions.ToggleAction
+    def toggleTimeSeriesList(self):
+        self.togglePanel(plotlistpanel.PlotListPanel, self, location=wx.TOP)
+
+        
+    @actions.ToggleAction
+    def toggleTimeSeriesControl(self):
+        self.togglePanel(
+            timeseriescontrolpanel.TimeSeriesControlPanel,
+            self,
+            location=wx.TOP) 
 
 
     def draw(self, *a):
diff --git a/fsl/fsleyes/views/viewpanel.py b/fsl/fsleyes/views/viewpanel.py
index 3637c5c08..f41493558 100644
--- a/fsl/fsleyes/views/viewpanel.py
+++ b/fsl/fsleyes/views/viewpanel.py
@@ -71,13 +71,12 @@ class ViewPanel(fslpanel.FSLEyesPanel):
     """The current interaction profile for this ``ViewPanel``. """
 
     
-    def __init__(self, parent, overlayList, displayCtx, actionz=None):
+    def __init__(self, parent, overlayList, displayCtx):
         """Create a ``ViewPanel``. All arguments are passed through to the
         :class:`.FSLEyesPanel` constructor.
         """
 
-        fslpanel.FSLEyesPanel.__init__(
-            self, parent, overlayList, displayCtx, actionz)
+        fslpanel.FSLEyesPanel.__init__(self, parent, overlayList, displayCtx)
 
         self.__profileManager = profiles.ProfileManager(
             self, overlayList, displayCtx)
-- 
GitLab