From 937f26021cdfe9e9a8600ba8181cb320b31ae214 Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauld.mccarthy@gmail.com>
Date: Thu, 7 Jan 2016 13:51:47 +0000
Subject: [PATCH] Simple module for displaying status messages. FSLEyesFrame
 now has a status bar to which such messages shall be directed.

---
 fsl/data/image.py                       | 47 ++++++++---------
 fsl/data/strings.py                     |  1 -
 fsl/fsleyes/frame.py                    | 21 +++++++-
 fsl/fsleyes/gl/textures/imagetexture.py |  6 +++
 fsl/fsleyes/overlay.py                  | 24 +++------
 fsl/fsleyes/perspectives.py             | 20 +++-----
 fsl/fsleyes/splash.py                   |  1 +
 fsl/tools/fsleyes.py                    | 45 ++++++++--------
 fsl/utils/status.py                     | 68 +++++++++++++++++++++++++
 9 files changed, 151 insertions(+), 82 deletions(-)
 create mode 100644 fsl/utils/status.py

diff --git a/fsl/data/image.py b/fsl/data/image.py
index 1ef041e92..ac29798a5 100644
--- a/fsl/data/image.py
+++ b/fsl/data/image.py
@@ -43,6 +43,7 @@ import nibabel    as nib
 import props
 
 import fsl.utils.transform as transform
+import fsl.utils.status    as status
 import fsl.data.strings    as strings
 import fsl.data.constants  as constants
 
@@ -323,7 +324,7 @@ class Image(Nifti1, props.HasProperties):
 
         All other arguments are passed through to :meth:`Nifti1.__init__`.
         """
-
+                    
         Nifti1.__init__(self, image, **kwargs)
 
         # Figure out the name of this image.
@@ -376,14 +377,23 @@ class Image(Nifti1, props.HasProperties):
         """Overrides :meth:`Nifti1.loadData`. Calls that method, and
         calculates initial values for :attr:`dataRange`.
         """
+
         Nifti1.loadData(self)
 
+        status.update('Calculating minimum/maximum '
+                      'for {}...'.format(self.dataSource))
+
         dataMin = np.nanmin(self.data)
         dataMax = np.nanmax(self.data)
 
+        log.debug('Calculated data range for {}: [{} - {}]'.format(
+            self.dataSource, dataMin, dataMax))
+
         if np.any(np.isnan((dataMin, dataMax))):
             dataMin = 0
-            dataMax = 0 
+            dataMax = 0
+
+        status.clear()
 
         self.dataRange.x = [dataMin, dataMax]
 
@@ -458,6 +468,9 @@ class Image(Nifti1, props.HasProperties):
 
         data = self.data
 
+        status.update('Calculating minimum/maximum '
+                      'for {}...'.format(self.dataSource))
+
         # The old image wide data range.
         oldMin    = self.dataRange.xlo
         oldMax    = self.dataRange.xhi
@@ -491,6 +504,8 @@ class Image(Nifti1, props.HasProperties):
         if np.isnan(newMin): newMin = 0
         if np.isnan(newMax): newMax = 0
 
+        status.clear()
+
         return newMin, newMax
     
 
@@ -637,17 +652,6 @@ def loadImage(filename):
     file name, or the name of the temporary decompressed file).
     """
 
-    # If we have a GUI, we can display a dialog
-    # message. Otherwise we print a log message
-    haveGui = False
-    try:
-        import wx
-        if wx.GetApp() is not None: 
-            haveGui = True
-            import fsl.utils.dialog as fsldlg
-    except:
-        pass
-
     realFilename = filename
     mbytes       = op.getsize(filename) / 1048576.0
 
@@ -661,11 +665,7 @@ def loadImage(filename):
         msg = strings.messages['image.loadImage.decompress']
         msg = msg.format(op.basename(realFilename), mbytes, filename)
 
-        if not haveGui:
-            log.info(msg)
-        else:
-            busyDlg = fsldlg.SimpleMessageDialog(message=msg)
-            busyDlg.Show()
+        status.update(msg)
 
         gzip = ['gzip', '-d', '-c', realFilename]
         log.debug('Running {} > {}'.format(' '.join(gzip), filename))
@@ -683,23 +683,18 @@ def loadImage(filename):
             os.remove(filename)
             filename = realFilename
 
-        if haveGui:
-            busyDlg.Destroy()
-
     log.debug('Loading image from {}'.format(filename))
 
     import nibabel as nib
 
-    if haveGui and (mbytes > 512):
+    if mbytes > 512:
         msg     = strings.messages['image.loadImage.largeFile']
         msg     = msg.format(op.basename(filename),  mbytes)
-        busyDlg = fsldlg.SimpleMessageDialog(message=msg)
-        busyDlg.Show()
+        status.update(msg)
     
     image = nib.load(filename)
 
