Skip to content

fix(ui): stop multi-select position spin from re-triggering the move - #594

Merged
knipknap merged 2 commits into
mainfrom
fix/multi-select-spin-position-loop
Oct 19, 2026
Merged

knipknap merged 2 commits into
mainfrom
fix/multi-select-spin-position-loop

Conversation

@knipknap

Copy link
Copy Markdown
Contributor

Summary

Stepping the X/Y Position spin row (e.g. clicking +) with several items selected hung the application in an endless move loop: the items ping-ponged between the old and new position at 100% CPU. Typing a value usually escaped the loop, which is why it appeared tied to the +/- buttons.

Root cause

Two defects compound:

  1. DocItemPropertiesWidget refreshed the panel synchronously on every item matrix change. A multi-item move (set_position_group) executes one command per item, so the refresh read the group bounding box mid-transaction — while only part of the move had been applied — and wrote that intermediate value back into the spin row. For a two-item selection the intermediate position alternates between old and new on each read.
  2. Programmatic spin row writes (set_value / set_value_in_base_units) did not record the value in _last_emitted_value. GTK delivers the entry text update asynchronously, so the deferred notify::text was treated as a user edit, re-entering _on_x_changed with the opposite value — closing the loop.

This is pre-existing on main; it was surfaced by a tester while evaluating #587 (sketches are irrelevant — any two selected items do it), but #587 does not touch the affected code.

Fix

  • DocItemPropertiesWidget._on_item_data_changed now schedules one coalesced GLib.idle_add refresh instead of refreshing synchronously per signal, so the panel only observes the settled post-transaction state (this also collapses N redundant refresh passes per multi-item command into one).
  • SpinRow.set_value and UnitSpinRow.set_value_in_base_units record _last_emitted_value, so the deferred GTK change notification is recognized as an echo of the programmatic set and deduped instead of re-triggering the transform handler.

Testing

  • New regression test tests/ui_gtk/doceditor/test_multi_select_position_spin.py (ui-marked, full window): selects two workpieces, steps the X row three times, asserts both the row and the model settle exactly 3 mm ahead. Without the fix it hangs and is killed by the built-in watchdog; with the fix it passes in ~3.5 s.
  • Reproduced the hang on unmodified main with a traced full-window repro under xvfb (watchdog stack dumps) before fixing; the fix was also verified against the feat: position workpieces relative to a chosen origin #587 branch build.
  • pixi run lint clean; pixi run uitest for tests/ui_gtk/doceditor/ (104 tests) and the spin-row/varset suites (125 tests) pass; headless transform/asset command suites pass.

Stepping the X/Y position spin row with several items selected hung
the application in an endless move loop between the old and new
position. Two defects compound:

- DocItemPropertiesWidget refreshed synchronously on every item
  matrix change. A multi-item move executes one command per item,
  so the refresh read the group bounding box mid-transaction while
  only part of the move had been applied, and wrote that
  intermediate value back into the spin row.
- Programmatic spin row writes did not record the value in
  _last_emitted_value, so the entry text update that GTK delivers
  asynchronously was treated as a user edit and re-entered
  _on_x_changed with the opposite value.

The panel now refreshes once on idle after the notifications
settle, and programmatic sets mark their value as echoed so the
deferred notification dedupes. Typing a value mostly escaped the
loop because text and adjustment agree at emission time; the +/-
buttons reliably hung.
The module-level DISPLAY guard used allow_module_level=False, which
raises a collection error instead of skipping and aborted the whole
Windows test run at collection time. Guard the skip on Linux like
test_mainwindow.py does and pass allow_module_level=True.
@knipknap
knipknap merged commit 7d232f3 into main Oct 19, 2026
34 checks passed
@github-actions

Copy link
Copy Markdown

🤖 Estimated LLM implementation cost

Replayed each of the 2 commits in this pull request and counted the code lines each commit added or rewrote:

Commit Message Lines (add/rewrite) Est. output tokens Est. output cost
0cab6500 fix(ui): stop multi-select position spin from re-triggering the move 124 / 5 1,290 $0.0193
5c13f6fc fix(test): skip the hang regression test cleanly without a display 7 / 5 120 $0.0018
Total 141 1,410 $0.0211

Added lines by language: Python 131

Full estimate

Component Est. tokens Est. cost
Code emission (141 add/rewrite lines) 1,410 $0.0211
Context input (reading touched files) 8,837 $0.0265
Conversation replay (≈15× output) 21,150 $0.0634
Prompting (pull request description) 477 $0.0014
Debug cycles (1 failed checks) 21,000 $0.0750
Lower bound (output only) 1,410 $0.0211
Best estimate 52,874 $0.1875

Cost model: ≈10 tokens per code line at $15/Mtok output and $3/Mtok input (assuming heavy prompt caching); input ≈ 15× output for conversation replay; a review round ≈ 42,000 tokens; a failed CI run ≈ 21,000 tokens; file reads capped at 20 kB each.

Active window from first commit to merge: <1 day.

⚠️ Caveat: the lower bound covers only the tokens for emitting the code. The best estimate adds modeled overhead for exploration, conversation replay, review and debug turns, but real sessions vary by tool and task — treat it as an order-of-magnitude figure. Human engineering time is not included.

This branch was previously deployed

1 inactive deployment
pypi — 5c13f6fc Deployed Oct 19, 2026 by knipknap via Build and Publish Wheel #2404
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant