Source code for fsleyes_widgets.widgetlist

#!/usr/bin/env python
#
# widgetlist.py - A widget which displays a list of groupable widgets.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides the :class:`WidgetList` class, which displays a list
of widgets.
"""


import wx
import wx.lib.newevent      as wxevent
import wx.lib.scrolledpanel as scrolledpanel

import fsleyes_widgets.togglepanel as togglepanel


[docs] class WidgetList(scrolledpanel.ScrolledPanel): """A scrollable list of widgets. The ``WidgetList`` provides a number of features: - Widgets can be grouped. - A label can be shown next to each widget. - Widget groups can be collapsed/expanded. - Widgets and groups can be dynamically added/removed. The most important methods are: .. autosummary:: :nosignatures: AddWidget AddGroup A ``WidgetList`` looks something like this: .. image:: images/widgetlist.png :scale: 50% :align: center A ``WidgetList`` emits a :data:`WidgetListChangeEvent` whenever its contents change. """ _defaultOddColour = None """Background colour for widgets on odd rows. Iniitalised in :meth:`__init__`. """ _defaultEvenColour = None """Background colour for widgets on even rows. Iniitalised in :meth:`__init__`. """ _defaultGroupColour = None """Border and title background colour for widget groups. Iniitalised in :meth:`__init__`. """ def __init__(self, parent, style=0, minHeight=-1): """Create a ``WidgetList``. :arg parent: The :mod:`wx` parent object. :arg style: Passed through to ``wx.ScrolledPanel.__init__`` :arg minHeight: Minimum height of each row """ odd = wx.SystemSettings.GetColour(wx.SYS_COLOUR_LISTBOX) even = odd.ChangeLightness(90) group = odd if WidgetList._defaultOddColour is None: WidgetList._defaultOddColour = odd if WidgetList._defaultEvenColour is None: WidgetList._defaultEvenColour = even if WidgetList._defaultGroupColour is None: WidgetList._defaultGroupColour = group self.__minHeight = minHeight self.__widgSizer = wx.BoxSizer(wx.VERTICAL) self.__sizer = wx.BoxSizer(wx.VERTICAL) self.__groupSizer = wx.BoxSizer(wx.VERTICAL) self.__widgets = {} self.__groups = {} self.__oddColour = WidgetList._defaultOddColour self.__evenColour = WidgetList._defaultEvenColour self.__groupColour = WidgetList._defaultGroupColour self.__sizer.Add(self.__widgSizer, flag=wx.EXPAND) self.__sizer.Add(self.__groupSizer, flag=wx.EXPAND) self.__oneExpanded = style & WL_ONE_EXPANDED # The SP.__init__ method seemingly # induces a call to DoGetBestSize, # which assumes that all of the # things above exist. So we call # init after we've created those # things. scrolledpanel.ScrolledPanel.__init__(self, parent) self.SetSizer(self.__sizer) self.SetupScrolling() self.SetAutoLayout(1)
[docs] def DoGetBestSize(self): """Returns the best size for the widget list, with all group widgets expanded. """ width, height = self.__widgSizer.GetSize().Get() for name, group in self.__groups.items(): w, h = group.parentPanel.GetBestSize().Get() w += 20 h += 10 if w > width: width = w height += h return wx.Size(width, height)
def __makeWidgetKey(self, widget): """Widgets are stored in a dictionary - this method generates a string to use as a key, based on the widget ``id``. """ return str(id(widget)) def __setLabelWidths(self, widgets): """Calculates the maximum width of all widget labels, and sets all labels to that width. This ensures that all labels/widgets line are horizontally aligned. """ if len(widgets) == 0: return dc = wx.ClientDC(widgets[0].label) lblWidths = [dc.GetTextExtent(w.displayName)[0] for w in widgets] maxWidth = max(lblWidths) for w in widgets: w.label.SetMinSize((maxWidth + 10, -1)) w.label.SetMaxSize((maxWidth + 10, -1)) def __setColours(self): """Called whenever the widget list needs to be refreshed. Makes sure that odd/even widgets and their labels have the correct background colour. """ def setWidgetColours(widgDict): for i, widg in enumerate(widgDict.values()): if i % 2: colour = self.__oddColour else: colour = self.__evenColour widg.SetBackgroundColour(colour) setWidgetColours(self.__widgets) for group in self.__groups.values(): setWidgetColours(group.widgets) group.parentPanel.SetBackgroundColour(self.__groupColour) group.colPanel .SetBackgroundColour(self.__groupColour) def __refresh(self, *args, **kwargs): """Updates widget colours (see :meth:`__setColours`), and lays out the widget list. :arg postEvent: If ``True`` (the default), a :data:`WidgetListChangeEvent` is posted. """ self.__setColours() self.FitInside() self.Layout() if kwargs.get('postEvent', True): wx.PostEvent(self, WidgetListChangeEvent())
[docs] def SetColours(self, odd=None, even=None, group=None): """Sets the colours used on this ``WidgetList``. Each argument is assumed to be a tuple of ``(r, g, b)`` values, each in the range ``[0 - 255]``. :arg odd: Background colour for widgets on odd rows. :arg even: Background colour for widgets on even rows. :arg group: Border/title colour for widget groups. """ if odd is not None: self.__oddColour = odd if even is not None: self.__evenColour = even if group is not None: self.__groupColour = group self.__setColours()
[docs] def GetGroups(self): """Returns a list containing the name of every group in this ``WidgetList``. """ return list(self.__groups.keys())
[docs] def HasGroup(self, groupName): """Returns ``True`` if this ``WidgetList`` contains a group with the specified name. """ return groupName in self.__groups
[docs] def RenameGroup(self, groupName, newDisplayName): """Changes the display name of the specified group. .. note:: This method only changes the *display name* of a group, not the group identifier name. See the :meth:`AddGroup` method. :arg groupName: Name of the group. :arg newDisplayName: New display name for the group. """ group = self.__groups[groupName] group.displayName = newDisplayName group.colPanel.SetLabel(newDisplayName)
[docs] def AddGroup(self, groupName, displayName=None): """Add a new group to this ``WidgetList``. A :exc:`ValueError` is raised if a group with the specified name already exists. :arg groupName: The name of the group - this is used as an identifier for the group. :arg displayName: A string to be shown in the title bar for the group. This can be changed later via the :meth:`RenameGroup` method. """ if displayName is None: displayName = groupName if groupName in self.__groups: raise ValueError('A group with name {} ' 'already exists'.format(groupName)) parentPanel = wx.Panel(self, style=wx.SUNKEN_BORDER) colPanel = togglepanel.TogglePanel(parentPanel, label=displayName) widgPanel = colPanel.GetPane() widgSizer = wx.BoxSizer(wx.VERTICAL) widgPanel.SetSizer(widgSizer) gapSizer = wx.BoxSizer(wx.VERTICAL) # A spacer exists at the top, # and between, every group. gapSizer.Add((-1, 5)) gapSizer.Add(parentPanel, border=10, flag=(wx.EXPAND | wx.LEFT | wx.RIGHT)) self.__groupSizer.Add(gapSizer, flag=wx.EXPAND) parentSizer = wx.BoxSizer(wx.VERTICAL) parentSizer.Add(colPanel, border=5, flag=wx.EXPAND | wx.BOTTOM, proportion=0) parentPanel.SetSizer(parentSizer) group = _Group(groupName, displayName, gapSizer, parentPanel, colPanel, widgPanel, widgSizer) self.__groups[groupName] = group self.__refresh() # Mouse wheel listener needed # on all children under linux/GTK if wx.Platform == '__WXGTK__': parentPanel.Bind(wx.EVT_MOUSEWHEEL, self.__onMouseWheel) colPanel .Bind(wx.EVT_MOUSEWHEEL, self.__onMouseWheel) colPanel.Bind(togglepanel.EVT_TOGGLEPANEL_EVENT, self.__onGroupExpand)
[docs] def GetWidgets(self, groupName=None): """Returns a list containing all of the widgets that have been added to this ``WidgetList``. :arg groupName: If provided, only widgets in the specified group will be returned. Otherwise, ungrouped widgets are returned. """ if groupName is None: widgDict = self.__widgets else: widgDict = self.__groups[groupName].widgets widgets = [w.widget for w in widgDict.values()] return widgets
[docs] def AddWidget(self, widget, displayName, tooltip=None, groupName=None): """Add an arbitrary widget to the property list. If the ``groupName`` is not provided, the widget is added to a list of *top level* widgets, which appear at the top of the list, above any groups. Otherwise, the widget is added to the collapsible panel corresponding to the specified group. A :exc:`ValueError` is raised if the widget is already contained in the list. :arg widget: The widget to add to the list. :arg displayName: The widget label/display name. :arg tooltip: A tooltip for the widget. :arg groupName: Name of the group to which the widget should be added. .. note:: The provided ``widget`` may also be a :class:`wx.Sizer` instances, although support for this is basic. Specifically, only one level of nesting is possible, i.e. the provided ``wx.Sizer`` may not have any other ``wx.Sizer`` instances as its children. """ if groupName is None: widgDict = self.__widgets parent = self parentSizer = self.__widgSizer else: group = self.__groups[groupName] widgDict = group.widgets parent = group.widgPanel parentSizer = group.sizer key = self.__makeWidgetKey(widget) if key in widgDict: raise ValueError('Widgets {} already exist'.format(key)) widgPanel = wx.Panel(parent) widgSizer = wx.BoxSizer(wx.HORIZONTAL) widgPanel.SetSizer(widgSizer) if isinstance(widget, wx.Sizer): for child in widget.GetChildren(): window = child.GetWindow() if window is not None: window.Reparent(widgPanel) else: w, h = widget.GetBestSize().Get() if self.__minHeight > h: h = self.__minHeight widget.SetMinSize( (w, h)) widget.Reparent(widgPanel) label = wx.StaticText(widgPanel, label=displayName, style=wx.ALIGN_RIGHT) widgSizer.Add(label, flag=wx.EXPAND) widgSizer.Add(widget, flag=wx.EXPAND, proportion=1) parentSizer.Add(widgPanel, flag=wx.EXPAND | wx.LEFT | wx.RIGHT, border=5) widg = _Widget(displayName, tooltip, label, widget, widgPanel, widgSizer) if tooltip is not None: widg.SetTooltip(tooltip) # Under linux/GTK, mouse events are # captured by child windows, so if # we want scrolling to work, we need # to capture scroll events on every # child. Under OSX/cocoa, this is # not necessary. if wx.Platform == '__WXGTK__': widg.Bind(wx.EVT_MOUSEWHEEL, self.__onMouseWheel) widgDict[key] = widg self.__setLabelWidths(list(widgDict.values())) self.__refresh()
def __onMouseWheel(self, ev): """Only called if running on GTK. Scrolls the widget list according to the mouse wheel rotation. """ posx, posy = self.GetViewStart() rotation = ev.GetWheelRotation() if rotation > 0: delta = 5 elif rotation < 0: delta = -5 else: return if ev.GetWheelAxis() == wx.MOUSE_WHEEL_VERTICAL: posy -= delta else: posx += delta self.Scroll(posx, posy) def __onGroupExpand(self, ev): """Called when the user expands or collapses a group. Enforces the :data:`WL_ONE_EXPANDED` style if it is enabled, and refreshes the panel. """ panel = ev.GetEventObject() if panel.IsExpanded() and self.__oneExpanded: for group in self.__groups.values(): if group.colPanel is not panel: group.colPanel.Collapse() wx.PostEvent(self, WidgetListExpandEvent()) self.__refresh()
[docs] def AddSpace(self, groupName=None): """Adds some empty vertical space to the widget list. :arg groupName: Name of the group tio which the space should be added. If not specified, the space is added to the *top level* widget list - see the :meth:`AddWidget` method. """ if groupName is None: parentSizer = self.__widgSizer else: parentSizer = self.__groups[groupName].sizer parentSizer.Add((-1, 10))
[docs] def RemoveWidget(self, widget, groupName=None): """Removes and destroys the specified widget from this ``WidgetList``. :arg widget: The widget to remove. :arg groupName: Name of the group in which the widget is contained. """ key = self.__makeWidgetKey(widget) if groupName is None: parentSizer = self.__widgSizer widgDict = self.__widgets else: group = self.__groups[groupName] parentSizer = group.sizer widgDict = group.widgets widg = widgDict.pop(key) parentSizer.Detach(widg.panel) widg.Destroy() self.__refresh()
[docs] def RemoveGroup(self, groupName): """Removes the specified group, and destroys all of the widgets contained within it. """ group = self.__groups.pop(groupName) self.__groupSizer.Detach(group.gapSizer) group.parentPanel.Destroy() self.__refresh()
[docs] def Clear(self): """Removes and destroys all widgets and groups. """ for key in list(self.__widgets.keys()): widg = self.__widgets.pop(key) self.__widgSizer.Detach(widg.sizer) widg.Destroy() for group in self.GetGroups(): self.RemoveGroup(group) self.__refresh()
[docs] def ClearGroup(self, groupName): """Removes and destroys all widgets in the specified group, but does not remove the group. """ group = self.__groups[groupName] group.sizer.Clear(True) group.widgets.clear() self.__refresh()
[docs] def GroupSize(self, groupName): """Returns the number of widgets that have been added to the specified group. """ return len(self.__groups[groupName].widgets)
[docs] def IsExpanded(self, groupName): """Returns ``True`` if the panel for the specified group is currently expanded, ``False`` if it is collapsed """ return self.__groups[groupName].colPanel.IsExpanded()
[docs] def Expand(self, groupName, expand=True): """Expands or collapses the panel for the specified group. """ panel = self.__groups[groupName].colPanel if expand: panel.Expand() else: panel.Collapse() self.__refresh()
class _Widget: """The ``_Widget`` class is used internally by the :class:`WidgetList` to organise references to each widget in the list. """ def __init__(self, displayName, tooltip, label, widget, panel, sizer): self.displayName = displayName self.tooltip = tooltip self.label = label self.widget = widget self.panel = panel self.sizer = sizer def SetBackgroundColour(self, colour): self.panel.SetBackgroundColour(colour) self.label.SetBackgroundColour(colour) def SetTooltip(self, tooltip): self.label.SetToolTip(wx.ToolTip(tooltip)) if isinstance(self.widget, wx.Sizer): for child in self.widget.GetChildren(): child.GetWindow().SetToolTip(wx.ToolTip(tooltip)) else: self.widget.SetToolTip(wx.ToolTip(tooltip)) def Bind(self, evType, callback): self.panel.Bind(evType, callback) self.label.Bind(evType, callback) if isinstance(self.widget, wx.Sizer): for c in self.widget.GetChildren(): window = c.GetWindow() if window is not None: window.Bind(evType, callback) else: self.widget.Bind(evType, callback) def Destroy(self): self.label.Destroy() if isinstance(self.widget, wx.Sizer): self.widget.Clear(True) else: self.widget.Destroy() class _Group: """The ``_Group`` class is used internally by :class:`WidgetList` instances to represent groups of widgets that are in the list. """ def __init__(self, groupName, displayName, gapSizer, parentPanel, colPanel, widgPanel, sizer): self.groupName = groupName self.displayName = displayName self.gapSizer = gapSizer self.parentPanel = parentPanel self.colPanel = colPanel self.widgPanel = widgPanel self.sizer = sizer self.widgets = {} _WidgetListChangeEvent, _EVT_WL_CHANGE_EVENT = wxevent.NewEvent() _WidgetListExpandEvent, _EVT_WL_EXPAND_EVENT = wxevent.NewEvent() WidgetListChangeEvent = _WidgetListChangeEvent """Event emitted by a :class:`WidgetList` when its contents change. """ WidgetListExpandEvent = _WidgetListExpandEvent """Event emitted by a :class:`WidgetList` when a group is expanded or collapsed. """ EVT_WL_CHANGE_EVENT = _EVT_WL_CHANGE_EVENT """Identifier for the :data:`WidgetListChangeEvent`. """ EVT_WL_EXPAND_EVENT = _EVT_WL_EXPAND_EVENT """Identifier for the :data:`WidgetListExpandEvent`. """ WL_ONE_EXPANDED = 1 """:class:`WidgetList` style flag. When applied, at most one group will be expanded at any one time. """