Source code for fsleyes.profiles

#
# The profiles module contains logic for mouse-keyboard interaction.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""The :mod:`profiles` package contains logic for mouse/keyboard interaction
with :class:`.ViewPanel` panels.


This logic is encapsulated in four classes:


 - The :class:`Profile` class is intended to be subclassed. A :class:`Profile`
   instance contains the mouse/keyboard event handlers for a particular type
   of ``ViewPanel`` to allow the user to interact with the view in a
   particular way. For example, the :class:`.OrthoViewProfile` class allows
   the user to navigate through the display space in an :class:`.OrthoPanel`
   canvas, whereas the :class:`.OrthoEditProfile` class contains interaction
   logic for selecting and editing :class:`.Image` voxels in an ``OrthoPanel``.


 - The :class:`.CanvasPanelEventManager` manages ``wx`` GUI events on the
   :class:`.SliceCanvas` instances contained in :class:`.CanvasPanel` views.


 - The :class:`.PlotPanelEventManager` manages ``matplotlib`` GUI events on
   the ``matplotlib Canvas`` instances contained in :class:`.PlotPanel`
   views.


 - The :class:`ProfileManager` class is used by ``ViewPanel`` instances to
   create and change the ``Profile`` instance currently in use.


The :mod:`.profilemap` module contains mappings between ``ViewPanel`` types,
and their corresponding ``Profile`` types.


