diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 8f768b7c7bf..e6d32bbefb9 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -419,7 +419,7 @@ def __init__(self, subject_id, hemi, surf, title=None, if len(size) not in (1, 2): raise ValueError('"size" parameter must be an int or length-2 ' 'sequence of ints.') - self._size = size if len(size) == 2 else size * 2 # 1-tuple to 2-tuple + size = size if len(size) == 2 else size * 2 # 1-tuple to 2-tuple subjects_dir = get_subjects_dir(subjects_dir) self.theme = theme @@ -474,12 +474,11 @@ def __init__(self, subject_id, hemi, surf, title=None, offset = (surf == 'inflated') offset = None if (not offset or hemi != 'both') else 0.0 - self._renderer = _get_renderer(name=self._title, size=self._size, + self._renderer = _get_renderer(name=self._title, size=size, bgcolor=background, shape=shape, fig=figure) - - self._renderer._window_initialize(self._clean) + self._renderer._window_close_connect(self._clean) self._renderer._window_set_theme(theme) self.plotter = self._renderer.plotter @@ -663,7 +662,7 @@ def setup_time_viewer(self, time_viewer=True, show_traces=True): self._configure_help() # show everything at the end self.toggle_interface() - self._renderer._window_show(self._size) + self._renderer.show() # sizes could change, update views for hemi in ('lh', 'rh'): @@ -726,7 +725,7 @@ def toggle_interface(self, value=None): self.visibility = value # update tool bar and dock - with self._renderer._window_ensure_minimum_sizes(self._size): + with self._renderer._window_ensure_minimum_sizes(): if self.visibility: self._renderer._dock_show() self._renderer._tool_bar_update_button_icon( diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index 9b80f9ccdd8..4e9eec86156 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -378,7 +378,7 @@ def test_brain_save_movie(tmpdir, renderer, brain_gc): brain.close() -_TINY_SIZE = (300, 250) +_TINY_SIZE = (350, 300) def tiny(tmpdir): diff --git a/mne/viz/backends/_abstract.py b/mne/viz/backends/_abstract.py index dbbf2abc232..d61b83f8c04 100644 --- a/mne/viz/backends/_abstract.py +++ b/mne/viz/backends/_abstract.py @@ -476,6 +476,10 @@ def _tool_bar_add_spacer(self): def _tool_bar_add_screenshot_button(self, name, desc, func): pass + @abstractmethod + def _tool_bar_set_theme(self, theme): + pass + class _AbstractDock(ABC): @abstractmethod @@ -716,8 +720,16 @@ def clear(self): class _AbstractWindow(ABC): + def _window_initialize(self): + self._window = None + self._interactor = None + self._mplcanvas = None + self._show_traces = None + self._separate_canvas = None + self._interactor_fraction = None + @abstractmethod - def _window_initialize(self, func=None): + def _window_close_connect(self, func): pass @abstractmethod @@ -757,13 +769,9 @@ def _window_set_cursor(self, cursor): pass @abstractmethod - def _window_ensure_minimum_sizes(self, sz): + def _window_ensure_minimum_sizes(self): pass @abstractmethod def _window_set_theme(self, theme): pass - - @abstractmethod - def _window_show(self, sz): - pass diff --git a/mne/viz/backends/_notebook.py b/mne/viz/backends/_notebook.py index 705a1b9e55a..3c3a11d2f0d 100644 --- a/mne/viz/backends/_notebook.py +++ b/mne/viz/backends/_notebook.py @@ -194,6 +194,9 @@ def _screenshot(): placeholder="Type a file name", ) + def _tool_bar_set_theme(self, theme): + pass + class _IpyMenuBar(_AbstractMenuBar): def _menu_initialize(self, window=None): @@ -244,14 +247,8 @@ def __init__(self, brain, width, height, dpi): class _IpyWindow(_AbstractWindow): - def _window_initialize(self, func=None): - self._window = None - self._interactor = None - self._mplcanvas = None - self._show_traces = None - self._separate_canvas = None - self._splitter = None - self._interactor_fraction = None + def _window_close_connect(self, func): + pass def _window_get_dpi(self): return 96 @@ -282,15 +279,12 @@ def _window_set_cursor(self, cursor): pass @contextmanager - def _window_ensure_minimum_sizes(self, sz): + def _window_ensure_minimum_sizes(self): yield def _window_set_theme(self, theme): pass - def _window_show(self, sz): - self.show() - class _IpyWidget(_AbstractWidget): def set_value(self, value): diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index c596e2bbce8..bc01a2da8e8 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -23,8 +23,7 @@ from ._abstract import _AbstractRenderer from ._utils import (_get_colormap_from_array, _alpha_blend_background, - ALLOWED_QUIVER_MODES, _init_qt_resources, - _qt_disable_paint) + ALLOWED_QUIVER_MODES, _init_qt_resources) from ...fixes import _get_args from ...transforms import apply_trans from ...utils import copy_base_doc_to_subclass_doc, _check_option @@ -211,35 +210,6 @@ def _get_screenshot_filename(self): dt_string = now.strftime("_%Y-%m-%d_%H-%M-%S") return "MNE" + dt_string + ".png" - @contextmanager - def _ensure_minimum_sizes(self): - sz = self.figure.store['window_size'] - # 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.plotter.interactor.setMinimumSize(*sz) - try: - yield # show - finally: - # 1. Process events - _process_events(self.plotter) - _process_events(self.plotter) - # 2. Get the window and interactor sizes that work - win_sz = self.plotter.app_window.size() - ren_sz = self.plotter.interactor.size() - # 3. Undo the min size setting and process events - self.plotter.interactor.setMinimumSize(0, 0) - _process_events(self.plotter) - _process_events(self.plotter) - # 4. Resize the window and interactor to the correct size - # (not sure why, but this is required on macOS at least) - self.plotter.window_size = (win_sz.width(), win_sz.height()) - self.plotter.interactor.resize(ren_sz.width(), ren_sz.height()) - _process_events(self.plotter) - _process_events(self.plotter) - def _index_to_loc(self, idx): _ncols = self.figure._ncols row = idx // _ncols @@ -625,12 +595,6 @@ def scalarbar(self, source, color="white", title=None, n_labels=4, def show(self): self.plotter.show() - if hasattr(self.plotter, "app_window"): - with _qt_disable_paint(self.plotter): - with self._ensure_minimum_sizes(): - self.plotter.app_window.show() - self.plotter.update() - return self.scene() def close(self): _close_3d_figure(figure=self.figure) diff --git a/mne/viz/backends/_qt.py b/mne/viz/backends/_qt.py index 66ad4d90ce2..77124df2e82 100644 --- a/mne/viz/backends/_qt.py +++ b/mne/viz/backends/_qt.py @@ -16,7 +16,7 @@ QHBoxLayout, QLabel, QToolButton, QMenuBar, QSlider, QSpinBox, QVBoxLayout, QWidget, QSizePolicy, QScrollArea, QStyle, QProgressBar, - QStyleOptionSlider, QLayout, QSplitter) + QStyleOptionSlider, QLayout) from ._pyvista import _PyVistaRenderer from ._pyvista import (_close_all, _close_3d_figure, _check_3d_figure, # noqa: F401,E501 analysis:ignore @@ -42,18 +42,10 @@ def _layout_add_widget(self, layout, widget, max_width=None): class _QtDock(_AbstractDock, _QtLayout): def _dock_initialize(self, window=None): - self.dock = QDockWidget() - self.scroll = QScrollArea(self.dock) - self.dock.setWidget(self.scroll) - widget = QWidget(self.scroll) - self.scroll.setWidget(widget) - self.scroll.setWidgetResizable(True) - self.dock.setAllowedAreas(Qt.LeftDockWidgetArea) - self.dock.setFeatures(QDockWidget.NoDockWidgetFeatures) window = self._window if window is None else window - window.addDockWidget(Qt.LeftDockWidgetArea, self.dock) - self.dock_layout = QVBoxLayout() - widget.setLayout(self.dock_layout) + self.dock, self.dock_layout = _create_dock_widget( + self._window, "Controls", Qt.LeftDockWidgetArea) + window.setCorner(Qt.BottomLeftCorner, Qt.LeftDockWidgetArea) def _dock_finalize(self): self.dock.setMinimumSize(self.dock.sizeHint().width(), 0) @@ -364,17 +356,14 @@ def __init__(self, brain, width, height, dpi): class _QtWindow(_AbstractWindow): - def _window_initialize(self, func=None): - self._window = self.figure.plotter.app_window + def _window_initialize(self): + super()._window_initialize() self._interactor = self.figure.plotter.interactor - self._mplcanvas = None - self._show_traces = None - self._separate_canvas = None - self._splitter = None - self._interactor_fraction = None + self._window = self.figure.plotter.app_window self._window.setLocale(QLocale(QLocale.Language.English)) - if func is not None: - self._window.signal_close.connect(func) + + def _window_close_connect(self, func): + self._window.signal_close.connect(func) def _window_get_dpi(self): return self._window.windowHandle().screen().logicalDotsPerInch() @@ -399,16 +388,9 @@ def _window_get_mplcanvas(self, brain, interactor_fraction, show_traces, def _window_adjust_mplcanvas_layout(self): canvas = self._mplcanvas.canvas - vlayout = self._interactor.frame.layout() - vlayout.removeWidget(self._interactor) - splitter = QSplitter( - orientation=Qt.Vertical, - parent=self._interactor.frame - ) - vlayout.addWidget(splitter) - splitter.addWidget(self._interactor) - splitter.addWidget(canvas) - self._splitter = splitter + dock, dock_layout = _create_dock_widget( + self._window, "Traces", Qt.BottomDockWidgetArea) + dock_layout.addWidget(canvas) def _window_get_cursor(self): return self._interactor.cursor() @@ -417,33 +399,38 @@ def _window_set_cursor(self, cursor): self._interactor.setCursor(cursor) @contextmanager - def _window_ensure_minimum_sizes(self, sz): - """Ensure that widgets respect the windows size.""" + def _window_ensure_minimum_sizes(self): + sz = self.figure.store['window_size'] adjust_mpl = (self._show_traces and not self._separate_canvas) - if not adjust_mpl: - yield - else: + # 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) + if adjust_mpl: mpl_h = int(round((sz[1] * self._interactor_fraction) / (1 - self._interactor_fraction))) self._mplcanvas.canvas.setMinimumSize(sz[0], mpl_h) - try: - yield - finally: - self._splitter.setSizes([sz[1], mpl_h]) - # 1. Process events - self._process_events() - self._process_events() - # 2. Get the window size that accommodates the size - sz = self._window.size() - # 3. Call app_window.setBaseSize and resize (in pyvistaqt) - self.figure.plotter.window_size = (sz.width(), sz.height()) - # 4. Undo the min size setting and process events - self._interactor.setMinimumSize(0, 0) - self._process_events() - self._process_events() - # 5. Resize the window (again!) to the correct size - # (not sure why, but this is required on macOS at least) - self.figure.plotter.window_size = (sz.width(), sz.height()) + 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._process_events() + self._process_events() + # 4. 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_sz.height()) + self._interactor.resize(ren_sz.width(), ren_sz.height()) self._process_events() self._process_events() @@ -468,11 +455,6 @@ def _window_set_theme(self, theme): self._window.setStyleSheet(stylesheet) - def _window_show(self, sz): - with _qt_disable_paint(self._interactor): - with self._window_ensure_minimum_sizes(sz): - self.show() - class _QtWidget(_AbstractWidget): def set_value(self, value): @@ -495,7 +477,34 @@ def get_value(self): class _Renderer(_PyVistaRenderer, _QtDock, _QtToolBar, _QtMenuBar, _QtStatusBar, _QtWindow, _QtPlayback): - pass + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._window_initialize() + + def show(self): + super().show() + with _qt_disable_paint(self.plotter): + with self._window_ensure_minimum_sizes(): + self.plotter.app_window.show() + self.plotter.update() + + +def _create_dock_widget(window, name, area): + dock = QDockWidget() + scroll = QScrollArea(dock) + dock.setWidget(scroll) + 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 + dock.setStyleSheet("QDockWidget { margin: 4px; }") + return dock, dock_layout def _detect_theme():