Source code for nglview.player

# TODO: reorg
# simplify code
import json
import time
import uuid
from collections import defaultdict

from IPython.display import Javascript, display
from ipywidgets import (Box, Button, Checkbox, ColorPicker, Dropdown,
                        FloatSlider, FloatText, HBox, IntSlider, IntText,
                        Label, Layout, Play, Tab, Text, Textarea, ToggleButton,
                        VBox, interactive, jslink)
from traitlets import (Any, Bool, CaselessStrEnum, Dict, Float, HasTraits, Int,
                       link, observe)
import traitlets

from . import default
from .layout import (_make_autofit, _make_box_layout, _make_delay_tab,
                     _relayout, _relayout_master, make_form_item_layout)
from .parameters import REPRESENTATION_NAMES
from .utils import js_utils


def _dry_run(v, func, *args, **kwargs):
    # Get remote call message.
    msg = []

    def _another_call(*args, **kwargs):
        msg.append(v._get_remote_call_msg(*args, **kwargs))

    old = v._remote_call
    setattr(v, '_remote_call', _another_call)
    func(*args, **kwargs)
    setattr(v, '_remote_call', old)

    return msg and msg[-1] or None  # FIXME: all messages?


def _cature_msg(f):
    def wrap(*args, **kwargs):
        self = args[0]
        v = self._view
        msg_dict = v._player_dict
        w = f(*args, **kwargs)
        # FIXME: add code here
        # if isinstance(w, Button):
        #     msg_dict[w.model_id] = _dry_run(v, w.click)
        return w
    return wrap


