"""Notebook implementation of _Renderer and GUI.""" # Authors: The MNE-Python contributors. # License: BSD-3-Clause # Copyright the MNE-Python contributors. import os import os.path as op import re from contextlib import contextmanager, nullcontext from ipyevents import Event from IPython.display import clear_output, display from ipywidgets import ( HTML, Accordion, BoundedFloatText, Button, Checkbox, Dropdown, # non-object-based-abstraction-only widgets, deprecate FloatSlider, GridBox, HBox, IntProgress, IntSlider, IntText, Label, Layout, Play, RadioButtons, Select, Text, VBox, Widget, jsdlink, link, ) from ...utils import _soft_import, check_version from ._abstract import ( _AbstractAction, _AbstractAppWindow, _AbstractBrainMplCanvas, _AbstractButton, _AbstractCanvas, _AbstractCheckBox, _AbstractComboBox, _AbstractDialog, _AbstractDock, _AbstractFileButton, _AbstractGridLayout, _AbstractGroupBox, _AbstractHBoxLayout, _AbstractKeyPress, _AbstractLabel, _AbstractLayout, _AbstractMenuBar, _AbstractMplCanvas, _AbstractMplInterface, _AbstractPlayback, _AbstractPlayMenu, _AbstractPopup, _AbstractProgressBar, _AbstractRadioButtons, _AbstractSlider, _AbstractSpinBox, _AbstractStatusBar, _AbstractText, _AbstractToolBar, _AbstractVBoxLayout, _AbstractWdgt, _AbstractWidget, _AbstractWidgetList, _AbstractWindow, ) from ._pyvista import ( Plotter, _check_3d_figure, # noqa: F401 _close_3d_figure, # noqa: F401 _close_all, # noqa: F401 _PyVistaRenderer, _set_3d_title, # noqa: F401 _set_3d_view, # noqa: F401 _take_3d_screenshot, # noqa: F401 ) from ._utils import _notebook_vtk_works from .renderer import _TimeInteraction # dict values are icon names from: https://fontawesome.com/icons _ICON_LUT = dict( help="question", play="play", pause="pause", reset="history", scale="magic", clear="trash", movie="video-camera", restore="replay", screenshot="camera", visibility_on="eye", visibility_off="eye", folder="folder", question="question", information="info", warning="triangle-exclamation", critical="exclamation", ) _BASE_MIN_SIZE = "20px" _BASE_KWARGS = dict(layout=Layout(min_width=_BASE_MIN_SIZE, min_height=_BASE_MIN_SIZE)) # TODO: We can drop ipyvtklink once we support PyVista 0.38.1+ if check_version("pyvista", "0.38.1"): _JUPYTER_BACKEND = "trame" else: _JUPYTER_BACKEND = "ipyvtklink" # %% # Widgets # ------- # The metaclasses need to share a base class in order for the inheritance # not to conflict, http://www.phyast.pitt.edu/~micheles/python/metatype.html # https://stackoverflow.com/questions/28720217/multiple-inheritance-metaclass-conflict class _BaseWidget(type(_AbstractWidget), type(Widget)): pass class _Widget(Widget, _AbstractWidget, metaclass=_BaseWidget): tooltip = None def __init__(self): _AbstractWidget.__init__() # Widget cannot init because the layouts (HBox, VBox and GridBox) don't # inherit from Widget like they do analogously for Qt, this isn't an # issue since each subclass __init__s it's own (e.g. Label) # Widget.__init__(self) def _set_range(self, rng): self.min = rng[0] self.max = rng[1] def _show(self): self.layout.visibility = "visible" def _hide(self): self.layout.visibility = "hidden" def _set_enabled(self, state): self.disabled = not state def _is_enabled(self): return not self.disabled def _update(self, repaint=True): pass def _get_tooltip(self): return self.tooltip def _set_tooltip(self, tooltip): self.tooltip = tooltip def _set_style(self, style): for key, val in style.items(): setattr(self.layout, key, val) def _add_keypress(self, callback): self._event_watcher = Event(source=self, watched_events=["keydown"]) self._event_watcher.on_dom_event( lambda event: callback(event["key"].lower().replace("arrow", "")) ) self._callback = callback def _trigger_keypress(self, key): # note: this doesn't actually simulate a keypress, it just calls the # callback function directly because this is not yet possible self._callback(key) def _set_focus(self): if hasattr(self, "focus"): # added in ipywidgets 8.0 self.focus() def _set_layout(self, layout): self.children = (layout,) def _set_theme(self, theme): pass def _set_size(self, width=None, height=None): if width: self.layout.width = width if height: self.layout.height = height class _Label(_Widget, _AbstractLabel, Label, metaclass=_BaseWidget): def __init__(self, value, center=False, selectable=False): _Widget.__init__(self) _AbstractLabel.__init__(value, center=center, selectable=selectable) kwargs = _BASE_KWARGS.copy() if center: kwargs["layout"].justify_content = "center" Label.__init__(self, value=value, disabled=True, **kwargs) class _Text(_AbstractText, _Widget, Text, metaclass=_BaseWidget): def __init__(self, value=None, placeholder=None, callback=None): _AbstractText.__init__(value=value, placeholder=placeholder, callback=callback) _Widget.__init__(self) Text.__init__(self, value=value, placeholder=placeholder, **_BASE_KWARGS) if callback is not None: self.observe(lambda x: callback(x["new"]), names="value") def _set_value(self, value): self.value = value class _Button(_Widget, _AbstractButton, Button, metaclass=_BaseWidget): def __init__(self, value, callback, icon=None): _Widget.__init__(self) _AbstractButton.__init__(value=value, callback=callback) Button.__init__(self, description=value, **_BASE_KWARGS) self.on_click(lambda x: callback()) if icon: self.icon = _ICON_LUT[icon] def _click(self): self.click() def _set_icon(self, icon): self.icon = _ICON_LUT[icon] class _Slider(_Widget, _AbstractSlider, IntSlider, metaclass=_BaseWidget): def __init__(self, value, rng, callback, horizontal=True): _Widget.__init__(self) _AbstractSlider.__init__( value=value, rng=rng, callback=callback, horizontal=horizontal ) IntSlider.__init__( self, value=int(value), min=int(rng[0]), max=int(rng[1]), readout=False, orientation="horizontal" if horizontal else "vertical", **_BASE_KWARGS, ) self.observe(lambda x: callback(x["new"]), names="value") def _set_value(self, value): self.value = value def _get_value(self): return self.value def set_range(self, rng): self.min = int(rng[0]) self.max = int(rng[1]) class _ProgressBar(_AbstractProgressBar, _Widget, IntProgress, metaclass=_BaseWidget): def __init__(self, count): _AbstractProgressBar.__init__(count=count) _Widget.__init__(self) IntProgress.__init__(self, max=count, **_BASE_KWARGS) def _increment(self): if self.value + 1 > self.max: return self.value += 1 return self.value class _CheckBox(_Widget, _AbstractCheckBox, Checkbox, metaclass=_BaseWidget): def __init__(self, value, callback): _Widget.__init__(self) _AbstractCheckBox.__init__(value=value, callback=callback) Checkbox.__init__(self, value=value, **_BASE_KWARGS) self.observe(lambda x: callback(x["new"]), names="value") def _set_checked(self, checked): self.value = checked def _get_checked(self): return self.value class _SpinBox(_Widget, _AbstractSpinBox, IntText, metaclass=_BaseWidget): def __init__(self, value, rng, callback, step=None): _Widget.__init__(self) _AbstractSpinBox.__init__(value=value, rng=rng, callback=callback, step=step) IntText.__init__(self, value=value, min=rng[0], max=rng[1], **_BASE_KWARGS) if step is not None: self.step = step self.observe(lambda x: callback(x["new"]), names="value") def _set_value(self, value): self.value = value def _get_value(self): return self.value class _ComboBox(_AbstractComboBox, _Widget, Dropdown, metaclass=_BaseWidget): def __init__(self, value, items, callback): _AbstractComboBox.__init__(value=value, items=items, callback=callback) _Widget.__init__(self) Dropdown.__init__(self, value=value, options=items, **_BASE_KWARGS) self.observe(lambda x: callback(x["new"]), names="value") def _set_value(self, value): self.value = value def _get_value(self): return self.value class _RadioButtons( _AbstractRadioButtons, _Widget, RadioButtons, metaclass=_BaseWidget ): def __init__(self, value, items, callback): _AbstractRadioButtons.__init__(value=value, items=items, callback=callback) _Widget.__init__(self) RadioButtons.__init__( self, value=value, options=items, disabled=False, **_BASE_KWARGS ) self.observe(lambda x: callback(x["new"]), names="value") def _set_value(self, value): self.value = value def _get_value(self): return self.value class _GroupBox(_AbstractGroupBox, _Widget, Accordion, metaclass=_BaseWidget): def __init__(self, name, items): _AbstractGroupBox.__init__(name=name, items=items) _Widget.__init__(self) kwargs = _BASE_KWARGS.copy() kwargs["layout"].min_height = f"{100 * len(items)}px" self._layout = VBox(**kwargs) for item in items: self._layout.children = self._layout.children + (item,) Accordion.__init__(self, children=[self._layout]) self.set_title(0, name) self.selected_index = 0 def _set_enabled(self, value): super()._set_enabled(value) for child in self._layout.children: child._set_enabled(value) # modified from: # https://gist.github.com/elkhadiy/284900b3ea8a13ed7b777ab93a691719 class _FilePicker: def __init__(self, rows=20, directory_only=False, ignore_dotfiles=True): self._callback = None self._directory_only = directory_only self._ignore_dotfiles = ignore_dotfiles self._empty_selection = True self._selected_dir = os.getcwd() self._item_layout = Layout(width="auto") self._nb_rows = rows self._file_selector = Select( options=self._get_selector_options(), rows=min(len(os.listdir(self._selected_dir)), self._nb_rows), layout=self._item_layout, ) self._open_button = Button( description="Open", layout=Layout(flex="auto 1 auto", width="auto") ) self._select_button = Button( description="Select", layout=Layout(flex="auto 1 auto", width="auto") ) self._cancel_button = Button( description="Cancel", layout=Layout(flex="auto 1 auto", width="auto") ) self._parent_button = Button( icon="chevron-up", layout=Layout(flex="auto 1 auto", width="auto") ) self._selection = Text( value=op.join(self._selected_dir, self._file_selector.value), disabled=True, layout=Layout(flex="1 1 auto", width="auto"), ) self._filename = Text(value="", layout=Layout(flex="1 1 auto", width="auto")) self._parent_button.on_click(self._parent_button_clicked) self._open_button.on_click(self._open_button_clicked) self._select_button.on_click(self._select_button_clicked) self._cancel_button.on_click(self._cancel_button_clicked) self._file_selector.observe(self._update_path) self._widget = VBox( [ HBox( [ self._parent_button, HTML(value="Look in:"), self._selection, ] ), self._file_selector, HBox( [ HTML(value="File name"), self._filename, self._open_button, self._select_button, self._cancel_button, ] ), ] ) def _get_selector_options(self): options = os.listdir(self._selected_dir) if self._ignore_dotfiles: tmp = list() for el in options: if el[0] != ".": tmp.append(el) options = tmp if self._directory_only: tmp = list() for el in options: if op.isdir(op.join(self._selected_dir, el)): tmp.append(el) options = tmp if not options: options = [""] self._empty_selection = True else: self._empty_selection = False return options def _update_selector_options(self): self._file_selector.options = self._get_selector_options() self._file_selector.rows = min( len(os.listdir(self._selected_dir)), self._nb_rows ) self._selection.value = op.join(self._selected_dir, self._file_selector.value) self._filename.value = self._file_selector.value def show(self): self._update_selector_options() self._widget.layout.display = "block" display(self._widget) def hide(self): self._widget.layout.display = "none" def set_directory_only(self, state): self._directory_only = state def set_ignore_dotfiles(self, state): self._ignore_dotfiles = state def connect(self, callback): self._callback = callback def _open_button_clicked(self, button): if self._empty_selection: return if op.isdir(self._selection.value): self._selected_dir = self._selection.value self._file_selector.options = self._get_selector_options() self._file_selector.rows = min( len(os.listdir(self._selected_dir)), self._nb_rows ) def _select_button_clicked(self, button): if self._empty_selection: return result = op.join(self._selected_dir, self._filename.value) if self._callback is not None: self._callback(result) # the picker is shared so only one connection is allowed at a time self._callback = None # reset the callback self.hide() def _cancel_button_clicked(self, button): self._callback = None # reset the callback self.hide() def _parent_button_clicked(self, button): self._selected_dir, _ = op.split(self._selected_dir) self._update_selector_options() def _update_path(self, change): self._selection.value = op.join(self._selected_dir, self._file_selector.value) self._filename.value = self._file_selector.value class _FileButton(_AbstractFileButton, _Widget, Button, metaclass=_BaseWidget): def __init__( self, callback, content_filter=None, initial_directory=None, save=False, is_directory=False, icon="folder", window=None, ): _AbstractFileButton.__init__( callback=callback, content_filter=content_filter, initial_directory=initial_directory, save=save, is_directory=is_directory, ) _Widget.__init__(self) self._file_picker = _FilePicker() def fp_callback(x=None): # Note, in order to display the file picker where the button was, # the output must be cleared and then redrawn when finished if window is not None: clear_output() self._file_picker.set_directory_only(is_directory) def callback_with_show(name): window._show() callback(name) self._file_picker.connect( callback if window is None else callback_with_show ) self._file_picker.show() Button.__init__(self, **_BASE_KWARGS) self.on_click(fp_callback) self.icon = _ICON_LUT[icon] class _PlayMenu(_AbstractPlayMenu, _Widget, VBox, metaclass=_BaseWidget): def __init__(self, value, rng, callback): _AbstractPlayMenu.__init__(value=value, rng=rng, callback=callback) _Widget.__init__(self) kwargs = _BASE_KWARGS.copy() kwargs["layout"].align_items = "center" kwargs["layout"].min_height = "100px" VBox.__init__(self, **kwargs) self._slider = IntSlider( value=value, min=rng[0], max=rng[1], readout=False, continuous_update=False ) self._play_widget = Play(value=value, min=rng[0], max=rng[1], interval=250) self.children = (self._slider, self._play_widget) link((self._play_widget, "value"), (self._slider, "value")) self._slider.observe(lambda x: callback(x["new"]), names="value") # play, pause, reset and loop require ipywidgets v8.0+ and so are # not currently tested, will be added upon release def _play(self): self.playing = True def _pause(self): self.playing = True def _reset(self): self.playing = True self.value = self.min def _loop(self): self.repeat = not self.repeat def _set_value(self, value): self._slider.value = value class _Popup(_AbstractPopup, _Widget, VBox, metaclass=_BaseWidget): def __init__( self, title, text, info_text=None, callback=None, icon="warning", buttons=None, window=None, ): _AbstractPopup.__init__( self, title=title, text=text, info_text=info_text, callback=callback, icon=icon, buttons=buttons, window=window, ) _Widget.__init__(self) VBox.__init__(self, **_BASE_KWARGS) if window is not None: clear_output() title_label = _Label(title) title_label._set_style(dict(fontsize="28")) text_label = _Label(text) text_label._set_style(dict(fontsize="18")) self.children = (title_label, text_label) if info_text: info_text_label = _Label(info_text) info_text_label._set_style(dict(fontsize="12")) self.children += (info_text_label,) self.icon = _ICON_LUT[icon] if buttons is None: buttons = ["Ok"] hbox = HBox() self._buttons = dict() for button in buttons: def callback_with_show(x): if window is not None: clear_output() window._show() if callback: callback(button) button_widget = Button(description=button) self._buttons[button] = button_widget button_widget.on_click(callback_with_show) hbox.children += (button_widget,) self.children += (hbox,) display(self) def _click(self, value): self._buttons[value].click() class _BoxLayout: def _handle_scroll(self, scroll=None): kwargs = _BASE_KWARGS.copy() if scroll is not None: kwargs["layout"].width = f"{scroll[0]}px" kwargs["layout"].height = f"{scroll[1]}px" kwargs["overflow_x"] = "scroll" kwargs["overflow_y"] = "scroll" return kwargs def _add_widget(self, widget): # if pyvista plotter, needs to be shown if isinstance(widget, Plotter): widget = widget.show(jupyter_backend=_JUPYTER_BACKEND, return_viewer=True) if hasattr(widget, "layout"): widget.layout.width = None # unlock the fixed layout widget.layout.margin = "2px 0px 2px 0px" if not isinstance(widget, Play): widget.layout.min_width = "0px" self.children += (widget,) class _HBoxLayout( _AbstractHBoxLayout, _BoxLayout, _Widget, HBox, metaclass=_BaseWidget ): def __init__(self, height=None, scroll=None): _Widget.__init__(self) _BoxLayout.__init__(self) _AbstractHBoxLayout.__init__(self, height=height, scroll=scroll) HBox.__init__(self, **self._handle_scroll(scroll=scroll)) self._height = height def _add_widget(self, widget): _BoxLayout._add_widget(self, widget) if self._height is not None: for child in self.children: child.layout.height = f"{int(self._height / len(self.children))}px" def _add_stretch(self, amount=1): self.children += ( self, _Label(" " * 4), ) class _VBoxLayout( _AbstractVBoxLayout, _BoxLayout, _Widget, VBox, metaclass=_BaseWidget ): def __init__(self, width=None, scroll=None): _Widget.__init__(self) _BoxLayout.__init__(self) _AbstractVBoxLayout.__init__(self, width=width, scroll=scroll) VBox.__init__(self, **self._handle_scroll(scroll=scroll)) self._width = width def _add_widget(self, widget): _BoxLayout._add_widget(self, widget) if self._width is not None: for child in self.children: child.layout.width = f"{int(self._width / len(self.children))}px" def _add_stretch(self, amount=1): self.children += ( self, _Label(" " * 4), ) class _GridLayout(_AbstractGridLayout, _Widget, GridBox, metaclass=_BaseWidget): def __init__(self, height=None, width=None): _Widget.__init__(self) _AbstractVBoxLayout.__init__(height=height, width=width) GridBox.__init__(self, **_BASE_KWARGS) def _add_widget(self, widget, row=None, col=None): _BoxLayout._add_widget(self, widget) class _Canvas(_AbstractCanvas, _Widget, HBox, metaclass=_BaseWidget): def __init__(self, width, height, dpi): import matplotlib.pyplot as plt _Widget.__init__(self) _AbstractCanvas.__init__(self, width=width, height=height, dpi=dpi) HBox.__init__(self, **_BASE_KWARGS) with plt.ioff(): self.fig, self.ax = plt.subplots(dpi=dpi) self.children = (self.fig.canvas,) def _set_size(self, width=None, height=None): if width: self.layout.width = width if height: self.layout.height = height class _AppWindow(_AbstractAppWindow, _Widget, VBox, metaclass=_BaseWidget): def __init__(self, size=None, fullscreen=False): _AbstractAppWindow.__init__(self) _Widget.__init__(self) VBox.__init__(self, **_BASE_KWARGS) def _set_central_layout(self, central_layout): self.children = (central_layout,) def _close_connect(self, func, *, after=True): pass def _close_disconnect(self, after=True): pass def _clean(self): pass def _get_dpi(self): return 96 def _get_size(self): # CSS objects don't have explicit widths and heights # https://github.com/jupyter-widgets/ipywidgets/issues/1639 return (256, 256) def _get_cursor(self): pass def _set_cursor(self, cursor): pass def _new_cursor(self, name): pass def _show(self, block=False): display(self) def _close(self): clear_output() class _3DRenderer(_PyVistaRenderer): _kind = "notebook" def __init__(self, *args, **kwargs): kwargs["notebook"] = True super().__init__(*args, **kwargs) if "show" in kwargs and kwargs["show"]: self.show() def _update(self): if _JUPYTER_BACKEND == "ipyvtklink": if self.figure.display is not None: self.figure.display.update_canvas() else: super()._update() @contextmanager def _ensure_minimum_sizes(self): yield def show(self): viewer = self.plotter.show(jupyter_backend=_JUPYTER_BACKEND, return_viewer=True) viewer.layout.width = None # unlock the fixed layout display(viewer) # ------------------------------------ # Non-object-based Widget Abstractions # ------------------------------------ # These are planned to be deprecated in favor of the simpler, object- # oriented abstractions above when time allows. # modified from: # https://gist.github.com/elkhadiy/284900b3ea8a13ed7b777ab93a691719 class _FilePckr: def __init__(self, rows=20, directory_only=False, ignore_dotfiles=True): self._callback = None self._directory_only = directory_only self._ignore_dotfiles = ignore_dotfiles self._empty_selection = True self._selected_dir = os.getcwd() self._item_layout = Layout(width="auto") self._nb_rows = rows self._file_selector = Select( options=self._get_selector_options(), rows=min(len(os.listdir(self._selected_dir)), self._nb_rows), layout=self._item_layout, ) self._open_button = Button( description="Open", layout=Layout(flex="auto 1 auto", width="auto") ) self._select_button = Button( description="Select", layout=Layout(flex="auto 1 auto", width="auto") ) self._cancel_button = Button( description="Cancel", layout=Layout(flex="auto 1 auto", width="auto") ) self._parent_button = Button( icon="chevron-up", layout=Layout(flex="auto 1 auto", width="auto") ) self._selection = Text( value=os.path.join(self._selected_dir, self._file_selector.value), disabled=True, layout=Layout(flex="1 1 auto", width="auto"), ) self._filename = Text(value="", layout=Layout(flex="1 1 auto", width="auto")) self._parent_button.on_click(self._parent_button_clicked) self._open_button.on_click(self._open_button_clicked) self._select_button.on_click(self._select_button_clicked) self._cancel_button.on_click(self._cancel_button_clicked) self._file_selector.observe(self._update_path) self._widget = VBox( [ HBox( [ self._parent_button, HTML(value="Look in:"), self._selection, ] ), self._file_selector, HBox( [ HTML(value="File name"), self._filename, self._open_button, self._select_button, self._cancel_button, ] ), ] ) def _get_selector_options(self): options = os.listdir(self._selected_dir) if self._ignore_dotfiles: tmp = list() for el in options: if el[0] != ".": tmp.append(el) options = tmp if self._directory_only: tmp = list() for el in options: if os.path.isdir(os.path.join(self._selected_dir, el)): tmp.append(el) options = tmp if not options: options = [""] self._empty_selection = True else: self._empty_selection = False return options def _update_selector_options(self): self._file_selector.options = self._get_selector_options() self._file_selector.rows = min( len(os.listdir(self._selected_dir)), self._nb_rows ) self._selection.value = os.path.join( self._selected_dir, self._file_selector.value ) self._filename.value = self._file_selector.value def show(self): self._update_selector_options() self._widget.layout.display = "block" def hide(self): self._widget.layout.display = "none" def set_directory_only(self, state): self._directory_only = state def set_ignore_dotfiles(self, state): self._ignore_dotfiles = state def connect(self, callback): self._callback = callback def _open_button_clicked(self, button): if self._empty_selection: return if os.path.isdir(self._selection.value): self._selected_dir = self._selection.value self._file_selector.options = self._get_selector_options() self._file_selector.rows = min( len(os.listdir(self._selected_dir)), self._nb_rows ) def _select_button_clicked(self, button): if self._empty_selection: return result = os.path.join(self._selected_dir, self._filename.value) if self._callback is not None: self._callback(result) # the picker is shared so only one connection is allowed at a time self._callback = None # reset the callback self.hide() def _cancel_button_clicked(self, button): self._callback = None # reset the callback self.hide() def _parent_button_clicked(self, button): self._selected_dir, _ = os.path.split(self._selected_dir) self._update_selector_options() def _update_path(self, change): self._selection.value = os.path.join( self._selected_dir, self._file_selector.value ) self._filename.value = self._file_selector.value class _IpyKeyPress(_AbstractKeyPress): def _keypress_initialize(self, widget=None): pass def _keypress_add(self, shortcut, callback): pass def _keypress_trigger(self, shortcut): pass class _IpyDialog(_AbstractDialog): def _dialog_create( self, title, text, info_text, callback, *, icon="Warning", buttons=(), modal=True, window=None, ): pass class _IpyLayout(_AbstractLayout): def _layout_initialize(self, max_width): self._layout_max_width = max_width def _layout_add_widget(self, layout, widget, stretch=0, *, row=None, col=None): widget.layout.margin = "2px 0px 2px 0px" if not isinstance(widget, Play): widget.layout.min_width = "0px" if isinstance(layout, Accordion): box = layout.children[0] else: box = layout children = list(box.children) children.append(widget) box.children = tuple(children) # Fix columns if self._layout_max_width is not None and isinstance(widget, HBox): children = widget.children if len(children) > 0: width = int(self._layout_max_width / len(children)) for child in children: child.layout.width = f"{width}px" def _layout_create(self, orientation="vertical"): if orientation == "vertical": layout = VBox() elif orientation == "horizontal": layout = HBox() else: assert orientation == "grid" layout = GridBox() return layout class _IpyDock(_AbstractDock, _IpyLayout): def _dock_initialize( self, window=None, name="Controls", area="left", max_width=None ): if self._docks is None: self._docks = dict() current_dock = VBox() self._dock_width = 302 self._dock = self._dock_layout = current_dock self._dock.layout.width = f"{self._dock_width}px" self._layout_initialize(self._dock_width) self._docks[area] = (self._dock, self._dock_layout) def _dock_finalize(self): pass def _dock_show(self): self._dock_layout.layout.visibility = "visible" def _dock_hide(self): self._dock_layout.layout.visibility = "hidden" def _dock_add_stretch(self, layout=None): layout = self._dock_layout if layout is None else layout widget = HTML(value="", disabled=True) widget.layout.width = "100%" self._layout_add_widget(layout, widget) return _IpyWidget(widget) def _dock_add_layout(self, vertical=True): return VBox() if vertical else HBox() def _dock_add_label(self, value, *, align=False, layout=None, selectable=False): layout = self._dock_layout if layout is None else layout widget = HTML(value=value, disabled=True) widget.layout.width = "100px" self._layout_add_widget(layout, widget) return _IpyWidget(widget) def _dock_add_button( self, name, callback, *, style="pushbutton", icon=None, tooltip=None, layout=None, ): layout = self._dock_layout if layout is None else layout kwargs = dict() if style == "pushbutton": kwargs["description"] = name if tooltip is not None: kwargs["tooltip"] = tooltip widget = Button(**kwargs) widget.on_click(lambda x: callback()) if icon is not None: widget.icon = icon self._layout_add_widget(layout, widget) return _IpyWidget(widget) def _dock_named_layout(self, name, *, layout=None, compact=True): layout = self._dock_layout if layout is None else layout if name is not None: hlayout = self._dock_add_layout(not compact) self._dock_add_label(value=name, align=not compact, layout=hlayout) self._layout_add_widget(layout, hlayout) layout = hlayout return layout def _dock_add_slider( self, name, value, rng, callback, *, compact=True, double=False, tooltip=None, layout=None, ): layout = self._dock_named_layout(name=name, layout=layout, compact=compact) klass = FloatSlider if double else IntSlider widget = klass( value=value, min=rng[0], max=rng[1], readout=False, ) widget.observe(_generate_callback(callback), names="value") self._layout_add_widget(layout, widget) return _IpyWidget(widget) def _dock_add_check_box(self, name, value, callback, *, tooltip=None, layout=None): layout = self._dock_layout if layout is None else layout widget = Checkbox(value=value, description=name, indent=False, disabled=False) hbox = HBox([widget]) # fix stretching to the right widget.observe(_generate_callback(callback), names="value") self._layout_add_widget(layout, hbox) return _IpyWidget(widget) def _dock_add_spin_box( self, name, value, rng, callback, *, compact=True, double=True, step=None, tooltip=None, layout=None, ): layout = self._dock_named_layout(name=name, layout=layout, compact=compact) klass = BoundedFloatText if double else IntText widget = klass( value=value, min=rng[0], max=rng[1], ) if step is not None: widget.step = step widget.observe(_generate_callback(callback), names="value") self._layout_add_widget(layout, widget) return _IpyWidget(widget) def _dock_add_combo_box( self, name, value, rng, callback, *, compact=True, tooltip=None, layout=None ): layout = self._dock_named_layout(name=name, layout=layout, compact=compact) widget = Dropdown( value=value, options=rng, ) widget.observe(_generate_callback(callback), names="value") self._layout_add_widget(layout, widget) return _IpyWidget(widget) def _dock_add_radio_buttons( self, value, rng, callback, *, vertical=True, layout=None ): # XXX: vertical=False is not supported yet layout = self._dock_layout if layout is None else layout widget = RadioButtons( options=rng, value=value, disabled=False, ) widget.observe(_generate_callback(callback), names="value") self._layout_add_widget(layout, widget) return _IpyWidgetList(widget) def _dock_add_group_box(self, name, *, collapse=None, layout=None): layout = self._dock_layout if layout is None else layout if collapse is None: hlayout = VBox([HTML("" + name + "")]) else: assert isinstance(collapse, bool) vbox = VBox() hlayout = Accordion([vbox]) hlayout.set_title(0, name) if collapse: hlayout.selected_index = None else: hlayout.selected_index = 0 self._layout_add_widget(layout, hlayout) return hlayout def _dock_add_text(self, name, value, placeholder, *, callback=None, layout=None): layout = self._dock_layout if layout is None else layout widget = Text(value=value, placeholder=placeholder) if callback is not None: widget.observe(_generate_callback(callback), names="value") self._layout_add_widget(layout, widget) return _IpyWidget(widget) def _dock_add_file_button( self, name, desc, func, *, filter_=None, initial_directory=None, save=False, is_directory=False, icon=False, tooltip=None, layout=None, ): layout = self._dock_layout if layout is None else layout def callback(): self._file_picker.set_directory_only(is_directory) self._file_picker.connect(func) self._file_picker.show() if icon: kwargs = dict(style="toolbutton", icon="folder") else: kwargs = dict() widget = self._dock_add_button( name=desc, callback=callback, tooltip=tooltip, layout=layout, **kwargs ) return widget def _generate_callback(callback, to_float=False): def func(data): value = data["new"] if "new" in data else data["old"] callback(float(value) if to_float else value) return func class _IpyToolBar(_AbstractToolBar, _IpyLayout): def _tool_bar_initialize(self, name="default", window=None): self.actions = dict() self._tool_bar = self._tool_bar_layout = HBox() self._layout_initialize(None) 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] if icon is None: return widget = Button(tooltip=desc, icon=icon) widget.layout.width = "50px" widget.on_click(lambda x: func()) self._layout_add_widget(self._tool_bar_layout, widget) self.actions[name] = _IpyAction(widget) def _tool_bar_update_button_icon(self, name, icon_name): self.actions[name].set_icon(self._icons[icon_name]) def _tool_bar_add_text(self, name, value, placeholder): widget = Text(value=value, placeholder=placeholder) self._layout_add_widget(self._tool_bar_layout, widget) self.actions[name] = _IpyAction(widget) def _tool_bar_add_spacer(self): pass def _tool_bar_add_file_button(self, name, desc, func, *, shortcut=None): def callback(name=name): fname = self.actions[f"{name}_field"]._action.value 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_add_play_button(self, name, desc, func, *, shortcut=None): widget = Play(interval=500) self._layout_add_widget(self._tool_bar_layout, widget) self.actions[name] = _IpyAction(widget) return _IpyWidget(widget) class _IpyMenuBar(_AbstractMenuBar): def _menu_initialize(self, window=None): self._menus = dict() self._menu_actions = dict() self._menu_desc2button = dict() # only for notebook self._menu_bar = self._menu_bar_layout = HBox() self._layout_initialize(None) def _menu_add_submenu(self, name, desc): widget = Dropdown(value=desc, options=[desc]) self._menus[name] = widget self._menu_actions[name] = dict() def callback(input_desc): if input_desc == desc: return button_name = self._menu_desc2button[input_desc] if button_name in self._menu_actions[name]: self._menu_actions[name][button_name].trigger() widget.value = desc widget.observe(_generate_callback(callback), names="value") self._layout_add_widget(self._menu_bar_layout, widget) def _menu_add_button(self, menu_name, name, desc, func): menu = self._menus[menu_name] options = list(menu.options) options.append(desc) menu.options = options self._menu_actions[menu_name][name] = _IpyAction(func) # associate the description with the name given by the user self._menu_desc2button[desc] = name class _IpyStatusBar(_AbstractStatusBar, _IpyLayout): def _status_bar_initialize(self, window=None): self._status_bar = HBox() self._layout_initialize(None) def _status_bar_add_label(self, value, *, stretch=0): widget = Text(value=value, disabled=True) self._layout_add_widget(self._status_bar, widget) return _IpyWidget(widget) def _status_bar_add_progress_bar(self, stretch=0): widget = IntProgress() self._layout_add_widget(self._status_bar, widget) return _IpyWidget(widget) def _status_bar_update(self): pass class _IpyPlayback(_AbstractPlayback): def _playback_initialize(self, func, timeout, value, rng, time_widget, play_widget): play = play_widget._widget play.min = rng[0] play.max = rng[1] play.value = round(value) slider = time_widget._widget jsdlink((play, "value"), (slider, "value")) class _IpyMplInterface(_AbstractMplInterface): def _mpl_initialize(self): ipympl = _soft_import("ipympl", "Drawing figures into a notebook.", strict=True) self.canvas = ipympl.backend_nbagg.Canvas(self.fig) self.manager = ipympl.backend_nbagg.FigureManager(self.canvas, 0) 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): def _window_initialize(self, *, window=None, central_layout=None, fullscreen=False): super()._window_initialize() self._window_load_icons() def _window_load_icons(self): # from: https://fontawesome.com/icons for key in ( "help", "reset", "scale", "clear", "movie", "restore", "screenshot", "visibility_on", "visibility_off", "folder", ): # noqa: E501 self._icons[key] = _ICON_LUT[key] self._icons["play"] = None self._icons["pause"] = None def _window_close_connect(self, func, *, after=True): pass def _window_close_disconnect(self, after=True): pass def _window_get_dpi(self): return 96 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 = _IpyBrainMplCanvas(brain, w, h, self._window_get_dpi()) return self._mplcanvas def _window_adjust_mplcanvas_layout(self): pass def _window_get_cursor(self): pass def _window_set_cursor(self, cursor): pass def _window_new_cursor(self, name): pass @contextmanager def _window_ensure_minimum_sizes(self): yield def _window_set_theme(self, theme): pass def _window_create(self): pass # XXX: this could be a VBox if _Renderer.show is refactored class _IpyWidgetList(_AbstractWidgetList): def __init__(self, src): self._src = src if isinstance(self._src, RadioButtons): self._widgets = _IpyWidget(self._src) else: self._widgets = list() for widget in self._src: if not isinstance(widget, _IpyWidget): widget = _IpyWidget(widget) self._widgets.append(widget) def set_enabled(self, state): if isinstance(self._src, RadioButtons): self._widgets.set_enabled(state) else: for widget in self._widgets: widget.set_enabled(state) def get_value(self, idx): if isinstance(self._src, RadioButtons): # for consistency, we do not use get_value() return self._widgets._widget.options[idx] else: return self._widgets[idx].get_value() def set_value(self, idx, value): if isinstance(self._src, RadioButtons): self._widgets.set_value(value) else: self._widgets[idx].set_value(value) class _IpyWidget(_AbstractWdgt): def set_value(self, value): if isinstance(self._widget, Button): self._widget.click() else: self._widget.value = 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 set_enabled(self, state): self._widget.disabled = not state def is_enabled(self): return not self._widget.disabled def update(self, repaint=True): pass def get_tooltip(self): assert hasattr(self._widget, "tooltip") return self._widget.tooltip def set_tooltip(self, tooltip): assert hasattr(self._widget, "tooltip") self._widget.tooltip = tooltip def set_style(self, style): for key, val in style.items(): setattr(self._widget.layout, key, val) class _IpyAction(_AbstractAction): def trigger(self): if callable(self._action): self._action() else: # standard Button widget self._action.click() def set_icon(self, icon): self._action.icon = icon def set_shortcut(self, shortcut): pass class _Renderer( _PyVistaRenderer, _IpyDock, _IpyToolBar, _IpyMenuBar, _IpyStatusBar, _IpyWindow, _IpyPlayback, _IpyDialog, _IpyKeyPress, _TimeInteraction, ): _kind = "notebook" def __init__(self, *args, **kwargs): self._docks = None self._menu_bar = None self._tool_bar = None self._status_bar = None self._file_picker = _FilePckr(rows=10) kwargs["notebook"] = True fullscreen = kwargs.pop("fullscreen", False) if not _notebook_vtk_works(): raise RuntimeError( "Using the notebook backend on Linux requires a compatible " "VTK setup. Consider using Xfvb or xvfb-run to set up a " "working virtual display, or install VTK with OSMesa enabled." ) super().__init__(*args, **kwargs) self._window_initialize(fullscreen=fullscreen) def _update(self): if _JUPYTER_BACKEND == "ipyvtklink": if self.figure.display is not None: self.figure.display.update_canvas() else: super()._update() def _display_default_tool_bar(self): self._tool_bar_initialize() self._tool_bar_add_file_button( name="screenshot", desc="Take a screenshot", func=self.screenshot, ) display(self._tool_bar) def show(self): # menu bar if self._menu_bar is not None: display(self._menu_bar) # tool bar if self._tool_bar is not None: display(self._tool_bar) else: self._display_default_tool_bar() # viewer viewer = self.plotter.show(jupyter_backend=_JUPYTER_BACKEND, return_viewer=True) if _JUPYTER_BACKEND == "trame": # Remove scrollbars, see https://github.com/pyvista/pyvista/pull/4847 # which adds this to the iframe PyVista creates. Once that's merged, this # workaround just becomes a redundant but is still safe. And in a worst # (realistic) case, this regex will fail to do any substitution and we just # live with the ugly 90's-style borders. We can probably remove once we # require PyVista 0.43 (assuming the above PR is merged). viewer.value = re.sub( r" style=[\"'](.+)[\"']>", # value taken from matplotlib's widget r" style='\1; border: 1px solid rgb(221,221,221);' scrolling='no'>", # noqa: E501 viewer.value, ) rendering_row = list() if self._docks is not None and "left" in self._docks: rendering_row.append(self._docks["left"][0]) rendering_row.append(viewer) if self._docks is not None and "right" in self._docks: rendering_row.append(self._docks["right"][0]) display(HBox(rendering_row)) self.figure.display = viewer # status bar if self._status_bar is not None: display(self._status_bar) # file picker self._file_picker.hide() display(self._file_picker._widget) return self.scene() _testing_context = nullcontext