"""Qt implementation of _Renderer and GUI."""

# Authors: The MNE-Python contributors.
# License: BSD-3-Clause
# Copyright the MNE-Python contributors.

import os
import platform
import sys
import weakref
from contextlib import contextmanager

# importing anything from qtpy forces a Qt API choice as a side effect, which is then
# used by matplotlib and pyvistaqt
from qtpy import API_NAME  # noqa: F401, isort: skip

import pyvista
from matplotlib.backends.backend_qtagg import FigureCanvas
from matplotlib.figure import Figure
from pyvistaqt.plotting import FileDialog, MainWindow
from qtpy.QtCore import (
    QEvent,
    QLibraryInfo,
    QLocale,
    QObject,
    Qt,
    QTimer,
    # non-object-based-abstraction-only, deprecate
    Signal,
)
from qtpy.QtGui import QCursor, QIcon, QKeyEvent
from qtpy.QtWidgets import (
    QButtonGroup,
    QCheckBox,
    QComboBox,
    # non-object-based-abstraction-only, deprecate
    QDockWidget,
    QDoubleSpinBox,
    QFileDialog,
    QGridLayout,
    QGroupBox,
    QHBoxLayout,
    QLabel,
    QLayout,
    QLineEdit,
    QMenuBar,
    QMessageBox,
    QProgressBar,
    QPushButton,
    QRadioButton,
    QScrollArea,
    QSizePolicy,
    QSlider,
    QSpinBox,
    QStyle,
    QStyleOptionSlider,
    QToolButton,
    QVBoxLayout,
    QWidget,
)

from ...fixes import _compare_version
from ...utils import _check_option, get_config
from ..utils import safe_event
from ._abstract import (
    _AbstractAction,
    _AbstractAppWindow,
    _AbstractBrainMplCanvas,
    _AbstractButton,
    _AbstractCanvas,
    _AbstractCheckBox,
    _AbstractComboBox,
    _AbstractDialog,
    _AbstractDock,
    _AbstractFileButton,
    _AbstractGridLayout,
    _AbstractGroupBox,
    _AbstractHBoxLayout,
    _AbstractKeyPress,
    _AbstractLabel,
    _AbstractLayout,
    _AbstractMenuBar,
    _AbstractMplCanvas,
    _AbstractMplInterface,
    _AbstractPlayback,
    _AbstractPlayMenu,
    _AbstractPopup,
    _AbstractProgressBar,
    _AbstractRadioButtons,
    _AbstractSlider,
    _AbstractSpinBox,
    _AbstractStatusBar,
    _AbstractText,
    _AbstractToolBar,
    _AbstractVBoxLayout,
    _AbstractWdgt,
    _AbstractWidget,
    _AbstractWidgetList,
    _AbstractWindow,
)
from ._pyvista import (
    _check_3d_figure,  # noqa: F401
    _close_3d_figure,  # noqa: F401
    _close_all,  # noqa: F401
    _PyVistaRenderer,
    _set_3d_title,  # noqa: F401
    _set_3d_view,  # noqa: F401
    _take_3d_screenshot,  # noqa: F401
)
from ._utils import (
    _ICONS_PATH,
    _init_mne_qtapp,
    _qt_app_exec,
    _qt_detect_theme,
    _qt_disable_paint,
    _qt_get_stylesheet,
    _qt_is_dark,
    _qt_raise_window,
    _qt_safe_window,
)
from .renderer import _TimeInteraction

# Adapted from matplotlib
if (
    sys.platform == "darwin"
    and _compare_version(platform.mac_ver()[0], ">=", "10.16")
    and QLibraryInfo.version().segments() <= [5, 15, 2]
):
    os.environ.setdefault("QT_MAC_WANTS_LAYER", "1")


# fix for qscroll needing two layouts, one parent, one child
def _get_layout(layout):
    if hasattr(layout, "_parent_layout"):
        return layout._parent_layout
    return layout


# -------
# Widgets
# -------
# The metaclasses need to share a base class in order for the inheritance
# not to conflict, http://www.phyast.pitt.edu/~micheles/python/metatype.html
# https://stackoverflow.com/questions/28720217/multiple-inheritance-metaclass-conflict


class _BaseWidget(type(QWidget), type(_AbstractWidget)):
    pass


# The inheritance has to be in this order for the _Widget and the opposite for
# the widgets (e.g. _PushButton) that inherit from it, not sure why
class _Widget(_AbstractWidget, QWidget, metaclass=_BaseWidget):
    tooltip = None
    _to_qt = dict(
        escape=Qt.Key_Escape,
        up=Qt.Key_Up,
        down=Qt.Key_Down,
        left=Qt.Key_Left,
        right=Qt.Key_Right,
        page_up=Qt.Key_PageUp,
        page_down=Qt.Key_PageDown,
    )
    _from_qt = {v: k for k, v in _to_qt.items()}

    def __init__(self):
        _AbstractWidget.__init__()
        # QWidget.__init__(self)

    def _show(self):
        self.show()

    def _hide(self):
        self.hide()

    def _set_enabled(self, state):
        self.setEnabled(state)

    def _is_enabled(self):
        return self.isEnabled()

    def _update(self, repaint=True):
        self.update()
        if repaint:
            self.repaint()

    def _get_tooltip(self):
        return self.toolTip()

    def _set_tooltip(self, tooltip):
        self.setToolTip(tooltip)

    def _set_style(self, style):
        stylesheet = ""
        for key, val in style.items():
            stylesheet = stylesheet + f"{key}:{val};"
        self.setStyleSheet(stylesheet)

    def _add_keypress(self, callback):
        self.keyPressEvent = lambda event: callback(
            self._from_qt[event.key()] if event.key() in self._from_qt else event.text()
        )

    def _trigger_keypress(self, key):
        if key in self._to_qt:
            key_int = self._to_qt[key]
        else:
            key_int = getattr(Qt, f"Key_{key.upper()}")
        self.keyPressEvent(
            QKeyEvent(QEvent.KeyRelease, key_int, Qt.NoModifier, text=key)
        )

    def _set_focus(self):
        self.setFocus()

    def _set_layout(self, layout):
        self.setLayout(_get_layout(layout))

    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")

    def _set_size(self, width=None, height=None):
        if width:
            self.setMinimumWidth(width)
            self.setMaximumWidth(width)
        if height:
            self.setMinimumHeight(height)
            self.setMaximumHeight(height)


class _Label(QLabel, _AbstractLabel, _Widget, metaclass=_BaseWidget):
    def __init__(self, value, center=False, selectable=False):
        _AbstractLabel.__init__(value, center=center, selectable=selectable)
        _Widget.__init__(self)
        QLabel.__init__(self)
        self.setText(value)
        if center:
            self.setAlignment(Qt.AlignCenter)
        self.setWordWrap(True)
        if selectable:
            self.setTextInteractionFlags(Qt.TextSelectableByMouse)


