-
Notifications
You must be signed in to change notification settings - Fork 22
Match plugin text/axis/icon colours to the napari theme. #138
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
Changes from all commits
499c0a7
4f8aa6d
4e198e7
b6d2894
d5c05c6
f5b4c2e
91d6937
010a02e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,9 +38,11 @@ class BaseNapariMPLWidget(QWidget): | |
|
||
def __init__( | ||
self, | ||
napari_viewer: napari.Viewer, | ||
parent: Optional[QWidget] = None, | ||
): | ||
super().__init__(parent=parent) | ||
self.viewer = napari_viewer | ||
|
||
self.canvas = FigureCanvas() | ||
|
||
|
@@ -50,6 +52,10 @@ def __init__( | |
self.canvas, parent=self | ||
) # type: ignore[no-untyped-call] | ||
self._replace_toolbar_icons() | ||
# callback to update when napari theme changed | ||
# TODO: this isn't working completely (see issue #140) | ||
# most of our styling respects the theme change but not all | ||
self.viewer.events.theme.connect(self._on_theme_change) | ||
|
||
self.setLayout(QVBoxLayout()) | ||
self.layout().addWidget(self.toolbar) | ||
|
@@ -69,25 +75,64 @@ def add_single_axes(self) -> None: | |
self.axes = self.figure.subplots() | ||
self.apply_napari_colorscheme(self.axes) | ||
|
||
@staticmethod | ||
def apply_napari_colorscheme(ax: Axes) -> None: | ||
def apply_napari_colorscheme(self, ax: Axes) -> None: | ||
"""Apply napari-compatible colorscheme to an Axes.""" | ||
# get the foreground colours from current theme | ||
theme = napari.utils.theme.get_theme(self.viewer.theme, as_dict=False) | ||
fg_colour = theme.foreground.as_hex() # fg is a muted contrast to bg | ||
text_colour = theme.text.as_hex() # text is high contrast to bg | ||
|
||
# changing color of axes background to transparent | ||
ax.set_facecolor("none") | ||
|
||
# changing colors of all axes | ||
for spine in ax.spines: | ||
ax.spines[spine].set_color("white") | ||
ax.spines[spine].set_color(fg_colour) | ||
|
||
ax.xaxis.label.set_color("white") | ||
ax.yaxis.label.set_color("white") | ||
ax.xaxis.label.set_color(text_colour) | ||
ax.yaxis.label.set_color(text_colour) | ||
|
||
# changing colors of axes labels | ||
ax.tick_params(axis="x", colors="white") | ||
ax.tick_params(axis="y", colors="white") | ||
ax.tick_params(axis="x", colors=text_colour) | ||
ax.tick_params(axis="y", colors=text_colour) | ||
|
||
def _on_theme_change(self) -> None: | ||
"""Update MPL toolbar and axis styling when `napari.Viewer.theme` is changed. | ||
|
||
Note: | ||
At the moment we only handle the default 'light' and 'dark' napari themes. | ||
""" | ||
self._replace_toolbar_icons() | ||
if self.figure.gca(): | ||
self.apply_napari_colorscheme(self.figure.gca()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure if this is the correct line, but I still don't quite have a perfect UX when switching the theme whilst the widgets are open... Screen.Recording.2023-05-31.at.16.51.00.movRe-opening or doing something to the plot seems to fix. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A bit of a dive into the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 😞 napari.settings.get_settings().appearance.events.theme.connect(
self._on_theme_change
) gives the same behaviour. I wonder if it might be a different way to do the same thing as
The theme-change event is undocumented* (scroll a bit from the start of § Viewer events) so I'd be up for asking and/or fixing the docs for napari. *) It's also duplicated: that page is made via a script in napari/docs. |
||
|
||
def _theme_has_light_bg(self) -> bool: | ||
""" | ||
Does this theme have a light background? | ||
|
||
Returns | ||
------- | ||
bool | ||
True if theme's background colour has hsl lighter than 50%, False if darker. | ||
""" | ||
theme = napari.utils.theme.get_theme(self.viewer.theme, as_dict=False) | ||
_, _, bg_lightness = theme.background.as_hsl_tuple() | ||
return bg_lightness > 0.5 | ||
|
||
def _get_path_to_icon(self) -> Path: | ||
""" | ||
Get the icons directory (which is theme-dependent). | ||
""" | ||
if self._theme_has_light_bg(): | ||
return ICON_ROOT / "black" | ||
else: | ||
return ICON_ROOT / "white" | ||
dstansby marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
def _replace_toolbar_icons(self) -> None: | ||
# Modify toolbar icons and some tooltips | ||
""" | ||
Modifies toolbar icons to match the napari theme, and add some tooltips. | ||
""" | ||
icon_dir = self._get_path_to_icon() | ||
for action in self.toolbar.actions(): | ||
text = action.text() | ||
if text == "Pan": | ||
|
@@ -101,7 +146,7 @@ def _replace_toolbar_icons(self) -> None: | |
"Click again to deactivate" | ||
) | ||
if len(text) > 0: # i.e. not a separator item | ||
icon_path = os.path.join(ICON_ROOT, text + ".png") | ||
icon_path = os.path.join(icon_dir, text + ".png") | ||
action.setIcon(QIcon(icon_path)) | ||
|
||
|
||
|
@@ -138,9 +183,7 @@ def __init__( | |
napari_viewer: napari.viewer.Viewer, | ||
parent: Optional[QWidget] = None, | ||
): | ||
super().__init__(parent=parent) | ||
|
||
self.viewer = napari_viewer | ||
super().__init__(napari_viewer=napari_viewer, parent=parent) | ||
self._setup_callbacks() | ||
self.layers: List[napari.layers.Layer] = [] | ||
|
||
|
@@ -235,22 +278,24 @@ def __init__(self, *args, **kwargs): # type: ignore[no-untyped-def] | |
def _update_buttons_checked(self) -> None: | ||
"""Update toggle tool icons when selected/unselected.""" | ||
super()._update_buttons_checked() | ||
icon_dir = self.parentWidget()._get_path_to_icon() | ||
|
||
# changes pan/zoom icons depending on state (checked or not) | ||
if "pan" in self._actions: | ||
if self._actions["pan"].isChecked(): | ||
self._actions["pan"].setIcon( | ||
QIcon(os.path.join(ICON_ROOT, "Pan_checked.png")) | ||
QIcon(os.path.join(icon_dir, "Pan_checked.png")) | ||
) | ||
else: | ||
self._actions["pan"].setIcon( | ||
QIcon(os.path.join(ICON_ROOT, "Pan.png")) | ||
QIcon(os.path.join(icon_dir, "Pan.png")) | ||
) | ||
if "zoom" in self._actions: | ||
if self._actions["zoom"].isChecked(): | ||
self._actions["zoom"].setIcon( | ||
QIcon(os.path.join(ICON_ROOT, "Zoom_checked.png")) | ||
QIcon(os.path.join(icon_dir, "Zoom_checked.png")) | ||
) | ||
else: | ||
self._actions["zoom"].setIcon( | ||
QIcon(os.path.join(ICON_ROOT, "Zoom.png")) | ||
QIcon(os.path.join(icon_dir, "Zoom.png")) | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import pytest | ||
|
||
from napari_matplotlib.base import NapariMPLWidget | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"theme_name, expected_icons", | ||
[("dark", "white"), ("light", "black")], | ||
) | ||
def test_theme_mpl_toolbar_icons( | ||
make_napari_viewer, theme_name, expected_icons | ||
): | ||
"""Check that the icons are taken from the correct folder for each napari theme.""" | ||
viewer = make_napari_viewer() | ||
viewer.theme = theme_name | ||
path_to_icons = NapariMPLWidget(viewer)._get_path_to_icon() | ||
assert path_to_icons.exists(), "The theme points to non-existant icons." | ||
assert ( | ||
path_to_icons.stem == expected_icons | ||
), "The theme is selecting unexpected icons." |
Uh oh!
There was an error while loading. Please reload this page.