glidepath 0.2.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.
- glidepath/__init__.py +3 -0
- glidepath/app/__init__.py +364 -0
- glidepath/app/backtest.py +281 -0
- glidepath/app/charts.py +759 -0
- glidepath/app/copy.py +174 -0
- glidepath/app/display.py +148 -0
- glidepath/app/drawdown.py +436 -0
- glidepath/app/example.py +66 -0
- glidepath/app/exports.py +487 -0
- glidepath/app/files.py +249 -0
- glidepath/app/firstrun.py +114 -0
- glidepath/app/forms.py +1750 -0
- glidepath/app/inspector.py +506 -0
- glidepath/app/labels.py +66 -0
- glidepath/app/montecarlo.py +399 -0
- glidepath/app/plan.py +354 -0
- glidepath/app/retirement.py +446 -0
- glidepath/app/scenarios.py +831 -0
- glidepath/app/shell.py +185 -0
- glidepath/app/tables.py +138 -0
- glidepath/core/__init__.py +390 -0
- glidepath/core/annuities.py +240 -0
- glidepath/core/backtest.py +514 -0
- glidepath/core/comparison.py +278 -0
- glidepath/core/config.py +82 -0
- glidepath/core/contributions.py +337 -0
- glidepath/core/engine.py +2811 -0
- glidepath/core/entities.py +264 -0
- glidepath/core/glide.py +289 -0
- glidepath/core/investments.py +175 -0
- glidepath/core/money.py +107 -0
- glidepath/core/montecarlo.py +609 -0
- glidepath/core/pensions.py +298 -0
- glidepath/core/periods.py +367 -0
- glidepath/core/provenance.py +271 -0
- glidepath/core/randomness.py +128 -0
- glidepath/core/region.py +46 -0
- glidepath/core/reporting.py +231 -0
- glidepath/core/results.py +504 -0
- glidepath/core/retirement.py +291 -0
- glidepath/core/returns.py +312 -0
- glidepath/core/scenarios.py +579 -0
- glidepath/core/state_pension.py +264 -0
- glidepath/core/tax.py +139 -0
- glidepath/core/withdrawals.py +461 -0
- glidepath/core/wrappers.py +278 -0
- glidepath/gui/__init__.py +6 -0
- glidepath/gui/assets/icon_128.png +0 -0
- glidepath/gui/assets/icon_16.png +0 -0
- glidepath/gui/assets/icon_24.png +0 -0
- glidepath/gui/assets/icon_256.png +0 -0
- glidepath/gui/assets/icon_32.png +0 -0
- glidepath/gui/assets/icon_48.png +0 -0
- glidepath/gui/assets/icon_64.png +0 -0
- glidepath/gui/assets/wordmark.png +0 -0
- glidepath/gui/charts.py +829 -0
- glidepath/gui/forms.py +359 -0
- glidepath/gui/inspector.py +186 -0
- glidepath/gui/main.py +51 -0
- glidepath/gui/scenarios.py +402 -0
- glidepath/gui/style.py +376 -0
- glidepath/gui/tableview.py +67 -0
- glidepath/gui/widgets.py +989 -0
- glidepath/persistence/__init__.py +48 -0
- glidepath/persistence/assumptions.py +112 -0
- glidepath/persistence/decode.py +747 -0
- glidepath/persistence/document.py +101 -0
- glidepath/persistence/encode.py +433 -0
- glidepath/persistence/migrations.py +158 -0
- glidepath/persistence/values.py +298 -0
- glidepath/py.typed +0 -0
- glidepath/regions/__init__.py +7 -0
- glidepath/regions/uk/__init__.py +189 -0
- glidepath/regions/uk/ages.py +156 -0
- glidepath/regions/uk/contributions.py +717 -0
- glidepath/regions/uk/data/age_rules.toml +78 -0
- glidepath/regions/uk/data/assumptions_default.toml +170 -0
- glidepath/regions/uk/data/returns_history.toml +150 -0
- glidepath/regions/uk/data/tax_year_2026_27.toml +98 -0
- glidepath/regions/uk/extension.py +479 -0
- glidepath/regions/uk/loader.py +704 -0
- glidepath/regions/uk/region.py +160 -0
- glidepath/regions/uk/schema.py +563 -0
- glidepath/regions/uk/state_pension.py +129 -0
- glidepath/regions/uk/tax.py +466 -0
- glidepath/regions/uk/wrappers.py +283 -0
- glidepath/regions/uk/years.py +92 -0
- glidepath-0.2.0.dist-info/METADATA +189 -0
- glidepath-0.2.0.dist-info/RECORD +93 -0
- glidepath-0.2.0.dist-info/WHEEL +4 -0
- glidepath-0.2.0.dist-info/entry_points.txt +3 -0
- glidepath-0.2.0.dist-info/licenses/LICENSE +21 -0
- glidepath-0.2.0.dist-info/licenses/LICENSE-DATA +28 -0
glidepath/gui/style.py
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""The shell's visual theme: palette, stylesheet, and branding (§4.7).
|
|
2
|
+
|
|
3
|
+
Purely presentational Qt mechanics — no copy, no policy. The Fusion
|
|
4
|
+
style gives an identical baseline on every platform; the palette and
|
|
5
|
+
stylesheet build a light look around the brand green — white cards on
|
|
6
|
+
a softly green-tinted window, the accent carrying buttons, focus,
|
|
7
|
+
selections, tab underlines, and headings; the window icon and the
|
|
8
|
+
About wordmark load from ``assets/`` (the icon at every
|
|
9
|
+
taskbar-to-tile size), so refreshed art is a file swap.
|
|
10
|
+
|
|
11
|
+
The chart series palette is a colour-vision-validated categorical
|
|
12
|
+
set (fixed slot order — the ordering is the CVD-safety mechanism);
|
|
13
|
+
the percentile band overlays take an ordered ink ramp so the bands
|
|
14
|
+
read apart from the series fills, and the Monte Carlo fan fills one
|
|
15
|
+
brand-green hue at stepped alphas so probability depth reads by
|
|
16
|
+
lightness alone (9.24).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from importlib import resources
|
|
20
|
+
from typing import TYPE_CHECKING
|
|
21
|
+
|
|
22
|
+
from PySide6.QtGui import QColor, QIcon, QPalette, QPixmap
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from PySide6.QtWidgets import QApplication
|
|
26
|
+
|
|
27
|
+
ACCENT = "#1e6d59"
|
|
28
|
+
"""The single accent colour — the brand tile's green: buttons, focus,
|
|
29
|
+
selections, the active tab underline."""
|
|
30
|
+
ACCENT_TEXT = "#14503f"
|
|
31
|
+
"""The accent stepped darker for text on light surfaces (card titles,
|
|
32
|
+
table headers, the selected tab) — the button green is too light to
|
|
33
|
+
read as small text."""
|
|
34
|
+
_ACCENT_HOVER = "#195b4a"
|
|
35
|
+
_ACCENT_PRESSED = "#144a3c"
|
|
36
|
+
_ACCENT_DISABLED = "#a3c4b9"
|
|
37
|
+
_WINDOW = "#e9eeeb"
|
|
38
|
+
_CARD = "#ffffff"
|
|
39
|
+
_BORDER = "#d3dbd5"
|
|
40
|
+
_TEXT = "#1f2523"
|
|
41
|
+
_MUTED = "#5c6663"
|
|
42
|
+
_DISABLED_TEXT = "#95a09b"
|
|
43
|
+
_DISABLED_FILL = "#f0f3f1"
|
|
44
|
+
_TINT_HOVER = "#e7f0ec"
|
|
45
|
+
_TINT_PRESSED = "#d7e7e0"
|
|
46
|
+
_ALT_ROW = "#f4f8f5"
|
|
47
|
+
_HEADER_BG = "#eef3f0"
|
|
48
|
+
_SCROLL_HANDLE = "#c2ccc6"
|
|
49
|
+
_SCROLL_HANDLE_HOVER = "#9db1a8"
|
|
50
|
+
_TOOLTIP_BG = "#253230"
|
|
51
|
+
|
|
52
|
+
CHART_SURFACE = "#ffffff"
|
|
53
|
+
"""The chart plot surface — white, matching the cards the charts sit
|
|
54
|
+
beside, and the surface the series palette is validated against."""
|
|
55
|
+
CHART_GRID = "#e8ece9"
|
|
56
|
+
"""Hairline horizontal gridlines — recessive, behind the marks."""
|
|
57
|
+
CHART_AXIS_LINE = "#c9d2cc"
|
|
58
|
+
"""The axis baseline pen — a step darker than the grid."""
|
|
59
|
+
CHART_LABEL_INK = _MUTED
|
|
60
|
+
"""Axis tick labels and titles — muted ink, never the series colour."""
|
|
61
|
+
CHART_TEXT_INK = _TEXT
|
|
62
|
+
"""Legend labels — primary ink; the marker beside them carries the
|
|
63
|
+
series identity."""
|
|
64
|
+
|
|
65
|
+
CHART_SERIES = (
|
|
66
|
+
"#2a78d6",
|
|
67
|
+
"#eb6834",
|
|
68
|
+
"#1baf7a",
|
|
69
|
+
"#eda100",
|
|
70
|
+
"#e87ba4",
|
|
71
|
+
"#008300",
|
|
72
|
+
"#4a3aa7",
|
|
73
|
+
"#e34948",
|
|
74
|
+
)
|
|
75
|
+
"""Categorical series colours, assigned to bar sets in this fixed
|
|
76
|
+
order, never cycled or re-ranked. The order is validated for adjacent
|
|
77
|
+
colour-vision-deficiency separation on the white chart surface; the
|
|
78
|
+
sub-3:1-contrast slots are relieved by the per-segment hover
|
|
79
|
+
tooltips, the white segment gaps, and the cash-flow CSV export."""
|
|
80
|
+
|
|
81
|
+
CHART_BAND_INKS = ("#0b0b0b", "#52514e", "#898781")
|
|
82
|
+
"""Percentile/backtest band overlays, assigned in band order — one
|
|
83
|
+
neutral ink hue stepped light-ward so the ordered bands read apart
|
|
84
|
+
from every series fill and from each other."""
|
|
85
|
+
|
|
86
|
+
CHART_FAN_FILL = ACCENT
|
|
87
|
+
"""The Monte Carlo fan's single fill hue — the brand green; depth
|
|
88
|
+
comes from the alpha ramp, so the fan stays colour-vision-safe
|
|
89
|
+
(lightness alone orders the fills)."""
|
|
90
|
+
|
|
91
|
+
CHART_FAN_ALPHAS = (45, 75, 110, 150)
|
|
92
|
+
"""Fill opacities in fan order, outermost first. The nested fills
|
|
93
|
+
overlap, so the stacked alphas deepen naturally toward the median —
|
|
94
|
+
central probability mass reads as colour depth."""
|
|
95
|
+
|
|
96
|
+
_ICON_SIZES = (16, 24, 32, 48, 64, 128, 256)
|
|
97
|
+
|
|
98
|
+
STYLESHEET = f"""
|
|
99
|
+
QGroupBox {{
|
|
100
|
+
background: {_CARD};
|
|
101
|
+
border: 1px solid {_BORDER};
|
|
102
|
+
border-radius: 8px;
|
|
103
|
+
margin-top: 14px;
|
|
104
|
+
padding: 12px 14px 12px 14px;
|
|
105
|
+
}}
|
|
106
|
+
QGroupBox::title {{
|
|
107
|
+
subcontrol-origin: margin;
|
|
108
|
+
left: 12px;
|
|
109
|
+
padding: 0 5px;
|
|
110
|
+
color: {ACCENT_TEXT};
|
|
111
|
+
font-weight: 600;
|
|
112
|
+
}}
|
|
113
|
+
QLineEdit, QComboBox {{
|
|
114
|
+
background: {_CARD};
|
|
115
|
+
border: 1px solid {_BORDER};
|
|
116
|
+
border-radius: 6px;
|
|
117
|
+
padding: 5px 9px;
|
|
118
|
+
selection-background-color: {ACCENT};
|
|
119
|
+
selection-color: white;
|
|
120
|
+
}}
|
|
121
|
+
QLineEdit:focus, QComboBox:focus {{
|
|
122
|
+
border: 1px solid {ACCENT};
|
|
123
|
+
}}
|
|
124
|
+
QLineEdit:disabled, QComboBox:disabled {{
|
|
125
|
+
background: {_DISABLED_FILL};
|
|
126
|
+
color: {_DISABLED_TEXT};
|
|
127
|
+
}}
|
|
128
|
+
QComboBox::drop-down {{
|
|
129
|
+
border: none;
|
|
130
|
+
width: 26px;
|
|
131
|
+
}}
|
|
132
|
+
QComboBox QAbstractItemView {{
|
|
133
|
+
background: {_CARD};
|
|
134
|
+
border: 1px solid {_BORDER};
|
|
135
|
+
padding: 4px;
|
|
136
|
+
selection-background-color: {_TINT_HOVER};
|
|
137
|
+
selection-color: {_TEXT};
|
|
138
|
+
}}
|
|
139
|
+
QPushButton {{
|
|
140
|
+
background: {_CARD};
|
|
141
|
+
border: 1px solid {_BORDER};
|
|
142
|
+
border-radius: 6px;
|
|
143
|
+
padding: 6px 16px;
|
|
144
|
+
}}
|
|
145
|
+
QPushButton:hover {{
|
|
146
|
+
background: {_TINT_HOVER};
|
|
147
|
+
border-color: {ACCENT};
|
|
148
|
+
color: {ACCENT_TEXT};
|
|
149
|
+
}}
|
|
150
|
+
QPushButton:pressed {{
|
|
151
|
+
background: {_TINT_PRESSED};
|
|
152
|
+
}}
|
|
153
|
+
QPushButton:disabled {{
|
|
154
|
+
background: {_DISABLED_FILL};
|
|
155
|
+
border-color: {_BORDER};
|
|
156
|
+
color: {_DISABLED_TEXT};
|
|
157
|
+
}}
|
|
158
|
+
QPushButton#primaryButton {{
|
|
159
|
+
background: {ACCENT};
|
|
160
|
+
border: 1px solid {ACCENT};
|
|
161
|
+
color: white;
|
|
162
|
+
font-weight: bold;
|
|
163
|
+
}}
|
|
164
|
+
QPushButton#primaryButton:hover {{
|
|
165
|
+
background: {_ACCENT_HOVER};
|
|
166
|
+
color: white;
|
|
167
|
+
}}
|
|
168
|
+
QPushButton#primaryButton:pressed {{
|
|
169
|
+
background: {_ACCENT_PRESSED};
|
|
170
|
+
}}
|
|
171
|
+
QPushButton#primaryButton:disabled {{
|
|
172
|
+
background: {_ACCENT_DISABLED};
|
|
173
|
+
border-color: {_ACCENT_DISABLED};
|
|
174
|
+
color: white;
|
|
175
|
+
}}
|
|
176
|
+
QTabWidget::pane {{
|
|
177
|
+
border: 1px solid {_BORDER};
|
|
178
|
+
border-radius: 6px;
|
|
179
|
+
top: -1px;
|
|
180
|
+
}}
|
|
181
|
+
QTabBar::tab {{
|
|
182
|
+
padding: 8px 18px;
|
|
183
|
+
border: 1px solid transparent;
|
|
184
|
+
border-bottom: 2px solid transparent;
|
|
185
|
+
color: {_MUTED};
|
|
186
|
+
margin-right: 2px;
|
|
187
|
+
}}
|
|
188
|
+
QTabBar::tab:hover {{
|
|
189
|
+
color: {_TEXT};
|
|
190
|
+
}}
|
|
191
|
+
QTabBar::tab:selected {{
|
|
192
|
+
color: {ACCENT_TEXT};
|
|
193
|
+
font-weight: bold;
|
|
194
|
+
border-bottom: 2px solid {ACCENT};
|
|
195
|
+
}}
|
|
196
|
+
QTableWidget {{
|
|
197
|
+
background: {_CARD};
|
|
198
|
+
alternate-background-color: {_ALT_ROW};
|
|
199
|
+
border: 1px solid {_BORDER};
|
|
200
|
+
border-radius: 6px;
|
|
201
|
+
gridline-color: {_ALT_ROW};
|
|
202
|
+
selection-background-color: {_TINT_PRESSED};
|
|
203
|
+
selection-color: {_TEXT};
|
|
204
|
+
}}
|
|
205
|
+
QHeaderView::section {{
|
|
206
|
+
background: {_HEADER_BG};
|
|
207
|
+
border: none;
|
|
208
|
+
border-bottom: 1px solid {_BORDER};
|
|
209
|
+
padding: 6px 10px;
|
|
210
|
+
font-weight: 600;
|
|
211
|
+
color: {ACCENT_TEXT};
|
|
212
|
+
}}
|
|
213
|
+
QListWidget {{
|
|
214
|
+
background: {_CARD};
|
|
215
|
+
border: 1px solid {_BORDER};
|
|
216
|
+
border-radius: 6px;
|
|
217
|
+
padding: 3px;
|
|
218
|
+
}}
|
|
219
|
+
QListWidget::item {{
|
|
220
|
+
padding: 5px 8px;
|
|
221
|
+
border-radius: 4px;
|
|
222
|
+
}}
|
|
223
|
+
QListWidget::item:hover {{
|
|
224
|
+
background: {_TINT_HOVER};
|
|
225
|
+
}}
|
|
226
|
+
QListWidget::item:selected {{
|
|
227
|
+
background: {_TINT_PRESSED};
|
|
228
|
+
color: {_TEXT};
|
|
229
|
+
}}
|
|
230
|
+
QScrollArea {{
|
|
231
|
+
border: none;
|
|
232
|
+
}}
|
|
233
|
+
QScrollBar:vertical {{
|
|
234
|
+
background: transparent;
|
|
235
|
+
width: 10px;
|
|
236
|
+
margin: 0;
|
|
237
|
+
}}
|
|
238
|
+
QScrollBar::handle:vertical {{
|
|
239
|
+
background: {_SCROLL_HANDLE};
|
|
240
|
+
border-radius: 5px;
|
|
241
|
+
min-height: 30px;
|
|
242
|
+
}}
|
|
243
|
+
QScrollBar::handle:vertical:hover {{
|
|
244
|
+
background: {_SCROLL_HANDLE_HOVER};
|
|
245
|
+
}}
|
|
246
|
+
QScrollBar:horizontal {{
|
|
247
|
+
background: transparent;
|
|
248
|
+
height: 10px;
|
|
249
|
+
margin: 0;
|
|
250
|
+
}}
|
|
251
|
+
QScrollBar::handle:horizontal {{
|
|
252
|
+
background: {_SCROLL_HANDLE};
|
|
253
|
+
border-radius: 5px;
|
|
254
|
+
min-width: 30px;
|
|
255
|
+
}}
|
|
256
|
+
QScrollBar::handle:horizontal:hover {{
|
|
257
|
+
background: {_SCROLL_HANDLE_HOVER};
|
|
258
|
+
}}
|
|
259
|
+
QScrollBar::add-line, QScrollBar::sub-line {{
|
|
260
|
+
width: 0;
|
|
261
|
+
height: 0;
|
|
262
|
+
}}
|
|
263
|
+
QScrollBar::add-page, QScrollBar::sub-page {{
|
|
264
|
+
background: transparent;
|
|
265
|
+
}}
|
|
266
|
+
QMenuBar {{
|
|
267
|
+
background: transparent;
|
|
268
|
+
padding: 2px 6px;
|
|
269
|
+
}}
|
|
270
|
+
QMenuBar::item {{
|
|
271
|
+
padding: 5px 10px;
|
|
272
|
+
border-radius: 5px;
|
|
273
|
+
color: {_TEXT};
|
|
274
|
+
}}
|
|
275
|
+
QMenuBar::item:selected {{
|
|
276
|
+
background: {_TINT_HOVER};
|
|
277
|
+
color: {ACCENT_TEXT};
|
|
278
|
+
}}
|
|
279
|
+
QMenu {{
|
|
280
|
+
background: {_CARD};
|
|
281
|
+
border: 1px solid {_BORDER};
|
|
282
|
+
padding: 5px;
|
|
283
|
+
}}
|
|
284
|
+
QMenu::item {{
|
|
285
|
+
padding: 6px 22px 6px 14px;
|
|
286
|
+
border-radius: 4px;
|
|
287
|
+
}}
|
|
288
|
+
QMenu::item:selected {{
|
|
289
|
+
background: {_TINT_HOVER};
|
|
290
|
+
color: {ACCENT_TEXT};
|
|
291
|
+
}}
|
|
292
|
+
QMenu::separator {{
|
|
293
|
+
height: 1px;
|
|
294
|
+
background: {_BORDER};
|
|
295
|
+
margin: 5px 8px;
|
|
296
|
+
}}
|
|
297
|
+
QStatusBar {{
|
|
298
|
+
color: {_MUTED};
|
|
299
|
+
}}
|
|
300
|
+
QStatusBar::item {{
|
|
301
|
+
border: none;
|
|
302
|
+
}}
|
|
303
|
+
QToolTip {{
|
|
304
|
+
background-color: {_TOOLTIP_BG};
|
|
305
|
+
color: {_WINDOW};
|
|
306
|
+
border: 1px solid {_TOOLTIP_BG};
|
|
307
|
+
padding: 5px 8px;
|
|
308
|
+
}}
|
|
309
|
+
QRadioButton {{
|
|
310
|
+
spacing: 6px;
|
|
311
|
+
}}
|
|
312
|
+
QLabel#answerLabel {{
|
|
313
|
+
font-size: 13pt;
|
|
314
|
+
font-weight: 600;
|
|
315
|
+
color: {ACCENT_TEXT};
|
|
316
|
+
}}
|
|
317
|
+
"""
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _asset_bytes(name: str) -> bytes:
|
|
321
|
+
"""One packaged asset's raw bytes."""
|
|
322
|
+
return (resources.files("glidepath.gui") / "assets" / name).read_bytes()
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def app_icon() -> QIcon:
|
|
326
|
+
"""The brand icon — the glide curve on the green tile — every size."""
|
|
327
|
+
icon = QIcon()
|
|
328
|
+
for size in _ICON_SIZES:
|
|
329
|
+
pixmap = QPixmap()
|
|
330
|
+
pixmap.loadFromData(_asset_bytes(f"icon_{size}.png"))
|
|
331
|
+
icon.addPixmap(pixmap)
|
|
332
|
+
return icon
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def wordmark_pixmap() -> QPixmap:
|
|
336
|
+
"""The horizontal wordmark, for the About screen."""
|
|
337
|
+
pixmap = QPixmap()
|
|
338
|
+
pixmap.loadFromData(_asset_bytes("wordmark.png"))
|
|
339
|
+
return pixmap
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def apply_theme(app: QApplication) -> None:
|
|
343
|
+
"""Apply the theme to the whole application (module docstring)."""
|
|
344
|
+
app.setStyle("Fusion")
|
|
345
|
+
palette = app.palette()
|
|
346
|
+
palette.setColor(QPalette.ColorRole.Window, QColor(_WINDOW))
|
|
347
|
+
palette.setColor(QPalette.ColorRole.Base, QColor(_CARD))
|
|
348
|
+
palette.setColor(QPalette.ColorRole.AlternateBase, QColor(_ALT_ROW))
|
|
349
|
+
palette.setColor(QPalette.ColorRole.Text, QColor(_TEXT))
|
|
350
|
+
palette.setColor(QPalette.ColorRole.WindowText, QColor(_TEXT))
|
|
351
|
+
palette.setColor(QPalette.ColorRole.ButtonText, QColor(_TEXT))
|
|
352
|
+
palette.setColor(QPalette.ColorRole.Highlight, QColor(ACCENT))
|
|
353
|
+
palette.setColor(QPalette.ColorRole.HighlightedText, QColor("white"))
|
|
354
|
+
palette.setColor(QPalette.ColorRole.PlaceholderText, QColor(_MUTED))
|
|
355
|
+
app.setPalette(palette)
|
|
356
|
+
app.setStyleSheet(STYLESHEET)
|
|
357
|
+
app.setWindowIcon(app_icon())
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
__all__ = [
|
|
361
|
+
"ACCENT",
|
|
362
|
+
"ACCENT_TEXT",
|
|
363
|
+
"CHART_AXIS_LINE",
|
|
364
|
+
"CHART_BAND_INKS",
|
|
365
|
+
"CHART_FAN_ALPHAS",
|
|
366
|
+
"CHART_FAN_FILL",
|
|
367
|
+
"CHART_GRID",
|
|
368
|
+
"CHART_LABEL_INK",
|
|
369
|
+
"CHART_SERIES",
|
|
370
|
+
"CHART_SURFACE",
|
|
371
|
+
"CHART_TEXT_INK",
|
|
372
|
+
"STYLESHEET",
|
|
373
|
+
"app_icon",
|
|
374
|
+
"apply_theme",
|
|
375
|
+
"wordmark_pixmap",
|
|
376
|
+
]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Shared read-only table widgets for view-model rows (§4.7).
|
|
2
|
+
|
|
3
|
+
Every result pane renders pre-formatted app-layer rows the same way;
|
|
4
|
+
these helpers keep that mechanical binding in one place.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from PySide6.QtWidgets import (
|
|
8
|
+
QAbstractItemView,
|
|
9
|
+
QTableWidget,
|
|
10
|
+
QTableWidgetItem,
|
|
11
|
+
QWidget,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
_MAX_COLUMN_WIDTH = 300
|
|
15
|
+
"""Widths beyond this elide (the view's default ElideRight) instead of
|
|
16
|
+
forcing the whole table into a horizontal scrollbar; the full text
|
|
17
|
+
stays reachable via the cell tooltip or the override dialog."""
|
|
18
|
+
|
|
19
|
+
_TOOLTIP_LENGTH = 45
|
|
20
|
+
"""Cells longer than this get their own text as tooltip — roughly what
|
|
21
|
+
fits in a capped column, so anything elided is hoverable in full."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def read_only_table(parent: QWidget) -> QTableWidget:
|
|
25
|
+
"""A table configured for display-only rows."""
|
|
26
|
+
table = QTableWidget(parent)
|
|
27
|
+
table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
|
28
|
+
table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
|
29
|
+
table.setAlternatingRowColors(True)
|
|
30
|
+
table.verticalHeader().setVisible(False)
|
|
31
|
+
return table
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def fill_table(
|
|
35
|
+
table: QTableWidget,
|
|
36
|
+
columns: tuple[str, ...],
|
|
37
|
+
rows: list[tuple[str, ...]],
|
|
38
|
+
tooltips: list[str] | None = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Replace a table's contents with pre-formatted cells.
|
|
41
|
+
|
|
42
|
+
Columns size to their content up to a cap; longer cells elide.
|
|
43
|
+
``tooltips`` are per-row descriptions shown on the first (label)
|
|
44
|
+
column only, so an elided cell elsewhere always hovers to its own
|
|
45
|
+
full text, never to the row description.
|
|
46
|
+
"""
|
|
47
|
+
table.setColumnCount(len(columns))
|
|
48
|
+
table.setHorizontalHeaderLabels(list(columns))
|
|
49
|
+
table.setRowCount(len(rows))
|
|
50
|
+
for row_index, row in enumerate(rows):
|
|
51
|
+
for column_index, text in enumerate(row):
|
|
52
|
+
item = QTableWidgetItem(text)
|
|
53
|
+
if tooltips is not None and column_index == 0:
|
|
54
|
+
item.setToolTip(tooltips[row_index])
|
|
55
|
+
elif len(text) > _TOOLTIP_LENGTH:
|
|
56
|
+
item.setToolTip(text)
|
|
57
|
+
table.setItem(row_index, column_index, item)
|
|
58
|
+
table.resizeColumnsToContents()
|
|
59
|
+
for index in range(table.columnCount()):
|
|
60
|
+
if table.columnWidth(index) > _MAX_COLUMN_WIDTH:
|
|
61
|
+
table.setColumnWidth(index, _MAX_COLUMN_WIDTH)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
__all__ = [
|
|
65
|
+
"fill_table",
|
|
66
|
+
"read_only_table",
|
|
67
|
+
]
|