|
| 1 | +from functools import reduce |
| 2 | +from types import SimpleNamespace |
| 3 | +from typing import Optional, List, Iterable, Tuple |
| 4 | + |
| 5 | +import numpy as np |
| 6 | + |
| 7 | +from AnyQt.QtCore import QRectF, QSizeF, Qt, QPointF, QMarginsF |
| 8 | +from AnyQt.QtWidgets import QGraphicsLayout, QGraphicsLayoutItem |
| 9 | + |
| 10 | +import sip |
| 11 | + |
| 12 | +FLT_MAX = np.finfo(np.float32).max |
| 13 | + |
| 14 | + |
| 15 | +class _FlowLayoutItem(SimpleNamespace): |
| 16 | + item: QGraphicsLayoutItem |
| 17 | + geom: QRectF |
| 18 | + size: QSizeF |
| 19 | + row: int = 0 |
| 20 | + alignment: Qt.Alignment = 0 |
| 21 | + |
| 22 | + |
| 23 | +class GraphicsFlowLayout(QGraphicsLayout): |
| 24 | + def __init__(self, parent: Optional[QGraphicsLayoutItem] = None): |
| 25 | + self.__items: List[QGraphicsLayoutItem] = [] |
| 26 | + self.__spacing: Tuple[float, float] = (1., 1.) |
| 27 | + super().__init__(parent) |
| 28 | + sp = self.sizePolicy() |
| 29 | + sp.setHeightForWidth(True) |
| 30 | + self.setSizePolicy(sp) |
| 31 | + |
| 32 | + def setVerticalSpacing(self, spacing: float) -> None: |
| 33 | + new = (self.__spacing[0], spacing) |
| 34 | + if new != self.__spacing: |
| 35 | + self.__spacing = new |
| 36 | + self.invalidate() |
| 37 | + |
| 38 | + def verticalSpacing(self) -> float: |
| 39 | + return self.__spacing[1] |
| 40 | + |
| 41 | + def setHorizontalSpacing(self, spacing: float) -> None: |
| 42 | + new = (spacing, self.__spacing[1]) |
| 43 | + if new != self.__spacing: |
| 44 | + self.__spacing = new |
| 45 | + self.invalidate() |
| 46 | + |
| 47 | + def horizontalSpacing(self) -> float: |
| 48 | + return self.__spacing[0] |
| 49 | + |
| 50 | + def setGeometry(self, rect: QRectF) -> None: |
| 51 | + super().setGeometry(rect) |
| 52 | + margins = QMarginsF(*self.getContentsMargins()) |
| 53 | + rect = rect.marginsRemoved(margins) |
| 54 | + for item, r in zip(self.__items, self.__doLayout(rect)): |
| 55 | + item.setGeometry(r) |
| 56 | + |
| 57 | + def invalidate(self) -> None: |
| 58 | + self.updateGeometry() |
| 59 | + super().invalidate() |
| 60 | + |
| 61 | + def __doLayout(self, rect: QRectF) -> Iterable[QRectF]: |
| 62 | + x = y = 0 |
| 63 | + rowheight = 0 |
| 64 | + width = rect.width() |
| 65 | + spacing_x, spacing_y = self.__spacing |
| 66 | + first_in_row = True |
| 67 | + rows: List[List[QRectF]] = [[]] |
| 68 | + |
| 69 | + def break_(): |
| 70 | + nonlocal x, y, rowheight, first_in_row |
| 71 | + y += rowheight + spacing_y |
| 72 | + x = 0 |
| 73 | + rowheight = 0 |
| 74 | + first_in_row = True |
| 75 | + rows.append([]) |
| 76 | + |
| 77 | + items = [_FlowLayoutItem(item=item, geom=QRectF(), size=QSizeF()) |
| 78 | + for item in self.__items] |
| 79 | + |
| 80 | + for flitem in items: |
| 81 | + item = flitem.item |
| 82 | + sh = item.effectiveSizeHint(Qt.PreferredSize) |
| 83 | + if x + sh.width() > width and not first_in_row: |
| 84 | + break_() |
| 85 | + r = QRectF(rect.x() + x, rect.y() + y, sh.width(), sh.height()) |
| 86 | + flitem.geom = r |
| 87 | + flitem.size = sh |
| 88 | + flitem.row = len(rows) - 1 |
| 89 | + rowheight = max(rowheight, sh.height()) |
| 90 | + x += sh.width() + spacing_x |
| 91 | + first_in_row = False |
| 92 | + rows[-1].append(flitem.geom) |
| 93 | + |
| 94 | + alignment = Qt.AlignVCenter | Qt.AlignLeft |
| 95 | + for flitem in items: |
| 96 | + row = rows[flitem.row] |
| 97 | + row_rect = reduce(QRectF.united, row, QRectF()) |
| 98 | + if row_rect.isEmpty(): |
| 99 | + continue |
| 100 | + flitem.geom = qrect_aligned_to( |
| 101 | + flitem.geom, row_rect, alignment & Qt.AlignVertical_Mask) |
| 102 | + return [fli.geom for fli in items] |
| 103 | + |
| 104 | + def sizeHint(self, which: Qt.SizeHint, constraint=QSizeF(-1, -1)) -> QSizeF: |
| 105 | + left, top, right, bottom = self.getContentsMargins() |
| 106 | + extra_margins = QSizeF(left + right, top + bottom) |
| 107 | + if constraint.width() >= 0: |
| 108 | + constraint.setWidth( |
| 109 | + max(constraint.width() - extra_margins.width(), 0.0)) |
| 110 | + |
| 111 | + if which == Qt.PreferredSize: |
| 112 | + if constraint.width() >= 0: |
| 113 | + rect = QRectF(0, 0, constraint.width(), FLT_MAX) |
| 114 | + else: |
| 115 | + rect = QRectF(0, 0, FLT_MAX, FLT_MAX) |
| 116 | + res = self.__doLayout(rect) |
| 117 | + sh = reduce(QRectF.united, res, QRectF()).size() |
| 118 | + return sh + extra_margins |
| 119 | + if which == Qt.MinimumSize: |
| 120 | + return reduce(QSizeF.expandedTo, |
| 121 | + (item.minimumSize() for item in self.__items), |
| 122 | + QSizeF()) + extra_margins |
| 123 | + return QSizeF() |
| 124 | + |
| 125 | + def count(self) -> int: |
| 126 | + return len(self.__items) |
| 127 | + |
| 128 | + def itemAt(self, i: int) -> QGraphicsLayoutItem: |
| 129 | + try: |
| 130 | + return self.__items[i] |
| 131 | + except IndexError: |
| 132 | + return None # type: ignore |
| 133 | + |
| 134 | + def removeAt(self, index: int) -> None: |
| 135 | + try: |
| 136 | + item = self.__items.pop(index) |
| 137 | + except IndexError: |
| 138 | + pass |
| 139 | + else: |
| 140 | + item.setParentLayoutItem(None) |
| 141 | + self.invalidate() |
| 142 | + |
| 143 | + def removeItem(self, item: QGraphicsLayoutItem): |
| 144 | + try: |
| 145 | + self.__items.remove(item) |
| 146 | + except ValueError: |
| 147 | + pass |
| 148 | + else: |
| 149 | + item.setParentLayoutItem(None) |
| 150 | + self.invalidate() |
| 151 | + |
| 152 | + def addItem(self, item: QGraphicsLayoutItem) -> None: |
| 153 | + self.insertItem(self.count(), item) |
| 154 | + |
| 155 | + def insertItem(self, index: int, item: QGraphicsLayoutItem, ) -> None: |
| 156 | + self.addChildLayoutItem(item) |
| 157 | + if 0 <= index < self.count(): |
| 158 | + self.__items.insert(index, item) |
| 159 | + else: |
| 160 | + self.__items.append(item) |
| 161 | + self.updateGeometry() |
| 162 | + self.invalidate() |
| 163 | + |
| 164 | + def __dtor__(self): |
| 165 | + items = self.__items |
| 166 | + self.__items = [] |
| 167 | + for item in items: |
| 168 | + item.setParentLayoutItem(None) |
| 169 | + if item.ownedByLayout(): |
| 170 | + sip.delete(item) |
| 171 | + |
| 172 | + |
| 173 | +def qrect_aligned_to( |
| 174 | + rect_a: QRectF, rect_b: QRectF, alignment: Qt.Alignment) -> QRectF: |
| 175 | + res = QRectF(rect_a) |
| 176 | + valign = alignment & Qt.AlignVertical_Mask |
| 177 | + halign = alignment & Qt.AlignHorizontal_Mask |
| 178 | + if valign == Qt.AlignTop: |
| 179 | + res.moveTop(rect_b.top()) |
| 180 | + if valign == Qt.AlignVCenter: |
| 181 | + res.moveCenter(QPointF(res.center().x(), rect_b.center().y())) |
| 182 | + if valign == Qt.AlignBottom: |
| 183 | + res.moveBottom(rect_b.bottom()) |
| 184 | + |
| 185 | + if halign == Qt.AlignLeft: |
| 186 | + res.moveLeft(rect_b.left()) |
| 187 | + if halign == Qt.AlignHCenter: |
| 188 | + res.moveCenter(QPointF(rect_b.center().x(), res.center().y())) |
| 189 | + if halign == Qt.AlignRight: |
| 190 | + res.moveRight(rect_b.right()) |
| 191 | + return res |
0 commit comments