class _Text(QLineEdit, _AbstractText, _Widget, metaclass=_BaseWidget):
    def __init__(self, value=None, placeholder=None, callback=None):
        _AbstractText.__init__(value=value, placeholder=placeholder, callback=callback)
        _Widget.__init__(self)
        QLineEdit.__init__(self, value)
        self.setPlaceholderText(placeholder)
        if callback is not None:
            self.textChanged.connect(callback)

    def _set_value(self, value):
        self.setText(value)


class _Button(QPushButton, _AbstractButton, _Widget, metaclass=_BaseWidget):
    def __init__(self, value, callback, icon=None):
        _AbstractButton.__init__(value=value, callback=callback)
        _Widget.__init__(self)
        with _disabled_init(_AbstractButton):
            QPushButton.__init__(self)
        self.setText(value)
        self.released.connect(callback)
        if icon:
            self.setIcon(_qicon(icon))

    def _click(self):
        self.click()

    def _set_icon(self, icon):
        self.setIcon(_qicon(icon))


class _Slider(QSlider, _AbstractSlider, _Widget, metaclass=_BaseWidget):
    def __init__(self, value, rng, callback, horizontal=True):
        _AbstractSlider.__init__(
            value=value, rng=rng, callback=callback, horizontal=horizontal
        )
        _Widget.__init__(self)
        with _disabled_init(_AbstractSlider):
            QSlider.__init__(self, Qt.Horizontal if horizontal else Qt.Vertical)
        self.setMinimum(rng[0])
        self.setMaximum(rng[1])
        self.setValue(value)
        self.valueChanged.connect(callback)

    def _set_value(self, value):
        self.setValue(value)

    def _get_value(self):
        return self.value()

    def _set_range(self, rng):
        self.setRange(int(rng[0]), int(rng[1]))


class _ProgressBar(QProgressBar, _AbstractProgressBar, _Widget, metaclass=_BaseWidget):
    def __init__(self, count):
        _AbstractProgressBar.__init__(count=count)
        _Widget.__init__(self)
        QProgressBar.__init__(self)
        self.setMaximum(count)

    def _increment(self):
        if self.value() + 1 > self.maximum():
            return
        self.setValue(self.value() + 1)
        return self.value()


class _CheckBox(QCheckBox, _AbstractCheckBox, _Widget, metaclass=_BaseWidget):
    def __init__(self, value, callback):
        _AbstractCheckBox.__init__(value=value, callback=callback)
        _Widget.__init__(self)
        with _disabled_init(_AbstractCheckBox):
            QCheckBox.__init__(self)
        self.setChecked(value)
        self.stateChanged.connect(lambda x: callback(bool(x)))

    def _set_checked(self, checked):
        self.setChecked(checked)

    def _get_checked(self):
        return self.checkState() != Qt.Unchecked


class _SpinBox(QDoubleSpinBox, _AbstractSpinBox, _Widget, metaclass=_BaseWidget):
    def __init__(self, value, rng, callback, step=None):
        _AbstractSpinBox.__init__(value=value, rng=rng, callback=callback, step=step)
        _Widget.__init__(self)
        with _disabled_init(_AbstractSpinBox):
            QDoubleSpinBox.__init__(self)
        self.setAlignment(Qt.AlignCenter)
        self.setMinimum(rng[0])
        self.setMaximum(rng[1])
        self.setKeyboardTracking(False)
        if step is None:
            self.setSingleStep((rng[1] - rng[0]) / 20.0)
        else:
            self.setSingleStep(step)
        self.setValue(value)
        self.valueChanged.connect(callback)

    def _set_value(self, value):
        self.setValue(value)

    def _get_value(self):
        return self.value()


class _ComboBox(QComboBox, _AbstractComboBox, _Widget, metaclass=_BaseWidget):
    def __init__(self, value, items, callback):
        _AbstractComboBox.__init__(value=value, items=items, callback=callback)
        _Widget.__init__(self)
        with _disabled_init(_AbstractComboBox):
            QComboBox.__init__(self)
        self.addItems(items)
        self.setCurrentText(value)
        self.currentTextChanged.connect(callback)
        self.setSizeAdjustPolicy(QComboBox.AdjustToContents)

    def _set_value(self, value):
        self.setCurrentText(value)

    def _get_value(self):
        return self.currentText()


class _RadioButtons(QVBoxLayout, _AbstractRadioButtons, _Widget, metaclass=_BaseWidget):
    def __init__(self, value, items, callback):
        _AbstractRadioButtons.__init__(value=value, items=items, callback=callback)
        _Widget.__init__(self)
        with _disabled_init(_AbstractRadioButtons):
            QVBoxLayout.__init__(self)
        self._button_group = QButtonGroup()
        self._button_group.setExclusive(True)
        for val in items:
            button = QRadioButton(val)
            if val == value:
                button.setChecked(True)
            self._button_group.addButton(button)
            self.addWidget(button)
        self._button_group.buttonClicked.connect(lambda button: callback(button.text()))

    def _set_value(self, value):
        for button in self._button_group.buttons():
            if button.text() == value:
                button.click()

    def _get_value(self):
        return self.checkedButton().text()


class _GroupBox(QGroupBox, _AbstractGroupBox, _Widget, metaclass=_BaseWidget):
    def __init__(self, name, items):
        _AbstractGroupBox.__init__(name=name, items=items)
        _Widget.__init__(self)
        with _disabled_init(_AbstractGroupBox):
            QGroupBox.__init__(self, name)
        self._layout = _VBoxLayout()
        for item in items:
            self._layout._add_widget(item)
        self.setLayout(self._layout)


class _FileButton(_Button, _AbstractFileButton, _Widget, metaclass=_BaseWidget):
    def __init__(
        self,
        callback,
        content_filter=None,
        initial_directory=None,
        save=False,
        is_directory=False,
        icon="folder",
        window=None,
    ):
        _AbstractFileButton.__init__(
            callback=callback,
            content_filter=content_filter,
            initial_directory=initial_directory,
            save=save,
            is_directory=is_directory,
            window=window,
        )
        _Widget.__init__(self)

        def fp_callback():
            if is_directory:
                name = QFileDialog.getExistingDirectory(
                    parent=window, directory=initial_directory
                )
            elif save:
                name = QFileDialog.getSaveFileName(
                    parent=window, directory=initial_directory, filter=content_filter
                )
            else:
                name = QFileDialog.getOpenFileName(
                    parent=window, directory=initial_directory, filter=content_filter
                )
            name = name[0] if isinstance(name, tuple) else name
            # handle the cancel button
            if len(name) == 0:
                return
            callback(name)

        _Button.__init__(self, "", callback=fp_callback, icon=icon)


