From abde58ac1b9e70b96f9a8e10904273ecc8e7aed3 Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauld.mccarthy@gmail.com>
Date: Fri, 17 Jul 2015 14:23:26 +0100
Subject: [PATCH] Basic support for overlay groups. Currently, only Display
 properties are synced. About to add support for DisplayOpts properties.

---
 fsl/fslview/controls/overlaylistpanel.py     |  72 +++++++++----
 fsl/fslview/displaycontext/__init__.py       |   1 +
 fsl/fslview/displaycontext/displaycontext.py |  11 +-
 fsl/fslview/displaycontext/group.py          | 107 +++++++++++++++++++
 fsl/fslview/overlay.py                       |   2 +-
 fsl/tools/fslview.py                         |  12 +++
 6 files changed, 181 insertions(+), 24 deletions(-)
 create mode 100644 fsl/fslview/displaycontext/group.py

diff --git a/fsl/fslview/controls/overlaylistpanel.py b/fsl/fslview/controls/overlaylistpanel.py
index 933504ccd..46800e001 100644
--- a/fsl/fslview/controls/overlaylistpanel.py
+++ b/fsl/fslview/controls/overlaylistpanel.py
@@ -29,47 +29,73 @@ class ListItemWidget(wx.Panel):
     _enabledFG  = '#000000'
     _disabledFG = '#CCCCCC'
 
-    def __init__(self, parent, overlay, display, listBox):
+    def __init__(self, parent, overlay, display, displayCtx, listBox):
         wx.Panel.__init__(self, parent)
 
-        self.overlay = overlay
-        self.display = display
-        self.listBox = listBox
-        self.name    = '{}_{}'.format(self.__class__.__name__, id(self))
-
-        self.saveButton = wx.Button(self, label='S', style=wx.BU_EXACTFIT)
-        self.visibility = props.makeWidget(self, display, 'enabled')
+        self.overlay    = overlay
+        self.display    = display
+        self.displayCtx = displayCtx
+        self.listBox    = listBox
+        self.name       = '{}_{}'.format(self.__class__.__name__, id(self))
+
+        self.saveButton = wx.Button(       self,
+                                           label='S',
+                                           style=wx.BU_EXACTFIT)
+        self.lockButton = wx.ToggleButton( self,
+                                           label='L',
+                                           style=wx.BU_EXACTFIT)
+        self.visibility = props.makeWidget(self,
+                                           display,
+                                           'enabled')
 
         self.sizer = wx.BoxSizer(wx.HORIZONTAL)
 
         self.SetSizer(self.sizer)
 
         self.sizer.Add(self.saveButton, flag=wx.EXPAND, proportion=1)
+        self.sizer.Add(self.lockButton, flag=wx.EXPAND, proportion=1)
         self.sizer.Add(self.visibility, flag=wx.EXPAND, proportion=1)
 
-        self.display.addListener('enabled', self.name, self._vizChanged)
+        # There is currently only one overlay
+        # group in the application. In the
+        # future there may be multiple groups.
+        group = displayCtx.overlayGroups[0]
 
+        display.addListener('enabled',  self.name, self.__vizChanged)
+        group  .addListener('overlays', self.name, self.__overlayGroupChanged)
+        
         if isinstance(overlay, fslimage.Image):
-            self.overlay.addListener('saved',
-                                     self.name,
-                                     self._saveStateChanged)
+            overlay.addListener('saved', self.name, self.__saveStateChanged)
         else:
             log.warn('No save button support for non-volumetric overlays')
             self.saveButton.Enable(False)
 
-        self.saveButton.Bind(wx.EVT_BUTTON, self._onSaveButton)
+        self.saveButton.Bind(wx.EVT_BUTTON,         self.__onSaveButton)
+        self.lockButton.Bind(wx.EVT_TOGGLEBUTTON,   self.__onLockButton)
+        self           .Bind(wx.EVT_WINDOW_DESTROY, self.__onDestroy)
+
+        self.__vizChanged()
+        self.__saveStateChanged()
+
 
-        self.Bind(wx.EVT_WINDOW_DESTROY, self._onDestroy)
+    def __overlayGroupChanged(self, *a):
 
