rubber band from cell padding with 30px drag threshold
- ThumbnailWidget detects clicks outside the pixmap and calls grid.on_padding_click() via parent walk (signals + event filters both failed on Wayland/QScrollArea) - Grid tracks a pending rubber band origin; only activates past 30px manhattan distance so small clicks deselect cleanly - Move/release events forwarded from ThumbnailWidget to grid for both the pending-drag check and the active rubber band drag - Fixed mapFrom/mapTo direction (mapFrom's first arg must be a parent)
This commit is contained in:
parent
c440065513
commit
fa9fcc3db0
@ -2,8 +2,11 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
log = logging.getLogger("booru")
|
||||||
|
|
||||||
from PySide6.QtCore import Qt, Signal, QSize, QRect, QRectF, QMimeData, QUrl, QPoint, Property, QPropertyAnimation, QEasingCurve
|
from PySide6.QtCore import Qt, Signal, QSize, QRect, QRectF, QMimeData, QUrl, QPoint, Property, QPropertyAnimation, QEasingCurve
|
||||||
from PySide6.QtGui import QPixmap, QPainter, QPainterPath, QColor, QPen, QKeyEvent, QWheelEvent, QDrag, QMouseEvent
|
from PySide6.QtGui import QPixmap, QPainter, QPainterPath, QColor, QPen, QKeyEvent, QWheelEvent, QDrag, QMouseEvent
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
@ -276,6 +279,17 @@ class ThumbnailWidget(QWidget):
|
|||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def mouseMoveEvent(self, event) -> None:
|
def mouseMoveEvent(self, event) -> None:
|
||||||
|
# If the grid has a pending or active rubber band, forward the move
|
||||||
|
grid = self._grid()
|
||||||
|
if grid and (grid._rb_origin or grid._rb_pending_origin):
|
||||||
|
vp_pos = self.mapTo(grid.viewport(), event.position().toPoint())
|
||||||
|
if grid._rb_origin:
|
||||||
|
grid._rb_drag(vp_pos)
|
||||||
|
return
|
||||||
|
if grid._maybe_start_rb(vp_pos):
|
||||||
|
grid._rb_drag(vp_pos)
|
||||||
|
return
|
||||||
|
return
|
||||||
# Update hover and cursor based on whether cursor is over the pixmap
|
# Update hover and cursor based on whether cursor is over the pixmap
|
||||||
over = self._hit_pixmap(event.position().toPoint()) if self._pixmap else False
|
over = self._hit_pixmap(event.position().toPoint()) if self._pixmap else False
|
||||||
if over != self._hover:
|
if over != self._hover:
|
||||||
@ -303,19 +317,49 @@ class ThumbnailWidget(QWidget):
|
|||||||
py = (self.height() - self._pixmap.height()) // 2
|
py = (self.height() - self._pixmap.height()) // 2
|
||||||
return QRect(px, py, self._pixmap.width(), self._pixmap.height()).contains(pos)
|
return QRect(px, py, self._pixmap.width(), self._pixmap.height()).contains(pos)
|
||||||
|
|
||||||
|
def _grid(self):
|
||||||
|
"""Walk up to the ThumbnailGrid ancestor."""
|
||||||
|
w = self.parentWidget()
|
||||||
|
while w:
|
||||||
|
if isinstance(w, ThumbnailGrid):
|
||||||
|
return w
|
||||||
|
w = w.parentWidget()
|
||||||
|
return None
|
||||||
|
|
||||||
def mousePressEvent(self, event) -> None:
|
def mousePressEvent(self, event) -> None:
|
||||||
if event.button() == Qt.MouseButton.LeftButton:
|
if event.button() == Qt.MouseButton.LeftButton:
|
||||||
self._drag_start = event.position().toPoint()
|
pos = event.position().toPoint()
|
||||||
|
if not self._hit_pixmap(pos):
|
||||||
|
grid = self._grid()
|
||||||
|
if grid:
|
||||||
|
grid.on_padding_click(self, pos)
|
||||||
|
event.accept()
|
||||||
|
return
|
||||||
|
self._drag_start = pos
|
||||||
self.clicked.emit(self.index, event)
|
self.clicked.emit(self.index, event)
|
||||||
elif event.button() == Qt.MouseButton.RightButton:
|
elif event.button() == Qt.MouseButton.RightButton:
|
||||||
self.right_clicked.emit(self.index, event.globalPosition().toPoint())
|
self.right_clicked.emit(self.index, event.globalPosition().toPoint())
|
||||||
|
|
||||||
def mouseReleaseEvent(self, event) -> None:
|
def mouseReleaseEvent(self, event) -> None:
|
||||||
self._drag_start = None
|
self._drag_start = None
|
||||||
|
grid = self._grid()
|
||||||
|
if grid:
|
||||||
|
if grid._rb_origin:
|
||||||
|
grid._rb_end()
|
||||||
|
elif grid._rb_pending_origin is not None:
|
||||||
|
# Click without drag — treat as deselect
|
||||||
|
grid._rb_pending_origin = None
|
||||||
|
grid.clear_selection()
|
||||||
|
|
||||||
def mouseDoubleClickEvent(self, event) -> None:
|
def mouseDoubleClickEvent(self, event) -> None:
|
||||||
self._drag_start = None
|
self._drag_start = None
|
||||||
if event.button() == Qt.MouseButton.LeftButton:
|
if event.button() == Qt.MouseButton.LeftButton:
|
||||||
|
pos = event.position().toPoint()
|
||||||
|
if not self._hit_pixmap(pos):
|
||||||
|
grid = self._grid()
|
||||||
|
if grid:
|
||||||
|
grid.on_padding_click(self, pos)
|
||||||
|
return
|
||||||
self.double_clicked.emit(self.index)
|
self.double_clicked.emit(self.index)
|
||||||
|
|
||||||
|
|
||||||
@ -436,9 +480,9 @@ class ThumbnailGrid(QScrollArea):
|
|||||||
self._last_click_index = -1 # for shift-click range
|
self._last_click_index = -1 # for shift-click range
|
||||||
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||||
self.verticalScrollBar().valueChanged.connect(self._check_scroll_bottom)
|
self.verticalScrollBar().valueChanged.connect(self._check_scroll_bottom)
|
||||||
self.viewport().installEventFilter(self)
|
|
||||||
# Rubber band drag selection
|
# Rubber band drag selection
|
||||||
self._rubber_band: QRubberBand | None = None
|
self._rubber_band: QRubberBand | None = None
|
||||||
|
self._rb_pending_origin: QPoint | None = None # press position, not yet confirmed as drag
|
||||||
self._rb_origin: QPoint | None = None
|
self._rb_origin: QPoint | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -466,7 +510,7 @@ class ThumbnailGrid(QScrollArea):
|
|||||||
thumb.clicked.connect(self._on_thumb_click)
|
thumb.clicked.connect(self._on_thumb_click)
|
||||||
thumb.double_clicked.connect(self._on_thumb_double_click)
|
thumb.double_clicked.connect(self._on_thumb_double_click)
|
||||||
thumb.right_clicked.connect(self._on_thumb_right_click)
|
thumb.right_clicked.connect(self._on_thumb_right_click)
|
||||||
thumb.installEventFilter(self)
|
|
||||||
self._flow.add_widget(thumb)
|
self._flow.add_widget(thumb)
|
||||||
self._thumbs.append(thumb)
|
self._thumbs.append(thumb)
|
||||||
|
|
||||||
@ -481,7 +525,7 @@ class ThumbnailGrid(QScrollArea):
|
|||||||
thumb.clicked.connect(self._on_thumb_click)
|
thumb.clicked.connect(self._on_thumb_click)
|
||||||
thumb.double_clicked.connect(self._on_thumb_double_click)
|
thumb.double_clicked.connect(self._on_thumb_double_click)
|
||||||
thumb.right_clicked.connect(self._on_thumb_right_click)
|
thumb.right_clicked.connect(self._on_thumb_right_click)
|
||||||
thumb.installEventFilter(self)
|
|
||||||
self._flow.add_widget(thumb)
|
self._flow.add_widget(thumb)
|
||||||
self._thumbs.append(thumb)
|
self._thumbs.append(thumb)
|
||||||
new_thumbs.append(thumb)
|
new_thumbs.append(thumb)
|
||||||
@ -571,24 +615,26 @@ class ThumbnailGrid(QScrollArea):
|
|||||||
self._rubber_band.show()
|
self._rubber_band.show()
|
||||||
self.clear_selection()
|
self.clear_selection()
|
||||||
|
|
||||||
def eventFilter(self, obj, event) -> bool:
|
def on_padding_click(self, thumb, local_pos) -> None:
|
||||||
if isinstance(obj, ThumbnailWidget):
|
"""Called directly by ThumbnailWidget when a click misses the pixmap."""
|
||||||
if event.type() in (event.Type.MouseButtonPress, event.Type.MouseButtonDblClick):
|
vp_pos = thumb.mapTo(self.viewport(), local_pos)
|
||||||
if event.button() == Qt.MouseButton.LeftButton and not obj._hit_pixmap(event.position().toPoint()):
|
self._rb_pending_origin = vp_pos
|
||||||
vp_pos = self.viewport().mapFromGlobal(obj.mapToGlobal(event.position().toPoint()))
|
|
||||||
self._start_rubber_band(vp_pos)
|
|
||||||
return True
|
|
||||||
elif obj is self.viewport():
|
|
||||||
if event.type() == event.Type.MouseButtonPress and event.button() == Qt.MouseButton.LeftButton:
|
|
||||||
self._start_rubber_band(event.position().toPoint())
|
|
||||||
return True
|
|
||||||
return super().eventFilter(obj, event)
|
|
||||||
|
|
||||||
def mouseMoveEvent(self, event: QMouseEvent) -> None:
|
def mousePressEvent(self, event: QMouseEvent) -> None:
|
||||||
if self._rb_origin and self._rubber_band:
|
# Clicks on viewport/flow (gaps, space below thumbs) start rubber band
|
||||||
rb_rect = QRect(self._rb_origin, event.position().toPoint()).normalized()
|
if event.button() == Qt.MouseButton.LeftButton:
|
||||||
|
child = self.childAt(event.position().toPoint())
|
||||||
|
if child is self.widget() or child is self.viewport():
|
||||||
|
self._rb_pending_origin = event.position().toPoint()
|
||||||
|
return
|
||||||
|
super().mousePressEvent(event)
|
||||||
|
|
||||||
|
def _rb_drag(self, vp_pos: QPoint) -> None:
|
||||||
|
"""Update rubber band geometry and intersected thumb selection."""
|
||||||
|
if not (self._rb_origin and self._rubber_band):
|
||||||
|
return
|
||||||
|
rb_rect = QRect(self._rb_origin, vp_pos).normalized()
|
||||||
self._rubber_band.setGeometry(rb_rect)
|
self._rubber_band.setGeometry(rb_rect)
|
||||||
# Select thumbnails that intersect the rubber band
|
|
||||||
vp_offset = self.widget().mapFrom(self.viewport(), QPoint(0, 0))
|
vp_offset = self.widget().mapFrom(self.viewport(), QPoint(0, 0))
|
||||||
self._clear_multi()
|
self._clear_multi()
|
||||||
for i, thumb in enumerate(self._thumbs):
|
for i, thumb in enumerate(self._thumbs):
|
||||||
@ -596,15 +642,42 @@ class ThumbnailGrid(QScrollArea):
|
|||||||
if rb_rect.intersects(thumb_rect):
|
if rb_rect.intersects(thumb_rect):
|
||||||
self._multi_selected.add(i)
|
self._multi_selected.add(i)
|
||||||
thumb.set_multi_selected(True)
|
thumb.set_multi_selected(True)
|
||||||
|
|
||||||
|
def _rb_end(self) -> None:
|
||||||
|
"""Hide the rubber band and clear origin."""
|
||||||
|
if self._rubber_band:
|
||||||
|
self._rubber_band.hide()
|
||||||
|
self._rb_origin = None
|
||||||
|
|
||||||
|
def _maybe_start_rb(self, vp_pos: QPoint) -> bool:
|
||||||
|
"""If a rubber band press is pending and we've moved past threshold, start it."""
|
||||||
|
if self._rb_pending_origin is None:
|
||||||
|
return False
|
||||||
|
if (vp_pos - self._rb_pending_origin).manhattanLength() < 30:
|
||||||
|
return False
|
||||||
|
self._start_rubber_band(self._rb_pending_origin)
|
||||||
|
self._rb_pending_origin = None
|
||||||
|
return True
|
||||||
|
|
||||||
|
def mouseMoveEvent(self, event: QMouseEvent) -> None:
|
||||||
|
pos = event.position().toPoint()
|
||||||
|
if self._rb_origin and self._rubber_band:
|
||||||
|
self._rb_drag(pos)
|
||||||
|
return
|
||||||
|
if self._maybe_start_rb(pos):
|
||||||
|
self._rb_drag(pos)
|
||||||
return
|
return
|
||||||
super().mouseMoveEvent(event)
|
super().mouseMoveEvent(event)
|
||||||
|
|
||||||
def mouseReleaseEvent(self, event: QMouseEvent) -> None:
|
def mouseReleaseEvent(self, event: QMouseEvent) -> None:
|
||||||
if self._rb_origin and self._rubber_band:
|
if self._rb_origin and self._rubber_band:
|
||||||
self._rubber_band.hide()
|
self._rb_end()
|
||||||
self._rb_origin = None
|
return
|
||||||
|
if self._rb_pending_origin is not None:
|
||||||
|
# Click without drag — treat as deselect
|
||||||
|
self._rb_pending_origin = None
|
||||||
|
self.clear_selection()
|
||||||
return
|
return
|
||||||
# Reset any stuck cursor from a cancelled drag-and-drop
|
|
||||||
self.unsetCursor()
|
self.unsetCursor()
|
||||||
super().mouseReleaseEvent(event)
|
super().mouseReleaseEvent(event)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user