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
29 changes: 29 additions & 0 deletions src/derzug/views/orange.py
Original file line number Diff line number Diff line change
Expand Up @@ -3444,7 +3444,9 @@ def set_scheme(self, new_scheme, freeze_creation=False):

def _reemit_restored_source_widgets(self) -> None:
"""Re-emit restored source-widget outputs after workflow reload settles."""
from derzug.widgets.select import Select
from derzug.widgets.spool import Spool
from derzug.widgets.waterfall import Waterfall

document = self.current_document()
scheme = getattr(document, "scheme", lambda: None)()
Expand All @@ -3457,6 +3459,33 @@ def _reemit_restored_source_widgets(self) -> None:
widget.run()
else:
widget._emit_current_output()
elif (
isinstance(widget, Waterfall)
and widget._patch is None
and widget._pending_saved_selection_restore
):
widget._emit_current_selection()
for link in scheme.links:
if not getattr(link, "enabled", True):
continue
if getattr(link.source_channel, "name", "") != "Select Params":
continue
if getattr(link.sink_channel, "name", "") != "Select Params":
continue
source_widget = scheme.widget_for_node(link.source_node)
sink_widget = scheme.widget_for_node(link.sink_node)
if not isinstance(source_widget, Waterfall) or not isinstance(
sink_widget, Select
):
continue
if (
source_widget._patch is not None
or not source_widget._pending_saved_selection_restore
):
continue
select_params = source_widget._saved_select_params()
if select_params is not None:
sink_widget.set_select_params(select_params)

