Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

napari widgets to manage atlas versions #130

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion brainrender_napari/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "0.0.1"


__all__ = "BrainrenderWidget"
__all__ = "BrainrenderWidget", "AtlasVersionManagerWidget"
28 changes: 28 additions & 0 deletions brainrender_napari/atlas_version_manager_widget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from qtpy.QtWidgets import QVBoxLayout, QWidget

from brainrender_napari.utils.brainglobe_logo import header_widget
from brainrender_napari.widgets.atlas_manager_view import AtlasManagerView


class AtlasVersionManagerWidget(QWidget):
def __init__(self):
"""Instantiates the version manager widget
and sets up coordinating connections"""
super().__init__()

self.setLayout(QVBoxLayout())
self.layout().addWidget(
header_widget(tutorial_file_name="update-atlas-napari.html")
)

# create widgets
self.atlas_manager_view = AtlasManagerView(parent=self)
self.layout().addWidget(self.atlas_manager_view)

self.atlas_manager_view.download_atlas_confirmed.connect(self._refresh)

self.atlas_manager_view.update_atlas_confirmed.connect(self._refresh)

def _refresh(self) -> None:
# refresh view once an atlas has been downloaded
self.atlas_manager_view = AtlasManagerView(parent=self)
4 changes: 3 additions & 1 deletion brainrender_napari/brainrender_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ def __init__(self, napari_viewer: Viewer):

self._viewer = napari_viewer
self.setLayout(QVBoxLayout())
self.layout().addWidget(header_widget())
self.layout().addWidget(
header_widget(tutorial_file_name="visualise-atlas-napari.html")
)

# create widgets
self.atlas_viewer_view = AtlasViewerView(parent=self)
Expand Down
5 changes: 5 additions & 0 deletions brainrender_napari/napari.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ contributions:
- id: brainrender-napari.make_brainrender_widget
python_name: brainrender_napari.brainrender_widget:BrainrenderWidget
title: Make Brainrender Napari
- id: brainrender-napari.make_atlas_version_manager_widget
python_name: brainrender_napari.atlas_version_manager_widget:AtlasVersionManagerWidget
title: Make Brainrender Napari
widgets:
- command: brainrender-napari.make_brainrender_widget
display_name: Brainrender
- command: brainrender-napari.make_atlas_version_manager_widget
display_name: Atlas version manager
37 changes: 18 additions & 19 deletions brainrender_napari/utils/brainglobe_logo.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,31 @@
<\h1>
"""

_docs_links_html = """
<h3>
<p>Atlas visualisation</p>
<p><a href="https://brainglobe.info" style="color:gray;">Website</a></p>
<p><a href="https://brainglobe.info/tutorials/visualise-atlas-napari.html" style="color:gray;">Tutorial</a></p>
<p><a href="https://github.com/brainglobe/brainrender-napari" style="color:gray;">Source</a></p>
<p><a href="https://doi.org/10.7554/eLife.65751" style="color:gray;">Citation</a></p>
<p><small>For help, hover the cursor over the atlases/regions.</small>
</h3>
""" # noqa: E501


def _docs_links_widget():
docs_links_widget = QLabel(_docs_links_html)

def _docs_links_widget(tutorial_file_name: str, parent: QWidget = None):
_docs_links_html = f"""
<h3>
<p>Atlas visualisation</p>
<p><a href="https://brainglobe.info" style="color:gray;">Website</a></p>
<p><a href="https://brainglobe.info/tutorials/{tutorial_file_name}" style="color:gray;">Tutorial</a></p>
<p><a href="https://github.com/brainglobe/brainrender-napari" style="color:gray;">Source</a></p>
<p><a href="https://doi.org/10.7554/eLife.65751" style="color:gray;">Citation</a></p>
<p><small>For help, hover the cursor over the atlases/regions.</small>
</h3>
""" # noqa: E501
docs_links_widget = QLabel(_docs_links_html, parent=parent)
docs_links_widget.setOpenExternalLinks(True)
return docs_links_widget


def _logo_widget():
return QLabel(_logo_html)
def _logo_widget(parent: QWidget = None):
return QLabel(_logo_html, parent=None)


def header_widget(parent: QWidget = None):
def header_widget(tutorial_file_name: str, parent: QWidget = None):
box = QGroupBox(parent)
box.setFlat(True)
box.setLayout(QHBoxLayout())
box.layout().addWidget(_logo_widget())
box.layout().addWidget(_docs_links_widget())
box.layout().addWidget(_logo_widget(parent=box))
box.layout().addWidget(_docs_links_widget(tutorial_file_name, parent=box))
return box
148 changes: 4 additions & 144 deletions brainrender_napari/widgets/structure_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,158 +2,18 @@
model and view classes for the structures that form part of an atlas.
The view is only visible if the atlas is downloaded."""

from typing import Dict, List

from bg_atlasapi.list_atlases import get_downloaded_atlases
from bg_atlasapi.structure_tree_util import get_structures_tree
from qtpy.QtCore import QAbstractItemModel, QModelIndex, Qt, Signal
from qtpy.QtGui import QStandardItem
from qtpy.QtCore import QModelIndex, Signal
from qtpy.QtWidgets import QTreeView, QWidget

from brainrender_napari.data_models.structure_tree_model import (
StructureTreeModel,
)
from brainrender_napari.utils.load_user_data import (
read_atlas_structures_from_file,
)


class StructureTreeItem(QStandardItem):
"""A class to hold items in a tree model."""

def __init__(self, data, parent=None):
self.parent_item = parent
self.item_data = data
self.child_items = []

def appendChild(self, item):
self.child_items.append(item)

def child(self, row):
return self.child_items[row]