class _PlayMenu(QVBoxLayout, _AbstractPlayMenu, _Widget, metaclass=_BaseWidget):
    def __init__(self, value, rng, callback):
        _AbstractPlayMenu.__init__(value=value, rng=rng, callback=callback)
        _Widget.__init__(self)
        with _disabled_init(_AbstractPlayMenu):
            QVBoxLayout.__init__(self)
        self._slider = QSlider(Qt.Horizontal)
        self._slider.setMinimum(rng[0])
        self._slider.setMaximum(rng[1])
        self._slider.setValue(value)
        self._slider.setTracking(False)
        self._slider.valueChanged.connect(callback)
        self._nav_hbox = QHBoxLayout()
        self._play_button = QPushButton()
        self._play_button.setIcon(_qicon("play"))
        self._nav_hbox.addWidget(self._play_button)
        self._pause_button = QPushButton()
        self._pause_button.setIcon(_qicon("pause"))
        self._nav_hbox.addWidget(self._pause_button)
        self._reset_button = QPushButton()
        self._reset_button.setIcon(_qicon("reset"))
        self._nav_hbox.addWidget(self._reset_button)
        self._loop_button = QPushButton()
        self._loop_button.setIcon(_qicon("restore"))
        self._loop_button.setStyleSheet("background-color : lightgray;")
        self._loop_button._checked = True

        def loop_callback():
            self._loop_button._checked = not self._loop_button._checked
            color = "lightgray" if self._loop_button._checked else "darkgray"
            self._loop_button.setStyleSheet(f"background-color : {color};")

        self._loop_button.released.connect(loop_callback)
        self._nav_hbox.addWidget(self._loop_button)
        self._timer = QTimer()

        def timer_callback():
            value = self._slider.value() + 1
            if value > rng[1]:
                if self._loop._checked:
                    self._timer.stop()
                value = rng[0]
            self._slider.setValue(value)

        self._timer.timeout.connect(timer_callback)
        self._timer.setInterval(250)
        self._play_button.released.connect(self._timer.start)
        self._pause_button.released.connect(self._timer.stop)
        self._reset_button.released.connect(lambda: self._slider.setValue(rng[0]))
        self.addWidget(self._slider)
        self.addLayout(self._nav_hbox)

    def _play(self):
        self._play_button.click()

    def _pause(self):
        self._pause_button.click()

    def _reset(self):
        self._reset_button.click()

    def _loop(self):
        self._loop_button.click()

    def _set_value(self, value):
        self._slider.setValue(value)


class _Popup(QMessageBox, _AbstractPopup, _Widget, metaclass=_BaseWidget):
    def __init__(
        self,
        title,
        text,
        info_text=None,
        callback=None,
        icon="warning",
        buttons=None,
        window=None,
    ):
        _AbstractPopup.__init__(
            self,
            title=title,
            text=text,
            info_text=info_text,
            callback=callback,
            icon=icon,
            buttons=buttons,
            window=window,
        )
        _Widget.__init__(self)
        with _disabled_init(_AbstractPopup):
            QMessageBox.__init__(self, parent=window)
        self.setWindowTitle(title)
        self.setText(text)
        # icon is one of _Dialog.supported_icon_names
        if icon is not None:
            self.setIcon(getattr(QMessageBox, icon.title()))
        if info_text:
            self.setInformativeText(info_text)

        if buttons is None:
            buttons = ["Ok"]

        button_ids = list()
        for button in buttons:
            # button is one of _Dialog.supported_button_names
            button_id = getattr(QMessageBox, button)
            button_ids.append(button_id)
        standard_buttons = default_button = button_ids[0]
        for button_id in button_ids[1:]:
            standard_buttons |= button_id
        self.setStandardButtons(standard_buttons)
        self.setDefaultButton(default_button)
        if callback:
            self.buttonClicked.connect(lambda button: callback(button.text().title()))
        _qt_raise_window(self)
        self._show()

    def _click(self, value):
        self.button(getattr(QMessageBox, value)).click()


class _ScrollArea(QScrollArea):
    def __init__(self, width, height, widget):
        QScrollArea.__init__(self)
        self.setWidget(widget)
        self.setFixedSize(width, height)
        self.setWidgetResizable(True)


class _HBoxLayout(QHBoxLayout, _AbstractHBoxLayout, _Widget, metaclass=_BaseWidget):
    def __init__(self, height=None, scroll=None):
        _AbstractHBoxLayout.__init__(self, height=height, scroll=scroll)
        _Widget.__init__(self)
        QHBoxLayout.__init__(self)

        if scroll is not None:
            self._scroll_widget = QWidget()
            self._parent_layout = QHBoxLayout()
            self._parent_layout.addWidget(
                _ScrollArea(scroll[0], scroll[1], self._scroll_widget)
            )
            self._scroll_widget.setLayout(self)

        self._height = height

    def _add_widget(self, widget):
        """Add a widget to an existing layout."""
        if isinstance(widget, QLayout):
            self.addLayout(widget)
        else:
            if self._height is not None:
                widget.setMinimumHeight(self._height)
                widget.setMaximumHeight(self._height)
            self.addWidget(widget)

    def _add_stretch(self, amount=1):
        self.addStretch(amount)


class _VBoxLayout(QVBoxLayout, _AbstractVBoxLayout, _Widget, metaclass=_BaseWidget):
    def __init__(self, width=None, scroll=None):
        _AbstractVBoxLayout.__init__(self, width=width, scroll=scroll)
        _Widget.__init__(self)
        QVBoxLayout.__init__(self)

        if scroll is not None:
            self._scroll_widget = QWidget()
            self._parent_layout = QHBoxLayout()
            self._parent_layout.addWidget(
                _ScrollArea(scroll[0], scroll[1], self._scroll_widget)
            )
            self._scroll_widget.setLayout(self)

        self._width = width

    def _add_widget(self, widget):
        """Add a widget to an existing layout."""
        if isinstance(widget, QLayout):
            self.addLayout(widget)
        else:
            if self._width is not None:
                widget.setMinimumWidth(self._width)
                widget.setMaximumWidth(self._width)
            self.addWidget(widget)

    def _add_stretch(self, amount=1):
        self.addStretch(amount)


class _GridLayout(QGridLayout, _AbstractGridLayout, _Widget, metaclass=_BaseWidget):
    def __init__(self, height=None, width=None):
        _AbstractGridLayout.__init__(self)
        _Widget.__init__(self)
        QGridLayout.__init__(self)
        if height:
            self.setMinimumHeight(height)
            self.setMaximumHeight(height)
        if width:
            self.setMinimumWidth(width)
            self.setMaximumWidth(width)

    def _add_widget(self, widget, row=None, col=None):
        """Add a widget to an existing layout."""
        if isinstance(widget, QLayout):
            self.addLayout(widget, row, col)
        else:
            self.addWidget(widget, row, col)


class _BaseCanvas(type(FigureCanvas), type(_AbstractCanvas)):
    pass


class _Canvas(FigureCanvas, _AbstractCanvas, metaclass=_BaseCanvas):
    def __init__(self, width, height, dpi):
        _AbstractCanvas.__init__(self, width=width, height=height, dpi=dpi)
        self.fig = Figure(figsize=(width, height), dpi=dpi)
        self.ax = self.fig.add_subplot(111, position=[0.15, 0.15, 0.75, 0.75])
        FigureCanvas.__init__(self, self.fig)
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self.setMinimumWidth(width)
        self.setMinimumHeight(height)

    def _set_size(self, width=None, height=None):
        if width:
            self.setMinimumWidth(width)
            self.setMaximumWidth(width)
        if height:
            self.setMinimumHeight(height)
            self.setMaximumHeight(height)


# %%
# Windows
# -------

