Skip to content
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
39 changes: 39 additions & 0 deletions quantui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,12 @@
from quantui.app_history import (
on_view_log as _hist_on_view_log,
)
from quantui.app_measurement import (
on_measure_clear as _measure_on_clear,
)
from quantui.app_measurement import (
on_measure_inbox_changed as _measure_on_inbox_changed,
)
from quantui.app_runflow import (
calc_type_key as _run_calc_type_key,
)
Expand Down Expand Up @@ -1219,6 +1225,15 @@ class QuantUIApp:
_iso_wireframe_cb: Any
_iso_resolution_dd: Any
_last_result_dir: Any
_measure_inbox: Any
_measure_js_bridge: Any
_measure_readout: Any
_measure_clear_btn: Any
_measure_help_btn: Any
_measure_controls: Any
_measure_fallback_msg: Any
_measure_panel: Any
_measure_picks: Any
_nmr_accordion: Any
_nmr_output: Any
_orb_accordion: Any
Expand Down Expand Up @@ -2258,6 +2273,14 @@ def _wire_callbacks(self) -> None:
self._reorg_png_inbox.observe(
self._safe_cb(self._on_reorg_png_captured), names="value"
)
# Click-to-measure (M-MEASURE MEAS.2/3): a click in the Analysis-tab
# viewer posts an atom index into this inbox the same way the PNG
# buttons post a data URI.
self._measure_inbox.observe(
self._safe_cb(self._on_measure_inbox_changed), names="value"
)
self._measure_clear_btn.on_click(self._on_measure_clear)
self._measure_help_btn.on_click(self._on_measure_help)
# Persist the grid choice so it survives a relaunch (ORBX.2).
self._iso_resolution_dd.observe(
self._safe_cb(self._on_iso_resolution_changed), names="value"
Expand Down Expand Up @@ -3199,6 +3222,12 @@ def _rerender_3d_views(self) -> None:
lighting=self._viz_lighting,
bgcolor=self._plotly_theme_colors()["scene_bgcolor"],
)
# M-MEASURE: same wiring as the post-calc render path — a
# backend-toggle switch is still a fresh viewer, so stale
# picks from the previous backend must not survive it.
from quantui.app_measurement import finalize_analysis_html

html = finalize_analysis_html(self, html, chosen)
self._set_html_output(self._analysis_mol_output, html)
self._update_analysis_backend_label(chosen)

Expand Down Expand Up @@ -3690,6 +3719,16 @@ def _on_orb_png_captured(self, change) -> None:
def _on_reorg_png_captured(self, change) -> None:
_exp_on_reorg_png_captured(self, change)

def _on_measure_inbox_changed(self, change) -> None:
_measure_on_inbox_changed(self, change)

def _on_measure_clear(self, btn=None) -> None:
_measure_on_clear(self, btn)

def _on_measure_help(self, btn) -> None:
_ = btn
self._show_help_topic("measure")

def _on_iso_resolution_changed(self, change) -> None:
"""Persist the isosurface grid choice.

Expand Down
64 changes: 64 additions & 0 deletions quantui/app_builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from IPython.display import HTML, display

import quantui
from quantui import app_measurement as _measure
from quantui import molecule_library as _ml
from quantui import theme as _theme
from quantui.help_content import HELP_TOPICS
Expand Down Expand Up @@ -2426,6 +2427,68 @@ def _plot_export_row(prefix: str) -> widgets.HBox:
# box around it — the Output widget cannot shrink-wrap.
app._analysis_mol_output = widgets.Output()

# ── Click-to-measure (M-MEASURE MEAS.2-.6) ──────────────────────────
# Hidden inbox for the click JS injected into the py3Dmol-rendered
# Analysis viewer (app_measurement.inject_click_js) — same JS->kernel
# "write into a hidden Textarea's DOM node, dispatch 'input'" trick
# ORBX.1's PNG capture uses, new inbox.
app._measure_inbox = widgets.Textarea(
value="", layout=layout_fn(width="1px", height="1px", visibility="hidden")
)
app._measure_inbox.add_class(_measure.MEASURE_INBOX_CLASS)
# Hidden Output that carries one-shot Javascript pushing highlight
# updates to the live viewer. Mirrors _iso_js_bridge.
app._measure_js_bridge = widgets.Output(
layout=layout_fn(width="0px", height="0px", visibility="hidden")
)
app._measure_readout = widgets.HTML(
value=_measure._readout_html(_measure._PLACEHOLDER_TEXT)
)
app._measure_clear_btn = widgets.Button(
description="Clear",
button_style="",
tooltip="Clear the picked atoms and their highlights",
layout=layout_fn(width="70px"),
)
app._measure_help_btn = widgets.Button(
description="?",
button_style="",
layout=layout_fn(width="28px", height="28px"),
tooltip="Click-to-measure bond length / angle / dihedral — opens Help tab",
)
app._measure_controls = widgets.VBox(
[
widgets.HBox(
[app._measure_readout],
layout=layout_fn(align_items="center"),
),
widgets.HBox(
[app._measure_clear_btn, app._measure_help_btn],
layout=layout_fn(align_items="center", gap="6px", margin="2px 0 0"),
),
]
)
# DEC-009: the panel stays visible regardless of backend — only its
# content swaps (MEAS.6). Shown when the resolved backend can't support
# native clicking (plotlymol, or py3Dmol simply unavailable).
app._measure_fallback_msg = widgets.HTML(
value=(
f'<p style="color:{_theme.TEXT_SECONDARY};font-size:12px;margin:4px 0">'
"Click-to-measure needs the py3Dmol viewer — switch backends above "
"(or in Settings) to use it.</p>"
),
layout=layout_fn(display="none"),
)
app._measure_panel = widgets.VBox(
[
app._measure_controls,
app._measure_fallback_msg,
app._measure_inbox,
app._measure_js_bridge,
],
layout=layout_fn(margin="4px 0 8px"),
)

# Analysis-tab backend toggle — mirrors the Calculate-tab `viz_backend_toggle`.
# Created only when both backends are available (matches Calculate-tab
# convention). Synchronized with the Calculate-tab toggle via
Expand Down Expand Up @@ -2490,6 +2553,7 @@ def _plot_export_row(prefix: str) -> widgets.HBox:
ana_children = [
app._analysis_context_lbl,
app._analysis_mol_output,
app._measure_panel,
]
if ana_backend_row is not None:
ana_children.append(ana_backend_row)
Expand Down
Loading