Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions mne/gui/tests/test_gui_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ def test_gui_api_notebook(renderer_notebook, nbexec, *, backend="qt"):
renderer._layout_add_widget(central_layout, widget, row=0, col=0)
renderer._window_initialize(window=window, central_layout=central_layout)

# an OS light/dark mode switch re-applies the theme (gh-9182)
from qtpy.QtCore import QEvent
from qtpy.QtGui import QIcon

QIcon.setThemeName("bogus")
window.event(QEvent(QEvent.PaletteChange))
assert QIcon.themeName() in ("dark", "light")

from unittest.mock import Mock

mock = Mock()
Expand Down
68 changes: 40 additions & 28 deletions mne/viz/backends/_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,17 +224,7 @@ def _set_focus(self):
self.setFocus()

def _set_theme(self, theme=None):
if theme is None:
default_theme = _qt_detect_theme()
else:
default_theme = theme
theme = get_config("MNE_3D_OPTION_THEME", default_theme)
stylesheet = _qt_get_stylesheet(theme)
self.setStyleSheet(stylesheet)
if _qt_is_dark(self):
QIcon.setThemeName("dark")
else:
QIcon.setThemeName("light")
_qt_set_theme(self, theme)

def _set_size(self, width=None, height=None):
if width:
Expand Down Expand Up @@ -682,22 +672,51 @@ def _set_size(self, width=None, height=None):
# -------


# In theory we should be able to set the theme later (e.g., in
# _window_initialize() below), but at least on Qt6 this has to be done
# earlier. So let's do it immediately upon instantiation of the QMainWindow
# class (see _AppWindow.__init__'s self._set_theme() call below).
# TODO: This should eventually allow us to handle
# https://github.com/mne-tools/mne-python/issues/9182
def _qt_set_theme(window, theme=None):
"""(Re)apply a theme to a window, remembering any explicitly requested one."""
if theme is not None:
window._mne_theme = theme
theme = getattr(window, "_mne_theme", None)
if theme is None:
theme = _qt_detect_theme()
theme = get_config("MNE_3D_OPTION_THEME", theme)
stylesheet = _qt_get_stylesheet(theme)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this logic seems a bit convoluted:

  1. if a theme was passed in, set window._mne_theme to it
  2. replace value of theme variable with window._mne_theme, or
  3. if it's None, with _qt_detect_theme()
  4. override value of theme with the config variable.

I think I get why we do step 4 (user set an MNE-specific preference, AKA "don't follow system theme"). But the other 3 steps I can't quite follow... Could this be re-ordered, simplified, or just commented a bit better? Naively I'd think that something like this would be clearer:

theme = (
    get_config("MNE_3D_OPTION_THEME", None)
    or theme
    or getattr(window, "_mne_theme", None)
    or _qt_detect_theme()
)

...but IDK what the significance is of changing window._mne_theme to be the passed-in value and not one of the other values

# our own setStyleSheet emits PaletteChange; without this the signal recurses
window._mne_theme_updating = True
try:
# re-setting an unchanged sheet costs styled children (sliders) native rendering

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this comment (specifically the verb "costs")

if stylesheet != window.styleSheet():
window.setStyleSheet(stylesheet)
QIcon.setThemeName("dark" if _qt_is_dark(window) else "light")
# not a no-op: setStyleSheet re-parses, re-resolving palette(...) refs that a
# palette change alone leaves stale
for widget in window.findChildren(QWidget):
if child_stylesheet := widget.styleSheet():
widget.setStyleSheet(child_stylesheet)
finally:
window._mne_theme_updating = False


class _MNEMainWindow(MainWindow):
signal_theme_change = Signal()

def __init__(self, parent=None, title=None, size=None):
MainWindow.__init__(self, parent=parent, title=title, size=size)
self.setAttribute(Qt.WA_ShowWithoutActivating, True)
self.setAttribute(Qt.WA_DeleteOnClose, True)
self._mne_theme = None
self._mne_theme_updating = False
from . import renderer

if renderer.MNE_3D_BACKEND_TESTING:
self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnBottomHint)

def event(self, ev):
"""Turn OS light/dark mode switches into a signal (macOS only for now)."""
if ev.type() == QEvent.PaletteChange and not self._mne_theme_updating:
self.signal_theme_change.emit()
return super().event(ev)


class _AppWindow(_AbstractAppWindow, _Widget, _MNEMainWindow, metaclass=_BaseWidget):
def __init__(self, size=None, fullscreen=False):
Expand All @@ -710,6 +729,7 @@ def __init__(self, size=None, fullscreen=False):
self.setWindowState(Qt.WindowFullScreen)

self._set_theme()
self.signal_theme_change.connect(self._set_theme)
self.setLocale(QLocale(QLocale.Language.English))
self.signal_close.connect(self._clean)

Expand Down Expand Up @@ -1514,6 +1534,8 @@ def _window_initialize(self, *, window=None, central_layout=None, fullscreen=Fal
central_widget.setLayout(central_layout)
self._window_load_icons()
self._window_set_theme()
if hasattr(self._window, "signal_theme_change"): # not for a foreign window
self._window.signal_theme_change.connect(self._window_set_theme)
self._window.setLocale(QLocale(QLocale.Language.English))
self._window.signal_close.connect(self._window_clean)
self._window_before_close_callbacks = list()
Expand Down Expand Up @@ -1675,17 +1697,7 @@ def _window_ensure_minimum_sizes(self):
_qt_activate_layouts(self._window, self._interactor)

def _window_set_theme(self, theme=None):
if theme is None:
default_theme = _qt_detect_theme()
else:
default_theme = theme
theme = get_config("MNE_3D_OPTION_THEME", default_theme)
stylesheet = _qt_get_stylesheet(theme)
self._window.setStyleSheet(stylesheet)
if _qt_is_dark(self._window):
QIcon.setThemeName("dark")
else:
QIcon.setThemeName("light")
_qt_set_theme(self._window, theme)

def _window_create(self):
return _MNEMainWindow()
Expand Down
Loading