# In theory we should be able to do this later (e.g., in _pyvista.py when
# initializing), but at least on Qt6 this has to be done earlier. So let's do
# it immediately upon instantiation of the QMainWindow class.
# TODO: This should eventually allow us to handle
# https://github.com/mne-tools/mne-python/issues/9182


# This is necessary to make PySide6 happy -- something weird with the
# __init__ calling causes the _AbstractXYZ class __init__ to be called twice
@contextmanager
def _disabled_init(klass):
    orig = klass.__init__
    klass.__init__ = lambda *args, **kwargs: None
    try:
        yield
    finally:
        klass.__init__ = orig


class _MNEMainWindow(MainWindow):
    def __init__(self, parent=None, title=None, size=None):
        with _disabled_init(_Widget):
            MainWindow.__init__(self, parent=parent, title=title, size=size)
        self.setAttribute(Qt.WA_ShowWithoutActivating, True)
        self.setAttribute(Qt.WA_DeleteOnClose, True)
        from . import renderer

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


class _AppWindow(_AbstractAppWindow, _MNEMainWindow, _Widget, metaclass=_BaseWidget):
    def __init__(self, size=None, fullscreen=False):
        self._app = _init_mne_qtapp()
        _AbstractAppWindow.__init__(self)
        _MNEMainWindow.__init__(self, size=size)
        _Widget.__init__(self)

        if fullscreen:
            self.setWindowState(Qt.WindowFullScreen)

        self._set_theme()
        self.setLocale(QLocale(QLocale.Language.English))
        self.signal_close.connect(self._clean)
        self._before_close_callbacks = list()
        self._after_close_callbacks = list()

        # patch closeEvent
        def closeEvent(event):
            # functions to call before closing
            accept_close_event = True
            for callback in self._before_close_callbacks:
                ret = callback()
                # check if one of the callbacks ignores the close event
                if isinstance(ret, bool) and not ret:
                    accept_close_event = False

            if accept_close_event:
                self.signal_close.emit()
                self._clean()
                event.accept()
            else:
                event.ignore()

            # functions to call after closing
            for callback in self._after_close_callbacks:
                callback()

        self.closeEvent = closeEvent

    def _set_central_layout(self, central_layout):
        central_widget = QWidget()
        central_widget.setLayout(_get_layout(central_layout))
        self.setCentralWidget(central_widget)

    def _get_dpi(self):
        return self.windowHandle().screen().logicalDotsPerInch()

    def _get_size(self):
        return (self.width(), self.height())

    def _get_cursor(self):
        return self.cursor()

    def _set_cursor(self, cursor):
        self.setCursor(cursor)

    def _new_cursor(self, name):
        return QCursor(getattr(Qt, name))

    def _close_connect(self, callback, *, after=True):
        if after:
            self._after_close_callbacks.append(callback)
        else:
            self._before_close_callbacks.append(callback)

    def _close_disconnect(self, after=True):
        if after:
            self._after_close_callbacks.clear()
        else:
            self._before_close_callbacks.clear()

    def _clean(self):
        self._app = None

    def _show(self, block=False):
        _qt_raise_window(self)
        _Widget._show(self)
        if block:
            _qt_app_exec(self._app)

    def _close(self):
        self.close()


class _3DRenderer(_PyVistaRenderer):
    _kind = "qt"

    @_qt_safe_window(always_close=False)
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    @_qt_safe_window()
    def show(self):
        super().show()
        with _qt_disable_paint(self.plotter):
            self.plotter.app_window.show()
        self._update()
        for plotter in self._all_plotters:
            plotter.updateGeometry()
            plotter._render()
        # Ideally we would just put a `splash.finish(plotter.window())` in the
        # same place that we initialize this (_init_qt_app call). However,
        # the window show event is triggered (closing the splash screen) well
        # before the window actually appears for complex scenes like the coreg
        # GUI. Therefore, we close after all these events have been processed
        # here.
        self._process_events()
        _qt_raise_window(self.plotter.app_window)

    def _clean(self):
        self.figure._plotter = None
        self._interactor = None


# ------------------------------------
# Non-object-based Widget Abstractions
# ------------------------------------
# These are planned to be deprecated in favor of the simpler, object-
# oriented abstractions above when time allows.


class _QtKeyPress(_AbstractKeyPress):
    _widget_id = 0
    _callbacks = dict()
    _to_qt = dict(
        escape=Qt.Key_Escape,
        up=Qt.Key_Up,
        down=Qt.Key_Down,
        left=Qt.Key_Left,
        right=Qt.Key_Right,
        comma=Qt.Key_Comma,
        period=Qt.Key_Period,
        page_up=Qt.Key_PageUp,
        page_down=Qt.Key_PageDown,
    )

    def _keypress_initialize(self, widget=None):
        widget = self._window if widget is None else widget
        self._widget_id = _QtKeyPress._widget_id
        _QtKeyPress._widget_id += 1
        _QtKeyPress._callbacks[self._widget_id] = dict()

        def keyPressEvent(event):
            text = event.text()
            widget_callbacks = _QtKeyPress._callbacks[self._widget_id]
            if text in widget_callbacks:
                callback = widget_callbacks[text]
                callback()
            else:
                key = event.key()
                if key in widget_callbacks:
                    callback = widget_callbacks[key]
                    callback()

        widget.keyPressEvent = keyPressEvent

    def _keypress_add(self, shortcut, callback):
        widget_callbacks = _QtKeyPress._callbacks[self._widget_id]
        if len(shortcut) > 1:  # special key
            shortcut = _QtKeyPress._to_qt[shortcut]
        widget_callbacks[shortcut] = callback

    def _keypress_trigger(self, shortcut):
        widget_callbacks = _QtKeyPress._callbacks[self._widget_id]
        if len(shortcut) > 1:  # special key
            shortcut = _QtKeyPress._to_qt[shortcut]
        widget_callbacks[shortcut]()


class _QtDialog(_AbstractDialog):
    # from QMessageBox.StandardButtons
    supported_button_names = [
        "Ok",
        "Open",
        "Save",
        "Cancel",
        "Close",
        "Discard",
        "Apply",
        "Reset",
        "RestoreDefaults",
        "Help",
        "SaveAll",
        "Yes",
        "YesToAll",
        "No",
        "NoToAll",
        "Abort",
        "Retry",
        "Ignore",
    ]
    # from QMessageBox.Icon
    supported_icon_names = ["NoIcon", "Question", "Information", "Warning", "Critical"]

    def _dialog_create(
        self,
        title,
        text,
        info_text,
        callback,
        *,
        icon="Warning",
        buttons=(),
        modal=True,
        window=None,
    ):
        window = self._window if window is None else window
        widget = QMessageBox(window)
        widget.setWindowTitle(title)
        widget.setText(text)
        # icon is one of _QtDialog.supported_icon_names
        icon_id = getattr(QMessageBox, icon)
        widget.setIcon(icon_id)
        widget.setInformativeText(info_text)

        if not buttons:
            buttons = ["Ok"]

        button_ids = list()
        for button in buttons:
            # button is one of _QtDialog.supported_button_names
            button_id = getattr(QMessageBox, button)
            button_ids.append(button_id)
        standard_buttons = default_button = button_ids[0]
        for button_id in button_ids[1:]:
            standard_buttons |= button_id
        widget.setStandardButtons(standard_buttons)
        widget.setDefaultButton(default_button)

        @safe_event
        def func(button):
            button_id = widget.standardButton(button)
            for button_name in _QtDialog.supported_button_names:
                if button_id == getattr(QMessageBox, button_name):
                    widget.setCursor(QCursor(Qt.WaitCursor))
                    try:
                        callback(button_name)
                    finally:
                        widget.unsetCursor()
                    break

        widget.buttonClicked.connect(func)
        return _QtDialogWidget(widget, modal)


