From 3a934425b0235d0eafbc755b07496a48e07f1ee7 Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauld.mccarthy@gmail.com>
Date: Wed, 16 Mar 2016 12:01:14 +0000
Subject: [PATCH] New module 'state' which saves/restores the state of a
 FSLEyesFrame - restore not implemented yet. Most logic moved from
 diagnosticreport into state.

---
 fsl/fsleyes/__init__.py                 |   3 +
 fsl/fsleyes/actions/diagnosticreport.py | 125 +----------
 fsl/fsleyes/frame.py                    |  18 +-
 fsl/fsleyes/perspectives.py             |   6 +-
 fsl/fsleyes/state.py                    | 272 ++++++++++++++++++++++++
 5 files changed, 306 insertions(+), 118 deletions(-)
 create mode 100644 fsl/fsleyes/state.py

diff --git a/fsl/fsleyes/__init__.py b/fsl/fsleyes/__init__.py
index cd6493777..f78c8a98e 100644
--- a/fsl/fsleyes/__init__.py
+++ b/fsl/fsleyes/__init__.py
@@ -187,8 +187,11 @@ Some other miscellaneous modules are contained in the ``fsleyes`` package:
    ~fsl.fsleyes.toolbar
    ~fsl.fsleyes.tooltips
    ~fsl.fsleyes.perspectives
+   ~fsl.fsleyes.autodisplay
+   ~fsl.fsleyes.state
    ~fsl.fsleyes.icons
    ~fsl.fsleyes.colourmaps
    ~fsl.fsleyes.plotting
    ~fsl.fsleyes.splash
+   ~fsl.fsleyes.about
 """
diff --git a/fsl/fsleyes/actions/diagnosticreport.py b/fsl/fsleyes/actions/diagnosticreport.py
index 56b08e81e..6c8d6a361 100644
--- a/fsl/fsleyes/actions/diagnosticreport.py
+++ b/fsl/fsleyes/actions/diagnosticreport.py
@@ -16,9 +16,11 @@ import logging
 import platform
 from collections import OrderedDict
 
-import                     action
-import fsl.data.strings as strings
-import fsl.utils.status as status
+import fsl.data.strings  as strings
+import fsl.utils.status  as status
+import fsl.fsleyes.state as fslstate
+
+from . import               action
 
 
 log = logging.getLogger(__name__)
@@ -92,8 +94,7 @@ class DiagnosticReportAction(action.Action):
         state.
         """
 
-        import fsl.version              as version
-        import fsl.fsleyes.perspectives as perspectives
+        import fsl.version as version
 
         report   = OrderedDict()
         overlays = []
@@ -111,25 +112,11 @@ class DiagnosticReportAction(action.Action):
         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,
-            self.__displayCtx)
 
-        for viewPanel in self.__frame.getViewPanels():
+        state = fslstate.save(self.__frame)
 
-            # Accessing the undocumented
-            # AuiPaneInfo.name attribute
-            vpName       = self.__frame.getViewPanelInfo(viewPanel).name
-            vpDisplayCtx = viewPanel.getDisplayContext()
-
-            report[vpName] = OrderedDict([
-                ('View',            self.__viewPanelReport(viewPanel)),
-                ('Display context', self.__displayContextReport(
-                    self.__overlayList,
-                    vpDisplayCtx))])
+        for k, v in state.items():
+            report[k] = v
 
         return report
 
@@ -182,100 +169,6 @@ class DiagnosticReportAction(action.Action):
 
         return report
 
