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,539 @@
1
+ ########################################################################
2
+ ## SPINN DESIGN CODE
3
+ # YOUTUBE: (SPINN TV) https://www.youtube.com/@SpinnTV
4
+ # WEBSITE: customwidgets.org
5
+ ########################################################################
6
+
7
+ ########################################################################
8
+ ## QCustomSankey - a painted Sankey flow diagram.
9
+ ##
10
+ ## Nodes in columns joined by ribbons whose thickness is the quantity flowing.
11
+ ## Energy budgets, traffic sources to conversions, spend breakdowns - anything
12
+ ## where the interesting thing is where the volume GOES, not just its size.
13
+ ##
14
+ ## The hardest of the painted charts, because nothing about the layout is
15
+ ## given: node columns, vertical order, and the stacking of each ribbon at
16
+ ## both ends all have to be derived from the link list alone.
17
+ ##
18
+ ## QPainter only, NO QtCharts (see docs/design/mui-charts-gap.md).
19
+ ##
20
+ ## Data goes in with setLinks([...]) in code, or linksCsv in Qt Designer:
21
+ ##
22
+ ## linksCsv = "Search>Signup=120;Social>Signup=80;Signup>Paid=60"
23
+ ## source > target = value, links separated by ";"
24
+ ##
25
+ ## Emits linkHovered(int), nodeHovered(str) and nodeClicked(str).
26
+ ########################################################################
27
+ from qtpy.QtCore import Qt, Signal, Property, QRectF, QSize, QPointF
28
+ from qtpy.QtGui import (QColor, QPainter, QPen, QBrush, QPainterPath,
29
+ QFontMetrics, QLinearGradient)
30
+ from qtpy.QtWidgets import QWidget, QSizePolicy
31
+
32
+
33
+ class QCustomSankey(QWidget):
34
+ linkHovered = Signal(int)
35
+ nodeHovered = Signal(str)
36
+ nodeClicked = Signal(str)
37
+
38
+ WIDGET_ICON = "components/icons/account_tree.png"
39
+ WIDGET_TOOLTIP = "A painted Sankey flow diagram"
40
+ WIDGET_MODULE = "Custom_Widgets.QCustomSankey"
41
+ WIDGET_DOM_XML = """
42
+ <ui language='c++'>
43
+ <widget class='QCustomSankey' name='customSankey'>
44
+ <property name='geometry'><rect><x>0</x><y>0</y><width>480</width><height>320</height></rect></property>
45
+ <property name='linksCsv'><string>Search&gt;Signup=120;Social&gt;Signup=80;Signup&gt;Paid=60;Signup&gt;Churn=140</string></property>
46
+ </widget>
47
+ </ui>
48
+ """
49
+ __catalog__ = {
50
+ "name": "QCustomSankey",
51
+ "props": {"linksCsv": {"type": "string", "default": ""},
52
+ "nodeColorsCsv": {"type": "string", "default": ""},
53
+ "nodeWidth": {"type": "int", "default": 14},
54
+ "nodePadding": {"type": "int", "default": 12},
55
+ "linkOpacity": {"type": "float", "default": 0.4},
56
+ "curvature": {"type": "float", "default": 0.5},
57
+ "showLabels": {"type": "bool", "default": True},
58
+ "showValues": {"type": "bool", "default": False},
59
+ "labelColor": {"type": "color", "default": "#0f172a"}},
60
+ "signals": ["linkHovered", "nodeHovered", "nodeClicked"],
61
+ "tokens_used": ["on-surface"],
62
+ }
63
+
64
+ # Rich editors for the Designer "Custom Properties" dock (see
65
+ # DesignerTools.CustomPropertiesDock).
66
+ DESIGNER_CUSTOM_PROPS = [
67
+ {"name": "linksCsv", "kind": "str", "group": "General"},
68
+ {"name": "nodeColorsCsv", "kind": "str", "group": "Colors"},
69
+ {"name": "nodeWidth", "kind": "int", "group": "General"},
70
+ {"name": "nodePadding", "kind": "int", "group": "General"},
71
+ {"name": "linkOpacity", "kind": "float", "group": "General"},
72
+ {"name": "curvature", "kind": "float", "group": "General"},
73
+ {"name": "showLabels", "kind": "bool", "group": "General"},
74
+ {"name": "showValues", "kind": "bool", "group": "General"},
75
+ {"name": "labelColor", "kind": "color", "group": "Colors"},
76
+ ]
77
+
78
+ _DEFAULT_COLORS = ["#2563eb", "#16a34a", "#f59e0b", "#dc2626", "#7c3aed",
79
+ "#0891b2", "#db2777", "#65a30d"]
80
+ _PAD = 8.0
81
+
82
+ def __init__(self, parent=None, links=None):
83
+ super().__init__(parent)
84
+ self.setObjectName("QCustomSankey")
85
+ self._links = [] # list of (source, target, value)
86
+ self._nodeColors = {}
87
+ self._nodeWidth = 14
88
+ self._nodePadding = 12
89
+ self._linkOpacity = 0.4
90
+ self._curvature = 0.5
91
+ self._showLabels = True
92
+ self._showValues = False
93
+ self._hoverLink = -1
94
+ self._hoverNode = ""
95
+ self._nodeRects = {} # name -> QRectF
96
+ self._ribbons = [] # per-link QPainterPath
97
+
98
+ self._labelColor = QColor("#0f172a")
99
+
100
+ self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
101
+ self.setMouseTracking(True)
102
+ if links:
103
+ self.setLinks(links)
104
+
105
+ # ------------------------------------------------------------------ #
106
+ ## Data
107
+ # ------------------------------------------------------------------ #
108
+ @staticmethod
109
+ def _coerce(link):
110
+ if isinstance(link, dict):
111
+ source, target, value = (link.get("source"), link.get("target"),
112
+ link.get("value"))
113
+ elif isinstance(link, (tuple, list)) and len(link) >= 3:
114
+ source, target, value = link[0], link[1], link[2]
115
+ else:
116
+ return None
117
+ try:
118
+ value = float(value)
119
+ except (TypeError, ValueError):
120
+ return None
121
+ source, target = str(source or ""), str(target or "")
122
+ if not source or not target or source == target or value <= 0:
123
+ return None
124
+ return (source, target, value)
125
+
126
+ def setLinks(self, links):
127
+ """Replace every link. Self-loops, zero flows and unnamed ends are
128
+ dropped: none of them can be laid out, and keeping them would put a
129
+ ribbon of nowhere-to-nowhere in the diagram."""
130
+ self._links = [l for l in (self._coerce(x) for x in (links or [])) if l]
131
+ self._hoverLink = -1
132
+ self._hoverNode = ""
133
+ self.update()
134
+
135
+ def links(self):
136
+ return list(self._links)
137
+
138
+ def linkCount(self):
139
+ return len(self._links)
140
+
141
+ def clearLinks(self):
142
+ self._links = []
143
+ self._hoverLink = -1
144
+ self._hoverNode = ""
145
+ self.update()
146
+
147
+ def nodes(self):
148
+ """Every node name, in first-seen order."""
149
+ seen, out = set(), []
150
+ for source, target, _v in self._links:
151
+ for name in (source, target):
152
+ if name not in seen:
153
+ seen.add(name)
154
+ out.append(name)
155
+ return out
156
+
157
+ def nodeDepth(self, name):
158
+ """Column index: the longest path from any source-only node.
159
+
160
+ Longest, not shortest — a node fed by both a 1-hop and a 3-hop path
161
+ must sit after both, or its ribbons run backwards.
162
+ """
163
+ depths = self.nodeDepths()
164
+ return depths.get(name, 0)
165
+
166
+ def nodeDepths(self):
167
+ names = self.nodes()
168
+ depths = {name: 0 for name in names}
169
+ # Relax repeatedly; bounded by node count so a cycle cannot spin.
170
+ for _round in range(len(names)):
171
+ changed = False
172
+ for source, target, _v in self._links:
173
+ candidate = depths[source] + 1
174
+ if candidate > depths[target]:
175
+ depths[target] = candidate
176
+ changed = True
177
+ if not changed:
178
+ break
179
+ return depths
180
+
181
+ def nodeValue(self, name):
182
+ """Throughput: the larger of what flows in and what flows out."""
183
+ incoming = sum(v for _s, t, v in self._links if t == name)
184
+ outgoing = sum(v for s, _t, v in self._links if s == name)
185
+ return max(incoming, outgoing)
186
+
187
+ def columns(self):
188
+ """[[node, ...], ...] left to right."""
189
+ depths = self.nodeDepths()
190
+ if not depths:
191
+ return []
192
+ out = [[] for _ in range(max(depths.values()) + 1)]
193
+ for name in self.nodes():
194
+ out[depths[name]].append(name)
195
+ return out
196
+
197
+ def nodeColor(self, name):
198
+ if name in self._nodeColors:
199
+ return QColor(self._nodeColors[name])
200
+ order = self.nodes()
201
+ index = order.index(name) if name in order else 0
202
+ return QColor(self._DEFAULT_COLORS[index % len(self._DEFAULT_COLORS)])
203
+
204
+ # ------------------------------------------------------------------ #
205
+ ## Layout
206
+ # ------------------------------------------------------------------ #
207
+ def sizeHint(self):
208
+ return QSize(480, 320)
209
+
210
+ def minimumSizeHint(self):
211
+ return QSize(180, 140)
212
+
213
+ def _plotRect(self):
214
+ """Reserve label room on BOTH sides.
215
+
216
+ Labels sit left of a node unless it is a sink, so insetting only the
217
+ right edge pushed every left-column label off-screen — "Search"
218
+ rendered as a stray "h".
219
+ """
220
+ inset = self._PAD
221
+ if self._showLabels:
222
+ fm = QFontMetrics(self.font())
223
+ widest = max((fm.horizontalAdvance(self._labelTextFor(n))
224
+ for n in self.nodes()), default=0)
225
+ inset += widest + 9
226
+ return QRectF(inset, self._PAD,
227
+ max(1.0, self.width() - 2 * inset),
228
+ max(1.0, self.height() - 2 * self._PAD))
229
+
230
+ def _labelTextFor(self, name):
231
+ if self._showValues:
232
+ return "%s %g" % (name, self.nodeValue(name))
233
+ return name
234
+
235
+ def isSink(self, name):
236
+ """True when nothing flows out of a node."""
237
+ return not any(s == name for s, _t, _v in self._links)
238
+
239
+ def _computeLayout(self):
240
+ """Node rectangles, then ribbon paths stacked at both ends."""
241
+ self._nodeRects = {}
242
+ self._ribbons = []
243
+ columns = self.columns()
244
+ if not columns:
245
+ return
246
+ rect = self._plotRect()
247
+ colCount = len(columns)
248
+ gap = ((rect.width() - self._nodeWidth) / (colCount - 1)
249
+ if colCount > 1 else 0.0)
250
+
251
+ # Scale so the busiest column exactly fills the height.
252
+ scale = None
253
+ for column in columns:
254
+ total = sum(self.nodeValue(n) for n in column)
255
+ padding = self._nodePadding * max(0, len(column) - 1)
256
+ usable = max(1.0, rect.height() - padding)
257
+ if total > 0:
258
+ candidate = usable / total
259
+ scale = candidate if scale is None else min(scale, candidate)
260
+ scale = scale or 1.0
261
+
262
+ for ci, column in enumerate(columns):
263
+ heights = [max(1.0, self.nodeValue(n) * scale) for n in column]
264
+ used = sum(heights) + self._nodePadding * max(0, len(column) - 1)
265
+ y = rect.top() + (rect.height() - used) / 2.0
266
+ x = rect.left() + ci * gap
267
+ for name, height in zip(column, heights):
268
+ self._nodeRects[name] = QRectF(x, y, float(self._nodeWidth), height)
269
+ y += height + self._nodePadding
270
+
271
+ self._computeRibbons(scale)
272
+
273
+ def _computeRibbons(self, scale):
274
+ """Stack each node's links in a stable order at both ends.
275
+
276
+ Sorting by the other end's vertical position keeps ribbons from
277
+ crossing more than the data forces them to.
278
+ """
279
+ outOffsets = {name: 0.0 for name in self._nodeRects}
280
+ inOffsets = {name: 0.0 for name in self._nodeRects}
281
+
282
+ def targetY(link):
283
+ rect = self._nodeRects.get(link[1])
284
+ return rect.top() if rect else 0.0
285
+
286
+ def sourceY(link):
287
+ rect = self._nodeRects.get(link[0])
288
+ return rect.top() if rect else 0.0
289
+
290
+ order = sorted(range(len(self._links)),
291
+ key=lambda i: (sourceY(self._links[i]),
292
+ targetY(self._links[i])))
293
+ placement = {}
294
+ for i in order:
295
+ source, target, value = self._links[i]
296
+ if source not in self._nodeRects or target not in self._nodeRects:
297
+ continue
298
+ thickness = max(1.0, value * scale)
299
+ srcRect = self._nodeRects[source]
300
+ dstRect = self._nodeRects[target]
301
+ y0 = srcRect.top() + outOffsets[source]
302
+ y1 = dstRect.top() + inOffsets[target]
303
+ outOffsets[source] += thickness
304
+ inOffsets[target] += thickness
305
+ placement[i] = (srcRect.right(), y0, dstRect.left(), y1, thickness)
306
+
307
+ self._ribbons = []
308
+ for i in range(len(self._links)):
309
+ if i not in placement:
310
+ self._ribbons.append(QPainterPath())
311
+ continue
312
+ x0, y0, x1, y1, thickness = placement[i]
313
+ self._ribbons.append(self._ribbonPath(x0, y0, x1, y1, thickness))
314
+
315
+ def _ribbonPath(self, x0, y0, x1, y1, thickness):
316
+ """A closed cubic band from (x0, y0) to (x1, y1)."""
317
+ bend = (x1 - x0) * max(0.0, min(1.0, self._curvature))
318
+ path = QPainterPath()
319
+ path.moveTo(x0, y0)
320
+ path.cubicTo(x0 + bend, y0, x1 - bend, y1, x1, y1)
321
+ path.lineTo(x1, y1 + thickness)
322
+ path.cubicTo(x1 - bend, y1 + thickness, x0 + bend, y0 + thickness,
323
+ x0, y0 + thickness)
324
+ path.closeSubpath()
325
+ return path
326
+
327
+ def nodeRects(self):
328
+ if not self._nodeRects:
329
+ self._computeLayout()
330
+ return dict(self._nodeRects)
331
+
332
+ def ribbons(self):
333
+ if not self._ribbons:
334
+ self._computeLayout()
335
+ return list(self._ribbons)
336
+
337
+ # ------------------------------------------------------------------ #
338
+ ## Painting
339
+ # ------------------------------------------------------------------ #
340
+ def paintEvent(self, e):
341
+ p = QPainter(self)
342
+ p.setRenderHint(QPainter.Antialiasing)
343
+ p.setFont(self.font())
344
+ self._computeLayout()
345
+ if not self._nodeRects:
346
+ return
347
+
348
+ for index, path in enumerate(self._ribbons):
349
+ if path.isEmpty():
350
+ continue
351
+ source, target, _value = self._links[index]
352
+ hovered = index == self._hoverLink or self._hoverNode in (source, target)
353
+ gradient = QLinearGradient(self._nodeRects[source].right(), 0,
354
+ self._nodeRects[target].left(), 0)
355
+ start = QColor(self.nodeColor(source))
356
+ stop = QColor(self.nodeColor(target))
357
+ alpha = min(1.0, self._linkOpacity * (1.8 if hovered else 1.0))
358
+ start.setAlphaF(alpha)
359
+ stop.setAlphaF(alpha)
360
+ gradient.setColorAt(0.0, start)
361
+ gradient.setColorAt(1.0, stop)
362
+ p.setPen(Qt.NoPen)
363
+ p.setBrush(QBrush(gradient))
364
+ p.drawPath(path)
365
+
366
+ for name, rect in self._nodeRects.items():
367
+ colour = self.nodeColor(name)
368
+ if name == self._hoverNode:
369
+ colour = colour.lighter(115)
370
+ p.setPen(Qt.NoPen)
371
+ p.setBrush(QBrush(colour))
372
+ p.drawRect(rect)
373
+
374
+ if self._showLabels:
375
+ self._paintLabels(p)
376
+
377
+ def _paintLabels(self, p):
378
+ fm = QFontMetrics(self.font())
379
+ p.setPen(QPen(self._labelColor))
380
+ for name, rect in self._nodeRects.items():
381
+ text = self._labelTextFor(name)
382
+ width = fm.horizontalAdvance(text)
383
+ # Sinks label to the right, everything else to the left, so a
384
+ # label never sits on top of the ribbons leaving its node. Keyed
385
+ # on "has no outgoing links", not on being in the last column: a
386
+ # dead end partway through is still a sink.
387
+ if self.isSink(name):
388
+ box = QRectF(rect.right() + 5, rect.center().y() - fm.height() / 2.0,
389
+ width + 4, fm.height())
390
+ align = Qt.AlignLeft
391
+ else:
392
+ box = QRectF(rect.left() - width - 9,
393
+ rect.center().y() - fm.height() / 2.0,
394
+ width + 4, fm.height())
395
+ align = Qt.AlignRight
396
+ p.drawText(box, int(align | Qt.AlignVCenter), text)
397
+
398
+ # ------------------------------------------------------------------ #
399
+ ## Interaction
400
+ # ------------------------------------------------------------------ #
401
+ def nodeAt(self, pos):
402
+ point = QPointF(pos)
403
+ for name, rect in self.nodeRects().items():
404
+ if rect.adjusted(-2, -2, 2, 2).contains(point):
405
+ return name
406
+ return ""
407
+
408
+ def linkAt(self, pos):
409
+ point = QPointF(pos)
410
+ for index in range(len(self._ribbons) - 1, -1, -1):
411
+ if self._ribbons[index].contains(point):
412
+ return index
413
+ return -1
414
+
415
+ def mouseMoveEvent(self, e):
416
+ node = self.nodeAt(e.pos())
417
+ link = -1 if node else self.linkAt(e.pos())
418
+ if node != self._hoverNode:
419
+ self._hoverNode = node
420
+ self.nodeHovered.emit(node)
421
+ self.update()
422
+ if link != self._hoverLink:
423
+ self._hoverLink = link
424
+ self.linkHovered.emit(link)
425
+ self.update()
426
+ super().mouseMoveEvent(e)
427
+
428
+ def leaveEvent(self, e):
429
+ if self._hoverNode:
430
+ self._hoverNode = ""
431
+ self.nodeHovered.emit("")
432
+ if self._hoverLink != -1:
433
+ self._hoverLink = -1
434
+ self.linkHovered.emit(-1)
435
+ self.update()
436
+ super().leaveEvent(e)
437
+
438
+ def mouseReleaseEvent(self, e):
439
+ if e.button() == Qt.LeftButton:
440
+ name = self.nodeAt(e.pos())
441
+ if name:
442
+ self.nodeClicked.emit(name)
443
+ super().mouseReleaseEvent(e)
444
+
445
+ # ------------------------------------------------------------------ #
446
+ ## Designer properties
447
+ # ------------------------------------------------------------------ #
448
+ @Property(str)
449
+ def linksCsv(self):
450
+ return ";".join("%s>%s=%g" % (s, t, v) for s, t, v in self._links)
451
+
452
+ @linksCsv.setter
453
+ def linksCsv(self, text):
454
+ links = []
455
+ for chunk in str(text).replace("|", ";").split(";"):
456
+ chunk = chunk.strip()
457
+ if not chunk or ">" not in chunk or "=" not in chunk:
458
+ continue
459
+ pair, _, raw = chunk.partition("=")
460
+ source, _, target = pair.partition(">")
461
+ coerced = self._coerce((source.strip(), target.strip(), raw.strip()))
462
+ if coerced:
463
+ links.append(coerced)
464
+ self.setLinks(links)
465
+
466
+ @Property(str)
467
+ def nodeColorsCsv(self):
468
+ return ",".join("%s=%s" % (name, colour.name())
469
+ for name, colour in self._nodeColors.items())
470
+
471
+ @nodeColorsCsv.setter
472
+ def nodeColorsCsv(self, text):
473
+ colors = {}
474
+ for chunk in str(text).replace(";", ",").split(","):
475
+ chunk = chunk.strip()
476
+ if "=" not in chunk:
477
+ continue
478
+ name, _, raw = chunk.partition("=")
479
+ colour = QColor(raw.strip())
480
+ if name.strip() and colour.isValid():
481
+ colors[name.strip()] = colour
482
+ self._nodeColors = colors
483
+ self.update()
484
+
485
+ @Property(int)
486
+ def nodeWidth(self):
487
+ return self._nodeWidth
488
+
489
+ @nodeWidth.setter
490
+ def nodeWidth(self, value):
491
+ self._nodeWidth = max(2, int(value)); self.update()
492
+
493
+ @Property(int)
494
+ def nodePadding(self):
495
+ return self._nodePadding
496
+
497
+ @nodePadding.setter
498
+ def nodePadding(self, value):
499
+ self._nodePadding = max(0, int(value)); self.update()
500
+
501
+ @Property(float)
502
+ def linkOpacity(self):
503
+ return self._linkOpacity
504
+
505
+ @linkOpacity.setter
506
+ def linkOpacity(self, value):
507
+ self._linkOpacity = max(0.0, min(1.0, float(value))); self.update()
508
+
509
+ @Property(float)
510
+ def curvature(self):
511
+ return self._curvature
512
+
513
+ @curvature.setter
514
+ def curvature(self, value):
515
+ self._curvature = max(0.0, min(1.0, float(value))); self.update()
516
+
517
+ @Property(bool)
518
+ def showLabels(self):
519
+ return self._showLabels
520
+
521
+ @showLabels.setter
522
+ def showLabels(self, value):
523
+ self._showLabels = bool(value); self.update()
524
+
525
+ @Property(bool)
526
+ def showValues(self):
527
+ return self._showValues
528
+
529
+ @showValues.setter
530
+ def showValues(self, value):
531
+ self._showValues = bool(value); self.update()
532
+
533
+ @Property(QColor)
534
+ def labelColor(self):
535
+ return self._labelColor
536
+
537
+ @labelColor.setter
538
+ def labelColor(self, c):
539
+ self._labelColor = QColor(c); self.update()