class _QtLayout(_AbstractLayout):
    def _layout_initialize(self, max_width):
        pass

    def _layout_add_widget(self, layout, widget, stretch=0, *, row=None, col=None):
        """Add a widget to an existing layout."""
        if isinstance(widget, QLayout):
            layout.addLayout(widget)
        else:
            if isinstance(layout, QGridLayout):
                layout.addWidget(widget, row, col)
            else:
                layout.addWidget(widget, stretch)

    def _layout_create(self, orientation="vertical"):
        if orientation == "vertical":
            layout = QVBoxLayout()
        elif orientation == "horizontal":
            layout = QHBoxLayout()
        else:
            assert orientation == "grid"
            layout = QGridLayout()
        return layout


class _QtDock(_AbstractDock, _QtLayout):
    def _dock_initialize(
        self, window=None, name="Controls", area="left", max_width=None
    ):
        window = self._window if window is None else window
        qt_area = getattr(Qt, f"{area.capitalize()}DockWidgetArea")
        self._dock, self._dock_layout = _create_dock_widget(
            window, name, qt_area, max_width=max_width
        )
        if area == "left":
            window.setCorner(Qt.BottomLeftCorner, Qt.LeftDockWidgetArea)
        else:
            window.setCorner(Qt.BottomRightCorner, Qt.RightDockWidgetArea)

    def _dock_finalize(self):
        self._dock.setMinimumSize(self._dock.sizeHint().width(), 0)
        self._dock_add_stretch(self._dock_layout)

    def _dock_show(self):
        self._dock.show()

    def _dock_hide(self):
        self._dock.hide()

    def _dock_add_stretch(self, layout=None):
        layout = self._dock_layout if layout is None else layout
        layout.addStretch()

    def _dock_add_layout(self, vertical=True):
        layout = QVBoxLayout() if vertical else QHBoxLayout()
        return layout

    def _dock_add_label(self, value, *, align=False, layout=None, selectable=False):
        layout = self._dock_layout if layout is None else layout
        widget = QLabel()
        if align:
            widget.setAlignment(Qt.AlignCenter)
        widget.setText(value)
        widget.setWordWrap(True)
        if selectable:
            widget.setTextInteractionFlags(Qt.TextSelectableByMouse)
        self._layout_add_widget(layout, widget)
        return _QtWidget(widget)

    def _dock_add_button(
        self,
        name,
        callback,
        *,
        style="pushbutton",
        icon=None,
        tooltip=None,
        layout=None,
    ):
        _check_option(
            parameter="style", value=style, allowed_values=("toolbutton", "pushbutton")
        )
        if style == "toolbutton":
            widget = QToolButton()
            widget.setText(name)
        else:
            widget = QPushButton(name)
            # Don't change text color upon button press
            widget.setStyleSheet("QPushButton:pressed {color: none;}")
        if icon is not None:
            widget.setIcon(self._icons[icon])

        _set_widget_tooltip(widget, tooltip)
        widget.clicked.connect(callback)

        layout = self._dock_layout if layout is None else layout
        self._layout_add_widget(layout, widget)
        return _QtWidget(widget)

    def _dock_named_layout(self, name, *, layout=None, compact=True):
        layout = self._dock_layout if layout is None else layout
        if name is not None:
            hlayout = self._dock_add_layout(not compact)
            self._dock_add_label(value=name, align=not compact, layout=hlayout)
            self._layout_add_widget(layout, hlayout)
            layout = hlayout
        return layout

    def _dock_add_slider(
        self,
        name,
        value,
        rng,
        callback,
        *,
        compact=True,
        double=False,
        tooltip=None,
        layout=None,
    ):
        layout = self._dock_named_layout(name=name, layout=layout, compact=compact)
        slider_class = QFloatSlider if double else QSlider
        cast = float if double else int
        widget = slider_class(Qt.Horizontal)
        _set_widget_tooltip(widget, tooltip)
        widget.setMinimum(cast(rng[0]))
        widget.setMaximum(cast(rng[1]))
        widget.setValue(cast(value))
        if double:
            widget.floatValueChanged.connect(callback)
        else:
            widget.valueChanged.connect(callback)
        self._layout_add_widget(layout, widget)
        return _QtWidget(widget)

    def _dock_add_check_box(self, name, value, callback, *, tooltip=None, layout=None):
        layout = self._dock_layout if layout is None else layout
        widget = QCheckBox(name)
        _set_widget_tooltip(widget, tooltip)
        widget.setChecked(value)
        widget.stateChanged.connect(callback)
        self._layout_add_widget(layout, widget)
        return _QtWidget(widget)

    def _dock_add_spin_box(
        self,
        name,
        value,
        rng,
        callback,
        *,
        compact=True,
        double=True,
        step=None,
        tooltip=None,
        layout=None,
    ):
        layout = self._dock_named_layout(name=name, layout=layout, compact=compact)
        value = value if double else int(value)
        widget = QDoubleSpinBox() if double else QSpinBox()
        _set_widget_tooltip(widget, tooltip)
        widget.setAlignment(Qt.AlignCenter)
        widget.setMinimum(rng[0])
        widget.setMaximum(rng[1])
        widget.setKeyboardTracking(False)
        if step is None:
            inc = (rng[1] - rng[0]) / 20.0
            inc = max(int(round(inc)), 1) if not double else inc
            widget.setSingleStep(inc)
        else:
            widget.setSingleStep(step)
        widget.setValue(value)
        widget.valueChanged.connect(callback)
        self._layout_add_widget(layout, widget)
        return _QtWidget(widget)

    def _dock_add_combo_box(
        self, name, value, rng, callback, *, compact=True, tooltip=None, layout=None
    ):
        layout = self._dock_named_layout(name=name, layout=layout, compact=compact)
        widget = QComboBox()
        _set_widget_tooltip(widget, tooltip)
        widget.addItems(rng)
        widget.setCurrentText(value)
        widget.currentTextChanged.connect(callback)
        widget.setSizeAdjustPolicy(QComboBox.AdjustToContents)
        self._layout_add_widget(layout, widget)
        return _QtWidget(widget)

    def _dock_add_radio_buttons(
        self, value, rng, callback, *, vertical=True, layout=None
    ):
        layout = self._dock_layout if layout is None else layout
        group_layout = QVBoxLayout() if vertical else QHBoxLayout()
        group = QButtonGroup()
        for val in rng:
            button = QRadioButton(val)
            if val == value:
                button.setChecked(True)
            group.addButton(button)
            self._layout_add_widget(group_layout, button)

        def func(button):
            callback(button.text())

        group.buttonClicked.connect(func)
        self._layout_add_widget(layout, group_layout)
        return _QtWidgetList(group)

    def _dock_add_group_box(self, name, *, collapse=None, layout=None):
        layout = self._dock_layout if layout is None else layout
        hlayout = QVBoxLayout()
        widget = QGroupBox(name)
        widget.setLayout(hlayout)
        self._layout_add_widget(layout, widget)
        return hlayout

    def _dock_add_text(self, name, value, placeholder, *, callback=None, layout=None):
        layout = self._dock_layout if layout is None else layout
        widget = QLineEdit(str(value))
        widget.setPlaceholderText(placeholder)
        self._layout_add_widget(layout, widget)
        if callback is not None:
            widget.textChanged.connect(callback)
        return _QtWidget(widget)

    def _dock_add_file_button(
        self,
        name,
        desc,
        func,
        *,
        filter_=None,
        initial_directory=None,
        save=False,
        is_directory=False,
        icon=False,
        tooltip=None,
        layout=None,
    ):
        layout = self._dock_layout if layout is None else layout
        weakself = weakref.ref(self)
        if initial_directory is not None:
            initial_directory = str(initial_directory)

        def callback():
            self = weakself()
            if not self:
                return
            if is_directory:
                name = QFileDialog.getExistingDirectory(
                    parent=self._window, directory=initial_directory
                )
            elif save:
                name = QFileDialog.getSaveFileName(
                    parent=self._window, directory=initial_directory, filter=filter_
                )
            else:
                name = QFileDialog.getOpenFileName(
                    parent=self._window, directory=initial_directory, filter=filter_
                )
            name = name[0] if isinstance(name, tuple) else name
            # handle the cancel button
            if len(name) == 0:
                return
            func(name)

        if icon:
            kwargs = dict(style="toolbutton", icon="folder")
        else:
            kwargs = dict()
        button_widget = self._dock_add_button(
            name=desc, callback=callback, tooltip=tooltip, layout=layout, **kwargs
        )
        return button_widget  # It's already a _QtWidget instance


