From cce833cfad230282436d9df371673761e0cc3231 Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauld.mccarthy@gmail.com>
Date: Mon, 30 Nov 2015 16:29:08 +0000
Subject: [PATCH] New module, displaydefaults, which attempts to configure
 sensible default display settings for new overlays. Used by the addfile
 action, the overlay list panel, and at startup via fsleyes_parseargs.

---
 fsl/fsleyes/actions/openfile.py          |  16 ++-
 fsl/fsleyes/actions/openstandard.py      |   4 +-
 fsl/fsleyes/controls/overlaylistpanel.py |  22 +++-
 fsl/fsleyes/displaycontext/volumeopts.py |   9 ++
 fsl/fsleyes/displaydefaults.py           | 154 +++++++++++++++++++++++
 fsl/fsleyes/fsleyes_parseargs.py         |  14 ++-
 fsl/fsleyes/overlay.py                   |  14 +--
 7 files changed, 213 insertions(+), 20 deletions(-)
 create mode 100644 fsl/fsleyes/displaydefaults.py

diff --git a/fsl/fsleyes/actions/openfile.py b/fsl/fsleyes/actions/openfile.py
index d74adfec3..dedbf4217 100644
--- a/fsl/fsleyes/actions/openfile.py
+++ b/fsl/fsleyes/actions/openfile.py
@@ -11,6 +11,8 @@ load overlay files into the :class:`.OverlayList`.
 
 import action
 
+import fsl.fsleyes.displaydefaults as displaydefaults
+
 
 class OpenFileAction(action.Action):
     """The ``OpenFileAction`` allows the user to add files to the
@@ -34,7 +36,15 @@ class OpenFileAction(action.Action):
         """Calls :meth:`.OverlayList.addOverlays` method. If overlays were added,
         updates the :attr:`.DisplayContext.selectedOverlay` accordingly.
         """
+
+        overlays = self.__overlayList.addOverlays()
+
+        if len(overlays) == 0:
+            return
         
-        if self.__overlayList.addOverlays():
-            self.__displayCtx.selectedOverlay = \
-                self.__displayCtx.overlayOrder[-1]
+        self.__displayCtx.selectedOverlay = self.__displayCtx.overlayOrder[-1]
+
+        for overlay in overlays:
+            displaydefaults.displayDefaults(overlay,
+                                            self.__overlayList,
+                                            self.__displayCtx)
diff --git a/fsl/fsleyes/actions/openstandard.py b/fsl/fsleyes/actions/openstandard.py
index 66032d814..5f37bf69b 100644
--- a/fsl/fsleyes/actions/openstandard.py
+++ b/fsl/fsleyes/actions/openstandard.py
@@ -49,6 +49,8 @@ class OpenStandardAction(action.Action):
         added some overlays, updates the
         :attr:`.DisplayContext.selectedOverlay` accordingly.
         """
-        if self.__overlayList.addOverlays(self.__stddir, addToEnd=False):
+        added = self.__overlayList.addOverlays(self.__stddir, addToEnd=False)
+        
+        if len(added) > 0:
             self.__displayCtx.selectedOverlay = \
                 self.__displayCtx.overlayOrder[0]
diff --git a/fsl/fsleyes/controls/overlaylistpanel.py b/fsl/fsleyes/controls/overlaylistpanel.py
index 33ea84a9a..62a4c5b73 100644
--- a/fsl/fsleyes/controls/overlaylistpanel.py
+++ b/fsl/fsleyes/controls/overlaylistpanel.py
@@ -15,11 +15,12 @@ import wx
 
 import props
 
-import pwidgets.elistbox as elistbox
+import pwidgets.elistbox           as elistbox
 
-import fsl.fsleyes.panel as fslpanel
-import fsl.fsleyes.icons as icons
-import fsl.data.image    as fslimage
+import fsl.fsleyes.panel           as fslpanel
+import fsl.fsleyes.icons           as icons
+import fsl.fsleyes.displaydefaults as displaydefaults
+import fsl.data.image              as fslimage
 
 
 log = logging.getLogger(__name__)
@@ -207,8 +208,17 @@ class OverlayListPanel(fslpanel.FSLEyesPanel):
         """Called when the *add* button on the list box is pressed.
         Calls the :meth:`.OverlayList.addOverlays` method.
         """
