Skip to content

fix(gui): bind the root <Configure> handler once, not per rebuild#41

Merged
donislawdev merged 1 commit into
masterfrom
fix/root-configure-binding-leak
Jul 24, 2026
Merged

fix(gui): bind the root <Configure> handler once, not per rebuild#41
donislawdev merged 1 commit into
masterfrom
fix/root-configure-binding-leak

Conversation

@donislawdev

Copy link
Copy Markdown
Owner

What and why

Follow-up to #40. That PR's changelog flagged one thing as deliberately deferred: App._build_ui re-binds <Configure> on the root every rebuild and never removes the old one, so the handler multiplies. This closes it — and a related leak the earlier note missed.

_build_ui runs on every language switch, and the root window outlives it. Binding <Configure> on the root inside _build_ui (with add="+", nothing removing it) piled up one handler per rebuild.

Measured on the fake tkinter (<Configure> handlers on the root):

after first build after 3 rebuilds
before 2 8
after 1 1

The #40 note named only _on_root_configure and undercounted (PROJECT_NOTES rule 5): the two banners built with wrapping_label(root, ...)engine_warning always, admin_warning when non-admin — each bound their own <Configure> on the same persistent root and multiplied identically, half the handlers. A wrapping_label on a short-lived container does not leak (its binding dies with the container); only the two whose container is the root do.

Each handler is cheap, and after #40 the dead ones are no-ops rather than crash records — so this is O(rebuilds) duplicated work on every resize, not a crash. Invisible, but real.

Changes (app.py only)

  • _on_root_configure is now bound once in __init__, before the first _build_ui; the line is gone from _build_ui. It reaches its widgets through self, so a single binding always drives the freshly rebuilt ones — no unbind needed, and none was safe (Misc.unbind(seq, funcid) still clears the whole sequence on the oldest Python in the CI matrix, and the root carries other <Configure> bindings).
  • The two banners are now plain ttk.Labels wrapped by that same single handler, not wrapping_label. Same look (left/anchored, an initial wraplength refined on the first resize). gui/labels.py and its short-lived-container callers are untouched; wrapping_label is no longer imported by gui/app.py.

Reviewer notes

  • New test verified by mutation (rule 5): test_the_ui_rebuild_does_not_pile_up_configure_handlers_on_the_root asserts exactly one <Configure> handler on the root after several rebuilds, and that the banner still wraps to the width-derived value (not just its build-time default — which would pass even with the fold gone). Reintroducing the per-rebuild bind, and dropping the banner wrap, each turns it red.
  • No user-facing changelog line, on purpose: identical look and wrapping, no behaviour change. A CHANGELOG.md line for an imperceptible change would be the filler PROJECT_NOTES rule 4 forbids. Documented in CHANGELOG-INTERNAL.md, which also corrects the stale "not fixed here" bullet from fix(gui): stop the UI rebuild from destroying the registry's windows #40.

Checklist

  • python -m pytest tests passes locally. (660 passed; run elevated, no admin-only deselects needed.)
  • New behaviour has tests (see tests/ for the style). One added, mutation-verified (both halves).
  • UI text goes through i18n keys, with both lang/en.json and lang/pl.json updated. (No new UI text — no lang/ edits needed.)
  • User-facing changes noted in CHANGELOG.md; technical ones and new tests in CHANGELOG-INTERNAL.md, under [Unreleased]. (Internal-only: nothing a user notices, see reviewer notes.)
  • Commits follow Conventional Commits (type(scope): summary).
  • No version bump — the owner closes a version via VERSION.txt.

🤖 Generated with Claude Code

App._build_ui runs on every language switch and the root window outlives it, so
binding <Configure> on the root inside _build_ui (add="+", nothing removing it)
accumulated one handler per rebuild: measured 2 after the first build, 8 after three
switches. The earlier "not fixed here" note named only _on_root_configure and
undercounted - the two banners built with wrapping_label(root, ...) each bound their
own <Configure> on the same persistent root and multiplied identically.

- _on_root_configure is bound once in __init__; the line is gone from _build_ui. It
  reaches its widgets through self, so one binding always drives the freshly rebuilt
  ones - no unbind (which is not safe on the oldest Python in the CI matrix anyway).
- The two banners are plain ttk.Labels wrapped by that same handler instead of
  wrapping_label; same look, no leak. labels.py and its other callers untouched.
- Not user-visible (identical look/wrapping), so CHANGELOG.md is left alone.

New test asserts exactly one <Configure> handler on the root across rebuilds and that
the banner still wraps to the width-derived value; both halves verified by mutation.
Full suite green (660 passed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@donislawdev
donislawdev merged commit bec76a1 into master Jul 24, 2026
8 checks passed
@donislawdev
donislawdev deleted the fix/root-configure-binding-leak branch July 24, 2026 17:01
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