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