-        if self._overlayList.addOverlays():
-            self._displayCtx.selectedOverlay = len(self._overlayList) - 1
+        overlays = self._overlayList.addOverlays()
+
+        if len(overlays) == 0:
+            return
+        
+        self._displayCtx.selectedOverlay = len(self._overlayList) - 1
+
+        for overlay in overlays:
+            displaydefaults.displayDefaults(overlay,
+                                            self._overlayList,
+                                            self._displayCtx)
 
 
     def __lbRemove(self, ev):
diff --git a/fsl/fsleyes/displaycontext/volumeopts.py b/fsl/fsleyes/displaycontext/volumeopts.py
index f7199d85b..2ac74501e 100644
--- a/fsl/fsleyes/displaycontext/volumeopts.py
+++ b/fsl/fsleyes/displaycontext/volumeopts.py
@@ -621,6 +621,15 @@ class VolumeOpts(ImageOpts):
                            display,
                            display.getSyncPropertyName('contrast'))
 
+            # If centreRanges, linkLowRanges or linkHighRanges
+            # have been set to True (this will happen if they
+            # are true on the parent VolumeOpts instance), make
+            # sure the property / listener states are up to date.
+            if self.centreRanges: self.__centreRangesChanged()
+            else:
+                if self.linkLowRanges:  self.__linkLowRangesChanged()
+                if self.linkHighRanges: self.__linkHighRangesChanged()
+
 
     @actions.action
     def resetDisplayRange(self):
diff --git a/fsl/fsleyes/displaydefaults.py b/fsl/fsleyes/displaydefaults.py
new file mode 100644
index 000000000..5ce8cff7e
--- /dev/null
+++ b/fsl/fsleyes/displaydefaults.py
@@ -0,0 +1,154 @@
+#!/usr/bin/env python
+#
+# displaydefaults.py - Routines for configuring default overlay display
+#                      settings.
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+"""This module provides the :func:`displayDefaults` function, which is used
+for configuring default overlay display settings.
+
+The :displayDefaults` function is called when *FSLeyes* is started, and when 
+new overlays are loaded.
+"""
+
+
+import re
+import sys
+import logging
+
+import fsl.data.image as fslimage
+
+
+log = logging.getLogger(__name__)
+
+
+def displayDefaults(overlay, overlayList, displayCtx):
+    """Configure default display settings for the given overlay.
+
+    :arg overlay:     The overlay object (e.g. an :class:`.Image` instance).
+    :arg overlayList: The :class:`.OverlayList`.
+    :arg displayCtx:  The :class:`.DisplayContext`.
+    """     
+
+    oType = type(overlay).__name__
+    func  = getattr(sys.modules[__name__], '_{}Defaults'.format(oType), None)
+
+    if func is None:
+        log.warn('Unknown overlay type: {}'.format(oType))
+        return
+
+    log.debug('Applying default display arguments for {}'.format(overlay))
+    func(overlay, overlayList, displayCtx)
+
+
+def _ImageDefaults(overlay, overlayList, displayCtx):
+    """Configure default display settings for the given :class:`.Image`
+    overlay.
+    """
+
+    if _isStatImage(overlay):
+        _statImageDefaults(overlay, overlayList, displayCtx)
+        
+
+def _isStatImage(overlay):
+    """Returns ``True`` if the given :class:`.Image` overlay looks like a
+    statistic image, ``False`` otherwise.
+    """
+    
+    basename = fslimage.removeExt(overlay.dataSource)
+    tokens   = ['zstat', 'tstat', 'fstat', 'zfstat']
+    pattern  = '_({})\d+'.format('|'.join(tokens))
+
+    return re.search(pattern, basename) is not None
+
+
+def _statImageDefaults(overlay, overlayList, displayCtx):
+    """Configure default display settings for the given statistic
+    :class:`.Image` overlay.
+    """ 
+
+    opts     = displayCtx.getOpts(overlay)
+    basename = fslimage.removeExt(overlay.dataSource)
+    cmap     = _statImageDefaults.cmaps[_statImageDefaults.currentCmap]
+
+    nameTokens = '_'.split(basename)
+
+    # Give each stat image
+    # a different colour map
+    _statImageDefaults.currentCmap += 1
+    _statImageDefaults.currentCmap %= len(_statImageDefaults.cmaps)
+    opts.cmap                       = cmap
+
+    pTokens          = ['p', 'corrp']
+    statTokens       = ['zstat', 'tstat', 'zfstat']
+    fStatTokens      = ['fstat']
+
+    # The order of these tests is
+    # important, due to name overlap
+
+    # P-value image ?
+    if any([token in nameTokens for token in pTokens]):
+        opts.displayRange  = [0.95, 1.0]
+        opts.clippingRange = [0.95, 1.0]
+
+    # T or Z stat image?
+    elif any([token in nameTokens for token in statTokens]):
+        
+        opts.clippingRange  = [-0.1, 0.1]
+        opts.displayRange   = [-7.5, 7.5]
+        opts.centreRanges   = True
+        opts.invertClipping = True
+
+    # F stat image?
+    elif any([token in nameTokens for token in fStatTokens]):
+        opts.displayRange = [0, 10]
+
+
+# Colour maps used for statistic images
+_statImageDefaults.cmaps = ['red-yellow',
+                            'blue-lightblue',
+                            'green',
+                            'cool',
+                            'hot',
+                            'blue',
+                            'red',
+                            'yellow',
+                            'pink',
+                            'copper']
+
+
+# Index into the cmaps list, pointing to the 
+# next colour map to use for statistic images.
+_statImageDefaults.currentCmap = 0
+
+
+def _FEATImageDefaults(overlay, overlayList, displayCtx):
+    """Configure default display settings for the given :class:`.FEATImage`
+    overlay.
+    """
+    pass
+
+
+def _MelodicImageDefaults(overlay, overlayList, displayCtx):
+    """Configure default display settings for the given :class:`.MelodicImage`
+    overlay.
+    """ 
+
+    opts = displayCtx.getOpts(overlay)
+
+    opts.cmap           = 'Render3'
+    opts.displayRange   = [-5.0, 5.0]
+    opts.clippingRange  = [-1.5, 1.5]
+
+    opts.centreRanges   = True
+    opts.invertClipping = True
+
+
+def _ModelDefaults(overlay, display, overlayList, displayCtx):
+    """Configure default display settings for the given :class:`.Model`
+    overlay.
+    """
+
+    # TODO some nice default colours
+    pass
diff --git a/fsl/fsleyes/fsleyes_parseargs.py b/fsl/fsleyes/fsleyes_parseargs.py
index e8775c7bf..835dfacab 100644
--- a/fsl/fsleyes/fsleyes_parseargs.py
+++ b/fsl/fsleyes/fsleyes_parseargs.py
@@ -1122,8 +1122,18 @@ def applyOverlayArgs(args, overlayList, displayCtx, **kwargs):
     for i, overlay in enumerate(overlayList):
 
         display = displayCtx.getDisplay(overlay)