-        self._vizChanged()
-        self._saveStateChanged()
+        group = self.displayCtx.overlayGroups[0]
+        self.lockButton.SetValue(self.overlay in group.overlays)
 
         
-    def _onSaveButton(self, ev):
+    def __onSaveButton(self, ev):
         self.overlay.save()
 
+
+    def __onLockButton(self, ev):
+        group = self.displayCtx.overlayGroups[0]
+        
+        if self.lockButton.GetValue(): group.addOverlay(   self.overlay)
+        else:                          group.removeOverlay(self.overlay)
+
         
-    def _onDestroy(self, ev):
+    def __onDestroy(self, ev):
         ev.Skip()
         if ev.GetEventObject() is not self:
             return
@@ -80,7 +106,7 @@ class ListItemWidget(wx.Panel):
             self.overlay.removeListener('saved', self.name)
 
         
-    def _saveStateChanged(self, *a):
+    def __saveStateChanged(self, *a):
 
         if not isinstance(self.overlay, fslimage.Image):
             return
@@ -95,7 +121,7 @@ class ListItemWidget(wx.Panel):
             self.listBox.SetItemBackgroundColour(idx, '#ffaaaa', '#aa4444') 
 
             
-    def _vizChanged(self, *a):
+    def __vizChanged(self, *a):
 
         idx = self.listBox.IndexOf(self.overlay)
 
@@ -226,7 +252,11 @@ class OverlayListPanel(fslpanel.FSLViewPanel):
             
             self._listBox.Append(name, overlay, tooltip)
 
-            widget = ListItemWidget(self, overlay, display, self._listBox)
+            widget = ListItemWidget(self,
+                                    overlay,
+                                    display,
+                                    self._displayCtx,
+                                    self._listBox)
 
             self._listBox.SetItemWidget(i, widget)
 
diff --git a/fsl/fslview/displaycontext/__init__.py b/fsl/fslview/displaycontext/__init__.py
index 22cc99362..08ec6a172 100644
--- a/fsl/fslview/displaycontext/__init__.py
+++ b/fsl/fslview/displaycontext/__init__.py
@@ -11,6 +11,7 @@ import display
 
 from displaycontext import DisplayContext
 from display        import Display
+from group          import OverlayGroup
 from sceneopts      import SceneOpts
 from orthoopts      import OrthoOpts
 from lightboxopts   import LightBoxOpts
diff --git a/fsl/fslview/displaycontext/displaycontext.py b/fsl/fslview/displaycontext/displaycontext.py
index 9a7134ecc..623c0286e 100644
--- a/fsl/fslview/displaycontext/displaycontext.py
+++ b/fsl/fslview/displaycontext/displaycontext.py
@@ -66,6 +66,12 @@ class DisplayContext(props.SyncableHasProperties):
     """
 
 
+    overlayGroups = props.List()
+    """A list of :class:`.OverlayGroup` instances, each of which defines
+    a group of overlays which share display properties.
+    """
+
+
     def __init__(self, overlayList, parent=None):
         """Create a :class:`DisplayContext` object.
 
