diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 6f0702da76f..ab9d06ca701 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -27,7 +27,7 @@ from .callback import (ShowView, TimeCallBack, SmartCallBack, UpdateLUT, UpdateColorbarScale) -from ..utils import _show_help, _get_color_list, concatenate_images +from ..utils import _show_help_fig, _get_color_list, concatenate_images from .._3d import _process_clim, _handle_time, _check_views from ...externals.decorator import decorator @@ -604,6 +604,7 @@ def setup_time_viewer(self, time_viewer=True, show_traces=True): self.color_list.remove("#7f7f7f") self.color_cycle = _ReuseCycle(self.color_list) self.mpl_canvas = None + self.help_canvas = None self.rms = None self.picked_patches = {key: list() for key in all_keys} self.picked_points = {key: list() for key in all_keys} @@ -652,6 +653,7 @@ def setup_time_viewer(self, time_viewer=True, show_traces=True): self._configure_menu() self._configure_status_bar() self._configure_playback() + self._configure_help() # show everything at the end self.toggle_interface() self._renderer._window_show(self._size) @@ -1641,8 +1643,7 @@ def plot_time_line(self): self.time_line.set_xdata(current_time) self.mpl_canvas.update_plot() - def help(self): - """Display the help window.""" + def _configure_help(self): pairs = [ ('?', 'Display help window'), ('i', 'Toggle interface'), @@ -1660,13 +1661,20 @@ def help(self): text1, text2 = zip(*pairs) text1 = '\n'.join(text1) text2 = '\n'.join(text2) - _show_help( + self.help_canvas = self._renderer._window_get_simple_canvas( + width=5, height=2, dpi=80) + _show_help_fig( col1=text1, col2=text2, - width=5, - height=2, + fig_help=self.help_canvas.fig, + ax=self.help_canvas.axes, + show=False, ) + def help(self): + """Display the help window.""" + self.help_canvas.show() + def _clear_callbacks(self): if not hasattr(self, 'callbacks'): return diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index f4bad075ede..850558bfbae 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -31,7 +31,6 @@ from matplotlib import cm, image from matplotlib.lines import Line2D -import matplotlib.pyplot as plt data_path = testing.data_path(download=False) subject_id = 'sample' @@ -506,11 +505,11 @@ def test_brain_time_viewer(renderer_interactive_pyvista, pixel_ratio, brain.apply_auto_scaling() brain.restore_user_scaling() brain.reset() - plt.close('all') + + assert brain.help_canvas is not None + assert not brain.help_canvas.canvas.isVisible() brain.help() - assert len(plt.get_fignums()) == 1 - plt.close('all') - assert len(plt.get_fignums()) == 0 + assert brain.help_canvas.canvas.isVisible() # screenshot # Need to turn the interface back on otherwise the window is too wide diff --git a/mne/viz/backends/_abstract.py b/mne/viz/backends/_abstract.py index c148fff895c..f8046fffa91 100644 --- a/mne/viz/backends/_abstract.py +++ b/mne/viz/backends/_abstract.py @@ -595,8 +595,14 @@ def get_value(self): pass +class _AbstractMplInterface(ABC): + @abstractmethod + def _mpl_initialize(): + pass + + class _AbstractMplCanvas(ABC): - def __init__(self, brain, width, height, dpi): + def __init__(self, width, height, dpi): """Initialize the MplCanvas.""" from matplotlib import rc_context from matplotlib.figure import Figure @@ -612,8 +618,7 @@ def __init__(self, brain, width, height, dpi): self.fig = Figure(figsize=(width, height), dpi=dpi) self.axes = self.fig.add_subplot(111) self.axes.set(xlabel='Time (sec)', ylabel='Activation (AU)') - self.brain = brain - self.time_func = brain.callbacks["time"] + self.manager = None def _connect(self): for event in ('button_press', 'motion_notify') + self._extra_events: @@ -635,12 +640,6 @@ def plot_time_line(self, x, label, **kwargs): def update_plot(self): """Update the plot.""" - leg = self.axes.legend( - prop={'family': 'monospace', 'size': 'small'}, - framealpha=0.5, handlelength=1., - facecolor=self.brain._bg_color) - for text in leg.get_texts(): - text.set_color(self.brain._fg_color) with warnings.catch_warnings(record=True): warnings.filterwarnings('ignore', 'constrained_layout') self.canvas.draw() @@ -658,15 +657,47 @@ def set_color(self, bg_color, fg_color): self.axes.tick_params(axis='y', colors=fg_color) self.fig.patch.set_facecolor(bg_color) - @abstractmethod def show(self): """Show the canvas.""" - pass + if self.manager is None: + self.canvas.show() + else: + self.manager.show() def close(self): """Close the canvas.""" self.canvas.close() + def clear(self): + """Clear internal variables.""" + self.close() + self.axes.clear() + self.fig.clear() + self.canvas = None + self.manager = None + + def on_resize(self, event): + """Handle resize events.""" + tight_layout(fig=self.axes.figure) + + +class _AbstractBrainMplCanvas(_AbstractMplCanvas): + def __init__(self, brain, width, height, dpi): + """Initialize the MplCanvas.""" + super().__init__(width, height, dpi) + self.brain = brain + self.time_func = brain.callbacks["time"] + + def update_plot(self): + """Update the plot.""" + leg = self.axes.legend( + prop={'family': 'monospace', 'size': 'small'}, + framealpha=0.5, handlelength=1., + facecolor=self.brain._bg_color) + for text in leg.get_texts(): + text.set_color(self.brain._fg_color) + super().update_plot() + def on_button_press(self, event): """Handle button presses.""" # left click (and maybe drag) in progress in axes @@ -676,20 +707,12 @@ def on_button_press(self, event): self.time_func( event.xdata, update_widget=True, time_as_index=False) + on_motion_notify = on_button_press # for now they can be the same + def clear(self): """Clear internal variables.""" - self.close() - self.axes.clear() - self.fig.clear() + super().clear() self.brain = None - self.canvas = None - self.manager = None - - on_motion_notify = on_button_press # for now they can be the same - - def on_resize(self, event): - """Handle resize events.""" - tight_layout(fig=self.axes.figure) class _AbstractWindow(ABC): @@ -712,6 +735,10 @@ def _window_get_mplcanvas_size(self, fraction): h /= ratio return (w / dpi, h / dpi) + @abstractmethod + def _window_get_simple_canvas(self, width, height, dpi): + pass + @abstractmethod def _window_get_mplcanvas(self, brain, interactor_fraction, show_traces, separate_canvas): diff --git a/mne/viz/backends/_notebook.py b/mne/viz/backends/_notebook.py index 4c4e3e045d3..44b2604e8d6 100644 --- a/mne/viz/backends/_notebook.py +++ b/mne/viz/backends/_notebook.py @@ -13,7 +13,8 @@ from ...fixes import nullcontext from ._abstract import (_AbstractDock, _AbstractToolBar, _AbstractMenuBar, _AbstractStatusBar, _AbstractLayout, _AbstractWidget, - _AbstractWindow, _AbstractMplCanvas, _AbstractPlayback) + _AbstractWindow, _AbstractMplCanvas, _AbstractPlayback, + _AbstractBrainMplCanvas, _AbstractMplInterface) from ._pyvista import _PyVistaRenderer, _close_all, _set_3d_view, _set_3d_title # noqa: F401,E501, analysis:ignore @@ -135,7 +136,7 @@ def func(data): class _IpyToolBar(_AbstractToolBar, _IpyLayout): def _tool_bar_load_icons(self): self.icons = dict() - self.icons["help"] = None + self.icons["help"] = "question" self.icons["play"] = None self.icons["pause"] = None self.icons["reset"] = "history" @@ -221,18 +222,25 @@ def _playback_initialize(self, func, timeout): pass -class _IpyMplCanvas(_AbstractMplCanvas): - def __init__(self, brain, width, height, dpi): - super().__init__(brain, width, height, dpi) +class _IpyMplInterface(_AbstractMplInterface): + def _mpl_initialize(self): from matplotlib.backends.backend_nbagg import (FigureCanvasNbAgg, FigureManager) self.canvas = FigureCanvasNbAgg(self.fig) self.manager = FigureManager(self.canvas, 0) - self._connect() - def show(self): - """Show the canvas.""" - self.manager.show() + +class _IpyMplCanvas(_AbstractMplCanvas, _IpyMplInterface): + def __init__(self, width, height, dpi): + super().__init__(width, height, dpi) + self._mpl_initialize() + + +class _IpyBrainMplCanvas(_AbstractBrainMplCanvas, _IpyMplInterface): + def __init__(self, brain, width, height, dpi): + super().__init__(brain, width, height, dpi) + self._mpl_initialize() + self._connect() class _IpyWindow(_AbstractWindow): @@ -251,13 +259,17 @@ def _window_get_dpi(self): def _window_get_size(self): return self.figure.plotter.window_size + def _window_get_simple_canvas(self, width, height, dpi): + return _IpyMplCanvas(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 = _IpyMplCanvas(brain, w, h, self._window_get_dpi()) + self._mplcanvas = _IpyBrainMplCanvas( + brain, w, h, self._window_get_dpi()) return self._mplcanvas def _window_adjust_mplcanvas_layout(self): diff --git a/mne/viz/backends/_qt.py b/mne/viz/backends/_qt.py index 465aee3619b..6405a6d2bc7 100644 --- a/mne/viz/backends/_qt.py +++ b/mne/viz/backends/_qt.py @@ -23,7 +23,8 @@ _set_3d_view, _set_3d_title, _take_3d_screenshot) # noqa: F401,E501 analysis:ignore from ._abstract import (_AbstractDock, _AbstractToolBar, _AbstractMenuBar, _AbstractStatusBar, _AbstractLayout, _AbstractWidget, - _AbstractWindow, _AbstractMplCanvas, _AbstractPlayback) + _AbstractWindow, _AbstractMplCanvas, _AbstractPlayback, + _AbstractBrainMplCanvas, _AbstractMplInterface) from ._utils import _init_qt_resources, _qt_disable_paint from ..utils import _save_ndarray_img @@ -321,27 +322,34 @@ def _playback_initialize(self, func, timeout): self.figure.plotter.add_callback(func, timeout) -class _QtMplCanvas(_AbstractMplCanvas): - def __init__(self, brain, width, height, dpi): - super().__init__(brain, width, height, dpi) +class _QtMplInterface(_AbstractMplInterface): + def _mpl_initialize(self): from PyQt5 import QtWidgets from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg self.canvas = FigureCanvasQTAgg(self.fig) - if brain.separate_canvas: - self.canvas.setParent(None) - else: - self.canvas.setParent(brain._renderer._window) FigureCanvasQTAgg.setSizePolicy( self.canvas, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding ) FigureCanvasQTAgg.updateGeometry(self.canvas) - self.manager = None - self._connect() - def show(self): - self.canvas.show() + +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): @@ -364,13 +372,17 @@ def _window_get_size(self): 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 = _QtMplCanvas(brain, w, h, self._window_get_dpi()) + self._mplcanvas = _QtBrainMplCanvas( + brain, w, h, self._window_get_dpi()) return self._mplcanvas def _window_adjust_mplcanvas_layout(self): diff --git a/mne/viz/utils.py b/mne/viz/utils.py index 7a24961f5c7..f5bb640e6dc 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -555,11 +555,8 @@ def figure_nobar(*args, **kwargs): return fig -def _show_help(col1, col2, width, height): - fig_help = figure_nobar(figsize=(width, height), dpi=80) +def _show_help_fig(col1, col2, fig_help, ax, show): _set_window_title(fig_help, 'Help') - - ax = fig_help.add_subplot(111) celltext = [[c1, c2] for c1, c2 in zip(col1.strip().split("\n"), col2.strip().split("\n"))] table = ax.table(cellText=celltext, loc="center", cellLoc="left") @@ -575,12 +572,19 @@ def _show_help(col1, col2, width, height): fig_help.canvas.mpl_connect('key_press_event', _key_press) - # this should work for non-test cases - try: - fig_help.canvas.draw() - plt_show(fig=fig_help, warn=False) - except Exception: - pass + if show: + # this should work for non-test cases + try: + fig_help.canvas.draw() + plt_show(fig=fig_help, warn=False) + except Exception: + pass + + +def _show_help(col1, col2, width, height): + fig_help = figure_nobar(figsize=(width, height), dpi=80) + ax = fig_help.add_subplot(111) + _show_help_fig(col1, col2, fig_help, ax, show=True) def _key_press(event):