diff --git a/apidoc/conf.py b/apidoc/conf.py
index 6a29f73e2be4c18b46f83a15dfbd84d9448312ae..ff39e3029272a455f440c137aba81b4ac60c7857 100644
--- a/apidoc/conf.py
+++ b/apidoc/conf.py
@@ -63,10 +63,11 @@ copyright = u'{}, Paul McCarthy, FMRIB Centre'.format(date.year)
 # built documents.
 #
 # The short X.Y version.
-import fsl
-version = fsl.__version__
+import fsl.version as fslversion
+version = fslversion.__version__
+
 # The full version, including alpha/beta/rc tags.
-release = version
+release = '{} ({})'.format(version, fslversion.__vcs_version__)
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
@@ -277,7 +278,7 @@ texinfo_documents = [
 epub_title = u'FSLpy'
 epub_author = u'Paul McCarthy'
 epub_publisher = u'Paul McCarthy'
-epub_copyright = u'{}, Paul McCarthy'.format(date.year)
+epub_copyright = u'{}, Paul McCarthy, FMRIB Centre, Oxford'.format(date.year)
 
 # The basename for the epub file. It defaults to the project name.
 #epub_basename = u'FSLpy'
diff --git a/apidoc/images/x11_slicecanvas_show_bug.png b/apidoc/images/x11_slicecanvas_show_bug.png
new file mode 100644
index 0000000000000000000000000000000000000000..6dbc92468d1b47814e384ffbee4cc1a3352018c6
Binary files /dev/null and b/apidoc/images/x11_slicecanvas_show_bug.png differ
diff --git a/fsl/__init__.py b/fsl/__init__.py
index a91349f8ac761c39bdc92728805fe17151cc4f82..d80e672baeaf78374a52a522603e8d4d5d981a6b 100644
--- a/fsl/__init__.py
+++ b/fsl/__init__.py
@@ -60,7 +60,6 @@ import importlib
 import threading
 import subprocess
 
-
 import fsl.tools          as tools
 import fsl.utils.settings as fslsettings
 
@@ -82,75 +81,126 @@ def main(args=None):
     :arg args: Command line arguments. If not provided, ``sys.argv`` is used.
     """
 
-    # Search the environment for FSLDIR 
-    fsldir       = os.environ.get('FSLDIR', None)
-    fslEnvActive = fsldir is not None
-
     if args is None:
         args = sys.argv[1:]
 
-    allTools                = _getFSLToolNames()
-    fslTool, args, toolArgs = _parseArgs(args, allTools)
+    # Get a list of all available tools, 
+    # and parse the top-level arguments
+    allTools                     = _getFSLToolNames()
+    fslTool, namespace, toolArgv = _parseTopLevelArgs(args, allTools)
 
-    # Is this a GUI tool?
-    if fslTool.interface is not None:
+    # GUI or command-line tool?
+    if fslTool.interface is not None: _runGUITool(fslTool, toolArgv)
+    else:                             _runCLITool(fslTool, toolArgv)
 
-        import wx
 
-        # The main interface is created on the
-        # wx.MainLoop, because it is difficult
-        # to force immediate GUI refreshes when
-        # not running on the main loop - this
-        # is important for, e.g. FSLEyes, which
-        # displays status updates to the user
-        # while it is loading overlays and
-        # setting up the interface.
-        # 
-        # To make this work, this buildGUI
-        # function is called on a separate thread
-        # (so it is executed after wx.MainLoop
-        # has been called), but it schedules its
-        # work to be done on the wx.MainLoop.
-        def buildGUI():
-            def realBuild():
-
-                if fslTool.context is not None: ctx = fslTool.context(toolArgs)
-                else:                           ctx = None
-
-                frame = _buildGUI(toolArgs, fslTool, ctx, fslEnvActive)
-                frame.Show()
-
-                # See comment below
-                dummyFrame.Destroy()
-
-                _fslDirWarning(frame, fslTool.toolName, fslEnvActive)
-
-                if args.wxinspect:
-                    import wx.lib.inspection
-                    wx.lib.inspection.InspectionTool().Show()
-
-            time.sleep(0.1)
-            wx.CallAfter(realBuild)
-
-        # Create the wx.App object, and create a dummy
-        # frame. If we don't create a dummy frame, the
-        # wx.MainLoop call will just return immediately.
-        # The buildGUI function above will kill the dummy
-        # frame when it has created the real interface.
-        app        = wx.App()
-        dummyFrame = wx.Frame(None)
-
-        threading.Thread(target=buildGUI).start()
-        app.MainLoop()
-
-    # Or is this a CLI tool?
-    elif fslTool.execute is not None:
-        
-        if fslTool.context is not None: ctx = fslTool.context(toolArgs)
-        else:                           ctx = None
+def _runGUITool(fslTool, toolArgv):
+    """Runs the given ``FSLTool``, which is assumed to be a GUI tool.
+
+    :arg fslTool:  The ``FSLTool`` to run - see the :func:`_loadFSLTool`
+                   function.
+
+    :arg toolArgv: Unparsed tool-specific command line arguments.
+    """
+    import wx
+
+    # Create the wx.App object befor fslTool.init,
+    # in case it does GUI stuff. Also create a dummy
+    # frame - if we don't create a dummy frame, the
+    # wx.MainLoop call below will just return
+    # immediately.
+    #
+    # The buildGUI function below will kill the dummy
+    # frame when it has created the real interface.
+    app        = wx.App()
+    dummyFrame = wx.Frame(None)
+
+    # Call the tool's init
+    # function if there is one
+    if fslTool.init is not None: initVal = fslTool.init()
+    else:                        initVal = None
+
+    # We are going do all processing on the
+    # wx.MainLoop, so the GUI can be shown
+    # as soon as possible, and because it is 
+    # difficult to force immediate GUI
+    # refreshes when not running on the main
+    # loop - this is important for, e.g.
+    # FSLEyes, which displays status updates
+    # to the user while it is loading overlays
+    # and setting up the interface.
+    # 
+    # To make this work, this buildGUI
+    # function is called on a separate thread
+    # (so it is executed after wx.MainLoop
+    # has been called), but it schedules its
+    # work to be done on the wx.MainLoop.
+    def buildGUI():
+        def realBuild():
+
+            # Parse the tool-specific
+            # command line arguments
+            toolNamespace = _parseToolArgs(fslTool, toolArgv)
+
+            # Call the tool context function
+            if fslTool.context is not None:
+                ctx = fslTool.context(toolNamespace, initVal)
+            else:
+                ctx = None
+
+            # Build the GUI
+            frame = _buildGUI(toolNamespace, fslTool, ctx)
+            frame.Show()
+
+            # See comment about the
+            # dummy frame below
+            dummyFrame.Destroy()
+
+        # Sleep a bit so the main thread (on
+        # which wx.MainLoop is running) can
+        # start.
+        time.sleep(0.01)
+        wx.CallAfter(_fslDirWarning,
+                     None,
+                     fslTool.toolName,
+                     'FSLDIR' in os.environ)
+        wx.CallAfter(realBuild)
+
+    threading.Thread(target=buildGUI).start()
+    app.MainLoop()
+
+
+def _runCLITool(fslTool, toolArgv):
+    """Runs the given ``FSLTool``, which is assumed to be a command-line (i.e.
+    non-GUI) tool.
+
+    :arg fslTool:  The ``FSLTool`` to run - see the :func:`_loadFSLTool`
+                   function.
+
+    :arg toolArgv: Unparsed tool-specific command line arguments.     
+    """
+
+    if fslTool.execute is None:
+        return
+
+    # Call the tool's init
+    # function if there is one
+    if fslTool.init is not None: initVal = fslTool.init()
+    else:                        initVal = None
+
+    # Parse the tool-specific
+    # command line arguments
+    namespace = _parseToolArgs(fslTool, toolArgv) 
+
+    initVal = None
+    ctx     = None
+    
+    if fslTool.init    is not None: initVal = fslTool.init()
+    if fslTool.context is not None: ctx     = fslTool.context(namespace,
+                                                              initVal)
         
-        _fslDirWarning(None, fslTool.toolName, fslEnvActive)
-        fslTool.execute(toolArgs, ctx)
+    _fslDirWarning(None, fslTool.toolName, 'FSLDIR' in os.environ)
+    fslTool.execute(namespace, ctx)
 
 
 def runTool(toolName, args, **kwargs):
@@ -204,7 +254,7 @@ def _getFSLToolNames():
     # directory, should work for both frozen and
     # unfrozen apps.
     if getattr(sys, 'frozen', False):
-        allTools = ['fsleyes', 'render', 'bet', 'flirt', 'feat']
+        allTools = ['fsleyes', 'render']
     else:
         allTools = [mod for _, mod, _ in pkgutil.iter_modules(tools.__path__)]
 
@@ -224,6 +274,7 @@ def _loadFSLTool(moduleName):
         ``moduleName`` The tool module name.
         ``toolName``   The tool name.
         ``helpPage``   The tool help page URL.
+        ``init``       An initialisation function.
         ``parseArgs``  A function to parse tool arguments.
         ``context``    A function to generate the tool context.
         ``interface``  A function to create the tool interface.
@@ -239,6 +290,7 @@ def _loadFSLTool(moduleName):
     # Each FSL tool module may specify several things
     toolName  = getattr(module, 'FSL_TOOLNAME',  None)
     helpPage  = getattr(module, 'FSL_HELPPAGE',  'index')
+    init      = getattr(module, 'FSL_INIT',      None)
     parseArgs = getattr(module, 'FSL_PARSEARGS', None)
     context   = getattr(module, 'FSL_CONTEXT',   None)
     interface = getattr(module, 'FSL_INTERFACE', None)
@@ -267,6 +319,7 @@ def _loadFSLTool(moduleName):
     fsltool.moduleName = moduleName
     fsltool.toolName   = toolName
     fsltool.helpPage   = helpPage
+    fsltool.init       = init
     fsltool.parseArgs  = parseArgs
     fsltool.context    = context
     fsltool.interface  = interface
@@ -276,20 +329,18 @@ def _loadFSLTool(moduleName):
     return fsltool
 
 
-def _parseArgs(argv, allTools):
-    """Creates a command line :class:`argparse.ArgumentParser` which will
-    process general arguments for ``fslpy`` and arguments for all FSL tools
-    which have defined their own command line arguments.
-
-    Parses the command line arguments, configures logging verbosity,  and
-    returns a tuple containing the following:
+def _parseTopLevelArgs(argv, allTools):
+    """Parses top-level command line arguments. This involves parsing arguments
+    which are shared across all tools.  Also identifies the tool to be invoked
+    and configures logging verbosity. Returns a tuple containing the
+    following:
     
       - The ``FSLTool`` instance (see :func:`_loadFSLTool`).
     
       - The :class:`argparse.Namespace` instance containing parsed arguments.
     
-      - The return value of the ``FSLTool.parseArgs`` function (see
-        :func:`_loadFSLTool`).
+      - All remaining unparsed command line arguments (to be passed to the
+        :func:`_parseToolArgs` function).
     """
 
     epilog = 'Type fslpy help <tool> for program-specific help. ' \
@@ -317,10 +368,6 @@ def _parseArgs(argv, allTools):
         '-m', '--memory', action='store_true',
         help='Output memory events (implied if -v is set)')
     
-    parser.add_argument(
-        '-w', '--wxinspect', action='store_true',
-        help='Run wx inspection tool')
-    
     parser.add_argument('tool', help='FSL program to run', nargs='?')
 
     # No arguments at all? 
@@ -353,7 +400,6 @@ def _parseArgs(argv, allTools):
     # the top level args
     fslArgv   = argv[:firstPos + 1]
     toolArgv  = argv[ firstPos + 1:]
-
     namespace = parser.parse_args(fslArgv)
 
     # Version number
@@ -449,14 +495,28 @@ def _parseArgs(argv, allTools):
     # things if its logging level has been
     # set to DEBUG, so we import it now so
     # it can set itself up.
-    import fsl.utils.trace
+    traceLogger = logging.getLogger('fsl.utils.trace')
+    if traceLogger.getEffectiveLevel() <= logging.DEBUG:
+        import fsl.utils.trace
 
     fslTool = _loadFSLTool(namespace.tool)
 
-    if fslTool.parseArgs is not None: toolArgs = fslTool.parseArgs(toolArgv)
-    else:                             toolArgs = None
+    return fslTool, namespace, toolArgv
+
+
+def _parseToolArgs(tool, argv):
+    """Parses tool-specific command-line arguments. Returns the result of
+    calling the ``FSL_PARSEARGS`` attribute of the given tool, or ``None``
+    if the tool does not have the function.
+
+    :arg tool:      The ``FSLTool`` to be invoked.
+    :arg argv:      Command line arguments to be parsed.
+    """
+
+    if tool.parseArgs is not None: toolNamespace = tool.parseArgs(argv)
+    else:                          toolNamespace = None
     
-    return fslTool, namespace, toolArgs
+    return toolNamespace
 
 
 def _fslDirWarning(parent, toolName, fslEnvActive):
@@ -515,16 +575,13 @@ def _fslDirWarning(parent, toolName, fslEnvActive):
         log.warn(warnmsg)
         
 
-def _buildGUI(args, fslTool, toolCtx, fslEnvActive):
+def _buildGUI(args, fslTool, toolCtx):
     """Builds a :mod:`wx` GUI for the tool.
 
     :arg fslTool:      The ``FSLTool`` instance (see :func:`_loadFSLTool`).
 
     :arg toolCtx:      The tool context, as returned by the 
                        ``FSLTool.context`` function.
-
-    :arg fslEnvActive: Set to ``True`` if ``$FSLDIR`` is set, ``False``
-                       otherwise. 
     """
 
     import wx
diff --git a/fsl/data/featimage.py b/fsl/data/featimage.py
index b18193e5b9c630a122fe9fe77a05c98f9815b10b..59cc9ce8929c1171d019dee99498d32fe8531138 100644
--- a/fsl/data/featimage.py
+++ b/fsl/data/featimage.py
@@ -286,8 +286,8 @@ class FEATImage(fslimage.Image):
         """
 
         if not fullmodel:
-            contrast  = np.array(contrast)
-            contrast /= np.sqrt((contrast ** 2).sum())
+            contrast = np.array(contrast)
+            contrast = contrast / np.sqrt((contrast ** 2).sum())
 
         x, y, z = xyz
         numEVs  = self.numEVs()
diff --git a/fsl/data/melodicresults.py b/fsl/data/melodicresults.py
index bcc60cb1449c332a92956e520f90e5fe7ab03127..d2254a3a2c8caa820079b00b2ac3cdd9083085c0 100644
--- a/fsl/data/melodicresults.py
+++ b/fsl/data/melodicresults.py
@@ -286,7 +286,7 @@ class MelodicClassification(props.HasProperties):
             labels = [self.getDisplayLabel(l) for l in self.labels[c]]
             allLabels.append(labels)
 
-        saveMelodicLabelFile(self.__melImage.getMelodicDir(),
+        saveMelodicLabelFile(self.__melimage.getMelodicDir(),
                              allLabels,
                              filename)
 
diff --git a/fsl/data/strings.py b/fsl/data/strings.py
index 11a892c760ae26c5dc069340a3041f877a607f39..b60c9d252983b9ac6522d2b00725ea89fd723d5d 100644
--- a/fsl/data/strings.py
+++ b/fsl/data/strings.py
@@ -912,6 +912,7 @@ about = {
     'email'      : 'paulmc@fmrib.ox.ac.uk',
     'company'    : u'\u00A9 FMRIB Centre, Oxford, UK',
     'version'    : 'FSLeyes version: {}',
+    'vcsVersion' : 'Internal version: {}',
     'glVersion'  : 'OpenGL version: {}',
     'glRenderer' : 'OpenGL renderer: {}',
     'software'   : textwrap.dedent(
diff --git a/fsl/fsleyes/about.py b/fsl/fsleyes/about.py
index 49a31bd675d0f9d11f41b359a6ce61009f033c05..23a3c7a0a645042a7c8628f8222ee1e12b26a071 100644
--- a/fsl/fsleyes/about.py
+++ b/fsl/fsleyes/about.py
@@ -38,43 +38,27 @@ class AboutDialog(wx.Dialog):
         splashimg       = splashbmp.ConvertToImage()
 
         # Create all the widgets
-        splashPanel     = imagepanel.ImagePanel(self, splashimg)
-        authorLabel     = wx.StaticText(self)
-        emailLabel      = wx.StaticText(self)
-        companyLabel    = wx.StaticText(self)
-        versionLabel    = wx.StaticText(self)
-        glVersionLabel  = wx.StaticText(self)
-        glRendererLabel = wx.StaticText(self)
-        softwareField   = wx.TextCtrl(  self,
-                                        size=(-1, 200),
-                                        style=(wx.TE_LEFT      |
-                                               wx.TE_RICH      | 
-                                               wx.TE_MULTILINE |
-                                               wx.TE_READONLY  |
-                                               wx.TE_AUTO_URL))
-        closeButton     = wx.Button(    self, id=wx.ID_CANCEL)
+        splashPanel = imagepanel.ImagePanel(self, splashimg)
+        textPanel   = wx.TextCtrl(self,
+                                  size=(-1, 200),
+                                  style=(wx.TE_LEFT      |
+                                         wx.TE_RICH      | 
+                                         wx.TE_MULTILINE |
+                                         wx.TE_READONLY  |
+                                         wx.TE_AUTO_URL))
+        closeButton = wx.Button(self, id=wx.ID_CANCEL)
 
         # Set foreground/background colours
-        objs = [self,
-                authorLabel,
-                emailLabel,
-                companyLabel,
-                versionLabel,
-                glVersionLabel,
-                glRendererLabel,
-                softwareField]
-
-        for obj in objs:
-            obj.SetBackgroundColour('#000000')
-            obj.SetForegroundColour('#ffffff')
-
-        softwareField.SetDefaultStyle(wx.TextAttr('#ffffff', wx.NullColour))
+        textPanel.SetBackgroundColour('#000000')
+        textPanel.SetForegroundColour('#ffffff')
+        textPanel.SetDefaultStyle(wx.TextAttr('#ffffff', wx.NullColour))
 
         # Create / retrieve all the content
-        verStr   = version.__version__
-        glVerStr = gl.glGetString(gl.GL_VERSION)
-        glRenStr = gl.glGetString(gl.GL_RENDERER)
-        swlibs   = strings.about['libs']
+        verStr    = version.__version__
+        vcsVerStr = version.__vcs_version__
+        glVerStr  = gl.glGetString(gl.GL_VERSION)
+        glRenStr  = gl.glGetString(gl.GL_RENDERER)
+        swlibs    = strings.about['libs']
 
         swVersions = []
         for lib in swlibs:
@@ -90,10 +74,11 @@ class AboutDialog(wx.Dialog):
             
             swVersions.append(swVer)
 
-        verStr   = strings.about['version']   .format(verStr)
-        glVerStr = strings.about['glVersion'] .format(glVerStr)
-        glRenStr = strings.about['glRenderer'].format(glRenStr)
-        swStr    = strings.about['software']  .format(*swVersions)
+        verStr    = strings.about['version']   .format(verStr)
+        vcsVerStr = strings.about['vcsVersion'].format(vcsVerStr)
+        glVerStr  = strings.about['glVersion'] .format(glVerStr)
+        glRenStr  = strings.about['glRenderer'].format(glRenStr)
+        swStr     = strings.about['software']  .format(*swVersions)
 
         # Tack the license file contents onto
         # the end of the software description.
@@ -109,47 +94,26 @@ class AboutDialog(wx.Dialog):
         swStr = swStr.strip()
 
         # Set the widget content
-        authorLabel    .SetLabel(strings.about['author'])
-        emailLabel     .SetLabel(strings.about['email'])
-        companyLabel   .SetLabel(strings.about['company'])
-        versionLabel   .SetLabel(verStr)
-        glVersionLabel .SetLabel(glVerStr)
-        glRendererLabel.SetLabel(glRenStr)
-        softwareField  .SetValue(swStr)
-        closeButton    .SetLabel('Close')
+        infoStr = '\n'.join((verStr,
+                             strings.about['company'],
+                             strings.about['author'],
+                             strings.about['email'],
+                             vcsVerStr,
+                             glVerStr,
+                             glRenStr))
 
-        # Arrange the widgets
-        mainSizer = wx.BoxSizer(wx.VERTICAL)
-        row1Sizer = wx.BoxSizer(wx.HORIZONTAL)
-        row2Sizer = wx.BoxSizer(wx.HORIZONTAL)
-        row3Sizer = wx.BoxSizer(wx.HORIZONTAL)
-        row4Sizer = wx.BoxSizer(wx.HORIZONTAL)
-
-        row1Sizer.Add(versionLabel,    flag=wx.EXPAND)
-        row1Sizer.Add((1, 1),          flag=wx.EXPAND, proportion=1)
-        row1Sizer.Add(authorLabel,     flag=wx.EXPAND)
- 
-        row2Sizer.Add(companyLabel,    flag=wx.EXPAND)
-        row2Sizer.Add((1, 1),          flag=wx.EXPAND, proportion=1)
-        row2Sizer.Add(emailLabel,      flag=wx.EXPAND)
-
-        row3Sizer.Add(glVersionLabel,  flag=wx.EXPAND)
-        row3Sizer.Add((1, 1),          flag=wx.EXPAND, proportion=1)
         
-        row4Sizer.Add(glRendererLabel, flag=wx.EXPAND)
-        row4Sizer.Add((1, 1),          flag=wx.EXPAND, proportion=1)
 
-        rowargs = {'border' : 3,
-                   'flag'   : wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM}
+        textPanel  .SetValue(infoStr + '\n\n' + swStr)
+        closeButton.SetLabel('Close')
+
+        # Arrange the widgets
+        sizer = wx.BoxSizer(wx.VERTICAL)
 
-        mainSizer.Add(splashPanel)
-        mainSizer.Add(row1Sizer,     **rowargs)
-        mainSizer.Add(row2Sizer,     **rowargs)
-        mainSizer.Add(row3Sizer,     **rowargs)
-        mainSizer.Add(row4Sizer,     **rowargs)
-        mainSizer.Add(softwareField, flag=wx.EXPAND, proportion=1)
-        mainSizer.Add(closeButton,   flag=wx.EXPAND)
+        sizer.Add(splashPanel)
+        sizer.Add(textPanel, flag=wx.EXPAND, proportion=1)
+        sizer.Add(closeButton, flag=wx.EXPAND)
 
-        self.SetSizer(mainSizer)
+        self.SetSizer(sizer)
         self.Layout()
         self.Fit()
diff --git a/fsl/fsleyes/actions/__init__.py b/fsl/fsleyes/actions/__init__.py
index 8d128acd88e40c084d5dcdf687ee13fe4b4c2e4e..f611bb0add0fb1ec9467fedd9345dc7d1e41f5ec 100644
--- a/fsl/fsleyes/actions/__init__.py
+++ b/fsl/fsleyes/actions/__init__.py
@@ -300,6 +300,10 @@ class ActionFactory(object):
             @otherCustomAction(arg1=8)
             def myAction3(self):
                 # do things here
+
+
+    .. todo:: Merge/replace this class with the :class:`.memoize.Instanceify`
+              decorator.
     """
 
     
