Skip to content

Commit

Permalink
Add as_popup (#101)
Browse files Browse the repository at this point in the history
Co-authored-by: Simon Høxbro Hansen <[email protected]>
  • Loading branch information
ahuang11 and hoxbro authored May 7, 2024
1 parent 1df707a commit d0def9d
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 20 deletions.
44 changes: 44 additions & 0 deletions examples/tutorial/04_Make_an_App.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,50 @@
"tools = PanelWidgets(annotator, field_values=fields_values)\n",
"pn.Row(tools, annotator_element).servable()"
]
},
{
"cell_type": "markdown",
"id": "b393d681",
"metadata": {},
"source": [
"## Make it pop!\n",
"\n",
"Rather than laying out the widgets on the side, the widgets can also be shown as a popup.\n",
"\n",
"Now, when an annotation is created, the widgets will popup next to the annotation, and closed when `x` or `✔️` is clicked.\n",
"\n",
"The widgets can also be displayed when double clicking anywhere on the plot."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "323fd44c",
"metadata": {},
"outputs": [],
"source": [
"PanelWidgets(annotator, field_values=fields_values, as_popup=True)\n",
"annotator_element"
]
},
{
"cell_type": "markdown",
"id": "4515e3b1",
"metadata": {},
"source": [
"It's also possible to use as a popup and display the widgets on the side."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "07a1853c",
"metadata": {},
"outputs": [],
"source": [
"tools = PanelWidgets(annotator, field_values=fields_values, as_popup=True)\n",
"pn.Row(tools, annotator_element).servable()"
]
}
],
"metadata": {
Expand Down
8 changes: 4 additions & 4 deletions holonote/annotate/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,16 +432,16 @@ def tap_selector(x, y) -> None: # Tap tool must be enabled on the element
else:
self.annotator.select_by_index()

tap_stream = hv.streams.Tap(source=element, transient=True)
tap_stream.add_subscriber(tap_selector)
self._tap_stream = hv.streams.Tap(source=element, transient=True)
self._tap_stream.add_subscriber(tap_selector)
return element

def register_double_tap_clear(self, element: hv.Element) -> hv.Element:
def double_tap_clear(x, y):
self.clear_indicated_region()

double_tap_stream = hv.streams.DoubleTap(source=element, transient=True)
double_tap_stream.add_subscriber(double_tap_clear)
self._double_tap_stream = hv.streams.DoubleTap(source=element, transient=True)
self._double_tap_stream.add_subscriber(double_tap_clear)
return element

def indicators(self) -> hv.DynamicMap:
Expand Down
115 changes: 99 additions & 16 deletions holonote/app/panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@


class PanelWidgets(Viewer):
reset_on_apply = param.Boolean(default=True, doc="Reset fields widgets on apply")

mapping = {
str: pn.widgets.TextInput,
bool: pn.widgets.Checkbox,
Expand All @@ -27,7 +29,15 @@ class PanelWidgets(Viewer):
float: pn.widgets.FloatSlider,
}

def __init__(self, annotator: Annotator, field_values: dict[str, Any] | None = None):
def __init__(
self,
annotator: Annotator,
field_values: dict[str, Any] | None = None,
as_popup: bool = False,
**params,
):
super().__init__(**params)
self._layouts = {}
self.annotator = annotator
self.annotator.snapshot()
self._widget_mode_group = pn.widgets.RadioButtonGroup(
Expand All @@ -48,6 +58,27 @@ def __init__(self, annotator: Annotator, field_values: dict[str, Any] | None = N

self._set_standard_callbacks()

self._layout = pn.Column(self.fields_widgets, self.tool_widgets)
if self.visible_widget is not None:
self._layout.insert(0, self.visible_widget)

self._as_popup = as_popup
if self._as_popup:
self._layout.visible = False
displays = self.annotator._displays
if not displays:
kdims = list(self.annotator.spec.keys())
display = self.annotator.get_display(*kdims)
display.indicators()
for display in displays.values():
if display.region_format in ("range", "range-range"):
stream = display._edit_streams[0]
elif display.region_format in ("point", "point-point"):
stream = display._edit_streams[1]
self._register_stream_popup(stream)
self._register_tap_popup(display)
self._register_double_tap_clear(display)

def _create_visible_widget(self):
if self.annotator.groupby is None:
self.visible_widget = None
Expand Down Expand Up @@ -167,6 +198,8 @@ def _reset_fields_widgets(self):
for widget_name, default in self._fields_values.items():
if isinstance(default, param.Parameter):
default = default.default
if isinstance(default, list):
default = default[0]
with contextlib.suppress(Exception):
# TODO: Fix when lists (for categories, not the same as the default!)
self._fields_widgets[widget_name].value = default
Expand All @@ -183,14 +216,68 @@ def _callback_apply(self, event):
fields_values = {k: v.value for k, v in self._fields_widgets.items()}
if self._widget_mode_group.value == "+":
self.annotator.add_annotation(**fields_values)
self._reset_fields_widgets()
if self.reset_on_apply:
self._reset_fields_widgets()
elif (self._widget_mode_group.value == "✏") and (selected_ind is not None):
self.annotator.update_annotation_fields(
selected_ind, **fields_values
) # TODO: Handle only changed
elif self._widget_mode_group.value == "-" and selected_ind is not None:
self.annotator.delete_annotation(selected_ind)

def _get_layout(self, name):
def close_layout(event):
layout.visible = False

layout = self._layouts.get(name)
if not layout:
layout = self._layout.clone(visible=False)
self._widget_apply_button.on_click(close_layout)
self._layouts[name] = layout
return layout

def _hide_layouts(self):
for layout in self._layouts.values():
layout.visible = False

def _register_stream_popup(self, stream):
def _popup(*args, **kwargs):
layout = self._get_layout(stream.name)
with param.parameterized.batch_call_watchers(self):
self._hide_layouts()
self._widget_mode_group.value = "+"
layout.visible = True
return layout

stream.popup = _popup

def _register_tap_popup(self, display):
def tap_popup(x, y) -> None: # Tap tool must be enabled on the element
layout = self._get_layout("tap")
if self.annotator.selection_enabled:
with param.parameterized.batch_call_watchers(self):
self._hide_layouts()
layout.visible = True
return layout

display._tap_stream.popup = tap_popup

def _register_double_tap_clear(self, display):
def double_tap_toggle(x, y):
layout = self._get_layout("doubletap")
if layout.visible:
with param.parameterized.batch_call_watchers(self):
self._hide_layouts()
layout.visible = True
return layout

try:
tools = display._element.opts["tools"]
except KeyError:
tools = []
display._element.opts(tools=[*tools, "doubletap"])
display._double_tap_stream.popup = double_tap_toggle

def _callback_commit(self, event):
self.annotator.commit()

Expand All @@ -204,17 +291,15 @@ def _watcher_selected_indices(self, event):
widget.value = value

def _watcher_mode_group(self, event):
if event.new in ["-", "✏"]:
self.annotator.selection_enabled = True
self.annotator.select_by_index()
self.annotator.editable_enabled = False
elif event.new == "+":
self.annotator.editable_enabled = True
self.annotator.select_by_index()
self.annotator.selection_enabled = False

for widget in self._fields_widgets.values():
widget.disabled = event.new == "-"
with param.parameterized.batch_call_watchers(self):
if event.new in ("-", "✏"):
self.annotator.selection_enabled = True
elif event.new == "+":
self.annotator.editable_enabled = True
self.annotator.selection_enabled = False

for widget in self._fields_widgets.values():
widget.disabled = event.new == "-"

def _set_standard_callbacks(self):
self._widget_apply_button.on_click(self._callback_apply)
Expand All @@ -224,6 +309,4 @@ def _set_standard_callbacks(self):
self._widget_mode_group.param.watch(self._watcher_mode_group, "value")

def __panel__(self):
if self.visible_widget is None:
return pn.Column(self.fields_widgets, self.tool_widgets)
return pn.Column(self.visible_widget, self.fields_widgets, self.tool_widgets)
return self._layout.clone(visible=True)
9 changes: 9 additions & 0 deletions holonote/tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,12 @@ def test_panel_app(annotator_range1d):
w = PanelWidgets(annotator_range1d)
assert isinstance(w.fields_widgets, pn.Column)
assert isinstance(w.tool_widgets, pn.Row)


def test_as_popup(annotator_range1d):
w = PanelWidgets(annotator_range1d, as_popup=True)
assert not w._layout.visible
for display in w.annotator._displays.values():
assert display._edit_streams[0].popup
assert display._tap_stream.popup
assert w.__panel__().visible

0 comments on commit d0def9d

Please sign in to comment.