mesofield 0.3.2b0__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.
- docs/_static/custom.css +40 -0
- docs/_static/favicon.png +0 -0
- docs/_static/logo.png +0 -0
- docs/api/index.md +70 -0
- docs/conf.py +200 -0
- docs/developer_guide.md +303 -0
- docs/index.md +25 -0
- docs/tutorial.md +4 -0
- docs/user_guide.md +172 -0
- examples/teensy_pulse_generator.py +320 -0
- experiments/pipeline_demo/experiment.json +24 -0
- experiments/pipeline_demo/hardware.yaml +23 -0
- experiments/pipeline_demo/procedure.py +50 -0
- experiments/two_cam_demo/experiment.json +24 -0
- experiments/two_cam_demo/hardware.yaml +58 -0
- experiments/two_cam_demo/load_dataset.py +213 -0
- experiments/two_cam_demo/procedure.py +87 -0
- external/video-codecs/openh264-1.8.0-win64.dll +0 -0
- mesofield/__init__.py +45 -0
- mesofield/__main__.py +11 -0
- mesofield/_version.py +24 -0
- mesofield/base.py +750 -0
- mesofield/cli/__init__.py +57 -0
- mesofield/cli/_richhelp.py +100 -0
- mesofield/cli/acquire.py +254 -0
- mesofield/cli/datakit.py +165 -0
- mesofield/cli/process.py +376 -0
- mesofield/cli/rig.py +108 -0
- mesofield/cli/tools.py +347 -0
- mesofield/config.py +751 -0
- mesofield/data/__init__.py +23 -0
- mesofield/data/batch.py +633 -0
- mesofield/data/manager.py +388 -0
- mesofield/data/writer.py +289 -0
- mesofield/datakit/__init__.py +44 -0
- mesofield/datakit/__main__.py +35 -0
- mesofield/datakit/_utils/_logger.py +5 -0
- mesofield/datakit/_version.py +141 -0
- mesofield/datakit/config.py +50 -0
- mesofield/datakit/core.py +783 -0
- mesofield/datakit/datamodel.py +200 -0
- mesofield/datakit/discover.py +124 -0
- mesofield/datakit/explore.py +651 -0
- mesofield/datakit/notebooks/pupil_dlc.ipynb +2445 -0
- mesofield/datakit/profile.py +535 -0
- mesofield/datakit/shell.py +83 -0
- mesofield/datakit/sources/__init__.py +65 -0
- mesofield/datakit/sources/analysis/mesomap.py +194 -0
- mesofield/datakit/sources/analysis/mesoscope.py +77 -0
- mesofield/datakit/sources/analysis/pupil.py +246 -0
- mesofield/datakit/sources/behavior/__init__.py +0 -0
- mesofield/datakit/sources/behavior/dataqueue.py +281 -0
- mesofield/datakit/sources/behavior/psychopy.py +364 -0
- mesofield/datakit/sources/behavior/treadmill.py +323 -0
- mesofield/datakit/sources/behavior/wheel.py +277 -0
- mesofield/datakit/sources/camera/mesoscope.py +32 -0
- mesofield/datakit/sources/camera/metadata_json.py +130 -0
- mesofield/datakit/sources/camera/pupil.py +28 -0
- mesofield/datakit/sources/camera/suite2p.py +547 -0
- mesofield/datakit/sources/register.py +204 -0
- mesofield/datakit/sources/session/config.py +130 -0
- mesofield/datakit/sources/session/notes.py +63 -0
- mesofield/datakit/sources/session/timestamps.py +58 -0
- mesofield/datakit/timeline.py +306 -0
- mesofield/devices/__init__.py +42 -0
- mesofield/devices/base.py +498 -0
- mesofield/devices/base_camera.py +295 -0
- mesofield/devices/cameras.py +740 -0
- mesofield/devices/daq.py +151 -0
- mesofield/devices/encoder.py +384 -0
- mesofield/devices/mocks.py +275 -0
- mesofield/devices/psychopy_device.py +455 -0
- mesofield/devices/subprocesses/__init__.py +0 -0
- mesofield/devices/subprocesses/psychopy.py +133 -0
- mesofield/devices/treadmill.py +318 -0
- mesofield/engines.py +380 -0
- mesofield/gui/Mesofield_icon.png +0 -0
- mesofield/gui/__init__.py +76 -0
- mesofield/gui/config_wizard.py +724 -0
- mesofield/gui/controller.py +535 -0
- mesofield/gui/dynamic_controller.py +78 -0
- mesofield/gui/maingui.py +427 -0
- mesofield/gui/mdagui.py +285 -0
- mesofield/gui/qt_device_adapter.py +109 -0
- mesofield/gui/speedplotter.py +152 -0
- mesofield/gui/theme.py +445 -0
- mesofield/gui/tiff_viewer.py +1050 -0
- mesofield/gui/viewer.py +691 -0
- mesofield/hardware.py +549 -0
- mesofield/playback.py +1298 -0
- mesofield/processing/__init__.py +12 -0
- mesofield/processing/runner.py +237 -0
- mesofield/processors/__init__.py +13 -0
- mesofield/processors/base.py +287 -0
- mesofield/processors/frame_mean.py +19 -0
- mesofield/protocols.py +378 -0
- mesofield/scaffold/__init__.py +34 -0
- mesofield/scaffold/experiment.py +400 -0
- mesofield/scaffold/rigs.py +121 -0
- mesofield/signals.py +85 -0
- mesofield/utils/__init__.py +0 -0
- mesofield/utils/_logger.py +156 -0
- mesofield/utils/retrofit.py +309 -0
- mesofield/utils/utils.py +217 -0
- mesofield-0.3.2b0.dist-info/METADATA +178 -0
- mesofield-0.3.2b0.dist-info/RECORD +111 -0
- mesofield-0.3.2b0.dist-info/WHEEL +5 -0
- mesofield-0.3.2b0.dist-info/entry_points.txt +2 -0
- mesofield-0.3.2b0.dist-info/licenses/LICENSE +21 -0
- mesofield-0.3.2b0.dist-info/top_level.txt +6 -0
- scripts/bench_frame_processor.py +103 -0
mesofield/gui/theme.py
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
"""Mesofield GUI theme — "Control Panel / Net-Runner" design language.
|
|
2
|
+
|
|
3
|
+
A single source of truth for the application's dark-mode look. The aesthetic is
|
|
4
|
+
a mechanical control-system panel with restrained net-runner cues: deep gunmetal
|
|
5
|
+
surfaces, hairline borders, square-ish corners, and a muted phosphor-green accent
|
|
6
|
+
(with a brighter phosphor reserved for active/recording states).
|
|
7
|
+
|
|
8
|
+
Usage (call once, right after creating the QApplication)::
|
|
9
|
+
|
|
10
|
+
from mesofield.gui import theme
|
|
11
|
+
app = QApplication([])
|
|
12
|
+
theme.apply_theme(app)
|
|
13
|
+
|
|
14
|
+
The module is intentionally dependency-light (only PyQt6) and cross-platform:
|
|
15
|
+
it relies solely on QSS + QPalette + web-safe font *stacks*, so it renders
|
|
16
|
+
consistently on macOS (Cocoa) and Windows 11.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
|
|
22
|
+
from PyQt6.QtGui import QColor, QPalette
|
|
23
|
+
from PyQt6.QtWidgets import QApplication
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
# Palette
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class Palette:
|
|
31
|
+
"""Canonical color tokens for the Mesofield theme."""
|
|
32
|
+
|
|
33
|
+
BG: str = "#0e1216" # app background (near-black blue-grey)
|
|
34
|
+
PANEL: str = "#161b22" # cards / group boxes / inputs
|
|
35
|
+
PANEL_HI: str = "#1d242d" # hover / alternating rows
|
|
36
|
+
BORDER: str = "#2b333d" # hairline mechanical borders
|
|
37
|
+
BORDER_HI: str = "#3a4450" # focused / hovered borders
|
|
38
|
+
TEXT: str = "#c8d0d8" # primary text
|
|
39
|
+
TEXT_DIM: str = "#7d8893" # secondary / disabled text
|
|
40
|
+
ACCENT: str = "#4fd6a0" # muted phosphor (primary accent)
|
|
41
|
+
ACCENT_HI: str = "#39FF14" # bright phosphor (active / recording only)
|
|
42
|
+
WARN: str = "#e0a23c" # amber (status warnings)
|
|
43
|
+
DANGER: str = "#e0574f" # red (abort / record)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
PALETTE = Palette()
|
|
47
|
+
|
|
48
|
+
# Convenience module-level aliases (so callers can do ``theme.ACCENT``).
|
|
49
|
+
BG = PALETTE.BG
|
|
50
|
+
PANEL = PALETTE.PANEL
|
|
51
|
+
PANEL_HI = PALETTE.PANEL_HI
|
|
52
|
+
BORDER = PALETTE.BORDER
|
|
53
|
+
BORDER_HI = PALETTE.BORDER_HI
|
|
54
|
+
TEXT = PALETTE.TEXT
|
|
55
|
+
TEXT_DIM = PALETTE.TEXT_DIM
|
|
56
|
+
ACCENT = PALETTE.ACCENT
|
|
57
|
+
ACCENT_HI = PALETTE.ACCENT_HI
|
|
58
|
+
WARN = PALETTE.WARN
|
|
59
|
+
DANGER = PALETTE.DANGER
|
|
60
|
+
|
|
61
|
+
# Monospace stack for data / numeric / terminal surfaces only.
|
|
62
|
+
MONO_FONT = '"JetBrains Mono", "Cascadia Mono", "SF Mono", "Menlo", "Consolas", monospace'
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
# Stylesheet
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
STYLESHEET = f"""
|
|
69
|
+
/* ===================== Base surfaces ===================== */
|
|
70
|
+
QWidget {{
|
|
71
|
+
background-color: {BG};
|
|
72
|
+
color: {TEXT};
|
|
73
|
+
selection-background-color: {ACCENT};
|
|
74
|
+
selection-color: {BG};
|
|
75
|
+
}}
|
|
76
|
+
QMainWindow, QDialog {{
|
|
77
|
+
background-color: {BG};
|
|
78
|
+
}}
|
|
79
|
+
QWidget:disabled {{
|
|
80
|
+
color: {TEXT_DIM};
|
|
81
|
+
}}
|
|
82
|
+
|
|
83
|
+
/* ===================== Toolbar ===================== */
|
|
84
|
+
QToolBar {{
|
|
85
|
+
background-color: {PANEL};
|
|
86
|
+
border: none;
|
|
87
|
+
border-bottom: 1px solid {BORDER};
|
|
88
|
+
spacing: 4px;
|
|
89
|
+
padding: 3px;
|
|
90
|
+
}}
|
|
91
|
+
QToolBar QToolButton {{
|
|
92
|
+
background-color: transparent;
|
|
93
|
+
color: {TEXT};
|
|
94
|
+
border: 1px solid transparent;
|
|
95
|
+
border-radius: 3px;
|
|
96
|
+
padding: 4px 8px;
|
|
97
|
+
}}
|
|
98
|
+
QToolBar QToolButton:hover {{
|
|
99
|
+
background-color: {PANEL_HI};
|
|
100
|
+
border: 1px solid {BORDER_HI};
|
|
101
|
+
}}
|
|
102
|
+
QToolBar QToolButton:pressed {{
|
|
103
|
+
background-color: {BG};
|
|
104
|
+
}}
|
|
105
|
+
|
|
106
|
+
/* ===================== Menus ===================== */
|
|
107
|
+
QMenuBar {{
|
|
108
|
+
background-color: {PANEL};
|
|
109
|
+
color: {TEXT};
|
|
110
|
+
border-bottom: 1px solid {BORDER};
|
|
111
|
+
}}
|
|
112
|
+
QMenuBar::item {{
|
|
113
|
+
background: transparent;
|
|
114
|
+
padding: 4px 10px;
|
|
115
|
+
}}
|
|
116
|
+
QMenuBar::item:selected {{
|
|
117
|
+
background-color: {PANEL_HI};
|
|
118
|
+
}}
|
|
119
|
+
QMenu {{
|
|
120
|
+
background-color: {PANEL};
|
|
121
|
+
color: {TEXT};
|
|
122
|
+
border: 1px solid {BORDER};
|
|
123
|
+
}}
|
|
124
|
+
QMenu::item {{
|
|
125
|
+
padding: 5px 22px;
|
|
126
|
+
}}
|
|
127
|
+
QMenu::item:selected {{
|
|
128
|
+
background-color: {PANEL_HI};
|
|
129
|
+
color: {ACCENT};
|
|
130
|
+
}}
|
|
131
|
+
QMenu::separator {{
|
|
132
|
+
height: 1px;
|
|
133
|
+
background: {BORDER};
|
|
134
|
+
margin: 4px 6px;
|
|
135
|
+
}}
|
|
136
|
+
|
|
137
|
+
/* ===================== Tabs ===================== */
|
|
138
|
+
QTabWidget::pane {{
|
|
139
|
+
background-color: {PANEL};
|
|
140
|
+
border: 1px solid {BORDER};
|
|
141
|
+
top: -1px;
|
|
142
|
+
}}
|
|
143
|
+
QTabBar::tab {{
|
|
144
|
+
background-color: {BG};
|
|
145
|
+
color: {TEXT_DIM};
|
|
146
|
+
border: 1px solid {BORDER};
|
|
147
|
+
border-bottom: none;
|
|
148
|
+
padding: 6px 14px;
|
|
149
|
+
margin-right: 1px;
|
|
150
|
+
}}
|
|
151
|
+
QTabBar::tab:hover {{
|
|
152
|
+
color: {TEXT};
|
|
153
|
+
background-color: {PANEL_HI};
|
|
154
|
+
}}
|
|
155
|
+
QTabBar::tab:selected {{
|
|
156
|
+
background-color: {PANEL};
|
|
157
|
+
color: {ACCENT};
|
|
158
|
+
border-top: 2px solid {ACCENT};
|
|
159
|
+
}}
|
|
160
|
+
|
|
161
|
+
/* ===================== Group boxes ===================== */
|
|
162
|
+
QGroupBox {{
|
|
163
|
+
background-color: {PANEL};
|
|
164
|
+
border: 1px solid {BORDER};
|
|
165
|
+
border-radius: 3px;
|
|
166
|
+
margin-top: 14px;
|
|
167
|
+
padding: 10px 8px 8px 8px;
|
|
168
|
+
}}
|
|
169
|
+
QGroupBox::title {{
|
|
170
|
+
subcontrol-origin: margin;
|
|
171
|
+
subcontrol-position: top left;
|
|
172
|
+
left: 8px;
|
|
173
|
+
padding: 0 5px;
|
|
174
|
+
color: {ACCENT};
|
|
175
|
+
text-transform: uppercase;
|
|
176
|
+
letter-spacing: 1px;
|
|
177
|
+
}}
|
|
178
|
+
|
|
179
|
+
/* ===================== Buttons ===================== */
|
|
180
|
+
QPushButton {{
|
|
181
|
+
background-color: {PANEL};
|
|
182
|
+
color: {TEXT};
|
|
183
|
+
border: 1px solid {BORDER};
|
|
184
|
+
border-radius: 3px;
|
|
185
|
+
padding: 5px 14px;
|
|
186
|
+
}}
|
|
187
|
+
QPushButton:hover {{
|
|
188
|
+
background-color: {PANEL_HI};
|
|
189
|
+
border: 1px solid {ACCENT};
|
|
190
|
+
}}
|
|
191
|
+
QPushButton:pressed {{
|
|
192
|
+
background-color: {BG};
|
|
193
|
+
}}
|
|
194
|
+
QPushButton:focus {{
|
|
195
|
+
border: 1px solid {ACCENT};
|
|
196
|
+
}}
|
|
197
|
+
QPushButton:disabled {{
|
|
198
|
+
background-color: {PANEL};
|
|
199
|
+
color: {TEXT_DIM};
|
|
200
|
+
border: 1px solid {BORDER};
|
|
201
|
+
}}
|
|
202
|
+
QPushButton:checked {{
|
|
203
|
+
border: 1px solid {ACCENT};
|
|
204
|
+
color: {ACCENT};
|
|
205
|
+
}}
|
|
206
|
+
|
|
207
|
+
/* ===================== Inputs ===================== */
|
|
208
|
+
QLineEdit, QComboBox, QSpinBox, QDoubleSpinBox, QPlainTextEdit, QTextEdit, QAbstractSpinBox {{
|
|
209
|
+
background-color: {BG};
|
|
210
|
+
color: {TEXT};
|
|
211
|
+
border: 1px solid {BORDER};
|
|
212
|
+
border-radius: 3px;
|
|
213
|
+
padding: 4px 6px;
|
|
214
|
+
selection-background-color: {ACCENT};
|
|
215
|
+
selection-color: {BG};
|
|
216
|
+
}}
|
|
217
|
+
QLineEdit:hover, QComboBox:hover, QSpinBox:hover, QDoubleSpinBox:hover, QAbstractSpinBox:hover {{
|
|
218
|
+
border: 1px solid {BORDER_HI};
|
|
219
|
+
}}
|
|
220
|
+
QLineEdit:focus, QComboBox:focus, QSpinBox:focus, QDoubleSpinBox:focus,
|
|
221
|
+
QPlainTextEdit:focus, QTextEdit:focus, QAbstractSpinBox:focus {{
|
|
222
|
+
border: 1px solid {ACCENT};
|
|
223
|
+
border-bottom: 2px solid {ACCENT};
|
|
224
|
+
}}
|
|
225
|
+
QLineEdit:disabled, QComboBox:disabled, QSpinBox:disabled, QDoubleSpinBox:disabled {{
|
|
226
|
+
color: {TEXT_DIM};
|
|
227
|
+
background-color: {PANEL};
|
|
228
|
+
}}
|
|
229
|
+
QComboBox::drop-down {{
|
|
230
|
+
border: none;
|
|
231
|
+
width: 18px;
|
|
232
|
+
}}
|
|
233
|
+
QComboBox QAbstractItemView {{
|
|
234
|
+
background-color: {PANEL};
|
|
235
|
+
color: {TEXT};
|
|
236
|
+
border: 1px solid {BORDER};
|
|
237
|
+
selection-background-color: {PANEL_HI};
|
|
238
|
+
selection-color: {ACCENT};
|
|
239
|
+
outline: none;
|
|
240
|
+
}}
|
|
241
|
+
|
|
242
|
+
/* ===================== Check / radio ===================== */
|
|
243
|
+
QCheckBox, QRadioButton {{
|
|
244
|
+
background: transparent;
|
|
245
|
+
color: {TEXT};
|
|
246
|
+
spacing: 6px;
|
|
247
|
+
}}
|
|
248
|
+
QCheckBox::indicator, QRadioButton::indicator {{
|
|
249
|
+
width: 14px;
|
|
250
|
+
height: 14px;
|
|
251
|
+
background-color: {BG};
|
|
252
|
+
border: 1px solid {BORDER_HI};
|
|
253
|
+
}}
|
|
254
|
+
QCheckBox::indicator {{
|
|
255
|
+
border-radius: 2px;
|
|
256
|
+
}}
|
|
257
|
+
QRadioButton::indicator {{
|
|
258
|
+
border-radius: 7px;
|
|
259
|
+
}}
|
|
260
|
+
QCheckBox::indicator:hover, QRadioButton::indicator:hover {{
|
|
261
|
+
border: 1px solid {ACCENT};
|
|
262
|
+
}}
|
|
263
|
+
QCheckBox::indicator:checked, QRadioButton::indicator:checked {{
|
|
264
|
+
background-color: {ACCENT};
|
|
265
|
+
border: 1px solid {ACCENT};
|
|
266
|
+
}}
|
|
267
|
+
|
|
268
|
+
/* ===================== Tables / trees ===================== */
|
|
269
|
+
QTableView, QTreeView, QTableWidget, QListView {{
|
|
270
|
+
background-color: {PANEL};
|
|
271
|
+
alternate-background-color: {PANEL_HI};
|
|
272
|
+
color: {TEXT};
|
|
273
|
+
gridline-color: {BORDER};
|
|
274
|
+
border: 1px solid {BORDER};
|
|
275
|
+
selection-background-color: {PANEL_HI};
|
|
276
|
+
selection-color: {ACCENT};
|
|
277
|
+
outline: none;
|
|
278
|
+
}}
|
|
279
|
+
QTableView::item:selected, QTreeView::item:selected, QListView::item:selected {{
|
|
280
|
+
background-color: {PANEL_HI};
|
|
281
|
+
color: {ACCENT};
|
|
282
|
+
}}
|
|
283
|
+
QHeaderView::section {{
|
|
284
|
+
background-color: {BG};
|
|
285
|
+
color: {TEXT_DIM};
|
|
286
|
+
border: none;
|
|
287
|
+
border-right: 1px solid {BORDER};
|
|
288
|
+
border-bottom: 1px solid {BORDER};
|
|
289
|
+
padding: 5px 8px;
|
|
290
|
+
text-transform: uppercase;
|
|
291
|
+
letter-spacing: 1px;
|
|
292
|
+
}}
|
|
293
|
+
QTableCornerButton::section {{
|
|
294
|
+
background-color: {BG};
|
|
295
|
+
border: 1px solid {BORDER};
|
|
296
|
+
}}
|
|
297
|
+
|
|
298
|
+
/* ===================== Scrollbars (slim, mechanical) ===================== */
|
|
299
|
+
QScrollBar:vertical {{
|
|
300
|
+
background: {BG};
|
|
301
|
+
width: 11px;
|
|
302
|
+
margin: 0;
|
|
303
|
+
}}
|
|
304
|
+
QScrollBar:horizontal {{
|
|
305
|
+
background: {BG};
|
|
306
|
+
height: 11px;
|
|
307
|
+
margin: 0;
|
|
308
|
+
}}
|
|
309
|
+
QScrollBar::handle:vertical, QScrollBar::handle:horizontal {{
|
|
310
|
+
background: {BORDER};
|
|
311
|
+
border-radius: 2px;
|
|
312
|
+
min-height: 24px;
|
|
313
|
+
min-width: 24px;
|
|
314
|
+
margin: 2px;
|
|
315
|
+
}}
|
|
316
|
+
QScrollBar::handle:hover {{
|
|
317
|
+
background: {BORDER_HI};
|
|
318
|
+
}}
|
|
319
|
+
QScrollBar::add-line, QScrollBar::sub-line {{
|
|
320
|
+
height: 0; width: 0;
|
|
321
|
+
}}
|
|
322
|
+
QScrollBar::add-page, QScrollBar::sub-page {{
|
|
323
|
+
background: transparent;
|
|
324
|
+
}}
|
|
325
|
+
|
|
326
|
+
/* ===================== Splitter ===================== */
|
|
327
|
+
QSplitter::handle {{
|
|
328
|
+
background-color: {BORDER};
|
|
329
|
+
}}
|
|
330
|
+
QSplitter::handle:hover {{
|
|
331
|
+
background-color: {ACCENT};
|
|
332
|
+
}}
|
|
333
|
+
|
|
334
|
+
/* ===================== Misc ===================== */
|
|
335
|
+
QToolTip {{
|
|
336
|
+
background-color: {PANEL};
|
|
337
|
+
color: {TEXT};
|
|
338
|
+
border: 1px solid {ACCENT};
|
|
339
|
+
padding: 4px 6px;
|
|
340
|
+
}}
|
|
341
|
+
QProgressBar {{
|
|
342
|
+
background-color: {BG};
|
|
343
|
+
border: 1px solid {BORDER};
|
|
344
|
+
border-radius: 3px;
|
|
345
|
+
text-align: center;
|
|
346
|
+
color: {TEXT};
|
|
347
|
+
}}
|
|
348
|
+
QProgressBar::chunk {{
|
|
349
|
+
background-color: {ACCENT};
|
|
350
|
+
}}
|
|
351
|
+
QLabel {{
|
|
352
|
+
background: transparent;
|
|
353
|
+
}}
|
|
354
|
+
"""
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
# ---------------------------------------------------------------------------
|
|
358
|
+
# Helpers for special widget states
|
|
359
|
+
# ---------------------------------------------------------------------------
|
|
360
|
+
def record_button_qss() -> str:
|
|
361
|
+
"""QSS for the record button: idle gunmetal → armed bright phosphor.
|
|
362
|
+
|
|
363
|
+
Use ``setProperty("armed", True)`` + ``style().polish()`` to switch the
|
|
364
|
+
button into its active (bright phosphor) recording state.
|
|
365
|
+
"""
|
|
366
|
+
return f"""
|
|
367
|
+
QPushButton {{
|
|
368
|
+
background-color: {PANEL};
|
|
369
|
+
color: {TEXT};
|
|
370
|
+
border: 1px solid {DANGER};
|
|
371
|
+
border-radius: 3px;
|
|
372
|
+
padding: 6px 12px;
|
|
373
|
+
}}
|
|
374
|
+
QPushButton:hover {{
|
|
375
|
+
background-color: {PANEL_HI};
|
|
376
|
+
border: 1px solid {ACCENT_HI};
|
|
377
|
+
}}
|
|
378
|
+
QPushButton:pressed {{
|
|
379
|
+
background-color: {BG};
|
|
380
|
+
}}
|
|
381
|
+
QPushButton[armed="true"] {{
|
|
382
|
+
background-color: {DANGER};
|
|
383
|
+
color: {BG};
|
|
384
|
+
border: 1px solid {ACCENT_HI};
|
|
385
|
+
}}
|
|
386
|
+
"""
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def terminal_qss() -> str:
|
|
390
|
+
"""QSS for the embedded IPython console (monospace phosphor-on-panel)."""
|
|
391
|
+
return f"""
|
|
392
|
+
RichJupyterWidget {{
|
|
393
|
+
background-color: {PANEL};
|
|
394
|
+
}}
|
|
395
|
+
QPlainTextEdit, QTextEdit {{
|
|
396
|
+
background-color: {PANEL};
|
|
397
|
+
color: {ACCENT_HI};
|
|
398
|
+
font-family: {MONO_FONT};
|
|
399
|
+
}}
|
|
400
|
+
QLabel {{
|
|
401
|
+
color: {ACCENT_HI};
|
|
402
|
+
}}
|
|
403
|
+
"""
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
# ---------------------------------------------------------------------------
|
|
407
|
+
# Application entry point
|
|
408
|
+
# ---------------------------------------------------------------------------
|
|
409
|
+
def _build_palette() -> QPalette:
|
|
410
|
+
"""Construct a dark QPalette so native / unstyled widgets render dark.
|
|
411
|
+
|
|
412
|
+
This also guards against the known Windows 11 dark-mode issue where Qt
|
|
413
|
+
paints dark backgrounds on native controls but leaves text dark.
|
|
414
|
+
"""
|
|
415
|
+
p = QPalette()
|
|
416
|
+
p.setColor(QPalette.ColorRole.Window, QColor(BG))
|
|
417
|
+
p.setColor(QPalette.ColorRole.WindowText, QColor(TEXT))
|
|
418
|
+
p.setColor(QPalette.ColorRole.Base, QColor(BG))
|
|
419
|
+
p.setColor(QPalette.ColorRole.AlternateBase, QColor(PANEL_HI))
|
|
420
|
+
p.setColor(QPalette.ColorRole.Text, QColor(TEXT))
|
|
421
|
+
p.setColor(QPalette.ColorRole.PlaceholderText, QColor(TEXT_DIM))
|
|
422
|
+
p.setColor(QPalette.ColorRole.Button, QColor(PANEL))
|
|
423
|
+
p.setColor(QPalette.ColorRole.ButtonText, QColor(TEXT))
|
|
424
|
+
p.setColor(QPalette.ColorRole.BrightText, QColor(ACCENT_HI))
|
|
425
|
+
p.setColor(QPalette.ColorRole.Highlight, QColor(ACCENT))
|
|
426
|
+
p.setColor(QPalette.ColorRole.HighlightedText, QColor(BG))
|
|
427
|
+
p.setColor(QPalette.ColorRole.ToolTipBase, QColor(PANEL))
|
|
428
|
+
p.setColor(QPalette.ColorRole.ToolTipText, QColor(TEXT))
|
|
429
|
+
p.setColor(QPalette.ColorRole.Link, QColor(ACCENT))
|
|
430
|
+
|
|
431
|
+
# Disabled-state roles.
|
|
432
|
+
disabled = QColor(TEXT_DIM)
|
|
433
|
+
for role in (
|
|
434
|
+
QPalette.ColorRole.WindowText,
|
|
435
|
+
QPalette.ColorRole.Text,
|
|
436
|
+
QPalette.ColorRole.ButtonText,
|
|
437
|
+
):
|
|
438
|
+
p.setColor(QPalette.ColorGroup.Disabled, role, disabled)
|
|
439
|
+
return p
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def apply_theme(app: QApplication) -> None:
|
|
443
|
+
"""Apply the Mesofield dark theme to *app* (palette first, then QSS)."""
|
|
444
|
+
app.setPalette(_build_palette())
|
|
445
|
+
app.setStyleSheet(STYLESHEET)
|