192 lines
6.2 KiB
Python
192 lines
6.2 KiB
Python
from PyQt6.QtWidgets import QWidget
|
|
from PyQt6.QtCore import QPoint, QTimer, Qt, QRect, QSize
|
|
from PyQt6.QtGui import QPainter, QColor, QPen
|
|
|
|
ITEM_W = 72
|
|
ITEM_H = 80
|
|
GAP = 4
|
|
|
|
|
|
RUBBER_MIN = 4 # 小于此尺寸的框选视为误触
|
|
|
|
|
|
class FlowContainer(QWidget):
|
|
"""手动绝对定位的流式容器,完全绕开 QLayout。"""
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self._widgets: list[QWidget] = []
|
|
self._pending = False
|
|
self._drag_highlight = False
|
|
self.setMouseTracking(True)
|
|
self._rubber_active = False
|
|
self._rubber_origin = QPoint()
|
|
self._rubber_rect: QRect | None = None
|
|
|
|
def add_widget(self, w: QWidget):
|
|
w.setParent(self)
|
|
self._widgets.append(w)
|
|
if self.width() > 0:
|
|
self._relayout()
|
|
else:
|
|
self._schedule_relayout()
|
|
|
|
def clear_widgets(self):
|
|
for w in self._widgets:
|
|
w.hide()
|
|
w.setParent(None)
|
|
self._widgets.clear()
|
|
self.setFixedHeight(60)
|
|
|
|
def get_widgets(self) -> list:
|
|
return list(self._widgets)
|
|
|
|
def set_drag_highlight(self, active: bool):
|
|
if self._drag_highlight != active:
|
|
self._drag_highlight = active
|
|
self.update()
|
|
|
|
def showEvent(self, event):
|
|
super().showEvent(event)
|
|
self._schedule_relayout()
|
|
|
|
def resizeEvent(self, event):
|
|
self._relayout()
|
|
super().resizeEvent(event)
|
|
|
|
def _schedule_relayout(self):
|
|
if not self._pending:
|
|
self._pending = True
|
|
QTimer.singleShot(0, self._delayed_relayout)
|
|
|
|
def _delayed_relayout(self):
|
|
self._pending = False
|
|
self._relayout()
|
|
|
|
def _relayout(self):
|
|
avail_w = self.width()
|
|
if avail_w <= 0:
|
|
p = self.parent()
|
|
while p:
|
|
if p.width() > 0:
|
|
avail_w = p.width() - 16
|
|
break
|
|
p = p.parent()
|
|
if avail_w <= 0:
|
|
self._schedule_relayout()
|
|
return
|
|
|
|
x, y = GAP, GAP
|
|
row_h = 0
|
|
|
|
for w in self._widgets:
|
|
if not w.isVisible():
|
|
w.move(-9999, -9999)
|
|
continue
|
|
iw = w.width() if w.width() > 0 else ITEM_W
|
|
ih = w.height() if w.height() > 0 else ITEM_H
|
|
if row_h > 0 and x + iw + GAP > avail_w:
|
|
x = GAP
|
|
y += row_h + GAP
|
|
row_h = 0
|
|
w.move(x, y)
|
|
x += iw + GAP
|
|
row_h = max(row_h, ih)
|
|
|
|
visible = [w for w in self._widgets if w.isVisible()]
|
|
total_h = (y + row_h + GAP) if visible else 60
|
|
self.setFixedHeight(max(total_h, 0))
|
|
|
|
def index_at(self, pos: QPoint) -> int:
|
|
"""拖拽插入位置:与流式布局顺序一致,同行每个图标单独判断,不再只认行内第一个。"""
|
|
visible = [w for w in self._widgets if w.isVisible()]
|
|
if not visible:
|
|
return 0
|
|
|
|
if pos.y() < visible[0].geometry().top():
|
|
return 0
|
|
|
|
for w in visible:
|
|
g = w.geometry()
|
|
if pos.y() < g.top():
|
|
return self._widgets.index(w)
|
|
if g.top() <= pos.y() <= g.bottom():
|
|
if pos.x() < g.left():
|
|
return self._widgets.index(w)
|
|
if pos.x() <= g.right():
|
|
mid = (g.left() + g.right()) // 2
|
|
if pos.x() < mid:
|
|
return self._widgets.index(w)
|
|
return self._widgets.index(w) + 1
|
|
# 落在该行、但在本图标右侧,继续看下一个(同行或下一行)
|
|
return len(self._widgets)
|
|
|
|
def _clear_selection_except(self, keep):
|
|
for w in self._widgets:
|
|
if w is not keep and hasattr(w, "_set_selected"):
|
|
w._set_selected(False)
|
|
|
|
def clear_selection(self):
|
|
self._clear_selection_except(None)
|
|
|
|
def _begin_rubber(self, pos: QPoint):
|
|
self.clear_selection()
|
|
self._rubber_active = True
|
|
self._rubber_origin = pos
|
|
self._rubber_rect = QRect(pos, QSize())
|
|
self.grabMouse()
|
|
self.update()
|
|
|
|
def mousePressEvent(self, event):
|
|
if event.button() == Qt.MouseButton.LeftButton:
|
|
ch = self.childAt(event.pos())
|
|
if ch is None:
|
|
self._begin_rubber(event.pos())
|
|
return
|
|
super().mousePressEvent(event)
|
|
|
|
def mouseMoveEvent(self, event):
|
|
if self._rubber_active:
|
|
self._rubber_rect = QRect(self._rubber_origin, event.pos()).normalized()
|
|
self.update()
|
|
return
|
|
super().mouseMoveEvent(event)
|
|
|
|
def mouseReleaseEvent(self, event):
|
|
if self._rubber_active and event.button() == Qt.MouseButton.LeftButton:
|
|
self._rubber_active = False
|
|
self.releaseMouse()
|
|
r = self._rubber_rect
|
|
self._rubber_rect = None
|
|
self.update()
|
|
if r is not None and r.width() >= RUBBER_MIN and r.height() >= RUBBER_MIN:
|
|
self.clear_selection()
|
|
for w in self._widgets:
|
|
if not w.isVisible():
|
|
continue
|
|
if hasattr(w, "_set_selected") and r.intersects(w.geometry()):
|
|
w._set_selected(True)
|
|
return
|
|
super().mouseReleaseEvent(event)
|
|
|
|
def paintEvent(self, event):
|
|
super().paintEvent(event)
|
|
need_drag = self._drag_highlight
|
|
need_rubber = self._rubber_rect is not None and self._rubber_active
|
|
if not need_drag and not need_rubber:
|
|
return
|
|
p = QPainter(self)
|
|
p.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
if need_drag:
|
|
pen = QPen(QColor("#4a9eff"), 2, Qt.PenStyle.DashLine)
|
|
pen.setDashPattern([6, 4])
|
|
p.setPen(pen)
|
|
p.setBrush(QColor(74, 158, 255, 25))
|
|
p.drawRoundedRect(2, 2, self.width() - 4, self.height() - 4, 6, 6)
|
|
if need_rubber:
|
|
pen = QPen(QColor("#4a9eff"), 1, Qt.PenStyle.DashLine)
|
|
p.setPen(pen)
|
|
p.setBrush(QColor(74, 158, 255, 40))
|
|
p.drawRoundedRect(self._rubber_rect, 2, 2)
|
|
p.end()
|