From f2ebe9e398292d4057a97ff570a66878b1172ceb Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 24 Mar 2021 16:49:08 +0100 Subject: [PATCH 01/15] First commit --- mne/viz/_brain/_brain.py | 26 +++++++++++++------------- mne/viz/backends/_abstract.py | 3 ++- mne/viz/backends/_notebook.py | 26 +++++++++++++------------- mne/viz/backends/_pyvista.py | 4 ++-- mne/viz/backends/_qt.py | 24 ++++++++++++------------ 5 files changed, 42 insertions(+), 41 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 8f768b7c7bf..6bdf6b34c46 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -1226,22 +1226,21 @@ def _configure_picking(self): self._on_pick ) - def _save_movie_noname(self): - return self.save_movie(None) - def _configure_tool_bar(self): self._renderer._tool_bar_load_icons() self._renderer._tool_bar_set_theme(self.theme) self._renderer._tool_bar_initialize() - self._renderer._tool_bar_add_screenshot_button( + self._renderer._tool_bar_add_file_button( name="screenshot", desc="Take a screenshot", - func=partial(self.screenshot, time_viewer=True), + func=self.save_image, + default_name=self._renderer._get_default_filename(".png"), ) - self._renderer._tool_bar_add_button( + self._renderer._tool_bar_add_file_button( name="movie", desc="Save movie...", - func=self._save_movie_noname, + func=self.save_movie, + default_name=self._renderer._get_default_filename(".mp4"), shortcut="ctrl+shift+s", ) self._renderer._tool_bar_add_button( @@ -2581,7 +2580,9 @@ def save_image(self, filename, mode='rgb'): mode : str Either 'rgb' or 'rgba' for values to return. """ - self._renderer.screenshot(mode=mode, filename=filename) + from ..utils import _save_ndarray_img + _save_ndarray_img( + filename, self.screenshot(mode=mode, time_viewer=True)) @fill_doc def screenshot(self, mode='rgb', time_viewer=False): @@ -3042,12 +3043,11 @@ def save_movie(self, filename, time_dilation=4., tmin=None, tmax=None, The opened dialog is returned for testing purpose only. """ if self.time_viewer: - try: - from pyvista.plotting.qt_plotting import FileDialog - except ImportError: - from pyvistaqt.plotting import FileDialog - if filename is None: + try: + from pyvista.plotting.qt_plotting import FileDialog + except ImportError: + from pyvistaqt.plotting import FileDialog self.status_msg.setText("Choose movie path ...") self.status_msg.show() self.status_progress.setValue(0) diff --git a/mne/viz/backends/_abstract.py b/mne/viz/backends/_abstract.py index dbbf2abc232..14da423864e 100644 --- a/mne/viz/backends/_abstract.py +++ b/mne/viz/backends/_abstract.py @@ -473,7 +473,8 @@ def _tool_bar_add_spacer(self): pass @abstractmethod - def _tool_bar_add_screenshot_button(self, name, desc, func): + def _tool_bar_add_file_button(self, name, desc, func, default_name, + shortcut=None): pass diff --git a/mne/viz/backends/_notebook.py b/mne/viz/backends/_notebook.py index 705a1b9e55a..247d572620a 100644 --- a/mne/viz/backends/_notebook.py +++ b/mne/viz/backends/_notebook.py @@ -9,7 +9,6 @@ from ipywidgets import (Button, Dropdown, FloatSlider, FloatText, HBox, IntSlider, IntText, Text, VBox) -from ..utils import _save_ndarray_img from ...fixes import nullcontext from ._abstract import (_AbstractDock, _AbstractToolBar, _AbstractMenuBar, _AbstractStatusBar, _AbstractLayout, _AbstractWidget, @@ -175,24 +174,25 @@ def _tool_bar_add_text(self, name, value, placeholder): def _tool_bar_add_spacer(self): pass - def _tool_bar_add_screenshot_button(self, name, desc, func): - def _screenshot(): + def _tool_bar_add_file_button(self, name, desc, func, default_name, + shortcut=None): + def callback(): fname = self.actions[f"{name}_field"].value - fname = self._get_screenshot_filename() \ - if len(fname) == 0 else fname - img = func() - _save_ndarray_img(fname, img) - - self._tool_bar_add_button( - name=name, - desc=desc, - func=_screenshot, - ) + fname = default_name if len(fname) == 0 else fname + func(fname) self._tool_bar_add_text( name=f"{name}_field", value=None, placeholder="Type a file name", ) + self._tool_bar_add_button( + name=name, + desc=desc, + func=callback, + ) + + def _tool_bar_add_movie_button(self, name, desc, func): + pass class _IpyMenuBar(_AbstractMenuBar): diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index c596e2bbce8..1bf1c6ebde3 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -206,10 +206,10 @@ def _update(self): for plotter in self._all_plotters: plotter.update() - def _get_screenshot_filename(self): + def _get_default_filename(self, ext=".png"): now = datetime.now() dt_string = now.strftime("_%Y-%m-%d_%H-%M-%S") - return "MNE" + dt_string + ".png" + return "MNE" + dt_string + ext @contextmanager def _ensure_minimum_sizes(self): diff --git a/mne/viz/backends/_qt.py b/mne/viz/backends/_qt.py index 66ad4d90ce2..a0143300782 100644 --- a/mne/viz/backends/_qt.py +++ b/mne/viz/backends/_qt.py @@ -6,9 +6,12 @@ # License: Simplified BSD from contextlib import contextmanager -from functools import partial import pyvista +try: + from pyvista.plotting.qt_plotting import FileDialog +except ImportError: + from pyvistaqt.plotting import FileDialog from PyQt5.QtCore import Qt, pyqtSignal, QLocale from PyQt5.QtGui import QIcon, QImage, QPixmap @@ -26,7 +29,7 @@ _AbstractWindow, _AbstractMplCanvas, _AbstractPlayback, _AbstractBrainMplCanvas, _AbstractMplInterface) from ._utils import _init_qt_resources, _qt_disable_paint -from ..utils import _save_ndarray_img, logger +from ..utils import logger class _QtLayout(_AbstractLayout): @@ -265,22 +268,19 @@ def _tool_bar_add_spacer(self): spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) self.tool_bar.addWidget(spacer) - def _tool_bar_add_screenshot_button(self, name, desc, func): - def _screenshot(): - img = func() - try: - from pyvista.plotting.qt_plotting import FileDialog - except ImportError: - from pyvistaqt.plotting import FileDialog - FileDialog( + def _tool_bar_add_file_button(self, name, desc, func, default_name, + shortcut=None): + def callback(): + return FileDialog( self.plotter.app_window, - callback=partial(_save_ndarray_img, img=img), + callback=func, ) self._tool_bar_add_button( name=name, desc=desc, - func=_screenshot, + func=callback, + shortcut=shortcut, ) def _tool_bar_set_theme(self, theme): From ddc36f9bff7040dcee327cc3bd3baf5c924552c3 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 24 Mar 2021 16:51:40 +0100 Subject: [PATCH 02/15] Fix icon --- mne/viz/backends/_notebook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/viz/backends/_notebook.py b/mne/viz/backends/_notebook.py index 8c0fc0176f1..f2feaa2e788 100644 --- a/mne/viz/backends/_notebook.py +++ b/mne/viz/backends/_notebook.py @@ -141,7 +141,7 @@ def _tool_bar_load_icons(self): self.icons["reset"] = "history" self.icons["scale"] = "magic" self.icons["clear"] = "trash" - self.icons["movie"] = None + self.icons["movie"] = "video-camera" self.icons["restore"] = "replay" self.icons["screenshot"] = "camera" self.icons["visibility_on"] = "eye" From 4702a0ee7fa7de74a9ea43afa6ef110cc5f2d549 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 24 Mar 2021 16:55:40 +0100 Subject: [PATCH 03/15] Remove cruft --- mne/viz/backends/_notebook.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/mne/viz/backends/_notebook.py b/mne/viz/backends/_notebook.py index f2feaa2e788..463484bc632 100644 --- a/mne/viz/backends/_notebook.py +++ b/mne/viz/backends/_notebook.py @@ -191,9 +191,6 @@ def callback(): func=callback, ) - def _tool_bar_add_movie_button(self, name, desc, func): - pass - def _tool_bar_set_theme(self, theme): pass From a46de62c7abc41a2164f3e0d9d8c29dae726a000 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 24 Mar 2021 17:03:40 +0100 Subject: [PATCH 04/15] Refactor --- mne/viz/_brain/_brain.py | 7 ++++--- mne/viz/backends/_pyvista.py | 6 ------ mne/viz/utils.py | 7 +++++++ 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index a1667407c2e..7e179254591 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -27,7 +27,8 @@ from .callback import (ShowView, TimeCallBack, SmartCallBack, UpdateLUT, UpdateColorbarScale) -from ..utils import _show_help_fig, _get_color_list, concatenate_images +from ..utils import (_show_help_fig, _get_color_list, concatenate_images, + _generate_default_filename) from .._3d import _process_clim, _handle_time, _check_views from ...externals.decorator import decorator @@ -1233,13 +1234,13 @@ def _configure_tool_bar(self): name="screenshot", desc="Take a screenshot", func=self.save_image, - default_name=self._renderer._get_default_filename(".png"), + default_name=_generate_default_filename(".png"), ) self._renderer._tool_bar_add_file_button( name="movie", desc="Save movie...", func=self.save_movie, - default_name=self._renderer._get_default_filename(".mp4"), + default_name=_generate_default_filename(".mp4"), shortcut="ctrl+shift+s", ) self._renderer._tool_bar_add_button( diff --git a/mne/viz/backends/_pyvista.py b/mne/viz/backends/_pyvista.py index bb17ea9dc0c..8e7cfac9be2 100644 --- a/mne/viz/backends/_pyvista.py +++ b/mne/viz/backends/_pyvista.py @@ -12,7 +12,6 @@ # License: Simplified BSD from contextlib import contextmanager -from datetime import datetime from distutils.version import LooseVersion import os import sys @@ -205,11 +204,6 @@ def _update(self): for plotter in self._all_plotters: plotter.update() - def _get_default_filename(self, ext=".png"): - now = datetime.now() - dt_string = now.strftime("_%Y-%m-%d_%H-%M-%S") - return "MNE" + dt_string + ext - def _index_to_loc(self, idx): _ncols = self.figure._ncols row = idx // _ncols diff --git a/mne/viz/utils.py b/mne/viz/utils.py index f5bb640e6dc..404b51ea574 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -23,6 +23,7 @@ from copy import deepcopy from distutils.version import LooseVersion import warnings +from datetime import datetime from ..defaults import _handle_default from ..fixes import _get_status @@ -2345,3 +2346,9 @@ def concatenate_images(images, axis=0, bgcolor='black', centered=True): ret[dec[0]:dec[0] + shape[0], dec[1]:dec[1] + shape[1], :] = image ptr += shape * sec return ret + + +def _generate_default_filename(ext=".png"): + now = datetime.now() + dt_string = now.strftime("_%Y-%m-%d_%H-%M-%S") + return "MNE" + dt_string + ext From 874e8e13ec59737bd726f5fe958efce21b7b205a Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 25 Mar 2021 13:32:05 +0100 Subject: [PATCH 05/15] Refactor --- mne/viz/_brain/_brain.py | 21 ++++++----- mne/viz/backends/_abstract.py | 22 +++++++++++- mne/viz/backends/_notebook.py | 67 +++++++++++++++++++++++------------ mne/viz/backends/_qt.py | 62 ++++++++++++++++++++------------ 4 files changed, 114 insertions(+), 58 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 7e179254591..41b13d2244c 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -705,7 +705,7 @@ def _clean(self): self.plotter.picker = None # XXX end PyVista for key in ('plotter', 'window', 'dock', 'tool_bar', 'menu_bar', - 'status_bar', 'interactor', 'mpl_canvas', 'time_actor', + 'interactor', 'mpl_canvas', 'time_actor', 'picked_renderer', 'act_data_smooth', '_scalar_bar', 'actions', 'widgets', 'geo', '_data'): setattr(self, key, None) @@ -3048,9 +3048,9 @@ def save_movie(self, filename, time_dilation=4., tmin=None, tmax=None, from pyvista.plotting.qt_plotting import FileDialog except ImportError: from pyvistaqt.plotting import FileDialog - self.status_msg.setText("Choose movie path ...") + self.status_msg.set_value("Choose movie path ...") self.status_msg.show() - self.status_progress.setValue(0) + self.status_progress.set_value(0) def _post_setup(unused): del unused @@ -3071,27 +3071,26 @@ def _post_setup(unused): def frame_callback(frame, n_frames): if frame == n_frames: # On the ImageIO step - self.status_msg.setText( + self.status_msg.set_value( "Saving with ImageIO: %s" % filename ) self.status_msg.show() self.status_progress.hide() - self.status_bar.layout().update() + self._renderer._status_bar_update() else: - self.status_msg.setText( + self.status_msg.set_value( "Rendering images (frame %d / %d) ..." % (frame + 1, n_frames) ) self.status_msg.show() self.status_progress.show() - self.status_progress.setRange(0, n_frames - 1) - self.status_progress.setValue(frame) + self.status_progress.forward( + "setRange", 0, n_frames - 1) + self.status_progress.set_value(frame) self.status_progress.update() - self.status_progress.repaint() self.status_msg.update() - self.status_msg.parent().update() - self.status_msg.repaint() + self._renderer._status_bar_update() # set cursor to busy default_cursor = self._renderer._window_get_cursor() diff --git a/mne/viz/backends/_abstract.py b/mne/viz/backends/_abstract.py index 91d3f0dc55c..5a0dbd852a1 100644 --- a/mne/viz/backends/_abstract.py +++ b/mne/viz/backends/_abstract.py @@ -566,6 +566,10 @@ def _status_bar_add_label(self, value, stretch=0): def _status_bar_add_progress_bar(self, stretch=0): pass + @abstractmethod + def _status_bar_update(self): + pass + class _AbstractPlayback(ABC): @abstractmethod @@ -579,7 +583,7 @@ def _layout_initialize(self, max_width): pass @abstractmethod - def _layout_add_widget(self, layout, widget): + def _layout_add_widget(self, layout, widget, stretch=0): pass @@ -599,6 +603,22 @@ def set_value(self, value): def get_value(self): pass + @abstractmethod + def show(self): + pass + + @abstractmethod + def hide(self): + pass + + @abstractmethod + def update(self, repaint=True): + pass + + def forward(self, name, *args, **kwargs): + if hasattr(self._widget, name): + getattr(self._widget, name)(*args, **kwargs) + class _AbstractMplInterface(ABC): @abstractmethod diff --git a/mne/viz/backends/_notebook.py b/mne/viz/backends/_notebook.py index 463484bc632..f800725a0d2 100644 --- a/mne/viz/backends/_notebook.py +++ b/mne/viz/backends/_notebook.py @@ -9,6 +9,7 @@ from ipywidgets import (Button, Dropdown, FloatSlider, FloatText, HBox, IntSlider, IntText, Text, VBox) +from ..utils import _generate_default_filename from ...fixes import nullcontext from ._abstract import (_AbstractDock, _AbstractToolBar, _AbstractMenuBar, _AbstractStatusBar, _AbstractLayout, _AbstractWidget, @@ -21,7 +22,7 @@ class _IpyLayout(_AbstractLayout): def _layout_initialize(self, max_width): self._layout_max_width = max_width - def _layout_add_widget(self, layout, widget): + def _layout_add_widget(self, layout, widget, stretch=0): widget.layout.margin = "2px 0px 2px 0px" widget.layout.min_width = "0px" children = list(layout.children) @@ -37,19 +38,19 @@ def _layout_add_widget(self, layout, widget): class _IpyDock(_AbstractDock, _IpyLayout): def _dock_initialize(self, window=None): - self.dock_width = 300 - self.dock = self.dock_layout = VBox() - self.dock.layout.width = f"{self.dock_width}px" - self._layout_initialize(self.dock_width) + self._dock_width = 300 + self._dock = self._dock_layout = VBox() + self._dock.layout.width = f"{self._dock_width}px" + self._layout_initialize(self._dock_width) def _dock_finalize(self): pass def _dock_show(self): - self.dock_layout.layout.visibility = "visible" + self._dock_layout.layout.visibility = "visible" def _dock_hide(self): - self.dock_layout.layout.visibility = "hidden" + self._dock_layout.layout.visibility = "hidden" def _dock_add_stretch(self, layout): pass @@ -58,7 +59,7 @@ def _dock_add_layout(self, vertical=True): return VBox() if vertical else HBox() def _dock_add_label(self, value, align=False, layout=None): - layout = self.dock_layout if layout is None else layout + layout = self._dock_layout if layout is None else layout widget = Text(value=value, disabled=True) self._layout_add_widget(layout, widget) return _IpyWidget(widget) @@ -70,7 +71,7 @@ def _dock_add_button(self, name, callback, layout=None): return _IpyWidget(widget) def _dock_named_layout(self, name, layout, compact): - layout = self.dock_layout if layout is None else layout + 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( @@ -119,7 +120,7 @@ def _dock_add_combo_box(self, name, value, rng, return _IpyWidget(widget) def _dock_add_group_box(self, name, layout=None): - layout = self.dock_layout if layout is None else layout + layout = self._dock_layout if layout is None else layout hlayout = VBox() self._layout_add_widget(layout, hlayout) return hlayout @@ -149,7 +150,7 @@ def _tool_bar_load_icons(self): def _tool_bar_initialize(self, name="default", window=None): self.actions = dict() - self.tool_bar = HBox() + self._tool_bar = self._tool_bar_layout = HBox() self._layout_initialize(None) def _tool_bar_add_button(self, name, desc, func, icon_name=None, @@ -160,7 +161,7 @@ def _tool_bar_add_button(self, name, desc, func, icon_name=None, return widget = Button(tooltip=desc, icon=icon) widget.on_click(lambda x: func()) - self._layout_add_widget(self.tool_bar, widget) + self._layout_add_widget(self._tool_bar_layout, widget) self.actions[name] = widget def _tool_bar_update_button_icon(self, name, icon_name): @@ -168,7 +169,7 @@ def _tool_bar_update_button_icon(self, name, icon_name): def _tool_bar_add_text(self, name, value, placeholder): widget = Text(value=value, placeholder=placeholder) - self._layout_add_widget(self.tool_bar, widget) + self._layout_add_widget(self._tool_bar_layout, widget) self.actions[name] = widget def _tool_bar_add_spacer(self): @@ -206,16 +207,22 @@ def _menu_add_button(self, menu_name, name, desc, func): pass -class _IpyStatusBar(_AbstractStatusBar): +class _IpyStatusBar(_AbstractStatusBar, _IpyLayout): def _status_bar_initialize(self, window=None): - pass + self._status_bar = self._status_bar_layout = HBox() + self._layout_initialize(None) def _status_bar_add_label(self, value, stretch=0): - pass + widget = Text(value=value, disabled=True) + self._layout_add_widget(self._status_bar_layout, widget) + return _IpyWidget(widget) def _status_bar_add_progress_bar(self, stretch=0): pass + def _status_bar_update(self): + pass + class _IpyPlayback(_AbstractPlayback): def _playback_initialize(self, func, timeout): @@ -290,12 +297,22 @@ def set_value(self, value): def get_value(self): return self._widget.value + def show(self): + self._widget.layout.visibility = "visible" + + def hide(self): + self._widget.layout.visibility = "hidden" + + def update(self, repaint=True): + pass + class _Renderer(_PyVistaRenderer, _IpyDock, _IpyToolBar, _IpyMenuBar, _IpyStatusBar, _IpyWindow, _IpyPlayback): def __init__(self, *args, **kwargs): - self.dock = None - self.tool_bar = None + self._dock = None + self._tool_bar = None + self._status_bar = None kwargs["notebook"] = True super().__init__(*args, **kwargs) @@ -306,28 +323,32 @@ def _update(self): def _create_default_tool_bar(self): self._tool_bar_load_icons() self._tool_bar_initialize() - self._tool_bar_add_screenshot_button( + self._tool_bar_add_file_button( name="screenshot", desc="Take a screenshot", func=self.screenshot, + default_name=_generate_default_filename(".png"), ) def show(self): # default tool bar - if self.tool_bar is None: + if self._tool_bar is None: self._create_default_tool_bar() - display(self.tool_bar) + display(self._tool_bar) # viewer viewer = self.plotter.show( use_ipyvtk=True, return_viewer=True) viewer.layout.width = None # unlock the fixed layout # main widget - if self.dock is None: + if self._dock is None: main_widget = viewer else: - main_widget = HBox([self.dock, viewer]) + main_widget = HBox([self._dock, viewer]) display(main_widget) self.figure.display = viewer + # status bar + if self._status_bar is not None: + display(self._status_bar) return self.scene() diff --git a/mne/viz/backends/_qt.py b/mne/viz/backends/_qt.py index cc1e0915c3c..83ac71a609f 100644 --- a/mne/viz/backends/_qt.py +++ b/mne/viz/backends/_qt.py @@ -36,7 +36,7 @@ class _QtLayout(_AbstractLayout): def _layout_initialize(self, max_width): pass - def _layout_add_widget(self, layout, widget, max_width=None): + def _layout_add_widget(self, layout, widget, stretch=0): if isinstance(widget, QLayout): layout.addLayout(widget) else: @@ -46,19 +46,19 @@ def _layout_add_widget(self, layout, widget, max_width=None): class _QtDock(_AbstractDock, _QtLayout): def _dock_initialize(self, window=None): window = self._window if window is None else window - self.dock, self.dock_layout = _create_dock_widget( + 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) - self._dock_add_stretch(self.dock_layout) + self._dock.setMinimumSize(self._dock.sizeHint().width(), 0) + self._dock_add_stretch(self._dock_layout) def _dock_show(self): - self.dock.show() + self._dock.show() def _dock_hide(self): - self.dock.hide() + self._dock.hide() def _dock_add_stretch(self, layout): layout.addStretch() @@ -68,7 +68,7 @@ def _dock_add_layout(self, vertical=True): return layout def _dock_add_label(self, value, align=False, layout=None): - layout = self.dock_layout if layout is None else layout + layout = self._dock_layout if layout is None else layout widget = QLabel() if align: widget.setAlignment(Qt.AlignCenter) @@ -77,7 +77,7 @@ def _dock_add_label(self, value, align=False, layout=None): return _QtWidget(widget) def _dock_add_button(self, name, callback, layout=None): - layout = self.dock_layout if layout is None else layout + layout = self._dock_layout if layout is None else layout # If we want one with text instead of an icon, we should use # QPushButton(name) widget = QToolButton() @@ -87,7 +87,7 @@ def _dock_add_button(self, name, callback, layout=None): return _QtWidget(widget) def _dock_named_layout(self, name, layout, compact): - layout = self.dock_layout if layout is None else layout + 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( @@ -139,7 +139,7 @@ def _dock_add_combo_box(self, name, value, rng, return _QtWidget(widget) def _dock_add_group_box(self, name, layout=None): - layout = self.dock_layout if layout is None else layout + layout = self._dock_layout if layout is None else layout hlayout = QVBoxLayout() widget = QGroupBox(name) widget.setLayout(hlayout) @@ -239,13 +239,14 @@ def _tool_bar_load_icons(self): 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 = 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] = self.tool_bar.addAction(icon, desc, func) + self.actions[name] = self._tool_bar.addAction(icon, desc, func) if shortcut is not None: self.actions[name].setShortcut(shortcut) @@ -258,7 +259,7 @@ def _tool_bar_add_text(self, name, value, placeholder): def _tool_bar_add_spacer(self): spacer = QWidget() spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) - self.tool_bar.addWidget(spacer) + self._tool_bar.addWidget(spacer) def _tool_bar_add_file_button(self, name, desc, func, default_name, shortcut=None): @@ -291,33 +292,37 @@ class _QtMenuBar(_AbstractMenuBar): def _menu_initialize(self, window=None): self._menus = dict() self._menu_actions = dict() - self.menu_bar = QMenuBar() - self.menu_bar.setNativeMenuBar(False) + self._menu_bar = QMenuBar() + self._menu_bar.setNativeMenuBar(False) window = self._window if window is None else window - window.setMenuBar(self.menu_bar) + window.setMenuBar(self._menu_bar) def _menu_add_submenu(self, name, desc): - self._menus[name] = self.menu_bar.addMenu(desc) + self._menus[name] = self._menu_bar.addMenu(desc) def _menu_add_button(self, menu_name, name, desc, func): menu = self._menus[menu_name] self._menu_actions[name] = menu.addAction(desc, func) -class _QtStatusBar(_AbstractStatusBar): +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() + self._status_bar = window.statusBar() + self._status_bar_layout = self._status_bar.layout() def _status_bar_add_label(self, value, stretch=0): widget = QLabel(value) - self.status_bar.layout().addWidget(widget, stretch) - return widget + 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.status_bar.layout().addWidget(widget, stretch) - return widget + 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): @@ -474,6 +479,17 @@ def get_value(self): elif hasattr(self._widget, "text"): return self._widget.text() + def show(self): + self._widget.show() + + def hide(self): + self._widget.hide() + + def update(self, repaint=True): + self._widget.update() + if repaint: + self._widget.repaint() + class _Renderer(_PyVistaRenderer, _QtDock, _QtToolBar, _QtMenuBar, _QtStatusBar, _QtWindow, _QtPlayback): From bdcece58a5a66bcdd020a3a389aa2ff925989c4d Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 25 Mar 2021 14:03:48 +0100 Subject: [PATCH 06/15] Refactor --- mne/viz/_brain/_brain.py | 6 ++---- mne/viz/backends/_abstract.py | 4 ++++ mne/viz/backends/_notebook.py | 3 +++ mne/viz/backends/_qt.py | 5 ++++- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 41b13d2244c..e89badfc81d 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -3065,9 +3065,6 @@ def _post_setup(unused): dialog.finished.connect(_post_setup) return dialog else: - from PyQt5.QtCore import Qt - from PyQt5.QtGui import QCursor - def frame_callback(frame, n_frames): if frame == n_frames: # On the ImageIO step @@ -3094,7 +3091,8 @@ def frame_callback(frame, n_frames): # set cursor to busy default_cursor = self._renderer._window_get_cursor() - self._renderer._window_set_cursor(QCursor(Qt.WaitCursor)) + self._renderer._window_set_cursor( + self._renderer._window_new_cursor("WaitCursor")) try: self._save_movie( diff --git a/mne/viz/backends/_abstract.py b/mne/viz/backends/_abstract.py index 5a0dbd852a1..d4d8de1b82e 100644 --- a/mne/viz/backends/_abstract.py +++ b/mne/viz/backends/_abstract.py @@ -789,6 +789,10 @@ def _window_get_cursor(self): def _window_set_cursor(self, cursor): pass + @abstractmethod + def _window_new_cursor(self, name): + pass + @abstractmethod def _window_ensure_minimum_sizes(self): pass diff --git a/mne/viz/backends/_notebook.py b/mne/viz/backends/_notebook.py index f800725a0d2..94287ecf540 100644 --- a/mne/viz/backends/_notebook.py +++ b/mne/viz/backends/_notebook.py @@ -282,6 +282,9 @@ def _window_get_cursor(self): def _window_set_cursor(self, cursor): pass + def _window_new_cursor(self, name): + pass + @contextmanager def _window_ensure_minimum_sizes(self): yield diff --git a/mne/viz/backends/_qt.py b/mne/viz/backends/_qt.py index 83ac71a609f..f65491dd8fe 100644 --- a/mne/viz/backends/_qt.py +++ b/mne/viz/backends/_qt.py @@ -14,7 +14,7 @@ from pyvistaqt.plotting import FileDialog from PyQt5.QtCore import Qt, pyqtSignal, QLocale -from PyQt5.QtGui import QIcon, QImage, QPixmap +from PyQt5.QtGui import QIcon, QImage, QPixmap, QCursor from PyQt5.QtWidgets import (QComboBox, QDockWidget, QDoubleSpinBox, QGroupBox, QHBoxLayout, QLabel, QToolButton, QMenuBar, QSlider, QSpinBox, QVBoxLayout, QWidget, @@ -403,6 +403,9 @@ def _window_get_cursor(self): def _window_set_cursor(self, cursor): self._interactor.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'] From d711b14e4b96c2074db9bc2354273492f9368aeb Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 25 Mar 2021 14:28:33 +0100 Subject: [PATCH 07/15] Refactor --- mne/viz/_brain/_brain.py | 115 ++++++++++++++-------------------- mne/viz/backends/_abstract.py | 8 +-- mne/viz/backends/_notebook.py | 10 ++- mne/viz/backends/_qt.py | 3 + 4 files changed, 63 insertions(+), 73 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index e89badfc81d..dc63217e18d 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -2989,6 +2989,50 @@ def _save_movie(self, filename, time_dilation=4., tmin=None, tmax=None, kwargs['bitrate'] = bitrate imageio.mimwrite(filename, images, **kwargs) + def _save_movie_tv(self, filename, time_dilation=4., tmin=None, tmax=None, + framerate=24, interpolation=None, codec=None, + bitrate=None, callback=None, time_viewer=False, + **kwargs): + def frame_callback(frame, n_frames): + if frame == n_frames: + # On the ImageIO step + self.status_msg.set_value( + "Saving with ImageIO: %s" + % filename + ) + self.status_msg.show() + self.status_progress.hide() + self._renderer._status_bar_update() + else: + self.status_msg.set_value( + "Rendering images (frame %d / %d) ..." + % (frame + 1, n_frames) + ) + self.status_msg.show() + self.status_progress.show() + self.status_progress.set_range([0, n_frames - 1]) + self.status_progress.set_value(frame) + self.status_progress.update() + self.status_msg.update() + self._renderer._status_bar_update() + + # set cursor to busy + default_cursor = self._renderer._window_get_cursor() + self._renderer._window_set_cursor( + self._renderer._window_new_cursor("WaitCursor")) + + try: + self._save_movie( + filename=filename, + time_dilation=(1. / self.playback_speed), + callback=frame_callback, + **kwargs + ) + except (Exception, KeyboardInterrupt): + warn('Movie saving aborted:\n' + traceback.format_exc()) + finally: + self._renderer._window_set_cursor(default_cursor) + @fill_doc def save_movie(self, filename, time_dilation=4., tmin=None, tmax=None, framerate=24, interpolation=None, codec=None, @@ -3042,73 +3086,10 @@ def save_movie(self, filename, time_dilation=4., tmin=None, tmax=None, dialog : object The opened dialog is returned for testing purpose only. """ - if self.time_viewer: - if filename is None: - try: - from pyvista.plotting.qt_plotting import FileDialog - except ImportError: - from pyvistaqt.plotting import FileDialog - self.status_msg.set_value("Choose movie path ...") - self.status_msg.show() - self.status_progress.set_value(0) - - def _post_setup(unused): - del unused - self.status_msg.hide() - self.status_progress.hide() - - dialog = FileDialog( - self.plotter.app_window, - callback=partial(self._save_movie, **kwargs) - ) - dialog.setDirectory(os.getcwd()) - dialog.finished.connect(_post_setup) - return dialog - else: - def frame_callback(frame, n_frames): - if frame == n_frames: - # On the ImageIO step - self.status_msg.set_value( - "Saving with ImageIO: %s" - % filename - ) - self.status_msg.show() - self.status_progress.hide() - self._renderer._status_bar_update() - else: - self.status_msg.set_value( - "Rendering images (frame %d / %d) ..." - % (frame + 1, n_frames) - ) - self.status_msg.show() - self.status_progress.show() - self.status_progress.forward( - "setRange", 0, n_frames - 1) - self.status_progress.set_value(frame) - self.status_progress.update() - self.status_msg.update() - self._renderer._status_bar_update() - - # set cursor to busy - default_cursor = self._renderer._window_get_cursor() - self._renderer._window_set_cursor( - self._renderer._window_new_cursor("WaitCursor")) - - try: - self._save_movie( - filename=filename, - time_dilation=(1. / self.playback_speed), - callback=frame_callback, - **kwargs - ) - except (Exception, KeyboardInterrupt): - warn('Movie saving aborted:\n' + traceback.format_exc()) - finally: - self._renderer._window_set_cursor(default_cursor) - else: - self._save_movie(filename, time_dilation, tmin, tmax, - framerate, interpolation, codec, - bitrate, callback, time_viewer, **kwargs) + func = self._save_movie_tv if self.time_viewer else self._save_movie + func(filename, time_dilation, tmin, tmax, + framerate, interpolation, codec, + bitrate, callback, time_viewer, **kwargs) def _make_movie_frames(self, time_dilation, tmin, tmax, framerate, interpolation, callback, time_viewer): diff --git a/mne/viz/backends/_abstract.py b/mne/viz/backends/_abstract.py index d4d8de1b82e..37b605b46dc 100644 --- a/mne/viz/backends/_abstract.py +++ b/mne/viz/backends/_abstract.py @@ -603,6 +603,10 @@ def set_value(self, value): def get_value(self): pass + @abstractmethod + def set_range(self, rng): + pass + @abstractmethod def show(self): pass @@ -615,10 +619,6 @@ def hide(self): def update(self, repaint=True): pass - def forward(self, name, *args, **kwargs): - if hasattr(self._widget, name): - getattr(self._widget, name)(*args, **kwargs) - class _AbstractMplInterface(ABC): @abstractmethod diff --git a/mne/viz/backends/_notebook.py b/mne/viz/backends/_notebook.py index 94287ecf540..3d612ae14aa 100644 --- a/mne/viz/backends/_notebook.py +++ b/mne/viz/backends/_notebook.py @@ -7,7 +7,7 @@ from contextlib import contextmanager from IPython.display import display from ipywidgets import (Button, Dropdown, FloatSlider, FloatText, HBox, - IntSlider, IntText, Text, VBox) + IntSlider, IntText, Text, VBox, IntProgress) from ..utils import _generate_default_filename from ...fixes import nullcontext @@ -218,7 +218,9 @@ def _status_bar_add_label(self, value, stretch=0): return _IpyWidget(widget) def _status_bar_add_progress_bar(self, stretch=0): - pass + widget = IntProgress() + self._layout_add_widget(self._status_bar_layout, widget) + return _IpyWidget(widget) def _status_bar_update(self): pass @@ -300,6 +302,10 @@ def set_value(self, value): def get_value(self): return self._widget.value + def set_range(self, rng): + self._widget.min = rng[0] + self._widget.max = rng[1] + def show(self): self._widget.layout.visibility = "visible" diff --git a/mne/viz/backends/_qt.py b/mne/viz/backends/_qt.py index f65491dd8fe..e10b54d9028 100644 --- a/mne/viz/backends/_qt.py +++ b/mne/viz/backends/_qt.py @@ -482,6 +482,9 @@ def get_value(self): elif 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() From fac7f9354c402ea8087d0fbe6be891cec9d76926 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 26 Mar 2021 13:30:07 +0100 Subject: [PATCH 08/15] Update conftest --- mne/conftest.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mne/conftest.py b/mne/conftest.py index 0f4607d22c3..0aa7ba704a2 100644 --- a/mne/conftest.py +++ b/mne/conftest.py @@ -330,15 +330,17 @@ def _use_backend(backend_name, interactive): def _check_skip_backend(name): from mne.viz.backends.tests._utils import (has_mayavi, has_pyvista, has_pyqt5, has_imageio_ffmpeg) + check_pyvista = name in ('pyvista', 'notebook') + check_pyqt5 = name in ('mayavi', 'pyvista') if name == 'mayavi': if not has_mayavi(): pytest.skip("Test skipped, requires mayavi.") elif name == 'pyvista': - if not has_pyvista(): - pytest.skip("Test skipped, requires pyvista.") if not has_imageio_ffmpeg(): pytest.skip("Test skipped, requires imageio-ffmpeg") - if not has_pyqt5(): + if check_pyvista and not has_pyvista(): + pytest.skip("Test skipped, requires pyvista.") + if check_pyqt5 and not has_pyqt5(): pytest.skip("Test skipped, requires PyQt5.") From b77af736b180d65dbb638b35209b89fa4dec218a Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 26 Mar 2021 13:46:01 +0100 Subject: [PATCH 09/15] Refactor --- mne/viz/_brain/_brain.py | 10 ++++++---- mne/viz/backends/_abstract.py | 3 +-- mne/viz/backends/_notebook.py | 6 ++---- mne/viz/backends/_qt.py | 3 +-- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index dc63217e18d..9e011ac0f85 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -1234,13 +1234,11 @@ def _configure_tool_bar(self): name="screenshot", desc="Take a screenshot", func=self.save_image, - default_name=_generate_default_filename(".png"), ) self._renderer._tool_bar_add_file_button( name="movie", desc="Save movie...", func=self.save_movie, - default_name=_generate_default_filename(".mp4"), shortcut="ctrl+shift+s", ) self._renderer._tool_bar_add_button( @@ -2570,7 +2568,7 @@ def reset_view(self): self._renderer.set_camera(**views_dicts[h][v], reset_camera=False) - def save_image(self, filename, mode='rgb'): + def save_image(self, filename=None, mode='rgb'): """Save view from all panels to disk. Parameters @@ -2580,6 +2578,8 @@ def save_image(self, filename, mode='rgb'): mode : str Either 'rgb' or 'rgba' for values to return. """ + default_name = _generate_default_filename(".png") + filename = default_name if filename is None else filename from ..utils import _save_ndarray_img _save_ndarray_img( filename, self.screenshot(mode=mode, time_viewer=True)) @@ -3034,7 +3034,7 @@ def frame_callback(frame, n_frames): self._renderer._window_set_cursor(default_cursor) @fill_doc - def save_movie(self, filename, time_dilation=4., tmin=None, tmax=None, + def save_movie(self, filename=None, time_dilation=4., tmin=None, tmax=None, framerate=24, interpolation=None, codec=None, bitrate=None, callback=None, time_viewer=False, **kwargs): """Save a movie (for data with a time axis). @@ -3086,6 +3086,8 @@ def save_movie(self, filename, time_dilation=4., tmin=None, tmax=None, dialog : object The opened dialog is returned for testing purpose only. """ + default_name = _generate_default_filename(".mp4") + filename = default_name if filename is None else filename func = self._save_movie_tv if self.time_viewer else self._save_movie func(filename, time_dilation, tmin, tmax, framerate, interpolation, codec, diff --git a/mne/viz/backends/_abstract.py b/mne/viz/backends/_abstract.py index 37b605b46dc..879e34786d9 100644 --- a/mne/viz/backends/_abstract.py +++ b/mne/viz/backends/_abstract.py @@ -473,8 +473,7 @@ def _tool_bar_add_spacer(self): pass @abstractmethod - def _tool_bar_add_file_button(self, name, desc, func, default_name, - shortcut=None): + def _tool_bar_add_file_button(self, name, desc, func, shortcut=None): pass @abstractmethod diff --git a/mne/viz/backends/_notebook.py b/mne/viz/backends/_notebook.py index 3d612ae14aa..b7c952d80f3 100644 --- a/mne/viz/backends/_notebook.py +++ b/mne/viz/backends/_notebook.py @@ -175,12 +175,10 @@ def _tool_bar_add_text(self, name, value, placeholder): def _tool_bar_add_spacer(self): pass - def _tool_bar_add_file_button(self, name, desc, func, default_name, - shortcut=None): + def _tool_bar_add_file_button(self, name, desc, func, shortcut=None): def callback(): fname = self.actions[f"{name}_field"].value - fname = default_name if len(fname) == 0 else fname - func(fname) + func(None if len(fname) == 0 else fname) self._tool_bar_add_text( name=f"{name}_field", value=None, diff --git a/mne/viz/backends/_qt.py b/mne/viz/backends/_qt.py index e10b54d9028..ac5e4ae4bb4 100644 --- a/mne/viz/backends/_qt.py +++ b/mne/viz/backends/_qt.py @@ -261,8 +261,7 @@ def _tool_bar_add_spacer(self): spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) self._tool_bar.addWidget(spacer) - def _tool_bar_add_file_button(self, name, desc, func, default_name, - shortcut=None): + def _tool_bar_add_file_button(self, name, desc, func, shortcut=None): def callback(): return FileDialog( self.plotter.app_window, From 6b6f5d647307c7fadd5505d823a622ca6531b077 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 26 Mar 2021 14:01:57 +0100 Subject: [PATCH 10/15] Fix layout --- mne/viz/backends/_notebook.py | 2 -- mne/viz/backends/_qt.py | 5 ++--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/mne/viz/backends/_notebook.py b/mne/viz/backends/_notebook.py index b7c952d80f3..85014870cb0 100644 --- a/mne/viz/backends/_notebook.py +++ b/mne/viz/backends/_notebook.py @@ -9,7 +9,6 @@ from ipywidgets import (Button, Dropdown, FloatSlider, FloatText, HBox, IntSlider, IntText, Text, VBox, IntProgress) -from ..utils import _generate_default_filename from ...fixes import nullcontext from ._abstract import (_AbstractDock, _AbstractToolBar, _AbstractMenuBar, _AbstractStatusBar, _AbstractLayout, _AbstractWidget, @@ -334,7 +333,6 @@ def _create_default_tool_bar(self): name="screenshot", desc="Take a screenshot", func=self.screenshot, - default_name=_generate_default_filename(".png"), ) def show(self): diff --git a/mne/viz/backends/_qt.py b/mne/viz/backends/_qt.py index ac5e4ae4bb4..2565b1ab9e9 100644 --- a/mne/viz/backends/_qt.py +++ b/mne/viz/backends/_qt.py @@ -40,7 +40,7 @@ def _layout_add_widget(self, layout, widget, stretch=0): if isinstance(widget, QLayout): layout.addLayout(widget) else: - layout.addWidget(widget) + layout.addWidget(widget, stretch) class _QtDock(_AbstractDock, _QtLayout): @@ -97,8 +97,7 @@ def _dock_named_layout(self, name, layout, compact): return layout def _dock_add_slider(self, name, value, rng, callback, - compact=True, double=False, layout=None, - stretch=0): + compact=True, double=False, layout=None): layout = self._dock_named_layout(name, layout, compact) slider_class = QFloatSlider if double else QSlider cast = float if double else int From 3fe5b88b110ddd26e31e23ae0ad0c701b5390704 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 26 Mar 2021 14:43:17 +0100 Subject: [PATCH 11/15] Nitpick --- mne/viz/backends/_qt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/viz/backends/_qt.py b/mne/viz/backends/_qt.py index 2565b1ab9e9..7009225dc87 100644 --- a/mne/viz/backends/_qt.py +++ b/mne/viz/backends/_qt.py @@ -320,7 +320,7 @@ def _status_bar_add_progress_bar(self, stretch=0): return _QtWidget(widget) def _status_bar_update(self): - self._status_bar.layout().update() + self._status_bar_layout.update() class _QtPlayback(_AbstractPlayback): From c7658fa5a24930db840f053a170832a6bab13e99 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Tue, 6 Apr 2021 14:02:15 +0200 Subject: [PATCH 12/15] Update based on reviews --- mne/viz/_brain/_brain.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index fd8fedbf260..8d0403b21db 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -28,7 +28,7 @@ UpdateLUT, UpdateColorbarScale) from ..utils import (_show_help_fig, _get_color_list, concatenate_images, - _generate_default_filename) + _generate_default_filename, _save_ndarray_img) from .._3d import _process_clim, _handle_time, _check_views from ...externals.decorator import decorator @@ -2582,9 +2582,8 @@ def save_image(self, filename=None, mode='rgb'): mode : str Either 'rgb' or 'rgba' for values to return. """ - default_name = _generate_default_filename(".png") - filename = default_name if filename is None else filename - from ..utils import _save_ndarray_img + if filename is None: + filename = _generate_default_filename(".png") _save_ndarray_img( filename, self.screenshot(mode=mode, time_viewer=True)) From 84296cdb468c46ae5613d73ef21c959e36ac72aa Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Tue, 6 Apr 2021 16:01:20 +0200 Subject: [PATCH 13/15] Use tempfile --- mne/viz/_brain/_brain.py | 4 ++-- mne/viz/_brain/tests/test.ipynb | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 8d0403b21db..cd4215a29a6 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -3089,8 +3089,8 @@ def save_movie(self, filename=None, time_dilation=4., tmin=None, tmax=None, dialog : object The opened dialog is returned for testing purpose only. """ - default_name = _generate_default_filename(".mp4") - filename = default_name if filename is None else filename + if filename is None: + filename = _generate_default_filename(".mp4") func = self._save_movie_tv if self.time_viewer else self._save_movie func(filename, time_dilation, tmin, tmax, framerate, interpolation, codec, diff --git a/mne/viz/_brain/tests/test.ipynb b/mne/viz/_brain/tests/test.ipynb index 66c4c8772de..1d4a771d7c7 100644 --- a/mne/viz/_brain/tests/test.ipynb +++ b/mne/viz/_brain/tests/test.ipynb @@ -33,6 +33,7 @@ "from ipywidgets import Button\n", "import matplotlib.pyplot as plt\n", "import mne\n", + "import tempfile\n", "from mne.datasets import testing\n", "data_path = testing.data_path()\n", "sample_dir = os.path.join(data_path, 'MEG', 'sample')\n", @@ -64,12 +65,19 @@ " assert brain._renderer.figure.display is not None\n", " brain._renderer._update()\n", " total_number_of_buttons = len([k for k in brain._renderer.actions.keys() if '_field' not in k])\n", + " tmp_path = tempfile.mkdtemp()\n", + " movie_path = os.path.join(tmp_path, 'test.mp4')\n", + " screenshot_path = os.path.join(tmp_path, 'test.png')\n", + " brain._renderer.actions['movie_field'].value = movie_path\n", + " brain._renderer.actions['screenshot_field'].value = screenshot_path\n", " number_of_buttons = 0\n", " for action in brain._renderer.actions.values():\n", " if isinstance(action, Button):\n", " action.click()\n", " number_of_buttons += 1\n", " assert number_of_buttons == total_number_of_buttons\n", + " assert os.path.isfile(movie_path)\n", + " assert os.path.isfile(screenshot_path)\n", " img_nv = brain.screenshot()\n", " assert img_nv.shape == (300, 300, 3), img_nv.shape\n", " img_v = brain.screenshot(time_viewer=True)\n", From 4344e3c88b8fc94db1da1c952e77184c3769cb70 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Tue, 6 Apr 2021 16:26:54 +0200 Subject: [PATCH 14/15] Fix --- mne/viz/_brain/tests/test.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/viz/_brain/tests/test.ipynb b/mne/viz/_brain/tests/test.ipynb index 1d4a771d7c7..62a6f7e8f06 100644 --- a/mne/viz/_brain/tests/test.ipynb +++ b/mne/viz/_brain/tests/test.ipynb @@ -66,7 +66,7 @@ " brain._renderer._update()\n", " total_number_of_buttons = len([k for k in brain._renderer.actions.keys() if '_field' not in k])\n", " tmp_path = tempfile.mkdtemp()\n", - " movie_path = os.path.join(tmp_path, 'test.mp4')\n", + " movie_path = os.path.join(tmp_path, 'test.gif')\n", " screenshot_path = os.path.join(tmp_path, 'test.png')\n", " brain._renderer.actions['movie_field'].value = movie_path\n", " brain._renderer.actions['screenshot_field'].value = screenshot_path\n", From d0f71e5cb48b7cb0bb88666b001d8bc0eb2d0e49 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 7 Apr 2021 10:53:42 +0200 Subject: [PATCH 15/15] Update testing --- mne/viz/_brain/tests/test_notebook.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mne/viz/_brain/tests/test_notebook.py b/mne/viz/_brain/tests/test_notebook.py index 8b67bf73667..a831522ea04 100644 --- a/mne/viz/_brain/tests/test_notebook.py +++ b/mne/viz/_brain/tests/test_notebook.py @@ -38,6 +38,7 @@ def test_notebook_alignment(renderer_notebook, brain_gc, nbexec): def test_notebook_interactive(renderer_notebook, brain_gc, nbexec): """Test interactive modes.""" import os + import tempfile from contextlib import contextmanager from numpy.testing import assert_allclose from ipywidgets import Button @@ -72,6 +73,11 @@ def interactive(on): assert brain._renderer.figure.notebook assert brain._renderer.figure.display is not None brain._renderer._update() + tmp_path = tempfile.mkdtemp() + movie_path = os.path.join(tmp_path, 'test.gif') + screenshot_path = os.path.join(tmp_path, 'test.png') + brain._renderer.actions['movie_field'].value = movie_path + brain._renderer.actions['screenshot_field'].value = screenshot_path total_number_of_buttons = sum( '_field' not in k for k in brain._renderer.actions.keys()) number_of_buttons = 0 @@ -80,6 +86,8 @@ def interactive(on): action.click() number_of_buttons += 1 assert number_of_buttons == total_number_of_buttons + assert os.path.isfile(movie_path) + assert os.path.isfile(screenshot_path) img_nv = brain.screenshot() assert img_nv.shape == (300, 300, 3), img_nv.shape img_v = brain.screenshot(time_viewer=True)