class QFloatSlider(QSlider):
    """Slider that handles float values."""

    floatValueChanged = Signal(float)

    def __init__(self, ori, parent=None):
        """Initialize the slider."""
        super().__init__(ori, parent)
        self._opt = QStyleOptionSlider()
        self.initStyleOption(self._opt)
        self._gr = self.style().subControlRect(
            QStyle.CC_Slider, self._opt, QStyle.SC_SliderGroove, self
        )
        self._sr = self.style().subControlRect(
            QStyle.CC_Slider, self._opt, QStyle.SC_SliderHandle, self
        )
        self._precision = 10000
        super().valueChanged.connect(self._convert)

    def _convert(self, value):
        self.floatValueChanged.emit(value / self._precision)

    def minimum(self):
        """Get the minimum."""
        return super().minimum() / self._precision

    def setMinimum(self, value):
        """Set the minimum."""
        super().setMinimum(int(value * self._precision))

    def maximum(self):
        """Get the maximum."""
        return super().maximum() / self._precision

    def setMaximum(self, value):
        """Set the maximum."""
        super().setMaximum(int(value * self._precision))

    def value(self):
        """Get the current value."""
        return super().value() / self._precision

    def setValue(self, value):
        """Set the current value."""
        super().setValue(int(value * self._precision))

    # Adapted from:
    # https://stackoverflow.com/questions/52689047/moving-qslider-to-mouse-click-position  # noqa: E501
    def mousePressEvent(self, event):
        """Add snap-to-location handling."""
        opt = QStyleOptionSlider()
        self.initStyleOption(opt)
        sr = self.style().subControlRect(
            QStyle.CC_Slider, opt, QStyle.SC_SliderHandle, self
        )
        if event.button() != Qt.LeftButton or sr.contains(event.pos()):
            super().mousePressEvent(event)
            return
        if self.orientation() == Qt.Vertical:
            half = (0.5 * sr.height()) + 0.5
            max_ = self.height()
            pos = max_ - event.pos().y()
        else:
            half = (0.5 * sr.width()) + 0.5
            max_ = self.width()
            pos = event.pos().x()
        max_ = max_ - 2 * half
        pos = min(max(pos - half, 0), max_) / max_
        val = self.minimum() + (self.maximum() - self.minimum()) * pos
        val = (self.maximum() - val) if self.invertedAppearance() else val
        self.setValue(val)
        event.accept()
        # Process afterward so it's seen as a drag
        super().mousePressEvent(event)


class _QtToolBar(_AbstractToolBar, _QtLayout):
    def _tool_bar_initialize(self, name="default", window=None):
        self.actions = dict()
        window = self._window if window is None else window
        self._tool_bar = window.addToolBar(name)
        self._tool_bar_layout = self._tool_bar.layout()

    def _tool_bar_add_button(self, name, desc, func, *, icon_name=None, shortcut=None):
        icon_name = name if icon_name is None else icon_name
        icon = self._icons[icon_name]
        self.actions[name] = _QtAction(self._tool_bar.addAction(icon, desc, func))
        if shortcut is not None:
            self.actions[name].set_shortcut(shortcut)

    def _tool_bar_update_button_icon(self, name, icon_name):
        self.actions[name].set_icon(self._icons[icon_name])

    def _tool_bar_add_text(self, name, value, placeholder):
        pass

    def _tool_bar_add_spacer(self):
        spacer = QWidget()
        spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
        self._tool_bar.addWidget(spacer)

    def _tool_bar_add_file_button(self, name, desc, func, *, shortcut=None):
        weakself = weakref.ref(self)

        def callback(weakself=weakself):
            weakself = weakself()
            if weakself is None:
                return
            return FileDialog(
                weakself._window,
                callback=func,
            )

        self._tool_bar_add_button(
            name=name,
            desc=desc,
            func=callback,
            shortcut=shortcut,
        )

    def _tool_bar_add_play_button(self, name, desc, func, *, shortcut=None):
        self._tool_bar_add_button(
            name=name, desc=desc, func=func, icon_name=None, shortcut=shortcut
        )


class _QtMenuBar(_AbstractMenuBar):
    def _menu_initialize(self, window=None):
        window = self._window if window is None else window
        self._menus = dict()
        self._menu_actions = dict()
        self._menu_bar = QMenuBar(window)
        self._menu_bar.setNativeMenuBar(False)

    def _menu_add_submenu(self, name, desc):
        self._menus[name] = self._menu_bar.addMenu(desc)
        self._menu_actions[name] = dict()

    def _menu_add_button(self, menu_name, name, desc, func):
        menu = self._menus[menu_name]
        self._menu_actions[menu_name][name] = _QtAction(menu.addAction(desc, func))


