Skip to content

Commit 533597d

Browse files
committed
add dynamic viewer theme matching
1 parent af9e17b commit 533597d

File tree

2 files changed

+51
-42
lines changed

2 files changed

+51
-42
lines changed

src/napari_matplotlib/base.py

Lines changed: 23 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import matplotlib
66
import matplotlib.style as mplstyle
77
import napari
8+
from napari.utils.events import Event
89
from napari.utils.theme import get_theme
910
from matplotlib.backends.backend_qtagg import ( # type: ignore[attr-defined]
1011
FigureCanvasQTAgg,
@@ -56,9 +57,6 @@ def __init__(
5657
self.canvas.figure.set_layout_engine("constrained")
5758
self.toolbar = NapariNavigationToolbar(self.canvas, parent=self)
5859
self._replace_toolbar_icons()
59-
# callback to update when napari theme changed
60-
# TODO: this isn't working completely (see issue #140)
61-
# most of our styling respects the theme change but not all
6260
self.viewer.events.theme.connect(self._on_napari_theme_changed)
6361

6462
self.setLayout(QVBoxLayout())
@@ -70,24 +68,6 @@ def figure(self) -> Figure:
7068
"""Matplotlib figure."""
7169
return self.canvas.figure
7270

73-
#@property
74-
#def mpl_style_sheet_path(self) -> Path:
75-
# """
76-
# Path to the set Matplotlib style sheet.
77-
# """
78-
# if self._mpl_style_sheet_path is not None:
79-
# return self._mpl_style_sheet_path
80-
# elif (_CUSTOM_STYLE_PATH).exists():
81-
# return _CUSTOM_STYLE_PATH
82-
# elif self._napari_theme_has_light_bg():
83-
# return Path(__file__).parent / "styles" / "light.mplstyle"
84-
# else:
85-
# return Path(__file__).parent / "styles" / "dark.mplstyle"
86-
87-
#@mpl_style_sheet_path.setter
88-
#def mpl_style_sheet_path(self, path: Path) -> None:
89-
# self._mpl_style_sheet_path = Path(path)
90-
9171
def add_single_axes(self) -> None:
9272
"""
9373
Add a single Axes to the figure.
@@ -99,11 +79,16 @@ def add_single_axes(self) -> None:
9979
with mplstyle.context(self.napari_theme_style_sheet):
10080
self.axes = self.figure.add_subplot()
10181

102-
def _on_napari_theme_changed(self) -> None:
82+
def _on_napari_theme_changed(self, event: Event) -> None:
10383
"""
10484
Called when the napari theme is changed.
85+
86+
Parameters
87+
----------
88+
event : napari.utils.events.Event
89+
Event that triggered the callback.
10590
"""
106-
self.napari_theme_style_sheet = style_sheet_from_theme(get_theme(self.napari_viewer.theme, as_dict=False))
91+
self.napari_theme_style_sheet = style_sheet_from_theme(get_theme(event.value, as_dict=False))
10792
self._replace_toolbar_icons()
10893

10994
def _napari_theme_has_light_bg(self) -> bool:
@@ -214,15 +199,18 @@ def current_z(self) -> int:
214199
"""
215200
return self.viewer.dims.current_step[0]
216201

217-
def _on_napari_theme_changed(self) -> None:
202+
def _on_napari_theme_changed(self, event: Event) -> None:
218203
"""Update MPL toolbar and axis styling when `napari.Viewer.theme` is changed.
219204
220-
Note:
221-
At the moment we only handle the default 'light' and 'dark' napari themes.
205+
Parameters
206+
----------
207+
event : napari.utils.events.Event
208+
Event that triggered the callback.
222209
"""
223-
super()._on_napari_theme_changed()
224-
self.clear()
225-
self.draw()
210+
super()._on_napari_theme_changed(event)
211+
# use self._draw instead of self.draw to cope with redraw while there are no
212+
# layers, this makes the self.clear() obsolete
213+
self._draw()
226214

227215
def _setup_callbacks(self) -> None:
228216
"""
@@ -256,12 +244,13 @@ def _draw(self) -> None:
256244
# Clearing axes sets new defaults, so need to make sure style is applied when
257245
# this happens
258246
with mplstyle.context(self.napari_theme_style_sheet):
247+
# everything should be done in the style context
259248
self.clear()
260-
if self.n_selected_layers in self.n_layers_input and all(
261-
isinstance(layer, self.input_layer_types) for layer in self.layers
262-
):
263-
self.draw()
264-
self.canvas.draw() # type: ignore[no-untyped-call]
249+
if self.n_selected_layers in self.n_layers_input and all(
250+
isinstance(layer, self.input_layer_types) for layer in self.layers
251+
):
252+
self.draw()
253+
self.canvas.draw() # type: ignore[no-untyped-call]
265254

266255
def clear(self) -> None:
267256
"""

src/napari_matplotlib/util.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -142,23 +142,43 @@ def from_napari_css_get_size_of(
142142

143143

144144
def style_sheet_from_theme(theme: Theme) -> Dict[str, str]:
145+
"""Translate napari theme to a matplotlib style dictionary.
146+
147+
Parameters
148+
----------
149+
theme : napari.utils.theme.Theme
150+
Napari theme object representing the theme of the current viewer.
151+
152+
Returns
153+
-------
154+
Dict[str, str]
155+
Matplotlib compatible style dictionary.
156+
"""
145157
return {
146158
'axes.edgecolor':theme.secondary.as_hex(),
147-
# alternatively "axes.facecolor" could be background color or not be set at all,
148-
# but this causes problems when saving figure as image
149-
'axes.facecolor':theme.canvas.as_hex(),
159+
#BUG: could be the same as napari canvas, but facecolors do not get
160+
# updated upon redraw for what ever reason
161+
#'axes.facecolor':theme.canvas.as_hex(),
162+
'axes.facecolor':'none',
150163
'axes.labelcolor':theme.text.as_hex(),
151164
'boxplot.boxprops.color':theme.text.as_hex(),
152165
'boxplot.capprops.color':theme.text.as_hex(),
153166
'boxplot.flierprops.markeredgecolor':theme.text.as_hex(),
154167
'boxplot.whiskerprops.color':theme.text.as_hex(),
155168
'figure.edgecolor':theme.secondary.as_hex(),
156-
# alternatively "figure.facecolor" could not be set, but this causes problems
157-
# when saving figure as image
158-
'figure.facecolor':theme.background.as_hex(),
169+
#BUG: should be the same as napari background, but facecolors do not get
170+
# updated upon redraw for what ever reason
171+
#'figure.facecolor':theme.background.as_hex(),
172+
'figure.facecolor':'none',
159173
'grid.color':theme.foreground.as_hex(),
160-
'legend.edgecolor':theme.secondary.as_hex(),
161-
'legend.facecolor':theme.background.as_hex(),
174+
#COMMENT: the hard coded colors are to match the previous behaviour
175+
# alternativly we could use the theme to style the legend as well
176+
#'legend.edgecolor':theme.secondary.as_hex(),
177+
'legend.edgecolor':'black',
178+
#'legend.facecolor':theme.background.as_hex(),
179+
'legend.facecolor':'white',
180+
#'legend.labelcolor':theme.text.as_hex()
181+
'legend.labelcolor':'black',
162182
'text.color':theme.text.as_hex(),
163183
'xtick.color':theme.secondary.as_hex(),
164184
'xtick.labelcolor':theme.text.as_hex(),

0 commit comments

Comments
 (0)