diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 24f2ec68819..cd4215a29a6 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, _save_ndarray_img) from .._3d import _process_clim, _handle_time, _check_views from ...externals.decorator import decorator @@ -704,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) @@ -1225,22 +1226,19 @@ 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, ) - 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, shortcut="ctrl+shift+s", ) self._renderer._tool_bar_add_button( @@ -2574,7 +2572,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 @@ -2584,7 +2582,10 @@ def save_image(self, filename, mode='rgb'): mode : str Either 'rgb' or 'rgba' for values to return. """ - self._renderer.screenshot(mode=mode, filename=filename) + if filename is None: + filename = _generate_default_filename(".png") + _save_ndarray_img( + filename, self.screenshot(mode=mode, time_viewer=True)) @fill_doc def screenshot(self, mode='rgb', time_viewer=False): @@ -2991,8 +2992,52 @@ 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, + 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). @@ -3044,77 +3089,12 @@ 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: - try: - from pyvista.plotting.qt_plotting import FileDialog - except ImportError: - from pyvistaqt.plotting import FileDialog - - if filename is None: - self.status_msg.setText("Choose movie path ...") - self.status_msg.show() - self.status_progress.setValue(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: - from PyQt5.QtCore import Qt - from PyQt5.QtGui import QCursor - - def frame_callback(frame, n_frames): - if frame == n_frames: - # On the ImageIO step - self.status_msg.setText( - "Saving with ImageIO: %s" - % filename - ) - self.status_msg.show() - self.status_progress.hide() - self.status_bar.layout().update() - else: - self.status_msg.setText( - "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.update() - self.status_progress.repaint() - self.status_msg.update() - self.status_msg.parent().update() - self.status_msg.repaint() - - # set cursor to busy - default_cursor = self._renderer._window_get_cursor() - self._renderer._window_set_cursor(QCursor(Qt.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) + 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, + bitrate, callback, time_viewer, **kwargs) def _make_movie_frames(self, time_dilation, tmin, tmax, framerate, interpolation, callback, time_viewer): 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) diff --git a/mne/viz/backends/_abstract.py b/mne/viz/backends/_abstract.py index d61b83f8c04..879e34786d9 100644 --- a/mne/viz/backends/_abstract.py +++ b/mne/viz/backends/_abstract.py @@ -473,7 +473,7 @@ 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, shortcut=None): pass @abstractmethod @@ -565,6 +565,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 @@ -578,7 +582,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 @@ -598,6 +602,22 @@ def set_value(self, value): def get_value(self): pass + @abstractmethod + def set_range(self, rng): + pass + + @abstractmethod + def show(self): + pass + + @abstractmethod + def hide(self): + pass + + @abstractmethod + def update(self, repaint=True): + pass + class _AbstractMplInterface(ABC): @abstractmethod @@ -768,6 +788,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 3c3a11d2f0d..85014870cb0 100644 --- a/mne/viz/backends/_notebook.py +++ b/mne/viz/backends/_notebook.py @@ -7,9 +7,8 @@ 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 _save_ndarray_img from ...fixes import nullcontext from ._abstract import (_AbstractDock, _AbstractToolBar, _AbstractMenuBar, _AbstractStatusBar, _AbstractLayout, _AbstractWidget, @@ -22,7 +21,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) @@ -38,19 +37,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 @@ -59,7 +58,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) @@ -71,7 +70,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( @@ -120,7 +119,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 @@ -142,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" @@ -150,7 +149,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, @@ -161,7 +160,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): @@ -169,30 +168,26 @@ 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): pass - def _tool_bar_add_screenshot_button(self, name, desc, func): - def _screenshot(): + def _tool_bar_add_file_button(self, name, desc, func, 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, - ) + func(None if len(fname) == 0 else 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_set_theme(self, theme): pass @@ -209,14 +204,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): + widget = IntProgress() + self._layout_add_widget(self._status_bar_layout, widget) + return _IpyWidget(widget) + + def _status_bar_update(self): pass @@ -278,6 +281,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 @@ -293,12 +299,26 @@ 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" + + 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) @@ -309,7 +329,7 @@ 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, @@ -317,20 +337,23 @@ def _create_default_tool_bar(self): 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/_pyvista.py b/mne/viz/backends/_pyvista.py index bc01a2da8e8..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_screenshot_filename(self): - now = datetime.now() - dt_string = now.strftime("_%Y-%m-%d_%H-%M-%S") - return "MNE" + dt_string + ".png" - def _index_to_loc(self, idx): _ncols = self.figure._ncols row = idx // _ncols diff --git a/mne/viz/backends/_qt.py b/mne/viz/backends/_qt.py index 77124df2e82..7009225dc87 100644 --- a/mne/viz/backends/_qt.py +++ b/mne/viz/backends/_qt.py @@ -6,12 +6,15 @@ # 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 +from PyQt5.QtGui import QIcon, QImage, QPixmap, QCursor from PyQt5.QtWidgets import (QComboBox, QDockWidget, QDoubleSpinBox, QGroupBox, QHBoxLayout, QLabel, QToolButton, QMenuBar, QSlider, QSpinBox, QVBoxLayout, QWidget, @@ -26,36 +29,36 @@ _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): 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: - layout.addWidget(widget) + layout.addWidget(widget, stretch) 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() @@ -65,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) @@ -74,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() @@ -84,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( @@ -94,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 @@ -136,7 +138,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) @@ -236,13 +238,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) @@ -255,24 +258,20 @@ 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_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, 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): @@ -291,33 +290,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): @@ -398,6 +401,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'] @@ -474,6 +480,20 @@ 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() + + 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): 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