diff --git a/fsl/fsleyes/actions/about.py b/fsl/fsleyes/actions/about.py
index 651e241ca32e421bbeb494a8fda9a2f14000a085..3ac176406023890f22522158ac3f01994a7afbae 100644
--- a/fsl/fsleyes/actions/about.py
+++ b/fsl/fsleyes/actions/about.py
@@ -9,8 +9,7 @@ displays an about dialog for *FSLeyes*.
 """
 
 
-import                      action
-import fsl.fsleyes.about as aboutdlg
+import action
 
 
 class AboutAction(action.Action):
@@ -36,6 +35,8 @@ class AboutAction(action.Action):
     def __showDialog(self):
         """Creates and shows an :class:`.AboutDialog`. """
 
+        import fsl.fsleyes.about as aboutdlg
+
         dlg = aboutdlg.AboutDialog(self.__frame)
         dlg.Show()
         dlg.CentreOnParent()
diff --git a/fsl/fsleyes/actions/diagnosticreport.py b/fsl/fsleyes/actions/diagnosticreport.py
index 33f54a6608960a02970bbcd697566d164a7bb311..56b08e81efc5e59faa697e58f10f2aedaa0ad667 100644
--- a/fsl/fsleyes/actions/diagnosticreport.py
+++ b/fsl/fsleyes/actions/diagnosticreport.py
@@ -104,14 +104,15 @@ class DiagnosticReportAction(action.Action):
                 ('name',   ovl.name),
                 ('source', ovl.dataSource)]))
 
-        report['Platform'] = platform.platform()
-        report['Python']   = '{}  {}'.format(platform.python_version(),
-                                             platform.python_compiler())
-        report['Version']  = version.__version__
-        report['OpenGL']   = self.__openGLReport()
-        report['Settings'] = self.__settingsReport()
-        report['Layout']   = perspectives.serialisePerspective(self.__frame)
-        report['Overlays'] = overlays
+        report['Platform']    = platform.platform()
+        report['Python']      = '{}  {}'.format(platform.python_version(),
+                                                platform.python_compiler())
+        report['Version']     = version.__version__
+        report['VCS Version'] = version.__vcs_version__
+        report['OpenGL']      = self.__openGLReport()
+        report['Settings']    = self.__settingsReport()
+        report['Layout']      = perspectives.serialisePerspective(self.__frame)
+        report['Overlays']    = overlays
 
         report['Master display context'] = self.__displayContextReport(
             self.__overlayList,
diff --git a/fsl/fsleyes/colourmaps.py b/fsl/fsleyes/colourmaps.py
index e8b5bdd7900d6ae9e23bfe7ad0e089929647974d..d8c234d8a645f893f48251a94334099dc2424f1b 100644
--- a/fsl/fsleyes/colourmaps.py
+++ b/fsl/fsleyes/colourmaps.py
@@ -372,13 +372,13 @@ def registerLookupTable(lut,
 
         lutChoice = opts.getProp('lut')
         lutChoice.addChoice(lut,
-                            alternate=[lut.name, key],
+                            alternate=list(set((lut.name, key))),
                             instance=opts)
 
     # and for any future label overlays
     fsldisplay.LabelOpts.lut.addChoice(
         lut,
-        alternate=[lut.name, key])
+        alternate=list(set((lut.name, key))))
     
     return lut
 
diff --git a/fsl/fsleyes/controls/atlasoverlaypanel.py b/fsl/fsleyes/controls/atlasoverlaypanel.py
index b2057b6b825cf12cbb1af574a1f1f030d83537a2..dd3859e4ba5064614c1fca159ae05b08d66b9308 100644
--- a/fsl/fsleyes/controls/atlasoverlaypanel.py
+++ b/fsl/fsleyes/controls/atlasoverlaypanel.py
@@ -244,12 +244,23 @@ class AtlasOverlayPanel(fslpanel.FSLEyesPanel):
 
             def addToRegionList(label, i):
 
-                regionList.Append(label.name)
-                widget = OverlayListWidget(regionList,
-                                           atlasDesc.atlasID,
-                                           self.__atlasPanel,
-                                           label.index)
-                regionList.SetItemWidget(i, widget)
+                # If the user kills this panel while
+                # the region list is being updated,
+                # suppress wx complaints.
+                #
+                # TODO You could make a chain of
+                # async.idle functions. instead of
+                # scheduling them all at once
+                try:
+                    regionList.Append(label.name)
+                    widget = OverlayListWidget(regionList,
+                                               atlasDesc.atlasID,
+                                               self.__atlasPanel,
+                                               label.index)
+                    regionList.SetItemWidget(i, widget)
+                    
+                except wx.PyDeadObjectError:
+                    pass
 
             log.debug('Creating region list for {} ({})'.format(
                 atlasDesc.atlasID, id(regionList)))
@@ -273,31 +284,38 @@ class AtlasOverlayPanel(fslpanel.FSLEyesPanel):
         # displayed before).
         def changeAtlasList():
 
-            filterStr = self.__regionFilter.GetValue().lower().strip()
-            regionList.ApplyFilter(filterStr, ignoreCase=True)
+            # See comment above about
+            # suppressing wx complaints
+            try:
 
-            self.__updateAtlasState(atlasIdx)
+                filterStr = self.__regionFilter.GetValue().lower().strip()
+                regionList.ApplyFilter(filterStr, ignoreCase=True)
 
-            status.update(strings.messages[self, 'regionsLoaded'].format(
-                atlasDesc.name))
-            log.debug('Showing region list for {} ({})'.format(
-                atlasDesc.atlasID, id(regionList)))
+                self.__updateAtlasState(atlasIdx)
+
+                status.update(strings.messages[self, 'regionsLoaded'].format(
+                    atlasDesc.name))
+                log.debug('Showing region list for {} ({})'.format(
+                    atlasDesc.atlasID, id(regionList)))
 
-            old = self.__regionSizer.GetItem(1).GetWindow()
+                old = self.__regionSizer.GetItem(1).GetWindow()
 
-            if old is not None:
-                old.Show(False)
+                if old is not None:
+                    old.Show(False)
 
-            regionList.Show(True)
-            self.__regionSizer.Remove(1)
+                regionList.Show(True)
+                self.__regionSizer.Remove(1)
 
-            self.__regionSizer.Insert(1,
-                                      regionList,
-                                      flag=wx.EXPAND,
-                                      proportion=1)
-            self.__regionSizer.Layout()
+                self.__regionSizer.Insert(1,
+                                          regionList,
+                                          flag=wx.EXPAND,
+                                          proportion=1)
+                self.__regionSizer.Layout()
 
-            self.Enable()
+                self.Enable()
+                
+            except wx.PyDeadObjectError:
+                pass
  
         async.idle(changeAtlasList)
 
diff --git a/fsl/fsleyes/controls/atlaspanel.py b/fsl/fsleyes/controls/atlaspanel.py
index c397bfef25d88022c1bd4e68973f1c304a54fa77..656bb740a3a61857f8729f3bb539fb432e05539f 100644
--- a/fsl/fsleyes/controls/atlaspanel.py
+++ b/fsl/fsleyes/controls/atlaspanel.py
@@ -192,6 +192,9 @@ class AtlasPanel(fslpanel.FSLEyesPanel):
         self.__enabledOverlays = None
         self.__infoPanel     .destroy()
         self.__overlayPanel  .destroy()
+        
+        self._overlayList.removeListener('overlays', self._name)
+        
         fslpanel.FSLEyesPanel.destroy(self)
 
 
diff --git a/fsl/fsleyes/controls/clusterpanel.py b/fsl/fsleyes/controls/clusterpanel.py
index 9e2a01f7785d343f2a361aff4255f6a3877acd20..c24ae0cc2b2979a3c1c52a97f18dbc03ec29803f 100644
--- a/fsl/fsleyes/controls/clusterpanel.py
+++ b/fsl/fsleyes/controls/clusterpanel.py
@@ -439,15 +439,26 @@ class ClusterPanel(fslpanel.FSLEyesPanel):
         # WidgetGrid panels for overlays
         # that have been removed from the
         # list.
-        for overlay in self.__featImages.keys():
+        for overlay in list(self.__featImages.keys()):
             if overlay not in self._overlayList:
                 
-                featImage = self.__featImages  .pop(overlay)
-                grids     = self.__clusterGrids.pop(featImage)
-                
-                for grid in grids:
-                    self.__mainSizer.Detach(grid)
-                    grid.Destroy()
+                featImage = self.__featImages.pop(overlay)
+
+                # Has the feat image associated with
+                # this overlay also been removed?
+                if featImage is overlay or \
+                   featImage not in self._overlayList:
+
+                    # The grid widgets for the feat image
+                    # associated with this overlay may
+                    # have already been destroyed.
+                    try:             grids = self.__clusterGrids.pop(featImage)
+                    except KeyError: grids = []
+
+                    for grid in grids:
+                        if grid is not None:
+                            self.__mainSizer.Detach(grid)
+                            grid.Destroy()
         
         self.__selectedOverlayChanged()
         self.__enableOverlayButtons()
@@ -493,6 +504,12 @@ class ClusterPanel(fslpanel.FSLEyesPanel):
             return
 
         overlay = self._displayCtx.getSelectedOverlay()
+
+        # Overlay is in-memory
+        if overlay.dataSource is None:
+            self.__disable(strings.messages[self, 'notFEAT'])
+            return
+        
         featDir = featresults.getAnalysisDir(overlay.dataSource)
 
         # No FEAT analysis, or not an Image,
diff --git a/fsl/fsleyes/controls/locationpanel.py b/fsl/fsleyes/controls/locationpanel.py
index c6ae6accac2ec859938e7c904411db7bcacb05dc..dc03aa7a8ef33f500d714edd3f22285c617493da 100644
--- a/fsl/fsleyes/controls/locationpanel.py
+++ b/fsl/fsleyes/controls/locationpanel.py
@@ -10,6 +10,7 @@ panel which shows information about the current display location.
 
 
 import logging
+import itertools as it
 
 import wx
 import wx.html as wxhtml
@@ -20,6 +21,7 @@ import props
 
 import pwidgets.floatspin  as floatspin
 
+import fsl.utils.typedict  as td
 import fsl.utils.transform as transform
 import fsl.data.image      as fslimage
 import fsl.data.constants  as constants
@@ -70,6 +72,18 @@ class LocationPanel(fslpanel.FSLEyesPanel):
     display the current :attr:`.DisplayContext.location` as-is (i.e. in the
     display coordinate system); furthermore, the voxel location controls will
     be disabled.
+
+
+    **Location updates**
+
+    
+    The :data:`DISPLAYOPTS_BOUNDS` and :data:`DISPLAYOPTS_INFO` dictionaries
+    contain lists of property names that the :class:`.LocationPanel` listens
+    on for changes, so it knows when the location widgets, and information
+    about the currenty location, need to be refreshed. For example, when the
+    :attr`.Nifti1Opts.volume` property of a :class:`.Nifti1` overlay changes,
+    the volume index, and potentially the overlay information, needs to be
+    updated.
     """
 
     
@@ -98,24 +112,14 @@ class LocationPanel(fslpanel.FSLEyesPanel):
 
         fslpanel.FSLEyesPanel.__init__(self, parent, overlayList, displayCtx)
 
-        # The world and voxel locations dispalyed by the LocationPanel
-        # are only really relevant to volumetric (i.e. NIFTI) overlay
-        # types. However, other overlay types (e.g. Model instances)
-        # may have an associated 'reference' image, from which details
-        # of the coordinate system may be obtained.
-        #
-        # When the current overlay is either an Image instance, or has
-        # an associated reference image, this attributes is used to
-        # store a reference to the image.
-        self.__refImage = None
-        
-        # When the currently selected overlay is 4D,
-        # this attribute will refer to the
-        # corresponding DisplayOpts instance, which
-        # has a volume property that controls the
-        # volume - see e.g. the Nifti1Opts class. This
-        # attribute is set in _selectedOverlayChanged.
-        self.__volumeTarget = None
+        # Whenever the selected overlay changes,
+        # a reference to it and its DisplayOpts
+        # instance is stored, as property listeners
+        # are registered on it (and need to be
+        # de-registered later on).
+        self.__registeredOverlay = None
+        self.__registeredDisplay = None
+        self.__registeredOpts    = None
 
         self.__column1 = wx.Panel(self)
         self.__column2 = wx.Panel(self)
@@ -221,6 +225,8 @@ class LocationPanel(fslpanel.FSLEyesPanel):
         self._displayCtx .removeListener('selectedOverlay', self._name)
         self._displayCtx .removeListener('location',        self._name)
 
+        self.__deregisterOverlay()
+
         fslpanel.FSLEyesPanel.destroy(self)
 
 
@@ -272,81 +278,155 @@ class LocationPanel(fslpanel.FSLEyesPanel):
         
     def __selectedOverlayChanged(self, *a):
         """Called when the :attr:`.DisplayContext.selectedOverlay` or
-        :class:`.OverlayList` is changed. Refreshes the ``LocationPanel``
-        interface accordingly.
+        :class:`.OverlayList` is changed. Registered with the new overlay,
+        and refreshes the ``LocationPanel`` interface accordingly.
         """
 
-        self.__updateReferenceImage()
-        self.__updateWidgets()
+        self.__deregisterOverlay()
 
         if len(self._overlayList) == 0:
+            self.__updateWidgets()
             self.__updateLocationInfo()
-            return
+            
+        else:
+            self.__registerOverlay()
+            self.__updateWidgets()
+            self.__displayLocationChanged()
+
+        
+    def __registerOverlay(self):
+        """Registers property listeners with the :class:`.Display` and
+        :class:`.DisplayOpts` instances associated with the currently
+        selected overlay.
+        """
 
-        # Register a listener on the DisplayOpts 
-        # instance of the currently selected overlay,
-        # so we can update the location if the
-        # overlay bounds change.
         overlay = self._displayCtx.getSelectedOverlay()
-        for ovl in self._overlayList:
-            display = self._displayCtx.getDisplay(ovl)
-            opts    = display.getDisplayOpts()
-            
-            if ovl is overlay:
-                opts.addListener('bounds',
-                                 self._name,
-                                 self.__overlayBoundsChanged,
-                                 overwrite=True)
-            else:
-                opts.removeListener('bounds', self._name)
 
-        # Refresh the world/voxel location properties
-        self.__displayLocationChanged()
+        if overlay is None:
+            return
 
+        display = self._displayCtx.getDisplay(overlay)
+        opts    = display.getDisplayOpts()
+
+        self.__registeredOverlay = overlay
+        self.__registeredDisplay = display
+        self.__registeredOpts    = opts
+
+        # The properties that we need to
+        # listen for are specified in the
+        # DISPLAYOPTS_BOUNDS and
+        # DISPLAYOPTS_INFO dictionaries.
+        boundPropNames = DISPLAYOPTS_BOUNDS.get(opts, [], allhits=True)
+        infoPropNames  = DISPLAYOPTS_INFO  .get(opts, [], allhits=True)
+        boundPropNames = it.chain(*boundPropNames)
+        infoPropNames  = it.chain(*infoPropNames)
+
+        # DisplayOpts instances get re-created
+        # when an overlay type is changed, so
+        # we need to re-register when this happens.
+        display.addListener('overlayType',
+                            self._name,
+                            self.__selectedOverlayChanged)
+
+        for n in boundPropNames:
+            opts.addListener(n, self._name, self.__boundsOptsChanged)
+        for n in infoPropNames:
+            opts.addListener(n, self._name, self.__infoOptsChanged) 
+
+        # Enable the volume widget if the
+        # overlay is a 4D image, and bind
+        # the widget to the volume property
+        # of  the associated Nifti1Opts
+        # instance
+        is4D = isinstance(overlay, fslimage.Nifti1) and \
+               len(overlay.shape) >= 4              and \
+               overlay.shape[3] > 1 
+
+        if is4D:
+            props.bindWidget(
+                self.__volume, opts, 'volume', floatspin.EVT_FLOATSPIN)
 
-    def __overlayBoundsChanged(self, *a):
-        """Called when the :attr:`.DisplayOpts.bounds` property associated
-        with the currently selected overlay changes. Updates the
-        ``LocationPanel`` interface accordingly.
-        """
+            self.__volume.SetRange(0, overlay.shape[3] - 1)
+            self.__volume.SetValue(opts.volume)
 
-        self.__updateReferenceImage()
-        self.__updateWidgets()
-        self.__displayLocationChanged()
-        
+            self.__volume     .Enable()
+            self.__volumeLabel.Enable()
+        else:
+            self.__volume.SetRange(0, 0)
+            self.__volume.SetValue(0)
+            self.__volume.Disable() 
 
-    def __updateReferenceImage(self):
-        """Called by the :meth:`__selectedOverlayChanged` and
-        :meth:`__overlayBoundsChanged` methods. Looks at the currently
-        selected overlay, and figures out if there is a reference image
-        that can be used to transform between display, world, and voxel
-        coordinate systems.
+    
+    def __deregisterOverlay(self):
+        """De-registers property listeners with the :class:`.Display` and
+        :class:`.DisplayOpts` instances associated with the previously
+        registered overlay.
         """
-
-        refImage = None
         
-        # Look at the currently selected overlay, and
-        # see if there is an associated NIFTI image
-        # that can be used as a reference image
-        if len(self._overlayList) > 0:
+        opts    = self.__registeredOpts
+        display = self.__registeredDisplay
+        overlay = self.__registeredOverlay
+
+        if overlay is None:
+            return
 
-            overlay  = self._displayCtx.getSelectedOverlay()
-            refImage = self._displayCtx.getReferenceImage(overlay)
+        self.__registeredOpts    = None
+        self.__registeredDisplay = None
+        self.__registeredOverlay = None
 
-            log.debug('Reference image for overlay {}: {}'.format(
-                overlay, refImage))
+        boundPropNames = DISPLAYOPTS_BOUNDS.get(opts, [], allhits=True)
+        infoPropNames  = DISPLAYOPTS_INFO  .get(opts, [], allhits=True)
+        boundPropNames = it.chain(*boundPropNames)
+        infoPropNames  = it.chain(*infoPropNames)
+
+        if display is not None:
+            display.removeListener('overlayType', self._name)
+
+        for p in boundPropNames: opts.removeListener(p, self._name)
+        for p in infoPropNames:  opts.removeListener(p, self._name) 
+
+        is4D      = isinstance(overlay, fslimage.Nifti1) and \
+                    len(overlay.shape) >= 4              and \
+                    overlay.shape[3] > 1 
+ 
+        if is4D:
+            props.unbindWidget(self.__volume,
+                               opts,
+                               'volume',
+                               floatspin.EVT_FLOATSPIN)
+
+
+    def __boundsOptsChanged(self, *a):
+        """Called when a :class:`.DisplayOpts` property associated
+        with the currently selected overlay, and listed in the
+        :data:`DISPLAYOPTS_BOUNDS` dictionary, changes. Refreshes the
+        ``LocationPanel`` interface accordingly.
+        """ 
+        self.__updateWidgets()
+        self.__displayLocationChanged() 
 
-        self.__refImage = refImage
+        
+    def __infoOptsChanged(self, *a):
+        """Called when a :class:`.DisplayOpts` property associated
+        with the currently selected overlay, and listed in the
+        :data:`DISPLAYOPTS_INFO` dictionary, changes. Refreshes the
+        ``LocationPanel`` interface accordingly.
+        """
+        self.__displayLocationChanged()
         
 
     def __updateWidgets(self):
         """Called by the :meth:`__selectedOverlayChanged` and
-        :meth:`__overlayBoundsChanged` methods.  Enables/disables the
+        :meth:`__displayOptsChanged` methods.  Enables/disables the
         voxel/world location and volume controls depending on the currently
         selected overlay (or reference image).
         """
 
-        refImage = self.__refImage
+        overlay = self.__registeredOverlay
+        opts    = self.__registeredOpts
+
+        if overlay is not None: refImage = opts.getReferenceImage()
+        else:                   refImage = None
 
         haveRef = refImage is not None
 
@@ -406,42 +486,6 @@ class LocationPanel(fslpanel.FSLEyesPanel):
         self.enableNotification('worldLocation')
         self.enableNotification('voxelLocation')
 
-        ###############
-        # Volume widget
-        ###############
-
-        # Unbind any listeners between the previous
-        # reference image and the volume widget
-        if self.__volumeTarget is not None:
-            props.unbindWidget(self.__volume,
-                               self.__volumeTarget,
-                               'volume',
-                               floatspin.EVT_FLOATSPIN)
-            
-            self.__volumeTarget = None
-            self.__volume.SetValue(0)
-            self.__volume.SetRange(0, 0)
-
-        # Enable/disable the volume widget if the
-        # overlay is a 4D image, and bind/unbind
-        # the widget to the volume property of
-        # the associated Nifti1Opts instance
-        if haveRef and refImage.is4DImage():
-            opts = self._displayCtx.getOpts(refImage)
-            self.__volumeTarget = opts
-
-            props.bindWidget(
-                self.__volume, opts, 'volume', floatspin.EVT_FLOATSPIN)
-
-            self.__volume.SetRange(0, refImage.shape[3] - 1)
-            self.__volume.SetValue(opts.volume)
-
-            self.__volume     .Enable()
-            self.__volumeLabel.Enable()
-        else:
-            self.__volume     .Disable()
-            self.__volumeLabel.Disable() 
-
             
     def __displayLocationChanged(self, *a):
         """Called when the :attr:`.DisplayContext.location` changes.
@@ -461,7 +505,8 @@ class LocationPanel(fslpanel.FSLEyesPanel):
                   between the three location properties.
         """
 
-        if len(self._overlayList) == 0: return
+        if len(self._overlayList) == 0:      return
+        if self.__registeredOverlay is None: return
 
         self.__prePropagate()
         self.__propagate('display', 'voxel')
@@ -476,7 +521,8 @@ class LocationPanel(fslpanel.FSLEyesPanel):
         :attr:`.DisplayContext.location` properties.
         """
         
-        if len(self._overlayList) == 0: return
+        if len(self._overlayList) == 0:      return
+        if self.__registeredOverlay is None: return
 
         self.__prePropagate()
         self.__propagate('world', 'voxel')
