1620 lines
50 KiB
Python
1620 lines
50 KiB
Python
"""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("<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=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
|