Skip to content
Snippets Groups Projects
Commit 13c79cb0 authored by Paul McCarthy's avatar Paul McCarthy
Browse files

Melodic LabelGrid is now functional. Bugfix to LookupTablePanel.

parent 92010646
No related branches found
No related tags found
No related merge requests found
...@@ -235,6 +235,10 @@ class MelodicClassification(props.HasProperties): ...@@ -235,6 +235,10 @@ class MelodicClassification(props.HasProperties):
def getLabels(self, component): def getLabels(self, component):
return list(self.labels[component]) return list(self.labels[component])
def hasLabel(self, component, label):
return label in self.labels[component]
def addLabel(self, component, label): def addLabel(self, component, label):
...@@ -292,6 +296,10 @@ class MelodicClassification(props.HasProperties): ...@@ -292,6 +296,10 @@ class MelodicClassification(props.HasProperties):
return list(self.__components.get(label, [])) return list(self.__components.get(label, []))
def hasComponent(self, label, component):
return component in self.getComponents(label)
def addComponent(self, label, component): def addComponent(self, label, component):
self.addLabel(component, label) self.addLabel(component, label)
......
...@@ -359,6 +359,8 @@ labels = TypeDict({ ...@@ -359,6 +359,8 @@ labels = TypeDict({
'ComponentGrid.componentColumn' : 'IC #', 'ComponentGrid.componentColumn' : 'IC #',
'ComponentGrid.labelColumn' : 'Labels', 'ComponentGrid.labelColumn' : 'Labels',
'LabelGrid.componentColumn' : 'IC #',
'LabelGrid.labelColumn' : 'Label',
}) })
......
...@@ -780,7 +780,7 @@ class LutLabelDialog(wx.Dialog): ...@@ -780,7 +780,7 @@ class LutLabelDialog(wx.Dialog):
return self.__enteredColour return self.__enteredColour
def ___onOk(self, ev): def __onOk(self, ev):
"""Called when the user confirms the dialog. Saves the name, colour, """Called when the user confirms the dialog. Saves the name, colour,
and value that were entered, and closes the dialog. and value that were entered, and closes the dialog.
""" """
...@@ -791,7 +791,7 @@ class LutLabelDialog(wx.Dialog): ...@@ -791,7 +791,7 @@ class LutLabelDialog(wx.Dialog):
self.EndModal(wx.ID_OK) self.EndModal(wx.ID_OK)
def onCancel(self, ev): def __onCancel(self, ev):
"""Called when the user cancells the dialog. Closes the dialog.""" """Called when the user cancells the dialog. Closes the dialog."""
self.EndModal(wx.ID_CANCEL) self.EndModal(wx.ID_CANCEL)
...@@ -4,8 +4,13 @@ ...@@ -4,8 +4,13 @@
# #
# Author: Paul McCarthy <pauldmccarthy@gmail.com> # Author: Paul McCarthy <pauldmccarthy@gmail.com>
# #
"""This module provides the :class:`ComponentGrid` and :class:`LabelGrid`
classes, which are used by the :class:`.MelodicClassificationPanel`.
"""
import logging
import wx import wx
import pwidgets.widgetgrid as widgetgrid import pwidgets.widgetgrid as widgetgrid
...@@ -18,9 +23,32 @@ import fsl.data.melodicimage as fslmelimage ...@@ -18,9 +23,32 @@ import fsl.data.melodicimage as fslmelimage
import fsl.data.strings as strings import fsl.data.strings as strings
class ComponentGrid(fslpanel.FSLEyesPanel): log = logging.getLogger(__name__)
class ComponentGrid(fslpanel.FSLEyesPanel):
"""The ``ComponentGrid`` uses a :class:`.WidgetGrid`, and a set of
:class:`.TextTagPanel` widgets, to display the component classifications
stored in the :class:`.MelodicClassification` object that is associated
with the currently selected overlay (if this overlay is a
:class:`.MelodicImage`.
The grid contains one row for each component, and a ``TextTagPanel`` is
used to display the labels associated with each component. Each
``TextTagPanel`` allows the user to add and remove labels to/from the
corresponding component.
"""
def __init__(self, parent, overlayList, displayCtx, lut): def __init__(self, parent, overlayList, displayCtx, lut):
"""Create a ``ComponentGrid``.
:arg parent: The ``wx`` parent object.
:arg overlayList: The :class:`.OverlayList`.
:arg displayCtx: The :class:`.DisplayContext`.
:arg lut: The :class:`.LookupTable` instance used to colour
each label tag.
"""
fslpanel.FSLEyesPanel.__init__(self, parent, overlayList, displayCtx) fslpanel.FSLEyesPanel.__init__(self, parent, overlayList, displayCtx)
...@@ -54,7 +82,9 @@ class ComponentGrid(fslpanel.FSLEyesPanel): ...@@ -54,7 +82,9 @@ class ComponentGrid(fslpanel.FSLEyesPanel):
def destroy(self): def destroy(self):
""" """Must be called when this ``ComponentGrid`` is no longer needed.
De-registers various property listeners, and calls
:meth:`.FSLEyesPanel.destroy`.
""" """
self._displayCtx .removeListener('selectedOverlay', self._name) self._displayCtx .removeListener('selectedOverlay', self._name)
...@@ -68,7 +98,8 @@ class ComponentGrid(fslpanel.FSLEyesPanel): ...@@ -68,7 +98,8 @@ class ComponentGrid(fslpanel.FSLEyesPanel):
def __deregisterCurrentOverlay(self): def __deregisterCurrentOverlay(self):
""" """Called when the selected overlay changes. De-registers listeners
associated with the previously selected overlay, if necessary.
""" """
if self.__overlay is None: if self.__overlay is None:
...@@ -85,12 +116,16 @@ class ComponentGrid(fslpanel.FSLEyesPanel): ...@@ -85,12 +116,16 @@ class ComponentGrid(fslpanel.FSLEyesPanel):
opts = display.getDisplayOpts() opts = display.getDisplayOpts()
opts .removeListener('volume', self._name) opts .removeListener('volume', self._name)
display.removeListener('overlayType', self._name) display.removeListener('overlayType', self._name)
except fsldisplay.InvalidOverlayError: except fsldisplay.InvalidOverlayError:
pass pass
def __selectedOverlayChanged(self, *a): def __selectedOverlayChanged(self, *a):
""" """Called when the :attr:`.DisplayContext.selectedOverlay` changes. If
the overlay is a :class:`.MelodicImage`, the :class:`.WidgetGrid` is
re-populated to display the component-label mappings contained in the
associated :class:`.MelodicClassification` instance.
""" """
self.__deregisterCurrentOverlay() self.__deregisterCurrentOverlay()
...@@ -123,7 +158,8 @@ class ComponentGrid(fslpanel.FSLEyesPanel): ...@@ -123,7 +158,8 @@ class ComponentGrid(fslpanel.FSLEyesPanel):
def __recreateTags(self): def __recreateTags(self):
""" """Re-creates a :class:`.TextTagPanel` for every component in the
:class:`.MelodicImage`.
""" """
overlay = self.__overlay overlay = self.__overlay
...@@ -146,8 +182,8 @@ class ComponentGrid(fslpanel.FSLEyesPanel): ...@@ -146,8 +182,8 @@ class ComponentGrid(fslpanel.FSLEyesPanel):
self.__grid.SetText( i, 0, str(i)) self.__grid.SetText( i, 0, str(i))
self.__grid.SetWidget(i, 1, tags) self.__grid.SetWidget(i, 1, tags)
tags.Bind(texttag.EVT_TTP_TAG_ADDED_EVENT, self.__onTagAdded) tags.Bind(texttag.EVT_TTP_TAG_ADDED, self.__onTagAdded)
tags.Bind(texttag.EVT_TTP_TAG_REMOVED_EVENT, self.__onTagRemoved) tags.Bind(texttag.EVT_TTP_TAG_REMOVED, self.__onTagRemoved)
self.__refreshTags() self.__refreshTags()
...@@ -155,6 +191,8 @@ class ComponentGrid(fslpanel.FSLEyesPanel): ...@@ -155,6 +191,8 @@ class ComponentGrid(fslpanel.FSLEyesPanel):
def __refreshTags(self): def __refreshTags(self):
"""Re-generates the tags on every :class:`.TextTagPanel` in the grid.
"""
overlay = self.__overlay overlay = self.__overlay
melclass = overlay.getICClassification() melclass = overlay.getICClassification()
...@@ -191,7 +229,8 @@ class ComponentGrid(fslpanel.FSLEyesPanel): ...@@ -191,7 +229,8 @@ class ComponentGrid(fslpanel.FSLEyesPanel):
def __addNewLutLabel(self, label, colour=None): def __addNewLutLabel(self, label, colour=None):
""" """Called when a new tag is added to a :class:`.TextTagPanel`. Adds a
corresponding label to the :class:`.LookupTable`.
""" """
lut = self.__lut lut = self.__lut
...@@ -210,7 +249,9 @@ class ComponentGrid(fslpanel.FSLEyesPanel): ...@@ -210,7 +249,9 @@ class ComponentGrid(fslpanel.FSLEyesPanel):
def __onTagAdded(self, ev): def __onTagAdded(self, ev):
""" """Called when a tag is added to a :class:`.TextTagPanel`. Adds the
corresponding component-label mapping to the
:class:`.MelodicClassification` instance.
""" """
tags = ev.GetEventObject() tags = ev.GetEventObject()
...@@ -243,8 +284,10 @@ class ComponentGrid(fslpanel.FSLEyesPanel): ...@@ -243,8 +284,10 @@ class ComponentGrid(fslpanel.FSLEyesPanel):
def __onTagRemoved(self, ev): def __onTagRemoved(self, ev):
""" """Called when a tag is removed from a :class:`.TextTagPanel`.
""" Removes the corresponding component-label mapping from the
:class:`.MelodicClassification` instance.
"""
tags = ev.GetEventObject() tags = ev.GetEventObject()
label = ev.tag label = ev.tag
...@@ -267,47 +310,308 @@ class ComponentGrid(fslpanel.FSLEyesPanel): ...@@ -267,47 +310,308 @@ class ComponentGrid(fslpanel.FSLEyesPanel):
def __onGridSelect(self, ev): def __onGridSelect(self, ev):
"""Called when a row is selected on the :class:`.WidgetGrid`. Makes
sure that the 'new tag' control in the corresponding
:class:`.TextTagPanel` is focused.
"""
component = ev.row component = ev.row
opts = self._displayCtx.getOpts(self.__overlay) opts = self._displayCtx.getOpts(self.__overlay)
opts.disableListener('volume', self._name) opts.disableListener('volume', self._name)
opts.volume = component opts.volume = component
opts.enableListener('volume', self._name) opts.enableListener('volume', self._name)
tags = self.__grid.GetWidget(ev.row, 1) tags = self.__grid.GetWidget(ev.row, 1)
tags.FocusNewTagCtrl() tags.FocusNewTagCtrl()
def __volumeChanged(self, *a): def __volumeChanged(self, *a):
"""Called when the :attr:`.ImageOpts.volume` property changes. Selects
opts = self._displayCtx.getOpts(self.__overlay) the corresponding row in the :class:`.WidgetGrid`.
self.__grid.SetSelection(opts.volume, -1) """
# Only change the row if we are
# currently visible, otherwise
# this will screw up the focus.
if not self.IsShown():
return
tags = self.__grid.GetWidget(opts.volume, 1) grid = self.__grid
opts = self._displayCtx.getOpts(self.__overlay)
tags = grid.GetWidget(opts.volume, 1)
grid.SetSelection(opts.volume, -1)
tags.FocusNewTagCtrl() tags.FocusNewTagCtrl()
def __labelsChanged(self, *a): def __labelsChanged(self, *a):
"""Called when the :attr:`.MelodicClassification.labels` change.
Re-generates the tags shown on every :class:`.TextTagPanel`.
"""
self.__refreshTags() self.__refreshTags()
def __lutChanged(self, *a): def __lutChanged(self, *a):
"""Called when the :attr:`.LookupTable.labels` change.
Re-generates the tags shown on every :class:`.TextTagPanel`.
"""
self.__refreshTags() self.__refreshTags()
class LabelGrid(fslpanel.FSLEyesPanel): class LabelGrid(fslpanel.FSLEyesPanel):
"""The ``LabelGrid`` class is the inverse of the :class:`ComponentGrid`.
It uses a :class:`.WidgetGrid` to display the label-component mappings
present on the :class:`.MelodicClassification` instance associated with
the currently selected overlay (if this overlay is a
:class:`.MelodicImage`.
The grid contains one row for each label, and a :class:`.TextTagPanel` is
used to display the components associated with each label. Each
``TextTagPanel`` allows the user to add and remove components to/from the
corresponding label.
"""
def __init__(self, parent, overlayList, displayCtx, lut): def __init__(self, parent, overlayList, displayCtx, lut):
"""Create a ``LabelGrid``.
:arg parent: The ``wx`` parent object.
:arg overlayList: The :class:`.OverlayList`.
:arg displayCtx: The :class:`.DisplayContext`.
:arg lut: The :class:`.LookupTable` to be used to colour
component tags.
"""
fslpanel.FSLEyesPanel.__init__(self, parent, overlayList, displayCtx) fslpanel.FSLEyesPanel.__init__(self, parent, overlayList, displayCtx)
self.__grid = widgetgrid.WidgetGrid(self) self.__lut = lut
self.__grid = widgetgrid.WidgetGrid(
self,
style=(wx.VSCROLL |
widgetgrid.WG_SELECTABLE_ROWS |
widgetgrid.WG_KEY_NAVIGATION))
self.__grid.ShowRowLabels(False)
self.__grid.ShowColLabels(True)
self.__sizer = wx.BoxSizer(wx.HORIZONTAL) self.__sizer = wx.BoxSizer(wx.HORIZONTAL)
self.__sizer.Add(self.__grid, flag=wx.EXPAND, proportion=1) self.__sizer.Add(self.__grid, flag=wx.EXPAND, proportion=1)
self.SetSizer(self.__sizer)
self.__grid.Bind(widgetgrid.EVT_WG_SELECT, self.__onGridSelect)
lut .addListener('labels', self._name, self.__lutChanged)
displayCtx .addListener('selectedOverlay',
self._name,
self.__selectedOverlayChanged)
overlayList.addListener('overlays',
self._name,
self.__selectedOverlayChanged)
self.__overlay = None
self.__recreateGrid()
self.__selectedOverlayChanged()
def destroy(self):
"""Must be called when this ``LabelGrid`` is no longer needed.
De-registers various property listeners, and calls
:meth:`.FSLEyesPanel.destroy`.
"""
self._displayCtx .removeListener('selectedOverlay', self._name)
self._overlayList.removeListener('overlays', self._name)
self.__lut .removeListener('labels', self._name)
self.__deregisterCurrentOverlay()
self.__lut = None
self.SetSizer(self.__sizer) fslpanel.FSLEyesPanel.destroy(self)
def __deregisterCurrentOverlay(self):
"""Called when the selected overlay changes. De-registers property
listeners associated with the previously selected overlay, if
necessary.
"""
if self.__overlay is None:
return
overlay = self.__overlay
self.__overlay = None
melclass = overlay.getICClassification()
melclass.removeListener('labels', self._name)
def __selectedOverlayChanged(self, *a):
"""Called when the :attr:`.DisplayContext.selectedOverlay` changes.
If the overlay is a :class:`.MelodicImage`, a listener is registered
with its :class:`.MelodicClassification`, and its component-label
mappings displayed on the :class:`.WidgetGrid`.
"""
self.__deregisterCurrentOverlay()
overlay = self._displayCtx.getSelectedOverlay()
if not isinstance(overlay, fslmelimage.MelodicImage):
return
self.__overlay = overlay
melclass = overlay.getICClassification()
melclass.addListener('labels', self._name, self.__labelsChanged)
self.__refreshTags()
def __recreateGrid(self):
"""Clears the :class:`.WidgetGrid`, and re-creates
a :class:`.TextTagPanel` for every available melodic classification
label.
"""
grid = self.__grid
lut = self.__lut
labels = lut.labels
grid.ClearGrid()
grid.SetGridSize(len(labels), 2, growCols=[1])
grid.SetColLabel(0, strings.labels[self, 'labelColumn'])
grid.SetColLabel(1, strings.labels[self, 'componentColumn'])
for i, label in enumerate(labels):
tags = texttag.TextTagPanel(self.__grid,
style=(texttag.TTP_NO_DUPLICATES |
texttag.TTP_KEYBOARD_NAV))
tags._label = label.name()
self.__grid.SetText( i, 0, label.name())
self.__grid.SetWidget(i, 1, tags)
tags.Bind(texttag.EVT_TTP_TAG_ADDED, self.__onTagAdded)
tags.Bind(texttag.EVT_TTP_TAG_REMOVED, self.__onTagRemoved)
tags.Bind(texttag.EVT_TTP_TAG_SELECT, self.__onTagSelect)
def __refreshTags(self):
"""Re-populates the label-component mappings shown on the
:class:`.TextTagPanel` widgets in the :class:`.WidgetGrid`.
"""
lut = self.__lut
grid = self.__grid
overlay = self.__overlay
numComps = overlay.numComponents()
melclass = overlay.getICClassification()
for i, label in enumerate(lut.labels):
tags = grid.GetWidget(i, 1)
comps = melclass.getComponents(label.name())
tags.ClearTags()
tags.SetOptions(map(str, range(numComps)))
for comp in comps:
colour = label.colour()
colour = [int(round(c * 255.0)) for c in colour]
tags.AddTag(str(comp), colour)
self.__grid.Layout()
def __onTagAdded(self, ev):
"""Called when a tag is added to a :class:`.TextTagPanel`. Adds
the corresponding label-component mapping to the
:class:`.MelodicClassification` instance.
"""
tags = ev.GetEventObject()
overlay = self.__overlay
melclass = overlay.getICClassification()
comp = int(ev.tag)
melclass.disableListener('labels', self._name)
# If this component now has two labels, and
# the other label is 'Unknown', remove the
# 'Unknown' label.
if len(melclass.getLabels(comp)) == 1 and \
melclass.hasLabel(comp, 'Unknown'):
melclass.removeLabel(comp, 'Unknown')
melclass.addComponent(tags._label, comp)
melclass.enableListener('labels', self._name)
self.__refreshTags()
def __onTagRemoved(self, ev):
"""Called when a tag is removed from a :class:`.TextTagPanel`. Removes
the corresponding label-component mapping from the
:class:`.MelodicClassification` instance.
"""
tags = ev.GetEventObject()
overlay = self.__overlay
melclass = overlay.getICClassification()
comp = int(ev.tag)
melclass.disableListener('labels', self._name)
melclass.removeComponent(tags._label, comp)
# If the component has no more labels,
# give it an 'Unknown' label
if len(melclass.getLabels(comp)) == 0:
melclass.addLabel(comp, 'Unknown')
melclass.enableListener('labels', self._name)
self.__refreshTags()
def __onGridSelect(self, ev):
"""Called when a row is selected in the :class:`.WidgetGrid`. Makes
sure that the first tag in the :class:`.TextTagPanel` has the focus.
"""
tags = self.__grid.GetWidget(ev.row, 1)
tags.FocusNewTagCtrl()
def __onTagSelect(self, ev):
"""Called when a tag from a :class:`.TextTagPanel` is selected.
Changes the current :attr:`.ImageOpts.volume` to the component
corresponding to the selected tag.
"""
tag = int(ev.tag)
overlay = self.__overlay
opts = self._displayCtx.getOpts(overlay)
opts.volume = tag
def __lutChanged(self, *a):
"""Called when the :attr:`LookupTable.labels` change. Re-creates and
re-populates the :class:`.WidgetGrid`.
"""
self.__recreateGrid()
self.__refreshTags()
def __labelsChanged(self, *a):
"""Called when the :attr:`.MelodicClassification.labels` change.
Re-populates the :class:`.WidgetGrid`.
"""
self.__refreshTags()
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment