@@ -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 )
0 commit comments