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.
- custom_widgets_pro/__init__.py +73 -0
- custom_widgets_pro/_catalog.py +25 -0
- custom_widgets_pro/_cli.py +132 -0
- custom_widgets_pro/_license.py +419 -0
- custom_widgets_pro/_version.py +14 -0
- custom_widgets_pro/datatable/__init__.py +17 -0
- custom_widgets_pro/datatable/datatable_pro.py +319 -0
- custom_widgets_pro/datatable/export.py +143 -0
- custom_widgets_pro/datatable/frozen_view.py +112 -0
- custom_widgets_pro/datatable/grouping.py +309 -0
- custom_widgets_pro/datatable/pivot.py +100 -0
- custom_widgets_pro/datatable/provider.py +98 -0
- custom_widgets_pro/datatable/virtual_model.py +451 -0
- custom_widgets_pro/widgets/__init__.py +7 -0
- custom_widgets_pro/widgets/charts/QCustomBeeswarm.py +317 -0
- custom_widgets_pro/widgets/charts/QCustomBubbleChart.py +655 -0
- custom_widgets_pro/widgets/charts/QCustomCandlestickChart.py +522 -0
- custom_widgets_pro/widgets/charts/QCustomDivergingBarChart.py +430 -0
- custom_widgets_pro/widgets/charts/QCustomDotMatrix.py +266 -0
- custom_widgets_pro/widgets/charts/QCustomFunnelChart.py +474 -0
- custom_widgets_pro/widgets/charts/QCustomGanttChart.py +376 -0
- custom_widgets_pro/widgets/charts/QCustomHeatmap.py +555 -0
- custom_widgets_pro/widgets/charts/QCustomRadarChart.py +600 -0
- custom_widgets_pro/widgets/charts/QCustomRadialBars.py +441 -0
- custom_widgets_pro/widgets/charts/QCustomRadialLines.py +554 -0
- custom_widgets_pro/widgets/charts/QCustomRangeBarChart.py +504 -0
- custom_widgets_pro/widgets/charts/QCustomSankey.py +539 -0
- custom_widgets_pro/widgets/charts/QCustomScatterChart.py +623 -0
- custom_widgets_pro/widgets/charts/__init__.py +1 -0
- custom_widgets_pro/widgets/data/QCustomCodeEditor.py +391 -0
- custom_widgets_pro/widgets/data/QCustomNodeGraph.py +1030 -0
- custom_widgets_pro/widgets/data/QCustomRichTextEditor.py +189 -0
- custom_widgets_pro/widgets/data/QCustomTableToolbar.py +566 -0
- custom_widgets_pro/widgets/data/__init__.py +1 -0
- custom_widgets_pro/widgets/media/QCustomImageViewer.py +300 -0
- custom_widgets_pro/widgets/media/QCustomMediaGrid.py +197 -0
- custom_widgets_pro/widgets/media/QCustomMediaTimeline.py +607 -0
- custom_widgets_pro/widgets/media/QCustomVideoPlayer.py +326 -0
- custom_widgets_pro/widgets/media/__init__.py +1 -0
- qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/METADATA +112 -0
- qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/RECORD +45 -0
- qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/WHEEL +5 -0
- qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/entry_points.txt +2 -0
- qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/licenses/LICENSE +91 -0
- 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."""
|