Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add as_popup #101

Merged
merged 25 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -431,16 +431,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
Loading