Repository: brainglobe/brainrender-napari (language: python)

Issue:
[BUG] Structure view doesn't show after atlas is newly downloaded.
Should just be a matter of calling `refresh()` in the `on_downloaded` function somewhere.

[BUG] Structure view doesn't show after atlas is newly downloaded.
Should just be a matter of calling `refresh()` in the `on_downloaded` function somewhere.

User instructions about basic brainrender-napari UI functionality
We should have an intuitive way for users to find out they can double-click on the atlas table view and structure tree view to add things to napari. Maybe an info logo to hover over? More/alternative suggestions welcome!

https://github.com/brainglobe/brainglobe-napari/pull/49#issuecomment-1653720850


Discussion / hints:


This is probably a good thing to get some user feedback on. Maybe we should just wander around SWC with a laptop?

* double-clicking
* hovering
* right-clicking
Should all be part of a user guide and discussed with users for feedback

Fix patch:
diff --git a/brainrender_napari/__init__.py b/brainrender_napari/__init__.py
index bb403de..5a57962 100644
--- a/brainrender_napari/__init__.py
+++ b/brainrender_napari/__init__.py
@@ -1,4 +1,4 @@
 __version__ = "0.0.1"
 
 
-__all__ = "AtlasViewerWidget"
+__all__ = "BrainrenderWidget"
diff --git a/brainrender_napari/atlas_viewer_widget.py b/brainrender_napari/atlas_viewer_widget.py
deleted file mode 100644
index b5433ed..0000000
--- a/brainrender_napari/atlas_viewer_widget.py
+++ /dev/null
@@ -1,257 +0,0 @@
-"""
-A napari widget to view atlases.
-
-Atlases that are exposed by the Brainglobe atlas API are
-shown in a table view using the Qt model/view framework
-[Qt Model/View framework](https://doc.qt.io/qt-6/model-view-programming.html)
-
-Users can download and add the atlas images/structures as layers to the viewer.
-"""
-from typing import TYPE_CHECKING
-
-if TYPE_CHECKING:
-    pass
-
-from bg_atlasapi import BrainGlobeAtlas
-from bg_atlasapi.list_atlases import (
-    get_all_atlases_lastversions,
-    get_downloaded_atlases,
-)
-from bg_atlasapi.update_atlases import install_atlas
-from napari.viewer import Viewer
-from qtpy import QtCore
-from qtpy.QtCore import QModelIndex, Qt
-from qtpy.QtWidgets import (
-    QAbstractItemView,
-    QMenu,
-    QTableView,
-    QTreeView,
-    QVBoxLayout,
-    QWidget,
-)
-
-from brainrender_napari.atlas_download_dialog import AtlasDownloadDialog
-from brainrender_napari.atlas_viewer_utils import (
-    read_atlas_metadata_from_file,
-    read_atlas_structures_from_file,
-)
-from brainrender_napari.napari_atlas_representation import (
-    NapariAtlasRepresentation,
-)
-from brainrender_napari.structure_tree_model import StructureTreeModel
-
-
-class AtlasTableModel(QtCore.QAbstractTableModel):
-    """A table data model for atlases."""
-
-    def __init__(self, data):
-        super().__init__()
-        self._data = data
-
-    def data(self, index: QModelIndex, role=Qt.DisplayRole):
-        if role == Qt.DisplayRole:
-            return self._data[index.row()][index.column()]
-        if role == Qt.ToolTipRole:
-            hovered_atlas_name = self._data[index.row()][0]
-            return AtlasViewerWidget.get_tooltip_text(hovered_atlas_name)
-
-    def rowCount(self, index: QModelIndex):
-        return len(self._data)
-
-    def columnCount(self, index: QModelIndex):
-        return len(self._data[0])
-
-    def headerData(
-        self, section: int, orientation: Qt.Orientation, role: Qt.ItemDataRole
-    ):
-        """Customises the horizontal header data of model,
-        and raises an error if an unexpected column is found."""
-        if role == Qt.DisplayRole and orientation == Qt.Orientation.Horizontal:
-            if section == 0:
-                return "Atlas name"
-            elif section == 1:
-                return "Latest Version"
-            else:
-                raise ValueError("Unexpected horizontal header value.")
-        else:
-            return super().headerData(section, orientation, role)
-
-
-class AtlasViewerWidget(QWidget):
-    """Widget to hold a selectable table view of atlases
-    and other widgets to visualise atlases in napari.
-
-    Internal state depends on currently selected row in the table view.
-    """
-
-    def __init__(self, napari_viewer: Viewer):
-        """Instantiates the atlas viewer widget
-        and sets up signal slot connections"""
-        super().__init__()
-
-        self._viewer = napari_viewer
-        self.setLayout(QVBoxLayout())
-
-        # setup atlas view
-        self.atlas_table_view = QTableView()
-        atlases = get_all_atlases_lastversions()
-        data = [[name, version] for name, version in atlases.items()]
-
-        self._model = AtlasTableModel(data)
-        self.atlas_table_view.setModel(self._model)
-        self.atlas_table_view.setEnabled(True)
-        self.atlas_table_view.verticalHeader().hide()
-
-        # select a single, entire row at a time.
-        self.atlas_table_view.setSelectionBehavior(
-            QAbstractItemView.SelectionBehavior.SelectRows
-        )
-        self.atlas_table_view.setSelectionMode(
-            QAbstractItemView.SelectionMode.SingleSelection
-        )
-
-        self._selected_atlas_row = None
-        self._selected_atlas_name = None
-
-        def on_atlas_row_double_clicked():
-            """Adds annotation and reference to the viewer if the currently
-            selected atlas is available locally. Asks the user to confirm
-            they'd like to download the atlas otherwise."""
-            if self._selected_atlas_row is not None:
-                if self._selected_atlas_name in get_downloaded_atlases():
-                    selected_atlas = BrainGlobeAtlas(self._selected_atlas_name)
-                    selected_atlas_representation = NapariAtlasRepresentation(
-                        selected_atlas, self._viewer
-                    )
-                    selected_atlas_representation.add_to_viewer()
-                else:
-                    self.download_dialog = AtlasDownloadDialog(
-                        self._selected_atlas_name
-                    )
-                    self.download_dialog.ok_button.clicked.connect(
-                        self._on_download_atlas_confirmed
-                    )
-                    self.download_dialog.exec()
-
-        self.atlas_table_view.doubleClicked.connect(
-            on_atlas_row_double_clicked
-        )
-        self.atlas_table_view.setContextMenuPolicy(
-            Qt.ContextMenuPolicy.CustomContextMenu
-        )
-        self.atlas_table_view.customContextMenuRequested.connect(
-            self._on_context_menu_requested
-        )
-
-        # implement logic to update state when selection changes.
-        def _on_selection_changed():
-            """Updates the internal state and widgets about selected row."""
-            selected_index = (
-                self.atlas_table_view.selectionModel().currentIndex()
-            )
-            if selected_index.isValid():
-                self._selected_atlas_row = selected_index.row()
-                self._selected_atlas_name = self._model.data(
-                    self._model.index(self._selected_atlas_row, 0)
-                )
-                self.refresh_structure_tree_view()
-
-        self.atlas_table_view.selectionModel().selectionChanged.connect(
-            _on_selection_changed
-        )
-
-        # show structures in a tree view
-        self.structure_tree_view = QTreeView()
-        self.structure_tree_view.hide()
-
-        def on_structure_row_double_clicked():
-            """Links add structure button to selected row
-            in the structure tree view"""
-            selected_index = (
-                self.structure_tree_view.selectionModel().currentIndex()
-            )
-            if selected_index.isValid():
-                selected_structure_name = (
-                    self.structure_tree_view.model().data(selected_index)
-                )
-                selected_atlas = BrainGlobeAtlas(self._selected_atlas_name)
-                selected_atlas_representation = NapariAtlasRepresentation(
-                    selected_atlas, self._viewer
-                )
-                selected_atlas_representation.add_structure_to_viewer(
-                    selected_structure_name
-                )
-
-        self.structure_tree_view.doubleClicked.connect(
-            on_structure_row_double_clicked
-        )
-
-        # add sub-widgets to top-level widget
-        self.layout().addWidget(self.atlas_table_view)
-        self.layout().addWidget(self.structure_tree_view)
-
-    @classmethod
-    def get_tooltip_text(cls, atlas_name: str):
-        if atlas_name in get_downloaded_atlases():
-            metadata = read_atlas_metadata_from_file(atlas_name)
-            metadata_as_string = ""
-            for key, value in metadata.items():
-                metadata_as_string += f"{key}:\t{value}\n"
-
-            tooltip_text = f"{atlas_name} (double-click to add to viewer)\
-            \n{metadata_as_string}"
-        elif atlas_name in get_all_atlases_lastversions().keys():
-            tooltip_text = f"{atlas_name} (double-click to download)"
-        else:
-            raise ValueError("Tooltip text called with invalid atlas name.")
-        return tooltip_text
-
-    def refresh_structure_tree_view(self):
-        """Updates the structure tree view with the currently selected atlas.
-        The view is only visible if the selected atlas has been downloaded.
-        """
-        if self._selected_atlas_name in get_downloaded_atlases():
-            structures = read_atlas_structures_from_file(
-                self._selected_atlas_name
-            )
-            region_model = StructureTreeModel(structures)
-            self.structure_tree_view.setModel(region_model)
-            self.structure_tree_view.hideColumn(1)  # don't show structure id
-            self.structure_tree_view.setExpandsOnDoubleClick(False)
-            self.structure_tree_view.setHeaderHidden(True)
-            self.structure_tree_view.setWordWrap(False)
-            self.structure_tree_view.expandToDepth(0)
-            self.structure_tree_view.show()
-        else:
-            self.structure_tree_view.hide()
-
-    def _on_download_atlas_confirmed(self):
-        """Downloads the selected atlas."""
-        if self._selected_atlas_row is not None:
-            if self._selected_atlas_name not in get_downloaded_atlases():
-                install_atlas(self._selected_atlas_name)
-
-    def _on_context_menu_requested(self, position):
-        if self._selected_atlas_name in get_downloaded_atlases():
-            metadata = read_atlas_metadata_from_file(self._selected_atlas_name)
-            if (
-                "additional_references" in metadata.keys()
-                and metadata["additional_references"]
-            ):
-                global_position = self.atlas_table_view.viewport().mapToGlobal(
-                    position
-                )
-                additional_reference_menu = QMenu()
-
-                for additional_reference in metadata["additional_references"]:
-                    additional_reference_menu.addAction(additional_reference)
-
-                selected_item = additional_reference_menu.exec(global_position)
-                if selected_item:
-                    atlas = BrainGlobeAtlas(self._selected_atlas_name)
-                    atlas_representation = NapariAtlasRepresentation(
-                        atlas, self._viewer
-                    )
-                    atlas_representation.add_additional_reference(
-                        selected_item.text()
-                    )
diff --git a/brainrender_napari/brainrender_widget.py b/brainrender_napari/brainrender_widget.py
new file mode 100644
index 0000000..6418693
--- /dev/null
+++ b/brainrender_napari/brainrender_widget.py
@@ -0,0 +1,99 @@
+"""
+A napari widget to view atlases.
+
+Atlases that are exposed by the Brainglobe atlas API are
+shown in a table view using the Qt model/view framework
+[Qt Model/View framework](https://doc.qt.io/qt-6/model-view-programming.html)
+
+Users can download and add the atlas images/structures as layers to the viewer.
+"""
+from bg_atlasapi import BrainGlobeAtlas
+from napari.viewer import Viewer
+from qtpy.QtWidgets import (
+    QVBoxLayout,
+    QWidget,
+)
+
+from brainrender_napari.napari_atlas_representation import (
+    NapariAtlasRepresentation,
+)
+from brainrender_napari.widgets.atlas_table_view import AtlasTableView
+from brainrender_napari.widgets.structure_view import StructureView
+
+
+class BrainrenderWidget(QWidget):
+    """The purpose of this class is
+    * to hold atlas visualisation widgets for napari
+    * coordinate between these widgets and napari by
+        * creating appropriate signal-slot connections
+        * creating napari representations as requested
+    """
+
+    def __init__(self, napari_viewer: Viewer):
+        """Instantiates the atlas viewer widget
+        and sets up coordinating connections"""
+        super().__init__()
+
+        self._viewer = napari_viewer
+        self.setLayout(QVBoxLayout())
+
+        # create widgets and add them to the layout
+        self.atlas_table_view = AtlasTableView(parent=self)
+        self.structure_view = StructureView(parent=self)
+        self.layout().addWidget(self.atlas_table_view)
+        self.layout().addWidget(self.structure_view)
+
+        # connect atlas view widget signals
+        self.atlas_table_view.download_atlas_confirmed.connect(
+            self._on_download_atlas_confirmed
+        )
+        self.atlas_table_view.add_atlas_requested.connect(
+            self._on_add_atlas_requested
+        )
+        self.atlas_table_view.additional_reference_requested.connect(
+            self._on_additional_reference_requested
+        )
+        self.atlas_table_view.selected_atlas_changed.connect(
+            self._on_atlas_selection_changed
+        )
+
+        # connect structure view signals
+        self.structure_view.add_structure_requested.connect(
+            self._on_add_structure_requested
+        )
+
+    def _on_download_atlas_confirmed(self, atlas_name):
+        """Ensure structure view is displayed if new atlas downloaded."""
+        self.structure_view.refresh(atlas_name)
+
+    def _on_add_structure_requested(self, structure_name: str):
+        """Add given structure as napari atlas representation"""
+        selected_atlas = BrainGlobeAtlas(
+            self.atlas_table_view.selected_atlas_name()
+        )
+        selected_atlas_representation = NapariAtlasRepresentation(
+            selected_atlas, self._viewer
+        )
+        selected_atlas_representation.add_structure_to_viewer(structure_name)
+
+    def _on_additional_reference_requested(
+        self, additional_reference_name: str
+    ):
+        """Add additional reference as napari atlas representation"""
+        atlas = BrainGlobeAtlas(self.atlas_table_view.selected_atlas_name())
+        atlas_representation = NapariAtlasRepresentation(atlas, self._viewer)
+        atlas_representation.add_additional_reference(
+            additional_reference_name
+        )
+
+    def _on_atlas_selection_changed(self, atlas_name: str):
+        """Refreshes the structure view to match the changed atlas selection"""
+        self.structure_view.refresh(atlas_name)
+
+    def _on_add_atlas_requested(self, atlas_name: str):
+        """Add reference and annotation as napari atlas representation"""
+        selected_atlas = BrainGlobeAtlas(atlas_name)
+        selected_atlas_representation = NapariAtlasRepresentation(
+            selected_atlas, self._viewer
+        )
+        selected_atlas_representation.add_to_viewer()
diff --git a/brainrender_napari/napari.yaml b/brainrender_napari/napari.yaml
index 238734b..9d740ab 100644
--- a/brainrender_napari/napari.yaml
+++ b/brainrender_napari/napari.yaml
@@ -2,9 +2,9 @@ name: brainrender-napari
 display_name: BrainGlobe Tools
 contributions:
   commands:
