#
# panel.py -
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides the :class:`FSLeyesPanel` class.
A :class:`FSLeyesPanel` object is a :class:`wx.Panel` which provides some sort
of view of a collection of overlay objects, contained within an
:class:`.OverlayList`. The :class:`FSLeyesPanel` is the base class for all GUI
panels in FSLeyes - see also the :class:`.ViewPanel` and :class:`.ControlPanel`
classes.
``FSLeyesPanel`` instances are also :class:`.ActionProvider` instances - any
actions which are specified in the class definitions may (or may not) be
exposed to the user. Furthermore, any display configuration options which
should be made available available to the user can be added as
:class:`.PropertyBase` attributes of the :class:`FSLeyesPanel` subclass.
.. note:: ``FSLeyesPanel`` instances are usually displayed within a
:class:`.FSLeyesFrame`, but they can be used on their own
as well. You will need to create, or need references to,
an :class:`.OverlayList` and a :class:`.DisplayContext`.
For example::
import fsleyes.overlay as ovl
import fsleyes.displaycontext as dc
import fsleyes.views.orthopanel as op
overlayList = ovl.OverlayList()
displayCtx = dc.DisplayContext(overlayList)
# the parent argument is some wx parent
# object such as a wx.Frame or wx.Panel.
# Pass in None as the FSLeyesFrame
orthoPanel = op.OrthoPanel(parent,
overlayList,
displayCtx,
None)
"""
import logging
import wx
import wx.siplib as sip
import fsleyes_props as props
import fsleyes_widgets.floatspin as floatspin
import fsleyes_widgets.floatslider as floatslider
import fsleyes_widgets.rangeslider as rangeslider
from . import actions
from . import displaycontext
log = logging.getLogger(__name__)
[docs]class _FSLeyesPanel(actions.ActionProvider, props.SyncableHasProperties):
"""The ``_FSLeyesPanel`` is the base class for the :class:`.FSLeyesPanel`
and the :class:`.FSLeyesToolBar`.
A ``_FSLeyesPanel`` has the following methods and properties, available for
use by subclasses:
.. autosummary::
:nosignatures:
name
frame
setNavOrder
destroy
destroyed
.. note:: When a ``_FSLeyesPanel`` is no longer required, the
:meth:`destroy` method **must** be called!
"""
[docs] def __init__(self, overlayList, displayCtx, frame, kbFocus=False):
"""Create a :class:`_FSLeyesPanel`.
:arg overlayList: A :class:`.OverlayList` instance.
:arg displayCtx: A :class:`.DisplayContext` instance.
:arg frame: The :class:`.FSLeyesFrame` that created this
``_FSLeyesPanel``. May be ``None``.
:arg kbFocus: If ``True``, a keyboard event handler is configured
to intercept ``Tab`` and ``Shift+Tab`` keyboard
events, to shift focus between a set of child
widgets. The child widgets to be included in the
navigation can be specified with the
:meth:`setNavOrder` method.
"""
actions.ActionProvider .__init__(self, overlayList, displayCtx)
props.SyncableHasProperties.__init__(self)
if not isinstance(displayCtx, displaycontext.DisplayContext):
raise TypeError(
'displayCtx must be a '
'{} instance'.format( displaycontext.DisplayContext.__name__))
self.__frame = frame
self.__name = '{}_{}'.format(self.__class__.__name__, id(self))
self.__destroyed = False
self.__navOrder = None
if kbFocus:
self.Bind(wx.EVT_CHAR_HOOK, self.__onCharHook)
[docs] def setNavOrder(self, children):
"""Set the keyboard (tab, shift+tab) navigation order to the
given list of controls, assumed to be children of this
``_FSLeyesPanel``.
"""
nav = []
allChildren = []
for c in children:
if type(c) == wx.Panel: allChildren.extend(c.GetChildren())
else: allChildren.append(c)
children = allChildren
log.debug('Updating nav order for {}'.format(type(self).__name__))
for i, w in enumerate(children):
log.debug('{} nav {:2d}: {}'.format(
type(self).__name__,
i,
type(w).__name__))
# Special cases for some of our custom controls
if isinstance(w, floatspin.FloatSpinCtrl):
nav.append(w.textCtrl)
elif isinstance(w, floatslider.SliderSpinPanel):
nav.append(w.spinCtrl.textCtrl)
elif isinstance(w, rangeslider.RangePanel):
low = w.lowWidget
high = w.highWidget
if isinstance(low, floatspin.FloatSpinCtrl):
low = low.textCtrl
if isinstance(high, floatspin.FloatSpinCtrl):
high = high.textCtrl
nav.extend([low, high])
elif isinstance(w, rangeslider.RangeSliderSpinPanel):
nav.extend([w.lowSpin.textCtrl, w.highSpin.textCtrl])
else:
nav.append(w)
self.__navOrder = nav
def __onCharHook(self, ev):
"""Called on ``EVT_CHAR_HOOK`` events. Intercepts tab key presses,
to force an explicit keyboard navigation ordering.
"""
if self.__navOrder is None or ev.GetKeyCode() != wx.WXK_TAB:
ev.Skip()
return
# Get the widget that has focus
try:
focusIdx = self.__navOrder.index(wx.Window.FindFocus())
log.debug('{} focus nav event ({:2d} [{}] is focused)'.format(
type(self).__name__,
focusIdx,
type(wx.Window.FindFocus()).__name__))
# Some other widget that we
# don't care about has focus.
except Exception:
ev.Skip()
return
if ev.ShiftDown(): offset = -1
else: offset = 1
# Get the next widget in
# the tab traversal order
nextIdx = (focusIdx + offset) % len(self.__navOrder)
# Search for the next enabled widget
while not (self.__navOrder[nextIdx].IsEnabled() and
self.__navOrder[nextIdx].IsShownOnScreen()):
if nextIdx == focusIdx:
break
nextIdx = (nextIdx + offset) % len(self.__navOrder)
toFocus = self.__navOrder[nextIdx]
log.debug('{}: moving focus to {:2d} [{}]'.format(
type(self).__name__,
nextIdx,
type(toFocus).__name__))
toFocus.SetFocus()
# If the next widget to receive
# focus is a TextCtrl, select
# all of its text
if isinstance(toFocus, wx.TextCtrl):
toFocus.SelectAll()
@property
def name(self):
"""Returns a unique name associated with this ``_FSLeyesPanel``. """
return self.__name
@property
def frame(self):
"""Returns the :class:`.FSLeyesFrame` which created this
``_FSLeyesPanel``. May be ``None``, if this panel was not created
by a ``FSLeyesFrame``.
"""
return self.__frame
[docs] def destroy(self):
"""This method must be called by whatever is managing this
``_FSLeyesPanel`` when it is to be closed/destroyed.
It seems to be impossible to define a single handler (on either the
:attr:`wx.EVT_CLOSE` and/or :attr:`wx.EVT_WINDOW_DESTROY` events)
which handles both cases where the window is destroyed (in the process
of destroying a parent window), and where the window is explicitly
closed by the user (e.g. when embedded as a page in a Notebook).
This issue is probably caused by my use of the AUI framework for
layout management, as the AUI manager/notebook classes do not seem to
call close/destroy in all cases. Everything that I've tried, which
relies upon ``EVT_CLOSE``/``EVT_WINDOW_DESTROY`` events, inevitably
results in the event handlers not being called, or in segmentation
faults (presumably due to double-frees at the C++ level).
Subclasses which need to perform any cleaning up when they are closed
may override this method, and should be able to assume that it will be
called. So this method *must* be called by managing code when a panel
is deleted.
Overriding subclass implementations must call this base class method,
otherwise memory leaks will probably occur, and warnings will probably
be output to the log (see :meth:`__del__`). This implememtation should
be called **after** the subclass has performed its own clean-up, as
this method expliciltly clears the ``overlayList`` and ``displayCtx``
references (via :meth:`.ActionProvider.destroy`).
"""
actions.ActionProvider.destroy(self)
self.__frame = None
self.__navOrder = None
self.__destroyed = True
@property
def destroyed(self):
"""Returns ``True`` if a call to :meth:`destroy` has been made,
``False`` otherwise.
"""
return self.__destroyed
[docs] def __del__(self):
"""If the :meth:`destroy` method has not been called, a warning message
is logged.
"""
if not self.__destroyed:
log.warning('The {}.destroy() method has not been called '
'- unless the application is shutting down, '
'this is probably a bug!'.format(type(self).__name__))
[docs]class FSLeyesPanel(_FSLeyesPanel, wx.Panel):
"""The ``FSLeyesPanel`` is the base class for all view and control panels
in *FSLeyes*. See the :mod:`fsleyes` documentation for more details.
See also the :class:`.ViewPanel` and :class:`.ControlPanel` classes.
"""
__metaclass__ = sip.wrappertype
[docs] def __init__(self,
parent,
overlayList,
displayCtx,
frame,
*args,
**kwargs):
# Slightly ugly way of supporting the _FSLeyesPanel
# kbFocus argument. In order to catch keyboard events,
# we need the WANTS_CHARS style. So we peek in
# kwargs to see if it is present. If it is, we add
# WANTS_CHARS to the style flag.
kbFocus = kwargs.pop('kbFocus', False)
if kbFocus:
# The wx.Panel style defaults to TAB_TRAVERSAL
style = kwargs.get('style', wx.TAB_TRAVERSAL)
kwargs['style'] = style | wx.WANTS_CHARS
wx.Panel.__init__(self, parent, *args, **kwargs)
_FSLeyesPanel.__init__(self, overlayList, displayCtx, frame, kbFocus)