@@ -491,7 +537,8 @@ class LocationPanel(fslpanel.FSLEyesPanel):
         :attr:`.DisplayContext.location` properties.
         """ 
         
-        if len(self._overlayList) == 0: return
+        if len(self._overlayList) == 0:      return
+        if self.__registeredOverlay is None: return
 
         self.__prePropagate()
         self.__propagate('voxel', 'world')
@@ -534,8 +581,10 @@ class LocationPanel(fslpanel.FSLEyesPanel):
         elif source == 'voxel':   coords = self.voxelLocation.xyz
         elif source == 'world':   coords = self.worldLocation.xyz
 
-        if self.__refImage is not None:
-            opts    = self._displayCtx.getOpts(self.__refImage)
+        refImage = self.__registeredOpts.getReferenceImage()
+
+        if refImage is not None:
+            opts    = self._displayCtx.getOpts(refImage)
             xformed = opts.transformCoords([coords],
                                            source,
                                            target,
@@ -574,7 +623,8 @@ class LocationPanel(fslpanel.FSLEyesPanel):
         in the :class:`.OverlayList`.
         """
 
-        if len(self._overlayList) == 0:
+        if len(self._overlayList) == 0 or \
+           self.__registeredOverlay is None: 
             self.__info.SetPage('')
             return
 
@@ -616,3 +666,25 @@ class LocationPanel(fslpanel.FSLEyesPanel):
                 
         self.__info.SetPage('<br>'.join(lines))
         self.__info.Refresh()
+
+
+DISPLAYOPTS_BOUNDS = td.TypeDict({
+    'DisplayOpts' : ['bounds'],
+    'ModelOpts'   : ['refImage'],
+})
+"""Different :class:`.DisplayOpts` types have different properties which
+affect the current overlay bounds.  Therefore, when the current overlay
+changes (as dictated by the :attr:`.DisplayContext.selectedOverlay`
+property),the :meth:`__registerOverlay` method registers property
+listeners on the properties specified in this dictionary.
+"""
+
+
+DISPLAYOPTS_INFO = td.TypeDict({
+    'Nifti1Opts'  : ['volume'],
+}) 
+"""Different :class:`.DisplayOpts` types have different properties which
+affect the current overlay location information.  Therefore, when the current
+overlay changes the :meth:`__registerOverlay` method registers property
+listeners on the properties specified in this dictionary.
+"""
diff --git a/fsl/fsleyes/controls/lookuptablepanel.py b/fsl/fsleyes/controls/lookuptablepanel.py
index 12d4b57e7306fb4f6e444b8272d7ccfd9fb01d7a..c7a5ad2e2dfaad174251387f019400cb46279887 100644
--- a/fsl/fsleyes/controls/lookuptablepanel.py
+++ b/fsl/fsleyes/controls/lookuptablepanel.py
@@ -79,7 +79,6 @@ class LookupTablePanel(fslpanel.FSLEyesPanel):
         :arg overlayList: The :class:`.OverlayList` instance.
         :arg displayCtx:  The :class:`.DisplayContext` instance.
         """ 
-
         
         fslpanel.FSLEyesPanel.__init__(self, parent, overlayList, displayCtx)
 
@@ -137,6 +136,9 @@ class LookupTablePanel(fslpanel.FSLEyesPanel):
         self.__selectedOpts    = None
         self.__selectedLut     = None
 
+        # See the __createLabelList method
+        self.__labelListCreateKey = 0
+
         overlayList.addListener('overlays',
                                 self._name,
                                 self.__selectedOverlayChanged)
@@ -219,24 +221,58 @@ class LookupTablePanel(fslpanel.FSLEyesPanel):
         ``LookupTable``.
         """
 
+        # The label list is created asynchronously on
+        # the wx.Idle loop, because it can take some
+        # time for big lookup tables. In the event
+        # that the list needs to be re-created (e.g.
+        # the current lookup table is changed), this
+        # attribute is used so that scheduled creation
+        # routines (the addLabel function defined
+        # below) can tell whether they should cancel.
+        myCreateKey = (self.__labelListCreateKey + 1) % 65536
+        self.__labelListCreateKey = myCreateKey
+
         log   .debug( 'Creating lookup table label list')
         status.update('Creating lookup table label list...', timeout=None)
 
+        self.Disable()
         self.__labelList.Clear()
 
         lut     = self.__selectedLut
         nlabels = len(lut.labels)
 
-        def addLabel(i, label):
-            self.__labelList.Append(label.displayName())
-            widget = LabelWidget(self, lut, label.value())
-            self.__labelList.SetItemWidget(i, widget)
+        def addLabel(labelIdx):
+
+            # If the user closes this panel while the
+            # label list is being created, wx will
+            # complain when we try to append things
+            # to a widget that has been destroyed.
+            try:
+
+                # A new request to re-create the list has
+                # been made - cancel this creation chain.
+                if self.__labelListCreateKey != myCreateKey:
+                    return
+ 
+                label  = lut.labels[labelIdx]
+                widget = LabelWidget(self, lut, label.value())
+                
+                self.__labelList.Append(label.displayName())
+                self.__labelList.SetItemWidget(labelIdx, widget)
 
-            if i == nlabels - 1:
-                status.update('Lookup table label list created.')
+                if labelIdx == nlabels - 1:
+                    status.update('Lookup table label list created.')
+                    self.Enable()
+                else:
+                    async.idle(addLabel, labelIdx + 1)
+                    
+            except wx.PyDeadObjectError:
+                pass
 
-        for i, label in enumerate(lut.labels):
-            async.idle(addLabel, i, label)
+        # If this is a new lut, it
+        # won't have any labels
+        if len(lut.labels) == 0: self.Enable()
+        else:                    async.idle(addLabel, 0)
 
 
     def __setLut(self, lut):
@@ -248,6 +284,9 @@ class LookupTablePanel(fslpanel.FSLEyesPanel):
         set to the new ``LookupTable``.
         """
 
+        if self.__selectedLut == lut:
+            return
+
         log.debug('Selecting lut: {}'.format(lut))
 
         if self.__selectedLut is not None:
@@ -319,7 +358,7 @@ class LookupTablePanel(fslpanel.FSLEyesPanel):
         log.debug('Creating and registering new '
                   'LookupTable: {}'.format(name))
 
-        lut = fslcmaps.LookupTable(name)
+        lut = fslcmaps.LookupTable(key=name, name=name)
         fslcmaps.registerLookupTable(lut, self._overlayList, self._displayCtx)
 
         self.__updateLutChoices()
@@ -346,7 +385,7 @@ class LookupTablePanel(fslpanel.FSLEyesPanel):
         log.debug('Creating and registering new '
                   'LookupTable {} (copied from {})'.format(newName, oldName))
 
-        lut = fslcmaps.LookupTable(newName)
+        lut = fslcmaps.LookupTable(key=newName, name=newName)
 
         for label in self.__selectedLut.labels:
             lut.set(label.value(),
@@ -609,10 +648,20 @@ class LabelWidget(wx.Panel):
 
         # Disable the LutPanel listener, otherwise
         # it will recreate the label list (see
-        # LookupTablePanel._createLabelList)
-        self.__lut.disableListener('labels', self.__lutPanel._name)
+        # LookupTablePanel._createLabelList).
+        # 
+        # We check to see if a listener exists,
+        # because the panel will only register
+        # a listener on label overlays.
+        toggle = self.__lut.hasListener('labels', self.__lutPanel._name)
+
+        if toggle:
+            self.__lut.disableListener('labels', self.__lutPanel._name)
+            
         self.__lut.set(self.__value, enabled=self.__enableBox.GetValue())
-        self.__lut.enableListener('labels', self.__lutPanel._name)
+
+        if toggle:
+            self.__lut.enableListener('labels', self.__lutPanel._name)
 
         
     def __onColour(self, ev):
@@ -623,10 +672,16 @@ class LabelWidget(wx.Panel):
         newColour = self.__colourButton.GetColour()
         newColour = [c / 255.0 for c in newColour]
 
-        # See comment in __onEnable 
-        self.__lut.disableListener('labels', self.__lutPanel._name)
+        # See comment in __onEnable
+        toggle = self.__lut.hasListener('labels', self.__lutPanel._name)
+
+        if toggle:
+            self.__lut.disableListener('labels', self.__lutPanel._name)
+            
         self.__lut.set(self.__value, colour=newColour)
-        self.__lut.enableListener('labels', self.__lutPanel._name)
+
+        if toggle:
+            self.__lut.enableListener('labels', self.__lutPanel._name)
 
         
 class NewLutDialog(wx.Dialog):
@@ -698,7 +753,7 @@ class NewLutDialog(wx.Dialog):
         """Called when the user confirms the dialog. Saves the name that the 
         user entered, and closes the dialog.
         """
-        self.__enteredName = self._name.GetValue()
+        self.__enteredName = self.__name.GetValue()
         self.EndModal(wx.ID_OK)
 
 
diff --git a/fsl/fsleyes/controls/overlaydisplaypanel.py b/fsl/fsleyes/controls/overlaydisplaypanel.py
index d2367f601628cd5833fa9b34daf55af844f0bd05..d2ba8a793a5dc07c5a85cfbfec4cfe87d9c93452 100644
--- a/fsl/fsleyes/controls/overlaydisplaypanel.py
+++ b/fsl/fsleyes/controls/overlaydisplaypanel.py
@@ -205,8 +205,9 @@ class OverlayDisplayPanel(fslpanel.FSLEyesPanel):
                         to :class:`.Display` or :class:`.DisplayOpts`
                         properties.
         """
-
-        self.__widgets.ClearGroup(groupName)
+        
+        self.__widgets.ClearGroup( groupName)
+        self.__widgets.RenameGroup(groupName, strings.labels[self, target])
 
         dispProps = _DISPLAY_PROPS.get(target, [], allhits=True)
         dispProps = functools.reduce(lambda a, b: a + b, dispProps)
diff --git a/fsl/fsleyes/controls/overlaylistpanel.py b/fsl/fsleyes/controls/overlaylistpanel.py
index 30b909eb6cf3c61bda1b52133f4720c157a09ab0..82d364e88537aae0dcb1dc30cd83f9dc2677240d 100644
--- a/fsl/fsleyes/controls/overlaylistpanel.py
+++ b/fsl/fsleyes/controls/overlaylistpanel.py
@@ -278,7 +278,7 @@ class ListItemWidget(wx.Panel):
     """
 
     
-    disabledFG = '#CCCCCC'
+    disabledFG = '#888888'
     """This colour is used as the foreground (text) colour for overlays
     where their :attr:`.Display.enabled` property is ``False``.
     """ 
diff --git a/fsl/fsleyes/controls/plotcontrolpanel.py b/fsl/fsleyes/controls/plotcontrolpanel.py
index acf562087c1dc9191d60a6f9828e61e5ea152001..ae5121829eae209ef38d473464451dce2364be69 100644
--- a/fsl/fsleyes/controls/plotcontrolpanel.py
+++ b/fsl/fsleyes/controls/plotcontrolpanel.py
@@ -349,12 +349,12 @@ class PlotControlPanel(fslpanel.FSLEyesPanel):
         if self.__widgets.HasGroup('currentDSSettings'):
             self.__widgets.RenameGroup(
                 'currentDSSettings',
-                strings.labels[self, 'currentDSettings'].format(display.name))
+                strings.labels[self, 'currentDSSettings'].format(display.name))
         
         if self.__widgets.HasGroup('customDSSettings'):
             self.__widgets.RenameGroup(
                 'customDSSettings',
-                strings.labels[self, 'customDSettings'].format(display.name)) 
+                strings.labels[self, 'customDSSettings'].format(display.name)) 
 
     
     def __selectedOverlayChanged(self, *a):
diff --git a/fsl/fsleyes/displaycontext/displaycontext.py b/fsl/fsleyes/displaycontext/displaycontext.py
index abc3ccb45e79567079e0a786b34e28a10ca0da31..cf47b27405032b0a1bdb266d8f49eaa286977401 100644
--- a/fsl/fsleyes/displaycontext/displaycontext.py
+++ b/fsl/fsleyes/displaycontext/displaycontext.py
@@ -276,7 +276,9 @@ class DisplayContext(props.SyncableHasProperties):
 
         self.detachFromParent()
 
-        self.__overlayList.removeListener('overlays', self.__name)
+        self.__overlayList.removeListener('overlays',           self.__name)
+        self              .removeListener('syncOverlayDisplay', self.__name)
+        self              .removeListener('displaySpace',       self.__name)
 
         for overlay, display in self.__displays.items():
             display.destroy()
diff --git a/fsl/fsleyes/displaycontext/labelopts.py b/fsl/fsleyes/displaycontext/labelopts.py
index 43482104f0ea8eaf5d6ecec520c9e54705f94732..126341754dbf1aac327d177b8d32193cb466fbae 100644
--- a/fsl/fsleyes/displaycontext/labelopts.py
+++ b/fsl/fsleyes/displaycontext/labelopts.py
@@ -55,7 +55,7 @@ class LabelOpts(volumeopts.Nifti1Opts):
         volumeopts.Nifti1Opts.__init__(self, overlay, *args, **kwargs)
 
         luts  = fslcm.getLookupTables()
-        alts  = [[l.name, l.key] for l in luts]
+        alts  = [list(set((l.name, l.key))) for l in luts]
 
         lutChoice = self.getProp('lut')
         lutChoice.setChoices(luts, alternates=alts)
diff --git a/fsl/fsleyes/frame.py b/fsl/fsleyes/frame.py
index 277e73e466a1a66b2471e388a1a9c1dca87d25e2..cc39af989e0ab96db11c8f28eef795f2ae90d790 100644
--- a/fsl/fsleyes/frame.py
+++ b/fsl/fsleyes/frame.py
@@ -124,10 +124,44 @@ class FSLEyesFrame(wx.Frame):
         self.__displayCtx  = displayCtx
         self.__mainPanel   = wx.Panel(self)
         self.__statusBar   = wx.StaticText(self)
+
+        # Even though the FSLEyesFrame does not allow
+        # panels to be floated, I am specifying the
+        # docking guide style for complicated reasons...
+        # 
+        # Each ViewPanel contained in this FSLEyesFrame
+        # has an AuiManager of its own; these child
+        # AuiManagers do support floating of their
+        # child panels. However, it seems that when
+        # a floating child panel of a ViewPanel is
+        # docked, this top-level AuiManager is called
+        # to draw the docking guides. This is because
+        # the wx.lib.agw.aui.framemanager.GetManager
+        # function uses the wx event handling system
+        # to figure out which AuiManager should be used
+        # to maange the docking (which is a ridiculous
+        # way to do this, in my opinion).
+        #
+        # Anyway, this means that the docking guides
+        # will be drawn according to the style set up
+        # in this AuiManager, instead of the ViewPanel
+        # AuiManager, which is the one that is actually
+        # managing the panel being docked.
+        #
+        # This wouldn't be a problem, if not for the fact
+        # that, when running over SSH/X11, the default
+        # docking guides seem to get sized incorrectly,
+        # and look terrible (probably related to the
+        # AuiDockingGuide monkey-patch at the bottom of
+        # viewpanel.py).
+        # 
+        # This problem does not occur with the aero/
+        # whidbey guides.
         self.__auiManager  = aui.AuiManager(
             self.__mainPanel,
-            agwFlags=(aui.AUI_MGR_RECTANGLE_HINT |
+            agwFlags=(aui.AUI_MGR_RECTANGLE_HINT          |
                       aui.AUI_MGR_NO_VENETIAN_BLINDS_FADE |
+                      aui.AUI_MGR_AERO_DOCKING_GUIDES     |
                       aui.AUI_MGR_LIVE_RESIZE))
 
         self.__sizer = wx.BoxSizer(wx.VERTICAL)
@@ -488,14 +522,19 @@ class FSLEyesFrame(wx.Frame):
         dctx .destroy()
 
         # If the removed panel was the centre
-        # pane, or if there is only one panel
-        # left, move another panel to the centre
+        # pane, move another panel to the centre
         numPanels = len(self.__viewPanels)
         wasCentre = paneInfo.dock_direction_get() == aui.AUI_DOCK_CENTRE
         
         if numPanels >= 1 and wasCentre:
             paneInfo = self.__auiManager.GetPane(self.__viewPanels[0])
-            paneInfo.Centre().Dockable(False).CaptionVisible(numPanels > 1)
+            paneInfo.Centre()
+
+        # If there is only one panel
+        # left, hide its title bar
+        if numPanels == 1:
+            paneInfo = self.__auiManager.GetPane(self.__viewPanels[0])
+            paneInfo.Dockable(False).CaptionVisible(False)
             
         # If there's only one panel left,
         # and it is a canvas panel, sync
diff --git a/fsl/fsleyes/gl/annotations.py b/fsl/fsleyes/gl/annotations.py
index 201e2d75745524071fd790d7b92d68f4de47fa98..e27bb06325da71705e07255ceceff08efd0c3825 100644
--- a/fsl/fsleyes/gl/annotations.py
+++ b/fsl/fsleyes/gl/annotations.py
@@ -31,10 +31,11 @@ import logging
 import numpy     as np
 import OpenGL.GL as gl
 
-import fsl.fsleyes.gl.globject as globject
-import fsl.fsleyes.gl.routines as glroutines
-import fsl.fsleyes.gl.textures as textures
-import fsl.utils.transform     as transform
+import fsl.fsleyes.gl.globject  as globject
+import fsl.fsleyes.gl.routines  as glroutines
+import fsl.fsleyes.gl.resources as glresources
+import fsl.fsleyes.gl.textures  as textures
+import fsl.utils.transform      as transform
 
 
 log = logging.getLogger(__name__)
@@ -95,21 +96,27 @@ class Annotations(object):
     def line(self, *args, **kwargs):
         """Queues a line for drawing - see the :class:`Line` class. """
         hold = kwargs.pop('hold', False)
-        return self.obj(Line(*args, **kwargs), hold)
+        obj  = Line(self.__xax, self.__yax, *args, **kwargs)
+        
+        return self.obj(obj, hold)
 
         
     def rect(self, *args, **kwargs):
         """Queues a rectangle for drawing - see the :class:`Rectangle` class.
         """
         hold = kwargs.pop('hold', False)
-        return self.obj(Rect(*args, **kwargs), hold)
+        obj  = Rect(self.__xax, self.__yax, *args, **kwargs)
+        
+        return self.obj(obj, hold)
 
 
     def grid(self, *args, **kwargs):
         """Queues a voxel grid for drawing - see the :class:`VoxelGrid` class.
         """ 
         hold = kwargs.pop('hold', False)
-        return self.obj(VoxelGrid(*args, **kwargs), hold)
+        obj  = VoxelGrid(self.__xax, self.__yax, *args, **kwargs)
+        
+        return self.obj(obj, hold)
 
     
     def selection(self, *args, **kwargs):
@@ -117,7 +124,9 @@ class Annotations(object):
         class.
         """ 
         hold = kwargs.pop('hold', False)
-        return self.obj(VoxelSelection(*args, **kwargs), hold) 
+        obj  = VoxelSelection(self.__xax, self.__yax, *args, **kwargs)
+        
+        return self.obj(obj, hold) 
     
         
     def obj(self, obj, hold=False):
@@ -236,9 +245,13 @@ class AnnotationObject(globject.GLSimpleObject):
     :meth:`globject.GLObject.draw` method.
     """
     
-    def __init__(self, xform=None, colour=None, width=None):
+    def __init__(self, xax, yax, xform=None, colour=None, width=None):
         """Create an ``AnnotationObject``.
 
+        :arg xax:    Initial display X axis
+
+        :arg yax:    Initial display Y axis        
+
         :arg xform:  Transformation matrix which will be applied to all
                      vertex coordinates.
         
@@ -246,7 +259,7 @@ class AnnotationObject(globject.GLSimpleObject):
         
         :arg width:  Line width to use for the annotation.
         """
-        globject.GLSimpleObject.__init__(self)
+        globject.GLSimpleObject.__init__(self, xax, yax)
         
         self.colour = colour
         self.width  = width
@@ -269,13 +282,17 @@ class Line(AnnotationObject):
     2D line.
     """
 
-    def __init__(self, xy1, xy2, *args, **kwargs):
+    def __init__(self, xax, yax, xy1, xy2, *args, **kwargs):
         """Create a ``Line`` annotation.
 
         The ``xy1`` and ``xy2`` coordinate tuples should be in relation to the
         axes which map to the horizontal/vertical screen axes on the target
         canvas.
 
+        :arg xax: Initial display X axis
+
+        :arg yax: Initial display Y axis        
+
         :arg xy1: Tuple containing the (x, y) coordinates of one endpoint.
         
         :arg xy2: Tuple containing the (x, y) coordinates of the second
@@ -284,7 +301,7 @@ class Line(AnnotationObject):
         All other arguments are passed through to
         :meth:`AnnotationObject.__init__`.
         """
-        AnnotationObject.__init__(self, *args, **kwargs)
+        AnnotationObject.__init__(self, xax, yax, *args, **kwargs)
         self.xy1 = xy1
         self.xy2 = xy2
 
@@ -310,18 +327,22 @@ class Rect(AnnotationObject):
     2D rectangle.
     """
 
-    def __init__(self, xy, w, h, *args, **kwargs):
+    def __init__(self, xax, yax, xy, w, h, *args, **kwargs):
         """Create a :class:`Rect` annotation.
 
-        :arg xy: Tuple specifying bottom left of the rectangle, in the display
-                 coordinate system.
-        :arg w:  Rectangle width.
-        :arg h:  Rectangle height.
+        :arg xax: Initial display X axis
+
+        :arg yax: Initial display Y axis        
+
+        :arg xy:  Tuple specifying bottom left of the rectangle, in the display
+                  coordinate system.
+        :arg w:   Rectangle width.
+        :arg h:   Rectangle height.
 
         All other arguments are passed through to
         :meth:`AnnotationObject.__init__`.        
         """
-        AnnotationObject.__init__(self, *args, **kwargs)
+        AnnotationObject.__init__(self, xax, yax, *args, **kwargs)
         self.xy = xy
         self.w  = w
         self.h  = h
@@ -371,6 +392,8 @@ class VoxelGrid(AnnotationObject):
 
     
     def __init__(self,
+                 xax,
+                 yax,
                  selectMask,
                  displayToVoxMat,
                  voxToDisplayMat,
@@ -379,6 +402,10 @@ class VoxelGrid(AnnotationObject):
                  **kwargs):
         """Create a ``VoxelGrid`` annotation.
 
+        :arg xax:             Initial display X axis
+
+        :arg yax:             Initial display Y axis        
+
         :arg selectMask:      A 3D numpy array, the same shape as the image
                               being annotated (or a sub-space of the image - 
                               see the ``offsets`` argument),  which is 
