Skip to content

Commit 499c0a7

Browse files
committed
Solves #86.
1 parent cb940ab commit 499c0a7

22 files changed

+69
-16
lines changed

src/napari_matplotlib/base.py

+50-16
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,11 @@ class BaseNapariMPLWidget(QWidget):
3838

3939
def __init__(
4040
self,
41+
napari_viewer: napari.Viewer,
4142
parent: Optional[QWidget] = None,
4243
):
4344
super().__init__(parent=parent)
45+
self.viewer = napari_viewer
4446

4547
self.canvas = FigureCanvas()
4648

@@ -50,6 +52,8 @@ def __init__(
5052
self.canvas, parent=self
5153
) # type: ignore[no-untyped-call]
5254
self._replace_toolbar_icons()
55+
# callback to update when napari theme changed
56+
self.viewer.events.theme.connect(self._on_theme_change)
5357

5458
self.setLayout(QVBoxLayout())
5559
self.layout().addWidget(self.toolbar)
@@ -69,25 +73,55 @@ def add_single_axes(self) -> None:
6973
self.axes = self.figure.subplots()
7074
self.apply_napari_colorscheme(self.axes)
7175

72-
@staticmethod
73-
def apply_napari_colorscheme(ax: Axes) -> None:
76+
def apply_napari_colorscheme(self, ax: Axes) -> None:
7477
"""Apply napari-compatible colorscheme to an Axes."""
78+
# get the foreground colours from current theme
79+
theme = napari.utils.theme.get_theme(self.viewer.theme, as_dict=False)
80+
fg = theme.foreground.as_hex() # fg is a muted contrast to bg
81+
tx = theme.text.as_hex() # text is high contrast to bg
82+
7583
# changing color of axes background to transparent
7684
ax.set_facecolor("none")
7785

7886
# changing colors of all axes
7987
for spine in ax.spines:
80-
ax.spines[spine].set_color("white")
88+
ax.spines[spine].set_color(fg)
8189

82-
ax.xaxis.label.set_color("white")
83-
ax.yaxis.label.set_color("white")
90+
ax.xaxis.label.set_color(tx)
91+
ax.yaxis.label.set_color(tx)
8492

8593
# changing colors of axes labels
86-
ax.tick_params(axis="x", colors="white")
87-
ax.tick_params(axis="y", colors="white")
94+
ax.tick_params(axis="x", colors=tx)
95+
ax.tick_params(axis="y", colors=tx)
96+
97+
def _on_theme_change(self) -> None:
98+
"""
99+
Update the MPL toolbar and axis styling when the `napari.Viewer.theme` is changed.
100+
101+
Note: At the moment we only recognise the default 'light' and 'dark' napari themes.
102+
"""
103+
self._replace_toolbar_icons()
104+
if self.figure.gca():
105+
self.apply_napari_colorscheme(self.figure.gca())
106+
107+
def _get_path_to_icon(self) -> Path:
108+
"""
109+
Get the icons directory (which is theme-dependent).
110+
"""
111+
# TODO: can make this more robust by doing some RGB tricks to figure out
112+
# whether white or black icons are going to be more visible given the
113+
# theme.background
114+
islight = self.viewer.theme == "light"
115+
if islight:
116+
return ICON_ROOT / "black"
117+
else:
118+
return ICON_ROOT / "white"
88119

89120
def _replace_toolbar_icons(self) -> None:
90-
# Modify toolbar icons and some tooltips
121+
"""
122+
Modifies toolbar icons to match the napari theme, and add some tooltips.
123+
"""
124+
icon_dir = self._get_path_to_icon()
91125
for action in self.toolbar.actions():
92126
text = action.text()
93127
if text == "Pan":
@@ -101,7 +135,7 @@ def _replace_toolbar_icons(self) -> None:
101135
"Click again to deactivate"
102136
)
103137
if len(text) > 0: # i.e. not a separator item
104-
icon_path = os.path.join(ICON_ROOT, text + ".png")
138+
icon_path = os.path.join(icon_dir, text + ".png")
105139
action.setIcon(QIcon(icon_path))
106140

107141

@@ -138,9 +172,7 @@ def __init__(
138172
napari_viewer: napari.viewer.Viewer,
139173
parent: Optional[QWidget] = None,
140174
):
141-
super().__init__(parent=parent)
142-
143-
self.viewer = napari_viewer
175+
super().__init__(napari_viewer=napari_viewer, parent=parent)
144176
self._setup_callbacks()
145177
self.layers: List[napari.layers.Layer] = []
146178

@@ -234,22 +266,24 @@ def __init__(self, *args, **kwargs): # type: ignore[no-untyped-def]
234266
def _update_buttons_checked(self) -> None:
235267
"""Update toggle tool icons when selected/unselected."""
236268
super()._update_buttons_checked()
269+
icon_dir = self.parentWidget()._get_path_to_icon()
270+
237271
# changes pan/zoom icons depending on state (checked or not)
238272
if "pan" in self._actions:
239273
if self._actions["pan"].isChecked():
240274
self._actions["pan"].setIcon(
241-
QIcon(os.path.join(ICON_ROOT, "Pan_checked.png"))
275+
QIcon(os.path.join(icon_dir, "Pan_checked.png"))
242276
)
243277
else:
244278
self._actions["pan"].setIcon(
245-
QIcon(os.path.join(ICON_ROOT, "Pan.png"))
279+
QIcon(os.path.join(icon_dir, "Pan.png"))
246280
)
247281
if "zoom" in self._actions:
248282
if self._actions["zoom"].isChecked():
249283
self._actions["zoom"].setIcon(
250-
QIcon(os.path.join(ICON_ROOT, "Zoom_checked.png"))
284+
QIcon(os.path.join(icon_dir, "Zoom_checked.png"))
251285
)
252286
else:
253287
self._actions["zoom"].setIcon(
254-
QIcon(os.path.join(ICON_ROOT, "Zoom.png"))
288+
QIcon(os.path.join(icon_dir, "Zoom.png"))
255289
)
6.76 KB
Loading
7.08 KB
Loading
6.65 KB
Loading
7.24 KB
Loading
7.14 KB
Loading
Loading
7.54 KB
Loading
6.88 KB
Loading
8.17 KB
Loading
Loading
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import pytest
2+
from napari_matplotlib.base import NapariMPLWidget
3+
4+
5+
@pytest.mark.parametrize(
6+
"theme_name, expected_icons",
7+
[("dark", "white"), ("light", "black")],
8+
)
9+
def test_theme_mpl_toolbar_icons(
10+
make_napari_viewer, theme_name, expected_icons
11+
):
12+
"""Check that the icons are taken from the correct folder for each napari theme."""
13+
viewer = make_napari_viewer()
14+
viewer.theme = theme_name
15+
path_to_icons = NapariMPLWidget(viewer)._get_path_to_icon()
16+
assert path_to_icons.exists(), "The theme points to non-existant icons."
17+
assert (
18+
path_to_icons.stem == expected_icons
19+
), "The theme is selecting unexpected icons."

0 commit comments

Comments
 (0)