#
# atlasoverlaypanel.py - The AtlasOverlayPanel class.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides the :class:`AtlasOverlayPanel`, which is a sub-panel
used by the :class:`.AtlasPanel`.
"""
import logging
import wx
import fsl.data.atlases as atlases
import fsl.utils.idle as idle
from fsl.utils.platform import platform as fslplatform
import fsleyes_widgets.elistbox as elistbox
import fsleyes_widgets.placeholder_textctrl as plctext
import fsleyes_widgets.utils.status as status
import fsleyes.panel as fslpanel
import fsleyes.strings as strings
log = logging.getLogger(__name__)
[docs]class AtlasOverlayPanel(fslpanel.FSLeyesPanel):
"""The ``AtlasOverlayPanel`` displays a list of all available FSL atlases
(see the :mod:`.atlases` module), and allows the user to:
1. Search across all atlases for regions, by name.
2. Toggle overlays for atlases, and for individual regions from any
atlas.
3. Move the :attr:`.DisplayContext.location` to any region from any
atlas.
An ``AtlasOverlayPanel`` looks something like this:
.. image:: images/atlasoverlaypanel.png
:scale: 50%
:align: center
The ``AtlasOverlayPanel`` has three main sections:
- The *atlas list* - a :class:`fsleyes_widgets.elistbox.EditableListBox`
with an entry, and an :class:`OverlayListWidget` for every available
atlas. The ``OverlayListWidget`` allows the user to turn on/off a
summary overlay image for the atlas (see the section on :ref:`atlas
panel overlays <atlas-panel-atlas-overlays>`).
- The *region list* - an ``EditableListBox`` which contains one entry
for every region in the atlas that is currently selected in the atlas
list, and an ``OverlayListWidget`` alongside every region. The
``OverlayListWidget`` allows the user to navigate to a region, and
to turn on/off a label or statistic/probabilistic region overlay.
- The *region filter* - a ``wx.TextCtrl`` located above the region
list. This allows the user to search for regions by name. When the
user types a string into this field, the region list will be updated
to show only regions that match the entered string (via a simple
substring match), and the atlas list will be updated such that all
atlases which have matching regions will be highlighted in bold.
"""
[docs] def __init__(self, parent, overlayList, displayCtx, frame, atlasPanel):
"""Create an ``AtlasOverlayPanel``.
:arg parent: the :mod:`wx` parent object.
:arg overlayList: The :class:`.OverlayList` instance.
:arg displayCtx: The :class:`.DisplayContext` instance.
:arg frame: The :class:`.FSLeyesFrame` instance.
:arg atlasPanel: The :class:`.AtlasPanel` instance that has created
this ``AtlasInfoPanel``.
"""
fslpanel.FSLeyesPanel.__init__(
self, parent, overlayList, displayCtx, frame)
# References to an EditableListBox
# for each atlas, containing a list
# of its regions. These are created
# on-demand in the __onAtlasSelect
# method.
self.__regionLists = {}
self.__atlasPanel = atlasPanel
self.__contentPanel = wx.SplitterWindow(self,
style=wx.SP_LIVE_UPDATE)
self.__atlasList = elistbox.EditableListBox(
self.__contentPanel,
vgap=5,
style=(elistbox.ELB_NO_ADD |
elistbox.ELB_NO_REMOVE |
elistbox.ELB_NO_MOVE))
self.__regionPanel = wx.Panel(self.__contentPanel)
self.__regionFilter = plctext.PlaceholderTextCtrl(
self.__regionPanel, placeholder='Search')
self.__contentPanel.SetMinimumPaneSize(50)
self.__contentPanel.SplitVertically(self.__atlasList,
self.__regionPanel)
self.__contentPanel.SetSashGravity(0.4)
self.__sizer = wx.BoxSizer(wx.HORIZONTAL)
self.__regionSizer = wx.BoxSizer(wx.VERTICAL)
self.__regionSizer.Add(self.__regionFilter, flag=wx.EXPAND)
self.__regionSizer.AddStretchSpacer()
self.__sizer .Add(self.__contentPanel,
flag=wx.EXPAND,
proportion=1)
self.__regionPanel.SetSizer(self.__regionSizer)
self .SetSizer(self.__sizer)
self.__regionFilter.Bind(wx.EVT_TEXT, self.__onRegionFilter)
self.__atlasList.Bind(elistbox.EVT_ELB_SELECT_EVENT,
self.__onAtlasSelect)
fslplatform.register( self.name, self.__fslDirChanged)
atlases.registry.register(self.name, self.__atlasAdded, 'add')
atlases.registry.register(self.name, self.__atlasRemoved, 'remove')
self.__buildAtlasList()
self.__regionSizer.Layout()
self.__sizer .Layout()
# Allow the atlas list
# to be minimised
self.__atlasList.SetMinSize((50, -1))
self.SetMinSize(self.__sizer.GetMinSize())
[docs] def destroy(self):
"""Performs some clean up operations. """
fslplatform.deregister(self.name)
fslpanel.FSLeyesPanel.destroy(self)
[docs] def Enable(self, enable=True):
"""Enables/disables this ``AtlasOverlayPanel``. """
self.__atlasList.Enable(enable)
for atlasID, regionList in self.__regionLists.items():
regionList.Enable(enable)
[docs] def Disable(self):
"""Disables this ``AtlasOverlayPanel``. """
self.Enable(False)
[docs] def setOverlayState(self, atlasDesc, labelIdx, summary, state):
"""Updates the *enabled* state of the specified atlas overlay.
This method is called by the :class:`.AtlasPanel` when an atlas overlay
is added/removed to/from the :class:`.OverlayList`. It ensures that the
:class:`OverlayListWidget` controls for every atlas, and every region,
are up to date.
"""
atlasIdx = self.__atlasList.IndexOf(atlasDesc)
log.debug('Setting {}/{} overlay state to {}'.format(
atlasDesc.atlasID, labelIdx, state))
if labelIdx is None:
widget = self.__atlasList.GetItemWidget(atlasIdx)
widget.SetEnableState(state)
else:
regionList = self.__regionLists.get(atlasDesc.atlasID, None)
if regionList is not None:
regionList.GetItemWidget(labelIdx).SetEnableState(state)
def __fslDirChanged(self, *a):
"""Called when the :attr:`.Platform.fsldir` changes. Refreshes
the atlas list.
"""
self.__buildAtlasList()
def __atlasAdded(self, *a):
"""Called when a new atlas is added to the :class:`.AtlasRegistry`.
Re-generates the atlas list.
"""
self.__buildAtlasList()
def __atlasRemoved(self, *a):
"""Called when an atlas is removed from the :class:`.AtlasRegistry`.
Re-generates the atlas list.
"""
self.__buildAtlasList()
def __buildAtlasList(self):
"""Clears and recreates the atlas list. Also clears all existing
region lists.
"""
atlasDescs = atlases.listAtlases()
# This method is called whenever any
# atlases are added/removed. We want
# to preserve any region lists for
# atlases that are still in the atlas
# registry.
regionLists = dict(self.__regionLists)
# If a region list is currently
# being shown, clear it.
regionList = self.__regionSizer.GetItem(1).GetWindow()
if regionList is not None:
regionList.Show(False)
self.__regionSizer.Remove(1)
self.__regionSizer.AddStretchSpacer()
self.__regionLists = {}
# Now clear and re-populate the atlas list
self.__atlasList.Clear()
for i, atlasDesc in enumerate(atlasDescs):
self.__atlasList.Append(atlasDesc.name, atlasDesc)
self.__updateAtlasState(atlasDesc)
widget = OverlayListWidget(self.__atlasList,
atlasDesc.atlasID,
self.__atlasPanel,
self)
self.__atlasList.SetItemWidget(i, widget)
# Restore references to region lists
# for atlases that still exist
for atlasID, regionList in regionLists.items():
if atlases.hasAtlas(atlasID):
self.__regionLists[atlasID] = regionList
else:
regionList.Destroy()
self.__regionSizer.Layout()
def __onRegionFilter(self, ev):
"""Called when the user enters some text in the region filter.
Filters the region list (see the :meth:`.EditableListBox.ApplyFilter`
method), and updates all items in the atlas list (see the
:meth:`__updateAtlasState` method).
"""
filterStr = self.__regionFilter.GetValue().lower().strip()
for atlasDesc in atlases.listAtlases():
self.__updateAtlasState(atlasDesc)
listBox = self.__regionLists.get(atlasDesc.atlasID, None)
if listBox is not None:
listBox.ApplyFilter(filterStr, ignoreCase=True)
def __updateAtlasState(self, atlasDesc):
"""Updates the state of the atlas list item which corresponds to the
atlas with the specified identifier.
If the atlas has regions which match the current region filter string,
the atlas label font is set to bold. Otherwise (or if the region filter
string is empty), the atlas label font is set to normal.
"""
atlasIdx = self.__atlasList.IndexOf(atlasDesc)
filterStr = self.__regionFilter.GetValue().lower().strip()
atlasDesc = self.__atlasList.GetItemData(atlasIdx)
if filterStr == '':
nhits = 0
else:
nhits = len([l for l in atlasDesc.labels
if filterStr in l.name.lower()])
if nhits == 0:
weight = wx.FONTWEIGHT_LIGHT
colour = '#404040'
else:
weight = wx.FONTWEIGHT_BOLD
colour = '#000000'
font = self.__atlasList.GetItemFont(atlasIdx)
font.SetWeight(weight)
self.__atlasList.SetItemFont(atlasIdx, font)
self.__atlasList.SetItemForegroundColour(atlasIdx, colour, colour)
def __onAtlasSelect(self, ev=None, atlasDesc=None):
"""Called when the user selects an atlas in the atlas list, or
the :meth:`selectAtlas` method is called.
If a region list (a list of :class:`OverlayListWidget` items for every
region in the atlas, to be displayed in the region list) has not yet
been created, it is created - this is done asynchronously (via the
:func:`idle.idle` function), as it can take quite a long time for
some of the atlases (e.g. the Talairach and Juelich).
Then the region list is updated to show the regions for the newly
selected atlas.
"""
if ev is not None:
atlasDesc = ev.data
regionList = self.__regionLists.get(atlasDesc.atlasID, None)
atlasPanelDisabled = regionList is None
# This function changes the displayed region
# list. We schedule it on the wx idle loop,
# so it will get called after the region list
# has been populated (if it has not been
# displayed before).
def changeAtlasList():
# See comment above about
# suppressing wx complaints
try:
filterStr = self.__regionFilter.GetValue().lower().strip()
regionList.ApplyFilter(filterStr, ignoreCase=True)
self.__updateAtlasState(atlasDesc)
status.update(strings.messages[self, 'regionsLoaded'].format(
atlasDesc.name))
log.debug('Showing region list for {} ({})'.format(
atlasDesc.atlasID, id(regionList)))
# Hide the currently
# shown region list
old = self.__regionSizer.GetItem(1).GetWindow()
if old is not None:
old.Show(False)
regionList.Show(True)
self.__regionSizer.Remove(1)
self.__regionSizer.Insert(1,
regionList,
flag=wx.EXPAND,
proportion=1)
self.__regionSizer.Layout()
if atlasPanelDisabled:
self.__atlasPanel.enableAtlasPanel()
except RuntimeError:
pass
if regionList is None:
# The region list for this atlas has not yet been
# created. So we create the list, and then create
# a widget for every region in the atlas. Some of
# the atlases (Juelich and Talairach in particular)
# have a large number of regions, so we create the
# widgets asynchronously on the wx idle loop.
regionList = elistbox.EditableListBox(
self.__regionPanel,
vgap=5,
style=(elistbox.ELB_NO_ADD |
elistbox.ELB_NO_REMOVE |
elistbox.ELB_NO_MOVE))
regionList.Show(False)
self.__regionLists[atlasDesc.atlasID] = regionList
# Add blockSize labels, starting from label[i],
# to the region list. Then, if necessary,
# schedule more labels be added, starting from
# label[i + blockSize].
blockSize = 20
nlabels = len(atlasDesc.labels)
def addToRegionList(start):
# If the user kills this panel while
# the region list is being updated,
# suppress wx complaints.
try:
for i in range(start, min(start + blockSize, nlabels)):
label = atlasDesc.labels[i]
widget = OverlayListWidget(regionList,
atlasDesc.atlasID,
self.__atlasPanel,
self,
label.index)
regionList.Append(label.name, extraWidget=widget)
if i < nlabels - 1: idle.idle(addToRegionList, i + 1)
else: idle.idle(changeAtlasList)
except RuntimeError:
pass
log.debug('Creating region list for {} ({})'.format(
atlasDesc.atlasID, id(regionList)))
status.update(
strings.messages[self, 'loadRegions'].format(atlasDesc.name),
timeout=None)
# Schedule addToRegionList on the
# wx idle loop for the first region.
# The function will recursively
# schedule itself to run for subsequent
# regions.
#
# Disable the panel while this is
# occurring.
atlasPanelDisabled = True
self.__atlasPanel.enableAtlasPanel(False)
idle.idle(addToRegionList, 0)
else:
idle.idle(changeAtlasList)
[docs] def selectAtlas(self, atlasDesc):
"""Selects the specified atlas. This method is used by
:class:`OverlayListWidget` instances.
:arg atlasID: The atlas identifier
"""
atlasIdx = self.__atlasList.IndexOf(atlasDesc)
self.__atlasList.SetSelection(atlasIdx)
self.__onAtlasSelect(atlasDesc=atlasDesc)