class _QtStatusBar(_AbstractStatusBar, _QtLayout):
    def _status_bar_initialize(self, window=None):
        window = self._window if window is None else window
        self._status_bar = window.statusBar()

    def _status_bar_add_label(self, value, *, stretch=0):
        widget = QLabel(value)
        self._layout_add_widget(self._status_bar.layout(), widget, stretch)
        return _QtWidget(widget)

    def _status_bar_add_progress_bar(self, stretch=0):
        widget = QProgressBar()
        self._layout_add_widget(self._status_bar.layout(), widget, stretch)
        return _QtWidget(widget)

    def _status_bar_update(self):
        self._status_bar.layout().update()


class _QtPlayback(_AbstractPlayback):
    def _playback_initialize(self, func, timeout, value, rng, time_widget, play_widget):
        self.figure.plotter.add_callback(func, timeout)


class _QtMplInterface(_AbstractMplInterface):
    def _mpl_initialize(self):
        from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
        from qtpy import QtWidgets

        self.canvas = FigureCanvasQTAgg(self.fig)
        FigureCanvasQTAgg.setSizePolicy(
            self.canvas,
            QtWidgets.QSizePolicy.Expanding,
            QtWidgets.QSizePolicy.Expanding,
        )
        FigureCanvasQTAgg.updateGeometry(self.canvas)


class _QtMplCanvas(_AbstractMplCanvas, _QtMplInterface):
    def __init__(self, width, height, dpi):
        super().__init__(width, height, dpi)
        self._mpl_initialize()


class _QtBrainMplCanvas(_AbstractBrainMplCanvas, _QtMplInterface):
    def __init__(self, brain, width, height, dpi):
        super().__init__(brain, width, height, dpi)
        self._mpl_initialize()
        if brain.separate_canvas:
            self.canvas.setParent(None)
        else:
            self.canvas.setParent(brain._renderer._window)
        self._connect()


class _QtWindow(_AbstractWindow):
    def _window_initialize(self, *, window=None, central_layout=None, fullscreen=False):
        super()._window_initialize()
        self._interactor = self.figure.plotter.interactor
        if window is None:
            self._window = self.figure.plotter.app_window
        else:
            self._window = window

        if fullscreen:
            self._window.setWindowState(Qt.WindowFullScreen)

        if central_layout is not None:
            central_widget = self._window.centralWidget()
            if central_widget is None:
                central_widget = QWidget()
                self._window.setCentralWidget(central_widget)
            central_widget.setLayout(central_layout)
        self._window_load_icons()
        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()
        self._window_after_close_callbacks = list()

        # patch closeEvent
        def closeEvent(event):
            # functions to call before closing
            accept_close_event = True
            for callback in self._window_before_close_callbacks:
                ret = callback()
                # check if one of the callbacks ignores the close event
                if isinstance(ret, bool) and not ret:
                    accept_close_event = False

            if accept_close_event:
                self._window.signal_close.emit()
                event.accept()
            else:
                event.ignore()

            # functions to call after closing
            for callback in self._window_after_close_callbacks:
                callback()

        self._window.closeEvent = closeEvent

    def _window_load_icons(self):
        self._icons["help"] = _qicon("help")
        self._icons["play"] = _qicon("play")
        self._icons["pause"] = _qicon("pause")
        self._icons["reset"] = _qicon("reset")
        self._icons["scale"] = _qicon("scale")
        self._icons["clear"] = _qicon("clear")
        self._icons["movie"] = _qicon("movie")
        self._icons["restore"] = _qicon("restore")
        self._icons["screenshot"] = _qicon("screenshot")
        self._icons["visibility_on"] = _qicon("visibility_on")
        self._icons["visibility_off"] = _qicon("visibility_off")
        self._icons["folder"] = _qicon("folder")

    def _window_clean(self):
        self.figure._plotter = None
        self._interactor = None

    def _window_close_connect(self, func, *, after=True):
        if after:
            self._window_after_close_callbacks.append(func)
        else:
            self._window_before_close_callbacks.append(func)

    def _window_close_disconnect(self, after=True):
        if after:
            self._window_after_close_callbacks.clear()
        else:
            self._window_before_close_callbacks.clear()

    def _window_get_dpi(self):
        return self._window.windowHandle().screen().logicalDotsPerInch()

    def _window_get_size(self):
        w = self._interactor.geometry().width()
        h = self._interactor.geometry().height()
        return (w, h)

    def _window_get_simple_canvas(self, width, height, dpi):
        return _QtMplCanvas(width, height, dpi)

    def _window_get_mplcanvas(
        self, brain, interactor_fraction, show_traces, separate_canvas
    ):
        w, h = self._window_get_mplcanvas_size(interactor_fraction)
        self._interactor_fraction = interactor_fraction
        self._show_traces = show_traces
        self._separate_canvas = separate_canvas
        self._mplcanvas = _QtBrainMplCanvas(brain, w, h, self._window_get_dpi())
        return self._mplcanvas

    def _window_adjust_mplcanvas_layout(self):
        canvas = self._mplcanvas.canvas
        self._mpl_dock, dock_layout = _create_dock_widget(
            self._window, "Traces", Qt.BottomDockWidgetArea
        )
        dock_layout.addWidget(canvas)

    def _window_get_cursor(self):
        return self._window.cursor()

    def _window_set_cursor(self, cursor):
        self._interactor.setCursor(cursor)
        self._window.setCursor(cursor)

    def _window_new_cursor(self, name):
        return QCursor(getattr(Qt, name))

    @contextmanager
    def _window_ensure_minimum_sizes(self):
        sz = self.figure.store["window_size"]
        adjust_mpl = self._show_traces and not self._separate_canvas
        # plotter:            pyvista.plotting.qt_plotting.BackgroundPlotter
        # plotter.interactor: vtk.qt.QVTKRenderWindowInteractor.QVTKRenderWindowInteractor -> QWidget  # noqa
        # plotter.app_window: pyvista.plotting.qt_plotting.MainWindow -> QMainWindow  # noqa
        # plotter.frame:      QFrame with QVBoxLayout with plotter.interactor as centralWidget  # noqa
        # plotter.ren_win:    vtkXOpenGLRenderWindow
        self._interactor.setMinimumSize(*sz)
        # Lines like this are useful for debugging these issues:
        # print('*' * 80)
        # print(0, self._interactor.app_window.size().height(), self._interactor.size().height(), self._mpl_dock.widget().height(), self._mplcanvas.canvas.size().height())  # noqa
        if adjust_mpl:
            mpl_h = int(
                round(
                    (sz[1] * self._interactor_fraction)
                    / (1 - self._interactor_fraction)
                )
            )
            self._mplcanvas.canvas.setMinimumSize(sz[0], mpl_h)
            self._mpl_dock.widget().setMinimumSize(sz[0], mpl_h)
        try:
            yield  # show
        finally:
            # 1. Process events
            self._process_events()
            self._process_events()
            # 2. Get the window and interactor sizes that work
            win_sz = self._window.size()
            ren_sz = self._interactor.size()
            # 3. Undo the min size setting and process events
            self._interactor.setMinimumSize(0, 0)
            if adjust_mpl:
                self._mplcanvas.canvas.setMinimumSize(0, 0)
                self._mpl_dock.widget().setMinimumSize(0, 0)
            self._process_events()
            self._process_events()
            # 4. Compute the extra height required for dock decorations and add
            win_h = win_sz.height()
            if adjust_mpl:
                win_h += max(self._mpl_dock.widget().size().height() - mpl_h, 0)
            # 5. Resize the window and interactor to the correct size
            #    (not sure why, but this is required on macOS at least)
            self._interactor.window_size = (win_sz.width(), win_h)
            self._interactor.resize(ren_sz.width(), ren_sz.height())
            self._process_events()
            self._process_events()

    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")

    def _window_create(self):
        return _MNEMainWindow()