[docs]class TrajectoryPlayer(HasTraits): # should set default values here different from desired defaults # so `observe` can be triggered step = Int(0) interpolate = Bool(False) delay = Float(0.0) parameters = Dict() iparams = Dict() _interpolation_t = Float() _iterpolation_type = CaselessStrEnum(['linear', 'spline']) camera = CaselessStrEnum(['perspective', 'orthographic'], default_value='perspective') _render_params = Dict() _real_time_update = Bool(False) widget_tab = Any(None) widget_repr = Any(None) widget_repr_parameters = Any(None) widget_quick_repr = Any(None) widget_general = Any(None) widget_picked = Any(None) widget_preference = Any(None) widget_extra = Any(None) widget_help = Any(None) widget_export_image = Any(None) widget_component_slider = Any(None) widget_repr_slider = Any(None) widget_repr_choices = Any(None) widget_repr_control_buttons = Any(None) widget_repr_add = Any(None) widget_accordion_repr_parameters = Any(None) widget_repr_parameters_dialog = Any(None) widget_repr_name = Any(None) widget_component_dropdown = Any(None) widget_player = Any(None) widget_background = Any(None) widget_camera = Any(None) btn_center = Any(None) def __init__(self, view, step=1, delay=100, min_delay=40): self._view = view self.step = step self.delay = delay self.min_delay = min_delay self._iplayer = None self._interpolation_t = 0.5 self._iterpolation_type = 'linear' self.iparams = dict(t=self._interpolation_t, step=1, type=self._iterpolation_type) self._render_params = dict(factor=4, antialias=True, trim=False, transparent=False) self._widget_names = [w for w in dir(self) if w.startswith('wiget_')] def _update_padding(self, padding=default.DEFAULT_PADDING): widget_collection = [ self.widget_general, self.widget_repr, self.widget_preference, self.widget_repr_parameters, self.widget_help, self.widget_extra, self.widget_picked ] for widget in widget_collection: if widget is not None: widget.layout.padding = padding def _create_all_widgets(self, thread=False): def make(): self.widget_tab = self._display() old_index = self.widget_tab.selected_index for index, _ in enumerate(self.widget_tab.children): self.widget_tab.selected_index = index if thread: time.sleep(0.1) self.widget_tab.selected_index = old_index if thread: self._view._run_on_another_thread(make) else: make()
[docs] def smooth(self): self.interpolate = True
@observe('delay') def _on_delay(self, change): if self.widget_player: self.widget_player.interval = change['new'] @observe('camera') def on_camera_changed(self, change): camera_type = change['new'] self._view._remote_call("setParameters", target='Stage', kwargs=dict(cameraType=camera_type)) @property def frame(self): return self._view.frame @frame.setter def frame(self, value): self._view.frame = value @property def max_frame(self): return self._view.max_frame @observe('parameters') def update_parameters(self, change): params = change['new'] self.delay = params.get("delay", self.delay) self.step = params.get("step", self.step) @observe('_interpolation_t') def _interpolation_t_changed(self, change): self.iparams['t'] = change['new'] def _display(self): if self.widget_tab is None: box_factory = [(self._make_general_box, 'General'), (self._make_widget_repr, 'Representation'), (self._make_widget_preference, 'Preference'), (self._make_extra_box, 'Extra')] tab = _make_delay_tab(box_factory, selected_index=0) # tab = _make_autofit(tab) tab.layout.align_self = 'center' tab.layout.align_items = 'stretch' self.widget_tab = self._view._igui = tab return self.widget_tab def _make_widget_tab(self): return self._display() @_cature_msg def _make_button_center(self): self.btn_center = Button(description=' Center', icon='fa-bullseye') @self.btn_center.on_click def on_click(_): self._view.center() return self.btn_center def _make_widget_preference(self, width='100%'): def make_func(): parameters = self._view._ngl_full_stage_parameters def func(pan_speed=parameters.get('panSpeed', 0.8), rotate_speed=parameters.get('rotateSpeed', 2), zoom_speed=parameters.get('zoomSpeed', 1.2), clip_dist=parameters.get('clipDist', 10), camera_fov=parameters.get('cameraFov', 40), clip_far=parameters.get('clipFar', 100), clip_near=parameters.get('clipNear', 0), fog_far=parameters.get('fogFar', 100), fog_near=parameters.get('fogNear', 50), impostor=parameters.get('impostor', True), light_intensity=parameters.get('lightIntensity', 1), quality=parameters.get('quality', 'medium'), sample_level=parameters.get('sampleLevel', 1)): self._view.parameters = dict(panSpeed=pan_speed, rotateSpeed=rotate_speed, zoomSpeed=zoom_speed, clipDist=clip_dist, clipFar=clip_far, clipNear=clip_near, cameraFov=camera_fov, fogFar=fog_far, fogNear=fog_near, impostor=impostor, lightIntensity=light_intensity, quality=quality, sampleLevel=sample_level) return func def make_widget_box(): widget_sliders = interactive(make_func(), pan_speed=(0, 10, 0.1), rotate_speed=(0, 10, 1), zoom_speed=(0, 10, 1), clip_dist=(0, 200, 5), clip_far=(0, 100, 1), clip_near=(0, 100, 1), camera_fov=(15, 120, 1), fog_far=(0, 100, 1), fog_near=(0, 100, 1), light_intensity=(0, 10, 0.02), quality=['low', 'medium', 'high'], sample_level=(-1, 5, 1)) for child in widget_sliders.children: if isinstance(child, (IntSlider, FloatSlider)): child.layout.width = default.DEFAULT_SLIDER_WIDTH return widget_sliders if self.widget_preference is None: widget_sliders = make_widget_box() reset_button = Button(description='Reset') widget_sliders.children = [ reset_button, ] + list(widget_sliders.children) @reset_button.on_click def on_click(reset_button): self._view.parameters = self._view._original_stage_parameters self._view._ngl_full_stage_parameters = self._view._original_stage_parameters widget_sliders.children = [ reset_button, ] + list(make_widget_box().children) self.widget_preference = _relayout_master(widget_sliders, width=width) return self.widget_preference def _show_download_image(self): # "interactive" does not work for True/False in ipywidgets 4 yet. button = Button(description=' Screenshot', icon='fa-camera') @button.on_click def on_click(button): self._view.download_image() return button def _make_text_picked(self): ta = Textarea(value=json.dumps(self._view.picked), description='Picked atom') ta.layout.width = '300px' _dry_run(self._view, self._view._on_picked, change={ 'old': '', 'new': '' }) return ta def _refresh(self, component_slider, repr_slider): """update representation and component information """ self._view._request_repr_parameters(component=component_slider.value, repr_index=repr_slider.value) self._view._update_repr_dict() self._view._handle_repr_dict_changed(change=dict( new=self._view._ngl_repr_dict)) @_cature_msg def _make_button_repr_control(self, component_slider, repr_slider, repr_selection): button_refresh = Button(description=' Refresh', tooltip='Get representation info', icon='fa-refresh') button_center_selection = Button(description=' Center', tooltip='center selected atoms', icon='fa-bullseye') button_center_selection._ngl_name = 'button_center_selection' button_hide = Button(description=' Hide', icon='fa-eye-slash', tooltip='Hide/Show current representation') button_remove = Button(description=' Remove', icon='fa-trash', tooltip='Remove current representation') @button_refresh.on_click def on_click_refresh(button): self._refresh(component_slider, repr_slider) @button_center_selection.on_click def on_click_center(center_selection): self._view.center(selection=repr_selection.value, component=component_slider.value) @button_hide.on_click def on_click_hide(button_hide): component = component_slider.value repr_index = repr_slider.value if button_hide.description == 'Hide': hide = True button_hide.description = 'Show' else: hide = False button_hide.description = 'Hide' self._view._remote_call('setVisibilityForRepr', target='Widget', args=[component, repr_index, not hide]) @button_remove.on_click def on_click_remove(button_remove): self._view._remove_representation(component=component_slider.value, repr_index=repr_slider.value) self._view._request_repr_parameters( component=component_slider.value, repr_index=repr_slider.value) bbox = _make_autofit( HBox([ button_refresh, button_center_selection, button_hide, button_remove ])) for b in bbox.children: if hasattr(b, 'click'): _dry_run(self._view, b.click) return bbox def _make_widget_repr(self): self.widget_repr_name = Text(value='', description='representation') self.widget_repr_name._ngl_name = 'repr_name_text' repr_selection = Text(value=' ', description='selection') repr_selection._ngl_name = 'repr_selection' repr_selection.width = self.widget_repr_name.width = default.DEFAULT_TEXT_WIDTH max_n_components = max(self._view.n_components - 1, 0) self.widget_component_slider = IntSlider(value=0, max=max_n_components, min=0, description='component') self.widget_component_slider._ngl_name = 'component_slider' cvalue = ' ' self.widget_component_dropdown = Dropdown(value=cvalue, options=[ cvalue, ], description='component') self.widget_component_dropdown._ngl_name = 'component_dropdown' self.widget_repr_slider = IntSlider(value=0, description='representation', width=default.DEFAULT_SLIDER_WIDTH) self.widget_repr_slider._ngl_name = 'repr_slider' self.widget_repr_slider.visible = True self.widget_component_slider.layout.width = default.DEFAULT_SLIDER_WIDTH self.widget_repr_slider.layout.width = default.DEFAULT_SLIDER_WIDTH self.widget_component_dropdown.layout.width = self.widget_component_dropdown.max_width = default.DEFAULT_TEXT_WIDTH # turn off for now self.widget_component_dropdown.layout.display = 'none' self.widget_component_dropdown.description = '' # self.widget_accordion_repr_parameters = Accordion() self.widget_accordion_repr_parameters = Tab() self.widget_repr_parameters = self._make_widget_repr_parameters( self.widget_component_slider, self.widget_repr_slider, self.widget_repr_name) self.widget_accordion_repr_parameters.children = [ self.widget_repr_parameters, Box() ] self.widget_accordion_repr_parameters.set_title(0, 'Parameters') self.widget_accordion_repr_parameters.set_title(1, 'Hide') self.widget_accordion_repr_parameters.selected_index = 1 checkbox_reprlist = Checkbox(value=False, description='reprlist') checkbox_reprlist._ngl_name = 'checkbox_reprlist' self.widget_repr_choices = self._make_repr_name_choices( self.widget_component_slider, self.widget_repr_slider) self.widget_repr_choices._ngl_name = 'reprlist_choices' self.widget_repr_add = self._make_add_widget_repr( self.widget_component_slider) def on_update_checkbox_reprlist(change): self.widget_repr_choices.visible = change['new'] checkbox_reprlist.observe(on_update_checkbox_reprlist, names='value') def on_repr_name_text_value_changed(change): name = change['new'].strip() old = change['old'].strip() should_update = (self._real_time_update and old and name and name in REPRESENTATION_NAMES and name != change['old'].strip()) if should_update: component = self.widget_component_slider.value repr_index = self.widget_repr_slider.value self._view._remote_call( 'setRepresentation', target='Widget', args=[change['new'], {}, component, repr_index]) self._view._request_repr_parameters(component, repr_index) def on_component_or_repr_slider_value_changed(change): self._view._request_repr_parameters( component=self.widget_component_slider.value, repr_index=self.widget_repr_slider.value) self.widget_component_dropdown.options = tuple( self._view._ngl_component_names) if self.widget_accordion_repr_parameters.selected_index >= 0: self.widget_repr_parameters.name = self.widget_repr_name.value self.widget_repr_parameters.repr_index = self.widget_repr_slider.value self.widget_repr_parameters.component_index = self.widget_component_slider.value def on_repr_selection_value_changed(change): if self._real_time_update: component = self.widget_component_slider.value repr_index = self.widget_repr_slider.value self._view._set_selection(change['new'], component=component, repr_index=repr_index) def on_change_component_dropdown(change): choice = change['new'] if choice and choice.strip(): # bool(" ") is True self.widget_component_slider.value = self._view._ngl_component_names.index( choice) self.widget_component_dropdown.observe(on_change_component_dropdown, names='value') self.widget_repr_slider.observe( on_component_or_repr_slider_value_changed, names='value') self.widget_component_slider.observe( on_component_or_repr_slider_value_changed, names='value') self.widget_repr_name.observe(on_repr_name_text_value_changed, names='value') repr_selection.observe(on_repr_selection_value_changed, names='value') self.widget_repr_control_buttons = self._make_button_repr_control( self.widget_component_slider, self.widget_repr_slider, repr_selection) blank_box = Box([Label("")]) all_kids = [ self.widget_repr_control_buttons, blank_box, self.widget_repr_add, self.widget_component_dropdown, self.widget_repr_name, repr_selection, self.widget_component_slider, self.widget_repr_slider, self.widget_repr_choices, self.widget_accordion_repr_parameters ] vbox = VBox(all_kids) self._view._request_repr_parameters( component=self.widget_component_slider.value, repr_index=self.widget_repr_slider.value) self.widget_repr = _relayout_master(vbox, width='100%') self._refresh(self.widget_component_slider, self.widget_repr_slider) setattr(self.widget_repr, "_saved_widgets", []) for _box in self.widget_repr.children: if hasattr(_box, 'children'): for kid in _box.children: self.widget_repr._saved_widgets.append(kid) return self.widget_repr def _make_widget_repr_parameters(self, component_slider, repr_slider, repr_name_text=None): name = repr_name_text.value if repr_name_text is not None else ' ' widget = self._view._display_repr(component=component_slider.value, repr_index=repr_slider.value, name=name) widget._ngl_name = 'repr_parameters_box' return widget def _make_button_export_image(self): slider_factor = IntSlider(value=4, min=1, max=10, description='scale') checkbox_antialias = Checkbox(value=True, description='antialias') checkbox_trim = Checkbox(value=False, description='trim') checkbox_transparent = Checkbox(value=False, description='transparent') filename_text = Text(value='Screenshot', description='Filename') delay_text = FloatText(value=1, description='delay (s)', tooltip='hello') start_text, stop_text, step_text = (IntText(value=0, description='start'), IntText(value=self._view.max_frame+1, description='stop'), IntText(value=1, description='step')) start_text.layout.max_width = stop_text.layout.max_width = step_text.layout.max_width \ = filename_text.layout.max_width = delay_text.layout.max_width = default.DEFAULT_TEXT_WIDTH button_movie_images = Button(description='Export Images') def download_image(filename): self._view.download_image(factor=slider_factor.value, antialias=checkbox_antialias.value, trim=checkbox_trim.value, transparent=checkbox_transparent.value, filename=filename) @button_movie_images.on_click def on_click_images(button_movie_images): for i in range(start_text.value, stop_text.value, step_text.value): self._view.frame = i time.sleep(delay_text.value) download_image(filename=filename_text.value + str(i)) time.sleep(delay_text.value) vbox = VBox([ button_movie_images, start_text, stop_text, step_text, delay_text, filename_text, slider_factor, checkbox_antialias, checkbox_trim, checkbox_transparent, ]) form_items = _relayout(vbox, make_form_item_layout()) form = Box(form_items, layout=_make_box_layout()) # form = _relayout_master(vbox) return form def _make_resize_notebook_slider(self): resize_notebook_slider = IntSlider(min=300, max=2000, description='resize notebook') def on_resize_notebook(change): width = change['new'] self._view._remote_call('resizeNotebook', target='Widget', args=[ width, ]) resize_notebook_slider.observe(on_resize_notebook, names='value') return resize_notebook_slider def _make_add_widget_repr(self, component_slider): dropdown_repr_name = Dropdown(options=REPRESENTATION_NAMES, value='cartoon') repr_selection = Text(value='*', description='') repr_button = Button(description='Add', tooltip="""Add representation. You can also hit Enter in selection box""") repr_button.layout = Layout(width='auto', flex='1 1 auto') dropdown_repr_name.layout.width = repr_selection.layout.width = default.DEFAULT_TEXT_WIDTH def on_click_or_submit(button_or_text_area): self._view.add_representation( selection=repr_selection.value.strip(), repr_type=dropdown_repr_name.value, component=component_slider.value) repr_button.on_click(on_click_or_submit) repr_selection.on_submit(on_click_or_submit) add_repr_box = HBox([repr_button, dropdown_repr_name, repr_selection]) add_repr_box._ngl_name = 'add_repr_box' return add_repr_box def _make_repr_playground(self): vbox = VBox() children = [] rep_names = REPRESENTATION_NAMES[:] excluded_names = ['ball+stick', 'distance'] for name in excluded_names: rep_names.remove(name) repr_selection = Text(value='*') repr_selection.layout.width = default.DEFAULT_TEXT_WIDTH repr_selection_box = HBox([Label('selection'), repr_selection]) setattr(repr_selection_box, 'value', repr_selection.value) for index, name in enumerate(rep_names): button = ToggleButton(description=name) def make_func(): def on_toggle_button_value_change(change, button=button): selection = repr_selection.value new = change['new'] # True/False if new: self._view.add_representation(button.description, selection=selection) else: self._view._remove_representations_by_name( button.description) return on_toggle_button_value_change button.observe(make_func(), names='value') children.append(button) pd = self._view._player_dict pd['widget_quick_repr'] = defaultdict(dict) for k in children: pd['widget_quick_repr'][k.model_id] = {} def click_true(k): k.value = True pd['widget_quick_repr'][k.model_id][True] = _dry_run(self._view, click_true, k=k) def click_false(k): k.value = False pd['widget_quick_repr'][k.model_id][False] = _dry_run(self._view, click_false, k=k) button_clear = Button(description='clear', button_style='info', icon='fa-eraser') @button_clear.on_click def on_clear(button_clear): self._view.clear() for kid in children: # unselect kid.value = False if hasattr(button_clear, 'click'): _dry_run(self._view, button_clear.click) vbox.children = children + [repr_selection, button_clear] _make_autofit(vbox) self.widget_quick_repr = vbox return self.widget_quick_repr def _make_repr_name_choices(self, component_slider, repr_slider): repr_choices = Dropdown(options=[ " ", ]) def on_chosen(change): repr_name = change.get('new', " ") try: repr_index = repr_choices.options.index(repr_name) repr_slider.value = repr_index except ValueError: pass repr_choices.observe(on_chosen, names='value') repr_choices.layout.width = default.DEFAULT_TEXT_WIDTH self.widget_repr_choices = repr_choices return self.widget_repr_choices def _make_widget_picked(self): self.widget_picked = self._make_text_picked() picked_box = HBox([ self.widget_picked, ]) return _relayout_master(picked_box, width='75%') def _make_export_image_widget(self): if self.widget_export_image is None: self.widget_export_image = HBox([self._make_button_export_image()]) return self.widget_export_image def _make_extra_box(self): if self.widget_extra is None: extra_list = [(self._make_widget_picked, 'Picked'), (self._make_repr_playground, 'Quick'), (self._make_export_image_widget, 'Image'), (self._make_command_box, 'Command')] self.widget_extra = _make_delay_tab(extra_list, selected_index=0) return self.widget_extra def _make_general_box(self): if self.widget_general is None: step_slide = IntSlider(value=self.step, min=-100, max=100, description='step') delay_text = IntSlider(value=self.delay, min=10, max=1000, description='delay') toggle_button_interpolate = ToggleButton( self.interpolate, description='Smoothing', tooltip='smoothing trajectory') link((toggle_button_interpolate, 'value'), (self, 'interpolate')) self.widget_background = background_color_picker = ColorPicker( value=self._view._ngl_full_stage_parameters.get( 'backgroundColor', 'white'), description='background') self.widget_camera = camera_type = Dropdown( value=self.camera, options=['perspective', 'orthographic'], description='camera') link((step_slide, 'value'), (self, 'step')) link((delay_text, 'value'), (self, 'delay')) link((toggle_button_interpolate, 'value'), (self, 'interpolate')) link((camera_type, 'value'), (self, 'camera')) link((background_color_picker, 'value'), (self._view, 'background')) center_button = self._make_button_center() render_button = self._show_download_image() center_render_hbox = _make_autofit( HBox([ toggle_button_interpolate, center_button, render_button, ])) v0_left = VBox([ step_slide, delay_text, background_color_picker, camera_type, center_render_hbox, ]) v0_left = _relayout_master(v0_left, width='100%') self.widget_general = v0_left return self.widget_general def _ipython_display_(self, **kwargs): display(self._display()) def _make_command_box(self): widget_text_command = Text() @widget_text_command.on_submit def _on_submit_command(_): command = widget_text_command.value js_utils.execute(command) widget_text_command.value = '' return widget_text_command def _create_all_tabs(self): tab = self._display() for index, _ in enumerate(tab.children): # trigger ceating widgets tab.selected_index = index self.widget_extra = self._make_extra_box() for index, _ in enumerate(self.widget_extra.children): self.widget_extra.selected_index = index def _simplify_repr_control(self): for widget in self.widget_repr._saved_widgets: if not isinstance(widget, Tab): widget.layout.display = 'none' self.widget_repr_choices.layout.display = 'flex' self.widget_accordion_repr_parameters.selected_index = 0