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,319 @@
1
+ ########################################################################
2
+ ## SPINN DESIGN CODE
3
+ # WEBSITE: customwidgets.org
4
+ ########################################################################
5
+ """QCustomDataTablePro - the commercial data grid.
6
+
7
+ Extends the free-core ``QCustomDataTable`` through its stable Pro contract:
8
+ the ``_createModel`` / ``_createView`` factory seams and the model's
9
+ ``Qt.UserRole`` raw-value seam. Free examples/tutorials transfer directly -
10
+ "swap the class, gain the features".
11
+
12
+ SCAFFOLD: this currently inherits the free table's behaviour. The Pro
13
+ capabilities (virtualization, frozen columns, grouping/pivot, inline editing,
14
+ server-side loading, CSV/XLSX export) are implemented on the seams below. See
15
+ the free-core repo: docs/design/datatable-pro-spec.md.
16
+
17
+ Contains ONLY original code - no bundled third-party assets. Export uses the
18
+ Python stdlib (csv, zipfile+xml for XLSX); indicators are painted, not shipped.
19
+ """
20
+ from qtpy.QtCore import Qt, Signal
21
+ from qtpy.QtGui import QColor, QPalette
22
+
23
+ from Custom_Widgets.QCustomDataTable import QCustomDataTable
24
+ from qtpy.QtWidgets import QAbstractItemView
25
+
26
+ from .._catalog import register
27
+ from .virtual_model import VirtualDataTableModel, GroupRole
28
+ from .provider import DataProvider, ListDataProvider, CallableDataProvider
29
+ from .frozen_view import FrozenHostView
30
+
31
+
32
+ @register
33
+ class QCustomDataTablePro(QCustomDataTable):
34
+ cellEdited = Signal(int, str, object, object) # row, key, old, new
35
+ validationFailed = Signal(int, str, object, str) # row, key, value, message
36
+ groupToggled = Signal(object, bool) # group path, expanded
37
+
38
+ __catalog__ = {
39
+ "name": "QCustomDataTablePro",
40
+ "extends": "QCustomDataTable",
41
+ "edition": "pro",
42
+ "props": {
43
+ "virtualized": {"type": "bool", "default": True},
44
+ "editable": {"type": "bool", "default": False},
45
+ "pinnedColumns": {"type": "list", "default": []},
46
+ "groupBy": {"type": "list", "default": []},
47
+ },
48
+ "signals": ["cellEdited", "groupToggled"],
49
+ "capabilities": ["virtualization", "frozen-columns", "grouping",
50
+ "pivot", "inline-edit", "server-side", "export-csv",
51
+ "export-xlsx"],
52
+ }
53
+
54
+ def __init__(self, parent=None, chunkSize=200):
55
+ self._chunkSize = int(chunkSize)
56
+ self._sortCol = -1
57
+ self._sortAsc = True
58
+ self._groupKeys = []
59
+ self._aggregates = {}
60
+ self._prePivot = None # (columns, provider) snapshot before pivot
61
+ self._pivotActive = False
62
+ super().__init__(parent)
63
+ # Virtualization: disable pagination so the view fetches lazily from the
64
+ # model (the sort/filter proxy forwards canFetchMore/fetchMore).
65
+ self.showPagination = False
66
+ # The view's built-in (client-side) sort stays OFF - it would only order
67
+ # the loaded window. Sort/filter are pushed DOWN to the provider instead;
68
+ # we drive the header ourselves so the arrow still works.
69
+ self.sortable = False
70
+ header = self._view.horizontalHeader()
71
+ header.setSectionsClickable(True)
72
+ header.setSortIndicatorShown(True)
73
+ header.sectionClicked.connect(self._onHeaderSortPushdown)
74
+
75
+ # inline editing: enable triggers on the view and re-emit model edits
76
+ self._view.setEditTriggers(QAbstractItemView.DoubleClicked
77
+ | QAbstractItemView.EditKeyPressed
78
+ | QAbstractItemView.AnyKeyPressed)
79
+ self._model.cellEdited.connect(self.cellEdited)
80
+ self._model.validationFailed.connect(self.validationFailed)
81
+
82
+ # grouping: clicking a group-header row toggles expand/collapse
83
+ self._view.clicked.connect(self._onProClicked)
84
+
85
+ # -- Server-side sort / filter (push-down) ------------------------------
86
+ def _onHeaderSortPushdown(self, column):
87
+ if column == self._sortCol:
88
+ self._sortAsc = not self._sortAsc
89
+ else:
90
+ self._sortCol, self._sortAsc = column, True
91
+ self.sortBy(column, Qt.AscendingOrder if self._sortAsc else Qt.DescendingOrder)
92
+
93
+ def sortBy(self, column, order=Qt.AscendingOrder):
94
+ """Push a sort down to the provider (overrides the free client-side sort)."""
95
+ self._sortCol = column
96
+ self._sortAsc = (order == Qt.AscendingOrder)
97
+ self._view.horizontalHeader().setSortIndicator(column, order)
98
+ self._model.setSort(column, order)
99
+ self._reapplyGrouping()
100
+ self.sortChanged.emit(column, order)
101
+
102
+ def setFilterText(self, text):
103
+ """Push a free-text filter down to the provider (overrides the free
104
+ client-side proxy filter)."""
105
+ self._model.setFilter(text)
106
+ self._reapplyGrouping()
107
+
108
+ def _reapplyGrouping(self):
109
+ """After a sort/filter reload, re-materialise and re-group so the
110
+ outline reflects the new result set."""
111
+ if self._groupKeys:
112
+ self._model.fetchAll()
113
+ self._model.setGrouping(self._groupKeys, self._aggregates)
114
+
115
+ # -- Pro extension seams (override the free factories) ------------------
116
+ def _createModel(self):
117
+ return VirtualDataTableModel(chunkSize=getattr(self, "_chunkSize", 200),
118
+ parent=self)
119
+
120
+ def _createView(self):
121
+ return FrozenHostView(self)
122
+
123
+ # -- Data API (virtualized) --------------------------------------------
124
+ def setDataProvider(self, provider):
125
+ """Attach a lazy DataProvider (row windows + optional total). Primes the
126
+ first window so the view shows content immediately; scrolling loads the
127
+ rest."""
128
+ if not isinstance(provider, DataProvider):
129
+ raise TypeError("expected a DataProvider, got %r" % type(provider))
130
+ self._model.setProvider(provider)
131
+ if self._model.canFetchMore():
132
+ self._model.fetchMore()
133
+
134
+ def setData(self, rows):
135
+ """Feed an in-memory list through the virtualized path."""
136
+ self.setDataProvider(ListDataProvider(rows))
137
+
138
+ setRows = setData
139
+
140
+ def setFetchCallback(self, fetch_fn, total=None):
141
+ """Convenience: virtualize over ``fetch_fn(offset, limit) -> list``."""
142
+ self.setDataProvider(CallableDataProvider(fetch_fn, total=total))
143
+
144
+ def loadedRowCount(self):
145
+ return self._model.loadedRowCount()
146
+
147
+ def totalRowCount(self):
148
+ return self._model.totalRowCount()
149
+
150
+ # -- Pro API surface (further stubs; per datatable-pro-spec.md) ---------
151
+ def setFrozenColumnCount(self, count):
152
+ """Freeze (pin) the leftmost `count` columns so they stay visible while
153
+ the rest scroll horizontally."""
154
+ self._view.setFrozenColumnCount(count)
155
+
156
+ def frozenColumnCount(self):
157
+ return self._view.frozenColumnCount()
158
+
159
+ def pinColumns(self, keys):
160
+ """Pin the columns for `keys`. Pinned columns are the leftmost ones, so
161
+ this freezes every column up to (and including) the right-most key."""
162
+ cols = self._model.columns()
163
+ index = {c.key: i for i, c in enumerate(cols)}
164
+ indices = [index[k] for k in keys if k in index]
165
+ self.setFrozenColumnCount(max(indices) + 1 if indices else 0)
166
+
167
+ # -- Grouping / aggregation --------------------------------------------
168
+ def groupBy(self, keys, aggregates=None):
169
+ """Group rows by one or more column ``keys`` (outer -> inner), with an
170
+ optional ``aggregates`` spec ({column_key: reducer}) shown on each group
171
+ header. ``reducer`` is a name (sum/avg/count/min/max/first/last) or a
172
+ callable ``fn(values) -> result``.
173
+
174
+ Grouping aggregates over the whole current result, so this materialises
175
+ the current sorted/filtered rows (leaving virtualization only while
176
+ grouped); ``groupBy([])`` clears grouping and restores lazy loading."""
177
+ keys = list(keys or [])
178
+ if self._pivotActive:
179
+ self.clearPivot() # grouping and pivot are exclusive
180
+ # aggregate spec may also be declared on the columns (col.aggregate).
181
+ agg = dict(aggregates or {})
182
+ for col in self._model.columns():
183
+ spec = getattr(col, "aggregate", None)
184
+ if spec is not None and col.key not in agg:
185
+ agg[col.key] = spec
186
+ self._groupKeys = keys
187
+ self._aggregates = agg
188
+ if keys:
189
+ self._model.fetchAll()
190
+ self._applyGroupColors()
191
+ self._model.setGrouping(keys, agg)
192
+
193
+ def clearGrouping(self):
194
+ """Remove grouping and return to the flat, virtualized view."""
195
+ self.groupBy([])
196
+
197
+ def groupKeys(self):
198
+ return list(self._groupKeys)
199
+
200
+ def isGrouped(self):
201
+ return self._model.isGrouped()
202
+
203
+ def expandAllGroups(self):
204
+ self._model.setAllGroupsExpanded(True)
205
+
206
+ def collapseAllGroups(self):
207
+ self._model.setAllGroupsExpanded(False)
208
+
209
+ def toggleGroup(self, row):
210
+ """Toggle the group header at source ``row`` (as returned by GroupRole).
211
+ Prefer clicking; this is the programmatic hook."""
212
+ result = self._model.toggleGroupRow(row)
213
+ if result is not None:
214
+ self.groupToggled.emit(*result)
215
+ return result
216
+
217
+ def _onProClicked(self, viewIndex):
218
+ if not self._model.isGrouped():
219
+ return
220
+ src = self._sortFilter.mapToSource(viewIndex)
221
+ header = self._model.data(self._model.index(src.row(), 0), GroupRole)
222
+ if header is not None:
223
+ self.toggleGroup(src.row())
224
+
225
+ def _applyGroupColors(self):
226
+ """Derive group-header colours from the design tokens, following the
227
+ widget's light/dark palette."""
228
+ try:
229
+ from Custom_Widgets.JSonStyles.tokens import DesignTokens
230
+ except Exception:
231
+ return
232
+ base = self.palette().color(QPalette.Base)
233
+ theme = "dark" if base.lightness() < 128 else "light"
234
+ tokens = DesignTokens(theme=theme)
235
+ self._model.setGroupColors(QColor(tokens.role("surface-muted")),
236
+ QColor(tokens.role("on-surface")))
237
+
238
+ # -- Pivot (cross-tab) -------------------------------------------------
239
+ def pivot(self, index, columns, values, aggfunc="sum", totals=True,
240
+ fill=None):
241
+ """Reshape the current result into a pivot (cross-tab) table.
242
+
243
+ index row-dimension key (or list of keys) - one output row per
244
+ distinct combination.
245
+ columns a single key whose distinct values become output columns.
246
+ values the key aggregated into each cell.
247
+ aggfunc reducer name (sum/avg/count/min/max/first/last) or a callable.
248
+ totals add a trailing per-row total column.
249
+
250
+ Snapshots the pre-pivot table so clearPivot() restores it. Pivot and
251
+ grouping are exclusive."""
252
+ from .pivot import pivot_table
253
+ if self._pivotActive:
254
+ self.clearPivot() # always pivot from the flat source
255
+ if self._groupKeys:
256
+ self.groupBy([]) # drop grouping first
257
+ self._model.fetchAll() # materialise current sort/filter
258
+ source_rows = self._model.rows()
259
+ self._prePivot = (self._model.columns(), self._model.provider())
260
+
261
+ col_defs, result = pivot_table(source_rows, index, columns, values,
262
+ agg=aggfunc, totals=totals, fill=fill)
263
+ # keep the index columns' original titles / types
264
+ original = {c.key: c for c in self._prePivot[0]}
265
+ for cd in col_defs:
266
+ src = original.get(cd["key"])
267
+ if src is not None:
268
+ cd["title"] = src.title
269
+ cd["type"] = src.type
270
+ self._pivotActive = True
271
+ self.setColumns(col_defs)
272
+ self.setData(result)
273
+
274
+ def clearPivot(self):
275
+ """Restore the table to its pre-pivot columns and data source."""
276
+ if self._prePivot is None:
277
+ return
278
+ columns, provider = self._prePivot
279
+ self._prePivot = None
280
+ self._pivotActive = False
281
+ self.setColumns(columns)
282
+ if provider is not None:
283
+ self.setDataProvider(provider)
284
+ else:
285
+ self.setData([])
286
+
287
+ def isPivoted(self):
288
+ return self._pivotActive
289
+
290
+ def setEditableColumns(self, keys):
291
+ """Make the given column keys editable (others stay read-only)."""
292
+ self._model.setEditableColumns(keys)
293
+
294
+ def setEditable(self, editable=True):
295
+ """Convenience: True -> all columns editable, False -> none, or pass a
296
+ list of column keys."""
297
+ if editable is True:
298
+ keys = [c.key for c in self._model.columns()]
299
+ elif editable is False:
300
+ keys = []
301
+ else:
302
+ keys = list(editable)
303
+ self._model.setEditableColumns(keys)
304
+
305
+ def setColumnValidator(self, key, fn):
306
+ """Set a validator ``fn(value) -> True | error_message`` for a column;
307
+ rejected edits emit validationFailed and are not written."""
308
+ self._model.setValidator(key, fn)
309
+
310
+ def editableColumns(self):
311
+ return self._model.editableColumns()
312
+
313
+ def exportTo(self, path, fmt=None, sheetName="Sheet1"):
314
+ """Export the current result (respecting the active sort + filter) to
315
+ .csv or .xlsx using the pure-stdlib writer. Rows stream from the
316
+ provider, so a large export is not held in memory."""
317
+ from .export import export_table
318
+ return export_table(path, self._model.columns(),
319
+ self._model.iterAllRows(), fmt=fmt, sheet_name=sheetName)
@@ -0,0 +1,143 @@
1
+ ########################################################################
2
+ ## SPINN DESIGN CODE
3
+ # WEBSITE: customwidgets.org
4
+ ########################################################################
5
+ """CSV / XLSX export for DataTable Pro - pure standard library.
6
+
7
+ CSV uses the stdlib ``csv`` module. XLSX is written directly: an .xlsx file is
8
+ just a ZIP of XML parts, so we emit the minimal Office Open XML package with
9
+ ``zipfile`` + string XML. No third-party dependency (no openpyxl), matching the
10
+ Pro rule that this package bundles only original code.
11
+
12
+ Rows are streamed - the worksheet is written straight into the zip entry, so a
13
+ large export never materialises the whole sheet in memory.
14
+ """
15
+ import csv
16
+ import zipfile
17
+ from xml.sax.saxutils import escape
18
+
19
+
20
+ def _cell_value(column, row):
21
+ """Raw value for a column key in a row (preserves numeric types)."""
22
+ return row.get(column.key)
23
+
24
+
25
+ # ---------------------------------------------------------------------- #
26
+ ## CSV
27
+ # ---------------------------------------------------------------------- #
28
+ def export_csv(path, columns, rows):
29
+ with open(path, "w", newline="", encoding="utf-8") as fh:
30
+ writer = csv.writer(fh)
31
+ writer.writerow([c.title for c in columns])
32
+ for row in rows:
33
+ writer.writerow([_csv_cell(_cell_value(c, row)) for c in columns])
34
+
35
+
36
+ def _csv_cell(value):
37
+ if value is None:
38
+ return ""
39
+ if isinstance(value, bool):
40
+ return "TRUE" if value else "FALSE"
41
+ return value
42
+
43
+
44
+ # ---------------------------------------------------------------------- #
45
+ ## XLSX (minimal Office Open XML, streamed)
46
+ # ---------------------------------------------------------------------- #
47
+ _CONTENT_TYPES = (
48
+ '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
49
+ '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
50
+ '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
51
+ '<Default Extension="xml" ContentType="application/xml"/>'
52
+ '<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>'
53
+ '<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>'
54
+ '</Types>')
55
+
56
+ _ROOT_RELS = (
57
+ '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
58
+ '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
59
+ '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>'
60
+ '</Relationships>')
61
+
62
+ _WORKBOOK_RELS = (
63
+ '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
64
+ '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
65
+ '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>'
66
+ '</Relationships>')
67
+
68
+ _SHEET_NS = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
69
+
70
+
71
+ def _sanitize_sheet_name(name):
72
+ for ch in "\\/?*[]:":
73
+ name = name.replace(ch, " ")
74
+ return (name.strip() or "Sheet1")[:31]
75
+
76
+
77
+ def _workbook_xml(sheet_name):
78
+ return (
79
+ '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
80
+ '<workbook xmlns="%s" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">'
81
+ '<sheets><sheet name="%s" sheetId="1" r:id="rId1"/></sheets>'
82
+ '</workbook>' % (_SHEET_NS, escape(sheet_name)))
83
+
84
+
85
+ def _col_letter(index):
86
+ """0-based column index -> A, B, ... Z, AA, ..."""
87
+ letters = ""
88
+ index += 1
89
+ while index:
90
+ index, rem = divmod(index - 1, 26)
91
+ letters = chr(65 + rem) + letters
92
+ return letters
93
+
94
+
95
+ def _cell_xml(ref, value):
96
+ if value is None or value == "":
97
+ return '<c r="%s"/>' % ref
98
+ if isinstance(value, bool):
99
+ return '<c r="%s" t="b"><v>%d</v></c>' % (ref, 1 if value else 0)
100
+ if isinstance(value, (int, float)):
101
+ return '<c r="%s"><v>%s</v></c>' % (ref, value)
102
+ return ('<c r="%s" t="inlineStr"><is><t xml:space="preserve">%s</t></is></c>'
103
+ % (ref, escape(str(value))))
104
+
105
+
106
+ def _row_xml(rownum, values):
107
+ cells = "".join(_cell_xml("%s%d" % (_col_letter(i), rownum), v)
108
+ for i, v in enumerate(values))
109
+ return '<row r="%d">%s</row>' % (rownum, cells)
110
+
111
+
112
+ def export_xlsx(path, columns, rows, sheet_name="Sheet1"):
113
+ sheet_name = _sanitize_sheet_name(sheet_name)
114
+ with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as zf:
115
+ zf.writestr("[Content_Types].xml", _CONTENT_TYPES)
116
+ zf.writestr("_rels/.rels", _ROOT_RELS)
117
+ zf.writestr("xl/workbook.xml", _workbook_xml(sheet_name))
118
+ zf.writestr("xl/_rels/workbook.xml.rels", _WORKBOOK_RELS)
119
+ with zf.open("xl/worksheets/sheet1.xml", "w") as sheet:
120
+ def w(text):
121
+ sheet.write(text.encode("utf-8"))
122
+ w('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>')
123
+ w('<worksheet xmlns="%s"><sheetData>' % _SHEET_NS)
124
+ w(_row_xml(1, [c.title for c in columns])) # header
125
+ rownum = 2
126
+ for row in rows:
127
+ w(_row_xml(rownum, [_cell_value(c, row) for c in columns]))
128
+ rownum += 1
129
+ w("</sheetData></worksheet>")
130
+
131
+
132
+ # ---------------------------------------------------------------------- #
133
+ ## Dispatch
134
+ # ---------------------------------------------------------------------- #
135
+ def export_table(path, columns, rows, fmt=None, sheet_name="Sheet1"):
136
+ fmt = (fmt or path.rsplit(".", 1)[-1]).lower()
137
+ if fmt == "csv":
138
+ export_csv(path, columns, rows)
139
+ elif fmt in ("xlsx", "xls"):
140
+ export_xlsx(path, columns, rows, sheet_name=sheet_name)
141
+ else:
142
+ raise ValueError("unsupported export format: %r (use .csv or .xlsx)" % fmt)
143
+ return fmt
@@ -0,0 +1,112 @@
1
+ ########################################################################
2
+ ## SPINN DESIGN CODE
3
+ # WEBSITE: customwidgets.org
4
+ ########################################################################
5
+ """Frozen (pinned) leftmost columns for DataTable Pro.
6
+
7
+ Classic Qt technique: a second QTableView overlays the left of the main view,
8
+ shares the same model and selection model, shows only the frozen columns, and
9
+ is kept in sync on vertical scroll, column resize and geometry changes. The
10
+ main view scrolls horizontally underneath while the frozen overlay stays put.
11
+ """
12
+ from qtpy.QtCore import Qt
13
+ from qtpy.QtWidgets import QTableView, QFrame, QHeaderView
14
+
15
+
16
+ class FrozenHostView(QTableView):
17
+ """The main table view; hosts a frozen overlay for the first N columns.
18
+
19
+ QCustomDataTablePro uses this as its view (via _createView). Call
20
+ setFrozenColumnCount(n) to pin the leftmost n columns.
21
+ """
22
+
23
+ def __init__(self, parent=None):
24
+ super().__init__(parent)
25
+ self._frozen = None
26
+ self._count = 0
27
+
28
+ # ------------------------------------------------------------------ #
29
+ ## Public
30
+ # ------------------------------------------------------------------ #
31
+ def frozenColumnCount(self):
32
+ return self._count
33
+
34
+ def setFrozenColumnCount(self, n):
35
+ n = max(0, int(n))
36
+ self._count = n
37
+ if n <= 0:
38
+ if self._frozen is not None:
39
+ self._frozen.setParent(None)
40
+ self._frozen.deleteLater()
41
+ self._frozen = None
42
+ return
43
+ if self._frozen is None:
44
+ self._frozen = QTableView(self)
45
+ self._setupFrozen()
46
+ self._applyFrozen()
47
+
48
+ def frozenView(self):
49
+ return self._frozen
50
+
51
+ # ------------------------------------------------------------------ #
52
+ ## Overlay setup / sync
53
+ # ------------------------------------------------------------------ #
54
+ def _setupFrozen(self):
55
+ f = self._frozen
56
+ f.setObjectName("dataTableView") # inherit the token QSS
57
+ if self.model() is not None:
58
+ f.setModel(self.model())
59
+ f.setSelectionModel(self.selectionModel())
60
+ f.setFocusPolicy(Qt.NoFocus)
61
+ f.setFrameShape(QFrame.NoFrame)
62
+ f.verticalHeader().setVisible(False)
63
+ f.horizontalHeader().setStretchLastSection(False)
64
+ f.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
65
+ f.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
66
+ f.setEditTriggers(self.editTriggers())
67
+ f.setSelectionBehavior(self.selectionBehavior())
68
+ f.setSelectionMode(self.selectionMode())
69
+ f.setShowGrid(self.showGrid())
70
+ f.setAlternatingRowColors(self.alternatingRowColors())
71
+ # keep the two views vertically in lock-step
72
+ self.verticalScrollBar().valueChanged.connect(f.verticalScrollBar().setValue)
73
+ f.verticalScrollBar().valueChanged.connect(self.verticalScrollBar().setValue)
74
+ self.horizontalHeader().sectionResized.connect(self._onSectionResized)
75
+ f.show()
76
+
77
+ def _applyFrozen(self):
78
+ f = self._frozen
79
+ model = self.model()
80
+ cols = model.columnCount() if model is not None else 0
81
+ for c in range(cols):
82
+ f.setColumnHidden(c, c >= self._count)
83
+ if c < self._count:
84
+ f.setColumnWidth(c, self.columnWidth(c))
85
+ self._updateFrozenGeometry()
86
+
87
+ def _onSectionResized(self, index, _old, new):
88
+ if self._frozen is not None and index < self._count:
89
+ self._frozen.setColumnWidth(index, new)
90
+ self._updateFrozenGeometry()
91
+
92
+ def _updateFrozenGeometry(self):
93
+ if self._frozen is None:
94
+ return
95
+ width = sum(self.columnWidth(c) for c in range(self._count))
96
+ height = self.viewport().height() + self.horizontalHeader().height()
97
+ fw = self.frameWidth()
98
+ self._frozen.setGeometry(fw, fw, width, height)
99
+
100
+ # ------------------------------------------------------------------ #
101
+ ## Keep the overlay valid across model/geometry changes
102
+ # ------------------------------------------------------------------ #
103
+ def setModel(self, model):
104
+ super().setModel(model)
105
+ if self._frozen is not None:
106
+ self._frozen.setModel(model)
107
+ self._frozen.setSelectionModel(self.selectionModel())
108
+ self._applyFrozen()
109
+
110
+ def resizeEvent(self, e):
111
+ super().resizeEvent(e)
112
+ self._updateFrozenGeometry()