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.
Files changed (45) hide show
  1. custom_widgets_pro/__init__.py +73 -0
  2. custom_widgets_pro/_catalog.py +25 -0
  3. custom_widgets_pro/_cli.py +132 -0
  4. custom_widgets_pro/_license.py +419 -0
  5. custom_widgets_pro/_version.py +14 -0
  6. custom_widgets_pro/datatable/__init__.py +17 -0
  7. custom_widgets_pro/datatable/datatable_pro.py +319 -0
  8. custom_widgets_pro/datatable/export.py +143 -0
  9. custom_widgets_pro/datatable/frozen_view.py +112 -0
  10. custom_widgets_pro/datatable/grouping.py +309 -0
  11. custom_widgets_pro/datatable/pivot.py +100 -0
  12. custom_widgets_pro/datatable/provider.py +98 -0
  13. custom_widgets_pro/datatable/virtual_model.py +451 -0
  14. custom_widgets_pro/widgets/__init__.py +7 -0
  15. custom_widgets_pro/widgets/charts/QCustomBeeswarm.py +317 -0
  16. custom_widgets_pro/widgets/charts/QCustomBubbleChart.py +655 -0
  17. custom_widgets_pro/widgets/charts/QCustomCandlestickChart.py +522 -0
  18. custom_widgets_pro/widgets/charts/QCustomDivergingBarChart.py +430 -0
  19. custom_widgets_pro/widgets/charts/QCustomDotMatrix.py +266 -0
  20. custom_widgets_pro/widgets/charts/QCustomFunnelChart.py +474 -0
  21. custom_widgets_pro/widgets/charts/QCustomGanttChart.py +376 -0
  22. custom_widgets_pro/widgets/charts/QCustomHeatmap.py +555 -0
  23. custom_widgets_pro/widgets/charts/QCustomRadarChart.py +600 -0
  24. custom_widgets_pro/widgets/charts/QCustomRadialBars.py +441 -0
  25. custom_widgets_pro/widgets/charts/QCustomRadialLines.py +554 -0
  26. custom_widgets_pro/widgets/charts/QCustomRangeBarChart.py +504 -0
  27. custom_widgets_pro/widgets/charts/QCustomSankey.py +539 -0
  28. custom_widgets_pro/widgets/charts/QCustomScatterChart.py +623 -0
  29. custom_widgets_pro/widgets/charts/__init__.py +1 -0
  30. custom_widgets_pro/widgets/data/QCustomCodeEditor.py +391 -0
  31. custom_widgets_pro/widgets/data/QCustomNodeGraph.py +1030 -0
  32. custom_widgets_pro/widgets/data/QCustomRichTextEditor.py +189 -0
  33. custom_widgets_pro/widgets/data/QCustomTableToolbar.py +566 -0
  34. custom_widgets_pro/widgets/data/__init__.py +1 -0
  35. custom_widgets_pro/widgets/media/QCustomImageViewer.py +300 -0
  36. custom_widgets_pro/widgets/media/QCustomMediaGrid.py +197 -0
  37. custom_widgets_pro/widgets/media/QCustomMediaTimeline.py +607 -0
  38. custom_widgets_pro/widgets/media/QCustomVideoPlayer.py +326 -0
  39. custom_widgets_pro/widgets/media/__init__.py +1 -0
  40. qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/METADATA +112 -0
  41. qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/RECORD +45 -0
  42. qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/WHEEL +5 -0
  43. qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/entry_points.txt +2 -0
  44. qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/licenses/LICENSE +91 -0
  45. qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,430 @@
