3737
3838import datetime
3939import platform
40- from collections import OrderedDict
40+ from collections import OrderedDict , defaultdict
4141from contextlib import contextmanager
4242from functools import partial
4343
5555 channel_indices_by_type ,
5656 pick_types ,
5757)
58+ from ..defaults import DEFAULTS
5859from ..fixes import _close_event
5960from ..utils import Bunch , _click_ch_name , check_version , logger
6061from ._figure import BrowserBase
8283ANNOTATION_FIG_CHECKBOX_COLUMN_W = 0.5
8384_OLD_BUTTONS = not check_version ("matplotlib" , "3.7" )
8485
86+ # DARK THEME COLORS
87+ # These colors are duplicated from mne-qt-browser (_dark_dict). If you change one, make
88+ # sure to update the other as well.
89+ _DARK_BGCOLOR = "#1e1e1e"
90+ _DARK_FGCOLOR = "#d0d0d0"
91+ _DARK_BAD_COLOR = "#696969"
92+ _DARK_BUTTON_COLOR = "#3a3a3a"
93+ _DARK_EVENT_COLOR = "#008b8b"
94+ _DARK_CHANNEL_OVERRIDES = {
95+ # "k" (black) channels → white in dark mode
96+ "eeg" : "#ffffff" ,
97+ "eog" : "#ffffff" ,
98+ "emg" : "#ffffff" ,
99+ "misc" : "#ffffff" ,
100+ "stim" : "#ffffff" ,
101+ "resp" : "#ffffff" ,
102+ "chpi" : "#ffffff" ,
103+ "exci" : "#ffffff" ,
104+ "ias" : "#ffffff" ,
105+ "syst" : "#ffffff" ,
106+ "dipole" : "#ffffff" ,
107+ "gof" : "#ffffff" ,
108+ "bio" : "#ffffff" ,
109+ "ecog" : "#ffffff" ,
110+ "fnirs_cw_amplitude" : "#ffffff" ,
111+ "fnirs_fd_ac_amplitude" : "#ffffff" ,
112+ "fnirs_fd_phase" : "#ffffff" ,
113+ "fnirs_od" : "#ffffff" ,
114+ "csd" : "#ffffff" ,
115+ "whitened" : "#ffffff" ,
116+ "eyegaze" : "#ffffff" ,
117+ "pupil" : "#ffffff" ,
118+ "mag" : "#add8e6" ,
119+ "grad" : "#6495ed" ,
120+ "ref_meg" : "#b0c4de" ,
121+ "ecg" : "#ee82ee" ,
122+ "seeg" : "#f4a460" ,
123+ "dbs" : "#20b2aa" ,
124+ "hbo" : "#ff69b4" ,
125+ "hbr" : "#6495ed" ,
126+ "gsr" : "#b0b055" ,
127+ "temperature" : "#aa6666" ,
128+ }
129+
130+
131+ def _resolve_mpl_theme (theme ):
132+ """Resolve "auto" theme to "light" or "dark" using darkdetect."""
133+ if theme == "auto" :
134+ from .backends ._utils import _qt_detect_theme
135+
136+ return _qt_detect_theme ()
137+ return theme
138+
139+
140+ def _apply_mpl_theme_to_kwargs (kwargs ):
141+ """Apply dark theme colors to browser kwargs in-place."""
142+ theme = kwargs .get ("theme" , "auto" )
143+ if _resolve_mpl_theme (theme ) != "dark" :
144+ return
145+ # bgcolor: override if absent or still at the light default "w"
146+ if kwargs .get ("bgcolor" , "w" ) == "w" :
147+ kwargs ["bgcolor" ] = _DARK_BGCOLOR
148+ # fgcolor: inject if not explicitly set by the caller
149+ kwargs .setdefault ("fgcolor" , _DARK_FGCOLOR )
150+ # bad channel colors: override if still at the light default
151+ for key in ("bad_color" , "ch_color_bad" ):
152+ if kwargs .get (key , "lightgray" ) == "lightgray" :
153+ kwargs [key ] = _DARK_BAD_COLOR
154+ # channel type colors: override only entries still at their light defaults
155+ if "ch_color_dict" in kwargs :
156+ light_defaults = DEFAULTS ["color" ]
157+ for ch_type , dark_color in _DARK_CHANNEL_OVERRIDES .items ():
158+ if ch_type in kwargs ["ch_color_dict" ] and kwargs ["ch_color_dict" ][
159+ ch_type
160+ ] == light_defaults .get (ch_type ):
161+ kwargs ["ch_color_dict" ][ch_type ] = dark_color
162+ # event color: override if still at the light default "cyan"
163+ if "event_color_dict" in kwargs :
164+ d = kwargs ["event_color_dict" ]
165+ if hasattr (d , "default_factory" ) and d .default_factory is not None :
166+ if d .default_factory () == "cyan" :
167+ new_d = defaultdict (lambda : _DARK_EVENT_COLOR )
168+ new_d .update (d )
169+ kwargs ["event_color_dict" ] = new_d
170+
85171
86172class MNEFigure (Figure ):
87173 """Base class for 2D figures & dialogs; wraps matplotlib.figure.Figure."""
@@ -360,6 +446,8 @@ def __init__(self, inst, figsize, ica=None, xlabel="Time (s)", **kwargs):
360446
361447 kwargs .update ({"inst" : inst , "figsize" : figsize , "ica" : ica , "xlabel" : xlabel })
362448
449+ _apply_mpl_theme_to_kwargs (kwargs )
450+
363451 BrowserBase .__init__ (self , ** kwargs )
364452 MNEFigure .__init__ (self , ** kwargs )
365453
@@ -567,6 +655,24 @@ def __init__(self, inst, figsize, ica=None, xlabel="Time (s)", **kwargs):
567655 vline_text = vline_text ,
568656 )
569657
658+ # apply theme colors (dark mode only)
659+ if self .mne .bgcolor == _DARK_BGCOLOR :
660+ self .patch .set_facecolor (self .mne .bgcolor )
661+ for _ax in (ax_hscroll , ax_vscroll ):
662+ _ax .set_facecolor (self .mne .bgcolor )
663+ for _ax in (ax_main , ax_hscroll ):
664+ for _spine in _ax .spines .values ():
665+ _spine .set_color (self .mne .fgcolor )
666+ _ax .tick_params (colors = self .mne .fgcolor , labelcolor = self .mne .fgcolor )
667+ _ax .xaxis .label .set_color (self .mne .fgcolor )
668+ self .mne .button_help .ax .set_facecolor (_DARK_BUTTON_COLOR )
669+ self .mne .button_help .color = _DARK_BUTTON_COLOR
670+ self .mne .button_help .label .set_color (self .mne .fgcolor )
671+ if ax_proj is not None :
672+ self .mne .button_proj .ax .set_facecolor (_DARK_BUTTON_COLOR )
673+ self .mne .button_proj .color = _DARK_BUTTON_COLOR
674+ self .mne .button_proj .label .set_color (self .mne .fgcolor )
675+
570676 def _get_size (self ):
571677 return self .get_size_inches ()
572678
@@ -873,6 +979,8 @@ def _create_ch_context_fig(self, idx):
873979
874980 def _new_child_figure (self , fig_name , * , layout = None , ** kwargs ):
875981 """Instantiate a new MNE dialog figure (with event listeners)."""
982+ kwargs .setdefault ("bgcolor" , self .mne .bgcolor )
983+ kwargs .setdefault ("fgcolor" , self .mne .fgcolor )
876984 fig = _figure (
877985 toolbar = False ,
878986 parent_fig = self ,
@@ -919,8 +1027,13 @@ def _create_help_fig(self):
9191027 ax = fig .add_axes ((0.01 , 0.01 , 0.98 , 0.98 ))
9201028 ax .set_axis_off ()
9211029 kwargs = dict (va = "top" , linespacing = 1.5 , usetex = False )
922- ax .text (0.42 , 1 , keys , ma = "right" , ha = "right" , ** kwargs )
923- ax .text (0.42 , 1 , vals , ma = "left" , ha = "left" , ** kwargs )
1030+ txt_keys = ax .text (0.42 , 1 , keys , ma = "right" , ha = "right" , ** kwargs )
1031+ txt_vals = ax .text (0.42 , 1 , vals , ma = "left" , ha = "left" , ** kwargs )
1032+ # apply theme colors
1033+ fig .patch .set_facecolor (fig .mne .bgcolor )
1034+ ax .set_facecolor (fig .mne .bgcolor )
1035+ txt_keys .set_color (fig .mne .fgcolor )
1036+ txt_vals .set_color (fig .mne .fgcolor )
9241037
9251038 def _toggle_help_fig (self , event ):
9261039 """Show/hide the help dialog window."""
@@ -1060,7 +1173,7 @@ def _create_annotation_fig(self):
10601173 r"$\mathbf{Esc:}$ exit annotation mode & close this window" ,
10611174 ]
10621175 )
1063- instructions_ax .text (
1176+ instr_text = instructions_ax .text (
10641177 0 , 1 , instructions , va = "top" , ha = "left" , linespacing = 1.7 , usetex = False
10651178 ) # force use of MPL mathtext parser
10661179 instructions_ax .set_axis_off ()
@@ -1070,7 +1183,7 @@ def _create_annotation_fig(self):
10701183 size = Fixed (3 * ANNOTATION_FIG_PAD ),
10711184 pad = Fixed (ANNOTATION_FIG_PAD ),
10721185 )
1073- text_entry_ax .text (
1186+ new_label_text = text_entry_ax .text (
10741187 0.4 , 0.5 , "New label:" , va = "center" , ha = "right" , weight = "bold"
10751188 )
10761189 fig .label = text_entry_ax .text (0.5 , 0.5 , "BAD_" , va = "center" , ha = "left" )
@@ -1089,7 +1202,7 @@ def _create_annotation_fig(self):
10891202 drag_ax = div .append_axes (
10901203 "bottom" , size = Fixed (drag_ax_height ), pad = Fixed (ANNOTATION_FIG_PAD )
10911204 )
1092- check_kwargs = _get_check_kwargs ()
1205+ check_kwargs = _get_check_kwargs (fgcolor = fig . mne . fgcolor )
10931206 checkbox = CheckButtons (
10941207 drag_ax ,
10951208 labels = ("Draggable edges?" ,),
@@ -1118,6 +1231,12 @@ def _create_annotation_fig(self):
11181231 text .set (position = (3 * _pad + _size , 0.45 ), va = "center" )
11191232 for artist in lines + (rect , text ):
11201233 artist .set_transform (drag_ax .transData )
1234+ rect .set_edgecolor (fig .mne .fgcolor )
1235+ for line in lines :
1236+ line .set_color (fig .mne .fgcolor )
1237+ text .set_color (fig .mne .fgcolor )
1238+ else :
1239+ checkbox .labels [0 ].set_color (fig .mne .fgcolor )
11211240 # setup interactivity in plot window
11221241 if fig .mne .radio_ax .buttons is None :
11231242 col = "#ff0000"
@@ -1138,6 +1257,18 @@ def _create_annotation_fig(self):
11381257 "motion_notify_event" , self ._hover
11391258 )
11401259
1260+ # apply theme colors to annotation dialog (dark mode only)
1261+ if fig .mne .bgcolor == _DARK_BGCOLOR :
1262+ fig .patch .set_facecolor (fig .mne .bgcolor )
1263+ for _ax in fig .axes :
1264+ _ax .set_facecolor (fig .mne .bgcolor )
1265+ fig .button .ax .set_facecolor (_DARK_BUTTON_COLOR )
1266+ fig .button .color = _DARK_BUTTON_COLOR
1267+ for _artist in (instr_text , new_label_text , fig .label , fig .button .label ):
1268+ _artist .set_color (fig .mne .fgcolor )
1269+ fig .mne .radio_ax ._left_title .set_color (fig .mne .fgcolor )
1270+ fig .mne .show_hide_ax ._right_title .set_color (fig .mne .fgcolor )
1271+
11411272 def _toggle_visible_annotations (self , event ):
11421273 """Enable/disable display of annotations on a per-label basis."""
11431274 checkboxes = self .mne .show_hide_annotation_checkboxes
@@ -1167,7 +1298,7 @@ def _update_annotation_fig(self, *, draw=True):
11671298 # populate center axes with labels & radio buttons
11681299 ax .clear ()
11691300 title = "Existing labels:" if len (labels ) else "No existing labels"
1170- ax .set_title (title , size = None , loc = "left" )
1301+ ax .set_title (title , size = None , loc = "left" ). set_color ( fig . mne . fgcolor )
11711302 if len (labels ):
11721303 if _OLD_BUTTONS :
11731304 ax .buttons = RadioButtons (ax , labels , ** _BLIT_KWARGS )
@@ -1200,6 +1331,9 @@ def _update_annotation_fig(self, *, draw=True):
12001331 )
12011332 else :
12021333 ax .buttons = None
1334+ if ax .buttons is not None :
1335+ for _lbl in ax .buttons .labels :
1336+ _lbl .set_color (fig .mne .fgcolor )
12031337 # adjust xlim to keep equal aspect & full width (keep circles round)
12041338 aspect = (
12051339 ANNOTATION_FIG_W - ANNOTATION_FIG_CHECKBOX_COLUMN_W - 3 * ANNOTATION_FIG_PAD
@@ -1228,14 +1362,16 @@ def _update_annotation_fig(self, *, draw=True):
12281362 check_values .update (self .mne .visible_annotations ) # existing checks
12291363 actives = [check_values [label ] for label in labels ]
12301364 # regenerate checkboxes
1231- check_kwargs = _get_check_kwargs ()
1365+ check_kwargs = _get_check_kwargs (fgcolor = fig . mne . fgcolor )
12321366 checkboxes = CheckButtons (
12331367 ax = fig .mne .show_hide_ax , labels = labels , actives = actives , ** check_kwargs
12341368 )
12351369 checkboxes .on_clicked (self ._toggle_visible_annotations )
12361370 # add title, hide labels
12371371 show_hide_title = "show/\n hide " if len (labels ) else ""
1238- show_hide_ax .set_title (show_hide_title , size = None , loc = "right" )
1372+ show_hide_ax .set_title (show_hide_title , size = None , loc = "right" ).set_color (
1373+ fig .mne .fgcolor
1374+ )
12391375 for label in checkboxes .labels :
12401376 label .set_visible (False )
12411377 show_hide_ax .set_axis_off ()
@@ -1253,9 +1389,11 @@ def _update_annotation_fig(self, *, draw=True):
12531389 bounds = (aspect , bbox .ymin , - bbox .width , bbox .height )
12541390 rect .set_bounds (bounds )
12551391 rect .set_clip_on (False )
1392+ rect .set_edgecolor (fig .mne .fgcolor )
12561393 for line in np .array (checkboxes .lines ).ravel ():
12571394 line .set_transform (show_hide_ax .transData )
12581395 line .set_xdata (aspect + 0.05 - np .array (line .get_xdata ()))
1396+ line .set_color (fig .mne .fgcolor )
12591397 # store state
12601398 self .mne .visible_annotations = check_values
12611399 self .mne .show_hide_annotation_checkboxes = checkboxes
@@ -1301,7 +1439,7 @@ def _add_annotation_label(self, event):
13011439 f"Existing labels: (duplicate label: { repr (text )} )" ,
13021440 size = None ,
13031441 loc = "left" ,
1304- )
1442+ ). set_color ( self . mne . fig_annotation . mne . fgcolor )
13051443 self .mne .fig_annotation .canvas .draw ()
13061444 return
13071445 self .mne .new_annotation_labels .append (text )
@@ -1602,7 +1740,7 @@ def _create_proj_fig(self):
16021740 ax ,
16031741 labels = labels ,
16041742 actives = self .mne .projs_on ,
1605- ** _get_check_kwargs (labels = labels ),
1743+ ** _get_check_kwargs (labels = labels , fgcolor = fig . mne . fgcolor ),
16061744 )
16071745 # gray-out already applied projectors
16081746 if _OLD_BUTTONS :
@@ -1909,6 +2047,8 @@ def _xtick_formatter(self, x, pos=None, ax_type="main"):
19092047 return str (round (x , digits ))
19102048 # format as timestamp
19112049 meas_date = self .mne .inst .info ["meas_date" ]
2050+ if meas_date is None :
2051+ return str (round (x , digits ))
19122052 first_time = datetime .timedelta (seconds = self .mne .inst .first_time )
19132053 xtime = datetime .timedelta (seconds = x )
19142054 xdatetime = meas_date + first_time + xtime
@@ -1919,6 +2059,8 @@ def _xtick_formatter(self, x, pos=None, ax_type="main"):
19192059
19202060 def _toggle_time_format (self ):
19212061 if self .mne .time_format == "float" :
2062+ if self .mne .inst .info ["meas_date" ] is None :
2063+ return # can't show clock time without a measurement date
19222064 self .mne .time_format = "clock"
19232065 x_axis_label = "Time (HH:MM:SS)"
19242066 else :
@@ -2517,13 +2659,17 @@ def _init_browser(**kwargs):
25172659 return fig
25182660
25192661
2520- def _get_check_kwargs (labels = None ):
2662+ def _get_check_kwargs (labels = None , fgcolor = None ):
25212663 check_kwargs = dict ()
25222664 if not _OLD_BUTTONS :
25232665 check_kwargs .update (
25242666 check_props = dict (s = 144 , clip_on = False ),
25252667 frame_props = dict (s = 144 , clip_on = False ),
25262668 )
2669+ if fgcolor is not None :
2670+ # Color check marks (unfilled 'x' marker uses facecolor) and frame borders
2671+ check_kwargs ["check_props" ].update (facecolor = fgcolor )
2672+ check_kwargs ["frame_props" ].update (edgecolor = fgcolor )
25272673 if labels is not None :
25282674 textcolor = list ()
25292675 checkcolor = list ()
@@ -2532,8 +2678,9 @@ def _get_check_kwargs(labels=None):
25322678 textcolor .append ("0.5" )
25332679 checkcolor .append ("0.7" )
25342680 else :
2535- textcolor .append ("k" )
2536- checkcolor .append ("k" )
2681+ _clr = fgcolor if fgcolor is not None else "k"
2682+ textcolor .append (_clr )
2683+ checkcolor .append (_clr )
25372684 check_kwargs ["check_props" ].update (facecolor = checkcolor , linewidth = 1 )
25382685 check_kwargs ["frame_props" ].update (edgecolor = checkcolor , linewidth = 1 )
25392686 check_kwargs ["label_props" ] = dict (color = textcolor )
0 commit comments