The ``profiles`` package is also home to the :mod:`.shortcuts` module, which
defines global *FSLeyes* keyboard shortcuts.
"""


import logging
import inspect
import collections

import wx

import matplotlib.backend_bases as mplbackend

from   fsl.utils.platform import platform as fslplatform
import fsl.utils.deprecated               as deprecated
import fsleyes_props                      as props
import fsleyes.actions                    as actions


log = logging.getLogger(__name__)


[docs]class ProfileManager(object): """Manages creation/registration/de-registration of :class:`Profile` instances for a :class:`.ViewPanel` instance. A :class:`ProfileManager` instance is created and used by every :class:`.ViewPanel` instance. The :mod:`.profilemap` module defines the :class:`Profile` types which should used for specific :class:`.ViewPanel` types. """
[docs] def __init__(self, viewPanel, overlayList, displayCtx): """Create a :class:`ProfileManager`. :arg viewPanel: The :class:`.ViewPanel` instance which this :class:`ProfileManager` is to manage. :arg overlayList: The :class:`.OverlayList` instance containing the overlays that are being displayed. :arg displayCtx: The :class:`.DisplayContext` instance which defines how overlays are being displayed. """ from . import profilemap self.__viewPanel = viewPanel self.__viewCls = viewPanel.__class__ self.__overlayList = overlayList self.__displayCtx = displayCtx self.__currentProfile = None profileProp = viewPanel.getProp('profile') profilez = profilemap.profiles.get(viewPanel.__class__, []) for profile in profilez: profileProp.addChoice(profile, instance=viewPanel) if len(profilez) > 0: viewPanel.profile = profilez[0]
[docs] def destroy(self): """This method must be called by the owning :class:`.ViewPanel` when it is about to be destroyed (or when it no longer needs a ``ProfileManager``). This method destroys the current :class:`Profile` (if any), and clears some internal object references to avoid memory leaks. """ if self.__currentProfile is not None: self.__currentProfile.deregister() self.__currentProfile.destroy() self.__currentProfile = None self.__viewPanel = None self.__overlayList = None self.__overlaydisplayCtx = None
[docs] def getCurrentProfile(self): """Returns the :class:`Profile` instance currently in use.""" return self.__currentProfile
[docs] def changeProfile(self, profile): """Deregisters the current :class:`Profile` instance (if necessary), and creates a new one corresponding to the named profile. """ from . import profilemap profileCls = profilemap.profileHandlers[self.__viewCls, profile] # the current profile is the requested profile if (self.__currentProfile is not None) and \ (self.__currentProfile.__class__ is profileCls): return if self.__currentProfile is not None: log.debug('Deregistering {} profile from {}'.format( self.__currentProfile.__class__.__name__, self.__viewCls.__name__)) self.__currentProfile.deregister() self.__currentProfile.destroy() self.__currentProfile = None self.__currentProfile = profileCls(self.__viewPanel, self.__overlayList, self.__displayCtx) log.debug('Registering {} profile with {}'.format( self.__currentProfile.__class__.__name__, self.__viewCls.__name__)) self.__currentProfile.register()
[docs]class Profile(props.SyncableHasProperties, actions.ActionProvider): """A :class:`Profile` class implements keyboard/mouse interaction behaviour for a :class:`.ViewPanel` instance. Subclasses should specify at least one *mode* of operation, which defines a sort of sub-profile. The current mode can be changed with the :attr:`mode` property. Subclasses must also override the :meth:`getEventTargets` method, to return the :mod:`wx` objects that are to be the targets for mouse/keyboard interaction. The ``Profile`` class currently only supports :class:`.CanvasPanel` and :class:`.PlotPanel` views. ``Profile`` instances use a :class:`.CanvasPanelEventManager` instance to manage GUI events on :class:`.CanvasPanel` instances, or a:class:`.PlotPanelEventManager` to manage GUI events on ``matplotlib Canvas`` objects. **Receiving events** In order to receive mouse or keyboard events, subclasses simply need to implement methods which handle the events of interest for the relevant mode, and name them appropriately. The name of a method handler must be of the form:: _[modeName]Mode[eventName] where ``modeName`` is an identifier for the profile mode (see the :meth:`__init__` method), and ``eventName`` is one of the following: - ``LeftMouseMove`` - ``LeftMouseDown`` - ``LeftMouseDrag`` - ``LeftMouseUp`` - ``RightMouseMove`` - ``RightMouseDown`` - ``RightMouseDrag`` - ``RightMouseUp`` - ``MiddleMouseMove`` - ``MiddleMouseDown`` - ``MiddleMouseDrag`` - ``MiddleMouseUp`` - ``MouseWheel`` - ``Char`` Profiles for :class:`.CanvasPanel` views may also implement handlers for these events: - ``MouseEnter`` - ``MouseLeave`` And :class:`.PlotPanel` views may implement handlers for these events: - ``LeftMouseArtistPick`` - ``RightMouseArtistPick`` .. note:: The ``MouseEnter`` and ``MouseLeave`` events are not supported on :class:`.PlotPanel` views due to bugs in ``matplotlib``. For example, if a particular profile has defined a mode called ``nav``, and is interested in left clicks, the profile class must provide a method called ``_navModeLeftMouseDown``. Then, whenever the profile is in the ``nav`` mode, this method will be called on left mouse clicks. **Handler methods** The parameters that are passed to these methods differs slightly depending on the type of event: - All mouse events, with the exception of ``MouseWheel`` must have the following signature:: def _[modeName]Mode[eventName](ev, canvas, mouseLoc, canvasLoc) where: - ``ev`` is the ``wx.Event`` object - ``canvas`` is the source canvas, - ``mouseLoc`` is the ``(x, y)`` mouse coordinates, - ``canvasLoc`` is the coordinates in the display/data coordinate system. - The ``MouseWheel`` handler must have the following signature:: def _[modeName]ModeMouseWheel(ev, canvas, wheel, mouseLoc, canvasLoc) where ``wheel`` is a positive or negative number indicating how much the mouse wheel was moved. - ``Char`` events must have the following signature:: def _[modeName]ModeChar(ev, canvas, key) where ``key`` is the key code of the key that was pressed. - Pick event handlers (only on :class:`.PlotPanel` views) must have the following signature:: def _[modeName]Mode[eventType](ev, canvas, artist, mouseLoc, canvasLoc) where ``artist`` is the ``matplotlib`` artist that was picked. All handler methods should return ``True`` to indicate that the event was handled, or ``False`` if the event was not handled. This is particularly important for ``Char`` event handlers - we don't want ``Profile`` sub-classes to be eating global keyboard shortcuts. A return value of ``None`` is interpreted as ``True``. If a handler returns ``False``, and a fallback handler is defined (see below), then that fallback handler will be called. **Extra handlers** Additional handlers can be registered for any event type via the :meth:`registerHandler` method. These handlers do not have to be methods of the ``Profile`` sub-class, and will be called for every occurrence of the event, regardless of the current mode. These handlers will be called after the standard handler method. When an extra handler is no longer needed, it must be removed via the :meth:`deregisterHandler` method. **Pre- and post- methods** A couple of other methods may be defined which, if they are present, will be called on all handled events: - ``_preEvent`` - ``_postEvent`` The ``_preEvent`` method will get called just before an event is passed to the handler. Likewise, the ``_postEvent`` method will get called just after the handler has been called. If no handlers for a particular event are defined, neither of these methods will be called. **Temporary, alternate and fallback handlers** The :mod:`.profilemap` module contains a set of dictionaries which define temporary, alternate, and fallback handlers. The :attr:`.profilemap.tempModeMap` defines, for each profile and each mod, a keyboard modifier which may be used to temporarily redirect mouse/keyboard events to the handlers for a different mode. For example, if while in ``nav`` mode, you would like the user to be able to switch to ``zoom`` mode with the control key, you can add a temporary mode map in the ``tempModeMap``. Additional temporary modes can be added via the :meth:`addTempMode` method. The :attr:`.profilemap.altHandlerMap`. dictionary allows you to re-use event handlers that have been defined for one mode in another mode. For example, if you would like right clicks in ``zoom`` mode to behave like left clicks in ``nav`` mode, you can set up such a mapping using the ``altHandlerMap`` dictionary. Additional alternate handlers can be added via the :meth:`addAltHandler` method. The :attr:`.profilemap.fallbackHandlerMap` dictionary allows you to define fallback handlers - if the default handler for a specific mode/event type returns a value of ``False``, the event will be forwarded to the fallback handler instead. Additional fallback handlers can be added via the :meth:`addFallbackHandler` method. **Actions and attributes** As the ``Profile`` class derives from the :class:`.ActionProvider` class, ``Profile`` subclasses may define properties and actions for the user to configure the profile behaviour, and/or to perform any relevant actions. The following instance attributes are present on a ``Profile`` instance, intended to be accessed by sub-classes: =============== ======================================================= ``viewPanel`` The :class:`ViewPanel` which is using this ``Profile``. ``overlayList`` A :class:`.OverlayList` instance. ``displayCtx`` A :class:`.DisplayContext` instance. ``name`` A unique name for this ``Profile`` instance. =============== ======================================================= """ mode = props.Choice() """The current profile mode - by default this is empty, but subclasses may specify the choice options in the :class:`__init__` method. """
[docs] def __init__(self, viewPanel, overlayList, displayCtx, modes=None): """Create a ``Profile`` instance. :arg viewPanel: The :class:`.ViewPanel` instance for which this ``Profile`` instance defines mouse/keyboard interaction behaviour. :arg overlayList: The :class:`.OverlayList` instance which contains the list of overlays being displayed. :arg displayCtx: The :class:`.DisplayContext` instance which defines how the overlays are to be displayed. :arg modes: A sequence of strings, containing the mode identifiers for this profile. These are added as options on the :attr:`mode` property. """ actions.ActionProvider .__init__(self, overlayList, displayCtx) props.SyncableHasProperties.__init__(self) self.__viewPanel = viewPanel self.__name = '{}_{}'.format(self.__class__.__name__, id(self)) import fsleyes.views.canvaspanel as canvaspanel import fsleyes.views.plotpanel as plotpanel if isinstance(viewPanel, canvaspanel.CanvasPanel): self.__evtManager = CanvasPanelEventManager(self) elif isinstance(viewPanel, plotpanel.PlotPanel): self.__evtManager = PlotPanelEventManager(self) else: raise ValueError('Unrecognised view panel type: {}'.format( type(viewPanel).__name__)) # Maps which define temporary modes and # alternate/fallback handlers when keyboard # modifiers are used, when a handler for # a particular event is not defined, or # when a handler indicates that the event # has not been handled. self.__tempModeMap = {} self.__altHandlerMap = {} self.__fallbackHandlerMap = {} # Extra handlers - for each event type, # a dictionary of { name : handler } # mappings which will be called after # the profile handler has been called # for a given event. self.__extraHandlers = collections.defaultdict(collections.OrderedDict) # of mouse/canvas event locations self.__lastCanvas = None self.__lastMousePos = None self.__lastCanvasPos = None self.__lastMouseUpPos = None self.__lastCanvasUpPos = None self.__mouseDownPos = None self.__canvasDownPos = None # we keep track of the mode we # were in on mosue down events, # so the correct mode is called # on subsequent drag/up events. self.__mouseDownMode = None # This field is used to keep # track of the last event for # which a handler was called. # After the first event, it # will be a tuple of strings # containing the (mode, event), # e.g. ('nav', 'LeftMouseMove'). # This is set in the __getHandler # method. # # The lastMouseUpHandler field # is set in __onMouseUp, to # keep track of the last mouse # handler self.__lastHandler = (None, None) self.__lastMouseUpHandler = (None, None) # Pre/post event handlers self.__preEventHandler = getattr(self, '_preEvent', None) self.__postEventHandler = getattr(self, '_postEvent', None) # Add all of the provided modes # as options to the mode property if modes is None: modes = [] modeProp = self.getProp('mode') for mode in modes: modeProp.addChoice(mode, instance=self) if len(modes) > 0: self.mode = modes[0] # Configure temporary modes and alternate # event handlers - see the profilemap # module from . import profilemap # We reverse the mro, so that the # modes/handlers defined on this # class take precedence. for cls in reversed(inspect.getmro(self.__class__)): tempModes = profilemap.tempModeMap .get(cls, {}) altHandlers = profilemap.altHandlerMap .get(cls, {}) fbHandlers = profilemap.fallbackHandlerMap.get(cls, {}) for (mode, keymod), tempMode in tempModes.items(): self.addTempMode(mode, keymod, tempMode) for (mode, handler), (altMode, altHandler) in altHandlers.items(): self.addAltHandler(mode, handler, altMode, altHandler) for (mode, handler), (fbMode, fbHandler) in fbHandlers.items(): self.addFallbackHandler(mode, handler, fbMode, fbHandler) # The __onEvent method delegates all # events based on this dictionary self.__eventMap = { wx.EVT_LEFT_DOWN.typeId : self.__onMouseDown, wx.EVT_MIDDLE_DOWN.typeId : self.__onMouseDown, wx.EVT_RIGHT_DOWN.typeId : self.__onMouseDown, wx.EVT_LEFT_UP.typeId : self.__onMouseUp, wx.EVT_MIDDLE_UP.typeId : self.__onMouseUp, wx.EVT_RIGHT_UP.typeId : self.__onMouseUp, wx.EVT_MOTION.typeId : self.__onMouseMove, wx.EVT_MOUSEWHEEL.typeId : self.__onMouseWheel, wx.EVT_ENTER_WINDOW.typeId : self.__onMouseEnter, wx.EVT_LEAVE_WINDOW.typeId : self.__onMouseLeave, wx.EVT_CHAR.typeId : self.__onChar, } log.debug('{}.init ({})'.format(type(self).__name__, id(self)))
[docs] def __del__(self): """Prints a log message. """ if log: log.debug('{}.del ({})'.format(type(self).__name__, id(self)))
[docs] def destroy(self): """This method must be called when this ``Profile`` is no longer needed - it is typically called by a :class:`ProfileManager`. Clears references to the display context, view panel, and overlay list, and calls :meth:`.ActionProvider.destroy`. """ actions.ActionProvider.destroy(self) self.__lastCanvas = None self.__viewPanel = None self.__extraHandlers = None
@property def name(self): """Returns the name of this ``Profile``. """ return self.__name @property def viewPanel(self): """Returns the :class:`.ViewPanel` associated with this ``Profile``. """ return self.__viewPanel @property @deprecated.deprecated('0.16.0', '1.0.0', 'Use viewPanel instead') def _viewPanel(self): return self.__viewPanel @property @deprecated.deprecated('0.16.0', '1.0.0', 'Use name instead') def _name(self): return self.__name @property @deprecated.deprecated('0.16.0', '1.0.0', 'Use displayCtx instead') def _displayCtx(self): return self.displayCtx @property @deprecated.deprecated('0.16.0', '1.0.0', 'Use overlayList instead') def _overlayList(self): return self.overlayList
[docs] def getEventTargets(self): """Must be overridden by subclasses, to return a sequence of :mod:`wx` objects that are the targets of mouse/keyboard interaction. .. note:: It is currently assumed that all of the objects in the sequence derive from the :class:`.SliceCanvas` class. """ raise NotImplementedError('Profile subclasses must implement ' 'the getEventTargets method')
[docs] def getMouseDownLocation(self): """If the mouse is currently down, returns a 2-tuple containing the x/y mouse coordinates, and the corresponding 3D display space coordinates, of the mouse down event. Otherwise, returns ``(None, None)``. """ return self.__mouseDownPos, self.__canvasDownPos
[docs] def getLastMouseLocation(self): """Returns a 2-tuple containing the most recent x/y mouse coordinates, and the corresponding 3D display space coordinates. """ return self.__lastMousePos, self.__lastCanvasPos
[docs] def getLastMouseUpLocation(self): """Returns a 2-tuple containing the most recent x/y mouse up event coordinates, and the corresponding 3D display space coordinates. """ return self.__lastMouseUpPos, self.__lastCanvasUpPos
[docs] def getLastMouseUpHandler(self): """Returns a tuple of two strings specifying the ``(mode, eventType)`` of the most recent mouse up event that was handled. If no events have been handled, returns ``(None, None)``. """ return self.__lastMouseUpHandler
[docs] def getLastHandler(self): """Returns a tuple of two strings specifying the ``(mode, eventType)`` of the most recent event that was handled. If no events have been handled, returns ``(None, None)``. """ return self.__lastHandler
[docs] def getLastCanvas(self): """Returns a reference to the canvas which most recently generated a mouse down or up event. """ return self.__lastCanvas
[docs] def getMplEvent(self): """If this ``Profile`` object is associated with a :class:`.PlotPanel`, this method will return the last ``matplotlib`` event that was generated. Otherwise, this method returns ``None``. This method can be called from within an event handler to retrieve the current ``matplotlib`` event. See the :meth:`PlotPanelEventManager.getMplEvent` method. """ if isinstance(self.__evtManager, PlotPanelEventManager): return self.__evtManager.getMplEvent() else: return None
[docs] def addTempMode(self, mode, modifier, tempMode): """Add a temporary mode to this ``Profile``, in addition to those defined in the :attr:`.profilemap.tempModeMap` dictionary. :arg mode: The mode to change from. :arg modifier: A keyboard modifier which will temporarily change the mode from ``mode`` to ``tempMode``. :arg tempMode: The temporary mode which the ``modifier`` key will change into. """ self.__tempModeMap[mode, modifier] = tempMode
[docs] def addAltHandler(self, mode, event, altMode, altEvent): """Add an alternate handler to this ``Profile``, in addition to those already defined in the :attr:`.profilemap.altHandleMap` dictionary. :arg mode: The source mode. :arg event: Name of the event to handle (e.g. ``LeftMouseDown``). :arg altMode: The mode for which the handler is defined. :arg altEvent: The event name for which the handler is defined. """ self.__altHandlerMap[mode, event] = (altMode, altEvent)
[docs] def addFallbackHandler(self, mode, event, fbMode, fbEvent): """Add a fallback handler to this ``Profile``, in addition to those already defined in the :attr:`.profilemap.fallbackHandleMap` dictionary. :arg mode: The source mode. :arg event: Name of the event to handle (e.g. ``LeftMouseDown``). :arg fbMode: The mode for which the handler is defined. :arg fbEvent: The event name for which the handler is defined. """ self.__fallbackHandlerMap[mode, event] = (fbMode, fbEvent)
[docs] def register(self): """This method must be called to register this ``Profile`` instance as the target for mouse/keyboard events. This method is called by the :class:`ProfileManager`. Subclasses may override this method to performa any initialisation, but must make sure to call this implementation. """ self.__evtManager.register()
[docs] def deregister(self): """This method de-registers this :class:`Profile` instance from receiving mouse/keybouard events. This method is called by the :class:`ProfileManager`. Subclasses may override this method to performa any initialisation, but must make sure to call this implementation. """ self.__evtManager.deregister()
[docs] def registerHandler(self, event, name, handler): """Add an extra handler for the specified event. When the event occurs, The ``handler`` function will be called after the default handler, provided by the ``Profie`` sub-class, is called. :arg event: The event type (e.g. ``LeftMouseDown``). :arg name: A unique name for the handler. A ``KeyError`` will be raised if a handler with ``name`` is already registered. :arg handler: Function to call when the event occurs. See the class documentation for details on the required signature. """ if name in self.__extraHandlers[event]: raise KeyError('A handler with name "{}" is ' 'already registered'.format(name)) self.__extraHandlers[event][name] = handler
[docs] def deregisterHandler(self, event, name): """Remove an extra handler from the specified event, that was previously added via :meth:`registerHandler` :arg event: The event type (e.g. ``LeftMouseDown``). :arg name: A unique name for the handler. A ``KeyError`` will be raised if a handler with ``name`` is already registered. """ if self.__extraHandlers is not None: self.__extraHandlers[event].pop(name)
[docs] def handleEvent(self, ev): """Called by the event manager when any event occurs on any of the :class:`.ViewPanel` targets. Delegates the event to one of the handler functions. :arg ev: The ``wx.Event`` that occurred. """ evType = ev.GetEventType() source = ev.GetEventObject() handler = self.__eventMap.get(evType, None) if source not in self.getEventTargets(): return if handler is None: return if evType in (wx.EVT_LEFT_DOWN .typeId, wx.EVT_MIDDLE_DOWN.typeId, wx.EVT_RIGHT_DOWN .typeId, wx.EVT_LEFT_UP .typeId, wx.EVT_MIDDLE_UP .typeId, wx.EVT_RIGHT_UP .typeId): self.__lastCanvas = source handler(ev)
[docs] def handlePickEvent(self, ev): """Called by the :class:`PlotPanelEventManager` when a ``matplotlib`` ``pick_event`` occurs. """ self.__onPick(ev)
def __getTempMode(self, ev): """Checks the temporary mode map to see if a temporary mode should be applied. Returns the mode identifier, or ``None`` if no temporary mode is applicable. """ mode = self.mode alt = ev.AltDown() ctrl = ev.ControlDown() shift = ev.ShiftDown() # Figure out the dictionary key to use, # based on the modifier keys that are down keys = { (False, False, False) : None, (False, False, True) : wx.WXK_SHIFT, (False, True, False) : wx.WXK_CONTROL, (False, True, True) : (wx.WXK_CONTROL, wx.WXK_SHIFT), (True, False, False) : wx.WXK_ALT, (True, False, True) : (wx.WXK_ALT, wx.WXK_SHIFT), (True, True, False) : (wx.WXK_ALT, wx.WXK_CONTROL), (True, True, True) : (wx.WXK_ALT, wx.WXK_CONTROL, wx.WXK_SHIFT) } return self.__tempModeMap.get((mode, keys[alt, ctrl, shift]), None) def __getMouseLocation(self, ev): """Returns two tuples; the first contains the x/y coordinates of the given :class:`wx.MouseEvent`, and the second contains the corresponding x/y/z display space coordinates (for :class:`.CanvasPanel` views), or x/y data coordinates (for :class:`.PlotPanel` views). See the :meth:`CanvasPanelEventManager.getMouseLocation` and :meth:`PlotPanelEventManager.getMouseLocation` methods. """ return self.__evtManager.getMouseLocation(ev) def __getMouseButton(self, ev): """Returns a string describing the mouse button associated with the given :class:`wx.MouseEvent`. """ btn = ev.GetButton() if btn == wx.MOUSE_BTN_LEFT: return 'Left' elif btn == wx.MOUSE_BTN_RIGHT: return 'Right' elif btn == wx.MOUSE_BTN_MIDDLE: return 'Middle' elif ev.LeftIsDown(): return 'Left' elif ev.RightIsDown(): return 'Right' elif ev.MiddleIsDown(): return 'Middle' else: return None def __getMode(self, ev): """Returns the current profile mode - either the value of :attr:`mode`, or a temporary mode if one is active. """ # Is a temporary mode active? tempMode = self.__getTempMode(ev) if tempMode is None: return self.mode else: return tempMode def __getHandler(self, ev, evType, mode=None, origEvType=None, direct=False): """Returns a function which will handle the given :class:`wx.MouseEvent` or :class:`wx.KeyEvent` (the ``ev`` argument), or ``None`` if no handlers are found. If an alternate handler for the mode/event has been specified, it is returned. :arg ev: The event object :arg evType: The event type (e.g. ``'LeftMouseDown'``) :arg mode: Override the default mode with this one. If not provided, the handler for the current mode (or temporary mode, if one is active) will be used. :arg origEvType: If the ``evType`` is not the actual event that occurred (e.g. this method has been called to look up an alternate or fallback handler), the original event type must be passed in here. :arg direct: If ``False`` (the default), the returned function will call the standard event handler (a method of the ``Profile`` sub-class), its fallback handler if it returns ``False`` and a fallback has been specfiied, any extra handlers that have been registered for the event type, and will also call the pre- and post- event methods. Otherwise, the returned function will just be the sub-class handler method for the specified for ``evType/ ``mode``. """ if origEvType is None: origEvType = evType if mode is None: mode = self.__getMode(ev) # Lookup any alternate/fallback # handlers for the event alt = self.__altHandlerMap .get((mode, evType), None) fallback = self.__fallbackHandlerMap.get((mode, evType), None) # Is an alternate handler active? # Alternate handlers take precedence # over default handlers. if alt is not None: altMode, altEvType = alt return self.__getHandler(ev, altEvType, mode=altMode, origEvType=evType) # A fallback handler has # been specified for this # event - get a direct ref # to the fallback function if fallback is not None: fbMode, fbEvType = fallback fallback = self.__getHandler(ev, fbEvType, mode=fbMode, origEvType=evType, direct=True) # Search for a default method # which can handle the specified # mode/evtype. if mode is not None: handlerName = '_{}Mode{}'.format(mode, evType) else: handlerName = '_{}{}'.format(evType[0].lower(), evType[1:]) defHandler = getattr(self, handlerName, None) # If direct=True, we just # return the handler method, # even it there isn't one # defined. if direct: return defHandler # Otherwise we return a wrapper # which calls the pre- and post- # methods, and any extra handlers # that have been registered, handlers = [] # Insert a placeholder for the # default handler, because we # need to check its return value. if defHandler is not None: handlers.append('defHandler') handlers.extend(self.__extraHandlers[origEvType].values()) def handlerWrapper(*args, **kwargs): retval = None if self.__preEventHandler is not None: self.__preEventHandler(mode, evType) for handler in handlers: # Get the return value of the # default handler, and call its # fallback if necessary. if handler == 'defHandler': retval = defHandler(*args, **kwargs) if retval is False and fallback is not None: retval = fallback(*args, **kwargs) else: handler(*args, **kwargs) if self.__postEventHandler is not None: self.__postEventHandler(mode, evType) # Store the last event # that was processed self.__lastHandler = (mode, evType) return retval if len(handlers) > 0: log.debug('{} Handler(s) found for mode {}, event {}'.format( len(handlers), mode, evType)) return handlerWrapper return None def __onMouseWheel(self, ev): """Called when the mouse wheel is moved. Delegates to a mode specific handler if one is present. """ handler = self.__getHandler(ev, 'MouseWheel') if handler is None: return mouseLoc, canvasLoc = self.__getMouseLocation(ev) canvas = ev.GetEventObject() wheel = ev.GetWheelRotation() # wx/osx has this really useful feature # whereby if shift is being held down # (typically used for horizontal scrolling), # a mouse wheel direction which would have # produced positive values will now produce # negative values. if ev.ShiftDown() and \ fslplatform.wxPlatform in (fslplatform.WX_MAC_COCOA, fslplatform.WX_MAC_CARBON): wheel = -wheel log.debug('Mouse wheel event ({}) on {}'.format( wheel, type(canvas).__name__)) handler(ev, canvas, wheel, mouseLoc, canvasLoc) def __onMouseEnter(self, ev): """Called when the mouse enters a canvas target. Delegates to a mode specific handler if one is present. """ handler = self.__getHandler(ev, 'MouseEnter') if handler is None: return canvas = ev.GetEventObject() mouseLoc, canvasLoc = self.__getMouseLocation(ev) log.debug('Mouse enter event on {}'.format( type(canvas).__name__)) handler(ev, canvas, mouseLoc, canvasLoc) def __onMouseLeave(self, ev): """Called when the mouse leaves a canvas target. Delegates to a mode specific handler if one is present. """ handler = self.__getHandler(ev, 'MouseLeave') if handler is None: return canvas = ev.GetEventObject() mouseLoc, canvasLoc = self.__getMouseLocation(ev) log.debug('Mouse leave event on {}'.format( type(canvas).__name__)) handler(ev, canvas, mouseLoc, canvasLoc) def __onMouseDown(self, ev): """Called when any mouse button is pushed. Delegates to a mode specific handler if one is present. """ mouseLoc, canvasLoc = self.__getMouseLocation(ev) canvas = ev.GetEventObject() # On GTK, a GLCanvas won't be given # focus when it is clicked on. canvas.SetFocus() # Save information about this mouse # down event, as it may be needed # by subesquent drag/up events. self.__mouseDownPos = mouseLoc self.__canvasDownPos = canvasLoc self.__mouseDownMode = self.__getMode(ev) if self.__lastMousePos is None: self.__lastMousePos = mouseLoc if self.__lastCanvasPos is None: self.__lastCanvasPos = canvasLoc evType = '{}MouseDown'.format(self.__getMouseButton(ev)) handler = self.__getHandler(ev, evType) if handler is None: ev.Skip() return log.debug('{} event ({}, {}) on {}'.format( evType, mouseLoc, canvasLoc, type(canvas).__name__)) # If a handler returns None, we # assume that it means True if handler(ev, canvas, mouseLoc, canvasLoc) is False: ev.Skip() self.__lastMousePos = mouseLoc self.__lastCanvasPos = canvasLoc def __onMouseUp(self, ev): """Called when any mouse button is released. Delegates to a mode specific handler if one is present. """ evType = '{}MouseUp'.format(self.__getMouseButton(ev)) handler = self.__getHandler(ev, evType, mode=self.__mouseDownMode) if handler is None: self.__mouseDownPos = None self.__canvasDownPos = None self.__mouseDownMode = None ev.Skip() return canvas = ev.GetEventObject() mouseLoc, canvasLoc = self.__getMouseLocation(ev) log.debug('{} event ({}, {}) on {}'.format( evType, mouseLoc, canvasLoc, type(canvas).__name__)) if handler(ev, canvas, mouseLoc, canvasLoc) is False: ev.Skip() self.__lastMouseUpHandler = (self.__mouseDownMode, evType) self.__lastMouseUpPos = mouseLoc self.__lastCanvasUpPos = canvasLoc self.__mouseDownPos = None self.__canvasDownPos = None self.__mouseDownMode = None def __onMouseMove(self, ev): """Called on mouse motion. If a mouse button is down, delegates to :meth:`__onMouseDrag`. Otherwise, delegates to a mode specific handler if one is present. """ if ev.Dragging(): self.__onMouseDrag(ev) return handler = self.__getHandler(ev, 'MouseMove') if handler is None: ev.Skip() return canvas = ev.GetEventObject() mouseLoc, canvasLoc = self.__getMouseLocation(ev) log.debug('Mouse move event ({}, {}) on {}'.format( mouseLoc, canvasLoc, type(canvas).__name__)) if handler(ev, canvas, mouseLoc, canvasLoc) is False: ev.Skip() self.__lastMousePos = mouseLoc self.__lastCanvasPos = canvasLoc def __onMouseDrag(self, ev): """Called on mouse drags. Delegates to a mode specific handler if one is present. """ ev.Skip() canvas = ev.GetEventObject() mouseLoc, canvasLoc = self.__getMouseLocation(ev) evType = '{}MouseDrag'.format(self.__getMouseButton(ev)) handler = self.__getHandler(ev, evType, mode=self.__mouseDownMode) if handler is None: ev.Skip() return log.debug('{} event ({}, {}) on {}'.format( evType, mouseLoc, canvasLoc, type(canvas).__name__)) if handler(ev, canvas, mouseLoc, canvasLoc) is False: ev.Skip() self.__lastMousePos = mouseLoc self.__lastCanvasPos = canvasLoc def __onChar(self, ev): """Called on keyboard key presses. Delegates to a mode specific handler if one is present. """ handler = self.__getHandler(ev, 'Char') if handler is None: ev.Skip() return canvas = ev.GetEventObject() key = ev.GetKeyCode() log.debug('Keyboard event ({}) on {}'.format( key, type(canvas).__name__)) if handler(ev, canvas, key) is False: ev.Skip() def __onPick(self, ev): """Called by the :meth:`handlePickEvent`. Delegates the event to a suitable handler, if one exists. """ evType = '{}MouseArtistPick'.format(self.__getMouseButton(ev)) handler = self.__getHandler(ev, evType) if handler is None: ev.Skip() return canvas = ev.GetEventObject() artist = self.__evtManager.getPickedArtist() mouseLoc, canvasLoc = self.__getMouseLocation(ev) log.debug('Pick event ({}, {}) on {}'.format( mouseLoc, canvasLoc, type(canvas).__name__)) if handler(ev, canvas, artist, mouseLoc, canvasLoc) is False: ev.Skip()
[docs]class CanvasPanelEventManager(object): """This class manages ``wx`` mouse/keyboard events originating from :class:`.SliceCanvas` instances contained within :class:`.CanvasPanel` views. """
[docs] def __init__(self, profile): """Create a ``CanvasPanelEventManager``. :arg profile: The :class:`Profile` instance that owns this event manager. """ self.__profile = profile
[docs] def register(self): """This method must be called to register event listeners on mouse/ keyboard events. This method is called by meth :`Profile.register`. """ for t in self.__profile.getEventTargets(): t.Bind(wx.EVT_LEFT_DOWN, self.__onEvent) t.Bind(wx.EVT_MIDDLE_DOWN, self.__onEvent) t.Bind(wx.EVT_RIGHT_DOWN, self.__onEvent) t.Bind(wx.EVT_LEFT_UP, self.__onEvent) t.Bind(wx.EVT_MIDDLE_UP, self.__onEvent) t.Bind(wx.EVT_RIGHT_UP, self.__onEvent) t.Bind(wx.EVT_MOTION, self.__onEvent) t.Bind(wx.EVT_MOUSEWHEEL, self.__onEvent) t.Bind(wx.EVT_ENTER_WINDOW, self.__onEvent) t.Bind(wx.EVT_LEAVE_WINDOW, self.__onEvent) t.Bind(wx.EVT_CHAR, self.__onEvent)
[docs] def deregister(self): """This method de-registers mouse/keybouard event listeners. This method is called by the :meth:`Profile.deregister` method. """ for t in self.__profile.getEventTargets(): t.Unbind(wx.EVT_LEFT_DOWN) t.Unbind(wx.EVT_MIDDLE_DOWN) t.Unbind(wx.EVT_RIGHT_DOWN) t.Unbind(wx.EVT_LEFT_UP) t.Unbind(wx.EVT_MIDDLE_UP) t.Unbind(wx.EVT_RIGHT_UP) t.Unbind(wx.EVT_MOTION) t.Unbind(wx.EVT_MOUSEWHEEL) t.Unbind(wx.EVT_ENTER_WINDOW) t.Unbind(wx.EVT_LEAVE_WINDOW) t.Unbind(wx.EVT_CHAR)
[docs] def getMouseLocation(self, ev): """Returns two tuples; the first contains the x/y coordinates of the given :class:`wx.MouseEvent`, and the second contains the corresponding x/y/z display space coordinates. """ mx, my = ev.GetPosition() canvas = ev.GetEventObject() w, h = canvas.GetClientSize() my = h - my canvasPos = canvas.canvasToWorld(mx, my) return (mx, my), canvasPos
def __onEvent(self, ev): """Event handler. Passes the event to :class:`Profile.handleEvent`. """ self.__profile.handleEvent(ev)
[docs]class PlotPanelEventManager(object): """This class manages events originating from ``matplotlib`` ``Canvas`` objects contained within :class:`.PlotPanel` views. .. note:: This class has no support for ``figure_enter_event``, ``figure_leave_event``, ``axis_enter_event``, or ``axis_leave_event`` events. There appears to be some bugs lurking in the ``matplotlib/backend_bases.py`` file (something to do with the ``LocationEvent.lastevent`` property) which cause FSLeyes to seg-fault on ``figure_enter_event`` events. """
[docs] def __init__(self, profile): """Create a ``PlotPanelEventManager``. :arg profile: The :class:`Profile` instance that owns this event manager. """ self.__profile = profile self.__lastEvent = None self.__lastLocEvent = None self.__lastPickEvent = None self.__cids = {} self.__eventTypes = [ 'button_press_event', 'button_release_event', 'motion_notify_event', 'pick_event', 'scroll_event', 'key_press_event', 'key_release_event']
[docs] def register(self): """Register listeners on all relevant ``matplotlib`` events. This method is called by :meth:`Profile.register`. """ for target in self.__profile.getEventTargets(): for ev in self.__eventTypes: self.__cids[ev] = target.mpl_connect(ev, self.__onEvent)
[docs] def deregister(self): """De-register listeners on all relevant ``matplotlib`` events. This method is called by :meth:`Profile.deregister`. """ for target in self.__profile.getEventTargets(): for ev in self.__eventTypes: target.mpl_disconnect(self.__cids[ev]) self.__cids = {}
[docs] def getMouseLocation(self, ev=None): """Returns two tuples; the first contains the x/y coordinates of the most recent ``matplotlib`` event, and the second contains the corresponding x/y data coordinates. If an event has not yet occurred, or if the mouse position is not on any event target, this method returns ``(None, None)``. :arg ev: Ignored. """ mplev = self.__lastLocEvent if mplev is None: return None, None if mplev.xdata is None: return None, None if mplev.ydata is None: return None, None mousex, mousey = mplev.x, mplev.y datax, datay = mplev.xdata, mplev.ydata return (mousex, mousey), (datax, datay)
[docs] def getPickedArtist(self): """Returns the ``matplotlib.Artist`` that was most recently picked (clicked on) by the user. """ mplev = self.__lastPickEvent if mplev is None: return None else: return mplev.artist
[docs] def getMplEvent(self): """Returns the most recent ``matplotlib`` event that occurred, or ``None`` if no events have occurred yet. """ return self.__lastEvent
def __onEvent(self, ev): """Handler for ``matplotlib`` events. Passes the corresponding ``wx`` event to the :meth:`Profile.handleEvent` method. """ if ev.name not in self.__eventTypes: return self.__lastEvent = ev islocev = isinstance(ev, mplbackend.LocationEvent) ispickev = isinstance(ev, mplbackend.PickEvent) if islocev: self.__lastLocEvent = ev if ispickev: self.__lastPickEvent = ev if ispickev: self.__profile.handlePickEvent(ev.guiEvent) else: self.__profile.handleEvent( ev.guiEvent)