1
+ ########################################################################
2
+ ## SPINN DESIGN CODE
3
+ # YOUTUBE: (SPINN TV) https://www.youtube.com/@SpinnTV
4
+ # WEBSITE: customwidgets.org
5
+ ########################################################################
6
+
7
+ ########################################################################
8
+ ## QCustomDivergingBarChart - a diverging (bipolar / up-down) bar chart.
9
+ ##
10
+ ## Each category is ONE column split across a zero axis: an UPWARD segment
11
+ ## (e.g. income, `upColor`) and a DOWNWARD segment (e.g. expense/expenditure,
12
+ ## `downColor`), so a single slot carries two colours and two signs. A
13
+ ## configurable `zeroGap` leaves clear space between the + and - bars around
14
+ ## the zero line (as in cash-flow dashboards). Painted (no QtCharts) so it is
15
+ ## crisp at any size and needs no toolbar/legend.
16
+ ##
17
+ ## This is the diverging sibling of QCustomMiniBarChart. Give data via
18
+ ## setData(up, down, labels) in code, or the upCsv / downCsv / labelsCsv
19
+ ## properties in Qt Designer. Optional y-axis (gridlines + prefixed/suffixed
20
+ ## value labels, e.g. "EUR 5K") and x labels underneath.
21
+ ########################################################################
22
+ from qtpy.QtCore import Qt, Property, QRectF
23
+ from qtpy.QtGui import QColor, QPainter, QBrush, QPen, QPainterPath, QFont, QFontMetrics
24
+ from qtpy.QtWidgets import QWidget, QSizePolicy
25
+
26
+
27
+ class QCustomDivergingBarChart(QWidget):
28
+
29
+ WIDGET_ICON = "components/icons/bar_chart.png"
30
+ WIDGET_TOOLTIP = "A diverging (up/down) bar chart: income up, expense down, split across a zero axis"
31
+ WIDGET_MODULE = "Custom_Widgets.QCustomDivergingBarChart"
32
+ WIDGET_DOM_XML = """
33
+ <ui language='c++'>
34
+ <widget class='QCustomDivergingBarChart' name='customDivergingBarChart'>
35
+ <property name='geometry'><rect><x>0</x><y>0</y><width>420</width><height>220</height></rect></property>
36
+ </widget>
37
+ </ui>
38
+ """
39
+ __catalog__ = {
40
+ "name": "QCustomDivergingBarChart",
41
+ "props": {"upCsv": {"type": "string", "default": "1.2,3.1,2.4,0.9,1.1,4.2,3.0,2.0"},
42
+ "downCsv": {"type": "string", "default": "0.9,1.4,2.6,0.7,0.9,1.7,1.2,1.3"},
43
+ "labelsCsv": {"type": "string", "default": ""},
44
+ "upColor": {"type": "color", "default": "#123f39"},
45
+ "downColor": {"type": "color", "default": "#34d17a"},
46
+ "barWidth": {"type": "int", "default": 12},
47
+ "cornerRadius": {"type": "int", "default": 3},
48
+ "zeroGap": {"type": "int", "default": 8},
49
+ "showAxis": {"type": "bool", "default": True},
50
+ "showLabels": {"type": "bool", "default": True},
51
+ "axisPrefix": {"type": "string", "default": ""},
52
+ "axisSuffix": {"type": "string", "default": ""},
53
+ "gridColor": {"type": "color", "default": "#e6e9ec"},
54
+ "axisTextColor": {"type": "color", "default": "#8b93a1"}},
55
+ "signals": [],
56
+ # Intentionally no tokens_used: this widget resolves no token roles.
57
+ }
58
+
59
+ # Rich editors for the Designer "Custom Properties" dock (see
60
+ # DesignerTools.CustomPropertiesDock).
61
+ DESIGNER_CUSTOM_PROPS = [
62
+ {"name": "upCsv", "kind": "str", "group": "General"},
63
+ {"name": "downCsv", "kind": "str", "group": "General"},
64
+ {"name": "labelsCsv", "kind": "str", "group": "General"},
65
+ {"name": "upColor", "kind": "color", "group": "Colors"},
66
+ {"name": "downColor", "kind": "color", "group": "Colors"},
67
+ {"name": "barWidth", "kind": "int", "group": "General"},
68
+ {"name": "cornerRadius", "kind": "int", "group": "General"},
69
+ {"name": "zeroGap", "kind": "int", "group": "General"},
70
+ {"name": "showAxis", "kind": "bool", "group": "General"},
71
+ {"name": "showLabels", "kind": "bool", "group": "General"},
72
+ {"name": "axisPrefix", "kind": "str", "group": "General"},
73
+ {"name": "axisSuffix", "kind": "str", "group": "General"},
74
+ {"name": "gridColor", "kind": "color", "group": "Colors"},
75
+ {"name": "axisTextColor", "kind": "color", "group": "Colors"},
76
+ ]
77
+
78
+ def __init__(self, parent=None, up=None, down=None, labels=None):
79
+ super().__init__(parent)
80
+ self.setObjectName("QCustomDivergingBarChart")
81
+ self._up = [float(v) for v in (up or [])]
82
+ self._down = [abs(float(v)) for v in (down or [])] # stored as magnitudes
83
+ self._labels = list(labels) if labels else []
84
+ self._up_color = QColor("#123f39")
85
+ self._down_color = QColor("#34d17a")
86
+ self._bar_width = 12
87
+ self._corner_radius = 3
88
+ self._zero_gap = 8 # px of clear space between + and - bars
89
+ self._show_axis = True
90
+ self._show_labels = True
91
+ self._axis_prefix = ""
92
+ self._axis_suffix = ""
93
+ self._grid_color = QColor("#e6e9ec")
94
+ self._axis_text_color = QColor("#8b93a1")
95
+ self._label_color = QColor("#8b93a1")
96
+ self._label_size = 11
97
+ self._axis_size = 10
98
+ self._tick_step = 0.0 # 0 = auto
99
+ self._baseline_gap = 10 # gap between chart and x labels
100
+ self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
101
+ self.setMinimumSize(120, 100)
102
+ # seed demo data so the widget previews in Designer / render_widget
103
+ # (replaced the moment setData()/upCsv is called).
104
+ if not self._up and not self._down:
105
+ self._up = [1.2, 3.1, 2.4, 0.9, 1.1, 1.3, 4.2, 4.8, 3.0, 1.6, 2.1, 3.4]
106
+ self._down = [0.9, 1.4, 2.6, 0.7, 0.9, 1.1, 1.7, 2.0, 1.2, 1.0, 1.9, 1.5]
107
+
108
+ # ------------------------------------------------------------------ #
109
+ ## Data API
110
+ # ------------------------------------------------------------------ #
111
+ def setData(self, up, down, labels=None):
112
+ """Set both series at once. `up` = upward (income) values, `down` =
113
+ downward (expense) values — either sign is accepted for `down`, its
114
+ magnitude is used."""
115
+ self._up = [float(v) for v in (up or [])]
116
+ self._down = [abs(float(v)) for v in (down or [])]
117
+ if labels is not None:
118
+ self._labels = list(labels)
119
+ self.update()
120
+
121
+ def setUpValues(self, values):
122
+ self._up = [float(v) for v in (values or [])]
123
+ self.update()
124
+
125
+ def setDownValues(self, values):
126
+ self._down = [abs(float(v)) for v in (values or [])]
127
+ self.update()
128
+
129
+ def setLabels(self, labels):
130
+ self._labels = list(labels or [])
131
+ self.update()
132
+
133
+ def setColors(self, up_color, down_color):
134
+ self._up_color = QColor(up_color)
135
+ self._down_color = QColor(down_color)
136
+ self.update()
137
+
138
+ def setTickStep(self, step):
139
+ """Axis gridline/label step in data units (0 = auto)."""
140
+ self._tick_step = max(0.0, float(step))
141
+ self.update()
142
+
143
+ def upValues(self):
144
+ return list(self._up)
145
+
146
+ def downValues(self):
147
+ return list(self._down)
148
+
149
+ # ------------------------------------------------------------------ #
150
+ ## Painting
151
+ # ------------------------------------------------------------------ #
152
+ @staticmethod
153
+ def _rounded_path(x, y, w, h, r, top=True):
154
+ """Rounded on the top edge (top=True) or the bottom edge (top=False)."""
155
+ r = max(0.0, min(r, w / 2.0, h))
156
+ p = QPainterPath()
157
+ if top:
158
+ p.moveTo(x, y + h)
159
+ p.lineTo(x, y + r)
160
+ p.quadTo(x, y, x + r, y)
161
+ p.lineTo(x + w - r, y)
162
+ p.quadTo(x + w, y, x + w, y + r)
163
+ p.lineTo(x + w, y + h)
164
+ else:
165
+ p.moveTo(x, y)
166
+ p.lineTo(x, y + h - r)
167
+ p.quadTo(x, y + h, x + r, y + h)
168
+ p.lineTo(x + w - r, y + h)
169
+ p.quadTo(x + w, y + h, x + w, y + h - r)
170
+ p.lineTo(x + w, y)
171
+ p.closeSubpath()
172
+ return p
173
+
174
+ def _nice_step(self, span):
175
+ if self._tick_step > 0:
176
+ return self._tick_step
177
+ if span <= 0:
178
+ return 1.0
179
+ raw = span / 4.0
180
+ import math
181
+ mag = 10 ** math.floor(math.log10(raw))
182
+ for m in (1, 2, 2.5, 5, 10):
183
+ if raw <= m * mag:
184
+ return m * mag
185
+ return 10 * mag
186
+
187
+ def _fmt(self, v):
188
+ txt = ("%g" % round(v, 3))
189
+ return "%s%s%s" % (self._axis_prefix, txt, self._axis_suffix)
190
+
191
+ def paintEvent(self, e):
192
+ if not self._up and not self._down:
193
+ return
194
+ p = QPainter(self)
195
+ p.setRenderHint(QPainter.Antialiasing, True)
196
+
197
+ w, h = self.width(), self.height()
198
+ n = max(len(self._up), len(self._down))
199
+ if n == 0:
200
+ p.end(); return
201
+
202
+ max_up = max(self._up) if self._up else 0.0
203
+ max_dn = max(self._down) if self._down else 0.0
204
+ if max_up <= 0 and max_dn <= 0:
205
+ p.end(); return
206
+
207
+ # x-label room at the bottom
208
+ show_x = bool(self._show_labels and self._labels)
209
+ lbl_font = QFont(self.font()); lbl_font.setPointSize(self._label_size)
210
+ lbl_h = QFontMetrics(lbl_font).height() if show_x else 0
211
+ x_room = (lbl_h + self._baseline_gap) if show_x else 0
212
+
213
+ # y-axis room on the left (widest tick label)
214
+ axis_font = QFont(self.font()); axis_font.setPointSize(self._axis_size)
215
+ afm = QFontMetrics(axis_font)
216
+ left_pad = 0
217
+ step = self._nice_step(max_up + max_dn)
218
+ if self._show_axis:
219
+ widest = max(afm.horizontalAdvance(self._fmt(max_up)),
220
+ afm.horizontalAdvance(self._fmt(-max_dn)),
221
+ afm.horizontalAdvance(self._fmt(0)))
222
+ left_pad = widest + 10
223
+
224
+ chart_x0 = left_pad
225
+ chart_w = max(1.0, w - left_pad)
226
+ chart_h = max(1.0, h - x_room)
227
+
228
+ total = (max_up + max_dn) or 1.0
229
+ gap = float(self._zero_gap)
230
+ usable = max(1.0, chart_h - gap)
231
+ up_region = usable * (max_up / total)
232
+ dn_region = usable * (max_dn / total)
233
+ zero_y = up_region + gap / 2.0 # y of the zero axis (from top)
234
+ up_base = zero_y - gap / 2.0 # up bars grow from here, upward
235
+ dn_base = zero_y + gap / 2.0 # down bars grow from here, downward
236
+
237
+ # --- axis: gridlines + value labels ------------------------------- #
238
+ if self._show_axis:
239
+ p.setFont(axis_font)
240
+ def y_of(val):
241
+ if val >= 0:
242
+ return up_base - (val / max_up) * up_region if max_up > 0 else up_base
243
+ return dn_base + (abs(val) / max_dn) * dn_region if max_dn > 0 else dn_base
244
+ # collect tick values from -max_dn..max_up on `step`, plus zero
245
+ ticks = set([0.0])
246
+ v = step
247
+ while v <= max_up + 1e-9:
248
+ ticks.add(v); v += step
249
+ v = -step
250
+ while v >= -max_dn - 1e-9:
251
+ ticks.add(v); v -= step
252
+ for tv in sorted(ticks):
253
+ gy = y_of(tv)
254
+ pen = QPen(QColor(self._grid_color), 1)
255
+ p.setPen(pen)
256
+ p.drawLine(int(chart_x0), int(gy), int(w), int(gy))
257
+ p.setPen(self._axis_text_color)
258
+ rect = QRectF(0, gy - afm.height() / 2.0, left_pad - 8, afm.height())
259
+ p.drawText(rect, Qt.AlignRight | Qt.AlignVCenter, self._fmt(tv))
260
+
261
+ # --- bars ---------------------------------------------------------- #
262
+ p.setPen(Qt.NoPen)
263
+ col_w = chart_w / n
264
+ bw = float(min(self._bar_width, col_w * 0.7))
265
+ r = float(self._corner_radius)
266
+ for i in range(n):
267
+ cx = chart_x0 + col_w * (i + 0.5)
268
+ x = cx - bw / 2.0
269
+ if i < len(self._up) and self._up[i] > 0 and max_up > 0:
270
+ uh = max(2.0, (self._up[i] / max_up) * up_region)
271
+ p.setBrush(QBrush(self._up_color))
272
+ p.drawPath(self._rounded_path(x, up_base - uh, bw, uh, r, top=True))
273
+ if i < len(self._down) and self._down[i] > 0 and max_dn > 0:
274
+ dh = max(2.0, (self._down[i] / max_dn) * dn_region)
275
+ p.setBrush(QBrush(self._down_color))
276
+ p.drawPath(self._rounded_path(x, dn_base, bw, dh, r, top=False))
277
+
278
+ # --- x labels ------------------------------------------------------ #
279
+ if show_x:
280
+ p.setFont(lbl_font)
281
+ p.setPen(self._label_color)
282
+ ly = chart_h + self._baseline_gap
283
+ for i in range(n):
284
+ if i >= len(self._labels):
285
+ break
286
+ if not str(self._labels[i]):
287
+ continue
288
+ cx = chart_x0 + col_w * (i + 0.5)
289
+ rect = QRectF(cx - col_w / 2.0, ly, col_w, lbl_h)
290
+ p.drawText(rect, Qt.AlignHCenter | Qt.AlignVCenter, str(self._labels[i]))
291
+ p.end()
292
+
293
+ # ------------------------------------------------------------------ #
294
+ ## Designer properties
295
+ # ------------------------------------------------------------------ #
296
+ @staticmethod
297
+ def _parse_floats(text):
298
+ out = []
299
+ for tok in str(text).replace(";", ",").split(","):
300
+ tok = tok.strip()
301
+ if tok:
302
+ try:
303
+ out.append(float(tok))
304
+ except ValueError:
305
+ pass
306
+ return out
307
+
308
+ @Property(str)
309
+ def upCsv(self):
310
+ return ",".join("%g" % v for v in self._up)
311
+
312
+ @upCsv.setter
313
+ def upCsv(self, text):
314
+ self.setUpValues(self._parse_floats(text))
315
+
316
+ @Property(str)
317
+ def downCsv(self):
318
+ return ",".join("%g" % v for v in self._down)
319
+
320
+ @downCsv.setter
321
+ def downCsv(self, text):
322
+ self.setDownValues(self._parse_floats(text))
323
+
324
+ @Property(str)
325
+ def labelsCsv(self):
326
+ return ",".join(str(x) for x in self._labels)
327
+
328
+ @labelsCsv.setter
329
+ def labelsCsv(self, text):
330
+ self.setLabels([t.strip() for t in str(text).replace(";", ",").split(",") if t.strip()])
331
+
332
+ @Property(QColor)
333
+ def upColor(self):
334
+ return self._up_color
335
+
336
+ @upColor.setter
337
+ def upColor(self, c):
338
+ self._up_color = QColor(c)
339
+ self.update()
340
+
341
+ @Property(QColor)
342
+ def downColor(self):
343
+ return self._down_color
344
+
345
+ @downColor.setter
346
+ def downColor(self, c):
347
+ self._down_color = QColor(c)
348
+ self.update()
349
+
350
+ @Property(int)
351
+ def barWidth(self):
352
+ return self._bar_width
353
+
354
+ @barWidth.setter
355
+ def barWidth(self, v):
356
+ self._bar_width = max(1, int(v))
357
+ self.update()
358
+
359
+ @Property(int)
360
+ def cornerRadius(self):
361
+ return self._corner_radius
362
+
363
+ @cornerRadius.setter
364
+ def cornerRadius(self, v):
365
+ self._corner_radius = max(0, int(v))
366
+ self.update()
367
+
368
+ @Property(int)
369
+ def zeroGap(self):
370
+ return self._zero_gap
371
+
372
+ @zeroGap.setter
373
+ def zeroGap(self, v):
374
+ self._zero_gap = max(0, int(v))
375
+ self.update()
376
+
377
+ @Property(bool)
378
+ def showAxis(self):
379
+ return self._show_axis
380
+
381
+ @showAxis.setter
382
+ def showAxis(self, v):
383
+ self._show_axis = bool(v)
384
+ self.update()
385
+
386
+ @Property(bool)
387
+ def showLabels(self):
388
+ return self._show_labels
389
+
390
+ @showLabels.setter
391
+ def showLabels(self, v):
392
+ self._show_labels = bool(v)
393
+ self.update()
394
+
395
+ @Property(str)
396
+ def axisPrefix(self):
397
+ return self._axis_prefix
398
+
399
+ @axisPrefix.setter
400
+ def axisPrefix(self, t):
401
+ self._axis_prefix = str(t)
402
+ self.update()
403
+
404
+ @Property(str)
405
+ def axisSuffix(self):
406
+ return self._axis_suffix
407
+
408
+ @axisSuffix.setter
409
+ def axisSuffix(self, t):
410
+ self._axis_suffix = str(t)
411
+ self.update()
412
+
413
+ @Property(QColor)
414
+ def gridColor(self):
415
+ return self._grid_color
416
+
417
+ @gridColor.setter
418
+ def gridColor(self, c):
419
+ self._grid_color = QColor(c)
420
+ self.update()
421
+
422
+ @Property(QColor)
423
+ def axisTextColor(self):
424
+ return self._axis_text_color
425
+
426
+ @axisTextColor.setter
427
+ def axisTextColor(self, c):
428
+ self._axis_text_color = QColor(c)
429
+ self._label_color = QColor(c)
430
+ self.update()
@@ -0,0 +1,266 @@
1
+ ########################################################################
2
+ ## SPINN DESIGN CODE
3
+ # YOUTUBE: (SPINN TV) https://www.youtube.com/@SpinnTV
4
+ # WEBSITE: customwidgets.org
5
+ ########################################################################
6
+
7
+ ########################################################################
8
+ ## QCustomDotMatrix - a density / category dot grid.
9
+ ##
10
+ ## A grid of small dots where each cell carries a STATE (0..N). State 0 is
11
+ ## the "empty" slot (drawn faintly with `emptyColor`); states 1..N pick a
12
+ ## colour from `colorsCsv`. Use it for the dot-matrix density panels seen on
13
+ ## modern dashboards (e.g. a "product" activity grid: valid / invalid / idle
14
+ ## dots that thicken toward one corner). Painted (no QtCharts) so it is crisp
15
+ ## at any size and needs no legend/toolbar.
16
+ ##
17
+ ## Give the grid in code with setData([[0,1,2,...], ...]) (row-major 2-D list
18
+ ## of state ints), or in Qt Designer with the `dataCsv` property (rows joined
19
+ ## by ';', cells by ','). `rows`/`cols` size an empty grid. Colours are
20
+ ## qproperties so a theme/manager can tokenise them and they flip on theme
21
+ ## switch.
22
+ ########################################################################
23
+ from qtpy.QtCore import Qt, Property, QRectF
24
+ from qtpy.QtGui import QColor, QPainter, QBrush
25
+ from qtpy.QtWidgets import QWidget, QSizePolicy
26
+
27
+
28
+ class QCustomDotMatrix(QWidget):
29
+
30
+ WIDGET_ICON = "components/icons/bar_chart.png"
31
+ WIDGET_TOOLTIP = "A density / category dot grid (valid / invalid / idle dots)"
32
+ WIDGET_MODULE = "Custom_Widgets.QCustomDotMatrix"
33
+ WIDGET_DOM_XML = """
34
+ <ui language='c++'>
35
+ <widget class='QCustomDotMatrix' name='customDotMatrix'>
36
+ <property name='geometry'><rect><x>0</x><y>0</y><width>260</width><height>120</height></rect></property>
37
+ </widget>
38
+ </ui>
39
+ """
40
+ __catalog__ = {
41
+ "name": "QCustomDotMatrix",
42
+ "props": {"dataCsv": {"type": "string", "default": ""},
43
+ "rows": {"type": "int", "default": 6},
44
+ "cols": {"type": "int", "default": 14},
45
+ "colorsCsv": {"type": "string", "default": "#8fe36b,#ffffff,#f6912b"},
46
+ "emptyColor": {"type": "color", "default": "#2c2c30"},
47
+ "dotDiameter": {"type": "int", "default": 0},
48
+ "gapRatio": {"type": "float", "default": 0.55},
49
+ "emptyOpacity": {"type": "float", "default": 0.6},
50
+ "square": {"type": "bool", "default": False}},
51
+ "signals": [],
52
+ # Intentionally no tokens_used: this widget resolves no token roles.
53
+ }
54
+
55
+ # Rich editors for the Designer "Custom Properties" dock (see
56
+ # DesignerTools.CustomPropertiesDock).
57
+ DESIGNER_CUSTOM_PROPS = [
58
+ {"name": "dataCsv", "kind": "str", "group": "General"},
59
+ {"name": "rows", "kind": "int", "group": "General"},
60
+ {"name": "cols", "kind": "int", "group": "General"},
61
+ {"name": "colorsCsv", "kind": "str", "group": "Colors"},
62
+ {"name": "emptyColor", "kind": "color", "group": "Colors"},
63
+ {"name": "dotDiameter", "kind": "int", "group": "General"},
64
+ {"name": "gapRatio", "kind": "float", "group": "General"},
65
+ {"name": "emptyOpacity", "kind": "float", "group": "General"},
66
+ {"name": "square", "kind": "bool", "group": "General"},
67
+ ]
68
+
69
+ # a deterministic demo pattern (density thickens toward the bottom-right)
70
+ _DEMO = [
71
+ [0, 0, 0, 0, 0, 0, 1, 0, 2, 0, 1, 2, 1, 2],
72
+ [0, 0, 0, 1, 0, 2, 1, 2, 1, 1, 2, 1, 2, 1],
73
+ [0, 1, 0, 2, 1, 1, 2, 1, 2, 2, 1, 2, 1, 2],
74
+ [1, 2, 1, 1, 2, 2, 1, 2, 1, 1, 2, 1, 2, 3],
75
+ [2, 1, 2, 2, 1, 1, 2, 1, 3, 2, 1, 2, 3, 2],
76
+ [1, 2, 1, 2, 3, 2, 1, 3, 2, 1, 3, 2, 1, 3],
77
+ ]
78
+
79
+ def __init__(self, parent=None, data=None):
80
+ super().__init__(parent)
81
+ self.setObjectName("QCustomDotMatrix")
82
+ self._data = [list(r) for r in data] if data else []
83
+ self._rows = 6
84
+ self._cols = 14
85
+ self._colors = [QColor("#8fe36b"), QColor("#ffffff"), QColor("#f6912b")]
86
+ self._empty = QColor("#2c2c30")
87
+ self._dot_d = 0 # 0 = auto-fit
88
+ self._gap_ratio = 0.55 # gap as a fraction of the dot diameter
89
+ self._empty_opacity = 0.6
90
+ self._square = False
91
+ self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
92
+ self.setMinimumSize(80, 48)
93
+ if not self._data:
94
+ self._data = [list(r) for r in self._DEMO]
95
+ self._rows = len(self._DEMO)
96
+ self._cols = len(self._DEMO[0])
97
+
98
+ # ------------------------------------------------------------------ #
99
+ ## Data API
100
+ # ------------------------------------------------------------------ #
101
+ def setData(self, data):
102
+ """2-D row-major list of state ints (0 = empty, 1..N = colour index)."""
103
+ self._data = [list(r) for r in (data or [])]
104
+ if self._data:
105
+ self._rows = len(self._data)
106
+ self._cols = max(len(r) for r in self._data)
107
+ self.update()
108
+
109
+ def data(self):
110
+ return [list(r) for r in self._data]
111
+
112
+ def setColors(self, colors):
113
+ self._colors = [QColor(c) for c in colors if c]
114
+ self.update()
115
+
116
+ def _grid(self):
117
+ """Normalised rows×cols grid (pads short rows with 0)."""
118
+ rows = self._data if self._data else [[0] * self._cols for _ in range(self._rows)]
119
+ cols = max((len(r) for r in rows), default=self._cols)
120
+ return [[(r[c] if c < len(r) else 0) for c in range(cols)] for r in rows], len(rows), cols
121
+
122
+ # ------------------------------------------------------------------ #
123
+ ## Painting
124
+ # ------------------------------------------------------------------ #
125
+ def paintEvent(self, e):
126
+ grid, nrows, ncols = self._grid()
127
+ if nrows == 0 or ncols == 0:
128
+ return
129
+ p = QPainter(self)
130
+ p.setRenderHint(QPainter.Antialiasing, True)
131
+ w, h = self.width(), self.height()
132
+
133
+ # cell pitch that fits both axes; dot = pitch / (1 + gapRatio)
134
+ g = max(0.0, float(self._gap_ratio))
135
+ pitch_x = w / ncols
136
+ pitch_y = h / nrows
137
+ pitch = min(pitch_x, pitch_y)
138
+ d = self._dot_d if self._dot_d > 0 else pitch / (1.0 + g)
139
+ d = max(2.0, d)
140
+ gap = pitch - d
141
+
142
+ grid_w = ncols * pitch
143
+ grid_h = nrows * pitch
144
+ ox = (w - grid_w) / 2.0
145
+ oy = (h - grid_h) / 2.0
146
+
147
+ p.setPen(Qt.NoPen)
148
+ for r in range(nrows):
149
+ for c in range(ncols):
150
+ state = grid[r][c]
151
+ if state <= 0:
152
+ col = QColor(self._empty)
153
+ col.setAlphaF(max(0.0, min(1.0, self._empty_opacity)))
154
+ else:
155
+ idx = (state - 1) % len(self._colors) if self._colors else 0
156
+ col = QColor(self._colors[idx]) if self._colors else QColor("#ffffff")
157
+ x = ox + c * pitch + gap / 2.0
158
+ y = oy + r * pitch + gap / 2.0
159
+ p.setBrush(QBrush(col))
160
+ rect = QRectF(x, y, d, d)
161
+ if self._square:
162
+ p.drawRoundedRect(rect, d * 0.28, d * 0.28)
163
+ else:
164
+ p.drawEllipse(rect)
165
+ p.end()
166
+
167
+ # ------------------------------------------------------------------ #
168
+ ## Designer properties
169
+ # ------------------------------------------------------------------ #
170
+ @Property(str)
171
+ def dataCsv(self):
172
+ return ";".join(",".join(str(int(v)) for v in row) for row in self._grid()[0])
173
+
174
+ @dataCsv.setter
175
+ def dataCsv(self, text):
176
+ rows = []
177
+ for line in str(text).replace("|", ";").split(";"):
178
+ line = line.strip()
179
+ if not line:
180
+ continue
181
+ row = []
182
+ for tok in line.split(","):
183
+ tok = tok.strip()
184
+ if not tok:
185
+ continue
186
+ try:
187
+ row.append(int(float(tok)))
188
+ except ValueError:
189
+ row.append(0)
190
+ rows.append(row)
191
+ if rows:
192
+ self.setData(rows)
193
+
194
+ @Property(int)
195
+ def rows(self):
196
+ return self._rows
197
+
198
+ @rows.setter
199
+ def rows(self, v):
200
+ self._rows = max(1, int(v))
201
+ if not self._data or len(self._data) != self._rows:
202
+ self.update()
203
+
204
+ @Property(int)
205
+ def cols(self):
206
+ return self._cols
207
+
208
+ @cols.setter
209
+ def cols(self, v):
210
+ self._cols = max(1, int(v))
211
+ self.update()
212
+
213
+ @Property(str)
214
+ def colorsCsv(self):
215
+ return ",".join(c.name() for c in self._colors)
216
+
217
+ @colorsCsv.setter
218
+ def colorsCsv(self, text):
219
+ cols = [t.strip() for t in str(text).replace(";", ",").split(",") if t.strip()]
220
+ if cols:
221
+ self.setColors(cols)
222
+
223
+ @Property(QColor)
224
+ def emptyColor(self):
225
+ return self._empty
226
+
227
+ @emptyColor.setter
228
+ def emptyColor(self, c):
229
+ self._empty = QColor(c)
230
+ self.update()
231
+
232
+ @Property(int)
233
+ def dotDiameter(self):
234
+ return self._dot_d
235
+
236
+ @dotDiameter.setter
237
+ def dotDiameter(self, v):
238
+ self._dot_d = max(0, int(v))
239
+ self.update()
240
+
241
+ @Property(float)
242
+ def gapRatio(self):
243
+ return self._gap_ratio
244
+
245
+ @gapRatio.setter
246
+ def gapRatio(self, v):
247
+ self._gap_ratio = max(0.0, float(v))
248
+ self.update()
249
+
250
+ @Property(float)
251
+ def emptyOpacity(self):
252
+ return self._empty_opacity
253
+
254
+ @emptyOpacity.setter
255
+ def emptyOpacity(self, v):
256
+ self._empty_opacity = max(0.0, min(1.0, float(v)))
257
+ self.update()
258
+
259
+ @Property(bool)
260
+ def square(self):
261
+ return self._square
262
+
263
+ @square.setter
264
+ def square(self, v):
265
+ self._square = bool(v)
266
+ self.update()