Repository: napari/napari

Issue:
Plugin provided theme does not have icons
## 🐛 Bug

If a plugin provides a napari theme, the theme is not correctly registered and so icons are not generated and cached in the qt resource files correctly. This was an oversight in the #3034 PR where the resource generation theme was connected to the correct method AFTER a theme has been added whereas it should be before.

Note: I already have a fix for this that will be forthcoming.

## To Reproduce

Steps to reproduce the behavior:

Hard to reproduce without using a plugin that uses the `napari_experimental_provide_theme` hook.

An example plugin that provides the theme `super-dark` but when the user switches to that theme the icons are not generated so no icon is shown anywhere in the app.
```
WARNING: Cannot open file ':/themes/super-dark/visibility.svg', because: No such file or directory
WARNING:vispy:Cannot open file ':/themes/super-dark/visibility.svg', because: No such file or directory
```

## Expected behavior

All icons should be generated.

## Environment

napari: 0.4.11rc3.dev31+g326cc7b0.d20210827
Platform: Windows-10-10.0.19042-SP0
Python: 3.8.10 (default, May 19 2021, 13:12:57) [MSC v.1916 64 bit (AMD64)]
Qt: 5.15.2
PyQt5: 5.15.4
NumPy: 1.20.3
SciPy: 1.6.3
Dask: 2021.06.1
VisPy: 0.7.3

OpenGL:
- GL version: 4.6.0 - Build 27.20.100.8681
- MAX_TEXTURE_SIZE: 16384

Screens:
- screen 1: resolution 1920x1080, scale 1.0

Plugins:
- Visualization(B/C): 0.1.1
- animation: 0.0.2
- console: 0.0.4.dev0+gfd3a291.d20210830
- napari-theme-test: 0.0.0
- scikit-image: 0.4.11rc3.dev31+g326cc7b0.d20210827
- svg: 0.1.5



Discussion:

Resolving patch:
diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py
--- a/napari/_qt/qt_main_window.py
+++ b/napari/_qt/qt_main_window.py
@@ -419,6 +419,12 @@ def __init__(self, viewer: 'Viewer', *, show: bool = True):
         # Connect the Viewer and create the Main Window
         self._qt_window = _QtMainWindow(viewer)
 
+        # connect theme events before collecting plugin-provided themes
+        # to ensure icons from the plugins are generated correctly.
+        _themes.events.added.connect(self._add_theme)
+        _themes.events.added.connect(register_napari_themes)
+        _themes.events.removed.connect(self._remove_theme)
+
         # discover any themes provided by plugins
         plugin_manager.discover_themes()
         self._setup_existing_themes()
@@ -441,9 +447,6 @@ def __init__(self, viewer: 'Viewer', *, show: bool = True):
         viewer.events.help.connect(self._help_changed)
         viewer.events.title.connect(self._title_changed)
         viewer.events.theme.connect(self._update_theme)
-        _themes.events.added.connect(self._add_theme)
-        _themes.events.added.connect(register_napari_themes)
-        _themes.events.removed.connect(self._remove_theme)
 
         if show:
             self.show()


Test patch:
diff --git a/napari/plugins/_tests/test_provide_theme.py b/napari/plugins/_tests/test_provide_theme.py
--- a/napari/plugins/_tests/test_provide_theme.py
+++ b/napari/plugins/_tests/test_provide_theme.py
@@ -1,9 +1,11 @@
 """Test `napari_experimental_provide_theme` hook specification."""
 from typing import TYPE_CHECKING
+from unittest.mock import patch
 
 import pytest
 from napari_plugin_engine import napari_hook_implementation
 
+from napari._qt.qt_main_window import Window
 from napari.settings import get_settings
 from napari.utils.theme import Theme, available_themes, get_theme
 from napari.viewer import ViewerModel
@@ -37,6 +39,44 @@ def napari_experimental_provide_theme():
     viewer.theme = "dark-test"
 
 
+@patch.object(Window, "_remove_theme")
+@patch.object(Window, "_add_theme")
+def test_provide_theme_hook_registered_correctly(
+    mock_add_theme,
+    mock_remove_theme,
+    make_napari_viewer,
+    napari_plugin_manager: "NapariPluginManager",
+):
+    with pytest.warns(FutureWarning):
+        dark = get_theme("dark")
+        dark["name"] = "dark-test-2"
+
+    class TestPlugin:
+        @napari_hook_implementation
+        def napari_experimental_provide_theme():
+            return {"dark-test-2": dark}
+
+    # create instance of viewer to make sure
+    # registration and unregistration methods are called
+    viewer = make_napari_viewer()
+
+    # register theme
+    napari_plugin_manager.register(TestPlugin)
+    reg = napari_plugin_manager._theme_data["TestPlugin"]
+    assert isinstance(reg["dark-test-2"], Theme)
+
+    viewer.theme = "dark-test-2"
+
+    # triggered when theme was added
+    mock_add_theme.assert_called()
+    mock_remove_theme.assert_not_called()
+
+    # now, lets unregister the theme
+    with pytest.warns(UserWarning, match="The current theme "):
+        napari_plugin_manager.unregister("TestPlugin")
+    mock_remove_theme.assert_called()
+
+
 def test_provide_theme_hook_bad(napari_plugin_manager: "NapariPluginManager"):
     napari_plugin_manager.discover_themes()