-    if haveGui and (mbytes > 512):
-        busyDlg.Destroy()
+    status.clear()
 
     return image, filename
 
diff --git a/fsl/data/strings.py b/fsl/data/strings.py
index 9ec0319ed..29782b2e0 100644
--- a/fsl/data/strings.py
+++ b/fsl/data/strings.py
@@ -39,7 +39,6 @@ messages = TypeDict({
     'FSLDirDialog.selectFSLDir'    : 'Select the directory in which '
                                      'FSL is installed',
 
-    'fsleyes.loading'              : 'Loading {}',
     'FSLEyesSplash.default'        : 'Loading ...',
 
     'FSLEyesFrame.restoringLayout' : 'Restoring layout from last session ...',
diff --git a/fsl/fsleyes/frame.py b/fsl/fsleyes/frame.py
index 411a52a6f..5466cdbfa 100644
--- a/fsl/fsleyes/frame.py
+++ b/fsl/fsleyes/frame.py
@@ -16,6 +16,7 @@ import wx.lib.agw.aui     as aui
 
 import fsl.data.strings   as strings
 import fsl.utils.settings as fslsettings
+import fsl.utils.status   as status
 
 import views
 import actions
@@ -117,12 +118,26 @@ class FSLEyesFrame(wx.Frame):
         
         self.__overlayList = overlayList
         self.__displayCtx  = displayCtx
+        self.__mainPanel   = wx.Panel(self)
+        self.__statusBar   = wx.StaticText(self)
         self.__auiManager  = aui.AuiManager(
-            self,
+            self.__mainPanel,
             agwFlags=(aui.AUI_MGR_RECTANGLE_HINT |
                       aui.AUI_MGR_NO_VENETIAN_BLINDS_FADE |
                       aui.AUI_MGR_LIVE_RESIZE))
 
+        self.__sizer = wx.BoxSizer(wx.VERTICAL)
+        self.__sizer.Add(self.__mainPanel, flag=wx.EXPAND, proportion=1)
+        self.__sizer.Add(self.__statusBar, flag=wx.EXPAND)
+
+        self.SetSizer(self.__sizer)
+
+        # Re-direct status updates to the status bar
+        def update(msg):
+            self.__statusBar.SetLabel(msg)
+            wx.YieldIfNeeded()
+        status.setTarget(update)
+
         # Keeping track of all open view panels
         # 
         # The __viewPanels list contains all
@@ -150,6 +165,8 @@ class FSLEyesFrame(wx.Frame):
         self.__auiManager.Bind(aui.EVT_AUI_PANE_CLOSE, self.__onViewPanelClose)
         self             .Bind(wx.EVT_CLOSE,           self.__onClose)
 
+        self.Layout()
+
         
     def getViewPanels(self):
         """Returns a list of all :class:`.ViewPanel` instances that are
@@ -223,7 +240,7 @@ class FSLEyesFrame(wx.Frame):
             childDC.syncOverlayDisplay = False
 
         panel = panelCls(
-            self,
+            self.__mainPanel,
             self.__overlayList,
             childDC)
 
diff --git a/fsl/fsleyes/gl/textures/imagetexture.py b/fsl/fsleyes/gl/textures/imagetexture.py
index 5545b341c..50c4bc9e7 100644
--- a/fsl/fsleyes/gl/textures/imagetexture.py
+++ b/fsl/fsleyes/gl/textures/imagetexture.py
@@ -16,6 +16,7 @@ import numpy     as np
 import OpenGL.GL as gl
 
 import fsl.utils.transform     as transform
+import fsl.utils.status        as status
 import fsl.fsleyes.gl.routines as glroutines
 
 import texture
@@ -527,6 +528,9 @@ class ImageTexture(texture.Texture):
             be used as-is).
         """
 
+        status.update('Preparing data for image {} - this may '
+                      'take some time ...'.format(self.image.name))
+
         image = self.image
         data  = image.data
         dtype = data.dtype
@@ -561,4 +565,6 @@ class ImageTexture(texture.Texture):
         elif dtype == np.uint16: pass
         elif dtype == np.int16:  data = np.array(data + 32768, dtype=np.uint16)
 
+        status.clear()
+
         return data
diff --git a/fsl/fsleyes/overlay.py b/fsl/fsleyes/overlay.py
index f1411e491..9b7ed1772 100644
--- a/fsl/fsleyes/overlay.py
+++ b/fsl/fsleyes/overlay.py
@@ -21,8 +21,8 @@ an overlay type are:
   - Must be able to be created with a single ``__init__`` parameter, which
     is a string specifying the data source location (e.g. a file name).
 
-  - Must have an attribute called ``name``, which is used as the display name
-    for the overlay.
+  - Must have an attribute called ``name``, which is used as the initial
+    display name for the overlay.
 
   - Must have an attribute called ``dataSource``, which is used to identify
     the source of the overlay data.
@@ -66,6 +66,7 @@ import props
 
 import fsl.data.strings   as strings
 import fsl.utils.settings as fslsettings
+import fsl.utils.status   as status
 
 
 log = logging.getLogger(__name__)
@@ -267,7 +268,7 @@ def loadOverlays(paths, loadFunc='default', errorFunc='default', saveDir=True):
 
     :arg loadFunc:  A function which is called just before each overlay
                     is loaded, and is passed the overlay path. The default
-                    load function uses a :mod:`wx` popup frame to display
+                    load function uses the :mod:`.status` module to display
                     the name of the overlay currently being loaded. Pass in
                     ``None`` to disable this default behaviour.
 
@@ -289,18 +290,11 @@ def loadOverlays(paths, loadFunc='default', errorFunc='default', saveDir=True):
 
     defaultLoad = loadFunc == 'default'
 
-    # If the default load function is
-    # being used, create a dialog window
-    # to show the currently loading image
-    if defaultLoad:
-        import fsl.utils.dialog as fsldlg
-        loadDlg = fsldlg.SimpleMessageDialog()
-
     # The default load function updates
     # the dialog window created above
     def defaultLoadFunc(s):
         msg = strings.messages['overlay.loadOverlays.loading'].format(s)
-        loadDlg.SetMessage(msg)
+        status.update(msg)
 
     # The default error function
     # shows an error dialog
@@ -325,11 +319,6 @@ def loadOverlays(paths, loadFunc='default', errorFunc='default', saveDir=True):
     
     overlays = []
 
-    # If using the default load 
-    # function, show the dialog
-    if defaultLoad:
-        loadDlg.Show()
-
     # Load the images
     for path in paths:
 
@@ -349,8 +338,7 @@ def loadOverlays(paths, loadFunc='default', errorFunc='default', saveDir=True):
         except Exception as e: errorFunc(path, e)
 
     if defaultLoad:
-        loadDlg.Close()
-        loadDlg.Destroy()
+        status.clear()
 
     if saveDir and len(paths) > 0:
         fslsettings.write('loadOverlayLastDir', op.dirname(paths[-1]))
diff --git a/fsl/fsleyes/perspectives.py b/fsl/fsleyes/perspectives.py
index 20db6f42f..d23e63530 100644
--- a/fsl/fsleyes/perspectives.py
+++ b/fsl/fsleyes/perspectives.py
@@ -25,7 +25,7 @@ import textwrap
 import collections
 
 import fsl.utils.settings   as fslsettings
-import fsl.utils.dialog     as fsldlg
+import fsl.utils.status     as status
 import fsl.data.strings     as strings
 
 
@@ -71,7 +71,7 @@ def loadPerspective(frame, name, **kwargs):
     applyPerspective(frame, name, persp, **kwargs)
 
 
-def applyPerspective(frame, name, perspective, showMessage=True, message=None):
+def applyPerspective(frame, name, perspective, message=None):
     """
     """
               
@@ -80,14 +80,12 @@ def applyPerspective(frame, name, perspective, showMessage=True, message=None):
 
     # Show a message while re-configuring the frame
 
-    if showMessage:
-        if message is None:
-            message = strings.messages[
-                'perspectives.applyingPerspective'].format(
-                    strings.perspectives.get(name, name))
+    if message is None:
+        message = strings.messages[
+            'perspectives.applyingPerspective'].format(
+                strings.perspectives.get(name, name))
             
-        dlg = fsldlg.SimpleMessageDialog(frame, message)
-        dlg.Show()
+    status.update(message)
 
     # Clear all existing view
     # panels from the frame
@@ -112,9 +110,7 @@ def applyPerspective(frame, name, perspective, showMessage=True, message=None):
             
         vp.getAuiManager().LoadPerspective(vpLayout)
 
-    if showMessage:
-        dlg.Close()
-        dlg.Destroy()
+    status.clear()
 
             
 def savePerspective(frame, name):
diff --git a/fsl/fsleyes/splash.py b/fsl/fsleyes/splash.py
index a3b8b8a6b..3ee9c2ae3 100644
--- a/fsl/fsleyes/splash.py
+++ b/fsl/fsleyes/splash.py
@@ -72,3 +72,4 @@ class FSLEyesSplash(wx.Frame):
     def SetStatus(self, text):
         """Sets the text shown on the status bar to the specified ``text``. """
         self.__statusBar.SetLabel(text)
+        wx.YieldIfNeeded()
diff --git a/fsl/tools/fsleyes.py b/fsl/tools/fsleyes.py
index a4afbf937..40ecacde5 100644
--- a/fsl/tools/fsleyes.py
+++ b/fsl/tools/fsleyes.py
@@ -27,6 +27,7 @@ import argparse
 import fsl.fsleyes.fsleyes_parseargs as fsleyes_parseargs
 import fsl.fsleyes.displaycontext    as displaycontext
 import fsl.fsleyes.overlay           as fsloverlay
+import fsl.utils.status              as status
 
 
 log = logging.getLogger(__name__)
@@ -90,11 +91,10 @@ def context(args):
     frame.CentreOnScreen()
     frame.Show()
     frame.Update()
-    wx.Yield()
+    wx.YieldIfNeeded()
 
     import props
-    import fsl.fsleyes.gl   as fslgl
-    import fsl.data.strings as strings
+    import fsl.fsleyes.gl as fslgl
 
     props.initGUI()
     
@@ -103,9 +103,9 @@ def context(args):
     fslgl.getWXGLContext(frame)
     fslgl.bootstrap(args.glversion)
 
-    def status(overlay):
-        frame.SetStatus(strings.messages['fsleyes.loading'].format(overlay))
-        wx.Yield()
+    # Redirect status updates
+    # to the splash frame
+    status.setTarget(frame.SetStatus)
 
     # Create the overlay list (only one of these
     # ever exists) and the master DisplayContext.
@@ -131,8 +131,7 @@ def context(args):
     
     # Load the images - the splash screen status will 
     # be updated with the currently loading overlay name
-    fsleyes_parseargs.applyOverlayArgs(
-        args, overlayList, displayCtx, loadFunc=status)  
+    fsleyes_parseargs.applyOverlayArgs(args, overlayList, displayCtx)  
 
     return overlayList, displayCtx, frame
 
@@ -177,6 +176,21 @@ def interface(parent, args, ctx):
     frame = fsleyesframe.FSLEyesFrame(
         parent, overlayList, displayCtx, restore)
 
+    # Make sure the new frame is shown
+    # before destroying the splash screen
+    frame.Show(True)
+    frame.Refresh()
+    frame.Update()
+
+    # Closing the splash screen immediately
+    # can cause a crash under linux/GTK, so
+    # we'll do it a bit later.
+    def closeSplash():
+        splashFrame.Close()
+
+    wx.CallLater(1, closeSplash)
+    wx.YieldIfNeeded()
+
     # Otherwise, we add the scene
     # specified by the user
     if   args.scene == 'ortho':    frame.addViewPanel(op .OrthoPanel)
@@ -202,21 +216,6 @@ def interface(parent, args, ctx):
         viewPanel.getXCanvas().centreDisplayAt(*xcentre)
         viewPanel.getYCanvas().centreDisplayAt(*ycentre)
         viewPanel.getZCanvas().centreDisplayAt(*zcentre)
-
-    # Make sure the new frame is shown
-    # before destroying the splash screen
-    frame.Show(True)
-    frame.Refresh()
-    frame.Update()
-
-    # Closing the splash screen immediately
-    # can cause a crash under linux/GTK, so
-    # we'll do it a bit later.
-    def closeSplash():
-        splashFrame.Close()
-
-    wx.CallLater(500, closeSplash)
-    
     return frame
 
     
diff --git a/fsl/utils/status.py b/fsl/utils/status.py
new file mode 100644
index 000000000..894302dba
--- /dev/null
+++ b/fsl/utils/status.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python
+#
+# status.py - A simple interface for displaying messages.
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+"""This is a little module which provides an interface for displaying a
+message, or status update, to the user.  The ``status`` module provides the
+following functions:
+
+ .. autosummary::
+    :nosignatures:
+
+    setTarget
+    update
+    clear
+
+
+The :func:`update` function may be used to display a message. By default, the
+message is simply logged (via the ``logging`` module). However, if a status
+target has been set via the :func:`setTarget` function, the message is also
+passed to this target. 
+"""
+
+
+import logging
+
+
+log = logging.getLogger(__name__)
+
+
+statusUpdateTarget = None
+"""A reference to the status update target - this is ``None`` by default, and
+can be set via :func:`setTarget`.
+"""
+
+
+def setTarget(target):
+    """Set a target function to receive status updates. The ``target`` must
+    be a function which accepts a string as its sole parameter.
+    """
+    global statusUpdateTarget
+    statusUpdateTarget = target
+
+
+def update(message):
+    """Display a status update to the user. The message is logged and,
+    if a status update target has been set, passed to the target.
+    """
+
+    global statusUpdateTarget
+
+    log.debug(message)
+
+    if statusUpdateTarget is None:
+        return
+        
+    statusUpdateTarget(message)
+
+
+def clear():
+    """Clear the status. If a status update target has been set, it is passed
+    the empty string.
+    """ 
+    if statusUpdateTarget is None:
+        return
+        
+    statusUpdateTarget('') 
-- 
GitLab