mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 20:07:18 +01:00
gatelock: bind the wheel per descendant in ScrollableSurface
Wheel-over-text did not scroll. Tk dispatches through *bindtags*, not the parent chain -- a child's tags are (itself, its class, the toplevel, "all") -- so binding <Button-4>/<Button-5> on `content` was never consulted for an event over a Label sitting on top of it. Content that fills its viewport covers that frame completely, which is every screen this class exists for, so the mouse wheel was dead exactly where a user points it. Found while retiring leetcode-guard's hand-rolled _scrollable, which got this right by accident: it used bind_all, which works but installs the handler on every widget in the application, including other windows' scroll regions. Binding per descendant inside the existing _track_focus walk keeps it scoped, and reuses that walk's WeakSet so a repaint cannot stack duplicate handlers. Verified both directions under xvfb (Button-5 scrolls down, Button-4 back up) from an event generated on a Label, not on the canvas. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4Rj3s7K2qzrLNgJqqPhno
This commit is contained in:
parent
c963cd88a8
commit
d82107ab9a
@ -344,16 +344,28 @@ class ScrollableSurface:
|
||||
self.canvas.yview_moveto(1.0)
|
||||
|
||||
def _track_focus(self, parent: tk.Misc) -> None:
|
||||
"""Bind <FocusIn> on every descendant, each exactly once.
|
||||
"""Bind <FocusIn> and the wheel on every descendant, each exactly once.
|
||||
|
||||
A repaint destroys and rebuilds the widgets, so this re-runs per
|
||||
screen; the WeakSet is what keeps a second :meth:`finalize` on the
|
||||
*same* widgets from stacking a second handler on each of them, and
|
||||
drops entries as the widgets are destroyed.
|
||||
|
||||
The wheel has to be bound per descendant because Tk dispatches through
|
||||
*bindtags*, not the parent chain: a child's tags are ``(itself, its
|
||||
class, the toplevel, "all")``, so an event over a Label never reaches
|
||||
a binding on ``content``. Content that fills its viewport covers that
|
||||
frame completely, which means wheel-over-text -- the only way anyone
|
||||
actually scrolls with a mouse -- would do nothing. The donor this
|
||||
replaced reached for ``bind_all`` instead, which works but installs
|
||||
the handler on *every* widget in the application, including other
|
||||
windows' scroll regions.
|
||||
"""
|
||||
for child in parent.winfo_children():
|
||||
if child not in self._focus_bound:
|
||||
child.bind("<FocusIn>", self._on_descendant_focus, add="+")
|
||||
child.bind(_WHEEL_UP, self._line_up, add="+")
|
||||
child.bind(_WHEEL_DOWN, self._line_down, add="+")
|
||||
self._focus_bound.add(child)
|
||||
self._track_focus(child)
|
||||
|
||||
|
||||
@ -327,3 +327,37 @@ def test_scroll_into_view_holds_still_for_a_widget_in_the_middle(
|
||||
surface.scroll_into_view(middle)
|
||||
|
||||
assert _offset(surface) == before
|
||||
|
||||
|
||||
def test_the_wheel_scrolls_from_over_the_content(surface: ScrollableSurface) -> None:
|
||||
"""Wheel-over-text scrolls, which is the only way anyone uses a mouse here.
|
||||
|
||||
Tk dispatches through *bindtags*, not the parent chain: a child's tags are
|
||||
``(itself, its class, the toplevel, "all")``, so a binding on ``content``
|
||||
is never consulted for an event over a Label sitting on top of it. Content
|
||||
that fills the viewport covers that frame entirely, so binding the wheel
|
||||
only on the canvas and the content frame makes wheel scrolling dead
|
||||
everywhere the user would actually point.
|
||||
"""
|
||||
child = surface.content.winfo_children()[10]
|
||||
before = surface.canvas.yview()[0]
|
||||
|
||||
child.event_generate("<Button-5>")
|
||||
surface.canvas.update()
|
||||
|
||||
assert surface.canvas.yview()[0] > before
|
||||
|
||||
|
||||
def test_the_wheel_scrolls_back_up_from_over_the_content(
|
||||
surface: ScrollableSurface,
|
||||
) -> None:
|
||||
"""The up notch is bound on descendants too, not just the down one."""
|
||||
child = surface.content.winfo_children()[10]
|
||||
child.event_generate("<Button-5>")
|
||||
surface.canvas.update()
|
||||
scrolled = surface.canvas.yview()[0]
|
||||
|
||||
child.event_generate("<Button-4>")
|
||||
surface.canvas.update()
|
||||
|
||||
assert surface.canvas.yview()[0] < scrolled
|
||||
|
||||
Loading…
Reference in New Issue
Block a user