-
-    def __displayContextReport(self, overlayList, displayCtx):
-        """Creates and returns a hierarchical dictionary containing
-        information about the given :class:`.DisplayContext` and the
-        :class:`.Display`/:class:`.DisplayOpts` instances which it
-        is managing.
-        """
-
-        report   = OrderedDict()
-        overlays = []
-        props    = displayCtx.getAllProperties()[0]
-
-        for overlay in overlayList:
-
-            display = displayCtx.getDisplay(overlay)
-            opts    = displayCtx.getOpts(   overlay)
-
-            overlays.append(OrderedDict([
-                ('Display',     self.__displayReport(    display)),
-                ('DisplayOpts', self.__displayOptsReport(opts))]))
-
-        for prop in props:
-            report[prop] = str(getattr(displayCtx, prop))
-            
-        report['overlays'] = overlays
-
-        return report
-
-    
-    def __displayReport(self, display):
-        """Creates and returns a dictionary containing informtion about
-        the given :class:`.Display` instance.
-        """
-        
-        report = OrderedDict()
-        props  = display.getAllProperties()[0]
-
-        for prop in props:
-            report[prop] = str(getattr(display, prop))
-        
-        return report 
-
-    
-    def __displayOptsReport(self, opts):
-        """Creates and returns a dictionary containing informtion about
-        the given :class:`.DisplayOpts` instance.
-        """ 
-        
-        report = OrderedDict()
-
-        report['type'] = type(opts).__name__
-
-        props  = opts.getAllProperties()[0]
-
-        for prop in props:
-            value = getattr(opts, prop)
-            if prop in ('cmap', 'negativeCmap'):
-                value = value.name
-                
-            report[prop] = str(value)
-
-        return report
-
-    
-    def __viewPanelReport(self, viewPanel):
-        """Creates and returns a dictionary containing informtion about
-        the given :class:`.ViewPanel`.
-        """ 
-
-        import fsl.fsleyes.views as views
-        
-        report = OrderedDict()
-        props  = viewPanel.getAllProperties()[0]
-
-        report['type'] = type(viewPanel).__name__
-
-        for prop in props:
-            report[prop] = str(getattr(viewPanel, prop))
-
-        if isinstance(viewPanel, views.CanvasPanel):
-            
-            sceneOptsReport = OrderedDict()
-            sceneOpts       = viewPanel.getSceneOptions()
-            props           = sceneOpts.getAllProperties()[0]
-
-            sceneOptsReport['type'] = type(sceneOpts).__name__
-            
-            for prop in props:
-                sceneOptsReport[prop] = str(getattr(sceneOpts, prop))
-
-            report['SceneOpts'] = sceneOptsReport
-        
-        return report
-
     
     def __formatReport(self, reportDict):
         """Converts the given hierarchical dictionary to a JSON-formatted
diff --git a/fsl/fsleyes/frame.py b/fsl/fsleyes/frame.py
index 85ac4b104..4d8ae9260 100644
--- a/fsl/fsleyes/frame.py
+++ b/fsl/fsleyes/frame.py
@@ -73,6 +73,8 @@ class FSLEyesFrame(wx.Frame):
     .. autosummary::
        :nosignatures:
 
+       getOverlayList
+       getDisplayContext
        getViewPanels
        getViewPanelInfo
        addViewPanel
@@ -209,6 +211,20 @@ class FSLEyesFrame(wx.Frame):
         self.Layout()
 
         
+    def getOverlayList(self):
+        """Returns the :class:`.OverlayList` which contains the overlays
+        being displayed by this ``FSLEyesFrame``.
+        """
+        return self.__overlayList
+
+    
+    def getDisplayContext(self):
+        """Returns the top-level :class:`.DisplayContext` associated with this
+        ``FSLEyesFrame``.
+        """
+        return self.__displayCtx
+
+        
     def getViewPanels(self):
         """Returns a list of all :class:`.ViewPanel` instances that are
         currenlty displayed in this ``FSLEyesFrame``.
@@ -266,7 +282,7 @@ class FSLEyesFrame(wx.Frame):
             self.__onViewPanelClose(panel=vp, displaySync=False)
             self.__auiManager.ClosePane(paneInfo)
             
-        self.__auiManager.Update() 
+        self.__auiManager.Update()
 
 
     def addViewPanel(self, panelCls):
diff --git a/fsl/fsleyes/perspectives.py b/fsl/fsleyes/perspectives.py
index 411f13582..58e1c4f9e 100644
--- a/fsl/fsleyes/perspectives.py
+++ b/fsl/fsleyes/perspectives.py
@@ -565,7 +565,11 @@ def _addControlPanel(viewPanel, panelType):
 
 
 def _getPanelProps(panel):
