Source code for fsleyes.views.viewpanel

#
# viewpanel.py - Superclass for all FSLeyes view panels.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides the :class:`ViewPanel` class, which is the base-class
for all of the *FSLeyes view* panels. See the :mod:`fsleyes` package
documentation for more details.
"""


import              logging
import itertools as it

import                                  wx
import wx.lib.agw.aui                as aui
import wx.lib.agw.aui.framemanager   as auifm

import fsl.utils.notifier            as notifier
import fsleyes_widgets               as fwidgets

import fsleyes.panel                 as fslpanel
import fsleyes.toolbar               as fsltoolbar
import fsleyes.controls.controlpanel as ctrlpanel
import fsleyes.profiles              as profiles
import fsleyes.strings               as strings
import fsleyes.actions               as actions


log = logging.getLogger(__name__)


[docs]class ViewPanel(fslpanel.FSLeyesPanel): """The ``ViewPanel`` class is the base-class for all *FSLeyes views*. A ``ViewPanel`` displays some sort of view of the overlays in an :class:`.OverlayList`. The settings for a ``ViewPanel`` are defined by a :class:`.DisplayContext` instance. **Panels and controls** A ``ViewPanel`` class uses a ``wx.lib.agw.aui.AuiManager`` to lay out its children. A ``ViewPanel`` has one central panel, which contains the primary view; and may have one or more secondary panels, which contain *controls* - see the :mod:`.controls` package. The centre panel can be set via the :meth:`centrePanel` property, and secondary panels can be added/removed to/from with the :meth:`togglePanel` method. The current state of a secondary panel (i.e. whether one is open or not) can be queried with the :meth:`isPanelOpen` method, and existing secondary panels can be accessed via the :meth:`getPanel` method. Secondary panels must be derived from either the :class:`.ControlPanel` or :class:`.ControlToolBar` base-classes. **Profiles** The logic which defines the way that a user interacts with a view panel is defined by a :class:`.Profile` object, which contains mouse and keyboard event handlers for reacting to user input. Sub-classes must call the :meth:`initProfile` method to initialise their default profile. Other profiles may be associated with a particular control panel - these profiles can be temporarily activated and deactivated when the control panel is toggled via the :meth:`togglePanel` method. The currently active interaction profile can be accessed with the :meth:`currentProfile` method. See the :mod:`.profiles` package for more information on interaction profiles. **Programming interface** The following methods are available on a ``Viewpanel`` for programmatically controlling its display and layout: .. autosummary:: :nosignatures: togglePanel isPanelOpen getPanel getPanels getTools removeFromFrame removeAllPanels getPanelInfo auiManager """
[docs] def controlOptions(self, cpType): """May be overridden by sub-classes. Given a control panel type, may return a dictionary containing arguments to be passed to the ``__init__`` method when the control panel is created. """ return None
[docs] @staticmethod def title(): """May be overridden by sub-classes. Returns a title for this ``ViewPanel``, to be used in menus and window title bars. """ return None
[docs] @staticmethod def controlOrder(): """May be overridden by sub-classes. Returns a list of names of control panel types, specifying a suggested order for the settings menu for views of this type. """ return None
[docs] @staticmethod def toolOrder(): """May be overridden by sub-classes. Returns a list of names of tools, specifying a suggested order for the corresponding entries in the FSLeyes tools menu. Note that the ordering of tools returned by the :meth:`getTools` method is honoured - the ordering returned by *this* method relates to tools which are implemented as plugins. """ return None
[docs] @staticmethod def defaultLayout(): """May be overridden by sub-classes. Should return a list of names of FSLeyes :class:`.ControlPanel` types which form the default layout for this view. """ return None
[docs] def __init__(self, parent, overlayList, displayCtx, frame): """Create a ``ViewPanel``. :arg parent: ``wx`` parent object :arg overlayList: The :class:`.OverlayList` :arg displayCtx: A :class:`.DisplayContext` object unique to this ``ViewPanel`` :arg frame: The :class:`.FSLeyesFrame` """ fslpanel.FSLeyesPanel.__init__( self, parent, overlayList, displayCtx, frame) # Currently only two profiles are allowed at # any one time - the default profile, passed # to initProfiles. and one more profile # associated with a control panel. # # If the default profile is active, and a # new control is opened which needs a different # profile, that profile is activated. # # If a non-default profile (#1) is active, and # a new control is opened which needs a # different profile (#2), all controls which # require profile #1 are closed, and profile # #2 is activated. # # This all happens in togglePanel. self.__profileManager = profiles.ProfileManager( self, overlayList, displayCtx, 2) # The __centrePanel attribute stores a reference # to the main (centre) panel on this ViewPanel. # It is set by sub-class implementations via # the centrePanel property. # # The panels dictionary stores a collection # of {type : instance} mappings of active # FSLeyes control panels that are contained # in this view panel. self.__centrePanel = None self.__panels = {} # Notifier instance for emitting events. # Currently only two events are emitted # - one of them is profile changes, and # the profilemanager emits these events # anyway, so we can just use it. As for # the other event (aui layout changes), # well, we're being a bit dodgy and # emitting these events via the # profilemanager. self.__events = self.__profileManager # See note in FSLeyesFrame about # the user of aero docking guides. self.__auiMgr = aui.AuiManager( self, agwFlags=(aui.AUI_MGR_RECTANGLE_HINT | aui.AUI_MGR_NO_VENETIAN_BLINDS_FADE | aui.AUI_MGR_ALLOW_FLOATING | aui.AUI_MGR_AERO_DOCKING_GUIDES | aui.AUI_MGR_LIVE_RESIZE)) self.__auiMgr.SetDockSizeConstraint(0.5, 0.5) self.__auiMgr.Bind(aui.EVT_AUI_PANE_CLOSE, self.__onPaneClose) self.__auiMgr.Bind(aui.EVT_AUI_PERSPECTIVE_CHANGED, self.__onPerspectiveChange) # A very shitty necessity. When panes are floated, # the AuiManager sets the size of the floating frame # to the minimum size of the panel, without taking # into account the size of its borders/title bar, # meaning that the panel size is too small. Here, # we're just creating a dummy MiniFrame (from which # the AuiFloatingFrame derives), and saving the size # of its trimmings for later use in the togglePanel # method. ff = wx.MiniFrame(self) size = ff.GetSize().Get() clientSize = ff.GetClientSize().Get() self.__floatOffset = (size[0] - clientSize[0], size[1] - clientSize[1]) ff.Destroy() self.reloadPlugins()
[docs] def reloadPlugins(self): """Called by :meth:`__init__`, and by the :class:`.LoadPluginAction` when new plugins are registered. This is a bit of a hack, but less hacky than it used to be. All plugin-provided control panels and tools which support this ``ViewPanel`` are looked up via the :mod:`.plugins.` module. Then, for each control, we create a :class:`.ToggleControlPanelAction`, and add it as an attribute on this ``ViewPanel``. Similarly, all plugin-provided tools which support this ``ViewPanel`` are created and added as attributes. In both cases, the class name of the control/tool is used as the attribute name. This is done so that these actions will work with the :class:`.ActionProvider` interface, and hence the :meth:`.FSLEeyesFrame.populateMenu` method. This implementation may change in the future if it becomes problematic (e.g. due to naming conflicts). """ import fsleyes.plugins as plugins # controls for ctrlTitle, ctrlType in plugins.listControls(type(self)).items(): name = ctrlType.__name__ if not hasattr(self, name): act = actions.ToggleControlPanelAction( self.overlayList, self.displayCtx, self, ctrlType, name=name, title=ctrlTitle) setattr(self, name, act) # tools for toolType in plugins.listTools(type(self)).values(): name = toolType.__name__ if not hasattr(self, name): act = toolType(self.overlayList, self.displayCtx, self) setattr(self, name, act)
[docs] def destroy(self): """Removes some property listeners, destroys all child panels, destroys the :class:`.ProfileManager`, and ``AuiManager``, and calls :meth:`.FSLeyesPanel.destroy`. """ # Order of operations is important. For example, # FSLeyesPanel.destroy will result in all Actions # associated with this ViewPanel being destroyed, # and their destroy routines may interact with # this ViewPanel. # remove listeners from overlaylist and display context lName = 'ViewPanel_{}'.format(self.name) self.overlayList.removeListener('overlays', lName) self.displayCtx .removeListener('selectedOverlay', lName) fslpanel.FSLeyesPanel.destroy(self) # Make sure that any control panels are correctly destroyed for panel in self.__panels.values(): self.__auiMgr.DetachPane(panel) panel.destroy() # Disable the ProfileManager self.__profileManager.destroy() # Un-initialise the AUI manager self.__auiMgr.UnInit() # The AUI manager does not clear its # reference to this panel, so let's # do it here. self.__auiMgr._frame = None self.__profileManager = None self.__auiMgr = None self.__panels = None self.__centrePanel = None self.__events = None
@property def events(self) -> notifier.Notifier: """Return a reference to a :class:`.Notifier` instance which can be used to be notified when certain events occur. Currently the following events are emitted: - ``'profile'``, when the current interaction profile changes. Callbacks which are registered with the ``'profile'`` topic will be passed a tuple containing the types (:class:`.Profile` sub-classes) of the de-registered and newly registered profiles. - ``'aui_perspective'``, when the AUI-managed layout changes, e.g. sash resizes, control panels added/removed, etc. This event is emitted whenever the ``AuiManager`` emits a ``EVT_AUI_PERSPECTIVE_CHANGED`` event. It is re-emitted via the :class:`.Notifer` interface so that non-wx entities can be notified (see e.g. the :class:`.ToggleControlPanelAction`). """ return self.__events
[docs] def initProfile(self, defaultProfile): """Must be called by subclasses, after they have initialised all of the attributes which may be needed by their associated :class:`.Profile` instances. :arg defaultProfile: Default profile type """ self.__profileManager.activateProfile(defaultProfile)
@property def currentProfile(self): """Returns the :class:`.Profile` instance currently in use. """ return self.__profileManager.getCurrentProfile() @property def profileManager(self): """Returns a reference to the :class:`.ProfileManager` used by this ``ViewPanel``. """ return self.__profileManager @property def centrePanel(self): """Returns the primary (centre) panel on this ``ViewPanel``. """ return self.__centrePanel @centrePanel.setter def centrePanel(self, panel): """Set the primary centre panel for this ``ViewPanel``. This method is only intended to be called by sub-classes. """ panel.Reparent(self) paneInfo = (aui.AuiPaneInfo() .Name(type(panel).__name__) .CentrePane()) self.__auiMgr.AddPane(panel, paneInfo) self.__auiMgrUpdate() self.__centrePanel = panel
[docs] def togglePanel(self, panelType, *args, **kwargs): """Add/remove the secondary control panel of the specified type to/from this ``ViewPanel``. If no keyword argunents are specified, the arguments returned by the :meth:`.ControlMixin.defaultLayout` method are returned. :arg panelType: Type of the secondary panel. :arg args: All positional arguments are passed to the ``panelType`` constructor. :arg floatPane: If ``True``, the secondary panel is initially floated. Defaults to ``False``. :arg floatOnly: If ``True``, and ``floatPane=True``, the panel will be permanently floated (i.e. it will not be dockable). :arg floatPos: If provided, and ``floatPane`` is ``True``, specifies the location of the floating panel as ``(w, h)`` proportions between 0 and 1, relative to this view panel. :arg closeable: If ``False``, and ``floatPane=True``, the panel will not have a close button when it is floated. :arg location: If ``floatPane=False``, the initial dock position of the panel - either ``wx.TOP``, ``wx.BOTTOM``, ``wx.LEFT``, or ``wx.RIGHT. Defaults to ``wx.BOTTOM``. :arg title: Title to give the control. If not provided, it is assumed that a title for ``panelType`` is in :attr:`.strings.titles`. :arg kwargs: All other keyword arguments, are passed to the ``panelType`` constructor. :returns: The newly created control panel. .. note:: The ``panelType`` type must be a sub-class of :class:`.ControlPanel` or :class:`.ControlToolBar`, which can be created like so:: panel = panelType(parent, overlayList, displayCtx, frame, *args, **kwargs) .. warning:: Do not define a control (a.k.a. secondary) panel constructor to accept arguments with the names ``floatPane``, ``floatOnly``, ``floatPos``, ``closeable``, or ``location``, as arguments with those names will get eaten by this method before they can be passed to the constructor. """ # Merge in default options for this control, # with precedence (highest to lowest): # 1. kwargs # 2. ViewPanel.controlOptions # 3. ControlPanel.defaultLayout layout = panelType.defaultLayout() cpopts = self.controlOptions(panelType) if layout is None: layout = {} if cpopts is None: cpopts = {} for k, v in it.chain(cpopts.items(), layout.items()): if k not in kwargs: kwargs[k] = v location = kwargs.pop('location', None) floatPane = kwargs.pop('floatPane', False) floatOnly = kwargs.pop('floatOnly', False) closeable = kwargs.pop('closeable', True) title = kwargs.pop('title', None) floatPos = kwargs.pop('floatPos', (0.5, 0.5)) profileCls = panelType.profileCls() if title is None: title = panelType.title() if title is None: title = strings.titles.get(panelType, panelType.__name__) if location not in (None, wx.TOP, wx.BOTTOM, wx.LEFT, wx.RIGHT): raise ValueError('Invalid value for location') supported = panelType.supportedViews() if supported is not None and not isinstance(self, tuple(supported)): raise ValueError( '{} views are not supported by {} controls'.format( type(self).__name__, panelType.__name__)) # The panel is already open - close it window = self.__panels.get(panelType, None) if window is not None: self.__onPaneClose(None, window) return # Otherwise, create a new panel of the specified type. # If this control is associated with a custom interaction # profile, check that that profile is active, create it # if needed, and close down any controls which are not # compatible with the new profile. profile = self.currentProfile if profileCls is not None and not isinstance(profile, profileCls): log.debug('New control %s requires interaction profile %s but ' 'profile %s is active', panelType.__name__, profileCls.__name__, type(profile).__name__) # close down incompatible controls for ctype, cpanel in list(self.__panels.items()): cprofileCls = ctype.profileCls() if cprofileCls is not None and \ not issubclass(cprofileCls, profileCls): log.debug('Closing down control %s as it is incompatible ' 'with profile %s', ctype.__name__, profileCls.__name__) self.__onPaneClose(None, cpanel) # change profile self.__profileManager.activateProfile(profileCls) # The PaneInfo Name is the control panel class name - # this is used for saving and restoring layouts. window = panelType(self, self.overlayList, self.displayCtx, self, *args, **kwargs) # save a ref to the control if it has a custom # profile so, when it is destroyed, we know if profileCls is not None: self.__activeControl = window paneInfo = self.__layoutNewPanel(window, title, location, floatPane, floatPos, floatOnly, closeable) self.__auiMgr.AddPane(window, paneInfo) self.__panels[panelType] = window self.__auiMgrUpdate(newPanel=window) return window
def __layoutNewPanel(self, panel, title, location, floatPane, floatPos, floatOnly, closeable): """Sub-method of :meth:`togglePanel`. Creates and returns an ``AuiPaneInfo`` instance describing the initial layout of a new control panel. See ``togglePanel`` for an explanation of the arguments. """ paneInfo = aui.AuiPaneInfo().Name(type(panel).__name__) isToolbar = isinstance(panel, ctrlpanel.ControlToolBar) if isToolbar: # ToolbarPane sets the panel layer to 10 paneInfo.ToolbarPane() if panel.GetOrient() == wx.VERTICAL: paneInfo.GripperTop() # We are going to put any new toolbars on # the top of the panel, below any existing # toolbars. This is annoyingly complicated, # because the AUI designer(s) decided to # give the innermost layer an index of 0. # # So in order to put a new toolbar at the # innermost layer, we need to adjust the # layers of all other existing toolbars for p in self.__panels.values(): if isinstance(p, ctrlpanel.ControlToolBar): info = self.__auiMgr.GetPane(p) # This is nasty - the agw.aui.AuiPaneInfo # class doesn't have any publicly documented # methods of querying its current state. # So I'm accessing its undocumented instance # attributes (determined by browsing the # source code) if info.IsDocked() and \ info.dock_direction == aui.AUI_DOCK_TOP: info.Layer(info.dock_layer + 1) # When the toolbar contents change, # update the layout, so that the # toolbar's new size is accommodated panel.Bind(fsltoolbar.EVT_TOOLBAR_EVENT, self.__auiMgrUpdate) paneInfo.Caption(title) # Dock the pane at the position specified # by the location parameter if not floatPane: if location is None: if isToolbar: location = aui.AUI_DOCK_TOP else: location = aui.AUI_DOCK_BOTTOM elif location == wx.TOP: location = aui.AUI_DOCK_TOP elif location == wx.BOTTOM: location = aui.AUI_DOCK_BOTTOM elif location == wx.LEFT: location = aui.AUI_DOCK_LEFT elif location == wx.RIGHT: location = aui.AUI_DOCK_RIGHT # Make sure the pane is # resizable in case it # gets floated later on paneInfo.Direction(location) \ .Resizable(not isToolbar) # Or, for floating panes, centre the # floating pane on this ViewPanel else: selfPos = self.GetScreenPosition().Get() selfSize = self.GetSize().Get() selfCentre = (selfPos[0] + selfSize[0] * floatPos[0], selfPos[1] + selfSize[1] * floatPos[1]) paneSize = panel.GetBestSize().Get() panePos = (int(round(selfCentre[0] - paneSize[0] * 0.5)), int(round(selfCentre[1] - paneSize[1] * 0.5))) paneInfo.Float() \ .Resizable(True) \ .Dockable(not floatOnly) \ .CloseButton(closeable) \ .FloatingPosition(panePos) return paneInfo
[docs] def isPanelOpen(self, panelType): """Returns ``True`` if a panel of type ``panelType`` is open, ``False`` otherwise. """ return self.getPanel(panelType) is not None
[docs] def getPanel(self, panelType): """If an instance of ``panelType`` exists, it is returned. Otherwise ``None`` is returned. """ if panelType in self.__panels: return self.__panels[panelType] else: return None
[docs] @actions.action def removeAllPanels(self): """Remove all control panels from this ``ViewPanel``.""" for panelType in list(self.__panels.keys()): self.togglePanel(panelType)
[docs] @actions.action def removeFromFrame(self): """Remove this ``ViewPanel`` from the :class:`.FSLeyesFrame`. Will raise an error if this ``ViewPanel`` is not in a ``FSLeyesFrame``. """ self.frame.removeViewPanel(self)
[docs] def getPanels(self): """Returns a list containing all control panels currently shown in this ``ViewPanel``. """ return list(self.__panels.values())
[docs] def getPanelInfo(self, panel): """Returns the ``AuiPaneInfo`` object which contains information about the given control panel. """ return self.__auiMgr.GetPane(panel)
@property def auiManager(self): """Returns the ``wx.lib.agw.aui.AuiManager`` object which manages the layout of this ``ViewPanel``. """ return self.__auiMgr
[docs] def getTools(self): """This method should be overridden by sub-classes (if necessary), and should return any ``action`` methods which should be added to the :class:`.FSLeyesFrame` *Tools* menu. See also the :meth:`.ActionProvider.getActions` method, which can also be overridden, and controls the actions which get added to the FSLeyes *settings* menu. """ return []
def __auiMgrUpdate(self, *args, **kwargs): """Called whenever a panel is added/removed to/from this ``ViewPanel``. Calls the ``Update`` method on the ``AuiManager`` instance that is managing this panel. :arg newPanel: Must be passed as a keyword argument. When a new panel is added, it should be passed here. """ newPanel = kwargs.pop('newPanel', None) # This method makes sure that size hints # for all existing and new panels are # set on their AuiPaneInfo objects, and # then calls AuiManager.Update. # We first loop through all panels, and # figure out their best sizes. Each entry # in this list is a tuple containing: # # - Panel # - AuiPaneInfo instance # - Dock direction (None for floating panels) # - Layer number (None for floating panels) # - Minimum size bestSizes = [] for panel in self.__panels.values(): if isinstance(panel, ctrlpanel.ControlToolBar): continue pinfo = self.__auiMgr.GetPane(panel) # If the panel is floating, use its # current size as its 'best' size, # as otherwise the AuiManager will # immediately resize the panel to # its best size. if pinfo.IsFloating(): dockDir = None layer = None bestSize = panel.GetSize().Get() # Unless its current size is tiny # (which probably means that it has # just been added) if bestSize[0] <= 20 or \ bestSize[1] <= 20: bestSize = panel.GetBestSize().Get() else: dockDir = pinfo.dock_direction layer = pinfo.dock_layer bestSize = panel.GetBestSize().Get() bestSizes.append((panel, pinfo, dockDir, layer, bestSize)) # Now we loop through one final time, and # set all of the necessary size hints on # the AuiPaneInfo instances. for panel, pinfo, dockDir, layer, bestSize in bestSizes: parent = panel.GetParent() # When a panel is added/removed from the AuiManager, # the position of floating panels seems to get reset # to their original position, when they were created. # Here, we explicitly set the position of each # floating frame, so the AuiManager doesn't move our # windows about the place. if pinfo.IsFloating() and \ isinstance(parent, aui.AuiFloatingFrame): pinfo.FloatingPosition(parent.GetScreenPosition()) # See comments in __init__ about # this silly 'float offset' thing floatSize = (bestSize[0] + self.__floatOffset[0], bestSize[1] + self.__floatOffset[1]) log.debug('New size for panel {} - ' 'best: {}, float: {}'.format( type(panel).__name__, bestSize, floatSize)) pinfo.MinSize( (1, 1)) \ .BestSize( bestSize) \ .FloatingSize(floatSize) \ .Resizable( True) # This is a terrible hack which forces # the AuiManager to grow a dock when a # new panel is added, which is bigger # than the existing dock contents. if panel is newPanel and not pinfo.IsFloating(): docks = aui.FindDocks(self.__auiMgr._docks, dockDir, layer) for d in docks: d.size = 0 self.__auiMgr.Update() def __onPaneClose(self, ev=None, panel=None): """Called when the user closes a control (a.k.a. secondary) panel. Calls the :class:`.ControlPanel.destroy`/:class:`.ControlToolBar.destroy` method on the panel. """ if ev is not None: ev.Skip() panel = ev.GetPane().window # If the user has grouped multiple control panels # into a single tabbed notebook, and then closed # the entire notebook, the AuiManager will generate # a single close event, and will pass us that # notebook. So we have to look in the notebook # to see which control panels were actually closed. if isinstance(panel, wx.lib.agw.aui.AuiNotebook): panels = [panel.GetPage(i) for i in range(panel.GetPageCount())] else: panels = [panel] for panel in list(panels): # note: in theory, all panels should be sub-classes # of ControlPanel/ControlToolBar. But this check is # kept here to support third party scripts which don't # honour the FSLeyes plugin rules. if isinstance(panel, (ctrlpanel.ControlPanel, ctrlpanel.ControlToolBar)): # WTF AUI. Sometimes this method gets called # twice for a panel, the second time with a # reference to a wx._wxpyDeadObject; in such # situations, the Destroy method call below # would result in an exception being raised. if self.__panels.pop(type(panel), None) is None: panels.remove(panel) # calling ControlPanel.destroy() # here - wx.Destroy is done below else: log.debug('Panel closed: %s', type(panel).__name__) panel.destroy() # Destroy all the panels for panel in panels: # Even when the user closes a pane, # AUI does not detach said pane - # we have to do it manually self.__auiMgr.DetachPane(panel) wx.CallAfter(panel.Destroy) # Update interaction profile. We do not # consider multiple tabbed controls here. # If the closed control is associated with # an interaction profile, if no other other # open panels rely on the same destroy the # profile and restore the default profile # # We assume that, if a panel which requires # a custom profile was open, that profile # was active. # # See WTF AUI comment above for reason for # len(panels) guard if len(panels) > 0: profileCls = panels[0].profileCls() closeProfile = True if profileCls is not None: for ctype in self.__panels: cprofileCls = ctype.profileCls() if cprofileCls is not None and \ issubclass(cprofileCls, profileCls): closeProfile = False if closeProfile: log.debug('Panel %s uses a custom interaction profile %s - ' 'deactivating it and restoring default profile', type(panels[0]).__name__, profileCls.__name__) self.__profileManager.deactivateProfile() # Update the view panel layout wx.CallAfter(self.__auiMgrUpdate) def __onPerspectiveChange(self, ev): """Called on ``EVT_AUI_PERSPECTIVE_CHANGED`` events. Re-emits the event via the :meth:`events` notifier, with topic ``'aui_perspective'``. This is performed for the benefit of non-wx entities which need to know about layout changes. """ self.events.notify(topic='aui_perspective')
class MyAuiFloatingFrame(auifm.AuiFloatingFrame): """Here I am monkey patching the ``wx.agw.aui.framemanager.AuiFloatingFrame.__init__`` method. I am doing this because I have observed some strange behaviour when running a remote instance of this application over an SSH/X11 session, with the X11 server (i.e. the local machine) running in OS X. When a combobox is embedded in a floating frame (either a pane or a toolbar), its dropdown list appears underneath the frame, meaning that the user is unable to actually select any items from the list! I have only seen this behaviour when using XQuartz on macOS. Ultimately, this appears to be caused by the ``wx.FRAME_TOOL_WINDOW`` style, as passed to the ``wx.MiniFrame`` constructor (from which the ``AuiFloatingFrame`` class derives). Removing this style flag fixes the problem, so this is exactly what I'm doing. I haven't looked any deeper into the situation. This class also overrieds the ``SetPaneWindow`` method, because under gtk3, the maximum size if a frame musr be set. """ def __init__(self, *args, **kwargs): """My new constructor, which makes sure that the ``FRAME_TOOL_WINDOW`` style is not passed through to the ``AuiFloatingFrame`` constructor """ if 'style' in kwargs: style = kwargs['style'] # This is the default style, as defined # in the AuiFloatingFrame constructor else: style = (wx.FRAME_TOOL_WINDOW | wx.FRAME_FLOAT_ON_PARENT | wx.FRAME_NO_TASKBAR | wx.CLIP_CHILDREN) if fwidgets.inSSHSession(): style &= ~wx.FRAME_TOOL_WINDOW kwargs['style'] = style super().__init__(*args, **kwargs) def SetPaneWindow(self, pane): """Make sure that floated toolbars are sized correctly. """ super().SetPaneWindow(pane) if isinstance(pane.window, ctrlpanel.ControlToolBar): size = self.GetBestSize() self.SetMaxSize(size)
[docs]def _AuiDockingGuide_init(self, *args, **kwargs): """I am also monkey-patching the ``wx.lib.agw.aui.AuiDockingGuide.__init__`` method, because in this instance, when running over SSH/X11, the ``wx.FRAME_TOOL_WINDOW`` style seems to result in the docking guide frames being given title bars, which is quite undesirable. I cannot patch the entire class in the aui package, because it is used as part of a class hierarchy. So I am just patching the method. """ if 'style' in kwargs: style = kwargs['style'] # This is the default style, as defined # in the AuiDockingGuide constructor else: style = (wx.FRAME_TOOL_WINDOW | wx.FRAME_STAY_ON_TOP | wx.FRAME_NO_TASKBAR | wx.NO_BORDER) if fwidgets.inSSHSession(): style &= ~wx.FRAME_TOOL_WINDOW kwargs['style'] = style _AuiDockingGuide_real_init(self, *args, **kwargs)
aui .AuiFloatingFrame = MyAuiFloatingFrame auifm.AuiFloatingFrame = MyAuiFloatingFrame _AuiDockingGuide_real_init = aui.AuiDockingGuide.__init__ aui.AuiDockingGuide.__init__ = _AuiDockingGuide_init