def childCount(self):
return len(self.child_items)

def columnCount(self):
return len(self.item_data)

def data(self, column):
try:
return self.item_data[column]
except IndexError:
return None

def parent(self):
return self.parent_item

def row(self):
if self.parent_item:
return self.parent_item.child_items.index(self)
return 0


class StructureTreeModel(QAbstractItemModel):
"""Implementation of a read-only QAbstractItemModel to hold
the structure tree information provided by the Atlas API in a Qt Model"""

def __init__(self, data: List, parent=None):
super().__init__()
self.root_item = StructureTreeItem(data=("acronym", "name", "id"))
self.build_structure_tree(data, self.root_item)

def build_structure_tree(self, structures: List, root: StructureTreeItem):
"""Build the structure tree given a list of structures."""
tree = get_structures_tree(structures)
structure_id_dict = {}
for structure in structures:
structure_id_dict[structure["id"]] = structure

inserted_items: Dict[int, StructureTreeItem] = {}
for n_id in tree.expand_tree(): # sorts nodes by default,
# so parents will always be already in the QAbstractItemModel
# before their children
node = tree.get_node(n_id)
acronym = structure_id_dict[node.identifier]["acronym"]
name = structure_id_dict[node.identifier]["name"]
if (
len(structure_id_dict[node.identifier]["structure_id_path"])
== 1
):
parent_item = root
else:
parent_id = tree.parent(node.identifier).identifier
parent_item = inserted_items[parent_id]

item = StructureTreeItem(
data=(acronym, name, node.identifier), parent=parent_item
)
parent_item.appendChild(item)
inserted_items[node.identifier] = item

def data(self, index: QModelIndex, role=Qt.DisplayRole):
"""Provides read-only data for a given index if
intended for display, otherwise None."""
if not index.isValid():
return None

if role != Qt.DisplayRole:
return None

item = index.internalPointer()

return item.data(index.column())

def rowCount(self, parent: StructureTreeItem):
"""Returns the number of rows(i.e. children) of an item"""
if parent.column() > 0:
return 0

if not parent.isValid():
parent_item = self.root_item
else:
parent_item = parent.internalPointer()

return parent_item.childCount()

def columnCount(self, parent: StructureTreeItem):
"""The number of columns of an item."""
if parent.isValid():
return parent.internalPointer().columnCount()
else:
return self.root_item.columnCount()

def parent(self, index: QModelIndex):
"""The first-column index of parent of the item
at a given index. Returns an empty index if the root,
or an invalid index, is passed.
"""
if not index.isValid():
return QModelIndex()

child_item = index.internalPointer()
parent_item = child_item.parent()

if parent_item == self.root_item:
return QModelIndex()

return self.createIndex(parent_item.row(), 0, parent_item)

def index(self, row, column, parent=QModelIndex()):
"""The index of the item at (row, column) with a given parent.
By default, the given parent is assumed to be the root."""
if not self.hasIndex(row, column, parent):
return QModelIndex()

if not parent.isValid():
parent_item = self.root_item
else:
parent_item = parent.internalPointer()

child_item = parent_item.child(row)
if child_item:
return self.createIndex(row, column, child_item)
else:
return QModelIndex()


class StructureView(QTreeView):
add_structure_requested = Signal(str)

Expand Down
63 changes: 63 additions & 0 deletions tests/test_integration/test_atlas_version_manager_widget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import pytest

from brainrender_napari.atlas_version_manager_widget import (
AtlasVersionManagerWidget,
)


@pytest.fixture
def atlas_version_manager_widget(qtbot) -> AtlasVersionManagerWidget:
"""A fixture to provide a version manager widget.

Depends on qtbot so Qt event loop is started.
"""
return AtlasVersionManagerWidget()


def test_refresh_calls_view_constructor(atlas_version_manager_widget, mocker):
"""Checks that refreshing the version manager widget
calls the view's constructor."""
atlas_manager_view_mock = mocker.patch(
"brainrender_napari.atlas_version_manager_widget.AtlasManagerView"
)
atlas_version_manager_widget._refresh()
atlas_manager_view_mock.assert_called_once_with(
parent=atlas_version_manager_widget
)


def test_refresh_on_download(qtbot, mocker):
"""Checks that when the view signals an atlas has been downloaded,
the version manager widget is refreshed."""
refresh_mock = mocker.patch(
"brainrender_napari.atlas_version_manager_widget.AtlasVersionManagerWidget._refresh"
)
# Don't use atlas_version_manager_widget fixture here,
# because otherwise mocking is ineffectual!
atlas_version_manager_widget = AtlasVersionManagerWidget()
with qtbot.waitSignal(
atlas_version_manager_widget.atlas_manager_view.download_atlas_confirmed
):
atlas_version_manager_widget.atlas_manager_view.download_atlas_confirmed.emit(
"allen_mouse_100um"
)
refresh_mock.assert_called_once_with("allen_mouse_100um")


def test_refresh_on_update(qtbot, mocker):
"""Checks that when the view signals an atlas has been updated,
the version manager widget is updated."""
refresh_mock = mocker.patch(
"brainrender_napari.atlas_version_manager_widget"
".AtlasVersionManagerWidget._refresh"
)
# Don't use atlas_version_manager_widget fixture here,
# because otherwise mocking is ineffectual!
atlas_version_manager_widget = AtlasVersionManagerWidget()
with qtbot.waitSignal(
atlas_version_manager_widget.atlas_manager_view.update_atlas_confirmed
):
atlas_version_manager_widget.atlas_manager_view.update_atlas_confirmed.emit(
"allen_mouse_100um"
)
refresh_mock.assert_called_once_with("allen_mouse_100um")