def _collect_open_widget_node_ids(self) -> list[int]:
"""Return indices of nodes whose widget windows are currently visible.
Expand Down
90 changes: 90 additions & 0 deletions src/derzug/widgets/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ def set_select_params(self, select_params: SelectParams | None) -> None:
self._restore_manual_selection_after_params()
else:
self._apply_external_select_params_if_ready()
self._selection_refresh_panel()
self._emit_selected_output()

def _selection_on_state_changed(self) -> None:
Expand All @@ -254,6 +255,89 @@ def _rebind_dynamic_controls(self) -> None:
"""Rebuild selection controls after a new input source arrives."""
self._selection_refresh_panel()

def _selection_refresh_panel(self) -> None:
"""Push the current selection state into the controls."""
if not self._refresh_external_select_params_panel():
super()._selection_refresh_panel()

def _refresh_external_select_params_panel(self) -> bool:
"""Show read-only SelectParams range controls without patch-mode input."""
if (
self._external_select_params is None
or self._input_kind == "patch"
or self._selection_panel is None
):
return False
patch = self._spool_display_patch()
state = self._external_select_params_display_state(patch)
if state is None:
return False
panel = self._selection_panel
panel.set_mode(state.mode)
panel.set_patch_basis(state.patch.basis)
panel.rebuild_patch_rows(
tuple(state.patch.extents),
state.patch.extents,
self._selection_preferred_patch_dims,
)
panel.set_patch_enabled(state.patch.enabled)
panel.set_patch_ranges(
state.patch.ranges,
state.patch.extents,
tuple(self._external_select_params.kwargs),
)
panel.set_patch_editable(False)
self._selection_patch_checkboxes = panel.patch_checkboxes
self._selection_patch_edits = panel.patch_edits
return True

def _external_select_params_display_state(
self, patch: dc.Patch | None
) -> SelectionState | None:
"""Return patch-style state for displaying external SelectParams."""
params = self._external_select_params
if params is None or not params.kwargs:
return None
state = SelectionState()
if patch is not None:
state.apply_select_params(params, patch)
return state

basis = PatchSelectionBasis.ABSOLUTE
if params.relative:
basis = PatchSelectionBasis.RELATIVE
elif params.samples:
basis = PatchSelectionBasis.SAMPLES
ranges: dict[str, tuple[object, object]] = {}
for dim, value_range in params.kwargs.items():
try:
low, high = value_range
except (TypeError, ValueError):
continue
ranges[dim] = (low, high)
if not ranges:
return None
state.mode = SelectionMode.PATCH
state.patch.basis = basis
state.patch.extents = dict(ranges)
state.patch.ranges = ranges
state.patch.enabled = {dim: True for dim in ranges}
return state

def _spool_display_patch(self) -> dc.Patch | None:
"""Return a representative spool patch for SelectParams display."""
spool = self._spool
if spool is None:
return None
try:
return spool[0]
except Exception:
pass
try:
return next(iter(spool))
except Exception:
return None

def _emit_selected_output(self) -> None:
"""Trigger the standard run lifecycle so _on_result is always the send site."""
self.run()
Expand Down Expand Up @@ -594,6 +678,12 @@ def _apply_external_select_params_if_ready(self) -> bool:
self._selection_refresh_panel()
return True

def _selection_show_full_extent_dims(self) -> tuple[str, ...]:
"""Show incoming SelectParams values even when they match patch extents."""
if self._external_select_params is None:
return ()
return tuple(self._external_select_params.kwargs)

def _restore_manual_selection_after_params(self) -> None:
"""Restore editable patch controls after external params disconnect."""
if self._input_kind != "patch" or self._patch is None:
Expand Down
25 changes: 19 additions & 6 deletions src/derzug/widgets/selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -980,8 +980,10 @@ def set_patch_ranges(
self,
ranges: dict[str, tuple[Any, Any]],
extents: dict[str, tuple[Any, Any]],
show_full_extent_dims: tuple[str, ...] = (),
) -> None:
"""Write current patch ranges into the visible line edits."""
show_full_extent = set(show_full_extent_dims)
with self.syncing():
for dim, edits in self.patch_edits.items():
if dim not in ranges:
Expand All @@ -991,12 +993,18 @@ def set_patch_ranges(
low_edit, high_edit = edits
low, high = ranges[dim]
full_low, full_high = extents[dim]
desired_low = (
"" if _values_equal(low, full_low) else _format_coord_value(low)
)
desired_high = (
"" if _values_equal(high, full_high) else _format_coord_value(high)
)
if dim in show_full_extent:
desired_low = _format_coord_value(low)
desired_high = _format_coord_value(high)
else:
desired_low = (
"" if _values_equal(low, full_low) else _format_coord_value(low)
)
desired_high = (
""
if _values_equal(high, full_high)
else _format_coord_value(high)
)
current_low = low_edit.text()
current_high = high_edit.text()
if desired_low == "":
Expand Down Expand Up @@ -1287,6 +1295,7 @@ def _selection_refresh_panel(self) -> None:
panel.set_patch_ranges(
self._selection_state.patch.ranges,
self._selection_state.patch.extents,
self._selection_show_full_extent_dims(),
)
panel.set_spool_filters(
self._selection_state.spool.options,
Expand All @@ -1305,6 +1314,10 @@ def _selection_set_patch_editable(self, editable: bool) -> None:
if self._selection_panel is not None:
self._selection_panel.set_patch_editable(self._selection_patch_editable)

def _selection_show_full_extent_dims(self) -> tuple[str, ...]:
"""Return dims whose full-extent ranges should be shown, not blanked."""
return ()

def _selection_request_panel_refresh(self) -> None:
"""Request a visible selection-panel refresh through the host widget."""
self._selection_refresh_panel()
Expand Down
41 changes: 39 additions & 2 deletions src/derzug/widgets/waterfall.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,11 @@
MultiDimPlotControlsMixin,
format_nd_coord_value,
)
from derzug.widgets.selection import SelectionControlsMixin
from derzug.widgets.selection import (
PatchSelectionBasis,
SelectionControlsMixin,
SelectionState,
)
from derzug.workflow import Task
from derzug.workflow.widget_tasks import PatchSelectionWithParamsTask

Expand Down Expand Up @@ -1477,7 +1481,12 @@ def _emit_current_selection(self) -> None:
self.Warning.empty_selection.clear()
if self._patch is None:
self.Outputs.patch.send(None)
self.Outputs.select_params.send(None)
select_params = (
self._saved_select_params()
if self._pending_saved_selection_restore
else None
)
self.Outputs.select_params.send(select_params)
return
if self._roi is not None and not self._restore_saved_roi_after_render:
self._update_selection_from_roi(notify=False)
Expand Down Expand Up @@ -1985,6 +1994,34 @@ def _load_saved_selection_state(self) -> dict[str, Any] | None:
return None
return {"basis": basis_name, "rows": rows}

def _saved_select_params(self) -> SelectParams | None:
"""Return saved selection settings as public SelectParams."""
payload = self._load_saved_selection_state()
if payload is None:
return None
basis_name = str(payload.get("basis", "")).strip()
rows = payload.get("rows")
if not isinstance(rows, list):
return None
kwargs: dict[str, tuple[object, object]] = {}
for row in rows:
if not isinstance(row, dict) or not row.get("enabled", True):
continue
dim = row.get("dim")
if not isinstance(dim, str) or not dim:
continue
kwargs[dim] = (
SelectionState._deserialize_value(row.get("low")),
SelectionState._deserialize_value(row.get("high")),
)
if not kwargs:
return None
return SelectParams(
kwargs=kwargs,
relative=basis_name == PatchSelectionBasis.RELATIVE.value,
samples=basis_name == PatchSelectionBasis.SAMPLES.value,
)

def _prime_saved_selection_state(self) -> None:
"""Seed selection state from stored workflow settings before patch input."""
primed = self._selection_state.prime_patch_state_from_settings(
Expand Down
54 changes: 54 additions & 0 deletions tests/test_views/test_orange_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -3018,6 +3018,60 @@ def test_workflow_roundtrip_preserves_select_patch_basis_and_sample_values(
assert low_edit.text() == "100"
assert high_edit.text() == "200"

def test_loaded_waterfall_select_params_populate_params_only_select(
self, derzug_app, tmp_path, qapp, orange_workflow
):
"""Saved Waterfall params should populate Select without a data link."""
window = derzug_app.window
workflow = orange_workflow(
(
("Spool", "spool-node"),
("Waterfall", "waterfall-node"),
("Select", "select-node"),
),
(
("spool-node", "Patch", "waterfall-node", "Patch"),
("waterfall-node", "Select Params", "select-node", "Select Params"),
),
)
waterfall_widget = workflow.widgets_by_title["waterfall-node"]
selected = {
"distance": (643.7278839133311, 793.7278839133311),
"time": (0.013890560242722401, 0.03889056024272312),
}
waterfall_widget.saved_selection_basis = "absolute"
waterfall_widget.saved_selection_ranges = [
{
"dim": dim,
"enabled": True,
"low": {"kind": "float", "value": low},
"high": {"kind": "float", "value": high},
}
for dim, (low, high) in selected.items()
]
waterfall_widget.saved_selection_has_roi = True

workflow_path = tmp_path / "waterfall-select-params-only.ows"
assert window.save_scheme_to(workflow.scheme, str(workflow_path))

window.load_scheme(str(workflow_path))
qapp.processEvents()
qapp.processEvents()
loaded_scheme = window.current_document().scheme()
loaded_node = next(
node for node in loaded_scheme.nodes if node.title == "select-node"
)
loaded_widget = loaded_scheme.widget_for_node(loaded_node)

assert loaded_widget._selection_panel.mode_label.text() == "Range selection"
assert set(loaded_widget._selection_patch_edits) == set(selected)
for dim, (low, high) in selected.items():
low_edit, high_edit = loaded_widget._selection_patch_edits[dim]
assert low_edit.text() == format_display(low)
assert high_edit.text() == format_display(high)
assert not low_edit.isEnabled()
assert not high_edit.isEnabled()

def test_workflow_roundtrip_preserves_select_spool_filter_values(
self, derzug_app, tmp_path, qapp, orange_workflow
):
Expand Down
Loading
Loading