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,566 @@
1
+ ########################################################################
2
+ ## SPINN DESIGN CODE
3
+ # YOUTUBE: (SPINN TV) https://www.youtube.com/@SpinnTV
4
+ # WEBSITE: customwidgets.org
5
+ ########################################################################
6
+
7
+ ########################################################################
8
+ ## QCustomTableToolbar - a rich filter/search bar for data tables.
9
+ ##
10
+ ## The companion chrome above a QCustomDataTable, modelled on modern SaaS
11
+ ## "Jobs / Records" screens (search + Filters button + removable filter chips +
12
+ ## Clear filters, and a second row of colour-coded status pills with counts and
13
+ ## a Show-statuses switch). It owns no data - it just emits intent:
14
+ ##
15
+ ## searchChanged(str) the search text changed (debounced by typing)
16
+ ## filtersClicked() the Filters button was pressed
17
+ ## filterChipRemoved(str) a filter chip's x was clicked (its key)
18
+ ## clearFiltersClicked() the Clear filters link was pressed
19
+ ## statusSelected(str) a status pill was picked ("" == the All pill)
20
+ ## showStatusesToggled(bool) the Show-statuses switch flipped
21
+ ##
22
+ ## Colours track the active theme through setThemeColors(...) (call it after
23
+ ## applyDesignTokens); each status keeps its own semantic hue for its pill.
24
+ ########################################################################
25
+ from qtpy.QtCore import Qt, Signal, QSize, QRectF, QPointF, Property
26
+ from qtpy.QtGui import QColor, QPainter, QPen, QPixmap, QIcon, QFont
27
+ from qtpy.QtWidgets import (
28
+ QWidget, QFrame, QLabel, QLineEdit, QPushButton, QToolButton,
29
+ QHBoxLayout, QVBoxLayout, QSizePolicy,
30
+ )
31
+
32
+ from Custom_Widgets.QCustomSwitch import QCustomSwitch
33
+
34
+
35
+ # --------------------------------------------------------------------------- #
36
+ ## Painted glyphs (crisp, theme-recolourable, no asset dependency)
37
+ # --------------------------------------------------------------------------- #
38
+ def _glyph_pixmap(kind, color, size=16, ratio=2):
39
+ """A small monochrome UI glyph rendered to a QPixmap in `color`."""
40
+ px = QPixmap(size * ratio, size * ratio)
41
+ px.setDevicePixelRatio(ratio)
42
+ px.fill(Qt.transparent)
43
+ p = QPainter(px)
44
+ p.setRenderHint(QPainter.Antialiasing, True)
45
+ pen = QPen(QColor(color))
46
+ pen.setWidthF(1.6)
47
+ pen.setCapStyle(Qt.RoundCap)
48
+ p.setPen(pen)
49
+ if kind == "search":
50
+ r = size * 0.42
51
+ cx, cy = size * 0.42, size * 0.42
52
+ p.drawEllipse(QRectF(cx - r / 2, cy - r / 2, r, r))
53
+ p.drawLine(QPointF(cx + r / 2 - 0.5, cy + r / 2 - 0.5),
54
+ QPointF(size * 0.86, size * 0.86))
55
+ elif kind == "sliders":
56
+ # three horizontal rails with offset knobs (a "filters" icon)
57
+ p.setBrush(QColor(color))
58
+ ys = (size * 0.28, size * 0.5, size * 0.72)
59
+ knobx = (size * 0.66, size * 0.36, size * 0.6)
60
+ for y, kx in zip(ys, knobx):
61
+ p.drawLine(QPointF(size * 0.16, y), QPointF(size * 0.84, y))
62
+ p.drawEllipse(QRectF(kx - 2.2, y - 2.2, 4.4, 4.4))
63
+ p.end()
64
+ return px
65
+
66
+
67
+ def _rgba(color, alpha):
68
+ c = QColor(color)
69
+ return "rgba(%d,%d,%d,%.3f)" % (c.red(), c.green(), c.blue(), alpha)
70
+
71
+
72
+ # --------------------------------------------------------------------------- #
73
+ ## Removable filter chip ("Status: Pending x")
74
+ # --------------------------------------------------------------------------- #
75
+ class _FilterChip(QFrame):
76
+ removed = Signal(str)
77
+
78
+ def __init__(self, key, label, value=None, parent=None):
79
+ super().__init__(parent)
80
+ self._key = str(key)
81
+ self.setObjectName("tableFilterChip")
82
+ self.setAttribute(Qt.WA_StyledBackground, True)
83
+ self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
84
+ # Pin an EVEN height so the pill radius (height/2) is exact. Qt does NOT
85
+ # clamp border-radius: a radius > height/2 falls back to SQUARE corners
86
+ # (platform font metrics made this 29px -> square on xcb/Breeze while it
87
+ # was 30px -> round offscreen). See qt-border-radius-pill-gotcha.
88
+ self.setFixedHeight(30)
89
+ lay = QHBoxLayout(self)
90
+ lay.setContentsMargins(12, 4, 8, 4)
91
+ lay.setSpacing(6)
92
+ self._prefix = QLabel(self)
93
+ self._prefix.setObjectName("tableFilterChipPrefix")
94
+ self._value = QLabel(self)
95
+ self._value.setObjectName("tableFilterChipValue")
96
+ lay.addWidget(self._prefix)
97
+ lay.addWidget(self._value)
98
+ self._close = QToolButton(self)
99
+ self._close.setObjectName("tableFilterChipClose")
100
+ self._close.setText("✕")
101
+ self._close.setCursor(Qt.PointingHandCursor)
102
+ self._close.setAutoRaise(True)
103
+ self._close.clicked.connect(lambda: self.removed.emit(self._key))
104
+ lay.addWidget(self._close)
105
+ self.setText(label, value)
106
+
107
+ def key(self):
108
+ return self._key
109
+
110
+ def setText(self, label, value=None):
111
+ if value is None or value == "":
112
+ self._prefix.setText("")
113
+ self._prefix.setVisible(False)
114
+ self._value.setText(str(label))
115
+ else:
116
+ self._prefix.setText("%s:" % label)
117
+ self._prefix.setVisible(True)
118
+ self._value.setText(str(value))
119
+
120
+
121
+ # --------------------------------------------------------------------------- #
122
+ ## Status pill ("Pending 1235") with a semantic outline colour
123
+ # --------------------------------------------------------------------------- #
124
+ class _StatusPill(QFrame):
125
+ clicked = Signal(str)
126
+
127
+ def __init__(self, key, label, count=None, color="#3b82f6", parent=None,
128
+ plain=False):
129
+ super().__init__(parent)
130
+ self._key = str(key)
131
+ self._color = color
132
+ self._plain = plain # plain = borderless text label (the "All" pill)
133
+ self._checked = False
134
+ self._text = QColor("#0f172a")
135
+ self._muted = QColor("#64748b")
136
+ self._surface = QColor("#ffffff")
137
+ self.setObjectName("tableStatusPill")
138
+ self.setAttribute(Qt.WA_StyledBackground, True)
139
+ self.setCursor(Qt.PointingHandCursor)
140
+ self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
141
+ # EVEN height so the pill radius (height/2) is exact; a radius that
142
+ # exceeds height/2 makes Qt render SQUARE corners (no clamping). See
143
+ # qt-border-radius-pill-gotcha — this only bit on the real xcb display.
144
+ self.setFixedHeight(30)
145
+ lay = QHBoxLayout(self)
146
+ lay.setContentsMargins((6 if plain else 14), 5, (6 if plain else 14), 5)
147
+ lay.setSpacing(9)
148
+ self._name = QLabel(label, self)
149
+ self._name.setObjectName("tableStatusPillName")
150
+ # thin vertical divider between label and count (like the reference)
151
+ self._divider = QFrame(self)
152
+ self._divider.setObjectName("tableStatusPillDivider")
153
+ self._divider.setFrameShape(QFrame.VLine)
154
+ self._divider.setFixedWidth(1)
155
+ self._count = QLabel("" if count is None else str(count), self)
156
+ self._count.setObjectName("tableStatusPillCount")
157
+ has_count = count is not None and not plain
158
+ self._divider.setVisible(has_count)
159
+ self._count.setVisible(has_count)
160
+ lay.addWidget(self._name)
161
+ lay.addWidget(self._divider)
162
+ lay.addWidget(self._count)
163
+ self._restyle()
164
+
165
+ def key(self):
166
+ return self._key
167
+
168
+ def isChecked(self):
169
+ return self._checked
170
+
171
+ def setChecked(self, on):
172
+ on = bool(on)
173
+ if on != self._checked:
174
+ self._checked = on
175
+ self._restyle()
176
+
177
+ def setCount(self, count):
178
+ self._count.setText("" if count is None else str(count))
179
+ has_count = count is not None and not self._plain
180
+ self._count.setVisible(has_count)
181
+ self._divider.setVisible(has_count)
182
+
183
+ def setColor(self, color):
184
+ self._color = color
185
+ self._restyle()
186
+
187
+ def applyThemeColors(self, text, muted, surface):
188
+ self._text = QColor(text)
189
+ self._muted = QColor(muted)
190
+ self._surface = QColor(surface)
191
+ self._restyle()
192
+
193
+ def mouseReleaseEvent(self, event):
194
+ if event.button() == Qt.LeftButton and self.rect().contains(event.pos()):
195
+ self.clicked.emit(self._key)
196
+ super().mouseReleaseEvent(event)
197
+
198
+ def resizeEvent(self, event):
199
+ super().resizeEvent(event)
200
+ self._restyle() # keep the pill radius == height/2 at any size
201
+
202
+ def _restyle(self):
203
+ hue = self._color
204
+ # radius must never exceed half the height or Qt draws square corners.
205
+ radius = max(2, self.height() // 2)
206
+ if self._plain:
207
+ # borderless text label (the "All" pill): dark when active, muted otherwise
208
+ name_col = self._text.name() if self._checked else self._muted.name()
209
+ self.setStyleSheet(
210
+ "#tableStatusPill { border: none; background: transparent; }"
211
+ "#tableStatusPillName { color: %s; font-weight: 600;"
212
+ " background: transparent; }" % name_col)
213
+ return
214
+ # outlined pill: status-hue border, dark label, MUTED grey count with a
215
+ # thin divider (matches the reference - the count is not the hue colour)
216
+ border = hue if self._checked else _rgba(hue, 0.65)
217
+ bg = _rgba(hue, 0.12) if self._checked else self._surface.name()
218
+ name_col = self._text.name()
219
+ self.setStyleSheet(
220
+ "#tableStatusPill { border: 1.4px solid %s; border-radius: %dpx;"
221
+ " background: %s; }"
222
+ "#tableStatusPillName { color: %s; font-weight: 600; background: transparent; }"
223
+ "#tableStatusPillDivider { color: %s; background: %s; border: none; }"
224
+ "#tableStatusPillCount { color: %s; font-weight: 600; background: transparent; }"
225
+ % (border, radius, bg, name_col, _rgba(self._muted, 0.5),
226
+ _rgba(self._muted, 0.5), self._muted.name()))
227
+
228
+
229
+ # --------------------------------------------------------------------------- #
230
+ ## The toolbar
231
+ # --------------------------------------------------------------------------- #
232
+ class QCustomTableToolbar(QWidget):
233
+
234
+ # -- Designer registration constants --
235
+ WIDGET_ICON = "components/icons/table.png"
236
+ WIDGET_TOOLTIP = "A search / filter / status-pill toolbar for data tables"
237
+ WIDGET_MODULE = "Custom_Widgets.QCustomTableToolbar"
238
+ WIDGET_DOM_XML = """
239
+ <ui language='c++'>
240
+ <widget class='QCustomTableToolbar' name='tableToolbar'>
241
+ <property name='geometry'>
242
+ <rect><x>0</x><y>0</y><width>720</width><height>110</height></rect>
243
+ </property>
244
+ </widget>
245
+ </ui>
246
+ """
247
+
248
+ __catalog__ = {
249
+ "name": "QCustomTableToolbar",
250
+ "props": {
251
+ "searchPlaceholder": {"type": "string", "default": "Search"},
252
+ "showStatuses": {"type": "bool", "default": True},
253
+ },
254
+ "signals": ["searchChanged", "filtersClicked", "filterChipRemoved",
255
+ "clearFiltersClicked", "statusSelected", "showStatusesToggled"],
256
+ # Intentionally no tokens_used. This widget never resolves a role
257
+ # itself -- colours arrive through setThemeColors(), which the APP has
258
+ # to call after applyDesignTokens(). Nothing in the library calls it,
259
+ # so the roles are whatever the caller passes, not a fixed set.
260
+ }
261
+
262
+ # Rich editors for the Designer "Custom Properties" dock (see
263
+ # DesignerTools.CustomPropertiesDock).
264
+ DESIGNER_CUSTOM_PROPS = [
265
+ {"name": "searchPlaceholder", "kind": "str", "group": "General"},
266
+ {"name": "showStatuses", "kind": "bool", "group": "General"},
267
+ ]
268
+
269
+ ALL_KEY = "" # the built-in "All" status pill
270
+
271
+ searchChanged = Signal(str)
272
+ filtersClicked = Signal()
273
+ filterChipRemoved = Signal(str)
274
+ clearFiltersClicked = Signal()
275
+ statusSelected = Signal(str)
276
+ showStatusesToggled = Signal(bool)
277
+
278
+ # -- designer-facing properties (editable in the Custom Properties dock) --
279
+ @Property(str)
280
+ def searchPlaceholder(self):
281
+ return self._input.placeholderText() if hasattr(self, "_input") else ""
282
+
283
+ @searchPlaceholder.setter
284
+ def searchPlaceholder(self, value):
285
+ self.setSearchPlaceholder(value)
286
+
287
+ @Property(bool)
288
+ def showStatuses(self):
289
+ return self._switch.isChecked() if hasattr(self, "_switch") else False
290
+
291
+ @showStatuses.setter
292
+ def showStatuses(self, value):
293
+ self.setShowStatuses(value)
294
+
295
+ def __init__(self, parent=None):
296
+ super().__init__(parent)
297
+ self.setObjectName("tableToolbar")
298
+ self.setAttribute(Qt.WA_StyledBackground, True)
299
+ self._chips = {} # key -> _FilterChip
300
+ self._pills = {} # key -> _StatusPill
301
+ self._activeStatus = self.ALL_KEY
302
+ # theme colours (sensible light defaults until setThemeColors is called)
303
+ self._c = dict(surface="#ffffff", on_surface="#0f172a",
304
+ muted="#64748b", outline="#e2e8f0", accent="#2563eb")
305
+ self._buildUi()
306
+ self._applyChrome()
307
+
308
+ # ------------------------------------------------------------------ #
309
+ ## UI
310
+ # ------------------------------------------------------------------ #
311
+ def _buildUi(self):
312
+ root = QVBoxLayout(self)
313
+ root.setContentsMargins(0, 0, 0, 0)
314
+ root.setSpacing(14)
315
+
316
+ # -- row 1: search + Filters + chips + Clear filters -------------
317
+ row1 = QHBoxLayout()
318
+ row1.setSpacing(10)
319
+
320
+ self._search = QFrame(self)
321
+ self._search.setObjectName("tableSearch")
322
+ self._search.setAttribute(Qt.WA_StyledBackground, True)
323
+ slay = QHBoxLayout(self._search)
324
+ slay.setContentsMargins(12, 0, 12, 0)
325
+ slay.setSpacing(8)
326
+ self._searchIcon = QLabel(self._search)
327
+ self._searchIcon.setObjectName("tableSearchIcon")
328
+ self._input = QLineEdit(self._search)
329
+ self._input.setObjectName("tableSearchInput")
330
+ self._input.setPlaceholderText("Search")
331
+ self._input.setFrame(False)
332
+ self._input.textChanged.connect(self.searchChanged.emit)
333
+ # input fills the field, magnifier sits on the RIGHT (matches the reference)
334
+ slay.addWidget(self._input, 1)
335
+ slay.addWidget(self._searchIcon)
336
+ self._search.setFixedHeight(40)
337
+ self._search.setMinimumWidth(240)
338
+ row1.addWidget(self._search)
339
+
340
+ self._filtersBtn = QPushButton("Filters", self)
341
+ self._filtersBtn.setObjectName("tableFiltersBtn")
342
+ self._filtersBtn.setCursor(Qt.PointingHandCursor)
343
+ # match the 40px search field so its radius (20px in _chromeQss) is
344
+ # exactly height/2 — a larger radius would render square (see the pill).
345
+ self._filtersBtn.setFixedHeight(40)
346
+ self._filtersBtn.clicked.connect(self.filtersClicked.emit)
347
+ row1.addWidget(self._filtersBtn)
348
+
349
+ self._chipBox = QWidget(self)
350
+ self._chipBox.setObjectName("tableChips")
351
+ self._chipLay = QHBoxLayout(self._chipBox)
352
+ self._chipLay.setContentsMargins(0, 0, 0, 0)
353
+ self._chipLay.setSpacing(8)
354
+ row1.addWidget(self._chipBox)
355
+
356
+ row1.addStretch(1)
357
+
358
+ self._clearBtn = QPushButton("Clear filters", self)
359
+ self._clearBtn.setObjectName("tableClearBtn")
360
+ self._clearBtn.setCursor(Qt.PointingHandCursor)
361
+ self._clearBtn.setFlat(True)
362
+ self._clearBtn.clicked.connect(self.clearFiltersClicked.emit)
363
+ row1.addWidget(self._clearBtn)
364
+ root.addLayout(row1)
365
+
366
+ # -- row 2: Show-statuses switch + All + status pills ------------
367
+ row2 = QHBoxLayout()
368
+ row2.setSpacing(10)
369
+ self._switch = QCustomSwitch(self, checked=True)
370
+ self._switch.toggled.connect(self._onShowStatusesToggled)
371
+ row2.addWidget(self._switch)
372
+ self._switchLabel = QLabel("Show statuses", self)
373
+ self._switchLabel.setObjectName("tableShowStatusesLabel")
374
+ row2.addWidget(self._switchLabel)
375
+ row2.addSpacing(6)
376
+
377
+ self._pillBox = QWidget(self)
378
+ self._pillBox.setObjectName("tableStatusPills")
379
+ self._pillLay = QHBoxLayout(self._pillBox)
380
+ self._pillLay.setContentsMargins(0, 0, 0, 0)
381
+ self._pillLay.setSpacing(9)
382
+ row2.addWidget(self._pillBox)
383
+ row2.addStretch(1)
384
+ root.addLayout(row2)
385
+
386
+ # the built-in "All" pill (plain text, selected by default)
387
+ self._allPill = _StatusPill(self.ALL_KEY, "All", None, self._c["muted"],
388
+ self._pillBox, plain=True)
389
+ self._allPill.setChecked(True)
390
+ self._allPill.clicked.connect(self._onStatusClicked)
391
+ self._pillLay.addWidget(self._allPill)
392
+ self._pills[self.ALL_KEY] = self._allPill
393
+
394
+ # ------------------------------------------------------------------ #
395
+ ## Search API
396
+ # ------------------------------------------------------------------ #
397
+ def searchInput(self):
398
+ return self._input
399
+
400
+ def setSearchPlaceholder(self, text):
401
+ self._input.setPlaceholderText(text or "")
402
+
403
+ def searchText(self):
404
+ return self._input.text()
405
+
406
+ def setSearchText(self, text):
407
+ self._input.setText(text or "")
408
+
409
+ # ------------------------------------------------------------------ #
410
+ ## Filter chips
411
+ # ------------------------------------------------------------------ #
412
+ def setFilterChips(self, items):
413
+ """Replace all filter chips. Each item is (key, label[, value]) or a
414
+ dict {"key":, "label":, "value":}."""
415
+ self.clearFilterChips()
416
+ for it in (items or []):
417
+ if isinstance(it, dict):
418
+ self.addFilterChip(it.get("key"), it.get("label"), it.get("value"))
419
+ elif isinstance(it, (tuple, list)):
420
+ self.addFilterChip(*it)
421
+ else:
422
+ self.addFilterChip(it, it)
423
+
424
+ def addFilterChip(self, key, label, value=None):
425
+ key = str(key)
426
+ if key in self._chips:
427
+ self._chips[key].setText(label, value)
428
+ return
429
+ chip = _FilterChip(key, label, value, self._chipBox)
430
+ chip.removed.connect(self._onChipRemoved)
431
+ self._chipLay.addWidget(chip)
432
+ self._chips[key] = chip
433
+ self._styleChip(chip)
434
+
435
+ def removeFilterChip(self, key):
436
+ chip = self._chips.pop(str(key), None)
437
+ if chip is not None:
438
+ self._chipLay.removeWidget(chip)
439
+ chip.deleteLater()
440
+
441
+ def clearFilterChips(self):
442
+ for key in list(self._chips.keys()):
443
+ self.removeFilterChip(key)
444
+
445
+ def filterChipKeys(self):
446
+ return list(self._chips.keys())
447
+
448
+ def _onChipRemoved(self, key):
449
+ self.removeFilterChip(key)
450
+ self.filterChipRemoved.emit(key)
451
+
452
+ # ------------------------------------------------------------------ #
453
+ ## Status pills
454
+ # ------------------------------------------------------------------ #
455
+ def setStatuses(self, items):
456
+ """Rebuild the status pills (the built-in All pill is kept first).
457
+ Each item is a dict {"key":, "label":, "count":, "color":}."""
458
+ for key in list(self._pills.keys()):
459
+ if key == self.ALL_KEY:
460
+ continue
461
+ pill = self._pills.pop(key)
462
+ self._pillLay.removeWidget(pill)
463
+ pill.deleteLater()
464
+ for it in (items or []):
465
+ key = str(it.get("key"))
466
+ pill = _StatusPill(key, it.get("label", key), it.get("count"),
467
+ it.get("color", self._c["accent"]), self._pillBox)
468
+ pill.applyThemeColors(self._c["on_surface"], self._c["muted"],
469
+ self._c["surface"])
470
+ pill.clicked.connect(self._onStatusClicked)
471
+ self._pillLay.addWidget(pill)
472
+ self._pills[key] = pill
473
+ self.setActiveStatus(self._activeStatus if self._activeStatus in self._pills
474
+ else self.ALL_KEY)
475
+
476
+ def setStatusCount(self, key, count):
477
+ pill = self._pills.get(str(key))
478
+ if pill is not None:
479
+ pill.setCount(count)
480
+
481
+ def setActiveStatus(self, key):
482
+ key = str(key)
483
+ if key not in self._pills:
484
+ key = self.ALL_KEY
485
+ self._activeStatus = key
486
+ for k, pill in self._pills.items():
487
+ pill.setChecked(k == key)
488
+
489
+ def activeStatus(self):
490
+ return self._activeStatus
491
+
492
+ def _onStatusClicked(self, key):
493
+ self.setActiveStatus(key)
494
+ self.statusSelected.emit(key)
495
+
496
+ # ------------------------------------------------------------------ #
497
+ ## Show-statuses switch
498
+ # ------------------------------------------------------------------ #
499
+ def showStatuses(self):
500
+ return self._switch.isChecked()
501
+
502
+ def setShowStatuses(self, on):
503
+ self._switch.setChecked(bool(on))
504
+
505
+ def _onShowStatusesToggled(self, on):
506
+ self._pillBox.setVisible(on)
507
+ self.showStatusesToggled.emit(on)
508
+
509
+ # ------------------------------------------------------------------ #
510
+ ## Theme
511
+ # ------------------------------------------------------------------ #
512
+ def setThemeColors(self, surface=None, on_surface=None, muted=None,
513
+ outline=None, accent=None):
514
+ """Track the active theme. Pass token role values (call after
515
+ applyDesignTokens). Any omitted colour keeps its current value."""
516
+ for name, val in (("surface", surface), ("on_surface", on_surface),
517
+ ("muted", muted), ("outline", outline),
518
+ ("accent", accent)):
519
+ if val:
520
+ self._c[name] = val
521
+ self._applyChrome()
522
+
523
+ def _applyChrome(self):
524
+ c = self._c
525
+ # recolour painted glyphs
526
+ self._searchIcon.setPixmap(_glyph_pixmap("search", c["muted"], 16))
527
+ self._filtersBtn.setIcon(QIcon(_glyph_pixmap("sliders", c["on_surface"], 16)))
528
+ self._filtersBtn.setIconSize(QSize(16, 16))
529
+ # the "All" pill borrows the muted hue so it reads as neutral
530
+ self._allPill.setColor(c["muted"])
531
+ for pill in self._pills.values():
532
+ pill.applyThemeColors(c["on_surface"], c["muted"], c["surface"])
533
+ for chip in self._chips.values():
534
+ self._styleChip(chip)
535
+ self.setStyleSheet(self._chromeQss())
536
+
537
+ def _styleChip(self, chip):
538
+ c = self._c
539
+ chip.setStyleSheet(
540
+ "#tableFilterChip { background: %s; border: 1px solid %s;"
541
+ " border-radius: 15px; }"
542
+ "#tableFilterChipPrefix { color: %s; background: transparent; }"
543
+ "#tableFilterChipValue { color: %s; font-weight: 600; background: transparent; }"
544
+ "#tableFilterChipClose { color: %s; border: none; font-size: 12px;"
545
+ " padding: 0 2px; background: transparent; }"
546
+ "#tableFilterChipClose:hover { color: %s; }"
547
+ % (_rgba(c["muted"], 0.10), c["outline"], c["muted"],
548
+ c["on_surface"], c["muted"], c["on_surface"]))
549
+
550
+ def _chromeQss(self):
551
+ c = self._c
552
+ return (
553
+ "#tableToolbar { background: transparent; }"
554
+ "#tableSearch { background: %(surface)s; border: 1px solid %(outline)s;"
555
+ " border-radius: 20px; }"
556
+ "#tableSearchInput { border: none; background: transparent;"
557
+ " color: %(on_surface)s; font-size: 14px; }"
558
+ "#tableFiltersBtn { background: %(surface)s; color: %(on_surface)s;"
559
+ " border: 1px solid %(outline)s; border-radius: 20px; padding: 8px 16px;"
560
+ " font-weight: 600; }"
561
+ "#tableFiltersBtn:hover { border-color: %(accent)s; color: %(accent)s; }"
562
+ "#tableClearBtn { border: none; background: transparent; color: %(accent)s;"
563
+ " font-weight: 600; padding: 6px 4px; }"
564
+ "#tableClearBtn:hover { text-decoration: underline; }"
565
+ "#tableShowStatusesLabel { color: %(on_surface)s; font-weight: 500; }"
566
+ % c)
@@ -0,0 +1 @@
1
+ """Pro widgets: data."""