@@ -403,7 +430,7 @@ class VoxelGrid(AnnotationObject):
         """
         
         kwargs['xform'] = voxToDisplayMat
-        AnnotationObject.__init__(self, *args, **kwargs)
+        AnnotationObject.__init__(self, xax, yax, *args, **kwargs)
         
         if offsets is None:
             offsets = [0, 0, 0]
@@ -453,6 +480,8 @@ class VoxelSelection(AnnotationObject):
 
     
     def __init__(self,
+                 xax,
+                 yax,
                  selection,
                  displayToVoxMat,
                  voxToDisplayMat,
@@ -461,6 +490,10 @@ class VoxelSelection(AnnotationObject):
                  **kwargs):
         """Create a ``VoxelSelection`` annotation.
 
+        :arg xax:             Initial display X axis
+
+        :arg yax:             Initial display Y axis        
+
         :arg selection:       A :class:`.Selection` instance which defines
                               the voxels to be highlighted.
         
@@ -484,7 +517,7 @@ class VoxelSelection(AnnotationObject):
         :meth:`AnnotationObject.__init__` method.
         """
         
-        AnnotationObject.__init__(self, *args, **kwargs)
+        AnnotationObject.__init__(self, xax, yax, *args, **kwargs)
 
         if offsets is None:
             offsets = [0, 0, 0]
@@ -493,9 +526,13 @@ class VoxelSelection(AnnotationObject):
         self.displayToVoxMat = displayToVoxMat
         self.voxToDisplayMat = voxToDisplayMat
         self.offsets         = offsets
+
+        texName = '{}_{}'.format(type(self).__name__, id(selection))
         
-        self.texture = textures.SelectionTexture(
-            '{}_{}'.format(type(self).__name__, id(selection)),
+        self.texture = glresources.get(
+            texName,
+            textures.SelectionTexture,
+            texName,
             selection)
 
         
@@ -503,7 +540,7 @@ class VoxelSelection(AnnotationObject):
         """Must be called when this ``VoxelSelection`` is no longer needed.
         Destroys the :class:`.SelectionTexture`.
         """
-        self.texture.destroy()
+        glresources.delete(self.texture.getTextureName())
         self.texture = None
 
 
diff --git a/fsl/fsleyes/gl/gl14/gllinevector_funcs.py b/fsl/fsleyes/gl/gl14/gllinevector_funcs.py
index a823ff9d1ec732ad5f5d31d94bd0375f6d9f7a4e..27ce4e28b2df72353f0f9c0fdfc80b2a72155458 100644
--- a/fsl/fsleyes/gl/gl14/gllinevector_funcs.py
+++ b/fsl/fsleyes/gl/gl14/gllinevector_funcs.py
@@ -126,8 +126,8 @@ def updateShaderState(self):
 
     glvector_funcs.updateFragmentShaderState(self)
 
-    shape    = np.array(list(image.shape[:3]) + [0], dtype=np.float32)
-    invShape = 1.0 / shape
+    shape    = list(image.shape[:3])
+    invShape = [1.0 / s for s in shape] + [0]
     offset   = [0.5, 0.5, 0.5, 0.0]
 
     self.shader.load()
diff --git a/fsl/fsleyes/gl/gl14/glvolume_funcs.py b/fsl/fsleyes/gl/gl14/glvolume_funcs.py
index 8fe4900c9eaacdfaa52a846189815915b18c24d2..c86d8992b633b554a43e784ffe4758ad5a11595b 100644
--- a/fsl/fsleyes/gl/gl14/glvolume_funcs.py
+++ b/fsl/fsleyes/gl/gl14/glvolume_funcs.py
@@ -98,15 +98,16 @@ def updateShaderState(self):
     clipping = [clipLo, clipHi, invClip, imageIsClip]
     negCmap  = [useNegCmap, texZero, 0, 0]
 
-    self.shader.setVertParam('imageShape',  shape)
-    self.shader.setFragParam('imageShape',  shape)
-    self.shader.setFragParam('voxValXform', voxValXform)
-    self.shader.setFragParam('clipping',    clipping)
-    self.shader.setFragParam('negCmap',     negCmap)
+    changed  = False
+    changed |= self.shader.setVertParam('imageShape',  shape)
+    changed |= self.shader.setFragParam('imageShape',  shape)
+    changed |= self.shader.setFragParam('voxValXform', voxValXform)
+    changed |= self.shader.setFragParam('clipping',    clipping)
+    changed |= self.shader.setFragParam('negCmap',     negCmap)
     
     self.shader.unload()
 
-    return True
+    return changed
 
 
 def preDraw(self):
diff --git a/fsl/fsleyes/gl/gl21/gllinevector_vert.glsl b/fsl/fsleyes/gl/gl21/gllinevector_vert.glsl
index 678ca2753f26228e3b6a2fce17bce2b0bf976cd7..349ead0534c023cc155f462f02016122d3c278a5 100644
--- a/fsl/fsleyes/gl/gl21/gllinevector_vert.glsl
+++ b/fsl/fsleyes/gl/gl21/gllinevector_vert.glsl
@@ -64,10 +64,12 @@ varying vec4 fragColourFactor;
 
 void main(void) {
 
-  vec3 texCoord;
-  vec3 vector;
-  vec3 voxCoord;
-  vec3 vertVoxCoord;
+  
+  vec3  texCoord;
+  vec3  vector;
+  vec3  voxCoord;
+  vec3  vertVoxCoord;
+  float vectorLen;
 
   /*
    * The vertVoxCoord vector contains the floating
@@ -105,11 +107,28 @@ void main(void) {
    * texture range of [0,1] to the original
    * data range
    */
-  vector *= voxValXform[0].x;
-  vector += voxValXform[3].x;
+  vector   *= voxValXform[0].x;
+  vector   += voxValXform[3].x;
+  vectorLen = length(vector);
+
+  /* 
+   * Kill the vector if its length is 0. 
+   * We have to be tolerant of errors, 
+   * because of the transformation to/
+   * from the texture data range. This 
+   * may end up being too tolerant.
+   */
+  if (vectorLen < 0.0001) {
+    fragColourFactor = vec4(0, 0, 0, 0);
+    return;
+  }
 
-  // Scale the vector so it has length 0.5 
-  vector /= 2 * length(vector);
+  /*
+   * Scale the vector so it has length 
+   * 0.5. Note that here we are assuming 
+   * that all vectors are of length 1.
+   */
+  vector /= 2 * vectorLen;
 
   /*
    * Scale the vector by the minimum voxel length,
diff --git a/fsl/fsleyes/gl/gllabel.py b/fsl/fsleyes/gl/gllabel.py
index a91e381fb3ccc1198bd9acc36e85f09666550e86..dcc501d01c96e0bff8125b95073f4cc797838282 100644
--- a/fsl/fsleyes/gl/gllabel.py
+++ b/fsl/fsleyes/gl/gllabel.py
@@ -43,25 +43,34 @@ class GLLabel(globject.GLImageObject):
     """
 
     
-    def __init__(self, image, display):
+    def __init__(self, image, display, xax, yax):
         """Create a ``GLLabel``.
 
         :arg image:   The :class:`.Image` instance.
         :arg display: The associated :class:`.Display` instance.
+        :arg xax:     Initial display X axis
+        :arg yax:     Initial display Y axis        
         """
 
-        globject.GLImageObject.__init__(self, image, display)
-
-        lutTexName   = '{}_lut'.format(self.name)
+        globject.GLImageObject.__init__(self, image, display, xax, yax)
 
+        lutTexName        = '{}_lut'.format(self.name)
         self.lutTexture   = textures.LookupTableTexture(lutTexName)
         self.imageTexture = None
 
-        self.refreshImageTexture()
+        # The shader attribute will be created 
+        # by the gllabel_funcs module
+        self.shader       = None
+
+        self.addListeners()
+
         self.refreshLutTexture()
  
-        fslgl.gllabel_funcs.init(self)
-        self.addListeners()
+        def onTextureReady():
+            fslgl.gllabel_funcs.init(self)
+            self.notify() 
+ 
+        async.wait([self.refreshImageTexture()], onTextureReady)
 
         
     def destroy(self):
@@ -82,7 +91,9 @@ class GLLabel(globject.GLImageObject):
         """Returns ``True`` if this ``GLLabel`` is ready to be drawn, ``False``
         otherwise.
         """
-        return self.imageTexture is not None and self.imageTexture.ready()
+        return self.shader       is not None and \
+               self.imageTexture is not None and \
+               self.imageTexture.ready()
 
 
     def addListeners(self):
@@ -101,8 +112,8 @@ class GLLabel(globject.GLImageObject):
 
         def shaderUpdate(*a):
             if self.ready():
-                if fslgl.gllabel_funcs.updateShaderState(self):
-                    self.notify()
+                fslgl.gllabel_funcs.updateShaderState(self)
+                self.notify()
             
         def shaderCompile(*a):
             fslgl.gllabel_funcs.compileShaders(self)
@@ -121,7 +132,8 @@ class GLLabel(globject.GLImageObject):
             if self.__lut is not None:
                 self.__lut.addListener('labels', self.name, lutUpdate)
  
-            lutUpdate()
+            self.refreshLutTexture()
+            shaderUpdate()
 
         def imageRefresh(*a):
             async.wait([self.refreshImageTexture()], shaderUpdate)
@@ -220,7 +232,8 @@ class GLLabel(globject.GLImageObject):
             textures.ImageTexture,
             texName,
             self.image,
-            notify=False)
+            notify=False,
+            volume=opts.volume)
         
         self.imageTexture.register(self.name, self.__imageTextureChanged)
 
diff --git a/fsl/fsleyes/gl/gllinevector.py b/fsl/fsleyes/gl/gllinevector.py
index 1549f9bdd5e9eb1313455054f429d2668b074814..ac662957d89ad45d62cf002702049d46f04bd9f4 100644
--- a/fsl/fsleyes/gl/gllinevector.py
+++ b/fsl/fsleyes/gl/gllinevector.py
@@ -53,12 +53,13 @@ class GLLineVector(glvector.GLVector):
     """
 
 
-    def __init__(self, image, display):
+    def __init__(self, image, display, xax, yax):
         """Create a ``GLLineVector`` instance.
 
         :arg image:   An :class:`.Image` or :class:`.TensorImage` instance.
-
         :arg display: The associated :class:`.Display` instance.
+        :arg xax:     Initial display X axis
+        :arg yax:     Initial display Y axis
         """
         
         # If the overlay is a TensorImage, use the
@@ -70,6 +71,8 @@ class GLLineVector(glvector.GLVector):
         glvector.GLVector.__init__(self,
                                    image,
                                    display,
+                                   xax,
+                                   yax,
                                    vectorImage=vecImage,
                                    init=lambda: fslgl.gllinevector_funcs.init(
                                        self))
@@ -281,9 +284,10 @@ class GLLineVertices(object):
         lens     = np.sqrt(x ** 2 + y ** 2 + z ** 2)
 
         # scale the vector lengths to 0.5
-        vertices[:, :, :, 0] = 0.5 * x / lens
-        vertices[:, :, :, 1] = 0.5 * y / lens
-        vertices[:, :, :, 2] = 0.5 * z / lens
+        with np.errstate(invalid='ignore'):
+            vertices[:, :, :, 0] = 0.5 * x / lens
+            vertices[:, :, :, 1] = 0.5 * y / lens
+            vertices[:, :, :, 2] = 0.5 * z / lens
 
         # Scale the vector data by the minimum
         # voxel length, so it is a unit vector
diff --git a/fsl/fsleyes/gl/glmask.py b/fsl/fsleyes/gl/glmask.py
index bf144ec17b8f76ad14f39cd38114b4e2fc86f9d6..2b1ef20c014640ef3622ba5e55949ccdcf85e652 100644
--- a/fsl/fsleyes/gl/glmask.py
+++ b/fsl/fsleyes/gl/glmask.py
@@ -53,9 +53,9 @@ class GLMask(glvolume.GLVolume):
         
         def shaderUpdate(*a):
             if self.ready():
-                if fslgl.glvolume_funcs.updateShaderState(self):
-                    self.notify() 
-
+                fslgl.glvolume_funcs.updateShaderState(self)
+                self.notify()
+                    
         def shaderCompile(*a):
             fslgl.glvolume_funcs.compileShaders(self)
             shaderUpdate()
@@ -72,7 +72,7 @@ class GLMask(glvolume.GLVolume):
             resolution = opts.resolution
 
             self.imageTexture.set(volume=volume, resolution=resolution)
-            async.wait([self.refreshThread()], shaderUpdate)
+            async.wait([self.refreshImageTexture()], shaderUpdate)
 
         display.addListener('alpha',         name, colourUpdate,  weak=False)
         display.addListener('brightness',    name, colourUpdate,  weak=False)
diff --git a/fsl/fsleyes/gl/glmodel.py b/fsl/fsleyes/gl/glmodel.py
index 0dd61055d9b2631825d14e2eab06a83d2104b9b2..0e348889ee4716e777f7d36b571e24ed68c7e74b 100644
--- a/fsl/fsleyes/gl/glmodel.py
+++ b/fsl/fsleyes/gl/glmodel.py
@@ -65,16 +65,20 @@ class GLModel(globject.GLObject):
     """
 
     
-    def __init__(self, overlay, display):
+    def __init__(self, overlay, display, xax, yax):
         """Create a ``GLModel``.
 
         :arg overlay: A :class:`.Model` overlay.
 
         :arg display: A :class:`.Display` instance defining how the
                       ``overlay`` is to be displayed.
+        
+        :arg xax:     Initial display X axis
+
+        :arg yax:     Initial display Y axis        
         """
 
-        globject.GLObject.__init__(self)
+        globject.GLObject.__init__(self, xax, yax)
 
         self.shader  = None
         self.overlay = overlay
diff --git a/fsl/fsleyes/gl/globject.py b/fsl/fsleyes/gl/globject.py
index 3c03d030e594c6b14bb1c216246b07a4e20bd83b..72fbc67e0dfc7730cdc7c262d8f5179be7afcd52 100644
--- a/fsl/fsleyes/gl/globject.py
+++ b/fsl/fsleyes/gl/globject.py
@@ -50,7 +50,7 @@ def getGLObjectType(overlayType):
     return typeMap.get(overlayType, None)
 
 
-def createGLObject(overlay, display):
+def createGLObject(overlay, display, xax, yax):
     """Create :class:`GLObject` instance for the given overlay, as specified
     by the :attr:`.Display.overlayType` property.
 
@@ -58,10 +58,14 @@ def createGLObject(overlay, display):
     
     :arg display: A :class:`.Display` instance describing how the overlay
                   should be displayed.
+
+    :arg xax:     Initial display X axis
+
+    :arg yax:     Initial display Y axis    
     """
     ctr = getGLObjectType(display.overlayType)
 
-    if ctr is not None: return ctr(overlay, display)
+    if ctr is not None: return ctr(overlay, display, xax, yax)
     else:               return None
 
 
@@ -104,7 +108,11 @@ class GLObject(notifier.Notifier):
     
     Sub-class implementations must do the following:
     
-     - Call :meth:`__init__`.
+     - Call :meth:`__init__`. A ``GLObject.__init__`` sub-class method must
+       have the following signature::
+    
+           def __init__(self, overlay, display, xax, yax)
+    
 
      - Call :meth:`notify` whenever its OpenGL representation changes.
 
@@ -132,23 +140,26 @@ class GLObject(notifier.Notifier):
     """
 
     
-    def __init__(self):
-        """Create a :class:`GLObject`.  The constructor adds one attribute
+    def __init__(self, xax, yax):
+        """Create a :class:`GLObject`.  The constructor adds one attribute 
         to this instance, ``name``, which is simply a unique name for this
-        instance, and gives default values to the ``xax``, ``yax``, and
-        ``zax`` attributes.
+        instance, and gives values to the ``xax``, ``yax``, and ``zax``
+        attributes.
 
         Subclass implementations must call this method, and should also
         perform any necessary OpenGL initialisation, such as creating
         textures.
+
+        :arg xax: Initial display X axis
+        :arg yax: Initial display Y axis
         """
 
         # Give this instance a name, and set 
         # initial values for the display axes
         self.name = '{}_{}'.format(type(self).__name__, id(self))
-        self.xax  = 0
-        self.yax  = 1
-        self.zax  = 2
+        self.xax  = xax
+        self.yax  = yax
+        self.zax  = 3 - xax - yax
 
         log.memory('{}.init ({})'.format(type(self).__name__, id(self)))
 
@@ -285,9 +296,9 @@ class GLSimpleObject(GLObject):
     be called.
     """
 
-    def __init__(self):
+    def __init__(self, xax, yax):
         """Create a ``GLSimpleObject``. """
-        GLObject.__init__(self)
+        GLObject.__init__(self, xax, yax)
 
 
     def ready(self):
@@ -315,7 +326,7 @@ class GLImageObject(GLObject):
     of :class:`.Nifti1` instances. 
     """
     
-    def __init__(self, image, display):
+    def __init__(self, image, display, xax, yax):
         """Create a ``GLImageObject``.
 
         This constructor adds the following attributes to this instance:
@@ -333,9 +344,13 @@ class GLImageObject(GLObject):
         :arg image:   The :class:`.Nifti1` instance
         
         :arg display: An associated :class:`.Display` instance.
+
+        :arg xax:     Initial display X axis
+
+        :arg yax:     Initial display Y axis
         """
         
-        GLObject.__init__(self)
+        GLObject.__init__(self, xax, yax)
         self.image       = image
         self.display     = display
         self.displayOpts = display.getDisplayOpts()
diff --git a/fsl/fsleyes/gl/glrgbvector.py b/fsl/fsleyes/gl/glrgbvector.py
index 5d290dd506ace290f48abe8b9bebda278f5970ad..4c25bd4f51f9639617c81ad32f3d6120c65e4420 100644
--- a/fsl/fsleyes/gl/glrgbvector.py
+++ b/fsl/fsleyes/gl/glrgbvector.py
@@ -59,11 +59,13 @@ class GLRGBVector(glvector.GLVector):
     """
 
 
-    def __init__(self, image, display):
+    def __init__(self, image, display, xax, yax):
         """Create a ``GLRGBVector``.
 
         :arg image:   An :class:`.Image` or :class:`.TensorImage` instance.
         :arg display: The associated :class:`.Display` instance.
+        :arg xax:     Initial display X axis
+        :arg yax:     Initial display Y axis        
         """
 
         # If the overlay is a TensorImage, use the
@@ -75,6 +77,8 @@ class GLRGBVector(glvector.GLVector):
         glvector.GLVector.__init__(self,
                                    image,
                                    display,
+                                   xax,
+                                   yax,
                                    prefilter=np.abs,
                                    vectorImage=vecImage,
                                    init=lambda: fslgl.glrgbvector_funcs.init(
diff --git a/fsl/fsleyes/gl/gltensor.py b/fsl/fsleyes/gl/gltensor.py
index 96f8e7d68bbd91d840a1136ea41e9baf33005279..064adc63da314730aa0fc9fe847cfafa28de6ec5 100644
--- a/fsl/fsleyes/gl/gltensor.py
+++ b/fsl/fsleyes/gl/gltensor.py
@@ -28,7 +28,7 @@ class GLTensor(glvector.GLVector):
     """
 
     
