Something went wrong on our end
Forked from
FSL / fslpy
3274 commits behind the upstream repository.
-
Paul McCarthy authored
2. Updated documentation references to __init__ methods, as sphinx doesn't resolve them. 3. Bug fix in featresults module when loading contrast matrix.
Paul McCarthy authored2. Updated documentation references to __init__ methods, as sphinx doesn't resolve them. 3. Bug fix in featresults module when loading contrast matrix.
frame.py 18.09 KiB
#!/usr/bin/env python
#
# frame.py - A wx.Frame which implements a 3D image viewer.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides the :class:`FSLEyesFrame` which is the top level frame
for FSLeyes.
"""
import logging
import collections
import wx
import wx.lib.agw.aui as aui
import fsl.data.strings as strings
import fsl.utils.settings as fslsettings
import views
import actions
import displaycontext
log = logging.getLogger(__name__)
class FSLEyesFrame(wx.Frame):
"""A ``FSLEyesFrame`` is a simple :class:`wx.Frame` which acts as a
container for :class:`.ViewPanel` instances.
A :class:`wx.lib.agw.aui.AuiManager` is used so that ``ViewPanel`` panels
can be dynamically laid out and reconfigured by the user.
**Menus**
The ``FSLEyesFrame`` has three menus:
========== ==============================================================
*File* Global actions, such as adding a new overlay
*View* Options to open a new :class:`.ViewPanel`.
*Settings* Options which are specific to the currently visible
:class:`.ViewPanel` instances. A separate sub-menu is added
for each visible ``ViewPanel``. All ``ViewPanel`` classes
inherit from the :class:`.ActionProvider` class - any actions
that have been defined are added as menu items here.
========== ==============================================================
**Saving/restoring state**
When a ``FSLEyesFrame`` is closed, it saves some display settings so that
they can be restored the next time a ``FSLEyesFrame`` is opened. The
settings are saved using the :class:`~fsl.utils.settings` module.
Currently, only the frame position and size are saved - in the future, I
plan to save and restore the ``ViewPanel`` layout as well.
**Programming interface**
At this stage, ``FSLEyesFrame`` only provides a couple of methods for
programmatically configuring the display:
.. autosummary::
:nosignatures:
getViewPanels
addViewPanel
"""
def __init__(self,
parent,
overlayList,
displayCtx,
restore=True):
"""Create a ``FSLEyesFrame``.
.. note:: The ``restore`` functionality is not currently implemented.
If ``restore=True``, an :class:`.OrthoPanel` is added to
the frame.
:arg parent: The :mod:`wx` parent object.
:arg overlayList: The :class:`.OverlayList`.
:arg displayCtx: The master :class:`.DisplayContext`.
:arg restore: Restores previous saved layout. If ``False``, no
view panels will be displayed.
"""
wx.Frame.__init__(self, parent, title='FSLeyes')
# Default application font - this is
# inherited by all child controls.
font = self.GetFont()
if wx.Platform == '__WXGTK__': font.SetPointSize(8)
else: font.SetPointSize(10)
font.SetWeight(wx.FONTWEIGHT_LIGHT)
self.SetFont(font)
self.__overlayList = overlayList
self.__displayCtx = displayCtx
self.__auiManager = aui.AuiManager(
self,
agwFlags=(aui.AUI_MGR_RECTANGLE_HINT |
aui.AUI_MGR_NO_VENETIAN_BLINDS_FADE |
aui.AUI_MGR_LIVE_RESIZE))
# Keeping track of all open view panels
#
# The __viewPanels dict contains
# {AuiPaneInfo : ViewPanel} mappings
#
# The other dicts contain
# {ViewPanel : something} mappings
#
self.__viewPanels = collections.OrderedDict()
self.__viewPanelDCs = {}
self.__viewPanelMenus = {}
self.__viewPanelIDs = {}
self.__makeMenuBar()
self.__restoreState(restore)
self.__auiManager.Bind(aui.EVT_AUI_PANE_CLOSE, self.__onViewPanelClose)
self .Bind(wx.EVT_CLOSE, self.__onClose)
def getViewPanels(self):
"""Returns a list of all :class:`.ViewPanel` instances that are
currenlty displayed in this ``FSLEyesFrame``.
"""
return self.__viewPanels.values()
def addViewPanel(self, panelCls):
"""Adds a new :class:`.ViewPanel` to the centre of the frame, and a
menu item allowing the user to configure the view.
:arg panelCls: The :class:`.ViewPanel` type to be added.
"""
self.Freeze()
if len(self.__viewPanelIDs) == 0:
panelId = 1
else:
panelId = max(self.__viewPanelIDs.values()) + 1
title = '{} {}'.format(strings.titles[panelCls], panelId)
childDC = displaycontext.DisplayContext(
self.__overlayList,
parent=self.__displayCtx)
# Create a child DisplayContext. The DC
# for the first view panel is synced to
# the master DC, but by default the
# overlay display settings for subsequent
# CanvasPanels are unsynced; other
# ViewPanels (e.g. TimeSeriesPanel) remain
# synced by default.
if panelId > 1 and issubclass(panelCls, views.CanvasPanel):
childDC.syncOverlayDisplay = False
panel = panelCls(
self,
self.__overlayList,
childDC)
log.debug('Created new {} ({}) with DisplayContext {}'.format(
panelCls.__name__,
id(panel),
id(childDC)))
paneInfo = (aui.AuiPaneInfo()
.Name(title)
.Caption(title)
.Dockable()
.CloseButton()
.Resizable()
.DestroyOnClose())
# When there is only one view panel
# displayed, the AuiManager seems to
# have trouble drawing the caption
# bar - it is drawn, but then the
# panel is drawn over the top of it.
# So if we only have one panel, we
# hide the caption bar
if panelId == 1:
paneInfo.Centre().CaptionVisible(False)
# But then re-show it when another
# panel is added. The __viewPanels
# dict is an OrderedDict, so the
# first key is the AuiPaneInfo of
# the first panel that was added.
else:
self.__viewPanels.keys()[0].CaptionVisible(True)
# If this is not the first view panel,
# give it a sensible initial size.
if panelId > 1:
width, height = self.GetClientSize().Get()
# PlotPanels are initially
# placed along the bottom
if isinstance(panel, views.PlotPanel):
paneInfo.Bottom().BestSize(-1, height / 3)
# Other panels (e.g. CanvasPanels)
# are placed on the right
else:
paneInfo.Right().BestSize(width / 3, -1)
self.__viewPanels[ paneInfo] = panel
self.__viewPanelDCs[panel] = childDC
self.__viewPanelIDs[panel] = panelId
self.__auiManager.AddPane(panel, paneInfo)
self.__addViewPanelMenu( panel, title)
self.__auiManager.Update()
self.Thaw()
def __addViewPanelMenu(self, panel, title):
"""Called by :meth:`addViewPanel`. Adds a menu item for the newly
created :class:`.ViewPanel` instance.
:arg panel: The newly created ``ViewPanel`` instance.
:arg title: The name given to the ``panel``.
"""
actionz = panel.getActions()
if len(actionz) == 0:
return
menu = wx.Menu()
submenu = self.__settingsMenu.AppendSubMenu(menu, title)
self.__viewPanelMenus[panel] = submenu
for actionName, actionObj in actionz.items():
menuItem = menu.Append(
wx.ID_ANY,
strings.actions[panel, actionName])
actionObj.bindToWidget(self, wx.EVT_MENU, menuItem)
# Add a 'Close' action to
# the menu for every panel
def closeViewPanel(ev):
paneInfo = self.__auiManager.GetPane(panel)
self.__onViewPanelClose( paneInfo=paneInfo)
self.__auiManager.ClosePane(paneInfo)
self.__auiManager.Update()
closeItem = menu.Append(
wx.ID_ANY,
strings.actions[self, 'closeViewPanel'])
self.Bind(wx.EVT_MENU, closeViewPanel, closeItem)
def __onViewPanelClose(self, ev=None, paneInfo=None):
"""Called when the user closes a :class:`.ViewPanel`.
The :meth:`__addViewPanelMenu` method adds a *Close* menu item
for every view panel, and binds it to this method.
This method does the following:
1. Makes sure that the ``ViewPanel``: is destroyed correctly
2. Removes the *Settings* sub-menu corresponding to the ``ViewPanel``.
3. Makes sure that any remaining ``ViewPanel`` panels are arranged
nicely.
"""
if ev is not None:
ev.Skip()
paneInfo = ev.GetPane()
panel = self .__viewPanels.pop(paneInfo, None)
if panel is None:
return
self .__viewPanelIDs .pop(panel)
dctx = self.__viewPanelDCs .pop(panel)
menu = self.__viewPanelMenus.pop(panel, None)
log.debug('Destroying {} ({}) and '
'associated DisplayContext ({})'.format(
type(panel).__name__,
id(panel),
id(dctx)))
# Unbind view panel menu
# items, and remove the menu
for actionName, actionObj in panel.getActions().items():
actionObj.unbindAllWidgets()
if menu is not None:
self.__settingsMenu.Remove(menu.GetId())
# Calling fslpanel.FSLEyesPanel.destroy()
# and DisplayContext.destroy() - the
# AUIManager should do the
# wx.Window.Destroy side of things ...
panel.destroy()
dctx .destroy()
# If the removed panel was the centre
# pane, or if there is only one panel
# left, move another panel to the centre
numPanels = len(self.__viewPanels)
wasCentre = paneInfo.dock_direction_get() == aui.AUI_DOCK_CENTRE
if numPanels == 1 or (numPanels > 0 and wasCentre):
paneInfo = self.__viewPanels.keys()[0]
paneInfo.Centre().CaptionVisible(False)
def __onClose(self, ev):
"""Called when the user closes this ``FSLEyesFrame``.
Saves the frame position, size, and layout, so it may be preserved the
next time it is opened. See the :meth:`_restoreState` method.
"""
ev.Skip()
size = self.GetSize().Get()
position = self.GetScreenPosition().Get()
fslsettings.write('framesize', str(size))
fslsettings.write('frameposition', str(position))
# It's nice to explicitly clean
# up our FSLEyesPanels, otherwise
# they'll probably complain
for panel in self.__viewPanels.values():
panel.destroy()
def __parseSavedSize(self, size):
"""Parses the given string, which is assumed to contain a size tuple.
"""
try: return tuple(map(int, size[1:-1].split(',')))
except: return None
def __parseSavedPoint(self, size):
"""A proxy for the :meth:`__parseSavedSize` method."""
return self.__parseSavedSize(size)
def __parseSavedLayout(self, layout):
"""Parses the given string, which is assumed to contain an encoded
:class:`.AuiManager` perspective (see
:meth:`.AuiManager.SavePerspective`).
Returns a list of class names, specifying the control panels
(e.g. :class:`.OverlayListPanel`) which were previously open, and need
to be created.
.. warning:: This method is not currently being used - it is from a
previous version. I may use it in the future to restore
``ViewPanel`` layouts, or I may re-write it.
"""
try:
names = []
sections = layout.split('|')[1:]
for section in sections:
if section.strip() == '': continue
attrs = section.split(';')
attrs = dict([tuple(nvpair.split('=')) for nvpair in attrs])
if 'name' in attrs:
names.append(attrs['name'])
return names
except:
return []
def __restoreState(self, restore=True):
"""Called by :meth:`__init__`.
If any frame size/layout properties have previously been saved via the
:mod:`~fsl.utils.settings` module, they are read in, and applied to
this frame.
:arg bool default: If ``True``, any saved state is ignored.
"""
from operator import itemgetter as iget
# Restore the saved frame size/position
size = self.__parseSavedSize(
fslsettings.read('framesize'))
position = self.__parseSavedPoint(
fslsettings.read('frameposition'))
if (size is not None) and (position is not None):
# Turn the saved size/pos into
# a (tlx, tly, brx, bry) tuple
frameRect = [position[0],
position[1],
position[0] + size[0],
position[1] + size[1]]
# Now make a bounding box containing the
# space made up of all available displays.
# Get the bounding rectangles of each
# display, and change them from
# (x, y, w, h) into (tlx, tly, brx, bry).
displays = [wx.Display(i) for i in range(wx.Display.GetCount())]
dispRects = [d.GetGeometry() for d in displays]
dispRects = [[d.GetTopLeft()[ 0],
d.GetTopLeft()[ 1],
d.GetBottomRight()[0],
d.GetBottomRight()[1]] for d in dispRects]
# get the union of these display
# rectangles (tlx, tly, brx, bry)
dispRect = [min(dispRects, key=iget(0))[0],
min(dispRects, key=iget(1))[1],
max(dispRects, key=iget(2))[2],
max(dispRects, key=iget(3))[3]]
# Now we have our two rectangles - the
# rectangle of our saved frame position,
# and the rectangle of the available
# display space.
# Calculate the area of intersection
# betwen the two rectangles, and the
# area of our saved frame position
xOverlap = max(0, min(frameRect[2], dispRect[2]) -
max(frameRect[0], dispRect[0]))
yOverlap = max(0, min(frameRect[3], dispRect[3]) -
max(frameRect[1], dispRect[1]))
intArea = xOverlap * yOverlap
frameArea = ((frameRect[2] - frameRect[0]) *
(frameRect[3] - frameRect[1]))
# If the ratio of (frame-display intersection) to
# (saved frame position) is 'not decent', then
# forget it, and use a default frame position/size
ratio = intArea / float(frameArea)
if ratio < 0.5:
log.debug('Intersection of saved frame area with available '
'display area is too small ({}) - reverting to '
'default frame size/position'.format(ratio))
size = None
position = None
if size is not None:
log.debug('Restoring previous size: {}'.format(size))
self.SetSize(size)
else:
# Default size is 90% of
# the first display size
size = list(wx.Display(0).GetGeometry().GetSize())
size[0] *= 0.9
size[1] *= 0.9
log.debug('Setting default frame size: {}'.format(size))
self.SetSize(size)
if position is not None:
log.debug('Restoring previous position: {}'.format(position))
self.SetPosition(position)
else:
self.Centre()
# TODO Restore the previous view panel layout.
# Currently, we just display an OrthoPanel.
if restore:
self.addViewPanel(views.OrthoPanel)
viewPanel = self.getViewPanels()[0]
# Set up a default for ortho views
# layout (this will hopefully eventually
# be restored from a saved state)
import fsl.fsleyes.controls.overlaylistpanel as olp
import fsl.fsleyes.controls.locationpanel as lop
viewPanel.togglePanel(olp.OverlayListPanel)
viewPanel.togglePanel(lop.LocationPanel)
def __makeMenuBar(self):
"""Constructs a bunch of menu items for this ``FSLEyesFrame``."""
menuBar = wx.MenuBar()
self.SetMenuBar(menuBar)
fileMenu = wx.Menu()
viewMenu = wx.Menu()
settingsMenu = wx.Menu()
menuBar.Append(fileMenu, 'File')
menuBar.Append(viewMenu, 'View')
menuBar.Append(settingsMenu, 'Settings')
self.__fileMenu = fileMenu
self.__viewMenu = viewMenu
self.__settingsMenu = settingsMenu
viewPanels = views .listViewPanels()
actionz = actions .listGlobalActions()
for action in actionz:
menuItem = fileMenu.Append(wx.ID_ANY, strings.actions[action])
actionObj = action(self.__overlayList, self.__displayCtx)
actionObj.bindToWidget(self, wx.EVT_MENU, menuItem)
for viewPanel in viewPanels:
viewAction = viewMenu.Append(wx.ID_ANY, strings.titles[viewPanel])
self.Bind(wx.EVT_MENU,
lambda ev, vp=viewPanel: self.addViewPanel(vp),
viewAction)