diff --git a/doc/images/overlaydisplaypanel.png b/doc/images/overlaydisplaypanel.png new file mode 100644 index 0000000000000000000000000000000000000000..a6d703147a9a4ef715f19124a11ba4abb82c2a77 Binary files /dev/null and b/doc/images/overlaydisplaypanel.png differ diff --git a/doc/images/overlayinfopanel.png b/doc/images/overlayinfopanel.png new file mode 100644 index 0000000000000000000000000000000000000000..8f8ce30d6951de94bd3750e3612b0fff52bd01c9 Binary files /dev/null and b/doc/images/overlayinfopanel.png differ diff --git a/fsl/fsleyes/controls/overlaydisplaypanel.py b/fsl/fsleyes/controls/overlaydisplaypanel.py index 210478f5affda6d2f223abb571e542da39f9475f..dee0feb071e040af9c003bcdc637cd5cbfd1f1c5 100644 --- a/fsl/fsleyes/controls/overlaydisplaypanel.py +++ b/fsl/fsleyes/controls/overlaydisplaypanel.py @@ -1,14 +1,14 @@ #!/usr/bin/env python # -# overlaydisplaypanel.py - A panel which shows display control options for the -# currently selected overlay. +# overlaydisplaypanel.py - The OverlayDisplayPanel. # # Author: Paul McCarthy <pauldmccarthy@gmail.com> -"""A :class:`wx.panel` which shows display control optionns for the currently -selected overlay. +"""This module provides the :class:`OverlayDisplayPanel` class, a *FSLeyes +control* panel which allows the user to change overlay display settings. """ + import logging import wx @@ -24,123 +24,44 @@ import fsl.fsleyes.actions.loadcolourmap as loadcmap import fsl.fsleyes.displaycontext as displayctx - log = logging.getLogger(__name__) - -def _imageName(img): - if img is None: return 'None' - else: return img.name - - -_DISPLAY_PROPS = td.TypeDict({ - 'Display' : [ - props.Widget('name'), - props.Widget('overlayType', - labels=strings.choices['Display.overlayType']), - props.Widget('enabled'), - props.Widget('alpha', showLimits=False), - props.Widget('brightness', showLimits=False), - props.Widget('contrast', showLimits=False)], - - 'VolumeOpts' : [ - props.Widget('resolution', showLimits=False), - props.Widget('transform', - labels=strings.choices['ImageOpts.transform']), - props.Widget('volume', - showLimits=False, - enabledWhen=lambda o: o.overlay.is4DImage()), - props.Widget('interpolation', - labels=strings.choices['VolumeOpts.interpolation']), - props.Widget('cmap'), - props.Widget('invert'), - props.Widget('invertClipping', - enabledWhen=lambda o, sw: not sw, - dependencies=[(lambda o: o.display, 'softwareMode')]), - props.Widget('displayRange', - showLimits=False, - slider=True, - labels=[strings.choices['VolumeOpts.displayRange.min'], - strings.choices['VolumeOpts.displayRange.max']]), - props.Widget('clippingRange', - showLimits=False, - slider=True, - labels=[strings.choices['VolumeOpts.displayRange.min'], - strings.choices['VolumeOpts.displayRange.max']])], - - 'MaskOpts' : [ - props.Widget('resolution', showLimits=False), - props.Widget('transform', - labels=strings.choices['ImageOpts.transform']), - props.Widget('volume', - showLimits=False, - enabledWhen=lambda o: o.overlay.is4DImage()), - props.Widget('colour'), - props.Widget('invert'), - props.Widget('threshold', showLimits=False)], - - 'RGBVectorOpts' : [ - props.Widget('resolution', showLimits=False), - props.Widget('transform', - labels=strings.choices['ImageOpts.transform']), - props.Widget('interpolation'), - props.Widget('xColour'), - props.Widget('yColour'), - props.Widget('zColour'), - props.Widget('suppressX'), - props.Widget('suppressY'), - props.Widget('suppressZ'), - props.Widget('modulate', labels=_imageName), - props.Widget('modThreshold', showLimits=False, spin=False)], - - 'LineVectorOpts' : [ - props.Widget('resolution', showLimits=False), - props.Widget('transform', - labels=strings.choices['ImageOpts.transform']), - props.Widget('xColour'), - props.Widget('yColour'), - props.Widget('zColour'), - props.Widget('suppressX'), - props.Widget('suppressY'), - props.Widget('suppressZ'), - props.Widget('directed'), - props.Widget('lineWidth', showLimits=False), - props.Widget('modulate', labels=_imageName), - props.Widget('modThreshold', showLimits=False, spin=False)], - - 'ModelOpts' : [ - props.Widget('colour'), - props.Widget('outline'), - props.Widget('outlineWidth', showLimits=False), - props.Widget('refImage', labels=_imageName), - # props.Widget('showName'), - props.Widget('coordSpace', - enabledWhen=lambda o, ri: ri != 'none', - dependencies=['refImage'])], - - 'LabelOpts' : [ - props.Widget('lut', labels=lambda l: l.name), - props.Widget('outline', - enabledWhen=lambda o, sw: not sw, - dependencies=[(lambda o: o.display, 'softwareMode')]), - props.Widget('outlineWidth', - showLimits=False, - enabledWhen=lambda o, sw: not sw, - dependencies=[(lambda o: o.display, 'softwareMode')]), - # props.Widget('showNames'), - props.Widget('resolution', showLimits=False), - props.Widget('transform', - labels=strings.choices['ImageOpts.transform']), - props.Widget('volume', - showLimits=False, - enabledWhen=lambda o: o.overlay.is4DImage())] -}) - class OverlayDisplayPanel(fslpanel.FSLEyesPanel): + """The ``OverlayDisplayPanel`` is a :Class:`.FSLEyesPanel` which allows + the user to change the display settings of the currently selected + overlay (which is defined by the :attr:`.DisplayContext.selectedOverlay` + property). The display settings for an overlay are contained in the + :class:`.Display` and :class:`.DisplayOpts` instances associated with + that overlay. An ``OverlayDisplayPanel`` looks something like the + following: + + .. image:: images/overlaydisplaypanel.png + :scale: 50% + :align: center + + An ``OverlayDisplayPanel`` uses a :class:`.WidgetGrid` to organise the + settings into two main sections: + + - Settings which are common across all overlays - these are defined + in the :class:`.Display` class. + + - Settings which are specific to the current + :attr:`.Display.overlayType` - these are defined in the + :class:`.DisplayOpts` sub-classes. + + + The settings that are displayed on an ``OverlayDisplayPanel`` are + defined in the :attr:`_DISPLAY_PROPS` dictionary. + """ + def __init__(self, parent, overlayList, displayCtx): - """ + """Create an ``OverlayDisplayPanel``. + + :arg parent: The :mod:`wx` parent object. + :arg overlayList: The :class:`.OverlayList` instance. + :arg displayCtx: The :class:`.DisplayContext` instance. """ fslpanel.FSLEyesPanel.__init__(self, parent, overlayList, displayCtx) @@ -169,6 +90,10 @@ class OverlayDisplayPanel(fslpanel.FSLEyesPanel): def destroy(self): + """Must be called when this ``OverlayDisplayPanel`` is no longer + needed. Removes property listeners, and calls the + :meth:`.FSLEyesPanel.destroy` method. + """ self._displayCtx .removeListener('selectedOverlay', self._name) self._overlayList.removeListener('overlays', self._name) @@ -190,6 +115,11 @@ class OverlayDisplayPanel(fslpanel.FSLEyesPanel): def __selectedOverlayChanged(self, *a): + """Called when the :class:`.OverlayList` or + :attr:`.DisplayContext.selectedOverlay` changes. Refreshes this + ``OverlayDisplayPanel`` so that the display settings for the newly + selected overlay are shown. + """ overlay = self._displayCtx.getSelectedOverlay() lastOverlay = self.__currentOverlay @@ -249,6 +179,10 @@ class OverlayDisplayPanel(fslpanel.FSLEyesPanel): def __ovlNameChanged(self, *a): + """Called when the :attr:`.Display.name` of the current overlay + changes. Updates the text label at the top of this + ``OverlayDisplayPanel``. + """ display = self._displayCtx.getDisplay(self.__currentOverlay) self.__overlayName.SetLabel(display.name) @@ -256,6 +190,11 @@ class OverlayDisplayPanel(fslpanel.FSLEyesPanel): def __ovlTypeChanged(self, *a): + """Called when the :attr:`.Display.overlayType` of the current overlay + changes. Refreshes the :class:`.DisplayOpts` settings which are shown, + as a new :class:`.DisplayOpts` instance will have been created for the + overlay. + """ opts = self._displayCtx.getOpts(self.__currentOverlay) self.__updateWidgets(opts, 'opts') @@ -263,6 +202,18 @@ class OverlayDisplayPanel(fslpanel.FSLEyesPanel): def __updateWidgets(self, target, groupName): + """Called by the :meth:`__selectedOverlayChanged` and + :meth:`__ovlTypeChanged` methods. Re-creates the controls on this + ``OverlayDisplayPanel`` for the specified group. + + :arg target: A :class:`.Display` or :class:`.DisplayOpts` instance, + which contains the properties that controls are to be + created for. + + :arg groupName: Either ``'display'`` or ``'opts'``, corresponding + to :class:`.Display` or :class:`.DisplayOpts` + properties. + """ self.__widgets.ClearGroup(groupName) @@ -325,6 +276,10 @@ class OverlayDisplayPanel(fslpanel.FSLEyesPanel): def __buildColourMapWidget(self, cmapWidget): + """Creates a control which allows the user to load a custom colour + map. This control is added to the settings for :class:`.Image` + overlays with a :attr:`.Display.overlayType` of ``'volume'``. + """ action = loadcmap.LoadColourMapAction(self._overlayList, self._displayCtx) @@ -340,3 +295,118 @@ class OverlayDisplayPanel(fslpanel.FSLEyesPanel): sizer.Add(button, flag=wx.EXPAND) return sizer + + +def _imageName(img): + """Used to generate choice labels for the :attr`.VectorOpts.modulate` and + :attr:`.ModelOpts.refImage` properties. + """ + if img is None: return 'None' + else: return img.name + + +_DISPLAY_PROPS = td.TypeDict({ + 'Display' : [ + props.Widget('name'), + props.Widget('overlayType', + labels=strings.choices['Display.overlayType']), + props.Widget('enabled'), + props.Widget('alpha', showLimits=False), + props.Widget('brightness', showLimits=False), + props.Widget('contrast', showLimits=False)], + + 'VolumeOpts' : [ + props.Widget('resolution', showLimits=False), + props.Widget('transform', + labels=strings.choices['ImageOpts.transform']), + props.Widget('volume', + showLimits=False, + enabledWhen=lambda o: o.overlay.is4DImage()), + props.Widget('interpolation', + labels=strings.choices['VolumeOpts.interpolation']), + props.Widget('cmap'), + props.Widget('invert'), + props.Widget('invertClipping', + enabledWhen=lambda o, sw: not sw, + dependencies=[(lambda o: o.display, 'softwareMode')]), + props.Widget('displayRange', + showLimits=False, + slider=True, + labels=[strings.choices['VolumeOpts.displayRange.min'], + strings.choices['VolumeOpts.displayRange.max']]), + props.Widget('clippingRange', + showLimits=False, + slider=True, + labels=[strings.choices['VolumeOpts.displayRange.min'], + strings.choices['VolumeOpts.displayRange.max']])], + + 'MaskOpts' : [ + props.Widget('resolution', showLimits=False), + props.Widget('transform', + labels=strings.choices['ImageOpts.transform']), + props.Widget('volume', + showLimits=False, + enabledWhen=lambda o: o.overlay.is4DImage()), + props.Widget('colour'), + props.Widget('invert'), + props.Widget('threshold', showLimits=False)], + + 'RGBVectorOpts' : [ + props.Widget('resolution', showLimits=False), + props.Widget('transform', + labels=strings.choices['ImageOpts.transform']), + props.Widget('interpolation'), + props.Widget('xColour'), + props.Widget('yColour'), + props.Widget('zColour'), + props.Widget('suppressX'), + props.Widget('suppressY'), + props.Widget('suppressZ'), + props.Widget('modulate', labels=_imageName), + props.Widget('modThreshold', showLimits=False, spin=False)], + + 'LineVectorOpts' : [ + props.Widget('resolution', showLimits=False), + props.Widget('transform', + labels=strings.choices['ImageOpts.transform']), + props.Widget('xColour'), + props.Widget('yColour'), + props.Widget('zColour'), + props.Widget('suppressX'), + props.Widget('suppressY'), + props.Widget('suppressZ'), + props.Widget('directed'), + props.Widget('lineWidth', showLimits=False), + props.Widget('modulate', labels=_imageName), + props.Widget('modThreshold', showLimits=False, spin=False)], + + 'ModelOpts' : [ + props.Widget('colour'), + props.Widget('outline'), + props.Widget('outlineWidth', showLimits=False), + props.Widget('refImage', labels=_imageName), + # props.Widget('showName'), + props.Widget('coordSpace', + enabledWhen=lambda o, ri: ri != 'none', + dependencies=['refImage'])], + + 'LabelOpts' : [ + props.Widget('lut', labels=lambda l: l.name), + props.Widget('outline', + enabledWhen=lambda o, sw: not sw, + dependencies=[(lambda o: o.display, 'softwareMode')]), + props.Widget('outlineWidth', + showLimits=False, + enabledWhen=lambda o, sw: not sw, + dependencies=[(lambda o: o.display, 'softwareMode')]), + # props.Widget('showNames'), + props.Widget('resolution', showLimits=False), + props.Widget('transform', + labels=strings.choices['ImageOpts.transform']), + props.Widget('volume', + showLimits=False, + enabledWhen=lambda o: o.overlay.is4DImage())] +}) +"""This dictionary contains specifications for all controls that are shown on +an ``OverlayDisplayPanel``. +""" diff --git a/fsl/fsleyes/controls/overlayinfopanel.py b/fsl/fsleyes/controls/overlayinfopanel.py index 245025693ba9d85bd468f0b96ddfb5ea23338672..8f15f85342208b794a0aca449b20e9df794764a2 100644 --- a/fsl/fsleyes/controls/overlayinfopanel.py +++ b/fsl/fsleyes/controls/overlayinfopanel.py @@ -1,9 +1,13 @@ #!/usr/bin/env python # -# overlayinfopanel.py - +# overlayinfopanel.py - The OverlayInfoPanel class. # # Author: Paul McCarthy <pauldmccarthy@gmail.com> # +"""This module provides the ``OverlayInfoPanel`` class, a *FSLeyes control* +panel which displays information about the currently selected overlay. +""" + import collections @@ -15,33 +19,35 @@ import fsl.data.constants as constants import fsl.fsleyes.panel as fslpanel -class OverlayInfo(object): - """A little class which encapsulates human-readable information about - one overlay. ``OverlayInfo`` objects are created and returned by the - ``OverlayInfoPanel.__get*Info`` methods. - """ - - def __init__(self, title): - - self.title = title - self.info = [] - self.sections = collections.OrderedDict() - - - def addSection(self, section): - self.sections[section] = [] - - - def addInfo(self, name, info, section=None): - if section is None: self.info .append((name, info)) - else: self.sections[section].append((name, info)) - - - class OverlayInfoPanel(fslpanel.FSLEyesPanel): + """An ``OverlayInfoPanel`` is a :class:`.FSLEyesPanel` which displays + information about the currently selected overlay in a + ``wx.html.HtmlWindow``. The currently selected overlay is defined by the + :attr:`.DisplayContext.selectedOverlay` property. An ``OverlayInfoPanel`` + looks something like the following: + + .. image:: images/overlayinfopanel.png + :scale: 50% + :align: center + + Slightly different informtion is shown depending on the overlay type, + and is generated by the following methods: + + =================== ========================== + :class:`.Image` :meth:`__getImageInfo` + :class:`.FEATImage` :meth:`__getFEATImageInfo` + :class:`.Model` :meth:`__getModelInfo` + =================== ========================== + """ def __init__(self, parent, overlayList, displayCtx): + """Create an ``OverlayInfoPanel``. + + :arg parent: The :mod:`wx` parent object. + :arg overlayList: The :class:`.OverlayList` instance. + :arg displayCtx: The :class:`.DisplayContext` instance. + """ fslpanel.FSLEyesPanel.__init__(self, parent, overlayList, displayCtx) @@ -69,6 +75,11 @@ class OverlayInfoPanel(fslpanel.FSLEyesPanel): def destroy(self): + """Must be called when this ``OverlayInfoPanel`` is no longer + needed. Removes some property listeners, and calls the + :meth:`.FSLEyesPanel.destroy` method. + """ + self._displayCtx .removeListener('selectedOverlay', self._name) self._overlayList.removeListener('overlays', self._name) @@ -82,6 +93,10 @@ class OverlayInfoPanel(fslpanel.FSLEyesPanel): def __selectedOverlayChanged(self, *a): + """Called when the :class:`.OverlayList` or + :attr:`.DisplayContext.selectedOverlay` changes. Refreshes the + information shown on this ``OverlayInfoPanel``. + """ overlay = self._displayCtx.getSelectedOverlay() @@ -114,10 +129,17 @@ class OverlayInfoPanel(fslpanel.FSLEyesPanel): def __overlayNameChanged(self, *a): + """Called when the :attr:`.Display.name` for the current overlay + changes. Updates the information display. + """ self.__updateInformation() def __updateInformation(self): + """Refreshes the information shown on this ``OverlayInfoPanel``. + Called by the :meth:`__selectedOverlayChanged` and + :meth:`__overlayNameChanged` methods. + """ overlay = self.__currentOverlay display = self.__currentDisplay @@ -125,6 +147,8 @@ class OverlayInfoPanel(fslpanel.FSLEyesPanel): type(overlay).__name__) infoFunc = getattr(self, infoFunc, None) + # Try and preserve the + # current scroll position scrollPos = self.__info.GetViewStart() # Overlay is none, or the overlay @@ -143,6 +167,13 @@ class OverlayInfoPanel(fslpanel.FSLEyesPanel): def __getImageInfo(self, overlay, display): + """Creates and returns an :class:`OverlayInfo` object containing + information about the given :class:`.Image` overlay. + + :arg overlay: A :class:`.Image` instance. + :arg display: The :class:`.Display` instance assocated with the + ``Image``. + """ info = OverlayInfo('{} - {}'.format( display.name, strings.labels[self, overlay])) @@ -238,6 +269,13 @@ class OverlayInfoPanel(fslpanel.FSLEyesPanel): def __getFEATImageInfo(self, overlay, display): + """Creates and returns an :class:`OverlayInfo` object containing + information about the given :class:`.FEATImage` overlay. + + :arg overlay: A :class:`.FEATImage` instance. + :arg display: The :class:`.Display` instance assocated with the + ``FEATImage``. + """ info = self.__getImageInfo(overlay, display) featInfo = collections.OrderedDict([ @@ -257,6 +295,13 @@ class OverlayInfoPanel(fslpanel.FSLEyesPanel): def __getModelInfo(self, overlay, display): + """Creates and returns an :class:`OverlayInfo` object containing + information about the given :class:`.Model` overlay. + + :arg overlay: A :class:`.Model` instance. + :arg display: The :class:`.Display` instance assocated with the + ``Model``. + """ info = OverlayInfo('{} - {}'.format( display.name, strings.labels[self, overlay])) @@ -273,6 +318,9 @@ class OverlayInfoPanel(fslpanel.FSLEyesPanel): def __formatArray(self, array): + """Creates and returns a string containing a HTML table which + formats the data in the given ``numpy.array``. + """ lines = [] @@ -290,6 +338,9 @@ class OverlayInfoPanel(fslpanel.FSLEyesPanel): def __formatOverlayInfo(self, info): + """Creates and returns a string containing some HTML which formats + the information in the given ``OverlayInfo`` instance. + """ lines = [] lines.append('<h3>{}</h3>'.format(info.title)) @@ -322,3 +373,45 @@ class OverlayInfoPanel(fslpanel.FSLEyesPanel): lines.append('</table>') return '\n'.join(lines) + + +class OverlayInfo(object): + """A little class which encapsulates human-readable information about + one overlay. ``OverlayInfo`` objects are created and returned by the + ``OverlayInfoPanel.__get*Info`` methods. + + The information stored in an ``OverlayInfo`` instance is organised into + *sections*. Within each section, information is organised into key-value + pairs. The order in which both ``OverlayInfo`` sections, and information, + is ultimately output, is the order in which the sections/information are + added, via the :meth:`addSection` and :meth:`addInfo` methods. + """ + + def __init__(self, title): + """Create an ``OverlayInfo`` instance. + + :arg title: The ``OverlaytInfo`` title. + """ + + self.title = title + self.info = [] + self.sections = collections.OrderedDict() + + + def addSection(self, section): + """Add a section to this ``OverlayInfo`` instance. + + :arg section: The section name. + """ + self.sections[section] = [] + + + def addInfo(self, name, info, section=None): + """Add some information to this ``OverlayInfo`` instance. + + :arg name: The information name. + :arg info: The information value. + :arg section: Section to place the information in. + """ + if section is None: self.info .append((name, info)) + else: self.sections[section].append((name, info))