Skip to content
Snippets Groups Projects
Commit 3e40cce8 authored by Paul McCarthy's avatar Paul McCarthy
Browse files

Action/ToggleAction classes moved into their own module. The

actions.__init__ module contains aliases for all the global Action
classes. FSLEyesFrame no longer stores a dict of {AuiPaneInfo :
ViewPanel} mappings, because AuiPaneInfo objects are evidently not
persistent, so I cannot use them as dict keys. New action -
ClearPerspectiveAction, which delets all user-saved
perspectives. SavePerspectiveAction is functional.  Perspectives module
is functional, but needs more work w.r.t. managing built-in
perspectives.
parent eeac7590
No related branches found
No related tags found
No related merge requests found
...@@ -55,6 +55,10 @@ messages = TypeDict({ ...@@ -55,6 +55,10 @@ messages = TypeDict({
'perspectives.savePerspective' : 'Enter a name for the perspective', 'perspectives.savePerspective' : 'Enter a name for the perspective',
'perspectives.applyingPerspective' : 'Applying {} 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.loading' : 'Loading {} ...',
'overlay.loadOverlays.error' : 'An error occurred loading the image ' 'overlay.loadOverlays.error' : 'An error occurred loading the image '
'{}\n\nDetails: {}', '{}\n\nDetails: {}',
...@@ -193,17 +197,21 @@ titles = TypeDict({ ...@@ -193,17 +197,21 @@ titles = TypeDict({
'MelodicClassificationPanel.saveDialog' : 'Save FIX/Melview file...', 'MelodicClassificationPanel.saveDialog' : 'Save FIX/Melview file...',
'MelodicClassificationPanel.loadError' : 'Error loading FIX/Melview file', 'MelodicClassificationPanel.loadError' : 'Error loading FIX/Melview file',
'MelodicClassificationPanel.saveError' : 'Error saving FIX/Melview file', 'MelodicClassificationPanel.saveError' : 'Error saving FIX/Melview file',
'ClearPerspectiveAction.confirmClear' : 'Clear all perspectives?',
}) })
actions = TypeDict({ actions = TypeDict({
'OpenFileAction' : 'Add overlay file', 'OpenFileAction' : 'Add overlay file',
'OpenStandardAction' : 'Add standard', 'OpenStandardAction' : 'Add standard',
'CopyOverlayAction' : 'Copy overlay', 'CopyOverlayAction' : 'Copy overlay',
'SaveOverlayAction' : 'Save overlay', 'SaveOverlayAction' : 'Save overlay',
'LoadColourMapAction' : 'Load custom colour map', 'LoadColourMapAction' : 'Load custom colour map',
'SavePerspectiveAction' : 'Save current perspective', 'SavePerspectiveAction' : 'Save current perspective',
'ClearPerspectiveAction' : 'Clear all perspectives',
'FSLEyesFrame.closeViewPanel' : 'Close', 'FSLEyesFrame.closeViewPanel' : 'Close',
...@@ -785,6 +793,7 @@ melodic = TypeDict({ ...@@ -785,6 +793,7 @@ melodic = TypeDict({
}) })
perspectives = { perspectives = {
'melview' : 'Melodic mode', 'default' : 'Default layout',
'melodic' : 'Melodic mode',
'feat' : 'FEAT mode', 'feat' : 'FEAT mode',
} }
...@@ -4,12 +4,12 @@ ...@@ -4,12 +4,12 @@
# #
# Author: Paul McCarthy <pauldmccarthy@gmail.com> # Author: Paul McCarthy <pauldmccarthy@gmail.com>
# #
"""This package provides a collection of actions, two package-level """This package provides a collection of actions, classes - the
classes - the :class:`Action` class and the :class:`ActionProvider` class, :class:`.Action` class and the :class:`.ActionProvider` class, and the
and the :func:`action` and :func:`toggleAction` decorators. :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 performed, enabled and disabled, and may be bound to a GUI menu item or
button. The :class:`ActionProvider` class represents some entity which can button. The :class:`ActionProvider` class represents some entity which can
perform one or more actions. As the :class:`.FSLEyesPanel` class derives from perform one or more actions. As the :class:`.FSLEyesPanel` class derives from
...@@ -58,16 +58,16 @@ and :meth:`ActionProvider.disableAction` methods:: ...@@ -58,16 +58,16 @@ and :meth:`ActionProvider.disableAction` methods::
It is useful to know that each method on the ``t`` instance has actually been 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 replaced with an :class:`.Action` instance, which encapsulates the method.
this knowledge, you can access the ``Action`` instances directly:: Using this knowledge, you can access the ``Action`` instances directly::
>>> t.doFirstThing.enabled = True >>> t.doFirstThing.enabled = True
>>> t.doFirstThing() >>> t.doFirstThing()
First thing done First thing done
The :meth:`Action.bindToWidget` method allows a widget to be bound to an The :meth:`.Action.bindToWidget` method allows a widget to be bound to an
:class:`Action`. For example:: :class:`.Action`. For example::
# We're assuming here that a wx.App, and # We're assuming here that a wx.App, and
# a parent window, has been created # a parent window, has been created
...@@ -76,8 +76,8 @@ The :meth:`Action.bindToWidget` method allows a widget to be bound to an ...@@ -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 All bound widgets of an ``Action`` can be accessed through the
:meth:`Action.getBoundWidgets` method, and can be unbound via the :meth:`.Action.getBoundWidgets` method, and can be unbound via the
:meth:`Action.unbindAllWidgets` method. :meth:`.Action.unbindAllWidgets` method.
This module also provides two classes which allow a widget to be automatically 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: ...@@ -100,6 +100,9 @@ Finally, some 'global' actions are also provided in this package:
~fsl.fsleyes.actions.openfile ~fsl.fsleyes.actions.openfile
~fsl.fsleyes.actions.openstandard ~fsl.fsleyes.actions.openstandard
~fsl.fsleyes.actions.saveoverlay ~fsl.fsleyes.actions.saveoverlay
~fsl.fsleyes.actions.saveperspective
~fsl.fsleyes.actions.loadperspective
~fsl.fsleyes.actions.clearperspective
""" """
...@@ -111,6 +114,28 @@ import props ...@@ -111,6 +114,28 @@ import props
import fsl.data.strings as strings 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__) log = logging.getLogger(__name__)
...@@ -125,194 +150,6 @@ def toggleAction(func): ...@@ -125,194 +150,6 @@ def toggleAction(func):
return ActionFactory(func, ToggleAction) 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): class ActionProvider(object):
"""The ``ActionProvider`` class is intended to be used as a base class for """The ``ActionProvider`` class is intended to be used as a base class for
classes which contain actions. The :func:`action` and :func:`toggleAction` classes which contain actions. The :func:`action` and :func:`toggleAction`
...@@ -360,13 +197,6 @@ class ActionProvider(object): ...@@ -360,13 +197,6 @@ class ActionProvider(object):
return acts return acts
class ActionDisabledError(Exception):
"""Exception raised when an attempt is made to call a disabled
:class:`Action`.
"""
pass
class ActionFactory(object): class ActionFactory(object):
"""The ``ActionFactory`` is used by the :func:`action` and """The ``ActionFactory`` is used by the :func:`action` and
:func:`toggleAction` decorators. Its job is to create :class:`Action` :func:`toggleAction` decorators. Its job is to create :class:`Action`
......
#!/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)
#!/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()
...@@ -9,13 +9,13 @@ which creates a copy of the currently selected overlay. ...@@ -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 action
import fsl.data.image as fslimage import fsl.data.image as fslimage
class CopyOverlayAction(actions.Action): class CopyOverlayAction(action.Action):
"""The ``CopyOverlayAction`` does as its name suggests - it creates a """The ``CopyOverlayAction`` does as its name suggests - it creates a
copy of the currently selected overlay. copy of the currently selected overlay.
""" """
...@@ -27,7 +27,7 @@ class CopyOverlayAction(actions.Action): ...@@ -27,7 +27,7 @@ class CopyOverlayAction(actions.Action):
:arg overlayList: The :class:`.OverlayList`. :arg overlayList: The :class:`.OverlayList`.
:arg displayCtx: The :class:`.DisplayContext`. :arg displayCtx: The :class:`.DisplayContext`.
""" """
actions.Action.__init__(self, self.__copyOverlay) action.Action.__init__(self, self.__copyOverlay)
self.__overlayList = overlayList self.__overlayList = overlayList
self.__displayCtx = displayCtx self.__displayCtx = displayCtx
...@@ -50,7 +50,7 @@ class CopyOverlayAction(actions.Action): ...@@ -50,7 +50,7 @@ class CopyOverlayAction(actions.Action):
self.__displayCtx .removeListener('selectedOverlay', self.__name) self.__displayCtx .removeListener('selectedOverlay', self.__name)
self.__overlayList.removeListener('overlays', self.__name) self.__overlayList.removeListener('overlays', self.__name)
actions.Action.destroy(self) action.Action.destroy(self)
def __selectedOverlayChanged(self, *a): def __selectedOverlayChanged(self, *a):
......
...@@ -12,7 +12,7 @@ import logging ...@@ -12,7 +12,7 @@ import logging
import os.path as op import os.path as op
import fsl.data.strings as strings import fsl.data.strings as strings
import fsl.fsleyes.actions as actions import action
import fsl.fsleyes.colourmaps as fslcmap import fsl.fsleyes.colourmaps as fslcmap
...@@ -22,7 +22,7 @@ log = logging.getLogger(__name__) ...@@ -22,7 +22,7 @@ log = logging.getLogger(__name__)
_stringID = 'actions.loadcolourmap.' _stringID = 'actions.loadcolourmap.'
class LoadColourMapAction(actions.Action): class LoadColourMapAction(action.Action):
"""The ``LoadColourMapAction`` allows the user to select a colour """The ``LoadColourMapAction`` allows the user to select a colour
map file and give it a name. map file and give it a name.
...@@ -38,7 +38,7 @@ class LoadColourMapAction(actions.Action): ...@@ -38,7 +38,7 @@ class LoadColourMapAction(actions.Action):
:arg overlayList: The :class:`.OverlayList`. :arg overlayList: The :class:`.OverlayList`.
:arg displayCtx: The :class:`.DisplayContext`. :arg displayCtx: The :class:`.DisplayContext`.
""" """
actions.Action.__init__(self, self.__loadColourMap) action.Action.__init__(self, self.__loadColourMap)
self.__overlayList = overlayList self.__overlayList = overlayList
self.__displayCtx = displayCtx self.__displayCtx = displayCtx
......
...@@ -8,11 +8,11 @@ ...@@ -8,11 +8,11 @@
""" """
import fsl.fsleyes.actions as actions import action
import fsl.fsleyes.perspectives as perspectives import fsl.fsleyes.perspectives as perspectives
class LoadPerspectiveAction(actions.Action): class LoadPerspectiveAction(action.Action):
""" """
""" """
...@@ -23,7 +23,7 @@ class LoadPerspectiveAction(actions.Action): ...@@ -23,7 +23,7 @@ class LoadPerspectiveAction(actions.Action):
self.__frame = frame self.__frame = frame
self.__perspective = perspective self.__perspective = perspective
actions.Action.__init__(self, self.__loadPerspective) action.Action.__init__(self, self.__loadPerspective)
def __loadPerspective(self): def __loadPerspective(self):
......
...@@ -9,10 +9,10 @@ load overlay files into the :class:`.OverlayList`. ...@@ -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 """The ``OpenFileAction`` allows the user to add files to the
:class:`.OverlayList`. This functionality is provided by the :class:`.OverlayList`. This functionality is provided by the
:meth:`.OverlayList.addOverlays` method. :meth:`.OverlayList.addOverlays` method.
...@@ -24,7 +24,7 @@ class OpenFileAction(actions.Action): ...@@ -24,7 +24,7 @@ class OpenFileAction(actions.Action):
:arg overlayList: The :class:`.OverlayList`. :arg overlayList: The :class:`.OverlayList`.
:arg displayCtx: The :class:`.DisplayContext`. :arg displayCtx: The :class:`.DisplayContext`.
""" """
actions.Action.__init__(self, self.__openFile) action.Action.__init__(self, self.__openFile)
self.__overlayList = overlayList self.__overlayList = overlayList
self.__displayCtx = displayCtx self.__displayCtx = displayCtx
......
...@@ -13,10 +13,10 @@ to load in standard space images from the ``$FSLDIR/data/standard/`` directory. ...@@ -13,10 +13,10 @@ to load in standard space images from the ``$FSLDIR/data/standard/`` directory.
import os import os
import os.path as op 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 """The ``OpenStandardAction`` prompts the user to open one or more
overlays, using ``$FSLDIR/data/standard/`` as the default directory. overlays, using ``$FSLDIR/data/standard/`` as the default directory.
""" """
...@@ -28,7 +28,7 @@ class OpenStandardAction(actions.Action): ...@@ -28,7 +28,7 @@ class OpenStandardAction(actions.Action):
:arg overlayList: The :class:`.OverlayList`. :arg overlayList: The :class:`.OverlayList`.
:arg displayCtx: The :class:`.DisplayContext`. :arg displayCtx: The :class:`.DisplayContext`.
""" """
actions.Action.__init__(self, self.__openStandard) action.Action.__init__(self, self.__openStandard)
self.__overlayList = overlayList self.__overlayList = overlayList
self.__displayCtx = displayCtx self.__displayCtx = displayCtx
......
...@@ -9,11 +9,11 @@ to save the currently selected overlay. ...@@ -9,11 +9,11 @@ to save the currently selected overlay.
""" """
import fsl.data.image as fslimage import fsl.data.image as fslimage
import fsl.fsleyes.actions as actions import action
class SaveOverlayAction(actions.Action): class SaveOverlayAction(action.Action):
"""The ``SaveOverlayAction`` allows the user to save the currently """The ``SaveOverlayAction`` allows the user to save the currently
selected overlay, if it has been edited, or only exists in memory. selected overlay, if it has been edited, or only exists in memory.
""" """
...@@ -25,7 +25,7 @@ class SaveOverlayAction(actions.Action): ...@@ -25,7 +25,7 @@ class SaveOverlayAction(actions.Action):
:arg overlayList: The :class:`.OverlayList`. :arg overlayList: The :class:`.OverlayList`.
:arg displayCtx: The :class:`.DisplayContext`. :arg displayCtx: The :class:`.DisplayContext`.
""" """
actions.Action.__init__(self, self.__saveOverlay) action.Action.__init__(self, self.__saveOverlay)
self.__overlayList = overlayList self.__overlayList = overlayList
self.__displayCtx = displayCtx self.__displayCtx = displayCtx
...@@ -48,7 +48,7 @@ class SaveOverlayAction(actions.Action): ...@@ -48,7 +48,7 @@ class SaveOverlayAction(actions.Action):
self.__displayCtx .removeListener('selectedOverlay', self.__name) self.__displayCtx .removeListener('selectedOverlay', self.__name)
self.__overlayList.removeListener('overlays', self.__name) self.__overlayList.removeListener('overlays', self.__name)
actions.Action.destroy(self) action.Action.destroy(self)
def __selectedOverlayChanged(self, *a): def __selectedOverlayChanged(self, *a):
......
#!/usr/bin/env python #!/usr/bin/env python
# #
# saveperspective.py - # saveperspective.py - The SavePerspectiveAction
# #
# Author: Paul McCarthy <pauldmccarthy@gmail.com> # 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 wx
import fsl.data.strings as strings import fsl.data.strings as strings
import fsl.fsleyes.actions as actions import action
import fsl.fsleyes.perspectives as perspectives 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): def __init__(self, frame):
""" """Create a ``SavePerspectiveAction``.
:arg frame: The :class:`.FSLEyesFrame`.
""" """
self.__frame = frame self.__frame = frame
actions.Action.__init__(self, self.__savePerspective) action.Action.__init__(self, self.__savePerspective)
def __savePerspective(self): 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( dlg = wx.TextEntryDialog(
...@@ -43,4 +50,10 @@ class SavePerspectiveAction(actions.Action): ...@@ -43,4 +50,10 @@ class SavePerspectiveAction(actions.Action):
if name.strip() == '': if name.strip() == '':
return return
# TODO Prevent using built-in perspective names
# TODO Name collision - confirm overwrite
perspectives.savePerspective(self.__frame, name) perspectives.savePerspective(self.__frame, name)
self.__frame.refreshPerspectiveMenu()
...@@ -10,7 +10,6 @@ for FSLeyes. ...@@ -10,7 +10,6 @@ for FSLeyes.
import logging import logging
import collections
import wx import wx
import wx.lib.agw.aui as aui import wx.lib.agw.aui as aui
...@@ -20,12 +19,6 @@ import fsl.utils.settings as fslsettings ...@@ -20,12 +19,6 @@ import fsl.utils.settings as fslsettings
import views import views
import actions 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 perspectives
import displaycontext import displaycontext
...@@ -84,6 +77,7 @@ class FSLEyesFrame(wx.Frame): ...@@ -84,6 +77,7 @@ class FSLEyesFrame(wx.Frame):
addViewPanel addViewPanel
removeViewPanel removeViewPanel
getAuiManager getAuiManager
refreshPerspectiveMenu
""" """
...@@ -129,17 +123,20 @@ class FSLEyesFrame(wx.Frame): ...@@ -129,17 +123,20 @@ class FSLEyesFrame(wx.Frame):
# Keeping track of all open view panels # Keeping track of all open view panels
# #
# The __viewPanels dict contains # The __viewPanels list contains all
# {AuiPaneInfo : ViewPanel} mappings # [ViewPanel] instances
# #
# The other dicts contain # The other dicts contain
# {ViewPanel : something} mappings # {ViewPanel : something} mappings
# #
self.__viewPanels = collections.OrderedDict() self.__viewPanels = []
self.__viewPanelDCs = {} self.__viewPanelDCs = {}
self.__viewPanelMenus = {} self.__viewPanelMenus = {}
self.__viewPanelIDs = {} self.__viewPanelIDs = {}
self.__menuBar = None
self.__perspMenu = None
self.__makeMenuBar() self.__makeMenuBar()
self.__restoreState(restore) self.__restoreState(restore)
...@@ -151,7 +148,7 @@ class FSLEyesFrame(wx.Frame): ...@@ -151,7 +148,7 @@ class FSLEyesFrame(wx.Frame):
"""Returns a list of all :class:`.ViewPanel` instances that are """Returns a list of all :class:`.ViewPanel` instances that are
currenlty displayed in this ``FSLEyesFrame``. currenlty displayed in this ``FSLEyesFrame``.
""" """
return list(self.__viewPanels.values()) return list(self.__viewPanels)
def getViewPanelInfo(self, viewPanel): def getViewPanelInfo(self, viewPanel):
...@@ -171,8 +168,11 @@ class FSLEyesFrame(wx.Frame): ...@@ -171,8 +168,11 @@ class FSLEyesFrame(wx.Frame):
def removeViewPanel(self, viewPanel): def removeViewPanel(self, viewPanel):
"""Removes the given :class:`.ViewPanel` from this ``FSLEyesFrame``. """Removes the given :class:`.ViewPanel` from this ``FSLEyesFrame``.
""" """
paneInfo = self.__auiManager.GetPane(viewPanel) paneInfo = self.__auiManager.GetPane(viewPanel)
self.__onViewPanelClose( paneInfo=paneInfo)
self.__onViewPanelClose(panel=viewPanel)
self.__auiManager.ClosePane(paneInfo) self.__auiManager.ClosePane(paneInfo)
self.__auiManager.Update() self.__auiManager.Update()
...@@ -244,7 +244,8 @@ class FSLEyesFrame(wx.Frame): ...@@ -244,7 +244,8 @@ class FSLEyesFrame(wx.Frame):
# first key is the AuiPaneInfo of # first key is the AuiPaneInfo of
# the first panel that was added. # the first panel that was added.
else: else:
self.__viewPanels.keys()[0].CaptionVisible(True) self.__auiManager.GetPane(self.__viewPanels[0])\
.CaptionVisible(True)
# If this is not the first view panel, # If this is not the first view panel,
# give it a sensible initial size. # give it a sensible initial size.
...@@ -261,9 +262,9 @@ class FSLEyesFrame(wx.Frame): ...@@ -261,9 +262,9 @@ class FSLEyesFrame(wx.Frame):
else: else:
paneInfo.Right().BestSize(width / 3, -1) paneInfo.Right().BestSize(width / 3, -1)
self.__viewPanels[ paneInfo] = panel self.__viewPanels.append(panel)
self.__viewPanelDCs[panel] = childDC self.__viewPanelDCs[ panel] = childDC
self.__viewPanelIDs[panel] = panelId self.__viewPanelIDs[ panel] = panelId
self.__auiManager.AddPane(panel, paneInfo) self.__auiManager.AddPane(panel, paneInfo)
self.__addViewPanelMenu( panel, title) self.__addViewPanelMenu( panel, title)
...@@ -273,6 +274,11 @@ class FSLEyesFrame(wx.Frame): ...@@ -273,6 +274,11 @@ class FSLEyesFrame(wx.Frame):
self.Thaw() self.Thaw()
def refreshPerspectiveMenu(self):
"""Re-creates the *View -> Perspectives* sub-menu. """
self.__makePerspectiveMenu()
def __addViewPanelMenu(self, panel, title): def __addViewPanelMenu(self, panel, title):
"""Called by :meth:`addViewPanel`. Adds a menu item for the newly """Called by :meth:`addViewPanel`. Adds a menu item for the newly
created :class:`.ViewPanel` instance. created :class:`.ViewPanel` instance.
...@@ -341,7 +347,7 @@ class FSLEyesFrame(wx.Frame): ...@@ -341,7 +347,7 @@ class FSLEyesFrame(wx.Frame):
self.Bind(wx.EVT_MENU, closeViewPanel, closeItem) 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`. """Called when the user closes a :class:`.ViewPanel`.
The :meth:`__addViewPanelMenu` method adds a *Close* menu item The :meth:`__addViewPanelMenu` method adds a *Close* menu item
...@@ -357,16 +363,26 @@ class FSLEyesFrame(wx.Frame): ...@@ -357,16 +363,26 @@ class FSLEyesFrame(wx.Frame):
if ev is not None: if ev is not None:
ev.Skip() 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() paneInfo = ev.GetPane()
panel = paneInfo.window
panel = self .__viewPanels.pop(paneInfo, None)
elif panel is not None:
paneInfo = self.__auiManager.GetPane(panel)
if panel is None: if panel is None:
return return
self .__viewPanelIDs .pop(panel) self .__viewPanels .remove(panel)
dctx = self.__viewPanelDCs .pop(panel) self .__viewPanelIDs .pop( panel)
menu = self.__viewPanelMenus.pop(panel, None) dctx = self.__viewPanelDCs .pop( panel)
menu = self.__viewPanelMenus.pop( panel, None)
log.debug('Destroying {} ({}) and ' log.debug('Destroying {} ({}) and '
'associated DisplayContext ({})'.format( 'associated DisplayContext ({})'.format(
...@@ -392,7 +408,7 @@ class FSLEyesFrame(wx.Frame): ...@@ -392,7 +408,7 @@ class FSLEyesFrame(wx.Frame):
wasCentre = paneInfo.dock_direction_get() == aui.AUI_DOCK_CENTRE wasCentre = paneInfo.dock_direction_get() == aui.AUI_DOCK_CENTRE
if numPanels >= 1 and wasCentre: if numPanels >= 1 and wasCentre:
paneInfo = self.__viewPanels.keys()[0] paneInfo = self.__auiManager.GetPane(self.__viewPanels[0])
paneInfo.Centre().Dockable(False).CaptionVisible(numPanels > 1) paneInfo.Centre().Dockable(False).CaptionVisible(numPanels > 1)
...@@ -414,7 +430,7 @@ class FSLEyesFrame(wx.Frame): ...@@ -414,7 +430,7 @@ class FSLEyesFrame(wx.Frame):
# It's nice to explicitly clean # It's nice to explicitly clean
# up our FSLEyesPanels, otherwise # up our FSLEyesPanels, otherwise
# they'll probably complain # they'll probably complain
for panel in self.__viewPanels.values(): for panel in self.__viewPanels:
panel.destroy() panel.destroy()
...@@ -566,7 +582,6 @@ class FSLEyesFrame(wx.Frame): ...@@ -566,7 +582,6 @@ class FSLEyesFrame(wx.Frame):
if restore: if restore:
self.addViewPanel(views.OrthoPanel) self.addViewPanel(views.OrthoPanel)
def __makeMenuBar(self): def __makeMenuBar(self):
"""Constructs a bunch of menu items for this ``FSLEyesFrame``.""" """Constructs a bunch of menu items for this ``FSLEyesFrame``."""
...@@ -578,6 +593,9 @@ class FSLEyesFrame(wx.Frame): ...@@ -578,6 +593,9 @@ class FSLEyesFrame(wx.Frame):
viewMenu = wx.Menu() viewMenu = wx.Menu()
perspectiveMenu = wx.Menu() perspectiveMenu = wx.Menu()
settingsMenu = wx.Menu() settingsMenu = wx.Menu()
self.__menuBar = menuBar
self.__perspMenu = perspectiveMenu
menuBar.Append(fileMenu, 'File') menuBar.Append(fileMenu, 'File')
menuBar.Append(viewMenu, 'View') menuBar.Append(viewMenu, 'View')
...@@ -588,10 +606,10 @@ class FSLEyesFrame(wx.Frame): ...@@ -588,10 +606,10 @@ class FSLEyesFrame(wx.Frame):
self.__settingsMenu = settingsMenu self.__settingsMenu = settingsMenu
# Global actions # Global actions
actionz = [openfile .OpenFileAction, actionz = [actions.OpenFileAction,
openstandard.OpenStandardAction, actions.OpenStandardAction,
copyoverlay .CopyOverlayAction, actions.CopyOverlayAction,
saveoverlay .SaveOverlayAction] actions.SaveOverlayAction]
for action in actionz: for action in actionz:
menuItem = fileMenu.Append(wx.ID_ANY, strings.actions[action]) menuItem = fileMenu.Append(wx.ID_ANY, strings.actions[action])
...@@ -614,17 +632,54 @@ class FSLEyesFrame(wx.Frame): ...@@ -614,17 +632,54 @@ class FSLEyesFrame(wx.Frame):
# Perspectives # Perspectives
viewMenu.AppendSubMenu(perspectiveMenu, '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)) 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) actionObj.bindToWidget(self, wx.EVT_MENU, menuItem)
# Save perspective # Add menu items for other perspective
perspectiveMenu.AppendSeparator() # operations, but separate them from the
savePerspAction = saveperspective.SavePerspectiveAction(self) # existing perspectives
savePerspMenuItem = perspectiveMenu.Append( if len(saved) > 0:
wx.ID_ANY, strings.actions[savePerspAction]) 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)
This diff is collapsed.
...@@ -4,8 +4,8 @@ ...@@ -4,8 +4,8 @@
# #
# Author: Paul McCarthy <pauldmccarthy@gmail.com> # Author: Paul McCarthy <pauldmccarthy@gmail.com>
# #
"""This module provides a simple API to :func:`read` and :func:`write` """This module provides a simple API to :func:`read`, :func:`write`, and
persistent application settings. :func:`delete` persistent application settings.
.. note:: Currently the configuration management API provided by :mod:`wx` .. note:: Currently the configuration management API provided by :mod:`wx`
(http://docs.wxwidgets.org/trunk/overview_config.html) is used for (http://docs.wxwidgets.org/trunk/overview_config.html) is used for
...@@ -69,3 +69,18 @@ def write(name, value): ...@@ -69,3 +69,18 @@ def write(name, value):
log.debug('Writing {}: {}'.format(name, value)) log.debug('Writing {}: {}'.format(name, value))
config.Write(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)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment