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,451 @@
1
+ ########################################################################
2
+ ## SPINN DESIGN CODE
3
+ # WEBSITE: customwidgets.org
4
+ ########################################################################
5
+ """Virtualized (lazy) table model for DataTable Pro.
6
+
7
+ Subclasses the free core's ``QCustomDataTableModel`` so all display formatting,
8
+ alignment and the ``Qt.UserRole`` raw-value sort seam are inherited. It holds
9
+ only the rows fetched so far (in the base's ``_rows`` cache) and pulls more from
10
+ a DataProvider on demand via Qt's ``canFetchMore`` / ``fetchMore`` protocol - so
11
+ scrolling a 100k+ or streaming source loads windows lazily instead of
12
+ materialising everything up front.
13
+
14
+ QSortFilterProxyModel forwards canFetchMore/fetchMore to its source, so this
15
+ model works unchanged inside the free table's model->proxy->view chain (with
16
+ pagination disabled).
17
+ """
18
+ from qtpy.QtCore import Qt, QModelIndex, Signal
19
+ from qtpy.QtGui import QBrush, QColor, QFont
20
+
21
+ from Custom_Widgets.QCustomDataTable import QCustomDataTableModel
22
+
23
+ from .grouping import GroupingEngine, GroupRow
24
+
25
+ #: Custom item-data role: on a grouped model, returns the ``GroupRow`` for a
26
+ #: group-header row (or None for data rows). Lets the widget detect header
27
+ #: clicks and tests introspect the outline without touching internals.
28
+ GroupRole = Qt.UserRole + 1000
29
+
30
+
31
+ class VirtualDataTableModel(QCustomDataTableModel):
32
+ cellEdited = Signal(int, str, object, object) # row, key, old, new
33
+ validationFailed = Signal(int, str, object, str) # row, key, value, message
34
+
35
+ def __init__(self, columns=None, provider=None, chunkSize=200, parent=None):
36
+ super().__init__(columns, [], parent)
37
+ self._provider = None
38
+ self._total = None
39
+ self._exhausted = True
40
+ self._chunk = max(1, int(chunkSize))
41
+ self._sort = None # (column_key, ascending) pushed to the provider
42
+ self._filter = None # free-text filter pushed to the provider
43
+ self._editableKeys = set()
44
+ self._validators = {}
45
+ self._engine = None # GroupingEngine when grouping is active
46
+ self._groupBg = QColor("#f1f5f9") # neutral defaults; token-set by the
47
+ self._groupFg = QColor("#0f172a") # widget via setGroupColors()
48
+ if provider is not None:
49
+ self.setProvider(provider)
50
+
51
+ # ------------------------------------------------------------------ #
52
+ ## Inline editing
53
+ # ------------------------------------------------------------------ #
54
+ def setEditableColumns(self, keys):
55
+ self._editableKeys = set(keys or [])
56
+ if self._rows:
57
+ self.dataChanged.emit(self.index(0, 0),
58
+ self.index(self.rowCount() - 1, self.columnCount() - 1))
59
+
60
+ def editableColumns(self):
61
+ return set(self._editableKeys)
62
+
63
+ def setValidator(self, key, fn):
64
+ if fn is None:
65
+ self._validators.pop(key, None)
66
+ else:
67
+ self._validators[key] = fn
68
+
69
+ def rowCount(self, parent=QModelIndex()):
70
+ if parent.isValid():
71
+ return 0
72
+ if self._engine is not None:
73
+ return len(self._engine.visible)
74
+ return super().rowCount(parent)
75
+
76
+ def flags(self, index):
77
+ # Columns are resolved through _dataColumnAt, never by indexing
78
+ # self._columns raw: with setSelectable(True) the view gains a
79
+ # synthetic select column, shifting every data column by one and
80
+ # putting the last view column past len(self._columns). The raw
81
+ # lookup raised IndexError inside a C++ virtual call on every paint
82
+ # of that column — and an exception thrown across the binding there
83
+ # corrupts state and segfaults later, far from this line.
84
+ if not index.isValid():
85
+ return Qt.NoItemFlags
86
+ row, col_idx = index.row(), index.column()
87
+ if not (0 <= col_idx < self.columnCount()):
88
+ return Qt.NoItemFlags
89
+ col = self._dataColumnAt(col_idx) # None for select/actions
90
+ if self._engine is not None:
91
+ if not (0 <= row < len(self._engine.visible)):
92
+ return Qt.NoItemFlags
93
+ entry = self._engine.visible[row]
94
+ if isinstance(entry, GroupRow):
95
+ return Qt.ItemIsEnabled # clickable, not selectable
96
+ if col is None:
97
+ return super().flags(index) # synthetic column
98
+ base = Qt.ItemIsEnabled | Qt.ItemIsSelectable
99
+ if col.key in self._editableKeys:
100
+ return base | (Qt.ItemIsUserCheckable if col.type == "bool"
101
+ else Qt.ItemIsEditable)
102
+ return base
103
+ base = super().flags(index)
104
+ if col is None:
105
+ return base
106
+ if col.key in self._editableKeys:
107
+ if col.type == "bool":
108
+ return base | Qt.ItemIsUserCheckable
109
+ return base | Qt.ItemIsEditable
110
+ return base
111
+
112
+ def data(self, index, role=Qt.DisplayRole):
113
+ if self._engine is not None:
114
+ return self._groupedData(index, role)
115
+ if index.isValid():
116
+ col = self._dataColumnAt(index.column()) # None for select/actions
117
+ if col is not None and col.type == "bool" and col.key in self._editableKeys:
118
+ if role == Qt.CheckStateRole and 0 <= index.row() < len(self._rows):
119
+ return Qt.Checked if self._rows[index.row()].get(col.key) else Qt.Unchecked
120
+ if role in (Qt.DisplayRole, Qt.EditRole):
121
+ return "" # show only the checkbox
122
+ return super().data(index, role)
123
+
124
+ def setData(self, index, value, role=Qt.EditRole):
125
+ if not index.isValid():
126
+ return False
127
+ row, col_idx = index.row(), index.column()
128
+ col = self._dataColumnAt(col_idx) # None for select/actions
129
+ if col is None:
130
+ return False
131
+
132
+ # Resolve the target row dict (grouped: skip headers; flat: _rows[row]).
133
+ if self._engine is not None:
134
+ if not (0 <= row < len(self._engine.visible)):
135
+ return False
136
+ entry = self._engine.visible[row]
137
+ if isinstance(entry, GroupRow):
138
+ return False
139
+ record = entry
140
+ else:
141
+ if not (0 <= row < len(self._rows)):
142
+ return False
143
+ record = self._rows[row]
144
+
145
+ if col.key not in self._editableKeys:
146
+ return False
147
+
148
+ if role == Qt.CheckStateRole and col.type == "bool":
149
+ try:
150
+ new = int(value) == int(Qt.Checked)
151
+ except (TypeError, ValueError):
152
+ new = bool(value)
153
+ elif role == Qt.EditRole:
154
+ new = self._coerce(col, value)
155
+ else:
156
+ return False
157
+
158
+ # cellEdited reports the underlying (flat) row index for a stable contract.
159
+ src_row = self._rows.index(record) if record in self._rows else row
160
+
161
+ validator = self._validators.get(col.key)
162
+ if validator is not None:
163
+ result = validator(new)
164
+ if result is not True:
165
+ msg = result if isinstance(result, str) else "invalid value"
166
+ self.validationFailed.emit(src_row, col.key, new, msg)
167
+ return False
168
+
169
+ old = record.get(col.key)
170
+ if new == old:
171
+ return True
172
+ record[col.key] = new
173
+ self.dataChanged.emit(index, index,
174
+ [Qt.DisplayRole, Qt.EditRole, Qt.CheckStateRole])
175
+ self.cellEdited.emit(src_row, col.key, old, new)
176
+ if self._engine is not None:
177
+ # an edited value may change a group's aggregates; refresh headers.
178
+ self._engine.recomputeAggregates()
179
+ self.dataChanged.emit(self.index(0, 0),
180
+ self.index(self.rowCount() - 1,
181
+ self.columnCount() - 1))
182
+ return True
183
+
184
+ # ------------------------------------------------------------------ #
185
+ ## Grouping / aggregation
186
+ # ------------------------------------------------------------------ #
187
+ def setGrouping(self, keys, aggregates=None):
188
+ """Group the currently-loaded rows by ``keys`` (a list of column keys),
189
+ computing the ``aggregates`` spec ({column_key: reducer}) per group.
190
+ Pass an empty ``keys`` to clear grouping and return to the flat model.
191
+
192
+ Grouping operates on the rows loaded so far, so the caller (the widget's
193
+ groupBy) fully materialises the current sorted/filtered result first."""
194
+ keys = list(keys or [])
195
+ self.beginResetModel()
196
+ if not keys:
197
+ self._engine = None
198
+ else:
199
+ titles = {c.key: c.title for c in self._columns}
200
+ engine = GroupingEngine(keys, aggregates or {}, titles=titles)
201
+ engine.setRows(self._rows)
202
+ self._engine = engine
203
+ self.endResetModel()
204
+
205
+ def isGrouped(self):
206
+ return self._engine is not None
207
+
208
+ def groupingEngine(self):
209
+ return self._engine
210
+
211
+ def setGroupColors(self, background, foreground):
212
+ """Set the group-header background / text colours (QColor or any value
213
+ QColor accepts). The widget derives these from the design tokens."""
214
+ self._groupBg = QColor(background)
215
+ self._groupFg = QColor(foreground)
216
+ if self._engine is not None and self._engine.visible:
217
+ self.dataChanged.emit(self.index(0, 0),
218
+ self.index(self.rowCount() - 1,
219
+ self.columnCount() - 1))
220
+
221
+ def toggleGroupRow(self, row):
222
+ """Expand/collapse the group whose header is at visible ``row``. Emits
223
+ the minimal insert/remove so the view animates instead of resetting.
224
+ Returns ``(path, expanded)`` or None if ``row`` is not a header."""
225
+ if self._engine is None:
226
+ return None
227
+ vis = self._engine.visible
228
+ if not (0 <= row < len(vis)):
229
+ return None
230
+ entry = vis[row]
231
+ if not isinstance(entry, GroupRow):
232
+ return None
233
+ plan = self._engine.planToggle(entry.path)
234
+ if plan is None:
235
+ return None
236
+ kind, start, count = plan
237
+ if count <= 0:
238
+ self._engine.applyToggle(entry.path)
239
+ elif kind == "remove":
240
+ self.beginRemoveRows(QModelIndex(), start, start + count - 1)
241
+ self._engine.applyToggle(entry.path)
242
+ self.endRemoveRows()
243
+ else:
244
+ self.beginInsertRows(QModelIndex(), start, start + count - 1)
245
+ self._engine.applyToggle(entry.path)
246
+ self.endInsertRows()
247
+ self.dataChanged.emit(self.index(row, 0),
248
+ self.index(row, self.columnCount() - 1))
249
+ return (entry.path, self._engine.isExpanded(entry.path))
250
+
251
+ def setAllGroupsExpanded(self, expanded):
252
+ if self._engine is None:
253
+ return
254
+ self.beginResetModel()
255
+ self._engine.setAllExpanded(bool(expanded))
256
+ self.endResetModel()
257
+
258
+ # ----- grouped data() helpers ----------------------------------------
259
+ def _groupedData(self, index, role):
260
+ if not index.isValid():
261
+ return None
262
+ row, col_idx = index.row(), index.column()
263
+ vis = self._engine.visible
264
+ if not (0 <= row < len(vis)) or not (0 <= col_idx < self.columnCount()):
265
+ return None
266
+ entry = vis[row]
267
+ # None for the synthetic select/actions columns (see flags()).
268
+ column = self._dataColumnAt(col_idx)
269
+ if isinstance(entry, GroupRow):
270
+ return self._groupHeaderData(entry, col_idx, column, role)
271
+ return self._groupChildData(entry, col_idx, column, role)
272
+
273
+ def _groupHeaderData(self, entry, col_idx, column, role):
274
+ if role == GroupRole:
275
+ return entry
276
+ # The outline label lives in the first DATA column — with a select
277
+ # column present that is view column 1, not 0.
278
+ labelCol = self._selOffset()
279
+ if role in (Qt.DisplayRole, Qt.EditRole):
280
+ if col_idx == labelCol:
281
+ indent = " " * entry.depth
282
+ arrow = "▾ " if entry.expanded else "▸ " # down / right
283
+ value = self._displayForKey(entry.key, entry.value)
284
+ return "%s%s%s: %s (%d)" % (indent, arrow, entry.keyTitle,
285
+ value, entry.count)
286
+ if column is not None and column.key in entry.aggregates:
287
+ return self._display(column, entry.aggregates[column.key])
288
+ return ""
289
+ if role == Qt.FontRole:
290
+ font = QFont()
291
+ font.setBold(True)
292
+ return font
293
+ if role == Qt.BackgroundRole:
294
+ return QBrush(self._groupBg)
295
+ if role == Qt.ForegroundRole:
296
+ return QBrush(self._groupFg)
297
+ if role == Qt.TextAlignmentRole:
298
+ if column is None or col_idx == labelCol:
299
+ return int(Qt.AlignLeft | Qt.AlignVCenter)
300
+ align = column.align if column.align is not None else column.defaultAlign()
301
+ return int(align)
302
+ return None
303
+
304
+ def _groupChildData(self, record, col_idx, column, role):
305
+ if role == GroupRole:
306
+ return None
307
+ if column is None:
308
+ # Select/actions cells stay inert while grouped: their per-row
309
+ # state is keyed by flat row indices, which grouping reorders.
310
+ return None
311
+ value = record.get(column.key)
312
+ if column.type == "bool" and column.key in self._editableKeys:
313
+ if role == Qt.CheckStateRole:
314
+ return Qt.Checked if value else Qt.Unchecked
315
+ if role in (Qt.DisplayRole, Qt.EditRole):
316
+ return ""
317
+ if role in (Qt.DisplayRole, Qt.EditRole):
318
+ return self._display(column, value)
319
+ if role == Qt.TextAlignmentRole:
320
+ align = column.align if column.align is not None else column.defaultAlign()
321
+ return int(align)
322
+ if role == Qt.UserRole:
323
+ return value
324
+ return None
325
+
326
+ def _displayForKey(self, key, value):
327
+ for col in self._columns:
328
+ if col.key == key:
329
+ return self._display(col, value)
330
+ return "" if value is None else str(value)
331
+
332
+ @staticmethod
333
+ def _coerce(col, value):
334
+ if col.type == "number":
335
+ s = str(value).strip()
336
+ try:
337
+ return int(s) if s.lstrip("-").isdigit() else float(s)
338
+ except (TypeError, ValueError):
339
+ return value
340
+ if col.type == "bool":
341
+ return str(value).strip().lower() in ("1", "true", "yes", "y")
342
+ return str(value)
343
+
344
+ # ------------------------------------------------------------------ #
345
+ ## Provider wiring
346
+ # ------------------------------------------------------------------ #
347
+ def setProvider(self, provider):
348
+ self._provider = provider
349
+ self._reset()
350
+
351
+ def provider(self):
352
+ return self._provider
353
+
354
+ def _reset(self):
355
+ """Clear the loaded window and recompute the (filtered) total. Callers
356
+ prime the first window afterwards where a refresh is expected. Grouping
357
+ is dropped here (the row set is being rebuilt); the widget re-applies it
358
+ via _reapplyGrouping after the reload."""
359
+ self._engine = None
360
+ self.beginResetModel()
361
+ self._rows = []
362
+ self._total = (self._provider.totalRowCount(filter=self._filter)
363
+ if self._provider is not None else None)
364
+ self._exhausted = self._provider is None
365
+ self.endResetModel()
366
+
367
+ # ------------------------------------------------------------------ #
368
+ ## Server-side sort / filter push-down
369
+ # ------------------------------------------------------------------ #
370
+ def setSort(self, column, order=Qt.AscendingOrder):
371
+ """Push a sort down to the provider by column index, then reload."""
372
+ if column is None or not (0 <= column < len(self._columns)):
373
+ self._sort = None
374
+ else:
375
+ self._sort = (self._columns[column].key, order == Qt.AscendingOrder)
376
+ self._reset()
377
+ if self.canFetchMore():
378
+ self.fetchMore()
379
+
380
+ def setFilter(self, text):
381
+ """Push a free-text filter down to the provider, then reload."""
382
+ self._filter = text or None
383
+ self._reset()
384
+ if self.canFetchMore():
385
+ self.fetchMore()
386
+
387
+ def sort(self, column, order=Qt.AscendingOrder):
388
+ # so a directly-bound view's header sort also pushes down
389
+ self.setSort(column, order)
390
+
391
+ def totalRowCount(self):
392
+ """Total rows in the source (loaded or not), or the loaded count when
393
+ the total is unknown/streaming."""
394
+ return self._total if self._total is not None else len(self._rows)
395
+
396
+ def loadedRowCount(self):
397
+ return len(self._rows)
398
+
399
+ # ------------------------------------------------------------------ #
400
+ ## Qt lazy-loading protocol
401
+ # ------------------------------------------------------------------ #
402
+ def canFetchMore(self, parent=QModelIndex()):
403
+ if self._engine is not None:
404
+ return False # grouped rows are fully materialised
405
+ if parent.isValid() or self._provider is None or self._exhausted:
406
+ return False
407
+ if self._total is None: # streaming / unknown total
408
+ return True
409
+ return len(self._rows) < self._total
410
+
411
+ def fetchMore(self, parent=QModelIndex()):
412
+ if not self.canFetchMore(parent):
413
+ return
414
+ offset = len(self._rows)
415
+ batch = list(self._provider.fetch(offset, self._chunk,
416
+ sort=self._sort, filter=self._filter) or [])
417
+ if not batch:
418
+ self._exhausted = True
419
+ return
420
+ self.beginInsertRows(QModelIndex(), offset, offset + len(batch) - 1)
421
+ self._rows.extend(batch)
422
+ self.endInsertRows()
423
+ if self._total is not None and len(self._rows) >= self._total:
424
+ self._exhausted = True
425
+
426
+ def iterAllRows(self, chunk=None):
427
+ """Yield every row of the current (sorted/filtered) result in windows,
428
+ without materialising all rows or disturbing the loaded view. Used by
429
+ export - 'what you see' = the current sort + filter."""
430
+ if self._provider is None:
431
+ for row in self._rows:
432
+ yield row
433
+ return
434
+ size = chunk or self._chunk
435
+ offset = 0
436
+ while True:
437
+ batch = list(self._provider.fetch(offset, size,
438
+ sort=self._sort, filter=self._filter) or [])
439
+ if not batch:
440
+ return
441
+ for row in batch:
442
+ yield row
443
+ offset += len(batch)
444
+
445
+ def fetchAll(self, cap=None):
446
+ """Eagerly load everything (or up to ``cap`` rows). For export or small
447
+ sources; defeats virtualization, so use deliberately."""
448
+ while self.canFetchMore():
449
+ self.fetchMore()
450
+ if cap is not None and len(self._rows) >= cap:
451
+ break
@@ -0,0 +1,7 @@
1
+ """Pro-only widgets.
2
+
3
+ These ship compiled in the Pro wheel and are NOT in the free GPLv3 package.
4
+ The free package keeps a stub at the same import path so Qt Designer .ui files
5
+ that name Custom_Widgets.<Widget> still resolve, and so a free user gets an
6
+ explanation instead of ModuleNotFoundError.
7
+ """