From 7249d578523e9acf3a540fa816271cf077265b43 Mon Sep 17 00:00:00 2001 From: pax Date: Sat, 11 Apr 2026 18:04:55 -0500 Subject: [PATCH] fix rubber band state getting stuck across interrupted drags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes: 1. Stale state cleanup. If a rubber band drag is interrupted without a matching release event (Wayland focus steal, drag outside window, tab switch, alt-tab), _rb_origin and the rubber band widget stay stuck. The next click then reuses the stale origin and rubber band stops working until the app is restarted. New _clear_stale_rubber_band helper is called at the top of every mouse press entry point (Grid.mousePressEvent, on_padding_click, ThumbnailWidget pixmap press) so the next interaction starts from a clean slate. 2. Scroll offset sign error in _rb_drag. The intersection test translated thumb geometry by +vp_offset, but thumb.geometry() is in widget coords and rb_rect is in viewport coords — the translation needs to convert between them. Switched to translating rb_rect into widget coords (rb_widget = rb_rect.translated(vp_offset)) before the intersection test, which is the mathematically correct direction. Rubber band selection now tracks the visible band when scrolled. behavior change: rubber band stays responsive after interrupted drags --- booru_viewer/gui/grid.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/booru_viewer/gui/grid.py b/booru_viewer/gui/grid.py index f33a6c9..ca1481d 100644 --- a/booru_viewer/gui/grid.py +++ b/booru_viewer/gui/grid.py @@ -335,6 +335,11 @@ class ThumbnailWidget(QWidget): grid.on_padding_click(self, pos) event.accept() return + # Pixmap click — clear any stale rubber band state from a + # previous interrupted drag before starting a new interaction. + grid = self._grid() + if grid: + grid._clear_stale_rubber_band() self._drag_start = pos self.clicked.emit(self.index, event) elif event.button() == Qt.MouseButton.RightButton: @@ -544,6 +549,21 @@ class ThumbnailGrid(QScrollArea): self._thumbs[self._selected_index].set_selected(False) self._selected_index = -1 + def _clear_stale_rubber_band(self) -> None: + """Reset any leftover rubber band state before starting a new interaction. + + Rubber band state can get stuck if a drag is interrupted without + a matching release event — Wayland focus steal, drag outside the + window, tab switch mid-drag, etc. Every new mouse press calls this + so the next interaction starts from a clean slate instead of + reusing a stale origin (which would make the rubber band "not + work" until the app is restarted). + """ + if self._rubber_band is not None: + self._rubber_band.hide() + self._rb_origin = None + self._rb_pending_origin = None + def _select(self, index: int) -> None: if index < 0 or index >= len(self._thumbs): return @@ -617,12 +637,14 @@ class ThumbnailGrid(QScrollArea): def on_padding_click(self, thumb, local_pos) -> None: """Called directly by ThumbnailWidget when a click misses the pixmap.""" + self._clear_stale_rubber_band() vp_pos = thumb.mapTo(self.viewport(), local_pos) self._rb_pending_origin = vp_pos def mousePressEvent(self, event: QMouseEvent) -> None: # Clicks on viewport/flow (gaps, space below thumbs) start rubber band if event.button() == Qt.MouseButton.LeftButton: + self._clear_stale_rubber_band() child = self.childAt(event.position().toPoint()) if child is self.widget() or child is self.viewport(): self._rb_pending_origin = event.position().toPoint() @@ -635,11 +657,15 @@ class ThumbnailGrid(QScrollArea): return rb_rect = QRect(self._rb_origin, vp_pos).normalized() self._rubber_band.setGeometry(rb_rect) + # rb_rect is in viewport coords; thumb.geometry() is in widget (content) + # coords. Convert rb_rect to widget coords for the intersection test — + # widget.mapFrom(viewport, (0,0)) gives the widget-coord of viewport's + # origin, which is exactly the translation needed when scrolled. vp_offset = self.widget().mapFrom(self.viewport(), QPoint(0, 0)) + rb_widget = rb_rect.translated(vp_offset) self._clear_multi() for i, thumb in enumerate(self._thumbs): - thumb_rect = thumb.geometry().translated(vp_offset) - if rb_rect.intersects(thumb_rect): + if rb_widget.intersects(thumb.geometry()): self._multi_selected.add(i) thumb.set_multi_selected(True)