-        
-        _applyArgs(args.overlays[i], display)
+
+        # Figure out how many arguments
+        # were passed in for this overlay
+        nArgs = len([v for k, v in vars(args.overlays[i]).items()
+                     if k != 'overlay' and v is not None])
+
+        # If no arguments were passed,
+        # apply default display settings 
+        if nArgs == 0 and args.defaultDisplay:
+            displaydefaults.displayDefaults(overlay, overlayList, displayCtx)
+        else:
+            _applyArgs(args.overlays[i], display)
 
         # Retrieve the DisplayOpts instance
         # after applying arguments to the
diff --git a/fsl/fsleyes/overlay.py b/fsl/fsleyes/overlay.py
index 1ede376e0..0dbe9ef6c 100644
--- a/fsl/fsleyes/overlay.py
+++ b/fsl/fsleyes/overlay.py
@@ -109,19 +109,17 @@ class OverlayList(props.HasProperties):
         """Convenience method for interactively adding overlays to this
         :class:`OverlayList`.
 
-        Returns: ``True`` if some overlays were added to the list, ``False``
-                 otherwise.
+        Returns: A list containing the overlays that were added - the list
+                 will be empty if no overlays were added.
         """
 
         overlays = interactiveLoadOverlays(fromDir)
 
-        if len(overlays) == 0:
-            return False
-        
-        if addToEnd: self.extend(      overlays)
-        else:        self.insertAll(0, overlays)
+        if len(overlays) > 0:
+            if addToEnd: self.extend(      overlays)
+            else:        self.insertAll(0, overlays)
 
-        return True
+        return overlays
 
 
     def find(self, name):
-- 
GitLab