-    - id: brainrender-napari.make_atlas_viewer_widget
-      python_name: brainrender_napari.atlas_viewer_widget:AtlasViewerWidget
+    - id: brainrender-napari.make_brainrender_widget
+      python_name: brainrender_napari.brainrender_widget:BrainrenderWidget
       title: Make Brainrender Napari
   widgets:
-    - command: brainrender-napari.make_atlas_viewer_widget
+    - command: brainrender-napari.make_brainrender_widget
       display_name: Brainrender Napari
diff --git a/brainrender_napari/atlas_viewer_utils.py b/brainrender_napari/utils/load_user_data.py
similarity index 100%
rename from brainrender_napari/atlas_viewer_utils.py
rename to brainrender_napari/utils/load_user_data.py
diff --git a/brainrender_napari/widgets/__init__.py b/brainrender_napari/widgets/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/brainrender_napari/atlas_download_dialog.py b/brainrender_napari/widgets/atlas_download_dialog.py
similarity index 100%
rename from brainrender_napari/atlas_download_dialog.py
rename to brainrender_napari/widgets/atlas_download_dialog.py
diff --git a/brainrender_napari/widgets/atlas_table_view.py b/brainrender_napari/widgets/atlas_table_view.py
new file mode 100644
index 0000000..46ad80a
--- /dev/null
+++ b/brainrender_napari/widgets/atlas_table_view.py
@@ -0,0 +1,165 @@
+"""The purpose of this file is to provide interactive model and view classes
+for a table holding atlases. Users interacting with the table can request to
+* download an atlas (double-click on row of a not-yet downloaded atlas)
+* add annotation and reference images (double-click on row of local atlas)
+* add additional references (right-click on a row and select from  menu)
+
+It is designed to be agnostic from the viewer framework by emitting signals
+that any interested observers can connect to.
+"""
+from bg_atlasapi.list_atlases import (
+    get_all_atlases_lastversions,
+    get_downloaded_atlases,
+)
+from bg_atlasapi.update_atlases import install_atlas
+from qtpy.QtCore import QAbstractTableModel, QModelIndex, Qt, Signal
+from qtpy.QtWidgets import QMenu, QTableView, QWidget
+
+from brainrender_napari.utils.load_user_data import (
+    read_atlas_metadata_from_file,
+)
+from brainrender_napari.widgets.atlas_download_dialog import (
+    AtlasDownloadDialog,
+)
+
+
+class AtlasTableModel(QAbstractTableModel):
+    """A table data model for atlases."""
+
+    def __init__(self, data):
+        super().__init__()
+        self._data = data
+
+    def data(self, index: QModelIndex, role=Qt.DisplayRole):
+        if role == Qt.DisplayRole:
+            return self._data[index.row()][index.column()]
+        if role == Qt.ToolTipRole:
+            hovered_atlas_name = self._data[index.row()][0]
+            return AtlasTableModel._get_tooltip_text(hovered_atlas_name)
+
+    def rowCount(self, index: QModelIndex):
+        return len(self._data)
+
+    def columnCount(self, index: QModelIndex):
+        return len(self._data[0])
+
+    def headerData(
+        self, section: int, orientation: Qt.Orientation, role: Qt.ItemDataRole
+    ):
+        """Customises the horizontal header data of model,
+        and raises an error if an unexpected column is found."""
+        if role == Qt.DisplayRole and orientation == Qt.Orientation.Horizontal:
+            if section == 0:
+                return "Atlas name"
+            elif section == 1:
+                return "Latest version"
+            else:
+                raise ValueError("Unexpected horizontal header value.")
+        else:
+            return super().headerData(section, orientation, role)
+
+    @classmethod
+    def _get_tooltip_text(cls, atlas_name: str):
+        """Returns the atlas metadata as a formatted string,
+        as well as instructions on how to interact with the atlas."""
+        if atlas_name in get_downloaded_atlases():
+            metadata = read_atlas_metadata_from_file(atlas_name)
+            metadata_as_string = ""
+            for key, value in metadata.items():
+                metadata_as_string += f"{key}:\t{value}\n"
+
+            tooltip_text = f"{atlas_name} (double-click to add to viewer)\
+            \n{metadata_as_string}"
+        elif atlas_name in get_all_atlases_lastversions().keys():
+            tooltip_text = f"{atlas_name} (double-click to download)"
+        else:
+            raise ValueError("Tooltip text called with invalid atlas name.")
+        return tooltip_text
+
+
+class AtlasTableView(QTableView):
+    add_atlas_requested = Signal(str)
+    download_atlas_confirmed = Signal(str)
+    additional_reference_requested = Signal(str)
+    selected_atlas_changed = Signal(str)
+
+    def __init__(self, parent: QWidget = None):
+        """Initialises an atlas table view with latest atlas versions.
+
+        Also responsible for appearance, behaviour on selection, and
+        setting up signal-slot connections.
+        """
+        super().__init__(parent)
+        atlases = get_all_atlases_lastversions()
+        data = [[name, version] for name, version in atlases.items()]
+
+        self.setModel(AtlasTableModel(data))
+        self.setEnabled(True)
+        self.verticalHeader().hide()
+
+        self.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
+        self.setSelectionMode(QTableView.SelectionMode.SingleSelection)
+        self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
+        self.customContextMenuRequested.connect(
+            self._on_context_menu_requested
+        )
+
+        self.doubleClicked.connect(self._on_row_double_clicked)
+
+        self.selectionModel().currentChanged.connect(self._on_current_changed)
+
+    def selected_atlas_name(self):
+        """A single place to get a valid selected atlas name."""
+        selected_index = self.selectionModel().currentIndex()
+        assert selected_index.isValid()
+        selected_atlas_name_index = selected_index.siblingAtColumn(0)
+        selected_atlas_name = self.model().data(selected_atlas_name_index)
+        return selected_atlas_name
+
+    def _on_context_menu_requested(self, position):
+        """Returns a context menu with a list of additional references for the
+        currently selected atlas if the atlas is downloaded and has any. If the
+        user selects one of the additional references, this is signalled.
+        """
+        selected_atlas_name = self.selected_atlas_name()
+        if selected_atlas_name in get_downloaded_atlases():
+            metadata = read_atlas_metadata_from_file(selected_atlas_name)
+            if (
+                "additional_references" in metadata.keys()
+                and metadata["additional_references"]
+            ):
+                global_position = self.viewport().mapToGlobal(position)
+                additional_reference_menu = QMenu()
+
+                for additional_reference in metadata["additional_references"]:
+                    additional_reference_menu.addAction(additional_reference)
+
+                selected_item = additional_reference_menu.exec(global_position)
+                if selected_item:
+                    self.additional_reference_requested.emit(
+                        selected_item.text()
+                    )
+
+    def _on_row_double_clicked(self):
+        """Emits add_atlas_requested if the currently
+        selected atlas is available locally. Asks the user to confirm
+        they'd like to download the atlas otherwise."""
+        atlas_name = self.selected_atlas_name()
+        if atlas_name in get_downloaded_atlases():
+            self.add_atlas_requested.emit(atlas_name)
+        else:
+            download_dialog = AtlasDownloadDialog(atlas_name)
+            download_dialog.ok_button.clicked.connect(
+                self._on_download_atlas_confirmed
+            )
+            download_dialog.exec()
+
+    def _on_download_atlas_confirmed(self):
+        """Downloads an atlas and signals that this has happened."""
+        atlas_name = self.selected_atlas_name()
+        install_atlas(atlas_name)
+        self.download_atlas_confirmed.emit(atlas_name)
+
+    def _on_current_changed(self):
+        """Emits a signal with the newly selected atlas name"""
+        self.selected_atlas_changed.emit(self.selected_atlas_name())
diff --git a/brainrender_napari/structure_tree_model.py b/brainrender_napari/widgets/structure_view.py
similarity index 70%
rename from brainrender_napari/structure_tree_model.py
rename to brainrender_napari/widgets/structure_view.py
index b597f66..287f4be 100644
--- a/brainrender_napari/structure_tree_model.py
+++ b/brainrender_napari/widgets/structure_view.py
@@ -1,8 +1,18 @@
+"""The purpose of this file is to provide interactive item,
+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
+from qtpy.QtCore import QAbstractItemModel, QModelIndex, Qt, Signal
 from qtpy.QtGui import QStandardItem
+from qtpy.QtWidgets import QTreeView, QWidget
+
+from brainrender_napari.utils.load_user_data import (
+    read_atlas_structures_from_file,
+)
 
 
 class StructureTreeItem(QStandardItem):
@@ -141,3 +151,40 @@ def index(self, row, column, parent=QModelIndex()):
             return self.createIndex(row, column, child_item)
         else:
             return QModelIndex()
+
+
+class StructureView(QTreeView):
+    add_structure_requested = Signal(str)
+
+    def __init__(self, parent: QWidget = None):
+        super().__init__(parent)
+        self.doubleClicked.connect(self._on_row_double_clicked)
+
+    def refresh(self, selected_atlas_name: str):
+        """Updates the structure tree view with the currently selected atlas.
+        The view is only visible if the selected atlas has been downloaded.
+        Resets the current index either way.
+        """
+        if selected_atlas_name in get_downloaded_atlases():
+            structures = read_atlas_structures_from_file(selected_atlas_name)
+            region_model = StructureTreeModel(structures)
+            self.setModel(region_model)
+            self.hideColumn(1)  # don't show structure id
+            self.setExpandsOnDoubleClick(False)
+            self.setHeaderHidden(True)
+            self.setWordWrap(False)
+            self.expandToDepth(0)
+            self.show()
+        else:
+            self.hide()
+        self.setCurrentIndex(QModelIndex())
+
+    def selected_structure_name(self) -> str:
+        """A single place to get a valid selected structure"""
+        selected_index = self.selectionModel().currentIndex()
+        assert selected_index.isValid()
+        selected_structure_name = self.model().data(selected_index)
+        return selected_structure_name
+
+    def _on_row_double_clicked(self):
+        self.add_structure_requested.emit(self.selected_structure_name())


Test patch:
diff --git a/tests/test_unit/conftest.py b/tests/conftest.py
similarity index 78%
rename from tests/test_unit/conftest.py
rename to tests/conftest.py
index e4bdbf5..9014e7b 100644
--- a/tests/test_unit/conftest.py
+++ b/tests/conftest.py
@@ -2,7 +2,7 @@
 from pathlib import Path
 
 import pytest
-from bg_atlasapi import config
+from bg_atlasapi import BrainGlobeAtlas, config
 from qtpy.QtCore import Qt
 
 
@@ -49,8 +49,26 @@ def mock_home():
         monkeypatch.setattr(config, "TEMPLATE_CONF_DICT", mock_default_dirs)
 
 
+@pytest.fixture(autouse=True)
+def setup_preexisting_local_atlases():
+    """Automatically setup all tests to have three downloaded atlases
+    in the test user data."""
+    preexisting_atlases = [
+        ("example_mouse_100um", "v1.2"),
+        ("allen_mouse_100um", "v1.2"),
+        ("osten_mouse_100um", "v1.1"),
+    ]
+    for atlas_name, version in preexisting_atlases:
+        if not Path.exists(
+            Path.home() / f".brainglobe/{atlas_name}_{version}"
+        ):
+            _ = BrainGlobeAtlas(atlas_name)
+
+
 @pytest.fixture
 def double_click_on_view(qtbot):
+    """Fixture to avoid code repetition to emulate double-click on a view."""
+
     def inner_double_click_on_view(view, index):
         viewport_index_position = view.visualRect(index).center()
 
diff --git a/tests/test_integration/test_brainrender_widget.py b/tests/test_integration/test_brainrender_widget.py
new file mode 100644
index 0000000..431141a
--- /dev/null
+++ b/tests/test_integration/test_brainrender_widget.py
@@ -0,0 +1,91 @@
+"""These tests should just check that the subwidget signal and napari
+are connected as expected. Lower level tests should happen in the tests
+for the widget themselves."""
+
+import pytest
+
+from brainrender_napari.brainrender_widget import BrainrenderWidget
+
+
+@pytest.fixture
+def brainrender_widget(make_napari_viewer) -> BrainrenderWidget:
+    """Fixture to expose the atlas viewer widget to tests.
+
+    Simultaneously acts as a smoke test that the widget
+    can be instantiated without crashing."""
+    viewer = make_napari_viewer()
+    return BrainrenderWidget(viewer)
+
+
+def test_download_confirmed_refreshes_view(brainrender_widget, mocker):
+    structure_view_refresh_mock = mocker.patch(
+        "brainrender_napari.brainrender_widget.StructureView.refresh"
+    )
+    brainrender_widget.atlas_table_view.download_atlas_confirmed.emit(
+        "allen_mouse_10um"
+    )
+    structure_view_refresh_mock.assert_called_once_with("allen_mouse_10um")
+
+
+@pytest.mark.parametrize(
+    "expected_atlas_name",
+    [
+        "example_mouse_100um",
+        "allen_mouse_100um",
+        "osten_mouse_100um",
+    ],
+)
+def test_double_click_on_locally_available_atlas_row(
+    brainrender_widget, mocker, qtbot, expected_atlas_name
+):
+    """Check for a few local low-res atlases that double-clicking them
+    on the atlas table view calls the expected atlas representation function.
+    """
+    add_atlas_to_viewer_mock = mocker.patch(
+        "brainrender_napari.brainrender_widget"
+        ".NapariAtlasRepresentation.add_to_viewer"
+    )
+    with qtbot.waitSignal(
+        brainrender_widget.atlas_table_view.add_atlas_requested
+    ):
+        brainrender_widget.atlas_table_view.add_atlas_requested.emit(
+            expected_atlas_name
+        )
+    add_atlas_to_viewer_mock.assert_called_once()
+
+
+def test_structure_row_double_clicked(brainrender_widget, mocker):
+    """Checks that when the structure view widgets emit "VS" and
+    the allen_mouse_100um atlas is selected, the NapariAtlasRepresentation
+    function is called in the expected way.
+    """
+    add_structure_to_viewer_mock = mocker.patch(
+        "brainrender_napari.brainrender_widget"
+        ".NapariAtlasRepresentation.add_structure_to_viewer"
+    )
+    brainrender_widget.atlas_table_view.selectRow(
+        4
+    )  # allen_mouse_100um is in row 4
+
+    brainrender_widget.structure_view.add_structure_requested.emit("VS")
+    add_structure_to_viewer_mock.assert_called_once_with("VS")
+
+
+def test_add_additional_reference_selected(brainrender_widget, mocker):
+    """Checks that when the atlas table view requests an additional
+    reference, the NapariAtlasRepresentation function is called in
+    the expected way."""
+    add_additional_reference_mock = mocker.patch(
+        "brainrender_napari.brainrender_widget"
+        ".NapariAtlasRepresentation.add_additional_reference"
+    )
+    brainrender_widget.atlas_table_view.selectRow(
+        5
+    )  # mpin_zfish_1um is in row 5
+    additional_reference_name = "GAD1b"
+    brainrender_widget.atlas_table_view.additional_reference_requested.emit(
+        additional_reference_name
+    )
+    add_additional_reference_mock.assert_called_once_with(
+        additional_reference_name
+    )
diff --git a/tests/test_unit/test_atlas_table_view.py b/tests/test_unit/test_atlas_table_view.py
new file mode 100644
index 0000000..6b61109
--- /dev/null
+++ b/tests/test_unit/test_atlas_table_view.py
@@ -0,0 +1,210 @@
+import shutil
+from pathlib import Path
+
+import pytest
+from qtpy.QtCore import QModelIndex, Qt
+
+from brainrender_napari.widgets.atlas_table_view import (
+    AtlasTableModel,
+    AtlasTableView,
+)
+
+
+@pytest.fixture
+def atlas_table_view(qtbot) -> AtlasTableView:
+    """Fixture to provide a valid atlas table view.
+
+    Depends on qtbot fixture to provide the qt event loop.
+    """
+    return AtlasTableView()
+
+
+@pytest.mark.parametrize(
+    "row, expected_atlas_name",
+    [
+        (4, "allen_mouse_100um"),  # part of downloaded test data
+        (6, "allen_human_500um"),  # not part of downloaded test data
+    ],
+)
+def test_atlas_table_view_valid_selection(
+    row, expected_atlas_name, atlas_table_view
+):
+    """Checks selected_atlas_name for valid current indices"""
+    model_index = atlas_table_view.model().index(row, 0)
+    atlas_table_view.setCurrentIndex(model_index)
+    assert atlas_table_view.selected_atlas_name() == expected_atlas_name
+
+
+def test_atlas_table_view_invalid_selection(atlas_table_view):
+    """Checks that selected_atlas_name throws an assertion error
+    if current index is invalid."""
+    with pytest.raises(AssertionError):
+        atlas_table_view.setCurrentIndex(QModelIndex())
+        atlas_table_view.selected_atlas_name()
+
+
+def test_hover_atlas_table_view(atlas_table_view, mocker):
+    """Check tooltip is called when hovering over view"""
+    index = atlas_table_view.model().index(2, 1)
+
+    get_tooltip_text_mock = mocker.patch(
+        "brainrender_napari.widgets"
+        ".atlas_table_view.AtlasTableModel._get_tooltip_text"
+    )
+
+    atlas_table_view.model().data(index, Qt.ToolTipRole)
+
+    get_tooltip_text_mock.assert_called_once()
+
+
+@pytest.mark.parametrize(
+    "column, expected_header",
+    [
+        (0, "Atlas name"),
+        (1, "Latest version"),
+    ],
+)
+def test_model_header(atlas_table_view, column, expected_header):
+    """Check the table model has expected header data"""
+    assert (
+        atlas_table_view.model().headerData(
+            column, Qt.Orientation.Horizontal, Qt.DisplayRole
+        )
+        == expected_header
+    )
+
+
+def test_model_header_invalid_column(atlas_table_view):
+    """Check the table model throws as expected for invalid column"""
+    invalid_column = 2
+    with pytest.raises(ValueError):
+        atlas_table_view.model().headerData(
+            invalid_column, Qt.Orientation.Horizontal, Qt.DisplayRole
+        )
+
+
+def test_get_tooltip_downloaded():
+    """Check tooltip on an example in the downloaded test data"""
+    tooltip_text = AtlasTableModel._get_tooltip_text("example_mouse_100um")
+    assert "example_mouse" in tooltip_text
+    assert "add to viewer" in tooltip_text
+
+
+def test_get_tooltip_not_locally_available():
+    """Check tooltip on an example in not-downloaded test data"""
+    tooltip_text = AtlasTableModel._get_tooltip_text("allen_human_500um")
+    assert "allen_human_500um" in tooltip_text
+    assert "double-click to download" in tooltip_text
+
+
+def test_get_tooltip_invalid_name():
+    """Check tooltip on non-existent test data"""
+    with pytest.raises(ValueError) as e:
+        _ = AtlasTableModel._get_tooltip_text("wrong_atlas_name")
+        assert "invalid atlas name" in e
+
+
+@pytest.mark.parametrize(
+    "row",
+    [
+        1,  # "allen_mouse_10um"
+        6,  # "allen_human_500um"
+    ],
+)
+def test_double_click_on_not_yet_downloaded_atlas_row(
+    atlas_table_view, mocker, double_click_on_view, row
+):
+    """Check for a few yet-to-be-downloaded atlases that double-clicking
+    them on the atlas table view executes the download dialog.
+    """
+
+    model_index = atlas_table_view.model().index(row, 0)
+    atlas_table_view.setCurrentIndex(model_index)
+
+    dialog_exec_mock = mocker.patch(
+        "brainrender_napari.widgets.atlas_table_view.AtlasDownloadDialog.exec"
+    )
+    double_click_on_view(atlas_table_view, model_index)
+    dialog_exec_mock.assert_called_once()
+
+
+@pytest.mark.parametrize(
+    "row,expected_atlas_name",
+    [
+        (0, "example_mouse_100um"),
+        (4, "allen_mouse_100um"),
+        (14, "osten_mouse_100um"),
+    ],
+)
+def test_double_click_on_locally_available_atlas_row(
+    atlas_table_view, double_click_on_view, qtbot, row, expected_atlas_name
+):
+    """Check for a few locally available low-res atlases that double-clicking
+    them on the atlas table view emits a signal with their expected names.
+    """
+    model_index = atlas_table_view.model().index(row, 0)
+    atlas_table_view.setCurrentIndex(model_index)
+
+    with qtbot.waitSignal(
+        atlas_table_view.add_atlas_requested
+    ) as add_atlas_requested_signal:
+        double_click_on_view(atlas_table_view, model_index)
+
+    assert add_atlas_requested_signal.args == [expected_atlas_name]
+
+
+def test_additional_reference_menu(atlas_table_view, qtbot, mocker):
+    """Checks callback to additional reference menu calls QMenu exec
+    and emits expected signal"""
+    atlas_table_view.selectRow(5)  # mpin_zfish_1um is in row 5
+    from qtpy.QtCore import QPoint
+    from qtpy.QtWidgets import QAction
+
+    x = atlas_table_view.rowViewportPosition(5)
+    y = atlas_table_view.columnViewportPosition(0)
+    position = QPoint(x, y)
+    qmenu_exec_mock = mocker.patch(
+        "brainrender_napari.widgets.atlas_table_view.QMenu.exec"
+    )
+    qmenu_exec_mock.return_value = QAction("mock_additional_reference")
+
+    with qtbot.waitSignal(
+        atlas_table_view.additional_reference_requested
+    ) as additional_reference_requested_signal:
+        atlas_table_view.customContextMenuRequested.emit(position)
+
+    qmenu_exec_mock.assert_called_once()
+    assert additional_reference_requested_signal.args == [
+        "mock_additional_reference"
+    ]
+
+
+def test_download_confirmed_callback(atlas_table_view, qtbot):
+    """Checks that confirming atlas download creates local copy of
+    example atlas files and emits expected signal.
+
+    Test setup consists of remembering the expected files and folders
+    of a preexisting atlas and then removing them. This allows checking
+    that the function triggers the creation of the same local copy
+    of the atlas as the `bg_atlasapi` itself.
+    """
+
+    atlas_directory = Path.home() / ".brainglobe/example_mouse_100um_v1.2"
+    expected_filenames = atlas_directory.iterdir()
+    shutil.rmtree(
+        path=atlas_directory
+    )  # now remove local copy so button has to trigger download
+    assert not Path.exists(
+        atlas_directory
+    )  # sanity check that local copy is gone
+
+    with qtbot.waitSignal(
+        atlas_table_view.download_atlas_confirmed
+    ) as download_atlas_confirmed_signal:
+        model_index = atlas_table_view.model().index(0, 0)
+        atlas_table_view.setCurrentIndex(model_index)
+        atlas_table_view._on_download_atlas_confirmed()
+
+    assert download_atlas_confirmed_signal.args == ["example_mouse_100um"]
+    for file in expected_filenames:
+        assert Path.exists(file)
diff --git a/tests/test_unit/test_atlas_viewer_widget.py b/tests/test_unit/test_atlas_viewer_widget.py
deleted file mode 100644
index 3ec938e..0000000
--- a/tests/test_unit/test_atlas_viewer_widget.py
+++ /dev/null
@@ -1,199 +0,0 @@
-import shutil
-from pathlib import Path
-from typing import Tuple
-
-import pytest
-from bg_atlasapi import BrainGlobeAtlas
-from napari.viewer import Viewer
-from qtpy.QtCore import Qt
-
-from brainrender_napari.atlas_viewer_widget import AtlasViewerWidget
-
-
-@pytest.fixture
-def make_atlas_viewer(make_napari_viewer) -> Tuple[Viewer, AtlasViewerWidget]:
-    """Fixture to expose the atlas viewer widget to tests.
-
-    Downloads three atlases as test data, if not already there.
-
-    Simultaneously acts as a smoke test that the widget and
-    local atlas files can be instantiated without crashing."""
-    viewer = make_napari_viewer()
-    preexisting_atlases = [
-        ("example_mouse_100um", "v1.2"),
-        ("allen_mouse_100um", "v1.2"),
-        ("osten_mouse_100um", "v1.1"),
-    ]
-    for atlas_name, version in preexisting_atlases:
-        if not Path.exists(
-            Path.home() / f".brainglobe/{atlas_name}_{version}"
-        ):
-            _ = BrainGlobeAtlas(atlas_name)
-
-    atlas_viewer = AtlasViewerWidget(viewer)
-    return viewer, atlas_viewer
-
-
-def test_download_confirmed_callback(make_atlas_viewer):
-    """Checks that confirming atlas download creates local copy of
-    example atlas files.
-
-    Test setup consists of remembering the expected files and folders
-    of a preexisting atlas and then removing them. This allows checking
-    that the function triggers the creation of the same local copy
-    of the atlas as the `bg_atlasapi` itself.
-    """
-    atlas_directory = Path.home() / ".brainglobe/example_mouse_100um_v1.2"
-    expected_filenames = atlas_directory.iterdir()
-    shutil.rmtree(
-        path=atlas_directory
-    )  # now remove local copy so button has to trigger download
-    assert not Path.exists(
-        atlas_directory
-    )  # sanity check that local copy is gone
-
-    _, atlas_viewer = make_atlas_viewer
-    atlas_viewer.atlas_table_view.selectRow(0)
-    atlas_viewer._on_download_atlas_confirmed()
-
-    for file in expected_filenames:
-        assert Path.exists(file)
-
-
-@pytest.mark.parametrize(
-    "row,expected_atlas_name",
-    [
-        (0, "example_mouse_100um"),
-        (4, "allen_mouse_100um"),
-        (14, "osten_mouse_100um"),
-    ],
-)
-def test_double_click_on_locally_available_atlas_row(
-    make_atlas_viewer, double_click_on_view, row, expected_atlas_name
-):
-    """Check for a few locally available low-res atlases that double-clicking
-    them on the atlas table view adds the layers with their expected names.
-    """
-    viewer, atlas_viewer = make_atlas_viewer
-
-    model_index = atlas_viewer.atlas_table_view.model().index(row, 0)
-    atlas_viewer.atlas_table_view.setCurrentIndex(model_index)
-
-    double_click_on_view(atlas_viewer.atlas_table_view, model_index)
-
-    assert len(viewer.layers) == 2
-    assert viewer.layers[1].name == f"{expected_atlas_name}_annotation"
-    assert viewer.layers[0].name == f"{expected_atlas_name}_reference"
-
-
-@pytest.mark.parametrize(
-    "row, expected_atlas_name",
-    [
-        (1, "allen_mouse_100um"),  # part of downloaded test data
-        (6, "allen_human_500um"),  # not part of downloaded test data
-    ],
-)
-def test_double_click_on_not_yet_downloaded_atlas_row(
-    make_atlas_viewer, mocker, double_click_on_view, row, expected_atlas_name
-):
-    """Check for a few yet-to-be-downloaded atlases that double-clicking
-    them on the atlas table view executes the download dialog.
-    """
-    _, atlas_viewer = make_atlas_viewer
-
-    model_index = atlas_viewer.atlas_table_view.model().index(row, 0)
-    atlas_viewer.atlas_table_view.setCurrentIndex(model_index)
-
-    dialog_exec_mock = mocker.patch(
-        "brainrender_napari.atlas_viewer_widget.AtlasDownloadDialog.exec"
-    )
-    # weirdly, to correctly emulate a double-click
-    # you need to click first. Also, note that the view
-    # needs to be interacted with via its viewport
-    double_click_on_view(atlas_viewer.atlas_table_view, model_index)
-    dialog_exec_mock.assert_called_once()
-
-
-def test_structure_row_double_clicked(
-    make_atlas_viewer, mocker, double_click_on_view
-):
-    """Checks that clicking the add_structure_button with
-    the allen_mouse_100um atlas and its VS submesh selected
-    in the widget views calls the NapariAtlasRepresentation
-    function in the expected way.
-    """
-    _, atlas_viewer = make_atlas_viewer
-    add_structure_to_viewer_mock = mocker.patch(
-        "brainrender_napari.atlas_viewer_widget"
-        ".NapariAtlasRepresentation.add_structure_to_viewer"
-    )
-    atlas_viewer.atlas_table_view.selectRow(4)  # allen_mouse_100um is in row 4
-
-    # find and select first sub-item of root mesh in structure tree view
-    root_index = atlas_viewer.structure_tree_view.rootIndex()
-    root_mesh_index = atlas_viewer.structure_tree_view.model().index(
-        0, 0, root_index
-    )
-    vs_mesh_index = atlas_viewer.structure_tree_view.model().index(
-        0, 0, root_mesh_index
-    )
-    assert vs_mesh_index.isValid()
-    atlas_viewer.structure_tree_view.setCurrentIndex(vs_mesh_index)
-
-    double_click_on_view(atlas_viewer.structure_tree_view, vs_mesh_index)
-
-    # First sub-item in tree view expected to be "VS"
-    add_structure_to_viewer_mock.assert_called_once_with("VS")
-
-
-@pytest.mark.parametrize(
-    "row, expected_visibility",
-    [
-        (4, True),  # allen_mouse_100um is part of downloaded test data
-        (6, False),  # allen_human_500um is not part of download test data
-    ],
-)
-def test_add_structure_visibility(make_atlas_viewer, row, expected_visibility):
-    """Checks that the structure tree view and add structure buttons
-    are visible iff atlas has previously been downloaded."""
-    _, atlas_viewer = make_atlas_viewer
-    atlas_viewer.show()  # show tree view ancestor for sensible check
-    atlas_viewer.atlas_table_view.selectRow(row)
-    assert atlas_viewer.structure_tree_view.isVisible() == expected_visibility
-
-
-def test_get_tooltip_downloaded():
-    """Check tooltip on an example in the downloaded test data"""
-    tooltip_text = AtlasViewerWidget.get_tooltip_text("example_mouse_100um")
-    assert "example_mouse" in tooltip_text
-    assert "add to viewer" in tooltip_text
-
-
-def test_get_tooltip_not_locally_available():
-    """Check tooltip on an example in not-downloaded test data"""
-    tooltip_text = AtlasViewerWidget.get_tooltip_text("allen_human_500um")
-    assert "allen_human_500um" in tooltip_text
-    assert "double-click to download" in tooltip_text
-
-
-def test_get_tooltip_invalid_name():
-    """Check tooltip on non-existent test data"""
-    with pytest.raises(ValueError) as e:
-        _ = AtlasViewerWidget.get_tooltip_text("wrong_atlas_name")
-        assert "invalid atlas name" in e
-
-
-def test_hover_atlas_table_view(make_atlas_viewer, mocker, qtbot):
-    """Check tooltip is called when hovering over view"""
-    _, atlas_viewer = make_atlas_viewer
-    view = atlas_viewer.atlas_table_view
-    index = view.model().index(2, 1)
-
-    get_tooltip_text_mock = mocker.patch(
-        "brainrender_napari.atlas_viewer_widget"
-        ".AtlasViewerWidget.get_tooltip_text"
-    )
-
-    view.model().data(index, Qt.ToolTipRole)
-
-    get_tooltip_text_mock.assert_called_once()
diff --git a/tests/test_unit/test_download_dialog.py b/tests/test_unit/test_download_dialog.py
index fba7eb4..dd02ab4 100644
--- a/tests/test_unit/test_download_dialog.py
+++ b/tests/test_unit/test_download_dialog.py
@@ -1,6 +1,8 @@
 import pytest
 
-from brainrender_napari.atlas_download_dialog import AtlasDownloadDialog
+from brainrender_napari.widgets.atlas_download_dialog import (
+    AtlasDownloadDialog,
+)
 
 
 def test_download_dialog(qtbot):
diff --git a/tests/test_unit/test_metadata_reading.py b/tests/test_unit/test_metadata_reading.py
index d298882..1f6b6a9 100644
--- a/tests/test_unit/test_metadata_reading.py
+++ b/tests/test_unit/test_metadata_reading.py
@@ -1,6 +1,8 @@
 from bg_atlasapi import BrainGlobeAtlas
 
-from brainrender_napari.atlas_viewer_utils import read_atlas_metadata_from_file
+from brainrender_napari.utils.load_user_data import (
+    read_atlas_metadata_from_file,
+)
 
 
 def test_metadata_reading():
diff --git a/tests/test_unit/test_napari_atlas_representation.py b/tests/test_unit/test_napari_atlas_representation.py
index a48f6f6..481d184 100644
--- a/tests/test_unit/test_napari_atlas_representation.py
+++ b/tests/test_unit/test_napari_atlas_representation.py
@@ -109,7 +109,7 @@ def test_structure_color(make_napari_viewer):
         assert a * 255 == e
 
 
-def test_add_additional_reference(make_napari_viewer, mocker):
+def test_add_additional_reference(make_napari_viewer):
     viewer = make_napari_viewer()
     atlas_name = "mpin_zfish_1um"
     additional_reference_name = "GAD1b"
diff --git a/tests/test_unit/test_structure_view.py b/tests/test_unit/test_structure_view.py
new file mode 100644
index 0000000..3c49f9f
--- /dev/null
+++ b/tests/test_unit/test_structure_view.py
@@ -0,0 +1,65 @@
+import pytest
+
+from brainrender_napari.widgets.structure_view import StructureView
+
+
+@pytest.fixture
+def structure_view(qtbot) -> StructureView:
+    return StructureView()
+
+
+def test_structure_view_valid_selection(structure_view):
+    """Checks that the correct structure name is returned
+    if a valid structure view index is selected."""
+    structure_view.refresh("allen_mouse_100um")
+
+    root_index = structure_view.rootIndex()
+    root_mesh_index = structure_view.model().index(0, 0, root_index)
+    vs_mesh_index = structure_view.model().index(0, 0, root_mesh_index)
+    assert vs_mesh_index.isValid()
+    structure_view.setCurrentIndex(vs_mesh_index)
+    assert structure_view.selected_structure_name() == "VS"
+
+
+def test_structure_view_invalid_selection(structure_view):
+    """Checks that selected_structure_name throws an assertion error
+    if current index is invalid."""
+    structure_view.refresh("allen_mouse_100um")
+    with pytest.raises(AssertionError):
+        structure_view.selected_structure_name()
+
+
+@pytest.mark.parametrize(
+    "atlas_name, expected_visibility",
+    [
+        ("allen_mouse_100um", True),  # is part of downloaded test data
+        ("allen_human_500um", False),  #  is not part of download test data
+    ],
+)
+def test_structure_view_visibility(
+    atlas_name, expected_visibility, structure_view
+):
+    """Checks that the structure view is visible
+    iff atlas has previously been downloaded."""
+    structure_view.refresh(atlas_name)
+    assert structure_view.isVisible() == expected_visibility
+
+
+def test_double_click_on_structure_row(
+    structure_view, double_click_on_view, qtbot
+):
+    """Checks that expected signal is emitted when
+    double-clicking on a row in the structure view"""
+    structure_view.refresh("allen_mouse_100um")
+
+    root_index = structure_view.rootIndex()
+    root_mesh_index = structure_view.model().index(0, 0, root_index)
+    vs_mesh_index = structure_view.model().index(0, 0, root_mesh_index)
+    assert vs_mesh_index.isValid()
+    structure_view.setCurrentIndex(vs_mesh_index)
+    with qtbot.waitSignal(
+        structure_view.add_structure_requested
+    ) as add_structure_requested_signal:
+        double_click_on_view(structure_view, vs_mesh_index)
+
+    assert add_structure_requested_signal.args == ["VS"]