-    def __init__(self, image, display):
+    def __init__(self, image, display, xax, yax):
         """Create a ``GLTensor``. Calls the :func:`.gl21.gltensor_funcs.init`
         function.
 
@@ -36,10 +36,16 @@ class GLTensor(glvector.GLVector):
         
         :arg display: The :class:`.Display` instance associated with the
                       ``image``.
+        
+        :arg xax:     Initial display X axis
+
+        :arg yax:     Initial display Y axis        
         """
         glvector.GLVector.__init__(self,
                                    image,
                                    display,
+                                   xax,
+                                   yax,
                                    prefilter=np.abs,
                                    vectorImage=image.V1(),
                                    init=lambda: fslgl.gltensor_funcs.init(
diff --git a/fsl/fsleyes/gl/glvector.py b/fsl/fsleyes/gl/glvector.py
index 6e97d1efb9e5b4583ef75eed188087364d178eda..dc86bd4bdeb4f6a49a76ea37ab755d7279bb56a6 100644
--- a/fsl/fsleyes/gl/glvector.py
+++ b/fsl/fsleyes/gl/glvector.py
@@ -96,6 +96,8 @@ class GLVector(globject.GLImageObject):
     def __init__(self,
                  image,
                  display,
+                 xax,
+                 yax,
                  prefilter=None,
                  vectorImage=None,
                  init=None):
@@ -116,6 +118,10 @@ class GLVector(globject.GLImageObject):
         :arg display:     A :class:`.Display` object which describes how the
                           image is to be displayed.
 
+        :arg xax:         Initial display X axis
+
+        :arg yax:         Initial display Y axis        
+
         :arg prefilter:   An optional function which filters the data before it
                           is stored as a 3D texture. See
                           :class:`.ImageTexture`. Whether or not this function
@@ -145,7 +151,7 @@ class GLVector(globject.GLImageObject):
             raise ValueError('Image must be 4 dimensional, with 3 volumes '
                              'representing the XYZ vector angles')
 
-        globject.GLImageObject.__init__(self, image, display)
+        globject.GLImageObject.__init__(self, image, display, xax, yax)
 
         name = self.name
 
@@ -257,8 +263,8 @@ class GLVector(globject.GLImageObject):
             
         def shaderUpdate(*a):
             if self.ready():
-                if self.updateShaderState():
-                    self.notify() 
+                self.updateShaderState()
+                self.notify() 
         
         def modUpdate( *a):
             self.deregisterAuxImage('modulate')
@@ -531,6 +537,9 @@ class GLVector(globject.GLImageObject):
             # Right?
             if unsynced:
                 texName = '{}_unsync_{}'.format(texName, id(opts))
+
+        if opts is not None: volume = opts.volume
+        else:                volume = 0
  
         tex = glresources.get(
             texName,
@@ -538,6 +547,7 @@ class GLVector(globject.GLImageObject):
             texName,
             image,
             normalise=norm,
+            volume=volume,
             notify=False)
         
         tex.register(self.name, self.__textureChanged)
diff --git a/fsl/fsleyes/gl/glvolume.py b/fsl/fsleyes/gl/glvolume.py
index 55e13c20218776114c953bb8b4dac8401575e79c..5e4f869aaf3b627144774f08077fd11bdbb96f69 100644
--- a/fsl/fsleyes/gl/glvolume.py
+++ b/fsl/fsleyes/gl/glvolume.py
@@ -142,16 +142,20 @@ class GLVolume(globject.GLImageObject):
     """
 
     
-    def __init__(self, image, display):
+    def __init__(self, image, display, xax, yax):
         """Create a ``GLVolume`` object.
 
         :arg image:   An :class:`.Image` object.
         
         :arg display: A :class:`.Display` object which describes how the image
                       is to be displayed.
+
+        :arg xax:     Initial display X axis
+
+        :arg yax:     Initial display Y axis 
         """
 
-        globject.GLImageObject.__init__(self, image, display)
+        globject.GLImageObject.__init__(self, image, display, xax, yax)
 
         # Add listeners to this image so the view can be
         # updated when its display properties are changed
@@ -257,69 +261,32 @@ class GLVolume(globject.GLImageObject):
         display properties are changed.
         """
 
-        image   = self.image
         display = self.display
         opts    = self.displayOpts
-        lName   = self.name
+        name    = self.name
 
-        def update(*a):
-            self.notify()
+        crPVs   = opts.getPropVal('clippingRange').getPropertyValueList()
 
-        def shaderUpdate(*a):
-            if self.ready():
-                if fslgl.glvolume_funcs.updateShaderState(self):
-                    self.notify()
+        display .addListener('alpha',           name, self._alphaChanged)
+        opts    .addListener('displayRange',    name,
+                             self._displayRangeChanged)
         
-        def colourUpdate(*a):
-            self.refreshColourTextures()
-            if self.ready():
-                shaderUpdate()
-
-        def imageRefresh(*a):
-            async.wait([self.refreshImageTexture()], shaderUpdate)
-
-        def imageUpdate(*a):
-            volume     = opts.volume
-            resolution = opts.resolution
-
-            if opts.interpolation == 'none': interp = gl.GL_NEAREST
-            else:                            interp = gl.GL_LINEAR
-            
-            self.imageTexture.set(volume=volume,
-                                  interp=interp,
-                                  resolution=resolution,
-                                  notify=False)
-
-            waitfor = [self.imageTexture.refreshThread()]
-
-            if self.clipTexture is not None:
-                self.clipTexture.set(interp=interp,
-                                     resolution=resolution,
-                                     notify=False)
-                waitfor.append(self.clipTexture.refreshThread())
-
-            async.wait(waitfor, shaderUpdate)
-
-        def clipUpdate(*a):
-            self.deregisterClipImage()
-            self.registerClipImage()
-            async.wait([self.refreshClipTexture()], shaderUpdate)
-
-        display.addListener('alpha',          lName, colourUpdate,  weak=False)
-        opts   .addListener('displayRange',   lName, colourUpdate,  weak=False)
-        opts   .addListener('clippingRange',  lName, shaderUpdate,  weak=False)
-        opts   .addListener('clipImage',      lName, clipUpdate,    weak=False)
-        opts   .addListener('invertClipping', lName, shaderUpdate,  weak=False)
-        opts   .addListener('cmap',           lName, colourUpdate,  weak=False)
-        opts   .addListener('negativeCmap',   lName, colourUpdate,  weak=False)
-        opts   .addListener('useNegativeCmap',
-                            lName, colourUpdate,  weak=False)
-        opts   .addListener('invert',         lName, colourUpdate,  weak=False)
-        opts   .addListener('volume',         lName, imageUpdate,   weak=False)
-        opts   .addListener('resolution',     lName, imageUpdate,   weak=False)
-        opts   .addListener('interpolation',  lName, imageUpdate,   weak=False)
-        opts   .addListener('transform',      lName, update,        weak=False)
-        image  .addListener('data',           lName, update,        weak=False)
+        crPVs[0].addListener(name, self._lowClippingRangeChanged)
+        crPVs[1].addListener(name, self._highClippingRangeChanged)
+        
+        opts    .addListener('clipImage',       name, self._clipImageChanged)
+        opts    .addListener('invertClipping',  name,
+                             self._invertClippingChanged)
+        opts    .addListener('cmap',            name, self._cmapChanged)
+        opts    .addListener('negativeCmap',    name, self._cmapChanged)
+        opts    .addListener('useNegativeCmap', name,
+                             self._useNegativeCmapChanged)
+        opts    .addListener('invert',          name, self._invertChanged)
+        opts    .addListener('volume',          name, self._volumeChanged)
+        opts    .addListener('interpolation',   name,
+                             self._interpolationChanged)
+        opts    .addListener('resolution',      name, self._resolutionChanged)
+        opts    .addListener('transform',       name, self._transformChanged)
 
         # Save a flag so the removeDisplayListeners
         # method knows whether it needs to de-register
@@ -331,12 +298,15 @@ class GLVolume(globject.GLImageObject):
         self.__syncListenersRegistered = opts.getParent() is not None
 
         if self.__syncListenersRegistered:
-            opts.addSyncChangeListener(
-                'volume',        lName, imageRefresh, weak=False)
-            opts.addSyncChangeListener(
-                'resolution',    lName, imageRefresh, weak=False)
-            opts.addSyncChangeListener(
-                'interpolation', lName, imageRefresh, weak=False)
+            opts.addSyncChangeListener('volume',
+                                       name,
+                                       self._imageSyncChanged)
+            opts.addSyncChangeListener('resolution',
+                                       name,
+                                       self._imageSyncChanged)
+            opts.addSyncChangeListener('interpolation',
+                                       name,
+                                       self._imageSyncChanged)
 
 
     def removeDisplayListeners(self):
@@ -347,29 +317,30 @@ class GLVolume(globject.GLImageObject):
         image   = self.image
         display = self.display
         opts    = self.displayOpts
-
-        lName = self.name
-        
-        display.removeListener(          'alpha',           lName)
-        opts   .removeListener(          'displayRange',    lName)
-        opts   .removeListener(          'clippingRange',   lName)
-        opts   .removeListener(          'clipImage',       lName)
-        opts   .removeListener(          'invertClipping',  lName)
-        opts   .removeListener(          'cmap',            lName)
-        opts   .removeListener(          'negativeCmap',    lName)
-        opts   .removeListener(          'useNegativeCmap', lName)
-        opts   .removeListener(          'cmap',            lName)
-        opts   .removeListener(          'invert',          lName)
-        opts   .removeListener(          'volume',          lName)
-        opts   .removeListener(          'resolution',      lName)
-        opts   .removeListener(          'interpolation',   lName)
-        opts   .removeListener(          'transform',       lName)
-        image  .removeListener(          'data',            lName)
+        name    = self.name
+        crPVs   = opts.getPropVal('clippingRange').getPropertyValueList()
+
+        display .removeListener(          'alpha',           name)
+        opts    .removeListener(          'displayRange',    name)
+        crPVs[0].removeListener(name)
+        crPVs[1].removeListener(name)
+        opts    .removeListener(          'clipImage',       name)
+        opts    .removeListener(          'invertClipping',  name)
+        opts    .removeListener(          'cmap',            name)
+        opts    .removeListener(          'negativeCmap',    name)
+        opts    .removeListener(          'useNegativeCmap', name)
+        opts    .removeListener(          'cmap',            name)
+        opts    .removeListener(          'invert',          name)
+        opts    .removeListener(          'volume',          name)
+        opts    .removeListener(          'resolution',      name)
+        opts    .removeListener(          'interpolation',   name)
+        opts    .removeListener(          'transform',       name)
+        image   .removeListener(          'data',            name)
         
         if self.__syncListenersRegistered:
-            opts.removeSyncChangeListener('volume',        lName)
-            opts.removeSyncChangeListener('resolution',    lName)
-            opts.removeSyncChangeListener('interpolation', lName)
+            opts.removeSyncChangeListener('volume',        name)
+            opts.removeSyncChangeListener('resolution',    name)
+            opts.removeSyncChangeListener('interpolation', name)
 
         
     def testUnsynced(self):
@@ -384,7 +355,144 @@ class GLVolume(globject.GLImageObject):
                 not self.displayOpts.isSyncedToParent('volume')     or
                 not self.displayOpts.isSyncedToParent('resolution') or
                 not self.displayOpts.isSyncedToParent('interpolation'))
+
+
+    def _alphaChanged(self, *a):
+        """Called when the :attr:`.Display.alpha` property changes. """
+        self.refreshColourTextures()
+        self.notify()
+
+
+    def _displayRangeChanged(self, *a):
+        """Called when the :attr:`.VolumeOpts.displayRange` property changes.
+        """
+        self.refreshColourTextures()
+        if fslgl.glvolume_funcs.updateShaderState(self):
+            self.notify()
+
+            
+    def _lowClippingRangeChanged(self, *a):
+        """Called when the low :attr:`.VolumeOpts.clippingRange` property
+        changes. Separate listeners are used for the low and high clipping
+        values to avoid unnecessary duplicate refreshes in the event that the
+        :attr:`.VolumeOpts.linkLowRanges` or
+        :attr:`.VolumeOpts.linkHighRanges` flags are set.
+        """ 
+        if self.displayOpts.linkLowRanges:
+            return
+        
+        if fslgl.glvolume_funcs.updateShaderState(self):
+            self.notify()
+
+    def _highClippingRangeChanged(self, *a):
+        """Called when the high :attr:`.VolumeOpts.clippingRange` property
+        changes (see :meth:`_lowClippingRangeChanged`).
+        """ 
+        if self.displayOpts.linkHighRanges:
+            return
+        
+        if fslgl.glvolume_funcs.updateShaderState(self):
+            self.notify()
+
+            
+    def _clipImageChanged(self, *a):
+        """Called when the :attr:`.VolumeOpts.clipImage` property changes.
+        """
+        def onRefresh():
+            if fslgl.glvolume_funcs.updateShaderState(self):
+                self.notify()
         
+        self.deregisterClipImage()
+        self.registerClipImage()
+        async.wait([self.refreshClipTexture()], onRefresh)
+
+        
+    def _invertClippingChanged(self, *a):
+        """Called when the :attr:`.VolumeOpts.invertClipping` property changes.
+        """ 
+        if fslgl.glvolume_funcs.updateShaderState(self):
+            self.notify()
+
+
+    def _cmapChanged(self, *a):
+        """Called when the :attr:`.VolumeOpts.cmap` or
+        :attr:`.VolumeOpts.negativeCmap` properties change.
+        """
+        self.refreshColourTextures()
+        self.notify()
+
+        
+    def _useNegativeCmapChanged(self, *a):
+        """Called when the :attr:`.VolumeOpts.useNegativeCmap` property
+        changes.
+        """ 
+        if fslgl.glvolume_funcs.updateShaderState(self):
+            self.notify()
+
+            
+    def _invertChanged(self, *a):
+        """Called when the :attr:`.VolumeOpts.invert` property changes. """
+        self.refreshColourTextures()
+        fslgl.glvolume_funcs.updateShaderState(self)
+        self.notify()
+
+
+    def _volumeChanged(self, *a):
+        """Called when the :attr:`.Nifti1Opts.volume` property changes. """
+        opts       = self.displayOpts
+        volume     = opts.volume
+        resolution = opts.resolution
+
+        if opts.interpolation == 'none': interp = gl.GL_NEAREST
+        else:                            interp = gl.GL_LINEAR
+
+        self.imageTexture.set(volume=volume,
+                              interp=interp,
+                              resolution=resolution,
+                              notify=False)
+
+        waitfor = [self.imageTexture.refreshThread()]
+
+        if self.clipTexture is not None:
+            self.clipTexture.set(interp=interp,
+                                 resolution=resolution,
+                                 notify=False)
+            waitfor.append(self.clipTexture.refreshThread())
+
+        def onRefresh():
+            fslgl.glvolume_funcs.updateShaderState(self)
+            self.notify()
+            
+        async.wait(waitfor, onRefresh)
+
+
+    def _interpolationChanged(self, *a):
+        """Called when the :attr:`.Nifti1Opts.interpolation` property changes.
+        """
+        self._volumeChanged()
+
+        
+    def _resolutionChanged(self, *a):
+        """Called when the :attr:`.Nifti1Opts.resolution` property changes.
+        """ 
+        self._volumeChanged() 
+
+
+    def _transformChanged(self, *a):
+        """Called when the :attr:`.Nifti1Opts.transform` property changes.
+        """ 
+        self.notify()
+
+
+    def _imageSyncChanged(self, *a):
+        """Called when the synchronisation state of the
+        :attr:`.Nifti1Opts.volume`, :attr:`.Nifti1Opts.resolution`, or
+        :attr:`.VolumeOpts.interpolation` properties change.
+        """ 
+        self.refreshImageTexture()
+        fslgl.glvolume_funcs.updateShaderState(self)
+        self.notify()
+
 
     def refreshImageTexture(self):
         """Refreshes the :class:`.ImageTexture` used to store the
@@ -423,7 +531,8 @@ class GLVolume(globject.GLImageObject):
             self.image,
             interp=interp,
             resolution=opts.resolution,
-            notify=False)
+            notify=False,
+            volume=opts.volume)
 
         self.imageTexture.register(self.name, self.__textureChanged)
 
diff --git a/fsl/fsleyes/gl/resources.py b/fsl/fsleyes/gl/resources.py
index 7df347ca5ec5e3303f5539490c0da153fc5d6f67..a80ef172a9a8407f2a62d33b519bd1ef0cb973fd 100644
--- a/fsl/fsleyes/gl/resources.py
+++ b/fsl/fsleyes/gl/resources.py
@@ -46,7 +46,7 @@ texture, and will increase its reference count::
          interp=gl.GL_LINEAR)
 
 
-.. note:: Here, we have used ``'myTexture'`` as the resource key in practice,
+.. note:: Here, we have used ``'myTexture'`` as the resource key. In practice,
           you will need to use something that is guaranteed to be unique
           throughout your application.
 
diff --git a/fsl/fsleyes/gl/shaders/arbp/program.py b/fsl/fsleyes/gl/shaders/arbp/program.py
index c11c9e17166e12c76d595f803ef376095041b064..26445466247a42cc9fc127a838ce23fc3be76663 100644
--- a/fsl/fsleyes/gl/shaders/arbp/program.py
+++ b/fsl/fsleyes/gl/shaders/arbp/program.py
@@ -18,6 +18,7 @@ import OpenGL.raw.GL._types           as gltypes
 import OpenGL.GL.ARB.fragment_program as arbfp
 import OpenGL.GL.ARB.vertex_program   as arbvp
 
+import fsl.utils.memoize              as memoize
 import                                   parse
 
 
@@ -207,12 +208,17 @@ class ARBPShader(object):
             gl.glDisableClientState(gl.GL_TEXTURE_COORD_ARRAY)
 
 
+    @memoize.Instanceify(memoize.skipUnchanged)
     def setVertParam(self, name, value):
         """Sets the value of the specified vertex program parameter.
 
         .. note:: It is assumed that the value is either a sequence of length
                   4 (for vector parameters), or a ``numpy`` array of shape 
                   ``(n, 4)`` (for matrix parameters).
+
+        .. note:: This method is decorated by the
+                  :func:`.memoize.skipUnchanged` decorator, which returns
+                  ``True`` if the value was changed, and ``False`` otherwise.
         """
 
         pos   = self.vertParamPositions[name]
@@ -225,10 +231,15 @@ class ARBPShader(object):
                 arbvp.GL_VERTEX_PROGRAM_ARB, pos + i,
                 row[0], row[1], row[2], row[3]) 
 
-    
+
+    @memoize.Instanceify(memoize.skipUnchanged)
     def setFragParam(self, name, value):
         """Sets the value of the specified vertex program parameter. See 
         :meth:`setVertParam` for infomration about possible values.
+
+        .. note:: This method is decorated by the
+                  :func:`.memoize.skipUnchanged` decorator, which returns
+                  ``True`` if the value was changed, and ``False`` otherwise.        
         """
         pos   = self.fragParamPositions[name]
         value = np.array(value, dtype=np.float32).reshape((-1, 4))
diff --git a/fsl/fsleyes/gl/shaders/glsl/program.py b/fsl/fsleyes/gl/shaders/glsl/program.py
index c39c8a23d0902ce32b5a6c9cd652878c8d65b975..f17ee31fa4f1713913dcb38e2eb9bd3123a7923b 100644
--- a/fsl/fsleyes/gl/shaders/glsl/program.py
+++ b/fsl/fsleyes/gl/shaders/glsl/program.py
@@ -18,6 +18,8 @@ import OpenGL.GL.ARB.instanced_arrays as arbia
 
 import parse
 
+import fsl.utils.memoize as memoize
+
 
 log = logging.getLogger(__name__)
 
@@ -149,12 +151,6 @@ class GLSLShader(object):
                                              self.vertUniforms,
                                              self.fragUniforms)
 
-        # We cache the most recent value for
-        # every uniform. When a call to set()
-        # is made, if the value is unchanged,
-        # we skip the GL call.
-        self.values = {n : None for n in self.positions.keys()}
-
         # Buffers for vertex attributes
         self.buffers = {}
 
@@ -239,31 +235,20 @@ class GLSLShader(object):
             gl.glDeleteBuffers(1, gltypes.GLuint(buf))
         self.program = None
         
-        
+
+    @memoize.Instanceify(memoize.skipUnchanged)
     def set(self, name, value):
         """Sets the value for the specified GLSL ``uniform`` variable.
 
         The ``GLSLShader`` keeps a copy of the value of every uniform, to
         avoid unnecessary GL calls.
 
-        :returns: ``True`` if the value was changed, ``False`` otherwise.
+        
+        .. note:: This method is decorated by the
+                  :func:`.memoize.skipUnchanged` decorator, which returns
+                  ``True`` if the value was changed, ``False`` otherwise.
         """
 
-        oldVal = self.values[name]
-
-        oldIsArray = isinstance(oldVal, np.ndarray)
-        newIsArray = isinstance(value,  np.ndarray)
-        isarray    = oldIsArray or newIsArray
-
-        if oldIsArray and (not newIsArray): value  = np.array(value)
-        if newIsArray and (not oldIsArray): oldVal = np.array(oldVal)
-
-        if isarray: nochange = np.all(oldVal == value)
-        else:       nochange =        oldVal == value
-
-        if nochange:
-            return False
-
         vPos  = self.positions[name]
         vType = self.types[    name]
 
@@ -278,8 +263,6 @@ class GLSLShader(object):
 
         setfunc(vPos, value)
 
-        return True
-
 
     def setAtt(self, name, value, divisor=None):
         """Sets the value for the specified GLSL ``attribute`` variable.
diff --git a/fsl/fsleyes/gl/slicecanvas.py b/fsl/fsleyes/gl/slicecanvas.py
index 76297f1552907a3c08c414edd471bf0bf67ff9ca..ef31b1c1eb7bac03aa3ac27c27a6853c79b42e1e 100644
--- a/fsl/fsleyes/gl/slicecanvas.py
+++ b/fsl/fsleyes/gl/slicecanvas.py
@@ -136,6 +136,7 @@ class SliceCanvas(props.HasProperties):
        panDisplayBy
        centreDisplayAt
        panDisplayToShow
+       zoomTo
        getAnnotations
     """
 
@@ -244,14 +245,15 @@ class SliceCanvas(props.HasProperties):
         :class:`.DisplayContext`, and :class:`.Display` instances, and
         destroys OpenGL representations of all overlays.
         """
-        self.removeListener('zax',           self.name)
-        self.removeListener('pos',           self.name)
-        self.removeListener('displayBounds', self.name)
-        self.removeListener('showCursor',    self.name)
-        self.removeListener('invertX',       self.name)
-        self.removeListener('invertY',       self.name)
-        self.removeListener('zoom',          self.name)
-        self.removeListener('renderMode',    self.name)
+        self.removeListener('zax',             self.name)
+        self.removeListener('pos',             self.name)
+        self.removeListener('displayBounds',   self.name)
+        self.removeListener('showCursor',      self.name)
+        self.removeListener('invertX',         self.name)
+        self.removeListener('invertY',         self.name)
+        self.removeListener('zoom',            self.name)
+        self.removeListener('renderMode',      self.name)
+        self.removeListener('resolutionLimit', self.name)
 
         self.overlayList.removeListener('overlays',     self.name)
         self.displayCtx .removeListener('bounds',       self.name)
@@ -375,8 +377,6 @@ class SliceCanvas(props.HasProperties):
         """Pans the display so the given x/y position is in the centre. """
 
         xcentre, ycentre = self.getDisplayCentre()
-
-        # move to the new centre
         self.panDisplayBy(xpos - xcentre, ypos - ycentre)
 
 
@@ -414,6 +414,56 @@ class SliceCanvas(props.HasProperties):
             self.panDisplayBy(xoff, yoff)
 
 
+    def zoomTo(self, xlo, xhi, ylo, yhi):
+        """Zooms the canvas to the given rectangle, specified in
+        horizontal/vertical display coordinates.
+        """
+        
+        # We are going to convert the rectangle specified by
+        # the inputs into a zoom value, set the canvas zoom
+        # level, and then centre the canvas on the rectangle.
+
+        # Middle of the rectangle, used 
+        # at the end for centering
+        xmid = xlo + (xhi - xlo) / 2.0
+        ymid = ylo + (yhi - ylo) / 2.0
+
+        # Size of the rectangle
+        rectXlen = abs(xhi - xlo)
+        rectYlen = abs(yhi - ylo)
+
+        if rectXlen == 0: return
+        if rectYlen == 0: return
+
+        # Size of the canvas limits, 
+        # and the zoom value limits
+        xmin, xmax = self.displayBounds.getLimits(0)
+        ymin, ymax = self.displayBounds.getLimits(1)
+        zoommin    = self.getConstraint('zoom', 'minval')
+        zoommax    = self.getConstraint('zoom', 'maxval') 
+
+        xlen    = xmax    - xmin
+        ylen    = ymax    - ymin
+        zoomlen = zoommax - zoommin
+
+        # Calculate the ratio of the
+        # rectangle to the canvas limits
+        xratio = rectXlen / xlen
+        yratio = rectYlen / ylen
+        ratio  = max(xratio, yratio)
+
+        # Calculate the zoom from this ratio -
+        # this is the inverse of the zoom->canvas
+        # bounds calculation, as implemented in
+        # _applyZoom.
+        zoom  = 100.0 / ratio
+        zoom  = ((zoom - zoommin) / zoomlen) ** (1.0 / 3.0)
+        zoom  = zoommin + zoom * zoomlen
+        
+        self.zoom = zoom
+        self.centreDisplayAt(xmid, ymid)
+
+
     def getAnnotations(self):
         """Returns an :class:`.Annotations` instance, which can be used to
         annotate the canvas.
@@ -802,10 +852,12 @@ class SliceCanvas(props.HasProperties):
                 self._glObjects.pop(overlay)
                 return
 
-            globj = globject.createGLObject(overlay, display)
+            globj = globject.createGLObject(overlay,
+                                            display,
+                                            self.xax,
+                                            self.yax)
 
             if globj is not None:
-                globj.setAxes(self.xax, self.yax)
                 globj.register(self.name, self._refresh)
 
             self._glObjects[overlay] = globj
@@ -955,8 +1007,27 @@ class SliceCanvas(props.HasProperties):
         if self.zoom == 100.0:
             return (xmin, xmax, ymin, ymax)
 
+        # The zoom is specified as a value
+        # between 100 and 5000 (the minval/
+        # maxaval). To make the zoom smoother
+        # at low levels, we re-scale this
+        # value to be exponential across the
+        # range.
+        minzoom = self.getConstraint('zoom', 'minval')
+        maxzoom = self.getConstraint('zoom', 'maxval')
+
+        # Transform zoom from [100 - 5000] into
+        # [0.0 - 1.0], then turn it from linear
+        # [0.0 - 1.0] to exponential [0.0 - 1.0],
+        # and then finally back to [100 - 5000].
+        zoom    = (self.zoom - minzoom) / (maxzoom - minzoom)
+        zoom    = minzoom + (zoom ** 3) * (maxzoom - minzoom)
+
+        # Then turn zoom from [100 - 5000]
+        # to [1.0 - 0.0] - this value is
+        # then used to scale the given bounds
+        zoomFactor = 100.0 / zoom
         bounds     = self.displayBounds
-        zoomFactor = 100.0 / self.zoom
 
         xlen    = xmax - xmin
         ylen    = ymax - ymin
@@ -1006,9 +1077,9 @@ class SliceCanvas(props.HasProperties):
                              oldLoc=None):
         """Called on canvas resizes, overlay bound changes, and zoom changes.
         
-        Calculates the bounding box, in display coordinates, to be displayed on
-        the canvas. Stores this bounding box in the displayBounds property. If
-        any of the parameters are not provided, the
+        Calculates the bounding box, in display coordinates, to be displayed
+        on the canvas. Stores this bounding box in the :attr:`displayBounds`
+        property. If any of the parameters are not provided, the
         :attr:`.DisplayContext.bounds` are used.
 
         
diff --git a/fsl/fsleyes/gl/wxglslicecanvas.py b/fsl/fsleyes/gl/wxglslicecanvas.py
index 6f789e77f5760b0f3c8f0069d8776e9031d47416..d56f5dd0eccc05283b1b56a60444800a317ab86a 100644
--- a/fsl/fsleyes/gl/wxglslicecanvas.py
+++ b/fsl/fsleyes/gl/wxglslicecanvas.py
@@ -43,3 +43,29 @@ class WXGLSliceCanvas(slicecanvas.SliceCanvas,
             self._updateDisplayBounds()
             ev.Skip()
         self.Bind(wx.EVT_SIZE, onResize)
+
+        
+    def Show(self, show):
+        """Overrides ``GLCanvas.Show``. When running over SSH/X11, it doesn't
+        seem to be possible to hide a ``GLCanvas`` - the most recent scene
+        displayed on the canvas seems to persist, does not get overridden, and
+        gets drawn on top of other things in the interface:
+
+        .. image:: images/x11_slicecanvas_show_bug.png
+           :scale: 50%
+           :align: center
+
+        This is not ideal, and I have no idea why it occurs. The only
+        workaround that I've found to work is, instead of hiding the canvas,
+        to set its size to 0. So this method does just that.
+        """
+
+        if not show:
+            self.SetMinSize((0, 0))
+            self.SetMaxSize((0, 0))
+            self.SetSize(   (0, 0))
+
+
+    def Hide(self):
+        """Overrides ``GLCanvas.Hide``. Calls :meth:`Show`. """
+        self.Show(False)
diff --git a/fsl/fsleyes/icons/splash/splash.png b/fsl/fsleyes/icons/splash/splash.png
index 9b01fb2e575a4be3ea8fc9ca9bed9e35d9c3f71a..1d5dc9770dfb12db7c137004464db0ddd1f55158 100644
Binary files a/fsl/fsleyes/icons/splash/splash.png and b/fsl/fsleyes/icons/splash/splash.png differ
diff --git a/fsl/fsleyes/icons/splash/splash.xcf b/fsl/fsleyes/icons/splash/splash.xcf
index 14d491e4bb34878bd2ac2baa1dfd3f3dae27350e..8aadf6e83dca3c9b00639772d61f9c0f937abf5e 100644
Binary files a/fsl/fsleyes/icons/splash/splash.xcf and b/fsl/fsleyes/icons/splash/splash.xcf differ
diff --git a/fsl/fsleyes/overlay.py b/fsl/fsleyes/overlay.py
index a90abfc50743e7d453c74ab2365ef9a9151cc9bc..daad912f809b5c56b98acfcef985a491ff37690d 100644
--- a/fsl/fsleyes/overlay.py
+++ b/fsl/fsleyes/overlay.py
@@ -65,6 +65,7 @@ import os.path as op
 import props
 
 import fsl.data.strings   as strings
+import fsl.data.image     as fslimage
 import fsl.utils.settings as fslsettings
 import fsl.utils.status   as status
 import fsl.utils.async    as async
@@ -157,9 +158,27 @@ class OverlayList(props.HasProperties):
         """Returns the first overlay with the given ``name`` or ``dataSource``,
         or ``None`` if there is no overlay with said ``name``/``dataSource``.
         """
+
+        if name is None:
+            return None
+        
         for overlay in self.overlays:
-            if overlay.name == name or overlay.dataSource == name:
+
+            if overlay.name == name:
                 return overlay
+
+            if overlay.dataSource is None:
+                return None
+
+            # Ignore file extensions for NIFTI1 images.
+            if isinstance(overlay, fslimage.Image):
+                if fslimage.removeExt(overlay.dataSource) == \
+                   fslimage.removeExt(name):
+                    return overlay
+            else:
+                if overlay.dataSource == name:
+                    return overlay
+                
         return None
             
 
diff --git a/fsl/fsleyes/perspectives.py b/fsl/fsleyes/perspectives.py
index 22868462afb53637a18d61bd67713350fab414fd..bbc0bb076c6383f279d228f1693c0a0f65085fb2 100644
--- a/fsl/fsleyes/perspectives.py
+++ b/fsl/fsleyes/perspectives.py
@@ -210,7 +210,8 @@ def serialisePerspective(frame):
      the ``AuiManager.SavePerspective`` and ``AuiManager.SavePaneInfo``
      methods. One of these strings consists of:
     
-       - A name.
+       - A name, `'layout1'` or `'layout2'`, specifying the AUI version 
+         (this will always be at least `'layout2'` for *FSLeyes*).
     
        - A set of key-value set of key-value pairs defining the top level
          panel layout.
@@ -222,7 +223,7 @@ def serialisePerspective(frame):
      separated with '|' characters, and the pane-level key-value pairs
      separated with a ';' character. For example:
     
-     layoutName|key1=value1|name=Pane1;caption=Pane 1|\
+     layout2|key1=value1|name=Pane1;caption=Pane 1|\
      name=Pane2;caption=Pane 2|doc_size(5,0,0)=22|
     
      This function queries each of the AuiManagers, and extracts the following:
@@ -585,16 +586,33 @@ def _getPanelProps(panel):
     
 
 VIEWPANEL_PROPS = {
-    'OrthoPanel'    : [['syncLocation',  'syncOverlayOrder',  'syncOverlayDisplay'],
-                       ['showCursor',    'bgColour',          'cursorColour',
-                        'showColourBar', 'colourBarLocation', 'showXCanvas',
-                        'showYCanvas',   'showZCanvas',       'showLabels',
+    'OrthoPanel'    : [['syncLocation',
+                        'syncOverlayOrder',
+                        'syncOverlayDisplay',
+                        'movieRate'],
+                       ['showCursor',
+                        'bgColour',
+                        'cursorColour',
+                        'showColourBar',
+                        'colourBarLocation',
+                        'showXCanvas',
+                        'showYCanvas',
+                        'showZCanvas',
+                        'showLabels',
                         'layout']
                        ],
-    'LightBoxPanel' : [['syncLocation',  'syncOverlayOrder',  'syncOverlayDisplay'],
-                       ['showCursor',    'bgColour',          'cursorColour',
-                        'showColourBar', 'colourBarLocation', 'zax',
-                        'showGridLines', 'highlightSlice']]}
+    'LightBoxPanel' : [['syncLocation',
+                        'syncOverlayOrder',
+                        'syncOverlayDisplay',
+                        'movieRate'],
+                       ['showCursor',
+                        'bgColour',
+                        'cursorColour',
+                        'showColourBar',
+                        'colourBarLocation',
+                        'zax',
+                        'showGridLines',
+                        'highlightSlice']]}
 
     
 BUILT_IN_PERSPECTIVES = collections.OrderedDict((
diff --git a/fsl/fsleyes/platform.py b/fsl/fsleyes/platform.py
index 9575f46e2c79d9783d2fadce441caee1e64751b7..305873b86f37349087584830dbe8b6ab4fe55d48 100644
--- a/fsl/fsleyes/platform.py
+++ b/fsl/fsleyes/platform.py
@@ -5,6 +5,8 @@
 # Author: Paul McCarthy <pauldmccarthy@gmail.com>
 #
 
+import os
+
 haveGui    = False
 wxFlavour  = None
 wxPlatform = None
@@ -16,18 +18,28 @@ WX_PHOENIX = 2
 WX_MAC = 1
 WX_GTK = 2
 
+class Platform(object):
+    def __init__(self):
 
-try:
-    import wx
-    haveGui = True
-
-except ImportError:
-    haveGui = False
+        self.haveGui    = False
+        self.wxFlavour  = None
+        self.wxPlatform = None
 
+        try:
+            import wx
+            self.haveGui = True
 
-if 'phoenix' in wx.PlatformInformation: wxFlavour = WX_PHOENIX
-else:                                   wxFlavour = WX_PYTHON
+        except ImportError:
+            pass
 
+        if self.haveGui:
+            if 'phoenix' in wx.PlatformInformation:
+                self.wxFlavour = WX_PHOENIX
+                
+            if 'MAC' in wx.Platform:
+                self.wxPlatform = WX_MAC
 
-if   'MAC' in wx.Platform: wxPlatform = WX_MAC
-elif 'GTK' in wx.Platform: wxPlatform = WX_GTK
+        # TODO Make Platform a notifier, so
+        #      things can register to listen
+        #      for changes to $FSLDIR
+        self.fsldir = os.environ['FSLDIR']
diff --git a/fsl/fsleyes/profiles/lightboxviewprofile.py b/fsl/fsleyes/profiles/lightboxviewprofile.py
index 5d032af1fd169af189e97c32aac0695e67ebbfc4..742a241ee65d38ef966f934960c2dbf09f6eff03 100644
--- a/fsl/fsleyes/profiles/lightboxviewprofile.py
+++ b/fsl/fsleyes/profiles/lightboxviewprofile.py
@@ -11,6 +11,7 @@
 import logging
 
 import fsl.fsleyes.profiles as profiles
+import fsl.utils.async      as async
 
 
 log = logging.getLogger(__name__)
@@ -76,7 +77,12 @@ class LightBoxViewProfile(profiles.Profile):
         if   wheel > 0: wheel = -1
         elif wheel < 0: wheel =  1
 
-        self._viewPanel.getCanvas().topRow += wheel
+        # See comment in OrthoViewProfile._zoomModeMouseWheel
+        # about timeout
+        def update():
+            self._viewPanel.getCanvas().topRow += wheel
+
+        async.idle(update, timeout=0.1)
 
         
     def _viewModeLeftMouseDrag(self, ev, canvas, mousePos, canvasPos):
@@ -106,4 +112,10 @@ class LightBoxViewProfile(profiles.Profile):
 
         if   wheel > 0: wheel =  50
         elif wheel < 0: wheel = -50
-        self._viewPanel.getSceneOptions().zoom += wheel 
+
+        # see comment in OrthoViewProfile._zoomModeMouseWheel
+        # about timeout
+        def update():
+            self._viewPanel.getSceneOptions().zoom += wheel
+
+        async.idle(update, timeout=0.1)
diff --git a/fsl/fsleyes/profiles/orthoeditprofile.py b/fsl/fsleyes/profiles/orthoeditprofile.py
index 62c1947e23741653e10fc9130a616b0a8ba12ad6..f547e1950aa40dc6dc943681f0f0ae2e347304a0 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.utils.async              as async
 import fsl.utils.dialog             as fsldlg
 import fsl.utils.status             as status
 import fsl.fsleyes.actions          as actions
@@ -193,7 +194,9 @@ class OrthoEditProfile(orthoviewprofile.OrthoViewProfile):
         self.__xcanvas           = viewPanel.getXCanvas()
         self.__ycanvas           = viewPanel.getYCanvas()
         self.__zcanvas           = viewPanel.getZCanvas() 
-        self.__selAnnotation     = None
+        self.__xselAnnotation    = None
+        self.__yselAnnotation    = None
+        self.__zselAnnotation    = None
         self.__selecting         = False
         self.__currentOverlay    = None
 
@@ -238,14 +241,29 @@ class OrthoEditProfile(orthoviewprofile.OrthoViewProfile):
             editor.removeListener('canRedo', self._name)
             editor.destroy()
 
-        if self.__selAnnotation is not None:
-            self.__selAnnotaiton.destroy()
+        xannot = self.__xcanvas.getAnnotations()
+        yannot = self.__ycanvas.getAnnotations()
+        zannot = self.__zcanvas.getAnnotations()
+
+        if self.__xselAnnotation is not None:
+            xannot.dequeue(self.__xselAnnotation, hold=True)
+            self.__xselAnnotaiton.destroy()
+            
+        if self.__yselAnnotation is not None:
+            yannot.dequeue(self.__yselAnnotation, hold=True)
+            self.__yselAnnotaiton.destroy()
+            
+        if self.__zselAnnotation is not None:
+            zannot.dequeue(self.__zselAnnotation, hold=True)
+            self.__zselAnnotaiton.destroy()
             
         self.__editors        = None
         self.__xcanvas        = None
         self.__ycanvas        = None
         self.__zcanvas        = None
-        self.__selAnnotation  = None
+        self.__xselAnnotation = None
+        self.__yselAnnotation = None
+        self.__zselAnnotation = None
         self.__currentOverlay = None
 
         orthoviewprofile.OrthoViewProfile.destroy(self)
@@ -255,14 +273,26 @@ class OrthoEditProfile(orthoviewprofile.OrthoViewProfile):
         """Destroys all :mod:`.annotations`, and calls
         :meth:`.OrthoViewProfile.deregister`.
         """
-        if self.__selAnnotation is not None:
-            sa = self.__selAnnotation
-            self.__xcanvas.getAnnotations().dequeue(sa, hold=True)
-            self.__ycanvas.getAnnotations().dequeue(sa, hold=True)
-            self.__zcanvas.getAnnotations().dequeue(sa, hold=True)
-            sa.destroy()
-
-        self.__selAnnotation = None
+
+        xannot = self.__xcanvas.getAnnotations()
+        yannot = self.__ycanvas.getAnnotations()
+        zannot = self.__zcanvas.getAnnotations()
+        
+        if self.__xselAnnotation is not None:
+            xannot.dequeue(self.__xselAnnotation, hold=True)
+            self.__xselAnnotation.destroy()
+
+        if self.__yselAnnotation is not None:
+            yannot.dequeue(self.__yselAnnotation, hold=True)
+            self.__yselAnnotation.destroy()
+
+        if self.__zselAnnotation is not None:
+            zannot.dequeue(self.__zselAnnotation, hold=True)
+            self.__zselAnnotation.destroy()
+
+        self.__xselAnnotation = None
+        self.__yselAnnotation = None
+        self.__zselAnnotation = None
             
         orthoviewprofile.OrthoViewProfile.deregister(self)
 
@@ -358,7 +388,7 @@ class OrthoEditProfile(orthoviewprofile.OrthoViewProfile):
         editor.getSelection().enableNotification('selection')
         
         self.__selectionChanged()
-        self.__selAnnotation.texture.refresh()
+        self.__xselAnnotation.texture.refresh()
         self._viewPanel.Refresh()
 
 
@@ -373,12 +403,14 @@ class OrthoEditProfile(orthoviewprofile.OrthoViewProfile):
 
         editor = self.__editors[self.__currentOverlay]
 
+        # See comment in undo method 
+        # about disabling notification
         editor.getSelection().disableNotification('selection')
         editor.redo()
         editor.getSelection().enableNotification('selection')
         
         self.__selectionChanged()
-        self.__selAnnotation.texture.refresh()
+        self.__xselAnnotation.texture.refresh()
         self._viewPanel.Refresh()
  
 
@@ -408,8 +440,14 @@ class OrthoEditProfile(orthoviewprofile.OrthoViewProfile):
         
         Updates the  :mod:`.annotations` colours accordingly.
         """
-        if self.__selAnnotation is not None:
-            self.__selAnnotation.colour = self.selectionOverlayColour
+        if self.__xselAnnotation is not None:
+            self.__xselAnnotation.colour = self.selectionOverlayColour
+            
+        if self.__yselAnnotation is not None:
+            self.__yselAnnotation.colour = self.selectionOverlayColour
+            
+        if self.__zselAnnotation is not None:
+            self.__zselAnnotation.colour = self.selectionOverlayColour 
 
 
     def __setFillValueLimits(self, overlay):
@@ -482,14 +520,21 @@ class OrthoEditProfile(orthoviewprofile.OrthoViewProfile):
         zannot = self.__zcanvas.getAnnotations()        
 
         # Clear the selection annotation
-        if self.__selAnnotation is not None:
-            xannot.dequeue(self.__selAnnotation, hold=True)
-            yannot.dequeue(self.__selAnnotation, hold=True)
-            zannot.dequeue(self.__selAnnotation, hold=True)
+        if self.__xselAnnotation is not None:
+            xannot.dequeue(self.__xselAnnotation, hold=True)
+            self.__xselAnnotation.destroy()
+            
+        if self.__yselAnnotation is not None:
+            yannot.dequeue(self.__yselAnnotation, hold=True)
+            self.__yselAnnotation.destroy()
             
-            self.__selAnnotation.destroy()
+        if self.__zselAnnotation is not None:
+            zannot.dequeue(self.__zselAnnotation, hold=True)
+            self.__zselAnnotation.destroy()
             
-        self.__selAnnotation = None
+        self.__xselAnnotation = None
+        self.__yselAnnotation = None
+        self.__zselAnnotation = None
 
         # Remove property listeners from the
         # editor/selection instances associated
@@ -603,15 +648,33 @@ class OrthoEditProfile(orthoviewprofile.OrthoViewProfile):
     
         # Create a selection annotation and
         # queue it on the canvases for drawing
-        self.__selAnnotation = annotations.VoxelSelection( 
+        self.__xselAnnotation = annotations.VoxelSelection(
+            self.__xcanvas.xax,
+            self.__xcanvas.yax,
+            editor.getSelection(),
+            opts.getTransform('pixdim', 'voxel'),
+            opts.getTransform('voxel',  'pixdim'),
+            colour=self.selectionOverlayColour)
+        
+        self.__yselAnnotation = annotations.VoxelSelection(
+            self.__ycanvas.xax,
+            self.__ycanvas.yax,
             editor.getSelection(),
             opts.getTransform('pixdim', 'voxel'),
             opts.getTransform('voxel',  'pixdim'),
             colour=self.selectionOverlayColour)
+        
+        self.__zselAnnotation = annotations.VoxelSelection(
+            self.__zcanvas.xax,
+            self.__zcanvas.yax,
+            editor.getSelection(),
+            opts.getTransform('pixdim', 'voxel'),
+            opts.getTransform('voxel',  'pixdim'),
+            colour=self.selectionOverlayColour) 
 
-        xannot.obj(self.__selAnnotation, hold=True)
-        yannot.obj(self.__selAnnotation, hold=True)
-        zannot.obj(self.__selAnnotation, hold=True)
+        xannot.obj(self.__xselAnnotation, hold=True)
+        yannot.obj(self.__yselAnnotation, hold=True)
+        zannot.obj(self.__zselAnnotation, hold=True)
 
         self._viewPanel.Refresh()
 
@@ -673,9 +736,11 @@ class OrthoEditProfile(orthoviewprofile.OrthoViewProfile):
         kwargs  = {'colour' : self.selectionCursorColour,
                    'width'  : 2}
 
-        cursors = [annotations.Rect((0, 0), 0, 0, **kwargs),
-                   annotations.Rect((0, 0), 0, 0, **kwargs),
-                   annotations.Rect((0, 0), 0, 0, **kwargs)]
+        cursors = []
+
+        for c in canvases:
+            r = annotations.Rect(c.xax, c.yax, (0, 0), 0, 0, **kwargs)
+            cursors.append(r)
 
         # If we are running in a low
         # performance mode, the cursor
@@ -775,16 +840,16 @@ class OrthoEditProfile(orthoviewprofile.OrthoViewProfile):
         On all lower performance settings, only the source canvas is updated.
         """
         perf = self._viewPanel.getSceneOptions().performance
-        if perf == 4:
-            if mousePos is None or canvasPos is None:
-                self._viewPanel.Refresh()
-
-            # If running in high performance mode, we make
-            # the canvas location track the edit cursor
-            # location, so that the other two canvases
-            # update to display the current cursor location.
-            else:
-                self._navModeLeftMouseDrag(ev, canvas, mousePos, canvasPos)
+
+        # If running in high performance mode, we make
+        # the canvas location track the edit cursor
+        # location, so that the other two canvases
+        # update to display the current cursor location.
+        if perf == 4               and \
+           (mousePos  is not None) and \
+           (canvasPos is not None):
+            self._navModeLeftMouseDrag(ev, canvas, mousePos, canvasPos)
+            
         else:
             canvas.Refresh()
 
@@ -868,10 +933,17 @@ class OrthoEditProfile(orthoviewprofile.OrthoViewProfile):
 
         voxel = self.__getVoxelLocation(canvasPos)
 
-        if voxel is not None:
+        if voxel is None:
+            return
+
+        # See comment in OrthoViewProfile._zoomModeMouseWheel
+        # about timeout
+        def update():
             self.__drawCursorAnnotation(canvas, voxel)
             self.__refreshCanvases(ev, canvas)
 
+        async.idle(update, timeout=0.1)
+
         
     def _deselModeLeftMouseDown(self, ev, canvas, mousePos, canvasPos):
         """Handles mouse down events in ``desel`` mode.
@@ -1056,17 +1128,24 @@ class OrthoEditProfile(orthoviewprofile.OrthoViewProfile):
         dataRange = opts.dataMax - opts.dataMin
         step      = 0.01 * dataRange
 
-        if   wheel > 0: self.intensityThres += step
-        elif wheel < 0: self.intensityThres -= step
+        if   wheel > 0: offset =  step
+        elif wheel < 0: offset = -step
         else:           return
 
-        if self.__selecting:
-            
-            voxel = self.__getVoxelLocation(canvasPos) 
 
-            if voxel is not None:
-                self.__selintSelect(voxel, canvas)
-                self.__refreshCanvases(ev, canvas) 
+        # See comment in OrthoViewProfile._zoomModeMouseWheel
+        # about timeout
+        def update():
+            self.intensityThres += offset
+            if self.__selecting:
+
+                voxel = self.__getVoxelLocation(canvasPos) 
+
+                if voxel is not None:
+                    self.__selintSelect(voxel, canvas)
+                    self.__refreshCanvases(ev, canvas)
+
+        async.idle(update, timeout=0.1)
 
                 
     def _chradModeMouseWheel(self, ev, canvas, wheel, mousePos, canvasPos):
@@ -1077,14 +1156,22 @@ class OrthoEditProfile(orthoviewprofile.OrthoViewProfile):
         select-by-intensity is re-run at the current mouse location.
         """ 
 
-        if   wheel > 0: self.searchRadius -= 5
-        elif wheel < 0: self.searchRadius += 5
+        if   wheel > 0: offset = -5
+        elif wheel < 0: offset =  5
         else:           return
 
-        if self.__selecting:
-            
-            voxel = self.__getVoxelLocation(canvasPos) 
+        # See comment in OrthoViewProfile._zoomModeMouseWheel
+        # about timeout
+        def update():
+
+            self.searchRadius += offset
+
+            if self.__selecting:
+
+                voxel = self.__getVoxelLocation(canvasPos) 
+
+                if voxel is not None:
+                    self.__selintSelect(voxel, canvas)
+                    self.__refreshCanvases(ev, canvas) 
 
-            if voxel is not None:
-                self.__selintSelect(voxel, canvas)
-                self.__refreshCanvases(ev, canvas) 
+        async.idle(update, timeout=0.1)
diff --git a/fsl/fsleyes/profiles/orthoviewprofile.py b/fsl/fsleyes/profiles/orthoviewprofile.py
index d23595f5a21593498bd54f7574c9b8826232c131..fd878510772238621c38dba732fc5c338a47957e 100644
--- a/fsl/fsleyes/profiles/orthoviewprofile.py
+++ b/fsl/fsleyes/profiles/orthoviewprofile.py
@@ -16,6 +16,7 @@ import numpy as np
 
 import fsl.fsleyes.profiles as profiles
 import fsl.fsleyes.actions  as actions
+import fsl.utils.async      as async
 import fsl.data.image       as fslimage
 import fsl.data.constants   as constants
 
@@ -332,7 +333,11 @@ class OrthoViewProfile(profiles.Profile):
         elif ch  in ('+', '='):   dirs[canvas.zax] =  1
         elif ch  in ('-', '_'):   dirs[canvas.zax] = -1
 
-        self._displayCtx.location.xyz = self.__offsetLocation(*dirs)
+        def update():
+            self._displayCtx.location.xyz = self.__offsetLocation(*dirs)
+
+        # See comment in _zoomModeMouseWheel about timeout
+        async.idle(update, timeout=0.1)
 
         
     #####################
@@ -357,7 +362,11 @@ class OrthoViewProfile(profiles.Profile):
 
         pos = self.__offsetLocation(*dirs)
 
-        self._displayCtx.location[canvas.zax] = pos[canvas.zax]
+        def update():
+            self._displayCtx.location[canvas.zax] = pos[canvas.zax]
+
+        # See comment in _zoomModeMouseWheel about timeout
+        async.idle(update, timeout=0.1)
 
         
     ####################
@@ -378,7 +387,17 @@ class OrthoViewProfile(profiles.Profile):
         """
         if   wheel > 0: wheel =  50
         elif wheel < 0: wheel = -50
-        canvas.zoom += wheel
+
+        # Over SSH/X11, mouse wheel events seem to get queued,
+        # and continue to get processed after the user has
+        # stopped spinning the mouse wheel, which is super
+        # frustrating. So we do the update asynchronously, and
+        # set a time out to drop the event, and prevent the
+        # horribleness from happening.
+        def update():
+            canvas.zoom += wheel
+        
+        async.idle(update, timeout=0.1)
 
         
     def _zoomModeChar(self, ev, canvas, key):
@@ -451,31 +470,12 @@ class OrthoViewProfile(profiles.Profile):
             canvas.getAnnotations().dequeue(self.__lastRect)
             self.__lastRect = None
 
-        rectXlen = abs(canvasPos[canvas.xax] - canvasDownPos[canvas.xax])
-        rectYlen = abs(canvasPos[canvas.yax] - canvasDownPos[canvas.yax])
-
-        if rectXlen == 0: return
-        if rectYlen == 0: return
-
-        rectXmid = (canvasPos[canvas.xax] + canvasDownPos[canvas.xax]) / 2.0
-        rectYmid = (canvasPos[canvas.yax] + canvasDownPos[canvas.yax]) / 2.0
-
-        xlen = self._displayCtx.bounds.getLen(canvas.xax)
-        ylen = self._displayCtx.bounds.getLen(canvas.yax)
-
-        xzoom   = xlen / rectXlen
-        yzoom   = ylen / rectYlen
-        zoom    = min(xzoom, yzoom) * 100.0
-        maxzoom = canvas.getConstraint('zoom', 'maxval')
+        xlo = min(canvasPos[canvas.xax], canvasDownPos[canvas.xax])
+        xhi = max(canvasPos[canvas.xax], canvasDownPos[canvas.xax])
+        ylo = min(canvasPos[canvas.yax], canvasDownPos[canvas.yax])
+        yhi = max(canvasPos[canvas.yax], canvasDownPos[canvas.yax])
 
-        if zoom >= maxzoom:
-            zoom = maxzoom
-
-        if zoom > canvas.zoom:
-            canvas.zoom = zoom
-            canvas.centreDisplayAt(rectXmid, rectYmid)
-
-        canvas.Refresh()
+        canvas.zoomTo(xlo, xhi, ylo, yhi)
         
         
     ###################
@@ -491,12 +491,12 @@ class OrthoViewProfile(profiles.Profile):
 
         If the target canvas is not zoomed in, this has no effect.
         """
-
-        if canvasPos is None:
-            return
         
         mouseDownPos, canvasDownPos = self.getMouseDownLocation()
 
+        if canvasPos     is None: return
+        if canvasDownPos is None: return
+
         xoff = canvasPos[canvas.xax] - canvasDownPos[canvas.xax]
         yoff = canvasPos[canvas.yax] - canvasDownPos[canvas.yax]
 
@@ -519,7 +519,11 @@ class OrthoViewProfile(profiles.Profile):
         elif key == wx.WXK_RIGHT: xoff =  2
         else:                     return
 
-        canvas.panDisplayBy(xoff, yoff)
+        def update():
+            canvas.panDisplayBy(xoff, yoff)
+
+        # See comment in _zoomModeMouseWheel about timeout
+        async.idle(update, timeout=0.1)
 
 
     #############
diff --git a/fsl/fsleyes/profiles/profilemap.py b/fsl/fsleyes/profiles/profilemap.py
index 16c365c1a12a98a5934f6d7ac48c69db10faf070..bc73b0a80af58f55295d745950c5a01407650cb0 100644
--- a/fsl/fsleyes/profiles/profilemap.py
+++ b/fsl/fsleyes/profiles/profilemap.py
@@ -159,8 +159,8 @@ altHandlerMap = {
         (('desel',  'MiddleMouseDrag'), ('pan', 'LeftMouseDrag')),
         (('selint', 'MiddleMouseDrag'), ('pan', 'LeftMouseDrag')),
 
-        (('sel',    'MouseWheel'),      ('chsize',  'MiddleMouse')),
-        (('desel',  'MouseWheel'),      ('chsize',  'MiddleMouse')),
+        (('sel',    'MouseWheel'),      ('chsize',  'MouseWheel')),
+        (('desel',  'MouseWheel'),      ('chsize',  'MouseWheel')),
         (('selint', 'MouseWheel'),      ('chthres', 'MouseWheel')),
 
         # Keyboard navigation works in the
diff --git a/fsl/fsleyes/views/histogrampanel.py b/fsl/fsleyes/views/histogrampanel.py
index 4a3ac926813bbdd0d7f2b6c852d212fe1f757985..19e4d79dc8d3bbd787c36388872ffd73ae9ed208 100644
--- a/fsl/fsleyes/views/histogrampanel.py
+++ b/fsl/fsleyes/views/histogrampanel.py
@@ -112,8 +112,9 @@ class HistogramPanel(plotpanel.OverlayPlotPanel):
         """Shows/hides a :class:`.HistogramControlPanel`. See
         :meth:`.ViewPanel.togglePanel`.
         """
-        self.togglePanel(
-            histogramcontrolpanel.HistogramControlPanel, self, location=wx.TOP)
+        self.togglePanel(histogramcontrolpanel.HistogramControlPanel,
+                         self,
+                         location=wx.RIGHT)
 
         
     def getActions(self):
diff --git a/fsl/fsleyes/views/orthopanel.py b/fsl/fsleyes/views/orthopanel.py
index f69bb26294f6e493aaac73fca4944a71cf78d36d..7ecba10f2c17fe7b12bd62dfaa9fb05b6d517d89 100644
--- a/fsl/fsleyes/views/orthopanel.py
+++ b/fsl/fsleyes/views/orthopanel.py
@@ -328,21 +328,23 @@ class OrthoPanel(canvaspanel.CanvasPanel):
         bg = self.getSceneOptions().bgColour
         fg = colourmaps.complementaryColour(bg)
 
-        bg = [int(round(c * 255)) for c in bg]
-        fg = [int(round(c * 255)) for c in fg]
+        # All wxwidgets things need colours
+        # to be specified between 0 and 255
+        intbg = [int(round(c * 255)) for c in bg]
+        intfg = [int(round(c * 255)) for c in fg]
 
-        self.getContentPanel().SetBackgroundColour(bg)
-        self.getContentPanel().SetForegroundColour(fg)
+        self.getContentPanel().SetBackgroundColour(intbg)
+        self.getContentPanel().SetForegroundColour(intfg)
 
         cbCanvas = self.getColourBarCanvas()
         if cbCanvas is not None:
             cbCanvas.textColour = fg
 
-        self.__xcanvas.SetBackgroundColour(bg)
-        self.__ycanvas.SetBackgroundColour(bg)
-        self.__zcanvas.SetBackgroundColour(bg)
+        self.__xcanvas.SetBackgroundColour(intbg)
+        self.__ycanvas.SetBackgroundColour(intbg)
+        self.__zcanvas.SetBackgroundColour(intbg)
 
-        self.__setLabelColours(bg, fg)
+        self.__setLabelColours(intbg, intfg)
 
         self.Refresh()
         self.Update()
@@ -406,8 +408,10 @@ class OrthoPanel(canvaspanel.CanvasPanel):
 
         for canvas, labels, show in zip(canvases, allLabels, shows):
 
+            # See WXGLSliceCanvas.Show for
+            # details of a horrible bug, and
+            # equally horrible workaround..
             canvas.Show(show)
-            self.__canvasSizer.Show(canvas, show)
 
             for label in labels.values():
                 self.__canvasSizer.Show(label, show and opts.showLabels)
@@ -456,11 +460,8 @@ class OrthoPanel(canvaspanel.CanvasPanel):
         :class:`.SliceCanvas`.
         """
 
-        sceneOpts = self.getSceneOptions()
-        allLabels = self.__xLabels.values() + \
-                    self.__yLabels.values() + \
-                    self.__zLabels.values()
-
+        sopts = self.getSceneOptions()
+        
         # Are we showing or hiding the labels?
         if len(self._overlayList) == 0:
             show = False
@@ -470,19 +471,23 @@ class OrthoPanel(canvaspanel.CanvasPanel):
 
         # Labels are only supported if we
         # have a volumetric reference image 
-        if   overlay is None:      show = False
-        elif sceneOpts.showLabels: show = True
-        else:                      show = False
+        if   overlay is None:  showLabels = False
+        elif sopts.showLabels: showLabels = True
+        else:                  showLabels = False
 
-        for lbl in allLabels:
-            self.__canvasSizer.Show(lbl, show)
+        canvases  = [self.__xcanvas,    self.__ycanvas,    self.__zcanvas]
+        allLabels = [self.__xLabels,    self.__yLabels,    self.__zLabels]
+        shows     = [sopts.showXCanvas, sopts.showYCanvas, sopts.showZCanvas]
+
+        for canvas, labels, show in zip(canvases, allLabels, shows):
+            for lbl in labels.values():
+                self.__canvasSizer.Show(lbl, show and showLabels)
 
         # If we're hiding the labels, do no more
-        if not show:
+        if not showLabels:
             self.PostSizeEvent()
             return
 
-
         log.debug('Refreshing orientation labels '
                   'according to {}'.format(overlay.name))
 
@@ -505,7 +510,7 @@ class OrthoPanel(canvaspanel.CanvasPanel):
         log.debug('Y orientation: {} - {}'.format(ylo, yhi))
         log.debug('Z orientation: {} - {}'.format(zlo, zhi))
 
-        bg = sceneOpts.bgColour
+        bg = sopts.bgColour
         fg = colourmaps.complementaryColour(bg)
         bg = [int(round(c * 255)) for c in bg]
         fg = [int(round(c * 255)) for c in fg]        
@@ -761,11 +766,14 @@ class OrthoPanel(canvaspanel.CanvasPanel):
                        space,         space,            space]
 
         # Add all those widgets to the grid sizer
-        flag = wx.ALIGN_CENTRE_HORIZONTAL | wx.ALIGN_CENTRE_VERTICAL
+        flag     = wx.ALIGN_CENTRE_HORIZONTAL | wx.ALIGN_CENTRE_VERTICAL
+        canvases = [self.__xcanvas, self.__ycanvas, self.__zcanvas]
         
         for w in widgets:
-            self.__canvasSizer.Add(w, flag=flag)
-                                          
+            
+            if w in canvases: self.__canvasSizer.Add(w, flag=flag | wx.EXPAND)
+            else:             self.__canvasSizer.Add(w, flag=flag)
+            
         self.getContentPanel().SetSizer(self.__canvasSizer)
 
         # Calculate/ adjust the appropriate sizes
diff --git a/fsl/fsleyes/views/plotpanel.py b/fsl/fsleyes/views/plotpanel.py
index a234b1d83486ea62f30910c72e3e2185e0606ea0..6b7b2afa153f26e0ef0425dcbba7878348e9154b 100644
--- a/fsl/fsleyes/views/plotpanel.py
+++ b/fsl/fsleyes/views/plotpanel.py
@@ -399,6 +399,11 @@ class PlotPanel(viewpanel.ViewPanel):
 
         for ds in toPlot:
             xlim, ylim = self.__drawOneDataSeries(ds, preproc, **plotArgs)
+
+            if (xlim[1] - xlim[0] < 0.0000000001) or \
+               (ylim[1] - ylim[0] < 0.0000000001):
+                continue
+            
             xlims.append(xlim)
             ylims.append(ylim)
 
@@ -942,6 +947,8 @@ class OverlayPlotPanel(PlotPanel):
         for ovl in self._overlayList:
             if ovl in self.__dataSeries:
                 continue
+
+            log.debug('Creating a DataSeries for overlay {}'.format(ovl))
                 
             ds, refreshTargets, refreshProps = self.createDataSeries(ovl)
 
diff --git a/fsl/fsleyes/views/powerspectrumpanel.py b/fsl/fsleyes/views/powerspectrumpanel.py
index 893eaebcec425812c6949f2565d7ac460ae9e9aa..5715c0cc05ddcdcfb24cce51b701ec5736ec9919 100644
--- a/fsl/fsleyes/views/powerspectrumpanel.py
+++ b/fsl/fsleyes/views/powerspectrumpanel.py
@@ -116,7 +116,7 @@ class PowerSpectrumPanel(plotpanel.OverlayPlotPanel):
         """
         self.togglePanel(pscontrol.PowerSpectrumControlPanel,
                          self,
-                         location=wx.TOP)
+                         location=wx.RIGHT)
 
         
     @actions.toggleControlAction(plotlistpanel.PlotListPanel)
diff --git a/fsl/fsleyes/views/timeseriespanel.py b/fsl/fsleyes/views/timeseriespanel.py
index dde4ece00d06acd3781cdc225d853b026c900286..b68a7880bac0942c4f9af6da454dd481279394ec 100644
--- a/fsl/fsleyes/views/timeseriespanel.py
+++ b/fsl/fsleyes/views/timeseriespanel.py
@@ -17,6 +17,7 @@ import                                                props
 
 import                                                plotpanel
 import fsl.data.featimage                          as fslfeatimage
+import fsl.data.featresults                        as featresults
 import fsl.data.melodicimage                       as fslmelimage
 import fsl.data.image                              as fslimage
 import fsl.fsleyes.actions                         as actions
@@ -171,7 +172,7 @@ class TimeSeriesPanel(plotpanel.OverlayPlotPanel):
         """
         self.togglePanel(timeseriescontrolpanel.TimeSeriesControlPanel,
                          self,
-                         location=wx.TOP)
+                         location=wx.RIGHT)
 
         
     def getActions(self):
@@ -204,10 +205,15 @@ class TimeSeriesPanel(plotpanel.OverlayPlotPanel):
         tss = [self.getDataSeries(o) for o in overlays]
         tss = [ts for ts in tss if ts is not None]
 
-        for i, ts in enumerate(list(tss)):
+        # Include all of the extra model series
+        # for all FEATTimeSeries instances
+        newTss = []
+        for ts in tss:
             if isinstance(ts, plotting.FEATTimeSeries):
-                tss.pop(i)
-                tss = tss[:i] + ts.getModelTimeSeries() + tss[i:]
+                newTss += ts.getModelTimeSeries()
+            else:
+                newTss.append(ts)
+        tss = newTss
 
         for ts in tss:
 
@@ -241,24 +247,55 @@ class TimeSeriesPanel(plotpanel.OverlayPlotPanel):
         data to be plotted), a tuple of ``None`` values is returned.
         """
 
-        if not (isinstance(overlay, fslimage.Image) and overlay.is4DImage()):
+        if not isinstance(overlay, fslimage.Image):
             return None, None, None
 
-        if isinstance(overlay, fslfeatimage.FEATImage):
-            ts = plotting.FEATTimeSeries(self, overlay, self._displayCtx)
+        if overlay.dataSource is not None:
+            featPath = featresults.getAnalysisDir(overlay.dataSource)
+        else:
+            featPath = None
+
+        # Is this a FEAT filtered_func_data image,
+        # or an image in a FEAT directory?
+        if isinstance(overlay, fslfeatimage.FEATImage) or featPath is not None:
+
+            dataPath  = featresults.getDataFile(featPath)
+            featImage = self._overlayList.find(dataPath)
+
+            # If this is an image in a FEAT directory, but the
+            # filtered_func_data for that FEAT directory has
+            # not been loaded, we show nothing. 
+            if not isinstance(overlay, fslfeatimage.FEATImage) and \
+               featImage is None:
+                return None, None, None
+
+            # If the filtered_func for this FEAT analysis
+            # has been loaded, we show its time series.
+            overlay  = featImage
+            ts        = plotting.FEATTimeSeries(self,
+                                                overlay,
+                                                self._displayCtx)
             targets   = [self._displayCtx]
             propNames = ['location']
-            
+
+        # If this is a melodic IC image, and we are
+        # currently configured to plot component ICs,
+        # we use a MelodicTimeSeries object.
         elif isinstance(overlay, fslmelimage.MelodicImage) and \
              self.plotMelodicICs:
             ts = plotting.MelodicTimeSeries(self, overlay, self._displayCtx)
             targets   = [self._displayCtx.getOpts(overlay)]
-            propNames = ['volume'] 
-            
-        else:
+            propNames = ['volume']
+
+        # Otherwise we just plot
+        # bog-standard 4D voxel data
+        elif len(overlay.shape) == 4 and overlay.shape[3] > 1:
             ts = plotting.VoxelTimeSeries(self, overlay, self._displayCtx)
             targets   = [self._displayCtx]
-            propNames = ['location'] 
+            propNames = ['location']
+            
+        else:
+            return None, None, None
 
         ts.colour    = self.getOverlayPlotColour(overlay)
         ts.alpha     = 1
diff --git a/fsl/fsleyes/views/viewpanel.py b/fsl/fsleyes/views/viewpanel.py
index 5c890174579ff3f3fc7a42c01fb842c1540498c6..1d6f62879c4142ec349b887960e1bb2f1fcbcf70 100644
--- a/fsl/fsleyes/views/viewpanel.py
+++ b/fsl/fsleyes/views/viewpanel.py
@@ -9,6 +9,7 @@ for all of the *FSLeyes view* panels. See the :mod:`~fsl.fsleyes` package
 documentation for more details.
 """
 
+
 import logging
 
 import                   wx
@@ -111,12 +112,16 @@ class ViewPanel(fslpanel.FSLEyesPanel):
         self.__centrePanel = None
         self.__panels      = {}
 
+        # See note in FSLEyesFrame about
+        # the user of aero docking guides.
         self.__auiMgr = aui.AuiManager(
             self,
             agwFlags=(aui.AUI_MGR_RECTANGLE_HINT          |
                       aui.AUI_MGR_NO_VENETIAN_BLINDS_FADE |
                       aui.AUI_MGR_ALLOW_FLOATING          |
-                      aui.AUI_MGR_LIVE_RESIZE)) 
+                      aui.AUI_MGR_AERO_DOCKING_GUIDES     |
+                      aui.AUI_MGR_LIVE_RESIZE))
+
         self.__auiMgr.Bind(aui.EVT_AUI_PANE_CLOSE, self.__onPaneClose)
 
         # Use a different listener name so that subclasses
@@ -552,37 +557,36 @@ class ViewPanel(fslpanel.FSLEyesPanel):
             ev.Skip()
             panel = ev.GetPane().window
 
-        log.debug('Panel closed: {}'.format(type(panel).__name__))
-        
         if isinstance(panel, (fslpanel  .FSLEyesPanel,
                               fsltoolbar.FSLEyesToolBar)):
             
-            self.__panels.pop(type(panel))
-
-            # calling fslpanel.FSLEyesPanel.destroy()
-            # here -  wx.Destroy is done below
-            panel.destroy()
-
-            # Even when the user closes a pane,
-            # AUI does not detach said pane -
-            # we have to do it manually
-            self.__auiMgr.DetachPane(panel)
-            self.__auiMgrUpdate()
-
-        # WTF AUI. Sometimes this method gets called
-        # twice for a panel, the second time with a
-        # reference to a wx._wxpyDeadObject; in such
-        # situations, the Destroy method call below
-        # will result in an exception being raised.
-        else:
-            return
+            panel = self.__panels.pop(type(panel), None)
+
+            # WTF AUI. Sometimes this method gets called
+            # twice for a panel, the second time with a
+            # reference to a wx._wxpyDeadObject; in such
+            # situations, the Destroy method call below
+            # will result in an exception being raised.
+            if panel is not None:
         
-        panel.Destroy()
+                log.debug('Panel closed: {}'.format(type(panel).__name__))
+
+                # calling fslpanel.FSLEyesPanel.destroy()
+                # here -  wx.Destroy is done below
+                panel.destroy()
 
+                # Even when the user closes a pane,
+                # AUI does not detach said pane -
+                # we have to do it manually
+                self.__auiMgr.DetachPane(panel)
+                self.__auiMgrUpdate()
+                
+                panel.Destroy()
 
 #
-# Here I am monkey patching the wx.agw.aui.framemanager.AuiFloatingFrame
-# __init__ method.
+# Here I am monkey patching the
+# wx.agw.aui.framemanager.AuiFloatingFrame.__init__ method.
+#
 #
 # I am doing this because I have observed some strange behaviour when running
 # a remote instance of this application over an SSH/X11 session, with the X11
@@ -627,3 +631,30 @@ def AuiFloatingFrame__init__(*args, **kwargs):
 # Patch my constructor in to the class definition.
 AuiFloatingFrame__real__init__ = aui.AuiFloatingFrame.__init__
 aui.AuiFloatingFrame.__init__  = AuiFloatingFrame__init__
+
+# I am also monkey-patching the wx.lib.agw.aui.AuiDockingGuide.__init__ method,
+# because in this instance, when running over SSH/X11, the wx.FRAME_TOOL_WINDOW
+# style seems to result in the docking guide frames being given title bars,
+# which is quite undesirable.
+def AuiDockingGuide__init__(*args, **kwargs):
+
+    if 'style' in kwargs:
+        style = kwargs['style']
+
+    # This is the default style, as defined 
+    # in the AuiDockingGuide constructor
+    else:
+        style = (wx.FRAME_TOOL_WINDOW |
+                 wx.FRAME_STAY_ON_TOP |
+                 wx.FRAME_NO_TASKBAR  |
+                 wx.NO_BORDER)
+
+    style &= ~wx.FRAME_TOOL_WINDOW
+
+    kwargs['style'] = style
+    
+    return AuiDockingGuide__real__init__(*args, **kwargs)
+
+
+AuiDockingGuide__real__init__ = aui.AuiDockingGuide.__init__
+aui.AuiDockingGuide.__init__  = AuiDockingGuide__init__
diff --git a/fsl/tools/__init__.py b/fsl/tools/__init__.py
index 6ba4bb324e857835896cdfc3e30545e702ca90d9..8d866cebef2e1fc732db7e6d460cd7bd3564bb49 100644
--- a/fsl/tools/__init__.py
+++ b/fsl/tools/__init__.py
@@ -27,6 +27,11 @@ package) a tool module must provide the following module level attributes:
 
 ``FSL_HELPPAGE``  Optional. A URL to a web page providing help/documentation.
 
+``FSL_INIT``      Optional. A function which is called called before the
+                  tool arguments are parsed. The return value of this function
+                  will be passed on to the ``FSL_CONTEXT`` function (if it is
+                  provided).
+
 ``FSL_PARSEARGS`` Optional. A function which is given a list of command line
                   arguments specific to the tool. The function should parse
                   the arguments and return, for example, an :mod:`argparse`
@@ -63,16 +68,20 @@ asked to run a ``fslpy`` tool:
 
         import fsl.tools.mytool as mytool
 
-2.  Calls the ``FSL_PARSEARGS`` function::
+2.  Calls the ``FSL_INIT`` function::
+
+        initVal = mytool.FSL_INIT()
+
+3.  Calls the ``FSL_PARSEARGS`` function::
 
         parsedArgs = mytool.FSL_PARSEARGS(cmdLineArgs)
 
-3.  Calls the ``FSL_CONTEXT`` function, giving it the value returned by
+4.  Calls the ``FSL_CONTEXT`` function, giving it the value returned by
     ``FSL_PARSEARGS``::
 
-        context = mytool.FSL_CONTEXT(parsedArgs)
+        context = mytool.FSL_CONTEXT(parsedArgs, initVal)
 
-4.
+5.
 
  a. If the tool is a command line application, calls the ``FSL_EXECUTE``
     function, passing it the arguments and the context::
diff --git a/fsl/tools/bet.py b/fsl/tools/bet.py
index 7d026c9100ea41e333e542ac8cf7b3f6bfc9106b..cb0fe09ff5ecc79c9cb954f8c0a515fb66850210 100644
--- a/fsl/tools/bet.py
+++ b/fsl/tools/bet.py
@@ -337,6 +337,6 @@ def runBet(parent, opts):
 
 FSL_TOOLNAME  = 'BET'
 FSL_HELPPAGE  = 'bet'
-FSL_CONTEXT   = lambda args: Options()
+FSL_CONTEXT   = lambda *a: Options()
 FSL_INTERFACE = interface
 FSL_ACTIONS   = [('Run BET', runBet)]
diff --git a/fsl/tools/feat.py b/fsl/tools/feat.py
index 759335e5ae3a38cbd89e57ef45271217ff7824a0..b0a30e73efbbdc35ef3b9a6e0a8fe24c3b5032b2 100644
--- a/fsl/tools/feat.py
+++ b/fsl/tools/feat.py
@@ -552,5 +552,5 @@ def interface(parent, args, featOpts):
 
 FSL_TOOLNAME  = 'FEAT'
 FSL_HELPPAGE  = 'feat'
-FSL_CONTEXT   = lambda args: Options()
+FSL_CONTEXT   = lambda *a: Options()
 FSL_INTERFACE = interface
diff --git a/fsl/tools/flirt.py b/fsl/tools/flirt.py
index 609252fa2282b17509531a4388be34a00a41e1bd..b89c3bcac26afadda4ca41afa2186bd834271d2b 100644
--- a/fsl/tools/flirt.py
+++ b/fsl/tools/flirt.py
@@ -196,6 +196,6 @@ def interface(parent, args, opts):
 
 FSL_TOOLNAME  = 'FLIRT'
 FSL_HELPPAGE  = 'flirt'
-FSL_CONTEXT   = lambda args: Options()
+FSL_CONTEXT   = lambda *a: Options()
 FSL_INTERFACE = interface
 FSL_ACTIONS   = [('Run FLIRT', runFlirt)]
diff --git a/fsl/tools/fsleyes.py b/fsl/tools/fsleyes.py
index edc8fd4a35707d410dfd48b9aab8f4852670e040..fe0201e218de4329362e63059bd184dcaa9614e6 100644
--- a/fsl/tools/fsleyes.py
+++ b/fsl/tools/fsleyes.py
@@ -25,18 +25,30 @@ import logging
 import textwrap
 import argparse
 
-import fsl.fsleyes.fsleyes_parseargs as fsleyes_parseargs
-import fsl.fsleyes.displaycontext    as displaycontext
-import fsl.fsleyes.perspectives      as perspectives
-import fsl.fsleyes.overlay           as fsloverlay
-import fsl.utils.status              as status
-import fsl.utils.async               as async
-import fsl.data.strings              as strings
+import fsl.fsleyes.perspectives as perspectives
+import fsl.utils.status         as status
+import fsl.utils.async          as async
+import fsl.data.strings         as strings
 
 
 log = logging.getLogger(__name__)
 
 
+def init():
+    """Creates and returns a :class:`.FSLEyesSplash` frame. """
+
+    import fsl.fsleyes.splash as fslsplash
+
+    frame = fslsplash.FSLEyesSplash(None)
+
+    frame.CentreOnScreen()
+    frame.Show()
+    frame.Refresh()
+    frame.Update()
+
+    return frame
+
+
 def parseArgs(argv):
     """Parses the given ``fsleyes`` command line arguments. See the
     :mod:`.fsleyes_parseargs` module for details on the ``fsleyes`` command
@@ -45,6 +57,8 @@ def parseArgs(argv):
     :arg argv: command line arguments for ``fsleyes``.
     """
 
+    import fsl.fsleyes.fsleyes_parseargs as fsleyes_parseargs
+
     parser = argparse.ArgumentParser(
         add_help=False,
         formatter_class=argparse.RawDescriptionHelpFormatter)
@@ -72,7 +86,7 @@ def parseArgs(argv):
                                        description)
 
 
-def context(args):
+def context(args, splash):
     """Creates the ``fsleyes`` context.
 
     This function does a few things:
@@ -88,7 +102,9 @@ def context(args):
 
      4. Loads all of the overlays which were passed in on the command line.
 
-    :arg args: Parsed command line arguments (see :func:`parseArgs`).
+    :arg args:   Parsed command line arguments (see :func:`parseArgs`).
+
+    :arg splash: The :class:`.FSLEyesSplash` frame, created in :func:`init`.
 
     :returns: a tuple containing:
                 - the :class:`.OverlayList`
@@ -96,36 +112,28 @@ def context(args):
                 - the :class:`.FSLEyesSplash` frame
     """
 
-    import fsl.fsleyes.splash as fslsplash
-
-    # Create a splash screen, and use it
-    # to initialise the OpenGL context
+    import fsl.fsleyes.overlay           as fsloverlay
+    import fsl.fsleyes.fsleyes_parseargs as fsleyes_parseargs
+    import fsl.fsleyes.displaycontext    as displaycontext
+    import fsl.fsleyes.gl                as fslgl
+    import props
     
+    props.initGUI()
+
     # The splash screen is used as the parent of the dummy
     # canvas created by the gl.getWXGLContext function; the
     # splash screen frame is returned by this function, and
-    # passed through to the interface function above, which
-    # takes care of destroying it.
-    frame = fslsplash.FSLEyesSplash(None)
-
-    frame.CentreOnScreen()
-    frame.Show()
-    frame.Refresh()
-    frame.Update()
-
-    import props
-    import fsl.fsleyes.gl as fslgl
-
-    props.initGUI()
+    # passed through to the interface function below, which
+    # takes care of destroying it.    
     
     # force the creation of a wx.glcanvas.GLContext object,
     # and initialise OpenGL version-specific module loads.
-    fslgl.getWXGLContext(frame)
+    fslgl.getWXGLContext(splash)
     fslgl.bootstrap(args.glversion)
 
     # Redirect status updates
     # to the splash frame
-    status.setTarget(frame.SetStatus)
+    status.setTarget(splash.SetStatus)
 
     # Create the overlay list (only one of these
     # ever exists) and the master DisplayContext.
@@ -153,7 +161,7 @@ def context(args):
     # be updated with the currently loading overlay name
     fsleyes_parseargs.applyOverlayArgs(args, overlayList, displayCtx)  
 
-    return overlayList, displayCtx, frame
+    return overlayList, displayCtx, splash
 
 
 def interface(parent, args, ctx):
@@ -181,9 +189,10 @@ def interface(parent, args, ctx):
     :returns: the :class:`.FSLEyesFrame` that was created.
     """
 
-    import                      wx
-    import fsl.fsleyes.frame as fsleyesframe
-    import fsl.fsleyes.views as views
+    import                                  wx
+    import fsl.fsleyes.fsleyes_parseargs as fsleyes_parseargs
+    import fsl.fsleyes.frame             as fsleyesframe
+    import fsl.fsleyes.views             as views
 
     overlayList, displayCtx, splashFrame = ctx
 
@@ -219,7 +228,21 @@ def interface(parent, args, ctx):
     splashFrame.Hide()
     splashFrame.Refresh()
     splashFrame.Update()
-    wx.CallLater(250, splashFrame.Close)
+
+    # In certain instances under Linux/GTK,
+    # closing the splash screen will crash
+    # the application. No idea why. So if
+    # running GTK, we leave the splash
+    # screen hidden, but not closed, and
+    # close it when the main frame is
+    # closed.
+    if wx.Platform == '__WXGTK__':
+        def onFrameDestroy(ev):
+            ev.Skip()
+            splashFrame.Close()
+        frame.Bind(wx.EVT_WINDOW_DESTROY, onFrameDestroy)
+    else:
+        wx.CallLater(250, splashFrame.Close)
 
     # If a perspective has been specified,
     # we load the perspective
@@ -283,8 +306,10 @@ def about(frame, ctx):
 
 
 FSL_TOOLNAME  = 'FSLeyes'
+FSL_HELPPAGE  = 'http://users.fmrib.ox.ac.uk/~paulmc/fsleyes/'
 FSL_INTERFACE = interface
 FSL_CONTEXT   = context
+FSL_INIT      = init
 FSL_PARSEARGS = parseArgs
 FSL_ACTIONS   = [(strings.actions['AboutAction'],            about),
                  (strings.actions['DiagnosticReportAction'], diagnosticReport)]
diff --git a/fsl/tools/render.py b/fsl/tools/render.py
index 0352af31984ac4271d4c58d4878b7f23e94af78e..327e97ed667e8b2911ea9ad517a8359aea1862cf 100644
--- a/fsl/tools/render.py
+++ b/fsl/tools/render.py
@@ -520,7 +520,7 @@ def parseArgs(argv):
     return namespace
 
 
-def context(args):
+def context(args, *a, **kwa):
 
     # Create an image list and display context.
     # The DisplayContext, Display and DisplayOpts
diff --git a/fsl/utils/async.py b/fsl/utils/async.py
index 73330044b4653df87e8142df5a3178b8a367ae05..801d0debbc0516623ca095390c0ea5431182b7a3 100644
--- a/fsl/utils/async.py
+++ b/fsl/utils/async.py
@@ -31,6 +31,8 @@ task to run. It waits until all the threads have finished, and then runs
 the task (via :func:`idle`).
 """
 
+
+import time
 import Queue
 import logging
 import threading
@@ -125,12 +127,18 @@ def _wxIdleLoop(ev):
         
     ev.Skip()
 
-    try:                task, args, kwargs = _idleQueue.get_nowait()
-    except Queue.Empty: return
+    try:
+        task, schedtime, timeout, args, kwargs = _idleQueue.get_nowait()
+    except Queue.Empty:
+        return
+
+    name    = getattr(task, '__name__', '<unknown>')
+    now     = time.time()
+    elapsed = now - schedtime
 
-    name = getattr(task, '__name__', '<unknown>')
-    log.debug('Running function ({}) on wx idle loop'.format(name))
-    task(*args, **kwargs)
+    if timeout == 0 or (elapsed < timeout):
+        log.debug('Running function ({}) on wx idle loop'.format(name))
+        task(*args, **kwargs)
 
     if _idleQueue.qsize() > 0:
         ev.RequestMore()
@@ -141,6 +149,12 @@ def idle(task, *args, **kwargs):
 
     :arg task: The task to run.
 
+    :arg timeout: Optional. If provided, must be provided as a keyword
+                  argument. Specifies a time out, in seconds. If this
+                  amount of time passes before the function gets
+                  scheduled to be called on the idle loop, the function
+                  is not called, and is dropped from the queue.
+
     All other arguments are passed through to the task function.
 
     If a ``wx.App`` is not running, the task is called directly.
@@ -149,6 +163,9 @@ def idle(task, *args, **kwargs):
     global _idleRegistered
     global _idleTasks
 
+    schedtime = time.time()
+    timeout   = kwargs.pop('timeout', 0)
+
     if _haveWX():
         import wx
 
@@ -159,7 +176,7 @@ def idle(task, *args, **kwargs):
         name = getattr(task, '__name__', '<unknown>')
         log.debug('Scheduling idle task ({}) on wx idle loop'.format(name))
 
-        _idleQueue.put_nowait((task, args, kwargs))
+        _idleQueue.put_nowait((task, schedtime, timeout, args, kwargs))
             
     else:
         log.debug('Running idle task directly') 
diff --git a/fsl/utils/dialog.py b/fsl/utils/dialog.py
index 92ff78d09800dd9c4cd404a2e2bb6130dc5c7e53..f922d06d13e3bc08c04e28df179f14f8cd4a72c2 100644
--- a/fsl/utils/dialog.py
+++ b/fsl/utils/dialog.py
@@ -710,7 +710,7 @@ class FSLDirDialog(wx.Dialog):
 
 class CheckBoxMessageDialog(wx.Dialog):
     """A ``wx.Dialog`` which displays a message, a ``wx.CheckBox``, and
-    an "Ok" button.
+    an *Ok* button.
     """
 
     
@@ -735,13 +735,13 @@ class CheckBoxMessageDialog(wx.Dialog):
         
         :arg cbState:   Initial state for the ``wx.CheckBox``.
         
-        :arg btnText:   Text to show on the button. Defaults to "OK".
+        :arg btnText:   Text to show on the button. Defaults to *OK*.
         
         :arg icon:      A ``wx`` icon identifier (e.g. 
                         ``wx.ICON_EXCLAMATION``).
         
         :arg style:     Passed through to the ``wx.Dialog.__init__`` method.
-                        Defaults to ``wx.DEFAULT_DIALOG_STYLE`.
+                        Defaults to ``wx.DEFAULT_DIALOG_STYLE``.
         """
 
         if style     is None: style     = wx.DEFAULT_DIALOG_STYLE
diff --git a/fsl/utils/memoize.py b/fsl/utils/memoize.py
index cf40efe67f514c82119bad87f0e2c33324bb17b4..07f6f1242643c7c25288e789639e4bf0de8d712b 100644
--- a/fsl/utils/memoize.py
+++ b/fsl/utils/memoize.py
@@ -10,10 +10,14 @@ a function:
  .. autosummary::
     :nosignatures:
 
+    Instanceify
     memoizeMD5
+    skipUnchanged
 """
 
+
 import hashlib
+import functools
 
 
 def memoizeMD5(func):
@@ -46,3 +50,129 @@ def memoizeMD5(func):
         return result
 
     return wrapper
+
+
+def skipUnchanged(func):
+    """This decorator is intended for use with *setter* functions - a function
+     which accepts a name and a value, and is intended to set some named
+     attribute to the given value.
+
+    This decorator keeps a cache of name-value pairs. When the decorator is
+    called with a specific name and value, the cache is checked and, if the
+    given value is the same as the cached value, the decorated function is
+    *not* called. If the given value is different from the cached value (or
+    there is no value), the decorated function is called.
+
+    .. note:: This decorator ignores the return value of the decorated
+              function.
+
+    :returns: ``True`` if the underlying setter function was called, ``False``
+              otherwise.
+    """
+
+    import numpy as np
+    
+    cache = {}
+    
+    def wrapper(name, value, *args, **kwargs):
+
+        oldVal = cache.get(name, None)
+
+        if oldVal is not None:
+            
+            oldIsArray = isinstance(oldVal, np.ndarray)
+            newIsArray = isinstance(value,  np.ndarray)
+            isarray    = oldIsArray or newIsArray
+
+            if isarray: nochange = np.all(oldVal == value)
+            else:       nochange =        oldVal == value
+
+            if nochange:
+                return False 
+
+        func(name, value, *args, **kwargs)
+
+        cache[name] = value
+
+        return True
+
+    return wrapper
+
+
+class Instanceify(object):
+    """This class is intended to be used to decorate other decorators, so they
+    can be applied to instance methods. For example, say we have the following
+    class::
+
+        class Container(object):
+
+            def __init__(self):
+                self.__items = {}
+
+            @skipUnchanged
+            def set(self, name, value):
+                self.__items[name] = value
+
+    
+    Given this definition, a single :func:`skipUnchanged` decorator will be
+    created and shared amongst all ``Container`` instances. This is not ideal,
+    as the value cache created by the :func:`skipUnchanged` decorator should
+    be associated with a single ``Container`` instance.
+
+    
+    By redefining the ``Container`` class definition like so::
+
+    
+        class Container(object):
+
+            def __init__(self):
+                self.__items = {}
+
+            @Instanceify(skipUnchanged)
+            def set(self, name, value):
+                self.__items[name] = value
+
+
+    a separate :func:`skipUnchanged` decorator is created for, and associated
+    with, every ``Container`` instance.
+
+    
+    This is achieved because an ``Instanceify`` instance is a descriptor. When
+    first accessed as an instance attribute, an ``Instanceify`` instance will
+    create the real decorator function, and replace itself on the instance.
+    """
+
+    
+    def __init__(self, realDecorator):
+        """Create an ``Instanceify`` decorator.
+
+        :arg realDecorator: A reference to the decorator that is to be
+                            *instance-ified*.
+        """
+
+        self.__realDecorator = realDecorator
+        self.__func          = None
+
+
+    def __call__(self, func):
+        """Called immediately after :meth:`__init__`, and passed the method
+        that is to be decorated.
+        """
+        self.__func = func
+        return self
+
+
+    def __get__(self, instance, cls):
+        """When an ``Instanceify`` instance is accessed as an attribute of
+        another object, it will create the real (instance-ified) decorator,
+        and replace itself on the instance with the real decorator.
+        """
+
+        if instance is None:
+            return self.__func
+
+        method    = functools.partial(self.__func, instance)
+        decMethod = self.__realDecorator(method)
+
+        setattr(instance, self.__func.__name__, decMethod)
+        return functools.update_wrapper(decMethod, self.__func)
diff --git a/fsl/version.py b/fsl/version.py
index 5a716b7ffda64bab500bcc50d360455c85e832f7..78d9ac1425f75fb6169845181152303c96cf8708 100644
--- a/fsl/version.py
+++ b/fsl/version.py
@@ -10,9 +10,14 @@ version number and information.
 .. autosummary::
 
    __version__
+   __vcs_version__
 
 .. todo:: Define a formal ``fslpy`` version number updating scheme.
 """
 
-__version__ = '0.1'
+__version__ = '0.9a'
 """Current version number, as a string. """
+
+
+__vcs_version__ = '0.9a'
+"""VCS (Version Control System) version number, for internal use. """
diff --git a/fsleyes_doc/ic_classification.rst b/fsleyes_doc/ic_classification.rst
index d581adea681d0ac2bc68c2af1efb7186e65c806c..5404f75ef23e5c6e956ead798ecb59e27ac1613e 100644
--- a/fsleyes_doc/ic_classification.rst
+++ b/fsleyes_doc/ic_classification.rst
@@ -11,9 +11,12 @@ IC classification
 
 .. sidebar:: I can't navigate with the keyboard!
 
-             Under OSX, you may need to enable *Full keyboard access* for the
-             melodic classification panel to work with keyboard
-             navigation/focus.  This setting can be changed through *System
-             Preferences* |right_arrow| *Keyboard* |right_arrow| *Shortcuts*,
-             and changing *Full Keyboard Access* to *All controls*.
-             
+             Under OSX, you may have focus-related issues while navigating
+             around the IC classification panel with the keyboard. 
+
+             If this is happening to you, you may need to enable *Full
+             keyboard access* for the melodic classification panel to work
+             with keyboard navigation/focus.  This setting can be changed
+             through *System Preferences* |right_arrow| *Keyboard*
+             |right_arrow| *Shortcuts*, and changing *Full Keyboard Access* to
+             *All controls*.