class _QtWidgetList(_AbstractWidgetList):
    def __init__(self, src):
        self._src = src
        self._widgets = list()
        if isinstance(self._src, QButtonGroup):
            widgets = self._src.buttons()
        else:
            widgets = src
        for widget in widgets:
            if not isinstance(widget, _QtWidget):
                widget = _QtWidget(widget)
            self._widgets.append(widget)

    def set_enabled(self, state):
        for widget in self._widgets:
            widget.set_enabled(state)

    def get_value(self, idx):
        return self._widgets[idx].get_value()

    def set_value(self, idx, value):
        if isinstance(self._src, QButtonGroup):
            self._widgets[idx].set_value(True)
        else:
            self._widgets[idx].set_value(value)


class _QtWidget(_AbstractWdgt):
    def set_value(self, value):
        if isinstance(self._widget, QRadioButton | QToolButton | QPushButton):
            self._widget.click()
        else:
            if hasattr(self._widget, "setValue"):
                self._widget.setValue(value)
            elif hasattr(self._widget, "setCurrentText"):
                self._widget.setCurrentText(value)
            elif hasattr(self._widget, "setChecked"):
                self._widget.setChecked(value)
            else:
                assert hasattr(self._widget, "setText")
                self._widget.setText(value)

    def get_value(self):
        if hasattr(self._widget, "value"):
            return self._widget.value()
        elif hasattr(self._widget, "currentText"):
            return self._widget.currentText()
        elif hasattr(self._widget, "checkState"):
            return self._widget.checkState() != Qt.Unchecked
        else:
            assert hasattr(self._widget, "text")
            return self._widget.text()

    def set_range(self, rng):
        self._widget.setRange(rng[0], rng[1])

    def show(self):
        self._widget.show()

    def hide(self):
        self._widget.hide()

    def set_enabled(self, state):
        self._widget.setEnabled(state)

    def is_enabled(self):
        return self._widget.isEnabled()

    def update(self, repaint=True):
        self._widget.update()
        if repaint:
            self._widget.repaint()

    def get_tooltip(self):
        assert hasattr(self._widget, "toolTip")
        return self._widget.toolTip()

    def set_tooltip(self, tooltip):
        assert hasattr(self._widget, "setToolTip")
        self._widget.setToolTip(tooltip)

    def set_style(self, style):
        stylesheet = ""
        for key, val in style.items():
            stylesheet = stylesheet + f"{key}:{val};"
        self._widget.setStyleSheet(stylesheet)


class _QtDialogCommunicator(QObject):
    signal_show = Signal()

    def __init__(self, parent=None):
        super().__init__(parent)


class _QtDialogWidget(_QtWidget):
    def __init__(self, widget, modal):
        super().__init__(widget)
        self._modal = modal
        self._communicator = _QtDialogCommunicator()
        self._communicator.signal_show.connect(self.show)
        self._widget.setAttribute(Qt.WA_DeleteOnClose, True)

    def trigger(self, button):
        button_id = getattr(QMessageBox, button)
        for current_button in self._widget.buttons():
            if self._widget.standardButton(current_button) == button_id:
                current_button.click()
                break

    def show(self, thread=False):
        if thread:
            self._communicator.signal_show.emit()
        else:
            if self._modal:
                self._widget.exec()
            else:
                self._widget.show()


class _QtAction(_AbstractAction):
    def trigger(self):
        self._action.trigger()

    def set_icon(self, icon):
        self._action.setIcon(icon)

    def set_shortcut(self, shortcut):
        self._action.setShortcut(shortcut)


class _Renderer(
    _PyVistaRenderer,
    _QtDock,
    _QtToolBar,
    _QtMenuBar,
    _QtStatusBar,
    _QtWindow,
    _QtPlayback,
    _QtDialog,
    _QtKeyPress,
    _TimeInteraction,
):
    _kind = "qt"

    @_qt_safe_window(always_close=False)
    def __init__(self, *args, **kwargs):
        fullscreen = kwargs.pop("fullscreen", False)
        super().__init__(*args, **kwargs)
        self._window_initialize(fullscreen=fullscreen)

    @_qt_safe_window()
    def show(self):
        super().show()
        with _qt_disable_paint(self.plotter):
            with self._window_ensure_minimum_sizes():
                self.plotter.app_window.show()
        self._update()
        for plotter in self._all_plotters:
            plotter.updateGeometry()
            plotter._render()
        # Ideally we would just put a `splash.finish(plotter.window())` in the
        # same place that we initialize this (_init_qt_app call). However,
        # the window show event is triggered (closing the splash screen) well
        # before the window actually appears for complex scenes like the coreg
        # GUI. Therefore, we close after all these events have been processed
        # here.
        self._process_events()
        _qt_raise_window(self.plotter.app_window)


def _set_widget_tooltip(widget, tooltip):
    if tooltip is not None:
        widget.setToolTip(tooltip)


def _create_dock_widget(window, name, area, *, max_width=None):
    # create dock widget
    dock = QDockWidget(name)
    # add scroll area
    scroll = QScrollArea(dock)
    dock.setWidget(scroll)
    # give the scroll area a child widget
    widget = QWidget(scroll)
    scroll.setWidget(widget)
    scroll.setWidgetResizable(True)
    dock.setAllowedAreas(area)
    dock.setTitleBarWidget(QLabel(name))
    window.addDockWidget(area, dock)
    dock_layout = QVBoxLayout()
    widget.setLayout(dock_layout)
    # Fix resize grip size
    # https://stackoverflow.com/a/65050468/2175965
    styles = ["margin: 4px;"]
    if max_width is not None:
        styles.append(f"max-width: {max_width};")
    style_sheet = "QDockWidget { " + "  \n".join(styles) + "\n}"
    dock.setStyleSheet(style_sheet)
    return dock, dock_layout


@contextmanager
def _testing_context(interactive):
    from . import renderer

    orig_offscreen = pyvista.OFF_SCREEN
    orig_testing = renderer.MNE_3D_BACKEND_TESTING
    renderer.MNE_3D_BACKEND_TESTING = True
    if interactive:
        pyvista.OFF_SCREEN = False
    else:
        pyvista.OFF_SCREEN = True
    try:
        yield
    finally:
        pyvista.OFF_SCREEN = orig_offscreen
        renderer.MNE_3D_BACKEND_TESTING = orig_testing


def _qicon(name):
    # Get icon from theme with a file fallback
    return QIcon.fromTheme(
        name, QIcon(str(_ICONS_PATH / "light" / "actions" / f"{name}.svg"))
    )