-    """
+    """Creates and returns two dictionaries, containing properties of the given
+    :class:`.ViewPanel` (and its associated :class:`.SceneOpts` instance, if
+    it is a :class:`.CanvasPanel`), which are to be saved as part of a
+    seriaised *FSLeyes* perspective. The properties to be saved are listed in
+    the :data:`VIEWPANEL_PROPS` dictionary.
     """
 
     import fsl.fsleyes.views as views
diff --git a/fsl/fsleyes/state.py b/fsl/fsleyes/state.py
new file mode 100644
index 000000000..6eab41bcb
--- /dev/null
+++ b/fsl/fsleyes/state.py
@@ -0,0 +1,272 @@
+#!/usr/bin/env python
+#
+# state.py - Functions for saving/restoring the state of *FSLeyes*.
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+"""This module provides two functions, :func:`save` and :func:`restore`.
+These functions may be used to save/restore the state of *FSLeyes*.
+
+ .. note:: :func:`restore` is currently not implemented.
+"""
+
+import                  importlib
+from collections import OrderedDict
+
+import                  props
+
+from .           import perspectives
+
+
+def save(frame):
+    """Creates and returns a dictionary containing the current state of the
+    given :class:`.FSLeyesFrame`, the :class:`.OverlayList` and
+    :class:`.DisplayContext` associated with it, and the :class:`.ViewPanel`
+    instances being displayed.
+    """
+
+    overlayList = frame.getOverlayList()
+    displayCtx  = frame.getDisplayContext()
+    viewPanels  = frame.getViewPanels()
+
+    layout   = perspectives.serialisePerspective(frame)
+    overlays = [(type(o).__name__, o.dataSource) for o in overlayList]
+
+    overlays = []
+        
+    for i, ovl in enumerate(overlayList):
+        overlays.append(OrderedDict([
+            ('type',   '{}.{}'.format(ovl.__module__, type(ovl).__name__)),
+            ('name',   ovl.name),
+            ('source', ovl.dataSource)]))
+
+    state                   = OrderedDict()
+    state['Layout']         = layout
+    state['Overlays']       = overlays
+    state['DisplayContext'] = _displayContextState(overlayList, displayCtx)
+
+    vpStates = {}
+
+    for vp in viewPanels:
+
+        # Accessing the undocumented
+        # AuiPaneInfo.name attribute
+        vpName       = frame.getViewPanelInfo(vp).name
+        vpDisplayCtx = vp.getDisplayContext()
+
+        vpStates[vpName] = OrderedDict([
+            ('View',           _viewPanelState(vp)),
+            ('DisplayContext', _displayContextState(overlayList,
+                                                    vpDisplayCtx))])
+
+    state['ViewPanels'] = vpStates
+
+    return state
+
+
+def _displayContextState(overlayList, displayCtx):
+    """Creates and returns a hierarchical dictionary containing
+    the state of the given :class:`.DisplayContext` and the
+    :class:`.Display`/:class:`.DisplayOpts` instances which it
+    is managing.
+    """
+
+    state     = OrderedDict()
+    overlays  = []
+    propNames = displayCtx.getAllProperties()[0]
+
+    for overlay in overlayList:
+
+        display = displayCtx.getDisplay(overlay)
+        opts    = displayCtx.getOpts(   overlay)
+
+        overlays.append(OrderedDict([
+            ('Display',     _displayState(    display)),
+            ('DisplayOpts', _displayOptsState(opts))]))
+
+    for propName in propNames:
+        state[propName] = props.serialise(displayCtx, propName)
+        print 'displayCtx.{} = {}'.format(propName, state[propName])
+
+    state['Overlays'] = overlays
+
+    return state
+
+
+def _displayState(display):
+    """Creates and returns a dictionary containing the state of the given
+    :class:`.Display` instance.
+    """
+
+    state     = OrderedDict()
+    propNames = display.getAllProperties()[0]
+
+    for propName in propNames:
+        state[propName] = props.serialise(display, propName)
+
+    return state
+
+
+def _displayOptsState(opts):
+    """Creates and returns a dictionary containing the state of the given
+    :class:`.DisplayOpts` instance.
+    """ 
+
+    state     = OrderedDict()
+    propNames = opts.getAllProperties()[0]
+    
+    state['type'] = type(opts).__name__
+
+    for propName in propNames:
+        state[propName] = props.serialise(opts, propName)
+
+    return state
+
+
+def _viewPanelState(viewPanel):
+    """Creates and returns a dictionary containing the state of the given
+    :class:`.ViewPanel`.
+    """ 
+
+    import fsl.fsleyes.views as views
+
+    state     = OrderedDict()
+    propNames = viewPanel.getAllProperties()[0]
+
+    state['type'] = type(viewPanel).__name__
+
+    for propName in propNames:
+        state[propName] = props.serialise(viewPanel, propName)
+
+    if isinstance(viewPanel, views.CanvasPanel):
+
+        sceneOptsState = OrderedDict()
+        sceneOpts      = viewPanel.getSceneOptions()
+        propNames      = sceneOpts.getAllProperties()[0]
+
+        sceneOptsState['type'] = type(sceneOpts).__name__
+
+        for propName in propNames:
+            sceneOptsState[propName] = props.serialise(sceneOpts, propName)
+
+        state['SceneOpts'] = sceneOptsState
+
+    return state
+
+
+def restore(frame, state):
+    """Applies a state, previously saved via :func:`save`, to the given
+    :class:`.FSLeyesFrame`.
+
+    .. note:: Not implemented.
+    """
+
+    return
+
+    overlayList     = frame.getOverlayList()
+    displayCtx      = frame.getDisplayContext()
+    layout          = state['Layout']
+    overlays        = state['Overlays']
+    displayCtxState = state['DisplayContext']
+    vpStates        = state['ViewPanels']
+
+    # First, remove all view panels and then
+    # clear the overlay list. We do it in this
+    # order to avoid any unncessary processing
+    # by existing view panel DisplayContexts.
+    frame.removeAllViewPanels()
+    overlayList[:] = []
+
+    # Populate the overlay list and configure
+    # the master display context
+    overlayObjs = []
+    for ovl in overlays:
+        klass      = ovl['type']
+        name       = ovl['name']
+        dataSource = ovl['source']
+
+        # We can't restore in-memory overlays
+        if dataSource is None or dataSource.lower() == 'none':
+            continue
+
+        # Split the (fully-qualified)
+        # class name into the containing 
+        # module and the class name.
+        klass  = klass.split('.')
+        module = '.'.join(klass[:-1])
+        klass  = klass[-1]
+
+        # Import the module, and 
+        # look up the class object
+        module = importlib.import_module(module)
+        klass  = getattr(module, klass)
+
+        # Create the overlay object
+        ovl      = klass(dataSource)
+        ovl.name = name
+        overlayObjs.append(ovl)
+
+    # Restore the master display contxt
+    _restoreDisplayContext(displayCtx, overlayList, displayCtxState)
+
+    # Apply the frame perspective
+    perspectives.applyPerspective(frame, None, layout)
+    
+    # Restore view panel settings
+    viewPanels = frame.getViewPanels()
+    for vp, vpState in zip(viewPanels, vpStates):
+        _restoreViewPanel(vp, overlayList, vpState)
+
+
+def _restoreViewPanel(viewPanel, overlayList, state):
+
+    displayCtx     = viewPanel.getDisplayContext()
+    sceneOpts      = viewPanel.getSceneOptions()
+    vpState        = state['View']
+    dcState        = state['DisplayContext']
+    vpType         = vpState.pop('type')
+    sceneOptsState = vpState.pop('SceneOpts')
+
+    _restoreDisplayContext(displayCtx, overlayList, dcState)
+    _applyProps(viewPanel, vpState)
+    _applyProps(sceneOpts, sceneOptsState)
+
+
+def _restoreDisplayContext(displayCtx, overlayList, state):
+
+    # Configure selected properties
+    # of the master display context
+    dcProps = ['selectedOverlay',
+               'location',
+               # 'overlayOrder',
+               'syncOverlayDisplay',
+               'displaySpace',
+               'autoDisplay']
+    
+    _applyProps(displayCtx, state, dcProps)
+
+    for overlay, ovlState in zip(overlayList, state['Overlays']):
+        displayState = ovlState['Display']
+        optsState    = ovlState['DisplayOpts']
+
+        display = displayCtx.getDisplay(overlay)
+        _applyProps(display, displayState)
+
+        # TODO: This will almost certainly fail
+        #       for non-trivial properties (e.g.
+        #       VolumeOpts.clipImage)
+        opts = displayCtx.getOpts(overlay)
+        _applyProps(opts, optsState)
+
+
+def _applyProps(target, values, propNames=None):
+
+    if propNames is None:
+        propNames = values.keys()
+    
+    for propName in propNames:
+
+        val = values[propName]
+        val = props.deserialise(target, propName, val)
+        
+        setattr(target, propName, val) 
-- 
GitLab