@@ -75,7 +81,9 @@ class DisplayContext(props.SyncableHasProperties):
         as the parent of this instance.
         """
 
-        props.SyncableHasProperties.__init__(self, parent)
+        props.SyncableHasProperties.__init__(self,
+                                             parent,
+                                             nounbind=['overlayGroups'])
         
         self.__overlayList = overlayList
         self.__name         = '{}_{}'.format(self.__class__.__name__, id(self))
@@ -95,7 +103,6 @@ class DisplayContext(props.SyncableHasProperties):
                                 self.__name,
                                 self.__overlayListChanged)
 
-
         log.memory('{}.init ({})'.format(type(self).__name__, id(self)))
 
         
diff --git a/fsl/fslview/displaycontext/group.py b/fsl/fslview/displaycontext/group.py
new file mode 100644
index 000000000..9a41acc1e
--- /dev/null
+++ b/fsl/fslview/displaycontext/group.py
@@ -0,0 +1,107 @@
+#!/usr/bin/env python
+#
+# group.py - Overlay groups
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+
+import logging
+import copy
+
+import props
+
+import display as fsldisplay
+import            volumeopts
+
+
+log = logging.getLogger(__name__)
+
+
+
+class OverlayGroup(props.HasProperties):
+
+    
+    name     = props.String()
+
+    
+    overlays = props.List()
+
+    
+    # Properties which are linked across all overlays
+    enabled    = copy.copy(fsldisplay.Display.enabled)
+    alpha      = copy.copy(fsldisplay.Display.alpha)
+    brightness = copy.copy(fsldisplay.Display.brightness)
+    contrast   = copy.copy(fsldisplay.Display.contrast)
+
+
+    # Properties which are linked across Image overlays
+    volume = copy.copy(volumeopts.ImageOpts.transform)
+
+    
+    # Properties which are linked across Volume overlays
+    displayRange   = copy.copy(volumeopts.VolumeOpts.displayRange)
+    clippingRange  = copy.copy(volumeopts.VolumeOpts.clippingRange)
+    invertClipping = copy.copy(volumeopts.VolumeOpts.invertClipping)
+    interpolation  = copy.copy(volumeopts.VolumeOpts.interpolation)
+
+    
+    # TODO Vector
+    # TODO Model
+    # TODO Label
+
+    
+    def __init__(self, displayCtx, overlayList, number, name=None):
+
+        self.__displayCtx  = displayCtx
+        self.__overlayList = overlayList
+        self.__number      = number
+
+        if name is not None:
+            self.name = name
+
+
+    def __copy__(self):
+        return OverlayGroup(
+            self,
+            self.__displayCtx,
+            self.__overlayList,
+            self.__number,
+            self.name)
+
+            
+    def addOverlay(self, overlay):
+
+        self.overlays.append(overlay)
+
+        display = self.__displayCtx.getDisplay(overlay)
+        opts    = display.getDisplayOpts()
+
+        # This is the first overlay to be added - the group
+        # should inherit its property values
+        if len(self.overlays) == 1: master, slave = display, self
+
+        # Other overlays are already in the group - the
+        # new overlay should inherit the group properties
+        else:                       master, slave = self, display
+
+        slave.bindProps('enabled',    master)
+        slave.bindProps('alpha',      master)
+        slave.bindProps('brightness', master)
+        slave.bindProps('contrast',   master)
+
+
+    def removeOverlay(self, overlay):
+
+        self.overlays.remove(overlay)
+
+        display = self.__displayCtx.getDisplay(overlay)
+        opts    = display.getDisplayOpts()
+
+        self.unbindProps('enabled',    display)
+        self.unbindProps('alpha',      display)
+        self.unbindProps('brightness', display)
+        self.unbindProps('contrast',   display)
+
+
+    def __overlayTypeChanged(self, *a):
+        pass
diff --git a/fsl/fslview/overlay.py b/fsl/fslview/overlay.py
index ab751b823..e23cc4d2a 100644
--- a/fsl/fslview/overlay.py
+++ b/fsl/fslview/overlay.py
@@ -43,7 +43,7 @@ class OverlayList(props.HasProperties):
 
       - ``name`` ...
     
-      - ``dataSoruce`` ..
+      - ``dataSource`` ..
 
 
     Furthermore, all overlay types must be able to be created with a single
diff --git a/fsl/tools/fslview.py b/fsl/tools/fslview.py
index c429066dc..a97f9aa6a 100644
--- a/fsl/tools/fslview.py
+++ b/fsl/tools/fslview.py
@@ -139,6 +139,18 @@ def context(args):
     overlayList = fsloverlay.OverlayList()
     displayCtx  = displaycontext.DisplayContext(overlayList)
 
+
+    # While the DisplayContext may refer to 
+    # multiple overlay groups, we are currently
+    # using just one, allowing the user to specify
+    # a set of overlays for which their display
+    # properties are 'locked'.
+    lockGroup   = displaycontext.OverlayGroup(displayCtx,
+                                              overlayList,
+                                              0,
+                                              'LockGroup')
+    displayCtx.overlayGroups.append(lockGroup)
+
     log.debug('Created overlay list and master DisplayContext ({})'.format(
         id(displayCtx)))
     
-- 
GitLab