Skip to content

Commit 7b0d681

Browse files
committed
graphicsflowlayout: Add a GraphicsFlowLayout utility class
1 parent a12ad00 commit 7b0d681

File tree

3 files changed

+237
-0
lines changed

3 files changed

+237
-0
lines changed
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
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
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from AnyQt.QtCore import Qt, QSizeF, QRectF
2+
from AnyQt.QtWidgets import QGraphicsWidget
3+
4+
from Orange.widgets.tests.base import GuiTest
5+
from Orange.widgets.utils.graphicsflowlayout import GraphicsFlowLayout
6+
7+
8+
class TestGraphicsFlowLayout(GuiTest):
9+
def test_layout(self):
10+
layout = GraphicsFlowLayout()
11+
layout.setContentsMargins(1, 1, 1, 1)
12+
layout.setHorizontalSpacing(3)
13+
self.assertEqual(layout.horizontalSpacing(), 3)
14+
layout.setVerticalSpacing(3)
15+
self.assertEqual(layout.verticalSpacing(), 3)
16+
17+
def widget():
18+
w = QGraphicsWidget()
19+
w.setMinimumSize(QSizeF(10, 10))
20+
w.setMaximumSize(QSizeF(10, 10))
21+
return w
22+
23+
layout.addItem(widget())
24+
layout.addItem(widget())
25+
layout.addItem(widget())
26+
self.assertEqual(layout.count(), 3)
27+
sh = layout.effectiveSizeHint(Qt.PreferredSize)
28+
self.assertEqual(sh, QSizeF(30 + 6 + 2, 12))
29+
sh = layout.effectiveSizeHint(Qt.PreferredSize, QSizeF(12, -1))
30+
self.assertEqual(sh, QSizeF(12, 30 + 6 + 2))
31+
layout.setGeometry(QRectF(0, 0, sh.width(), sh.height()))
32+
w1 = layout.itemAt(0)
33+
self.assertEqual(w1.geometry(), QRectF(1, 1, 10, 10))
34+
w3 = layout.itemAt(2)
35+
self.assertEqual(w3.geometry(), QRectF(1, 1 + 2 * 10 + 2 * 3, 10, 10))
36+
37+
def test_add_remove(self):
38+
layout = GraphicsFlowLayout()
39+
layout.addItem(GraphicsFlowLayout())
40+
layout.removeAt(0)
41+
self.assertEqual(layout.count(), 0)
42+
layout.addItem(GraphicsFlowLayout())
43+
item = layout.itemAt(0)
44+
self.assertIs(item.parentLayoutItem(), layout)
45+
layout.removeItem(item)
46+
self.assertIs(item.parentLayoutItem(), None)

Orange/widgets/visualize/utils/graphicsrichtextwidget.py

Whitespace-only changes.

0 commit comments

Comments
 (0)