"""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, remove
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 removed 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("<strong>" + name + "</strong>")])
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=str(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=[\"'](.+)[\"']></iframe>",
# value taken from matplotlib's widget
r" style='\1; border: 1px solid rgb(221,221,221);' scrolling='no'></iframe>", # 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