diff --git a/fsl/data/strings.py b/fsl/data/strings.py index 48f017d3f0b69e98791b083d9d8cbd7c2370275b..52722919cb21df9f7ae6ddde0fac99dab405010f 100644 --- a/fsl/data/strings.py +++ b/fsl/data/strings.py @@ -55,6 +55,10 @@ messages = TypeDict({ 'perspectives.savePerspective' : 'Enter a name for the perspective', 'perspectives.applyingPerspective' : 'Applying {} perspective ...', + 'ClearPerspectiveAction.confirmClear' : 'All saved perspectives will be ' + 'cleared! Are you sure you want ' + 'to continue?', + 'overlay.loadOverlays.loading' : 'Loading {} ...', 'overlay.loadOverlays.error' : 'An error occurred loading the image ' '{}\n\nDetails: {}', @@ -193,17 +197,21 @@ titles = TypeDict({ 'MelodicClassificationPanel.saveDialog' : 'Save FIX/Melview file...', 'MelodicClassificationPanel.loadError' : 'Error loading FIX/Melview file', 'MelodicClassificationPanel.saveError' : 'Error saving FIX/Melview file', + + + 'ClearPerspectiveAction.confirmClear' : 'Clear all perspectives?', }) actions = TypeDict({ - 'OpenFileAction' : 'Add overlay file', - 'OpenStandardAction' : 'Add standard', - 'CopyOverlayAction' : 'Copy overlay', - 'SaveOverlayAction' : 'Save overlay', - 'LoadColourMapAction' : 'Load custom colour map', - 'SavePerspectiveAction' : 'Save current perspective', + 'OpenFileAction' : 'Add overlay file', + 'OpenStandardAction' : 'Add standard', + 'CopyOverlayAction' : 'Copy overlay', + 'SaveOverlayAction' : 'Save overlay', + 'LoadColourMapAction' : 'Load custom colour map', + 'SavePerspectiveAction' : 'Save current perspective', + 'ClearPerspectiveAction' : 'Clear all perspectives', 'FSLEyesFrame.closeViewPanel' : 'Close', @@ -785,6 +793,7 @@ melodic = TypeDict({ }) perspectives = { - 'melview' : 'Melodic mode', + 'default' : 'Default layout', + 'melodic' : 'Melodic mode', 'feat' : 'FEAT mode', } diff --git a/fsl/fsleyes/actions/__init__.py b/fsl/fsleyes/actions/__init__.py index 489ed76de956a7caa20c52eb5e70a786d3dc9626..a6a96d961a0e1f2530eaac055002837ff06e4836 100644 --- a/fsl/fsleyes/actions/__init__.py +++ b/fsl/fsleyes/actions/__init__.py @@ -4,12 +4,12 @@ # # Author: Paul McCarthy <pauldmccarthy@gmail.com> # -"""This package provides a collection of actions, two package-level -classes - the :class:`Action` class and the :class:`ActionProvider` class, -and the :func:`action` and :func:`toggleAction` decorators. +"""This package provides a collection of actions, classes - the +:class:`.Action` class and the :class:`.ActionProvider` class, and the +:func:`action` and :func:`toggleAction` decorators. -The :class:`Action` class represents some sort of action which may be +The :class:`.Action` class represents some sort of action which may be performed, enabled and disabled, and may be bound to a GUI menu item or button. The :class:`ActionProvider` class represents some entity which can perform one or more actions. As the :class:`.FSLEyesPanel` class derives from @@ -58,16 +58,16 @@ and :meth:`ActionProvider.disableAction` methods:: It is useful to know that each method on the ``t`` instance has actually been -replaced with an :class:`Action` instance, which encapsulates the method. Using -this knowledge, you can access the ``Action`` instances directly:: +replaced with an :class:`.Action` instance, which encapsulates the method. +Using this knowledge, you can access the ``Action`` instances directly:: >>> t.doFirstThing.enabled = True >>> t.doFirstThing() First thing done -The :meth:`Action.bindToWidget` method allows a widget to be bound to an -:class:`Action`. For example:: +The :meth:`.Action.bindToWidget` method allows a widget to be bound to an +:class:`.Action`. For example:: # We're assuming here that a wx.App, and # a parent window, has been created @@ -76,8 +76,8 @@ The :meth:`Action.bindToWidget` method allows a widget to be bound to an All bound widgets of an ``Action`` can be accessed through the -:meth:`Action.getBoundWidgets` method, and can be unbound via the -:meth:`Action.unbindAllWidgets` method. +:meth:`.Action.getBoundWidgets` method, and can be unbound via the +:meth:`.Action.unbindAllWidgets` method. This module also provides two classes which allow a widget to be automatically @@ -100,6 +100,9 @@ Finally, some 'global' actions are also provided in this package: ~fsl.fsleyes.actions.openfile ~fsl.fsleyes.actions.openstandard ~fsl.fsleyes.actions.saveoverlay + ~fsl.fsleyes.actions.saveperspective + ~fsl.fsleyes.actions.loadperspective + ~fsl.fsleyes.actions.clearperspective """ @@ -111,6 +114,28 @@ import props import fsl.data.strings as strings +import action +import copyoverlay +import openfile +import openstandard +import saveoverlay +import loadcolourmap +import saveperspective +import loadperspective +import clearperspective + + +Action = action .Action +ToggleAction = action .ToggleAction +CopyOverlayAction = copyoverlay .CopyOverlayAction +OpenFileAction = openfile .OpenFileAction +OpenStandardAction = openstandard .OpenStandardAction +SaveOverlayAction = saveoverlay .SaveOverlayAction +LoadColourMapAction = loadcolourmap .LoadColourMapAction +SavePerspectiveAction = saveperspective .SavePerspectiveAction +LoadPerspectiveAction = loadperspective .LoadPerspectiveAction +ClearPerspectiveAction = clearperspective.ClearPerspectiveAction + log = logging.getLogger(__name__) @@ -125,194 +150,6 @@ def toggleAction(func): return ActionFactory(func, ToggleAction) -class Action(props.HasProperties): - """Represents an action of some sort. - """ - - - enabled = props.Boolean(default=True) - """Controls whether the action is currently enabled or disabled. - When this property is ``False`` calls to the action will - result in a :exc:`ActionDisabledError`. - """ - - - def __init__(self, func, instance=None): - """Create an ``Action``. - - :arg func: The action function. - - :arg instance: Object associated with the function, if this ``Action`` - is encapsulating an instance method. - """ - self.__instance = instance - self.__func = func - self.__name = func.__name__ - self.__boundWidgets = [] - - self.addListener('enabled', - 'Action_{}_internal'.format(id(self)), - self.__enabledChanged) - - - def __str__(self): - """Returns a string representation of this ``Action``. """ - return '{}({})'.format(type(self).__name__, self.__name) - - - def __repr__(self): - """Returns a string representation of this ``Action``. """ - return self.__str__() - - - def __call__(self, *args, **kwargs): - """Calls this action. An :exc:`ActionDisabledError` will be raised - if :attr:`enabled` is ``False``. - """ - - if not self.enabled: - raise ActionDisabledError('Action {} is disabled'.format( - self.__name)) - - log.debug('Action {}.{} called'.format( - type(self.__instance).__name__, - self.__name)) - - if self.__instance is not None: - args = [self.__instance] + list(args) - - return self.__func(*args, **kwargs) - - - def destroy(self): - """Must be called when this ``Action`` is no longer needed. """ - self.unbindAllWidgets() - self.__func = None - self.__instance = None - - - def bindToWidget(self, parent, evType, widget): - """Binds this action to the given :mod:`wx` widget. - - :arg parent: The :mod:`wx` object on which the event should be bound. - :arg evType: The :mod:`wx` event type. - :arg widget: The :mod:`wx` widget. - """ - - def wrappedAction(ev): - self() - - parent.Bind(evType, wrappedAction, widget) - widget.Enable(self.enabled) - self.__boundWidgets.append((parent, evType, widget)) - - - def unbindAllWidgets(self): - """Unbinds all widgets which have been bound via :meth:`bindToWidget`. - """ - - import wx - - for parent, evType, widget in self.__boundWidgets: - - # Only attempt to unbind if the parent - # and widget have not been destroyed - try: - parent.Unbind(evType, source=widget) - except wx.PyDeadObjectError: - pass - - self.__boundWidgets = [] - - - def getBoundWidgets(self): - """Returns a list containing all widgets which have been bound to - this ``Action``. - """ - return [w for _, _, w in self.__boundWidgets] - - - def __enabledChanged(self, *args): - """Internal method which is called when the :attr:`enabled` property - changes. Enables/disables any bound widgets. - """ - - for _, _, widget in self.__boundWidgets: - widget.Enable(self.enabled) - - -class ToggleAction(Action): - """A ``ToggleAction`` an ``Action`` which is intended to encapsulate - actions that toggle some sort of state. For example, a ``ToggleAction`` - could be used to encapsulate an action which opens and/or closes a dialog - window. - """ - - - toggled = props.Boolean(default=False) - """Boolean which tracks the current state of the ``ToggleAction``. """ - - - def __init__(self, *args, **kwargs): - """Create a ``ToggleAction``. All arguments are passed to - :meth:`Action.__init__`. - """ - - Action.__init__(self, *args, **kwargs) - - self.addListener('toggled', - 'ToggleAction_{}_internal'.format(id(self)), - self.__toggledChanged) - - - def __call__(self, *args, **kwargs): - """Call this ``ToggleAction`. The value of the :attr:`toggled` property - is flipped. - """ - - # Copy the toggled value before running - # the action, in case it gets inadvertently - # changed - toggled = self.toggled - result = Action.__call__(self, *args, **kwargs) - self.toggled = not toggled - - return result - - - def bindToWidget(self, parent, evType, widget): - """Bind this ``ToggleAction`` to a widget. If the widget is a - ``wx.MenuItem``, its ``Check`` is called whenever the :attr:`toggled` - state changes. - """ - - import wx - - Action.bindToWidget(self, parent, evType, widget) - - if isinstance(widget, wx.MenuItem): - widget.Check(self.toggled) - - - def __toggledChanged(self, *a): - """Internal method called when :attr:`toggled` changes. Updates the - state of any bound widgets. - """ - - import wx - import pwidgets.bitmaptoggle as bmptoggle - - for widget in self.getBoundWidgets(): - - if isinstance(widget, wx.MenuItem): - widget.Check(self.toggled) - - elif isinstance(widget, (wx.CheckBox, - wx.ToggleButton, - bmptoggle.BitmapToggleButton)): - widget.SetValue(self.toggled) - - class ActionProvider(object): """The ``ActionProvider`` class is intended to be used as a base class for classes which contain actions. The :func:`action` and :func:`toggleAction` @@ -360,13 +197,6 @@ class ActionProvider(object): return acts -class ActionDisabledError(Exception): - """Exception raised when an attempt is made to call a disabled - :class:`Action`. - """ - pass - - class ActionFactory(object): """The ``ActionFactory`` is used by the :func:`action` and :func:`toggleAction` decorators. Its job is to create :class:`Action` diff --git a/fsl/fsleyes/actions/action.py b/fsl/fsleyes/actions/action.py new file mode 100644 index 0000000000000000000000000000000000000000..93f0c12957ab362e415c839ab73c39304f3841fa --- /dev/null +++ b/fsl/fsleyes/actions/action.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python +# +# action.py - The Action and ToggleAction classes. +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# +"""This module provides the :class:`Action` and :class:`ToggleAction` classes. +See the :mod:`.actions` package documentation for more details. +""" + +import logging + +import props + + +log = logging.getLogger(__name__) + + +class ActionDisabledError(Exception): + """Exception raised when an attempt is made to call a disabled + :class:`Action`. + """ + pass + + +class Action(props.HasProperties): + """Represents an action of some sort. """ + + + enabled = props.Boolean(default=True) + """Controls whether the action is currently enabled or disabled. + When this property is ``False`` calls to the action will + result in a :exc:`ActionDisabledError`. + """ + + + def __init__(self, func, instance=None): + """Create an ``Action``. + + :arg func: The action function. + + :arg instance: Object associated with the function, if this ``Action`` + is encapsulating an instance method. + """ + self.__instance = instance + self.__func = func + self.__name = func.__name__ + self.__boundWidgets = [] + + self.addListener('enabled', + 'Action_{}_internal'.format(id(self)), + self.__enabledChanged) + + + def __str__(self): + """Returns a string representation of this ``Action``. """ + return '{}({})'.format(type(self).__name__, self.__name) + + + def __repr__(self): + """Returns a string representation of this ``Action``. """ + return self.__str__() + + + def __call__(self, *args, **kwargs): + """Calls this action. An :exc:`ActionDisabledError` will be raised + if :attr:`enabled` is ``False``. + """ + + if not self.enabled: + raise ActionDisabledError('Action {} is disabled'.format( + self.__name)) + + log.debug('Action {}.{} called'.format( + type(self.__instance).__name__, + self.__name)) + + if self.__instance is not None: + args = [self.__instance] + list(args) + + return self.__func(*args, **kwargs) + + + def destroy(self): + """Must be called when this ``Action`` is no longer needed. """ + self.unbindAllWidgets() + self.__func = None + self.__instance = None + + + def bindToWidget(self, parent, evType, widget): + """Binds this action to the given :mod:`wx` widget. + + :arg parent: The :mod:`wx` object on which the event should be bound. + :arg evType: The :mod:`wx` event type. + :arg widget: The :mod:`wx` widget. + """ + + def wrappedAction(ev): + self() + + parent.Bind(evType, wrappedAction, widget) + widget.Enable(self.enabled) + self.__boundWidgets.append((parent, evType, widget)) + + + def unbindAllWidgets(self): + """Unbinds all widgets which have been bound via :meth:`bindToWidget`. + """ + + import wx + + for parent, evType, widget in self.__boundWidgets: + + # Only attempt to unbind if the parent + # and widget have not been destroyed + try: + parent.Unbind(evType, source=widget) + except wx.PyDeadObjectError: + pass + + self.__boundWidgets = [] + + + def getBoundWidgets(self): + """Returns a list containing all widgets which have been bound to + this ``Action``. + """ + return [w for _, _, w in self.__boundWidgets] + + + def __enabledChanged(self, *args): + """Internal method which is called when the :attr:`enabled` property + changes. Enables/disables any bound widgets. + """ + + for _, _, widget in self.__boundWidgets: + widget.Enable(self.enabled) + + +class ToggleAction(Action): + """A ``ToggleAction`` an ``Action`` which is intended to encapsulate + actions that toggle some sort of state. For example, a ``ToggleAction`` + could be used to encapsulate an action which opens and/or closes a dialog + window. + """ + + + toggled = props.Boolean(default=False) + """Boolean which tracks the current state of the ``ToggleAction``. """ + + + def __init__(self, *args, **kwargs): + """Create a ``ToggleAction``. All arguments are passed to + :meth:`Action.__init__`. + """ + + Action.__init__(self, *args, **kwargs) + + self.addListener('toggled', + 'ToggleAction_{}_internal'.format(id(self)), + self.__toggledChanged) + + + def __call__(self, *args, **kwargs): + """Call this ``ToggleAction`. The value of the :attr:`toggled` property + is flipped. + """ + + # Copy the toggled value before running + # the action, in case it gets inadvertently + # changed + toggled = self.toggled + result = Action.__call__(self, *args, **kwargs) + self.toggled = not toggled + + return result + + + def bindToWidget(self, parent, evType, widget): + """Bind this ``ToggleAction`` to a widget. If the widget is a + ``wx.MenuItem``, its ``Check`` is called whenever the :attr:`toggled` + state changes. + """ + + import wx + + Action.bindToWidget(self, parent, evType, widget) + + if isinstance(widget, wx.MenuItem): + widget.Check(self.toggled) + + + def __toggledChanged(self, *a): + """Internal method called when :attr:`toggled` changes. Updates the + state of any bound widgets. + """ + + import wx + import pwidgets.bitmaptoggle as bmptoggle + + for widget in self.getBoundWidgets(): + + if isinstance(widget, wx.MenuItem): + widget.Check(self.toggled) + + elif isinstance(widget, (wx.CheckBox, + wx.ToggleButton, + bmptoggle.BitmapToggleButton)): + widget.SetValue(self.toggled) diff --git a/fsl/fsleyes/actions/clearperspective.py b/fsl/fsleyes/actions/clearperspective.py new file mode 100644 index 0000000000000000000000000000000000000000..12abd6def1cf68fbbbd3e4eedd49f7f9b92cbaa8 --- /dev/null +++ b/fsl/fsleyes/actions/clearperspective.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python +# +# clearperspectives.py - The ClearPerspectiveAction class. +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# +"""This module provides the :class:`ClearPerspectiveAction`, which allows +the user to clear/delete all saved perspectives. +""" + + +import wx + +import fsl.data.strings as strings +import action +import fsl.fsleyes.perspectives as perspectives + + +class ClearPerspectiveAction(action.Action): + """The ``ClearPerspectiveAction`` allows the user to delete all saved + perspectives. + """ + + def __init__(self, frame): + """Create a ``ClearPerspectiveAction``. """ + action.Action.__init__(self, func=self.__clearPerspectives) + + self.__frame = frame + + + def __clearPerspectives(self): + """Deletes all saved perspectives. Gets the user to confirm that + they want to proceed before doing so. + """ + + dlg = wx.MessageDialog( + wx.GetTopLevelWindows()[0], + message=strings.messages[self, 'confirmClear'], + caption=strings.titles[ self, 'confirmClear'], + style=(wx.ICON_WARNING | + wx.YES_NO | + wx.NO_DEFAULT)) + + if dlg.ShowModal() != wx.ID_YES: + return + + for p in perspectives.getAllPerspectives(): + perspectives.removePerspective(p) + + self.__frame.refreshPerspectiveMenu() diff --git a/fsl/fsleyes/actions/copyoverlay.py b/fsl/fsleyes/actions/copyoverlay.py index 7c78fb11dfc7667d181ff480db57d737b981ba5b..18107fe9261c583d3990dab5c73c3ea3f8970a9b 100644 --- a/fsl/fsleyes/actions/copyoverlay.py +++ b/fsl/fsleyes/actions/copyoverlay.py @@ -9,13 +9,13 @@ which creates a copy of the currently selected overlay. """ -import numpy as np +import numpy as np -import fsl.fsleyes.actions as actions -import fsl.data.image as fslimage +import action +import fsl.data.image as fslimage -class CopyOverlayAction(actions.Action): +class CopyOverlayAction(action.Action): """The ``CopyOverlayAction`` does as its name suggests - it creates a copy of the currently selected overlay. """ @@ -27,7 +27,7 @@ class CopyOverlayAction(actions.Action): :arg overlayList: The :class:`.OverlayList`. :arg displayCtx: The :class:`.DisplayContext`. """ - actions.Action.__init__(self, self.__copyOverlay) + action.Action.__init__(self, self.__copyOverlay) self.__overlayList = overlayList self.__displayCtx = displayCtx @@ -50,7 +50,7 @@ class CopyOverlayAction(actions.Action): self.__displayCtx .removeListener('selectedOverlay', self.__name) self.__overlayList.removeListener('overlays', self.__name) - actions.Action.destroy(self) + action.Action.destroy(self) def __selectedOverlayChanged(self, *a): diff --git a/fsl/fsleyes/actions/loadcolourmap.py b/fsl/fsleyes/actions/loadcolourmap.py index 3f6c416bd00319fd5dfd1baf41ea4e4c867f4465..5742db668598792e4b86f287fd31bcec83630f49 100644 --- a/fsl/fsleyes/actions/loadcolourmap.py +++ b/fsl/fsleyes/actions/loadcolourmap.py @@ -12,7 +12,7 @@ import logging import os.path as op import fsl.data.strings as strings -import fsl.fsleyes.actions as actions +import action import fsl.fsleyes.colourmaps as fslcmap @@ -22,7 +22,7 @@ log = logging.getLogger(__name__) _stringID = 'actions.loadcolourmap.' -class LoadColourMapAction(actions.Action): +class LoadColourMapAction(action.Action): """The ``LoadColourMapAction`` allows the user to select a colour map file and give it a name. @@ -38,7 +38,7 @@ class LoadColourMapAction(actions.Action): :arg overlayList: The :class:`.OverlayList`. :arg displayCtx: The :class:`.DisplayContext`. """ - actions.Action.__init__(self, self.__loadColourMap) + action.Action.__init__(self, self.__loadColourMap) self.__overlayList = overlayList self.__displayCtx = displayCtx diff --git a/fsl/fsleyes/actions/loadperspective.py b/fsl/fsleyes/actions/loadperspective.py index ee7e37fcfc8ab68f8ea71d68883c29a188eb8647..e4e0b2f4a3108778ab3b630c7fd09c80f871768b 100644 --- a/fsl/fsleyes/actions/loadperspective.py +++ b/fsl/fsleyes/actions/loadperspective.py @@ -8,11 +8,11 @@ """ -import fsl.fsleyes.actions as actions +import action import fsl.fsleyes.perspectives as perspectives -class LoadPerspectiveAction(actions.Action): +class LoadPerspectiveAction(action.Action): """ """ @@ -23,7 +23,7 @@ class LoadPerspectiveAction(actions.Action): self.__frame = frame self.__perspective = perspective - actions.Action.__init__(self, self.__loadPerspective) + action.Action.__init__(self, self.__loadPerspective) def __loadPerspective(self): diff --git a/fsl/fsleyes/actions/openfile.py b/fsl/fsleyes/actions/openfile.py index 7e0e127c57b08a17c3e3ed10990560c15edddbe3..d74adfec3ec9b710ae54c8acf9ec93b013c11dae 100644 --- a/fsl/fsleyes/actions/openfile.py +++ b/fsl/fsleyes/actions/openfile.py @@ -9,10 +9,10 @@ load overlay files into the :class:`.OverlayList`. """ -import fsl.fsleyes.actions as actions +import action -class OpenFileAction(actions.Action): +class OpenFileAction(action.Action): """The ``OpenFileAction`` allows the user to add files to the :class:`.OverlayList`. This functionality is provided by the :meth:`.OverlayList.addOverlays` method. @@ -24,7 +24,7 @@ class OpenFileAction(actions.Action): :arg overlayList: The :class:`.OverlayList`. :arg displayCtx: The :class:`.DisplayContext`. """ - actions.Action.__init__(self, self.__openFile) + action.Action.__init__(self, self.__openFile) self.__overlayList = overlayList self.__displayCtx = displayCtx diff --git a/fsl/fsleyes/actions/openstandard.py b/fsl/fsleyes/actions/openstandard.py index 3603264c273628c884f028d110045e44ddcf9440..66032d81489d680975c8d3116d7561b38d86b1f8 100644 --- a/fsl/fsleyes/actions/openstandard.py +++ b/fsl/fsleyes/actions/openstandard.py @@ -13,10 +13,10 @@ to load in standard space images from the ``$FSLDIR/data/standard/`` directory. import os import os.path as op -import fsl.fsleyes.actions as actions +import action -class OpenStandardAction(actions.Action): +class OpenStandardAction(action.Action): """The ``OpenStandardAction`` prompts the user to open one or more overlays, using ``$FSLDIR/data/standard/`` as the default directory. """ @@ -28,7 +28,7 @@ class OpenStandardAction(actions.Action): :arg overlayList: The :class:`.OverlayList`. :arg displayCtx: The :class:`.DisplayContext`. """ - actions.Action.__init__(self, self.__openStandard) + action.Action.__init__(self, self.__openStandard) self.__overlayList = overlayList self.__displayCtx = displayCtx diff --git a/fsl/fsleyes/actions/saveoverlay.py b/fsl/fsleyes/actions/saveoverlay.py index 639fb2f0fe85c106485936d5b5edb4edfc5c0cac..f3afb29a57fce83a02cca23513a3400677dd5529 100644 --- a/fsl/fsleyes/actions/saveoverlay.py +++ b/fsl/fsleyes/actions/saveoverlay.py @@ -9,11 +9,11 @@ to save the currently selected overlay. """ -import fsl.data.image as fslimage -import fsl.fsleyes.actions as actions +import fsl.data.image as fslimage +import action -class SaveOverlayAction(actions.Action): +class SaveOverlayAction(action.Action): """The ``SaveOverlayAction`` allows the user to save the currently selected overlay, if it has been edited, or only exists in memory. """ @@ -25,7 +25,7 @@ class SaveOverlayAction(actions.Action): :arg overlayList: The :class:`.OverlayList`. :arg displayCtx: The :class:`.DisplayContext`. """ - actions.Action.__init__(self, self.__saveOverlay) + action.Action.__init__(self, self.__saveOverlay) self.__overlayList = overlayList self.__displayCtx = displayCtx @@ -48,7 +48,7 @@ class SaveOverlayAction(actions.Action): self.__displayCtx .removeListener('selectedOverlay', self.__name) self.__overlayList.removeListener('overlays', self.__name) - actions.Action.destroy(self) + action.Action.destroy(self) def __selectedOverlayChanged(self, *a): diff --git a/fsl/fsleyes/actions/saveperspective.py b/fsl/fsleyes/actions/saveperspective.py index 4f517c1eb177bade645cbafa05d6b1d447f5cac4..f121ccaeab4a1f66d73576bd3e1961873b6f6508 100644 --- a/fsl/fsleyes/actions/saveperspective.py +++ b/fsl/fsleyes/actions/saveperspective.py @@ -1,34 +1,41 @@ #!/usr/bin/env python # -# saveperspective.py - +# saveperspective.py - The SavePerspectiveAction # # Author: Paul McCarthy <pauldmccarthy@gmail.com> # -""" +"""This module provides the :class:`SavePerspectiveAction` class, an action +which allows the user to save the current perspective. """ import wx import fsl.data.strings as strings -import fsl.fsleyes.actions as actions +import action import fsl.fsleyes.perspectives as perspectives -class SavePerspectiveAction(actions.Action): - """ +class SavePerspectiveAction(action.Action): + """The ``SavePerspectiveAction`` allows the user to save the current + :class:`.FSLEyesFrame` layout as a perspective, so it can be restored + at a later time. See the :mod:`.perspectives` module. """ def __init__(self, frame): - """ + """Create a ``SavePerspectiveAction``. + + :arg frame: The :class:`.FSLEyesFrame`. """ self.__frame = frame - actions.Action.__init__(self, self.__savePerspective) + action.Action.__init__(self, self.__savePerspective) def __savePerspective(self): - """ + """Save the current :class:`.FSLEyesFrame` layout as a perspective. + The user is prompted to enter a name, and the current frame layout + is saved via the :func:`.perspectives.savePerspective` function. """ dlg = wx.TextEntryDialog( @@ -43,4 +50,10 @@ class SavePerspectiveAction(actions.Action): if name.strip() == '': return + # TODO Prevent using built-in perspective names + + # TODO Name collision - confirm overwrite + perspectives.savePerspective(self.__frame, name) + + self.__frame.refreshPerspectiveMenu() diff --git a/fsl/fsleyes/frame.py b/fsl/fsleyes/frame.py index 990493b926f75dc4487fd2dbd030269e82be85f0..a10174cf54589fdcd50e337f1ee9140e9837d5e9 100644 --- a/fsl/fsleyes/frame.py +++ b/fsl/fsleyes/frame.py @@ -10,7 +10,6 @@ for FSLeyes. import logging -import collections import wx import wx.lib.agw.aui as aui @@ -20,12 +19,6 @@ import fsl.utils.settings as fslsettings import views import actions -import actions.copyoverlay as copyoverlay -import actions.openfile as openfile -import actions.openstandard as openstandard -import actions.saveoverlay as saveoverlay -import actions.loadperspective as loadperspective -import actions.saveperspective as saveperspective import perspectives import displaycontext @@ -84,6 +77,7 @@ class FSLEyesFrame(wx.Frame): addViewPanel removeViewPanel getAuiManager + refreshPerspectiveMenu """ @@ -129,17 +123,20 @@ class FSLEyesFrame(wx.Frame): # Keeping track of all open view panels # - # The __viewPanels dict contains - # {AuiPaneInfo : ViewPanel} mappings + # The __viewPanels list contains all + # [ViewPanel] instances # # The other dicts contain # {ViewPanel : something} mappings # - self.__viewPanels = collections.OrderedDict() + self.__viewPanels = [] self.__viewPanelDCs = {} self.__viewPanelMenus = {} self.__viewPanelIDs = {} + self.__menuBar = None + self.__perspMenu = None + self.__makeMenuBar() self.__restoreState(restore) @@ -151,7 +148,7 @@ class FSLEyesFrame(wx.Frame): """Returns a list of all :class:`.ViewPanel` instances that are currenlty displayed in this ``FSLEyesFrame``. """ - return list(self.__viewPanels.values()) + return list(self.__viewPanels) def getViewPanelInfo(self, viewPanel): @@ -171,8 +168,11 @@ class FSLEyesFrame(wx.Frame): def removeViewPanel(self, viewPanel): """Removes the given :class:`.ViewPanel` from this ``FSLEyesFrame``. """ + paneInfo = self.__auiManager.GetPane(viewPanel) - self.__onViewPanelClose( paneInfo=paneInfo) + + self.__onViewPanelClose(panel=viewPanel) + self.__auiManager.ClosePane(paneInfo) self.__auiManager.Update() @@ -244,7 +244,8 @@ class FSLEyesFrame(wx.Frame): # first key is the AuiPaneInfo of # the first panel that was added. else: - self.__viewPanels.keys()[0].CaptionVisible(True) + self.__auiManager.GetPane(self.__viewPanels[0])\ + .CaptionVisible(True) # If this is not the first view panel, # give it a sensible initial size. @@ -261,9 +262,9 @@ class FSLEyesFrame(wx.Frame): else: paneInfo.Right().BestSize(width / 3, -1) - self.__viewPanels[ paneInfo] = panel - self.__viewPanelDCs[panel] = childDC - self.__viewPanelIDs[panel] = panelId + self.__viewPanels.append(panel) + self.__viewPanelDCs[ panel] = childDC + self.__viewPanelIDs[ panel] = panelId self.__auiManager.AddPane(panel, paneInfo) self.__addViewPanelMenu( panel, title) @@ -273,6 +274,11 @@ class FSLEyesFrame(wx.Frame): self.Thaw() + def refreshPerspectiveMenu(self): + """Re-creates the *View -> Perspectives* sub-menu. """ + self.__makePerspectiveMenu() + + def __addViewPanelMenu(self, panel, title): """Called by :meth:`addViewPanel`. Adds a menu item for the newly created :class:`.ViewPanel` instance. @@ -341,7 +347,7 @@ class FSLEyesFrame(wx.Frame): self.Bind(wx.EVT_MENU, closeViewPanel, closeItem) - def __onViewPanelClose(self, ev=None, paneInfo=None): + def __onViewPanelClose(self, ev=None, panel=None): """Called when the user closes a :class:`.ViewPanel`. The :meth:`__addViewPanelMenu` method adds a *Close* menu item @@ -357,16 +363,26 @@ class FSLEyesFrame(wx.Frame): if ev is not None: ev.Skip() + + # Undocumented - the window associated with an + # AuiPaneInfo is available as an attribute called + # 'window'. Honestly, I don't know why there is + # not a method available on the AuiPaneInfo or + # AuiManager to retrieve a managed Window given + # the associated AuiPaneInfo object. paneInfo = ev.GetPane() - - panel = self .__viewPanels.pop(paneInfo, None) + panel = paneInfo.window + + elif panel is not None: + paneInfo = self.__auiManager.GetPane(panel) if panel is None: return - self .__viewPanelIDs .pop(panel) - dctx = self.__viewPanelDCs .pop(panel) - menu = self.__viewPanelMenus.pop(panel, None) + self .__viewPanels .remove(panel) + self .__viewPanelIDs .pop( panel) + dctx = self.__viewPanelDCs .pop( panel) + menu = self.__viewPanelMenus.pop( panel, None) log.debug('Destroying {} ({}) and ' 'associated DisplayContext ({})'.format( @@ -392,7 +408,7 @@ class FSLEyesFrame(wx.Frame): wasCentre = paneInfo.dock_direction_get() == aui.AUI_DOCK_CENTRE if numPanels >= 1 and wasCentre: - paneInfo = self.__viewPanels.keys()[0] + paneInfo = self.__auiManager.GetPane(self.__viewPanels[0]) paneInfo.Centre().Dockable(False).CaptionVisible(numPanels > 1) @@ -414,7 +430,7 @@ class FSLEyesFrame(wx.Frame): # It's nice to explicitly clean # up our FSLEyesPanels, otherwise # they'll probably complain - for panel in self.__viewPanels.values(): + for panel in self.__viewPanels: panel.destroy() @@ -566,7 +582,6 @@ class FSLEyesFrame(wx.Frame): if restore: self.addViewPanel(views.OrthoPanel) - def __makeMenuBar(self): """Constructs a bunch of menu items for this ``FSLEyesFrame``.""" @@ -578,6 +593,9 @@ class FSLEyesFrame(wx.Frame): viewMenu = wx.Menu() perspectiveMenu = wx.Menu() settingsMenu = wx.Menu() + + self.__menuBar = menuBar + self.__perspMenu = perspectiveMenu menuBar.Append(fileMenu, 'File') menuBar.Append(viewMenu, 'View') @@ -588,10 +606,10 @@ class FSLEyesFrame(wx.Frame): self.__settingsMenu = settingsMenu # Global actions - actionz = [openfile .OpenFileAction, - openstandard.OpenStandardAction, - copyoverlay .CopyOverlayAction, - saveoverlay .SaveOverlayAction] + actionz = [actions.OpenFileAction, + actions.OpenStandardAction, + actions.CopyOverlayAction, + actions.SaveOverlayAction] for action in actionz: menuItem = fileMenu.Append(wx.ID_ANY, strings.actions[action]) @@ -614,17 +632,54 @@ class FSLEyesFrame(wx.Frame): # Perspectives viewMenu.AppendSubMenu(perspectiveMenu, 'Perspectives') - for persp in perspectives.getAllPerspectives(): + self.__makePerspectiveMenu() + + + def __makePerspectiveMenu(self): + """Re-creates the *View->Perspectives* menu. """ + + perspMenu = self.__perspMenu + + # Remove any existing menu items + for item in perspMenu.GetMenuItems(): + perspMenu.DeleteItem(item) + + builtIns = perspectives.BUILT_IN_PERSPECTIVES.keys() + saved = perspectives.getAllPerspectives() + + # Add a menu item to load each built-in perspectives + for persp in builtIns: + menuItem = perspMenu.Append( + wx.ID_ANY, strings.perspectives.get(persp, persp)) + + actionObj = actions.LoadPerspectiveAction(self, persp) + actionObj.bindToWidget(self, wx.EVT_MENU, menuItem) + + if len(builtIns) > 0: + perspMenu.AppendSeparator() + + # Add a menu item to load each saved perspective + for persp in saved: - menuItem = perspectiveMenu.Append( + menuItem = perspMenu.Append( wx.ID_ANY, strings.perspectives.get(persp, persp)) - actionObj = loadperspective.LoadPerspectiveAction(self, persp) + actionObj = actions.LoadPerspectiveAction(self, persp) actionObj.bindToWidget(self, wx.EVT_MENU, menuItem) - # Save perspective - perspectiveMenu.AppendSeparator() - savePerspAction = saveperspective.SavePerspectiveAction(self) - savePerspMenuItem = perspectiveMenu.Append( - wx.ID_ANY, strings.actions[savePerspAction]) + # Add menu items for other perspective + # operations, but separate them from the + # existing perspectives + if len(saved) > 0: + perspMenu.AppendSeparator() - savePerspAction.bindToWidget(self, wx.EVT_MENU, savePerspMenuItem) + # TODO: Delete a single perspective? + # Save to/load from file? + perspActions = [actions.SavePerspectiveAction, + actions.ClearPerspectiveAction] + + for pa in perspActions: + + actionObj = pa(self) + perspMenuItem = perspMenu.Append(wx.ID_ANY, strings.actions[pa]) + + actionObj.bindToWidget(self, wx.EVT_MENU, perspMenuItem) diff --git a/fsl/fsleyes/perspectives.py b/fsl/fsleyes/perspectives.py index 29c0f804e70f5e2dadadef9032cebd7b25998026..f8ca8d6de8022ad4f768ebc0abdef7eb276fdf20 100644 --- a/fsl/fsleyes/perspectives.py +++ b/fsl/fsleyes/perspectives.py @@ -1,16 +1,31 @@ #!/usr/bin/env python # -# perspectives.py - +# perspectives.py - The perspectives API. # # Author: Paul McCarthy <pauldmccarthy@gmail.com> # +"""This module provides functions for managing *perspectives*, view and +control panel layouts for *FSLeyes*. -import logging +.. autosummary:: + :nosignatures: + + getAllPerspectives + loadPerspective + savePerspective + removePerspective + serialisePerspective + deserialisePerspective +""" +import logging +import textwrap +import collections + import fsl.utils.settings as fslsettings -import fsl.fsleyes.views as views -import fsl.fsleyes.controls as controls +import fsl.utils.dialog as fsldlg +import fsl.data.strings as strings log = logging.getLogger(__name__) @@ -26,91 +41,357 @@ def getAllPerspectives(): perspectives = perspectives.split(',') perspectives = [p.strip() for p in perspectives] perspectives = [p for p in perspectives if p != ''] - - return perspectives + uniq = [] + for p in perspectives: + if p not in uniq: + uniq.append(p) + + return uniq def loadPerspective(frame, name): + """ + """ log.debug('Loading perspective {}'.format(name)) persp = fslsettings.read('fsleyes.perspectives.{}'.format(name), None) + log.debug('Serialised perspective:\n{}'.format(persp)) + + persp = deserialisePerspective(persp) + + frameChildren, frameLayout, vpChildrens, vpLayouts = persp + + dlg = fsldlg.SimpleMessageDialog( + frame, + strings.messages['perspectives.applyingPerspective'].format(name)) + dlg.Show() + # Clear all existing view + # panels from the frame + for vp in frame.getViewPanels(): + print 'Removing view panel {}'.format(type(vp).__name__) + frame.removeViewPanel(vp) + # Add all of the view panels + # specified in the perspective + for vp in frameChildren: + frame.addViewPanel(vp) + + # Apply the layout to those view panels + frame.getAuiManager().LoadPerspective(frameLayout) + + # For each view panel, add all of the + # control panels, and lay them out + viewPanels = frame.getViewPanels() + for vp, vpChildren, vpLayout in zip(viewPanels, vpChildrens, vpLayouts): + + for child in vpChildren: + + _addControlPanel(vp, child) + vp.getAuiManager().LoadPerspective(vpLayout) + + dlg.Close() + dlg.Destroy() + + def savePerspective(frame, name): + """ + """ log.debug('Saving current perspective with name {}'.format(name)) persp = serialisePerspective(frame) fslsettings.write('fsleyes.perspectives.{}'.format(name), persp) + _addToPerspectivesList(name) + + log.debug('Serialised perspective:\n{}'.format(persp)) + + +def removePerspective(name): + """ + """ + log.debug('Deleting perspective with name {}'.format(name)) + + fslsettings.delete('fsleyes.perspectives.{}'.format(name)) + _removeFromPerspectivesList(name) def serialisePerspective(frame): - log.debug('Serialising current perspective') + """ + """ + + # Written against wx.lib.agw.aui.AuiManager as it + # exists in wxPython 3.0.2.0. + # + # FSLEyes uses a hierarchy of AuiManager instances + # for its layout - the FSLEyesFrame uses an AuiManager + # to lay out ViewPanel instances, and each of these + # ViewPanels use their own AuiManager to lay out + # control panels. The layout for a single AuiManager + # can be serialised to a string via the + # AuiManager.SavePerspective and AuiManager.SavePaneInfo + # methods. + # + # An Aui perspective string consists of: + # - A name. + # + # - A set of key-value set of key-value pairs defining + # the top level panel layout. + # + # - A set of key-value pairs for each pane, + # defining its layout. the AuiManager.SavePaneInfo + # method returns this for a single pane. + # + # These are all encoded in a single string, with + # the above components separated with '|' + # characters, and the pane-level key-value pairs + # separated with a ';' character. For example: + # + # layoutName|key1=value1|name=Pane1;caption=Pane 1|\ + # name=Pane2;caption=Pane 2|doc_size(5,0,0)=22| + # + # The following code is going to query each of the + # AuiManagers, and extract the following: + # + # - A layout string for the FSLEyesFrame + # + # - A string containing a comma-separated list of + # ViewPanels (class names, in the same order as + # they are specified in the frame layout string) + # + # - For each ViewPanel: + # + # - A layout string for the ViewPanel + # - A string containing a comma-separated list + # of ControlPanels (class names, in the same + # order as specified in the ViewPanel layout + # string) + # + # + # We'll start by defining this silly function, which + # takes an ``AuiManager`` layout string, and a list + # of the children which are being managed by the + # AuiManager, and makes sure that the order of the + # child pane layout specifications in the string is + # the same as the order of the children in the list. + def patchLayoutString(auiMgr, panels): + + layoutStr = auiMgr.SavePerspective() + + # The different sections of the string + # returned by SavePerspective are + # separated with a '|' character. + sections = layoutStr.split('|') + sections = [s.strip() for s in sections] + sections = [s for s in sections if s != ''] + + # Here, we identify sections which specify + # the layout of a child pane, remove them, + # and patch them back in, in the order that + # the child panels are specified in the list. + pi = 0 + for si, s in enumerate(sections): + if s.find('name=') > -1: + panel = panels[pi] + panelInfo = auiMgr.GetPane(panel) + panelLayout = auiMgr.SavePaneInfo(panelInfo) + pi += 1 + sections[si] = panelLayout + + # Now the panel layouts in our layout string + # are in the same order as our list of view + # panels - we can re-join the layout string + # sections, and we're done. + return '|'.join(sections) + '|' + # Now we can start extracting the layout information. + # We start with the FSLEyesFrame layout. auiMgr = frame.getAuiManager() viewPanels = frame.getViewPanels() - frameLayout = auiMgr.SavePerspective() + # Generate the frame layout string, and a + # list of the children of the frame + frameLayout = patchLayoutString(auiMgr, viewPanels) + frameChildren = [type(vp).__name__ for vp in viewPanels] + frameChildren = ','.join(frameChildren) + ',' - # The different sections of the string - # returned by SavePerspective are - # separated with a '|' character. - lines = frameLayout.split('|') - lines = [l.strip() for l in lines] - lines = [l for l in lines if l != ''] - - # Even though the layout for each view - # panel is included in the perspective - # string, we are going to remove them, - # and patch them back in, in the loop - # below. This is so we can match view - # panel layouts with their child control - # panel layouts, and be sure that each - # view panel is paired to the correct - # set of control panels. - lines = [l for l in lines if l.find('name=') == -1] + # We are going to build a list of layout strings, + # one for each ViewPanel, and a corresponding list + # of control panels displayed on each ViewPanel. + vpLayouts = [] + vpChildrens = [] for vp in viewPanels: - # Get the layout for this view panel - # (which we just removed, above) - vpLayout = auiMgr.SavePaneInfo(frame.getViewPanelInfo(vp)) + # Get the auiManager and layout for this view panel. + # This is a little bit complicated, as ViewPanels + # differentiate between the main 'centre' panel, and + # all other secondary (control) panels. The layout + # string needs to contain layout information for + # all of these panels, but we only care about the + # control panels. + vpAuiMgr = vp.getAuiManager() + ctrlPanels = vp.getPanels() + centrePanel = vp.getCentrePanel() - # Each ViewPanel is itself managed by - # an AuiManager, which manages the layout - # of the control panels that have been - # added to the ViewPanel. Here, we get - # the layout for this view panel. - vpAuiMgr = vp.getAuiManager() - vpInnerLayout = vpAuiMgr.SavePerspective() + # The process is now identical to that used + # for the frame layout and children, above. + vpLayout = patchLayoutString(vpAuiMgr, [centrePanel] + ctrlPanels) + vpChildren = [type(cp).__name__ for cp in ctrlPanels] + vpChildren = ','.join(vpChildren) + ',' - # After the Frame-level layout for a view - # panel, we add in the ViewPanel-level - # layout for the control panels within - # that view panel. - lines.append(vpLayout) - lines.append(vpInnerLayout) + vpLayouts .append(vpLayout) + vpChildrens.append(vpChildren) - # Both the frame-level, and the viewpanel-level - # layouts use '|' characters to separate their - # sections. To avoid confusing the two, we're - # replacing the pipes in the frame-level layout - # with newlines. - layout = '\n'.join(lines) + # We serialise all of these pieces of information + # as a single newline-separated string. + perspective = [frameChildren, frameLayout] + for vpChildren, vpLayout in zip(vpChildrens, vpLayouts): + perspective.append(vpChildren) + perspective.append(vpLayout) - return layout + # And we're done! + return '\n'.join(perspective) def deserialisePerspective(persp): """ """ - # Returns: - # - Layout string for Frame - # - List of ViewPanels - # - # - For each ViewPanel: - # - Layout string for ViewPanel - # - List of ControlPanels + import fsl.fsleyes.views as views + import fsl.fsleyes.controls as controls + + # This function deserialises a string which was + # generated by the serialisePerspective function. + # It returns: + # + # - A list of ViewPanel class types - the + # children of the FSLEyesFrame. + # + # - A layout string for the FSLEyesFrame. + # + # - A list of lists, each inner list containing + # a collection of ControlPanel class types - + # the children of the corresponding ViewPanel. + # + # - A list of layout strings, one for each + # ViewPanel. + lines = persp.split('\n') + lines = [l.strip() for l in lines] + lines = [l for l in lines if l != ''] + + frameChildren = lines[0] + frameLayout = lines[1] + + # The children strings are comma-separated + # class names. The frame children are ViewPanels, + # which are all defined in the fsl.fsleyes.views + # package. + frameChildren = frameChildren.split(',') + frameChildren = [fc.strip() for fc in frameChildren] + frameChildren = [fc for fc in frameChildren if fc != ''] + frameChildren = [getattr(views, fc) for fc in frameChildren] + + # Collate the children/layouts for each view panel + vpChildren = [] + vpLayouts = [] + for i in range(2, len(frameChildren) + 2, 2): + vpChildren.append(lines[i]) + vpLayouts .append(lines[i + 1]) + + # And the ViewPanel children are control panels, + # all defined in the fsl.fsleyes.controls package. + for i in range(len(vpChildren)): + + vpChildren[i] = vpChildren[i].split(',') + vpChildren[i] = [vpc.strip() for vpc in vpChildren[i]] + vpChildren[i] = [vpc for vpc in vpChildren[i] if vpc != ''] + vpChildren[i] = [getattr(controls, vpc) for vpc in vpChildren[i]] + + return frameChildren, frameLayout, vpChildren, vpLayouts + + +def _addToPerspectivesList(persp): + """ + """ + perspectives = getAllPerspectives() + + if persp not in perspectives: + perspectives.append(persp) + + perspectives = ','.join(perspectives) + + log.debug('Updating stored perspective list: {}'.format(perspectives)) + fslsettings.write('fsleyes.perspectives', perspectives) + + +def _removeFromPerspectivesList(persp): + """ + """ + + perspectives = getAllPerspectives() + + try: perspectives.remove(persp) + except ValueError: return + + perspectives = ','.join(perspectives) + + log.debug('Updating stored perspective list: {}'.format(perspectives)) + fslsettings.write('fsleyes.perspectives', perspectives) + + +def _addControlPanel(viewPanel, panelType): + """ + """ + import fsl.fsleyes.controls as controls + + args = { + controls.CanvasSettingsPanel : {'canvasPanel' : viewPanel}, + controls.HistogramControlPanel : {'plotPanel' : viewPanel}, + controls.LightBoxToolBar : {'lb' : viewPanel}, + controls.OrthoEditToolBar : {'ortho' : viewPanel}, + controls.OrthoToolBar : {'ortho' : viewPanel}, + controls.OverlayDisplayToolBar : {'viewPanel' : viewPanel}, + controls.PlotListPanel : {'plotPanel' : viewPanel}, + controls.PowerSpectrumControlPanel : {'plotPanel' : viewPanel}, + controls.ShellPanel : {'canvasPanel' : viewPanel}, + controls.TimeSeriesControlPanel : {'plotPanel' : viewPanel}, + } + + args = args.get(panelType, {}) + + viewPanel.togglePanel(panelType, **args) + + +BUILT_IN_PERSPECTIVES = collections.OrderedDict(( + ('default', + textwrap.dedent(""" + OrthoPanel, + layout2|name=OrthoPanel;caption=Ortho View 1;state=67376064;dir=5;layer=0;row=0;pos=0;prop=100000;bestw=20;besth=20;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|dock_size(5,0,0)=22| + LocationPanel,OverlayListPanel,OverlayDisplayToolBar,OrthoToolBar, + layout2|name=Panel;caption=;state=768;dir=5;layer=0;row=0;pos=0;prop=100000;bestw=20;besth=20;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|name=LocationPanel;caption=Location;state=67373052;dir=3;layer=0;row=0;pos=1;prop=100000;bestw=440;besth=109;minw=440;minh=109;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=440;floath=125;notebookid=-1;transparent=255|name=OverlayListPanel;caption=Overlay list;state=67373052;dir=3;layer=0;row=0;pos=0;prop=100000;bestw=197;besth=80;minw=197;minh=80;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=197;floath=96;notebookid=-1;transparent=255|name=OverlayDisplayToolBar;caption=Display toolbar;state=67382012;dir=1;layer=11;row=0;pos=0;prop=100000;bestw=860;besth=49;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|name=OrthoToolBar;caption=Ortho view toolbar;state=67382012;dir=1;layer=10;row=0;pos=0;prop=100000;bestw=755;besth=34;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|dock_size(5,0,0)=22|dock_size(3,0,0)=130|dock_size(1,10,0)=36|dock_size(1,11,0)=51| + """)), + + ('melodic', + textwrap.dedent(""" + LightBoxPanel,TimeSeriesPanel,PowerSpectrumPanel, + layout2|name=LightBoxPanel;caption=Lightbox View 1;state=67377088;dir=5;layer=0;row=0;pos=0;prop=100000;bestw=853;besth=-1;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|name=TimeSeriesPanel;caption=Time series 2;state=67377148;dir=3;layer=0;row=0;pos=0;prop=100000;bestw=-1;besth=472;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|name=PowerSpectrumPanel;caption=Power spectra 3;state=67377148;dir=3;layer=0;row=0;pos=1;prop=100000;bestw=-1;besth=472;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|dock_size(5,0,0)=22|dock_size(3,0,0)=493| + OverlayListPanel,LightBoxToolBar,OverlayDisplayToolBar,LocationPanel,MelodicClassificationPanel,LookupTablePanel, + layout2|name=Panel;caption=;state=768;dir=5;layer=0;row=0;pos=0;prop=100000;bestw=20;besth=20;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|name=OverlayListPanel;caption=Overlay list;state=67373052;dir=3;layer=0;row=0;pos=0;prop=100000;bestw=197;besth=80;minw=197;minh=80;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=197;floath=96;notebookid=-1;transparent=255|name=LightBoxToolBar;caption=Lightbox view toolbar;state=67382012;dir=1;layer=10;row=0;pos=0;prop=100000;bestw=757;besth=43;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|name=OverlayDisplayToolBar;caption=Display toolbar;state=67382012;dir=1;layer=11;row=0;pos=0;prop=100000;bestw=860;besth=49;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|name=LocationPanel;caption=Location;state=67373052;dir=3;layer=0;row=0;pos=1;prop=100000;bestw=440;besth=109;minw=440;minh=109;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=440;floath=125;notebookid=-1;transparent=255|name=MelodicClassificationPanel;caption=Melodic IC classification;state=67373052;dir=2;layer=0;row=0;pos=1;prop=100000;bestw=400;besth=100;minw=400;minh=100;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=400;floath=116;notebookid=-1;transparent=255|name=LookupTablePanel;caption=Lookup tables;state=67373052;dir=2;layer=0;row=0;pos=0;prop=100000;bestw=358;besth=140;minw=358;minh=140;maxw=-1;maxh=-1;floatx=3614;floaty=658;floatw=358;floath=156;notebookid=-1;transparent=255|dock_size(5,0,0)=22|dock_size(3,0,0)=130|dock_size(1,10,0)=45|dock_size(1,11,0)=10|dock_size(2,0,0)=402| + , + layout2|name=FigureCanvasWxAgg;caption=;state=768;dir=5;layer=0;row=0;pos=0;prop=100000;bestw=640;besth=480;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|dock_size(5,0,0)=642| + , + layout2|name=FigureCanvasWxAgg;caption=;state=768;dir=5;layer=0;row=0;pos=0;prop=100000;bestw=640;besth=480;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|dock_size(5,0,0)=642| + """)), + + ('feat', + textwrap.dedent(""" + OrthoPanel,TimeSeriesPanel, + layout2|name=OrthoPanel;caption=Ortho View 1;state=67377088;dir=5;layer=0;row=0;pos=0;prop=100000;bestw=853;besth=-1;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|name=TimeSeriesPanel;caption=Time series 2;state=67377148;dir=3;layer=0;row=0;pos=0;prop=100000;bestw=-1;besth=472;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|dock_size(5,0,0)=22|dock_size(3,0,0)=261| + LocationPanel,ClusterPanel,OverlayListPanel,AtlasPanel, + layout2|name=Panel;caption=;state=768;dir=5;layer=0;row=0;pos=0;prop=100000;bestw=20;besth=20;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|name=LocationPanel;caption=Location;state=67373052;dir=2;layer=1;row=0;pos=1;prop=98544;bestw=440;besth=109;minw=440;minh=109;maxw=-1;maxh=-1;floatx=1051;floaty=349;floatw=440;floath=125;notebookid=-1;transparent=255|name=ClusterPanel;caption=Cluster browser;state=67373052;dir=3;layer=0;row=0;pos=0;prop=100000;bestw=390;besth=96;minw=390;minh=96;maxw=-1;maxh=-1;floatx=276;floaty=578;floatw=390;floath=112;notebookid=-1;transparent=255|name=OverlayListPanel;caption=Overlay list;state=67373052;dir=2;layer=1;row=0;pos=2;prop=87792;bestw=197;besth=80;minw=197;minh=80;maxw=-1;maxh=-1;floatx=1047;floaty=492;floatw=197;floath=96;notebookid=-1;transparent=255|name=AtlasPanel;caption=Atlases;state=67373052;dir=2;layer=1;row=0;pos=0;prop=113664;bestw=318;besth=84;minw=318;minh=84;maxw=-1;maxh=-1;floatx=1091;floaty=143;floatw=318;floath=100;notebookid=-1;transparent=255|dock_size(5,0,0)=10|dock_size(3,0,0)=130|dock_size(2,1,0)=566| + , + layout2|name=FigureCanvasWxAgg;caption=;state=768;dir=5;layer=0;row=0;pos=0;prop=100000;bestw=640;besth=480;minw=-1;minh=-1;maxw=-1;maxh=-1;floatx=-1;floaty=-1;floatw=-1;floath=-1;notebookid=-1;transparent=255|dock_size(5,0,0)=10| + """)))) diff --git a/fsl/utils/settings.py b/fsl/utils/settings.py index 502af28076cfcf02cd8cadbe9350f6c248c2fdda..e3b759d47330f2dcf7b371fd0dde01ca5a53c84b 100644 --- a/fsl/utils/settings.py +++ b/fsl/utils/settings.py @@ -4,8 +4,8 @@ # # Author: Paul McCarthy <pauldmccarthy@gmail.com> # -"""This module provides a simple API to :func:`read` and :func:`write` -persistent application settings. +"""This module provides a simple API to :func:`read`, :func:`write`, and +:func:`delete` persistent application settings. .. note:: Currently the configuration management API provided by :mod:`wx` (http://docs.wxwidgets.org/trunk/overview_config.html) is used for @@ -69,3 +69,18 @@ def write(name, value): log.debug('Writing {}: {}'.format(name, value)) config.Write(name, value) + + +def delete(name): + """Delete the setting with the given ``name``. """ + try: import wx + except: return + + if wx.GetApp() is None: + return + + config = wx.Config(_CONFIG_ID) + + log.debug('Deleting {}'.format(name)) + + config.DeleteEntry(name)