QT-PyQt-PySide-Custom-Widgets-Pro 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- custom_widgets_pro/__init__.py +73 -0
- custom_widgets_pro/_catalog.py +25 -0
- custom_widgets_pro/_cli.py +132 -0
- custom_widgets_pro/_license.py +419 -0
- custom_widgets_pro/_version.py +14 -0
- custom_widgets_pro/datatable/__init__.py +17 -0
- custom_widgets_pro/datatable/datatable_pro.py +319 -0
- custom_widgets_pro/datatable/export.py +143 -0
- custom_widgets_pro/datatable/frozen_view.py +112 -0
- custom_widgets_pro/datatable/grouping.py +309 -0
- custom_widgets_pro/datatable/pivot.py +100 -0
- custom_widgets_pro/datatable/provider.py +98 -0
- custom_widgets_pro/datatable/virtual_model.py +451 -0
- custom_widgets_pro/widgets/__init__.py +7 -0
- custom_widgets_pro/widgets/charts/QCustomBeeswarm.py +317 -0
- custom_widgets_pro/widgets/charts/QCustomBubbleChart.py +655 -0
- custom_widgets_pro/widgets/charts/QCustomCandlestickChart.py +522 -0
- custom_widgets_pro/widgets/charts/QCustomDivergingBarChart.py +430 -0
- custom_widgets_pro/widgets/charts/QCustomDotMatrix.py +266 -0
- custom_widgets_pro/widgets/charts/QCustomFunnelChart.py +474 -0
- custom_widgets_pro/widgets/charts/QCustomGanttChart.py +376 -0
- custom_widgets_pro/widgets/charts/QCustomHeatmap.py +555 -0
- custom_widgets_pro/widgets/charts/QCustomRadarChart.py +600 -0
- custom_widgets_pro/widgets/charts/QCustomRadialBars.py +441 -0
- custom_widgets_pro/widgets/charts/QCustomRadialLines.py +554 -0
- custom_widgets_pro/widgets/charts/QCustomRangeBarChart.py +504 -0
- custom_widgets_pro/widgets/charts/QCustomSankey.py +539 -0
- custom_widgets_pro/widgets/charts/QCustomScatterChart.py +623 -0
- custom_widgets_pro/widgets/charts/__init__.py +1 -0
- custom_widgets_pro/widgets/data/QCustomCodeEditor.py +391 -0
- custom_widgets_pro/widgets/data/QCustomNodeGraph.py +1030 -0
- custom_widgets_pro/widgets/data/QCustomRichTextEditor.py +189 -0
- custom_widgets_pro/widgets/data/QCustomTableToolbar.py +566 -0
- custom_widgets_pro/widgets/data/__init__.py +1 -0
- custom_widgets_pro/widgets/media/QCustomImageViewer.py +300 -0
- custom_widgets_pro/widgets/media/QCustomMediaGrid.py +197 -0
- custom_widgets_pro/widgets/media/QCustomMediaTimeline.py +607 -0
- custom_widgets_pro/widgets/media/QCustomVideoPlayer.py +326 -0
- custom_widgets_pro/widgets/media/__init__.py +1 -0
- qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/METADATA +112 -0
- qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/RECORD +45 -0
- qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/WHEEL +5 -0
- qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/entry_points.txt +2 -0
- qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/licenses/LICENSE +91 -0
- qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
########################################################################
|
|
2
|
+
## SPINN DESIGN CODE
|
|
3
|
+
# YOUTUBE: (SPINN TV) https://www.youtube.com/@SpinnTV
|
|
4
|
+
# WEBSITE: customwidgets.org
|
|
5
|
+
########################################################################
|
|
6
|
+
|
|
7
|
+
########################################################################
|
|
8
|
+
## QCustomImageViewer - a modal lightbox for a gallery of images.
|
|
9
|
+
##
|
|
10
|
+
## A frameless overlay that covers its host window with a dark scrim and
|
|
11
|
+
## shows one image centred (aspect-fit), with painted prev / next arrows, a
|
|
12
|
+
## close button and a "3 / 12" counter. Feed it images with setImages([...])
|
|
13
|
+
## (QPixmap / path) and open a given one with openAt(index, host); it parents
|
|
14
|
+
## itself to the host and resizes to cover it. Left / Right / Esc navigate and
|
|
15
|
+
## close; clicking the scrim closes. Emits indexChanged(int) and closed(). The
|
|
16
|
+
## scrim is intentionally dark in every theme (a lightbox convention); its
|
|
17
|
+
## colours are qproperties so a form can still retint them.
|
|
18
|
+
########################################################################
|
|
19
|
+
from qtpy.QtCore import Qt, Property, Signal, QRectF, QPoint
|
|
20
|
+
from qtpy.QtGui import QColor, QPainter, QBrush, QPen, QPolygonF, QPixmap, QFont
|
|
21
|
+
from qtpy.QtCore import QPointF
|
|
22
|
+
from qtpy.QtWidgets import QWidget, QSizePolicy
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _NavButton(QWidget):
|
|
26
|
+
"""A painted circular button: kind = 'prev' | 'next' | 'close'."""
|
|
27
|
+
clicked = Signal()
|
|
28
|
+
|
|
29
|
+
def __init__(self, kind, parent=None):
|
|
30
|
+
super().__init__(parent)
|
|
31
|
+
self._kind = kind
|
|
32
|
+
self._bg = QColor(255, 255, 255, 28)
|
|
33
|
+
self._fg = QColor("#ffffff")
|
|
34
|
+
self._hover = False
|
|
35
|
+
self._d = 46 if kind != "close" else 40
|
|
36
|
+
self.setFixedSize(self._d, self._d)
|
|
37
|
+
self.setCursor(Qt.PointingHandCursor)
|
|
38
|
+
self.setAttribute(Qt.WA_Hover, True)
|
|
39
|
+
|
|
40
|
+
def setColors(self, bg, fg):
|
|
41
|
+
self._bg, self._fg = QColor(bg), QColor(fg)
|
|
42
|
+
self.update()
|
|
43
|
+
|
|
44
|
+
def enterEvent(self, e):
|
|
45
|
+
self._hover = True
|
|
46
|
+
self.update()
|
|
47
|
+
|
|
48
|
+
def leaveEvent(self, e):
|
|
49
|
+
self._hover = False
|
|
50
|
+
self.update()
|
|
51
|
+
|
|
52
|
+
def paintEvent(self, e):
|
|
53
|
+
p = QPainter(self)
|
|
54
|
+
p.setRenderHint(QPainter.Antialiasing, True)
|
|
55
|
+
p.setPen(Qt.NoPen)
|
|
56
|
+
bg = QColor(self._bg)
|
|
57
|
+
if self._hover:
|
|
58
|
+
bg = bg.lighter(140)
|
|
59
|
+
bg.setAlpha(min(255, self._bg.alpha() + 40))
|
|
60
|
+
p.setBrush(QBrush(bg))
|
|
61
|
+
p.drawEllipse(self.rect())
|
|
62
|
+
c = self._d / 2.0
|
|
63
|
+
pen = QPen(self._fg, max(2.0, self._d * 0.055))
|
|
64
|
+
pen.setCapStyle(Qt.RoundCap)
|
|
65
|
+
pen.setJoinStyle(Qt.RoundJoin)
|
|
66
|
+
p.setPen(pen)
|
|
67
|
+
p.setBrush(Qt.NoBrush)
|
|
68
|
+
s = self._d * 0.17
|
|
69
|
+
if self._kind == "close":
|
|
70
|
+
p.drawLine(QPointF(c - s, c - s), QPointF(c + s, c + s))
|
|
71
|
+
p.drawLine(QPointF(c - s, c + s), QPointF(c + s, c - s))
|
|
72
|
+
elif self._kind == "prev":
|
|
73
|
+
p.drawPolyline(QPolygonF([QPointF(c + s * 0.6, c - s),
|
|
74
|
+
QPointF(c - s * 0.6, c),
|
|
75
|
+
QPointF(c + s * 0.6, c + s)]))
|
|
76
|
+
else: # next
|
|
77
|
+
p.drawPolyline(QPolygonF([QPointF(c - s * 0.6, c - s),
|
|
78
|
+
QPointF(c + s * 0.6, c),
|
|
79
|
+
QPointF(c - s * 0.6, c + s)]))
|
|
80
|
+
p.end()
|
|
81
|
+
|
|
82
|
+
def mousePressEvent(self, e):
|
|
83
|
+
if e.button() == Qt.LeftButton:
|
|
84
|
+
self.clicked.emit()
|
|
85
|
+
e.accept()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class QCustomImageViewer(QWidget):
|
|
89
|
+
|
|
90
|
+
indexChanged = Signal(int)
|
|
91
|
+
closed = Signal()
|
|
92
|
+
|
|
93
|
+
WIDGET_ICON = "components/icons/image.png"
|
|
94
|
+
WIDGET_TOOLTIP = "A modal lightbox for a gallery of images"
|
|
95
|
+
WIDGET_MODULE = "Custom_Widgets.QCustomImageViewer"
|
|
96
|
+
WIDGET_DOM_XML = """
|
|
97
|
+
<ui language='c++'>
|
|
98
|
+
<widget class='QCustomImageViewer' name='customImageViewer'>
|
|
99
|
+
<property name='geometry'><rect><x>0</x><y>0</y><width>480</width><height>320</height></rect></property>
|
|
100
|
+
</widget>
|
|
101
|
+
</ui>
|
|
102
|
+
"""
|
|
103
|
+
__catalog__ = {
|
|
104
|
+
"name": "QCustomImageViewer",
|
|
105
|
+
"props": {
|
|
106
|
+
"scrimColor": {"type": "color", "default": "#e60a0e18"},
|
|
107
|
+
"buttonColor": {"type": "color", "default": "#1cffffff"},
|
|
108
|
+
"buttonIconColor": {"type": "color", "default": "#ffffff"},
|
|
109
|
+
"textColor": {"type": "color", "default": "#eef1f6"},
|
|
110
|
+
"imageRadius": {"type": "int", "default": 16},
|
|
111
|
+
},
|
|
112
|
+
"signals": ["indexChanged", "closed"],
|
|
113
|
+
"tokens_used": [],
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
def __init__(self, parent=None):
|
|
117
|
+
super().__init__(parent)
|
|
118
|
+
self.setObjectName("QCustomImageViewer")
|
|
119
|
+
self._pixmaps = []
|
|
120
|
+
self._index = 0
|
|
121
|
+
self._scrim = QColor(10, 12, 24, 230)
|
|
122
|
+
self._text = QColor("#eef1f6")
|
|
123
|
+
self._radius = 16
|
|
124
|
+
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
|
125
|
+
self.setAttribute(Qt.WA_StyledBackground, True)
|
|
126
|
+
|
|
127
|
+
self._prev = _NavButton("prev", self)
|
|
128
|
+
self._next = _NavButton("next", self)
|
|
129
|
+
self._close = _NavButton("close", self)
|
|
130
|
+
self._prev.clicked.connect(lambda: self.setIndex(self._index - 1))
|
|
131
|
+
self._next.clicked.connect(lambda: self.setIndex(self._index + 1))
|
|
132
|
+
self._close.clicked.connect(self.close)
|
|
133
|
+
self.hide()
|
|
134
|
+
|
|
135
|
+
# ------------------------------------------------------------------ #
|
|
136
|
+
## Data API
|
|
137
|
+
# ------------------------------------------------------------------ #
|
|
138
|
+
def setImages(self, images):
|
|
139
|
+
self._pixmaps = [im if isinstance(im, QPixmap) else QPixmap(str(im))
|
|
140
|
+
for im in (images or [])]
|
|
141
|
+
self.update()
|
|
142
|
+
|
|
143
|
+
def setImageAt(self, index, pm):
|
|
144
|
+
"""Swap one image in place (e.g. the full-res photo arriving async)."""
|
|
145
|
+
pm = pm if isinstance(pm, QPixmap) else QPixmap(str(pm))
|
|
146
|
+
if 0 <= index < len(self._pixmaps):
|
|
147
|
+
self._pixmaps[index] = pm
|
|
148
|
+
if index == self._index:
|
|
149
|
+
self.update()
|
|
150
|
+
|
|
151
|
+
def count(self):
|
|
152
|
+
return len(self._pixmaps)
|
|
153
|
+
|
|
154
|
+
def index(self):
|
|
155
|
+
return self._index
|
|
156
|
+
|
|
157
|
+
def setIndex(self, i):
|
|
158
|
+
if not self._pixmaps:
|
|
159
|
+
return
|
|
160
|
+
i = max(0, min(len(self._pixmaps) - 1, int(i)))
|
|
161
|
+
if i != self._index:
|
|
162
|
+
self._index = i
|
|
163
|
+
self.indexChanged.emit(i)
|
|
164
|
+
self._syncNav()
|
|
165
|
+
self.update()
|
|
166
|
+
|
|
167
|
+
def openAt(self, index, host=None):
|
|
168
|
+
"""Cover `host` (or the current parent) and show image `index`."""
|
|
169
|
+
if host is not None and host is not self.parent():
|
|
170
|
+
self.setParent(host)
|
|
171
|
+
parent = self.parent()
|
|
172
|
+
if parent is not None:
|
|
173
|
+
self.setGeometry(parent.rect())
|
|
174
|
+
self._index = max(0, min(max(0, len(self._pixmaps) - 1), int(index)))
|
|
175
|
+
self._syncNav()
|
|
176
|
+
self.raise_()
|
|
177
|
+
self.show()
|
|
178
|
+
self.setFocus(Qt.OtherFocusReason)
|
|
179
|
+
self.update()
|
|
180
|
+
|
|
181
|
+
# ------------------------------------------------------------------ #
|
|
182
|
+
## Layout / paint
|
|
183
|
+
# ------------------------------------------------------------------ #
|
|
184
|
+
def _syncNav(self):
|
|
185
|
+
multi = len(self._pixmaps) > 1
|
|
186
|
+
self._prev.setVisible(multi)
|
|
187
|
+
self._next.setVisible(multi)
|
|
188
|
+
|
|
189
|
+
def resizeEvent(self, e):
|
|
190
|
+
w, h = self.width(), self.height()
|
|
191
|
+
m = 22
|
|
192
|
+
self._close.move(w - self._close.width() - m, m)
|
|
193
|
+
self._prev.move(m, (h - self._prev.height()) // 2)
|
|
194
|
+
self._next.move(w - self._next.width() - m, (h - self._next.height()) // 2)
|
|
195
|
+
super().resizeEvent(e)
|
|
196
|
+
|
|
197
|
+
def _imageRect(self):
|
|
198
|
+
w, h = self.width(), self.height()
|
|
199
|
+
avail_w, avail_h = w - 200, h - 150
|
|
200
|
+
pm = self._pixmaps[self._index] if self._pixmaps else None
|
|
201
|
+
if pm is None or pm.isNull() or avail_w <= 0 or avail_h <= 0:
|
|
202
|
+
return QRectF(), None
|
|
203
|
+
scaled = pm.size().scaled(avail_w, avail_h, Qt.KeepAspectRatio)
|
|
204
|
+
x = (w - scaled.width()) / 2.0
|
|
205
|
+
y = (h - scaled.height()) / 2.0
|
|
206
|
+
return QRectF(x, y, scaled.width(), scaled.height()), pm
|
|
207
|
+
|
|
208
|
+
def paintEvent(self, e):
|
|
209
|
+
p = QPainter(self)
|
|
210
|
+
p.setRenderHint(QPainter.Antialiasing, True)
|
|
211
|
+
p.setRenderHint(QPainter.SmoothPixmapTransform, True)
|
|
212
|
+
p.fillRect(self.rect(), self._scrim)
|
|
213
|
+
rect, pm = self._imageRect()
|
|
214
|
+
if pm is not None:
|
|
215
|
+
from qtpy.QtGui import QPainterPath
|
|
216
|
+
path = QPainterPath()
|
|
217
|
+
path.addRoundedRect(rect, self._radius, self._radius)
|
|
218
|
+
p.save()
|
|
219
|
+
p.setClipPath(path)
|
|
220
|
+
p.drawPixmap(rect.toRect(), pm)
|
|
221
|
+
p.restore()
|
|
222
|
+
# counter
|
|
223
|
+
if len(self._pixmaps) > 1:
|
|
224
|
+
f = QFont(self.font())
|
|
225
|
+
f.setPointSizeF(11)
|
|
226
|
+
f.setWeight(QFont.DemiBold)
|
|
227
|
+
p.setFont(f)
|
|
228
|
+
p.setPen(QPen(self._text))
|
|
229
|
+
txt = "%d / %d" % (self._index + 1, len(self._pixmaps))
|
|
230
|
+
p.drawText(QRectF(0, 20, self.width(), 30), Qt.AlignHCenter | Qt.AlignVCenter, txt)
|
|
231
|
+
p.end()
|
|
232
|
+
|
|
233
|
+
def mousePressEvent(self, e):
|
|
234
|
+
# click on the scrim (not the image) closes
|
|
235
|
+
rect, pm = self._imageRect()
|
|
236
|
+
if pm is None or not rect.contains(e.position()):
|
|
237
|
+
self.close()
|
|
238
|
+
e.accept()
|
|
239
|
+
|
|
240
|
+
def keyPressEvent(self, e):
|
|
241
|
+
if e.key() == Qt.Key_Escape:
|
|
242
|
+
self.close()
|
|
243
|
+
elif e.key() in (Qt.Key_Left, Qt.Key_Up):
|
|
244
|
+
self.setIndex(self._index - 1)
|
|
245
|
+
elif e.key() in (Qt.Key_Right, Qt.Key_Down):
|
|
246
|
+
self.setIndex(self._index + 1)
|
|
247
|
+
else:
|
|
248
|
+
super().keyPressEvent(e)
|
|
249
|
+
|
|
250
|
+
def closeEvent(self, e):
|
|
251
|
+
self.closed.emit()
|
|
252
|
+
super().closeEvent(e)
|
|
253
|
+
|
|
254
|
+
# ------------------------------------------------------------------ #
|
|
255
|
+
## Designer properties
|
|
256
|
+
# ------------------------------------------------------------------ #
|
|
257
|
+
@Property(QColor)
|
|
258
|
+
def scrimColor(self):
|
|
259
|
+
return self._scrim
|
|
260
|
+
|
|
261
|
+
@scrimColor.setter
|
|
262
|
+
def scrimColor(self, c):
|
|
263
|
+
self._scrim = QColor(c)
|
|
264
|
+
self.update()
|
|
265
|
+
|
|
266
|
+
@Property(QColor)
|
|
267
|
+
def buttonColor(self):
|
|
268
|
+
return self._prev._bg
|
|
269
|
+
|
|
270
|
+
@buttonColor.setter
|
|
271
|
+
def buttonColor(self, c):
|
|
272
|
+
for b in (self._prev, self._next, self._close):
|
|
273
|
+
b.setColors(QColor(c), b._fg)
|
|
274
|
+
|
|
275
|
+
@Property(QColor)
|
|
276
|
+
def buttonIconColor(self):
|
|
277
|
+
return self._prev._fg
|
|
278
|
+
|
|
279
|
+
@buttonIconColor.setter
|
|
280
|
+
def buttonIconColor(self, c):
|
|
281
|
+
for b in (self._prev, self._next, self._close):
|
|
282
|
+
b.setColors(b._bg, QColor(c))
|
|
283
|
+
|
|
284
|
+
@Property(QColor)
|
|
285
|
+
def textColor(self):
|
|
286
|
+
return self._text
|
|
287
|
+
|
|
288
|
+
@textColor.setter
|
|
289
|
+
def textColor(self, c):
|
|
290
|
+
self._text = QColor(c)
|
|
291
|
+
self.update()
|
|
292
|
+
|
|
293
|
+
@Property(int)
|
|
294
|
+
def imageRadius(self):
|
|
295
|
+
return self._radius
|
|
296
|
+
|
|
297
|
+
@imageRadius.setter
|
|
298
|
+
def imageRadius(self, v):
|
|
299
|
+
self._radius = max(0, int(v))
|
|
300
|
+
self.update()
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
########################################################################
|
|
2
|
+
## SPINN DESIGN CODE
|
|
3
|
+
# YOUTUBE: (SPINN TV) https://www.youtube.com/@SpinnTV
|
|
4
|
+
# WEBSITE: customwidgets.org
|
|
5
|
+
########################################################################
|
|
6
|
+
|
|
7
|
+
########################################################################
|
|
8
|
+
## QCustomMediaGrid - a data-driven thumbnail gallery.
|
|
9
|
+
##
|
|
10
|
+
## The "Media, Files & Links" photo grid: a responsive grid of rounded
|
|
11
|
+
## thumbnails. Feed it images with setImages([...]) (QPixmap / path) or coloured
|
|
12
|
+
## placeholders with setPlaceholders([(top,bottom), …]); it lays them out in
|
|
13
|
+
## `columns` with rounded corners and emits `tileClicked(index)` so a manager
|
|
14
|
+
## can open a viewer. Layout, corner radius and spacing are Designer
|
|
15
|
+
## qproperties — the manager supplies DATA only, never builds tiles.
|
|
16
|
+
########################################################################
|
|
17
|
+
from qtpy.QtCore import Qt, Property, Signal, QSize
|
|
18
|
+
from qtpy.QtGui import QColor, QPixmap, QPainter, QPainterPath, QBrush, QLinearGradient
|
|
19
|
+
from qtpy.QtWidgets import QLabel, QWidget, QGridLayout, QSizePolicy
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _Tile(QLabel):
|
|
23
|
+
clicked = Signal(int)
|
|
24
|
+
|
|
25
|
+
def __init__(self, index, radius, parent=None):
|
|
26
|
+
super().__init__(parent)
|
|
27
|
+
self._index = index
|
|
28
|
+
self._radius = radius
|
|
29
|
+
self.setObjectName("mediaTile%d" % index)
|
|
30
|
+
self.setScaledContents(True)
|
|
31
|
+
self.setCursor(Qt.PointingHandCursor)
|
|
32
|
+
self.setMinimumSize(10, 10)
|
|
33
|
+
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
34
|
+
|
|
35
|
+
def setRoundedPixmap(self, pm):
|
|
36
|
+
self.setPixmap(_rounded(pm, self._radius))
|
|
37
|
+
|
|
38
|
+
def mousePressEvent(self, e):
|
|
39
|
+
if e.button() == Qt.LeftButton:
|
|
40
|
+
self.clicked.emit(self._index)
|
|
41
|
+
super().mousePressEvent(e)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _rounded(pm, radius):
|
|
45
|
+
if pm is None or pm.isNull():
|
|
46
|
+
return QPixmap()
|
|
47
|
+
out = QPixmap(pm.size())
|
|
48
|
+
out.setDevicePixelRatio(pm.devicePixelRatio())
|
|
49
|
+
out.fill(QColor(0, 0, 0, 0))
|
|
50
|
+
p = QPainter(out)
|
|
51
|
+
p.setRenderHint(QPainter.Antialiasing, True)
|
|
52
|
+
path = QPainterPath()
|
|
53
|
+
path.addRoundedRect(0, 0, pm.width(), pm.height(), radius, radius)
|
|
54
|
+
p.setClipPath(path)
|
|
55
|
+
p.drawPixmap(0, 0, pm)
|
|
56
|
+
p.end()
|
|
57
|
+
return out
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _gradient(top, bottom, w=220, h=150, radius=22):
|
|
61
|
+
pm = QPixmap(w, h)
|
|
62
|
+
pm.fill(QColor(0, 0, 0, 0))
|
|
63
|
+
p = QPainter(pm)
|
|
64
|
+
p.setRenderHint(QPainter.Antialiasing, True)
|
|
65
|
+
g = QLinearGradient(0, 0, w, h)
|
|
66
|
+
g.setColorAt(0.0, QColor(top))
|
|
67
|
+
g.setColorAt(1.0, QColor(bottom))
|
|
68
|
+
p.setPen(Qt.NoPen)
|
|
69
|
+
p.setBrush(QBrush(g))
|
|
70
|
+
p.drawRoundedRect(0, 0, w, h, radius, radius)
|
|
71
|
+
p.end()
|
|
72
|
+
return pm
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class QCustomMediaGrid(QWidget):
|
|
76
|
+
|
|
77
|
+
tileClicked = Signal(int)
|
|
78
|
+
|
|
79
|
+
WIDGET_ICON = "components/icons/collections.png"
|
|
80
|
+
WIDGET_TOOLTIP = "A data-driven thumbnail gallery"
|
|
81
|
+
WIDGET_MODULE = "Custom_Widgets.QCustomMediaGrid"
|
|
82
|
+
WIDGET_DOM_XML = """
|
|
83
|
+
<ui language='c++'>
|
|
84
|
+
<widget class='QCustomMediaGrid' name='customMediaGrid'>
|
|
85
|
+
<property name='geometry'><rect><x>0</x><y>0</y><width>280</width><height>200</height></rect></property>
|
|
86
|
+
</widget>
|
|
87
|
+
</ui>
|
|
88
|
+
"""
|
|
89
|
+
__catalog__ = {
|
|
90
|
+
"name": "QCustomMediaGrid",
|
|
91
|
+
"props": {"columns": {"type": "int", "default": 3},
|
|
92
|
+
"spacing": {"type": "int", "default": 8},
|
|
93
|
+
"tileRadius": {"type": "int", "default": 12},
|
|
94
|
+
"tileHeight": {"type": "int", "default": 60}},
|
|
95
|
+
"signals": ["tileClicked"],
|
|
96
|
+
"tokens_used": [],
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
def __init__(self, parent=None):
|
|
100
|
+
super().__init__(parent)
|
|
101
|
+
self.setObjectName("QCustomMediaGrid")
|
|
102
|
+
self._columns = 3
|
|
103
|
+
self._spacing = 8
|
|
104
|
+
self._radius = 12
|
|
105
|
+
self._tile_h = 60
|
|
106
|
+
self._pixmaps = []
|
|
107
|
+
self._grid = QGridLayout(self)
|
|
108
|
+
self._grid.setContentsMargins(0, 0, 0, 0)
|
|
109
|
+
self._grid.setHorizontalSpacing(self._spacing)
|
|
110
|
+
self._grid.setVerticalSpacing(self._spacing)
|
|
111
|
+
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
|
|
112
|
+
|
|
113
|
+
# ------------------------------------------------------------------ #
|
|
114
|
+
## Data API — manager supplies DATA, the widget builds the tiles.
|
|
115
|
+
# ------------------------------------------------------------------ #
|
|
116
|
+
def setImages(self, images):
|
|
117
|
+
self._pixmaps = [im if isinstance(im, QPixmap) else QPixmap(str(im)) for im in (images or [])]
|
|
118
|
+
self._rebuild()
|
|
119
|
+
|
|
120
|
+
def setPlaceholders(self, pairs):
|
|
121
|
+
self._pixmaps = [_gradient(a, b) for (a, b) in (pairs or [])]
|
|
122
|
+
self._rebuild()
|
|
123
|
+
|
|
124
|
+
def setImageAt(self, index, pm):
|
|
125
|
+
"""Swap one tile's pixmap in place (e.g. a real photo arriving async).
|
|
126
|
+
Also updates the stored pixmap so pixmaps() reflects the real image."""
|
|
127
|
+
pm = pm if isinstance(pm, QPixmap) else QPixmap(str(pm))
|
|
128
|
+
if 0 <= index < len(self._pixmaps):
|
|
129
|
+
self._pixmaps[index] = pm
|
|
130
|
+
if 0 <= index < len(self._tiles):
|
|
131
|
+
self._tiles[index].setRoundedPixmap(pm)
|
|
132
|
+
|
|
133
|
+
def count(self):
|
|
134
|
+
return len(self._pixmaps)
|
|
135
|
+
|
|
136
|
+
def pixmaps(self):
|
|
137
|
+
"""The current tile pixmaps (for feeding a lightbox / viewer)."""
|
|
138
|
+
return list(self._pixmaps)
|
|
139
|
+
|
|
140
|
+
_tiles = []
|
|
141
|
+
|
|
142
|
+
def _rebuild(self):
|
|
143
|
+
while self._grid.count():
|
|
144
|
+
it = self._grid.takeAt(0)
|
|
145
|
+
w = it.widget()
|
|
146
|
+
if w is not None:
|
|
147
|
+
w.setParent(None)
|
|
148
|
+
w.deleteLater()
|
|
149
|
+
self._tiles = []
|
|
150
|
+
for i, pm in enumerate(self._pixmaps):
|
|
151
|
+
tile = _Tile(i, self._radius, self)
|
|
152
|
+
tile.setFixedHeight(self._tile_h)
|
|
153
|
+
tile.setRoundedPixmap(pm)
|
|
154
|
+
tile.clicked.connect(self.tileClicked)
|
|
155
|
+
self._grid.addWidget(tile, i // self._columns, i % self._columns)
|
|
156
|
+
self._tiles.append(tile)
|
|
157
|
+
|
|
158
|
+
# ------------------------------------------------------------------ #
|
|
159
|
+
## Designer properties
|
|
160
|
+
# ------------------------------------------------------------------ #
|
|
161
|
+
@Property(int)
|
|
162
|
+
def columns(self):
|
|
163
|
+
return self._columns
|
|
164
|
+
|
|
165
|
+
@columns.setter
|
|
166
|
+
def columns(self, v):
|
|
167
|
+
self._columns = max(1, int(v))
|
|
168
|
+
self._rebuild()
|
|
169
|
+
|
|
170
|
+
@Property(int)
|
|
171
|
+
def spacing(self):
|
|
172
|
+
return self._spacing
|
|
173
|
+
|
|
174
|
+
@spacing.setter
|
|
175
|
+
def spacing(self, v):
|
|
176
|
+
self._spacing = max(0, int(v))
|
|
177
|
+
self._grid.setHorizontalSpacing(self._spacing)
|
|
178
|
+
self._grid.setVerticalSpacing(self._spacing)
|
|
179
|
+
|
|
180
|
+
@Property(int)
|
|
181
|
+
def tileRadius(self):
|
|
182
|
+
return self._radius
|
|
183
|
+
|
|
184
|
+
@tileRadius.setter
|
|
185
|
+
def tileRadius(self, v):
|
|
186
|
+
self._radius = max(0, int(v))
|
|
187
|
+
self._rebuild()
|
|
188
|
+
|
|
189
|
+
@Property(int)
|
|
190
|
+
def tileHeight(self):
|
|
191
|
+
return self._tile_h
|
|
192
|
+
|
|
193
|
+
@tileHeight.setter
|
|
194
|
+
def tileHeight(self, v):
|
|
195
|
+
self._tile_h = max(20, int(v))
|
|
196
|
+
for t in self._tiles:
|
|
197
|
+
t.setFixedHeight(self._tile_h)
|