Source code for fsleyes.toolbar

#
# toolbar.py - FSLeyes toolbars
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides the :class:`FSLeyesToolBar` class, the base class
for all toolbars in *FSLeyes*.
"""

import logging

import wx
import wx.lib.newevent as wxevent

import numpy as np

import fsleyes.panel as fslpanel
import fsleyes.icons as icons


log = logging.getLogger(__name__)


[docs]class FSLeyesToolBar(fslpanel.FSLeyesPanel): """Base class for all *FSLeyes* toolbars. The ``FSLeyesToolBar`` is a regular :class:`wx.PyPanel` which to which a group of *tools* can be added, where a tool may be any ``wx`` control. See also the :class:`.ControlToolBar`, which is the true base-class for all toolbars that are added to FSLeyes view panels. Tools can be added to a ``FSLeyesToolBar`` with the following methods: .. autosummary:: :nosignatures: AddTool InsertTool InsertTools SetTools MakeLabelledTool When the horizontal size of a ``FSLeyesToolBar`` becomes too small to display all of its tools, the toolbar is compressed: some tools are hidden, and buttons are displayed on each end of the toolbar, allowing the user to scroll through the toolbar, to access the hidden tools. The user may also use the mouse wheel to scroll through the toolbar. A collapsed ``FSLeyesToolBar`` looks something like this: .. image:: images/fsleyestoolbar.png :scale: 50% :align: center """
[docs] def __init__(self, parent, overlayList, displayCtx, viewPanel, height=32, orient=wx.HORIZONTAL, *args, **kwargs): """Create a ``FSLeyesToolBar``. :arg parent: The :mod:`wx` parent object. :arg overlayList: The :class:`.OverlayList`, containing all overlays being displayed. :arg displayCtx: A :class:`.DisplayContext`, which defines how the overlays are to be displayed. :arg viewPanel: The :class:`.ViewPanel` that owns this toolbar. :arg height: Desired toolbar height in pixels. This value is used to look up appropriately sized left/right arrow icons. :arg actionz: A dictionary of actions passed through to the :meth:`.ActionProvider.__init__`. All other arguments are passed through to :meth:`.FSLeyesPanel.__init__`. """ if orient not in (wx.HORIZONTAL, wx.VERTICAL): raise ValueError('Invalid orientation: {}'.format(orient)) fslpanel.FSLeyesPanel.__init__(self, parent, overlayList, displayCtx, viewPanel.frame, *args, **kwargs) self.__tools = [] self.__visibleOffset = 0 self.__numVisible = 0 self.__height = height self.__orient = orient font = self.GetFont() self.SetFont(font.Smaller()) style = wx.BU_EXACTFIT | wx.BU_NOTEXT if orient == wx.HORIZONTAL: lBmp = icons.loadBitmap('thinLeftArrow{}' .format(height)) rBmp = icons.loadBitmap('thinRightArrow{}'.format(height)) else: lBmp = icons.loadBitmap('thinUpArrow{}' .format(height)) rBmp = icons.loadBitmap('thinDownArrow{}'.format(height)) self.__leftButton = wx.Button(self, style=style) self.__rightButton = wx.Button(self, style=style) self.__leftButton .SetBitmap(lBmp) self.__rightButton.SetBitmap(rBmp) for btn in [self.__leftButton, self.__rightButton]: size = btn.GetBestSize() btn.SetMinSize(size) self.__sizer = wx.BoxSizer(orient) self.SetSizer(self.__sizer) self.__sizer.Add(self.__leftButton, flag=wx.EXPAND) self.__sizer.Add((0, 0), flag=wx.EXPAND, proportion=1) self.__sizer.Add(self.__rightButton, flag=wx.EXPAND) self.__leftButton .Bind(wx.EVT_BUTTON, self.__onLeftButton) self.__rightButton.Bind(wx.EVT_BUTTON, self.__onRightButton) self .Bind(wx.EVT_MOUSEWHEEL, self.__onMouseWheel) self .Bind(wx.EVT_SIZE, self.__drawToolBar)
[docs] def GetOrient(self): """Returns the orientation of this ``FSLeyesToolBar``, either ``wx.HORIZONTAL`` or ``wx.VERTICAL``. """ return self.__orient
[docs] def MakeLabelledTool(self, tool, labelText, labelSide=wx.TOP, expand=False): """Creates a panel containing the given tool, and a label for the tool. The panel is returned, but is not added to this ``FSLeyesToolBar`` - you will have to do that yourself, e.g.:: labelledTool = toolbar.MakeLabelledTool(tool, 'Label', wx.BOTTOM) toolbar.AddTool(labelledTool) :arg tool: A :mod:`wx` control. :arg labelText: A label for the tool. :arg labelSide: Which side of the tool to put the label - ``wx.TOP``, ``wx.BOTTOM``, ``wx.LEFT``, or ``wx.RIGHT``. :arg expand: Defaults to ``False``. If ``True``, the widget and label will be set up so they expand to fit all available space """ if labelSide in (wx.TOP, wx.BOTTOM): orient = wx.VERTICAL elif labelSide in (wx.LEFT, wx.RIGHT): orient = wx.HORIZONTAL oldParent = tool.GetParent() panel = wx.Panel(oldParent) sizer = wx.BoxSizer(orient) panel.SetSizer(sizer) tool.Reparent(panel) label = wx.StaticText(panel, style=wx.ALIGN_CENTRE_HORIZONTAL) label.SetLabel(labelText) if expand: sizerArgs = { 'flag' : wx.EXPAND, 'proportion' : 1 } else: sizerArgs = { 'flag' : wx.ALIGN_CENTRE, } if labelSide in (wx.TOP, wx.LEFT): sizer.Add(label, **sizerArgs) sizer.Add(tool, **sizerArgs) else: sizer.Add(tool, **sizerArgs) sizer.Add(label, **sizerArgs) return panel
[docs] def Enable(self, *args, **kwargs): """Enables/disables all tools in this ``FSLeyesToolBar``. :arg args: Passed to the ``Enable`` method of each tool. :arg kwargs: Passed to the ``Enable`` method of each tool. """ super(FSLeyesToolBar, self).Enable(*args, **kwargs) for t in self.__tools: t.Enable(*args, **kwargs)
[docs] def GetTools(self): """Returns a list containing all tools in this ``FSLeyesToolBar``. """ return self.__tools[:]
[docs] def GetToolCount(self): """Returns the number of tools in this ``FSLeyesToolBar``. """ return len(self.__tools)
[docs] def AddDivider(self): """Adds a :class:`.ToolBarDivider` to the end of the toolbar. """ self.InsertDivider()
[docs] def InsertDivider(self, index=None): """Inserts a :class:`.ToolBarDivider` into the toolbar at the specified ``index``. """ if self.__orient == wx.VERTICAL: orient = wx.HORIZONTAL elif self.__orient == wx.HORIZONTAL: orient = wx.VERTICAL self.InsertTool(ToolBarDivider(self, self.__height, orient), index)
[docs] def AddTool(self, tool): """Adds the given tool to this ``FSLeyesToolBar``. """ self.InsertTool(tool)
[docs] def InsertTools(self, tools, index=None): """Inserts the given sequence of tools into this ``FSLeyesToolBar``, at the specified index. :arg tools: A sequence of tools to add. :arg index: Insert the tools before this index (default: end). """ if index is None: index = self.GetToolCount() for i, tool in enumerate(tools, index): self.InsertTool(tool, i, postevent=False) wx.PostEvent(self, ToolBarEvent())
[docs] def SetTools(self, tools, destroy=False): """Replaces all of the existing tools in this ``FSLeyesToolBar`` with the given sequence of tools. :arg tools: Sequence of new tools to add. :arg destroy: If ``True`` all of the old tools are destroyed. """ self.ClearTools(destroy, postevent=False) for tool in tools: self.InsertTool(tool, postevent=False, redraw=False) self.__drawToolBar() wx.PostEvent(self, ToolBarEvent())
[docs] def InsertTool(self, tool, index=None, postevent=True, redraw=True): """Inserts the given tool into this ``FSLeyesToolBar``, at the specified index. :arg tool: The tool to insert. :arg index: Index to insert the tool. :arg postevent: If ``True``, a :data:`ToolBarEvent` will be generated. Pass ``False`` to suppress this event. :arg redraw: If ``True``, the toolbar is redrawn. Pass ``False`` to suppress this behaviour. """ if index is None: index = len(self.__tools) log.debug('{}: adding tool at index {}: {}'.format( type(self).__name__, index, type(tool).__name__)) tool.Bind(wx.EVT_MOUSEWHEEL, self.__onMouseWheel) # gtk3: something somewhere sometimes # clobbers the best size, so widgets # don't get shown. Only observed with # BitmapToggleButtons. size = tool.GetBestSize() tool.SetMinSize(size) tool.SetMaxSize(size) self.__tools.insert(index, tool) self.__sizer.Insert(index + 1, tool, flag=wx.ALIGN_CENTRE) self.InvalidateBestSize() if redraw: self.__drawToolBar() if postevent: wx.PostEvent(self, ToolBarEvent())
[docs] def DoGetBestSize(self): """Calculates and returns the best size for this toolbar, simply the minimum size that will fit all tools. This method is called by :mod:`wx` when this toolbar is laid out. """ # Calculate the minimum/maximum size # for this toolbar, given the addition # of the new tool. If the orientation # of this toolbar (set in __init__) is # HORIZONTAL, the ttlSpace is used to # store total width, otherwise it is # used to store total height. ttlSpace = 0 minWidth = 0 minHeight = 0 for tool in self.__tools: tw, th = tool.GetBestSize().Get() if tw > minWidth: minWidth = tw if th > minHeight: minHeight = th if self.__orient == wx.HORIZONTAL: ttlSpace += tw else: ttlSpace += th if self.__orient == wx.HORIZONTAL: leftWidth = self.__leftButton .GetBestSize().GetWidth() rightWidth = self.__rightButton.GetBestSize().GetWidth() minWidth = minWidth + leftWidth + rightWidth else: topHeight = self.__leftButton .GetBestSize().GetHeight() bottomHeight = self.__rightButton.GetBestSize().GetHeight() minHeight = minHeight + topHeight + bottomHeight if self.__orient == wx.HORIZONTAL: size = (ttlSpace, minHeight) else: size = (minWidth, ttlSpace) # The agw.AuiManager does not honour the best size when # toolbars are floated, but it does honour the minimum # size. So I'm just setting the minimum size to the best # size. log.debug('Setting toolbar size: {}'.format(size)) self.SetMinSize( size) self.SetMaxSize( size) self.CacheBestSize(size) return size
[docs] def ClearTools( self, destroy=False, startIdx=None, endIdx=None, postevent=True): """Removes all tools, or a range of tools, from this ``FSLeyesToolBar``. :arg destroy: If ``True``, the removed tools are destroyed. :arg startIdx: Start index of tools to remove. If not provided, defaults to 0. :arg endIdx: End index of tools to remove (exclusive). If not provided, defaults to :meth:`GetToolCount()`. :arg postevent: If ``True``, a :data:`ToolBarEvent` will be generated. Set to ``False`` to suppress the event. """ if len(self.__tools) == 0: return if startIdx is None: startIdx = 0 if endIdx is None: endIdx = len(self.__tools) for i in range(startIdx, endIdx): tool = self.__tools[i] self.__sizer.Detach(tool) if destroy: tool.Destroy() self.__tools[startIdx:endIdx] = [] self.InvalidateBestSize() self.Layout() if postevent: wx.PostEvent(self, ToolBarEvent())
def __onMouseWheel(self, ev): """Called when the mouse wheel is rotated on this ``FSLeyesToolBar``. Calls :meth:`__onLeftButton` or :meth:`__onRightButton`, depending on the rotation direction. """ wheelDir = ev.GetWheelRotation() if wheelDir < 0: self.__onRightButton() elif wheelDir > 0: self.__onLeftButton() def __onLeftButton(self, ev=None): """Called when the left toolbar button is pressed. If the toolbar is compressed, it is scrolled to the left. """ self.__visibleOffset -= 1 if self.__visibleOffset <= 0: self.__visibleOffset = 0 log.debug('Left button pushed - setting start ' 'tool index to {}'.format(self.__visibleOffset)) self.__drawToolBar() def __onRightButton(self, ev=None): """Called when the right toolbar button is pressed. If the toolbar is compressed, it is scrolled to the right. """ self.__visibleOffset += 1 if self.__visibleOffset + self.__numVisible >= len(self.__tools): self.__visibleOffset = len(self.__tools) - self.__numVisible log.debug('Right button pushed - setting start ' 'tool index to {}'.format(self.__visibleOffset)) self.__drawToolBar() def __drawToolBar(self, *a): """Draws this ``FSLeyesToolBar``. If the toolbar is big enough, all tools are drawn. Otherwise, the method figures out out how many tools can be drawn, and which tools to draw, given the current size. """ sizer = self.__sizer tools = self.__tools orient = self.__orient lbtn = self.__leftButton rbtn = self.__rightButton if orient == wx.HORIZONTAL: availSpace = self.GetSize().GetWidth() reqdSpace = [tool.GetBestSize().GetWidth() for tool in tools] leftSpace = lbtn .GetBestSize().GetWidth() rightSpace = rbtn .GetBestSize().GetWidth() else: availSpace = self.GetSize().GetHeight() reqdSpace = [tool.GetBestSize().GetHeight() for tool in tools] leftSpace = lbtn .GetBestSize().GetHeight() rightSpace = rbtn .GetBestSize().GetHeight() enoughSpace = availSpace >= sum(reqdSpace) sizer.Show(lbtn, not enoughSpace) sizer.Show(rbtn, not enoughSpace) # show all tools if enoughSpace: log.debug('{}: All tools fit ({} >= {})'.format( type(self).__name__, availSpace, sum(reqdSpace))) self.__visibleOffset = 0 self.__numVisible = len(tools) for tool in tools: sizer.Show(tool) # show <numVisible> tools, starting from <visibleOffset> # (see __onMouseWheel/__onLeftButton/__onRightButton) else: reqdSpace = reqdSpace[self.__visibleOffset:] cumSpace = np.cumsum(reqdSpace) + leftSpace + rightSpace biggerIdxs = [int(i) for i in np.where(cumSpace > availSpace)[0]] if len(biggerIdxs) == 0: lastIdx = len(tools) else: lastIdx = biggerIdxs[0] + self.__visibleOffset self.__numVisible = lastIdx - self.__visibleOffset log.debug('{}: {} tools fit ({} - {})'.format( type(self).__name__, self.__numVisible, self.__visibleOffset, lastIdx)) lbtn.Enable(self.__visibleOffset > 0) rbtn.Enable(lastIdx < len(tools)) for i in range(len(tools)): sizer.Show(tools[i], self.__visibleOffset <= i < lastIdx) self.Layout()
_ToolBarEvent, _EVT_TOOLBAR_EVENT = wxevent.NewEvent() EVT_TOOLBAR_EVENT = _EVT_TOOLBAR_EVENT """Identifier for the :data:`ToolBarEvent` event. """ ToolBarEvent = _ToolBarEvent """Event emitted when one or more tools is/are added/removed to/from a :class:`FSLeyesToolBar`. """
[docs]class ToolBarDivider(wx.Panel): """An empty ``wx.Panel`` intended to be used for dividing space in a :class:`FSLeyesToolBar`. """
[docs] def __init__(self, parent, width=10, height=32, orient=wx.VERTICAL): wx.Panel.__init__(self, parent) if orient == wx.VERTICAL: size = (width, height) elif orient == wx.HORIZONTAL: size = (height, width) self.SetMinSize(size) self.SetMaxSize(size)