misleep 0.3.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.
- misleep/__init__.py +111 -0
- misleep/__main__.py +16 -0
- misleep/analysis/__init__.py +23 -0
- misleep/analysis/auto_stage.py +158 -0
- misleep/analysis/detection.py +198 -0
- misleep/analysis/features.py +131 -0
- misleep/analysis/models/P30_EEG_F_lightgbm.pkl +0 -0
- misleep/analysis/models/P30_EEG_P_lightgbm.pkl +0 -0
- misleep/analysis/models/__init__.py +0 -0
- misleep/analysis/models/ado_EEG_F_lightgbm.pkl +0 -0
- misleep/analysis/models/ado_EEG_P_lightgbm.pkl +0 -0
- misleep/analysis/models/adult_EEG_F_lightgbm.pkl +0 -0
- misleep/analysis/models/adult_EEG_P_lightgbm.pkl +0 -0
- misleep/analysis/transformer/__init__.py +46 -0
- misleep/analysis/transformer/checkpoints/CausalTransformer_best.pt +0 -0
- misleep/analysis/transformer/checkpoints/__init__.py +0 -0
- misleep/analysis/transformer/configs.py +52 -0
- misleep/analysis/transformer/data.py +362 -0
- misleep/analysis/transformer/inference.py +328 -0
- misleep/analysis/transformer/models.py +220 -0
- misleep/analysis/transformer/preprocessing.py +105 -0
- misleep/config/__init__.py +105 -0
- misleep/config/default_config.ini +38 -0
- misleep/data/__init__.py +13 -0
- misleep/data/annotation.py +114 -0
- misleep/data/midata.py +387 -0
- misleep/gui/__init__.py +23 -0
- misleep/gui/app.py +183 -0
- misleep/gui/config_dialog.py +429 -0
- misleep/gui/dialogs.py +1107 -0
- misleep/gui/event_filters.py +26 -0
- misleep/gui/main_window.py +2699 -0
- misleep/gui/qt_utils.py +277 -0
- misleep/gui/resources/__init__.py +9 -0
- misleep/gui/resources/entire_logo.png +0 -0
- misleep/gui/resources/logo.png +0 -0
- misleep/gui/resources/misleep.ico +0 -0
- misleep/gui/resources/misleep.qrc +7 -0
- misleep/gui/resources/misleep_rc.py +1324 -0
- misleep/gui/spec_window.py +187 -0
- misleep/gui/style.py +994 -0
- misleep/gui/uis/SWA_detect_dialog.ui +181 -0
- misleep/gui/uis/SWA_detect_dialog_ui.py +176 -0
- misleep/gui/uis/__init__.py +6 -0
- misleep/gui/uis/about.ui +88 -0
- misleep/gui/uis/about_ui.py +80 -0
- misleep/gui/uis/auto_stage_causalTransformer_dialog.ui +77 -0
- misleep/gui/uis/auto_stage_causalTransformer_dialog_ui.py +90 -0
- misleep/gui/uis/auto_stage_lightGBM_dialog.ui +124 -0
- misleep/gui/uis/auto_stage_lightGBM_dialog_ui.py +124 -0
- misleep/gui/uis/horizontal_line_dialog.ui +166 -0
- misleep/gui/uis/horizontal_line_dialog_ui.py +157 -0
- misleep/gui/uis/label_dialog.ui +65 -0
- misleep/gui/uis/label_dialog_ui.py +72 -0
- misleep/gui/uis/main_window.ui +871 -0
- misleep/gui/uis/main_window_ui.py +616 -0
- misleep/gui/uis/save_data_dialog.ui +95 -0
- misleep/gui/uis/save_data_dialog_ui.py +98 -0
- misleep/gui/uis/spec_window.ui +109 -0
- misleep/gui/uis/spec_window_ui.py +103 -0
- misleep/gui/uis/spindle_detect_dialog.ui +202 -0
- misleep/gui/uis/spindle_detect_dialog_ui.py +194 -0
- misleep/gui/uis/state_spectral_dialog.ui +240 -0
- misleep/gui/uis/state_spectral_dialog_ui.py +204 -0
- misleep/gui/uis/transfer_result_dialog.ui +104 -0
- misleep/gui/uis/transfer_result_dialog_ui.py +94 -0
- misleep/gui/workers.py +69 -0
- misleep/io/__init__.py +68 -0
- misleep/io/annotation.py +367 -0
- misleep/io/array.py +209 -0
- misleep/io/base.py +174 -0
- misleep/io/edf.py +109 -0
- misleep/io/mat.py +189 -0
- misleep/logger.py +79 -0
- misleep/preprocessing/__init__.py +23 -0
- misleep/preprocessing/artifacts.py +76 -0
- misleep/preprocessing/filtering.py +94 -0
- misleep/preprocessing/segment.py +59 -0
- misleep/preprocessing/spectral.py +169 -0
- misleep/utils/__init__.py +45 -0
- misleep/utils/annotation.py +130 -0
- misleep/utils/entropy.py +135 -0
- misleep/utils/misc.py +69 -0
- misleep/utils/time_utils.py +61 -0
- misleep/viz/__init__.py +12 -0
- misleep/viz/hypnogram.py +48 -0
- misleep/viz/signals.py +51 -0
- misleep/viz/spectral.py +98 -0
- misleep-0.3.0.dist-info/METADATA +249 -0
- misleep-0.3.0.dist-info/RECORD +94 -0
- misleep-0.3.0.dist-info/WHEEL +5 -0
- misleep-0.3.0.dist-info/entry_points.txt +15 -0
- misleep-0.3.0.dist-info/licenses/LICENSE +29 -0
- misleep-0.3.0.dist-info/top_level.txt +1 -0
misleep/gui/style.py
ADDED
|
@@ -0,0 +1,994 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
"""Theme system for the MiSleep GUI: light & dark Qt styling + matplotlib theming.
|
|
3
|
+
|
|
4
|
+
Everything visual lives here so the look of MiSleep is:
|
|
5
|
+
|
|
6
|
+
* **consistent** -- one palette drives the Qt widgets *and* the matplotlib
|
|
7
|
+
canvases, so the plot area and the surrounding chrome feel like a single
|
|
8
|
+
designed surface,
|
|
9
|
+
* **precise** -- every control has an explicit hover / pressed / focus /
|
|
10
|
+
disabled state, a calm rounded geometry and a clear accent colour,
|
|
11
|
+
* **portable** -- sizes are expressed in ``pt`` / logical pixels (Qt scales
|
|
12
|
+
them with the display DPI), fonts are picked per platform, and the same
|
|
13
|
+
stylesheet renders on Windows, macOS and Linux (a ``Fusion`` base style is
|
|
14
|
+
used so the QSS result is identical everywhere),
|
|
15
|
+
* **data friendly** -- the matplotlib theme keeps the plots on high-contrast
|
|
16
|
+
surfaces with soft grids and legible ticks.
|
|
17
|
+
|
|
18
|
+
The public API is small::
|
|
19
|
+
|
|
20
|
+
apply_theme(app, "light") # full theme: style + fonts + matplotlib
|
|
21
|
+
apply_theme(app, "dark")
|
|
22
|
+
retheme_figures("dark") # restyle every open figure after a toggle
|
|
23
|
+
apply_app_style(app) # backward compatible alias -> light theme
|
|
24
|
+
|
|
25
|
+
The previous behaviour of :func:`apply_app_style` (a fixed light stylesheet)
|
|
26
|
+
is preserved for compatibility.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import copy
|
|
32
|
+
import sys
|
|
33
|
+
from string import Template
|
|
34
|
+
|
|
35
|
+
from PySide6.QtGui import QFont
|
|
36
|
+
from PySide6.QtWidgets import QApplication
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
# Palettes
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
THEMES = {
|
|
43
|
+
"light": {
|
|
44
|
+
"name": "Light",
|
|
45
|
+
# surfaces
|
|
46
|
+
"window": "#d8dee8",
|
|
47
|
+
"surface": "#f2f4f7",
|
|
48
|
+
"surface_alt": "#cfd7e3",
|
|
49
|
+
"input": "#f8f9fb",
|
|
50
|
+
"disabled_bg": "#d9dfe8",
|
|
51
|
+
# lines
|
|
52
|
+
"border": "#aeb9c8",
|
|
53
|
+
"border_strong": "#75859b",
|
|
54
|
+
"button_top": "#f8f9fb",
|
|
55
|
+
"button_bottom": "#cbd4e0",
|
|
56
|
+
# text
|
|
57
|
+
"text": "#141d2b",
|
|
58
|
+
"text_secondary": "#4a5a70",
|
|
59
|
+
"text_disabled": "#97a3b3",
|
|
60
|
+
# accent (indigo)
|
|
61
|
+
"accent": "#4b56d9",
|
|
62
|
+
"accent_hover": "#3d47c2",
|
|
63
|
+
"accent_pressed": "#333ba6",
|
|
64
|
+
"accent_soft": "#e2e7fc",
|
|
65
|
+
"accent_soft_2": "#d4dbfa",
|
|
66
|
+
# semantic
|
|
67
|
+
"danger": "#dc3d43",
|
|
68
|
+
"danger_hover": "#c43339",
|
|
69
|
+
"success": "#148a5e",
|
|
70
|
+
# misc
|
|
71
|
+
"tooltip_bg": "#1c2736",
|
|
72
|
+
"tooltip_text": "#f4f7fb",
|
|
73
|
+
"scroll_handle": "#aeb9cb",
|
|
74
|
+
"scroll_handle_hover": "#8b9ab2",
|
|
75
|
+
"slider_groove": "#cfd8e6",
|
|
76
|
+
# matplotlib / plot area
|
|
77
|
+
"plot": {"trace": "#0d1524", "grid": "#2f9d74", "bg": "#ffffff"},
|
|
78
|
+
"mpl": {
|
|
79
|
+
"figure.facecolor": "#d8dee8",
|
|
80
|
+
"axes.facecolor": "#ffffff",
|
|
81
|
+
"axes.edgecolor": "#a9b6c9",
|
|
82
|
+
"axes.labelcolor": "#141d2b",
|
|
83
|
+
"xtick.color": "#4a5a70",
|
|
84
|
+
"ytick.color": "#4a5a70",
|
|
85
|
+
"text.color": "#141d2b",
|
|
86
|
+
"grid.color": "#dbe2ec",
|
|
87
|
+
"savefig.facecolor": "#ffffff",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
"dark": {
|
|
91
|
+
"name": "Dark",
|
|
92
|
+
# surfaces
|
|
93
|
+
"window": "#10151f",
|
|
94
|
+
"surface": "#171e2b",
|
|
95
|
+
"surface_alt": "#1e2736",
|
|
96
|
+
"input": "#131a25",
|
|
97
|
+
"disabled_bg": "#171e2b",
|
|
98
|
+
# lines
|
|
99
|
+
"border": "#2c374a",
|
|
100
|
+
"border_strong": "#596984",
|
|
101
|
+
"button_top": "#273247",
|
|
102
|
+
"button_bottom": "#192230",
|
|
103
|
+
# text
|
|
104
|
+
"text": "#eef2f9",
|
|
105
|
+
"text_secondary": "#9aa8bc",
|
|
106
|
+
"text_disabled": "#5b6a80",
|
|
107
|
+
# accent (indigo)
|
|
108
|
+
"accent": "#7a84f0",
|
|
109
|
+
"accent_hover": "#959df5",
|
|
110
|
+
"accent_pressed": "#616bd9",
|
|
111
|
+
"accent_soft": "#28304f",
|
|
112
|
+
"accent_soft_2": "#313b60",
|
|
113
|
+
# semantic
|
|
114
|
+
"danger": "#f0606b",
|
|
115
|
+
"danger_hover": "#f27c85",
|
|
116
|
+
"success": "#2bc48a",
|
|
117
|
+
# misc
|
|
118
|
+
"tooltip_bg": "#2c374a",
|
|
119
|
+
"tooltip_text": "#eef2f9",
|
|
120
|
+
"scroll_handle": "#414e68",
|
|
121
|
+
"scroll_handle_hover": "#5a6a8a",
|
|
122
|
+
"slider_groove": "#2c374a",
|
|
123
|
+
# matplotlib / plot area
|
|
124
|
+
"plot": {"trace": "#d9e3f2", "grid": "#3fae85", "bg": "#101824"},
|
|
125
|
+
"mpl": {
|
|
126
|
+
"figure.facecolor": "#10151f",
|
|
127
|
+
"axes.facecolor": "#101824",
|
|
128
|
+
"axes.edgecolor": "#414e68",
|
|
129
|
+
"axes.labelcolor": "#eef2f9",
|
|
130
|
+
"xtick.color": "#9aa8bc",
|
|
131
|
+
"ytick.color": "#9aa8bc",
|
|
132
|
+
"text.color": "#eef2f9",
|
|
133
|
+
"grid.color": "#232f41",
|
|
134
|
+
"savefig.facecolor": "#101824",
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# Color-tone presets are independent from light/dark mode. They change the
|
|
140
|
+
# application chrome and accent colors while keeping data/state colors under
|
|
141
|
+
# the user's explicit control in Settings.
|
|
142
|
+
COLOR_TONES = {
|
|
143
|
+
"black": {
|
|
144
|
+
"name": "Black / Gray",
|
|
145
|
+
"light": {
|
|
146
|
+
"window": "#d6d6d6", "surface": "#f0f0f0",
|
|
147
|
+
"surface_alt": "#c9c9c9", "input": "#f8f8f8",
|
|
148
|
+
"disabled_bg": "#d9d9d9", "border": "#aaaaaa",
|
|
149
|
+
"border_strong": "#666a70", "button_top": "#fafafa",
|
|
150
|
+
"button_bottom": "#c8c8c8", "text": "#151515",
|
|
151
|
+
"text_secondary": "#414141", "text_disabled": "#858585",
|
|
152
|
+
"accent": "#3c3f44", "accent_hover": "#24272b",
|
|
153
|
+
"accent_pressed": "#111315", "accent_soft": "#d3d3d3",
|
|
154
|
+
"accent_soft_2": "#bdbdbd", "scroll_handle": "#969696",
|
|
155
|
+
"scroll_handle_hover": "#707070", "slider_groove": "#bcbcbc",
|
|
156
|
+
"plot": {"grid": "#737373"},
|
|
157
|
+
"mpl": {"figure.facecolor": "#d6d6d6",
|
|
158
|
+
"axes.edgecolor": "#8a8a8a", "xtick.color": "#414141",
|
|
159
|
+
"ytick.color": "#414141", "grid.color": "#d0d0d0"},
|
|
160
|
+
},
|
|
161
|
+
"dark": {
|
|
162
|
+
"accent": "#aeb2b8", "accent_hover": "#d0d2d5",
|
|
163
|
+
"accent_pressed": "#858a91", "accent_soft": "#303236",
|
|
164
|
+
"accent_soft_2": "#3c3f44", "text_secondary": "#b4b4b4",
|
|
165
|
+
"plot": {"grid": "#777b80"},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
"pink": {
|
|
169
|
+
"name": "Pink",
|
|
170
|
+
"light": {
|
|
171
|
+
"window": "#ded4d8", "surface": "#f5f0f2",
|
|
172
|
+
"surface_alt": "#d8c6cd", "input": "#fbf8f9",
|
|
173
|
+
"disabled_bg": "#e2dadd", "border": "#bca5ae",
|
|
174
|
+
"border_strong": "#8d6f7b", "button_top": "#fffafb",
|
|
175
|
+
"button_bottom": "#d9c4cc", "text": "#241b1f",
|
|
176
|
+
"text_secondary": "#604b54", "text_disabled": "#9d8991",
|
|
177
|
+
"accent": "#a63f69", "accent_hover": "#862d51",
|
|
178
|
+
"accent_pressed": "#68213e", "accent_soft": "#ead1db",
|
|
179
|
+
"accent_soft_2": "#ddb8c7", "scroll_handle": "#b88b9d",
|
|
180
|
+
"scroll_handle_hover": "#996a7d", "slider_groove": "#d6bbc6",
|
|
181
|
+
"plot": {"grid": "#a85777"},
|
|
182
|
+
"mpl": {"figure.facecolor": "#ded4d8",
|
|
183
|
+
"axes.edgecolor": "#aa8b98", "xtick.color": "#604b54",
|
|
184
|
+
"ytick.color": "#604b54", "grid.color": "#eadde2"},
|
|
185
|
+
},
|
|
186
|
+
"dark": {"accent": "#df7da4", "accent_hover": "#ee9fbe",
|
|
187
|
+
"accent_pressed": "#bd5f85", "accent_soft": "#482a37",
|
|
188
|
+
"accent_soft_2": "#593344"},
|
|
189
|
+
},
|
|
190
|
+
"blue": {
|
|
191
|
+
"name": "Blue",
|
|
192
|
+
"light": {},
|
|
193
|
+
"dark": {},
|
|
194
|
+
},
|
|
195
|
+
"khaki": {
|
|
196
|
+
"name": "Khaki",
|
|
197
|
+
"light": {
|
|
198
|
+
"window": "#ddd9c9", "surface": "#f3f1e8",
|
|
199
|
+
"surface_alt": "#d4cfb8", "input": "#faf9f3",
|
|
200
|
+
"disabled_bg": "#e0ddcf", "border": "#b8af8e",
|
|
201
|
+
"border_strong": "#82785b", "button_top": "#fbfaf3",
|
|
202
|
+
"button_bottom": "#d2ccb3", "text": "#242117",
|
|
203
|
+
"text_secondary": "#5d5742", "text_disabled": "#99927b",
|
|
204
|
+
"accent": "#71663a", "accent_hover": "#5a512d",
|
|
205
|
+
"accent_pressed": "#40391e", "accent_soft": "#e2ddc4",
|
|
206
|
+
"accent_soft_2": "#d2c9a6", "scroll_handle": "#aaa17f",
|
|
207
|
+
"scroll_handle_hover": "#8c825f", "slider_groove": "#cbc4a7",
|
|
208
|
+
"plot": {"grid": "#847a49"},
|
|
209
|
+
"mpl": {"figure.facecolor": "#ddd9c9",
|
|
210
|
+
"axes.edgecolor": "#9f9676", "xtick.color": "#5d5742",
|
|
211
|
+
"ytick.color": "#5d5742", "grid.color": "#e3dfd0"},
|
|
212
|
+
},
|
|
213
|
+
"dark": {"accent": "#c4b76f", "accent_hover": "#d8cd8d",
|
|
214
|
+
"accent_pressed": "#9f944f", "accent_soft": "#3d3928",
|
|
215
|
+
"accent_soft_2": "#4b4630"},
|
|
216
|
+
},
|
|
217
|
+
"blue": {
|
|
218
|
+
"name": "Blue",
|
|
219
|
+
"light": {
|
|
220
|
+
"window": "#d6dde8", "surface": "#eef3f9",
|
|
221
|
+
"surface_alt": "#c9d6e6", "input": "#f7fafd",
|
|
222
|
+
"disabled_bg": "#dde4ee", "border": "#a9bcd4",
|
|
223
|
+
"border_strong": "#6f87a5", "button_top": "#fafcfe",
|
|
224
|
+
"button_bottom": "#c2d2e6", "text": "#16202e",
|
|
225
|
+
"text_secondary": "#41536b", "text_disabled": "#8b9ab0",
|
|
226
|
+
"accent": "#2f5f9e", "accent_hover": "#274f85",
|
|
227
|
+
"accent_pressed": "#1e3f6b", "accent_soft": "#d4e2f3",
|
|
228
|
+
"accent_soft_2": "#c0d4ec", "scroll_handle": "#8fa6c2",
|
|
229
|
+
"scroll_handle_hover": "#6d87a8", "slider_groove": "#b9cbe2",
|
|
230
|
+
"plot": {"grid": "#3e76b0"},
|
|
231
|
+
"mpl": {"figure.facecolor": "#d6dde8",
|
|
232
|
+
"axes.edgecolor": "#7f96b4", "xtick.color": "#41536b",
|
|
233
|
+
"ytick.color": "#41536b", "grid.color": "#dfe7f1"},
|
|
234
|
+
},
|
|
235
|
+
"dark": {"accent": "#6d9fd8", "accent_hover": "#8cb6e8",
|
|
236
|
+
"accent_pressed": "#5182bd", "accent_soft": "#23344d",
|
|
237
|
+
"accent_soft_2": "#2b405f"},
|
|
238
|
+
},
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def resolved_theme(theme_name: str = "light", tone_name: str = "black") -> dict:
|
|
243
|
+
"""Return a theme with the selected color-tone overrides applied."""
|
|
244
|
+
theme_name = theme_name if theme_name in THEMES else "light"
|
|
245
|
+
tone_name = tone_name if tone_name in COLOR_TONES else "black"
|
|
246
|
+
result = copy.deepcopy(THEMES[theme_name])
|
|
247
|
+
for key, value in COLOR_TONES[tone_name][theme_name].items():
|
|
248
|
+
if isinstance(value, dict):
|
|
249
|
+
result[key].update(value)
|
|
250
|
+
else:
|
|
251
|
+
result[key] = value
|
|
252
|
+
return result
|
|
253
|
+
|
|
254
|
+
#: Buttons that act as *primary* actions get the accent colour.
|
|
255
|
+
_PRIMARY_BUTTONS = (
|
|
256
|
+
"SaveLabelBt", "FilterConfirmBt", "MultipleScalerConfirmBt",
|
|
257
|
+
"PlotSpecBt", "LabelBt",
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
#: Buttons that destroy things get the danger colour.
|
|
261
|
+
_DANGER_BUTTONS = ("DeleteChBt",)
|
|
262
|
+
|
|
263
|
+
_PRIMARY_SELECTOR = ", ".join(f"#{name}" for name in _PRIMARY_BUTTONS)
|
|
264
|
+
_DANGER_SELECTOR = ", ".join(f"#{name}" for name in _DANGER_BUTTONS)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _state_selector(names, state):
|
|
268
|
+
"""Apply a pseudo-state to every selector in a selector group."""
|
|
269
|
+
return ", ".join(f"#{name}:{state}" for name in names)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def font_families() -> list[str]:
|
|
273
|
+
"""Return the recommended font stack for the current platform."""
|
|
274
|
+
if sys.platform == "win32":
|
|
275
|
+
return ["Segoe UI", "Microsoft YaHei UI", "PingFang SC",
|
|
276
|
+
"Noto Sans CJK SC", "Arial"]
|
|
277
|
+
if sys.platform == "darwin":
|
|
278
|
+
return ["SF Pro Text", "PingFang SC", "Helvetica Neue", "Arial"]
|
|
279
|
+
return ["Noto Sans", "DejaVu Sans", "Liberation Sans", "sans-serif"]
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# ---------------------------------------------------------------------------
|
|
283
|
+
# Qt stylesheet
|
|
284
|
+
# ---------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
_STYLESHEET_TEMPLATE = Template("""
|
|
287
|
+
/* ================= MiSleep theme ================= */
|
|
288
|
+
|
|
289
|
+
QMainWindow, QDialog, QMessageBox, QInputDialog,
|
|
290
|
+
QColorDialog, QFileDialog, QFontDialog {
|
|
291
|
+
background-color: $window;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
QWidget {
|
|
295
|
+
font-size: 9.5pt;
|
|
296
|
+
color: $text;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/* ---------------- menu bar & menus ---------------- */
|
|
300
|
+
QMenuBar {
|
|
301
|
+
background-color: $window;
|
|
302
|
+
border-bottom: 1px solid $border;
|
|
303
|
+
padding: 2px 4px;
|
|
304
|
+
}
|
|
305
|
+
QMenuBar::item {
|
|
306
|
+
background: transparent;
|
|
307
|
+
padding: 4px 10px;
|
|
308
|
+
border-radius: 0px;
|
|
309
|
+
color: $text;
|
|
310
|
+
}
|
|
311
|
+
QMenuBar::item:selected {
|
|
312
|
+
background: $accent_soft;
|
|
313
|
+
color: $text;
|
|
314
|
+
}
|
|
315
|
+
QMenuBar::item:pressed {
|
|
316
|
+
background: $accent_soft_2;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
QMenu {
|
|
320
|
+
background-color: $surface;
|
|
321
|
+
border: 1px solid $border;
|
|
322
|
+
border-radius: 0px;
|
|
323
|
+
padding: 5px;
|
|
324
|
+
}
|
|
325
|
+
QMenu::item {
|
|
326
|
+
padding: 5px 26px 5px 10px;
|
|
327
|
+
border-radius: 0px;
|
|
328
|
+
color: $text;
|
|
329
|
+
}
|
|
330
|
+
QMenu::item:selected {
|
|
331
|
+
background: $accent_soft;
|
|
332
|
+
color: $text;
|
|
333
|
+
}
|
|
334
|
+
QMenu::item:disabled {
|
|
335
|
+
color: $text_disabled;
|
|
336
|
+
}
|
|
337
|
+
QMenu::separator {
|
|
338
|
+
height: 1px;
|
|
339
|
+
background: $border;
|
|
340
|
+
margin: 5px 8px;
|
|
341
|
+
}
|
|
342
|
+
QMenu::indicator {
|
|
343
|
+
width: 14px;
|
|
344
|
+
height: 14px;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/* ---------------- toolbar ---------------- */
|
|
348
|
+
QToolBar {
|
|
349
|
+
background: $surface;
|
|
350
|
+
border-bottom: 1px solid $border;
|
|
351
|
+
spacing: 3px;
|
|
352
|
+
padding: 4px 8px;
|
|
353
|
+
}
|
|
354
|
+
QToolBar::separator {
|
|
355
|
+
background: $border;
|
|
356
|
+
width: 1px;
|
|
357
|
+
margin: 5px 8px;
|
|
358
|
+
}
|
|
359
|
+
QToolButton {
|
|
360
|
+
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
|
361
|
+
stop:0 $button_top, stop:1 $button_bottom);
|
|
362
|
+
border: 1px solid $border_strong;
|
|
363
|
+
border-radius: 4px;
|
|
364
|
+
padding: 3px 8px;
|
|
365
|
+
color: $text;
|
|
366
|
+
}
|
|
367
|
+
QToolButton:hover {
|
|
368
|
+
background: $accent_soft;
|
|
369
|
+
}
|
|
370
|
+
QToolButton:pressed, QToolButton:checked {
|
|
371
|
+
background: $accent_soft_2;
|
|
372
|
+
color: $accent;
|
|
373
|
+
}
|
|
374
|
+
QToolButton:disabled {
|
|
375
|
+
color: $text_disabled;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/* ---------------- buttons ---------------- */
|
|
379
|
+
QPushButton {
|
|
380
|
+
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
|
381
|
+
stop:0 $button_top, stop:1 $button_bottom);
|
|
382
|
+
border: 1px solid $border_strong;
|
|
383
|
+
border-radius: 4px;
|
|
384
|
+
padding: 2px 9px;
|
|
385
|
+
color: $text;
|
|
386
|
+
min-height: 20px;
|
|
387
|
+
max-height: 24px;
|
|
388
|
+
}
|
|
389
|
+
QPushButton:hover {
|
|
390
|
+
background: $surface_alt;
|
|
391
|
+
border-color: $accent;
|
|
392
|
+
}
|
|
393
|
+
QPushButton:pressed {
|
|
394
|
+
background: $accent_soft_2;
|
|
395
|
+
}
|
|
396
|
+
QPushButton:focus {
|
|
397
|
+
border-color: $accent;
|
|
398
|
+
}
|
|
399
|
+
QPushButton:disabled {
|
|
400
|
+
color: $text_disabled;
|
|
401
|
+
background: $disabled_bg;
|
|
402
|
+
border-color: $border;
|
|
403
|
+
}
|
|
404
|
+
QPushButton:checked {
|
|
405
|
+
background: $accent_soft;
|
|
406
|
+
border-color: $accent;
|
|
407
|
+
color: $accent;
|
|
408
|
+
}
|
|
409
|
+
QPushButton[default="true"] {
|
|
410
|
+
background: $accent;
|
|
411
|
+
border-color: $accent;
|
|
412
|
+
color: #ffffff;
|
|
413
|
+
}
|
|
414
|
+
QPushButton[default="true"]:hover {
|
|
415
|
+
background: $accent_hover;
|
|
416
|
+
border-color: $accent_hover;
|
|
417
|
+
}
|
|
418
|
+
QPushButton[default="true"]:pressed {
|
|
419
|
+
background: $accent_pressed;
|
|
420
|
+
border-color: $accent_pressed;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/* channel-move arrows share a row and expand like Show/Hide/Delete */
|
|
424
|
+
#MoveUpBt, #MoveDownBt {
|
|
425
|
+
min-height: 20px;
|
|
426
|
+
max-height: 24px;
|
|
427
|
+
min-width: 52px;
|
|
428
|
+
padding: 0px;
|
|
429
|
+
font-size: 10pt;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/* primary actions */
|
|
433
|
+
$_primary {
|
|
434
|
+
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
|
435
|
+
stop:0 $accent, stop:1 $accent_pressed);
|
|
436
|
+
border-color: $accent;
|
|
437
|
+
color: #ffffff;
|
|
438
|
+
font-weight: 600;
|
|
439
|
+
}
|
|
440
|
+
$_primary_hover {
|
|
441
|
+
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
|
442
|
+
stop:0 $accent_hover, stop:1 $accent);
|
|
443
|
+
border-color: $accent_hover;
|
|
444
|
+
}
|
|
445
|
+
$_primary_pressed {
|
|
446
|
+
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
|
447
|
+
stop:0 $accent_pressed, stop:1 $accent_pressed);
|
|
448
|
+
border-color: $accent_pressed;
|
|
449
|
+
}
|
|
450
|
+
$_primary_disabled {
|
|
451
|
+
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
|
452
|
+
stop:0 $disabled_bg, stop:1 $disabled_bg);
|
|
453
|
+
border-color: $border;
|
|
454
|
+
color: $text_disabled;
|
|
455
|
+
font-weight: 400;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/* destructive actions */
|
|
459
|
+
$_danger {
|
|
460
|
+
background: $surface;
|
|
461
|
+
border-color: $danger;
|
|
462
|
+
color: $danger;
|
|
463
|
+
}
|
|
464
|
+
$_danger_hover {
|
|
465
|
+
background: $danger_hover;
|
|
466
|
+
border-color: $danger_hover;
|
|
467
|
+
color: #ffffff;
|
|
468
|
+
}
|
|
469
|
+
$_danger_pressed {
|
|
470
|
+
background: $danger;
|
|
471
|
+
border-color: $danger;
|
|
472
|
+
color: #ffffff;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/* ---------------- inputs ---------------- */
|
|
476
|
+
QComboBox, QSpinBox, QDoubleSpinBox, QLineEdit, QDateTimeEdit,
|
|
477
|
+
QTimeEdit, QDateEdit, QPlainTextEdit, QTextEdit {
|
|
478
|
+
background-color: $input;
|
|
479
|
+
border: 1px solid $border_strong;
|
|
480
|
+
border-radius: 4px;
|
|
481
|
+
padding: 2px 7px;
|
|
482
|
+
selection-background-color: $accent;
|
|
483
|
+
selection-color: #ffffff;
|
|
484
|
+
color: $text;
|
|
485
|
+
}
|
|
486
|
+
QComboBox, QSpinBox, QDoubleSpinBox, QLineEdit, QDateTimeEdit,
|
|
487
|
+
QTimeEdit, QDateEdit {
|
|
488
|
+
min-height: 20px;
|
|
489
|
+
max-height: 24px;
|
|
490
|
+
}
|
|
491
|
+
QComboBox:focus, QSpinBox:focus, QDoubleSpinBox:focus, QLineEdit:focus,
|
|
492
|
+
QDateTimeEdit:focus, QTimeEdit:focus, QDateEdit:focus,
|
|
493
|
+
QPlainTextEdit:focus, QTextEdit:focus {
|
|
494
|
+
border-color: $accent;
|
|
495
|
+
background-color: $surface;
|
|
496
|
+
}
|
|
497
|
+
QComboBox:disabled, QSpinBox:disabled, QDoubleSpinBox:disabled,
|
|
498
|
+
QLineEdit:disabled, QDateTimeEdit:disabled, QPlainTextEdit:disabled {
|
|
499
|
+
color: $text_disabled;
|
|
500
|
+
background-color: $disabled_bg;
|
|
501
|
+
border-color: $border;
|
|
502
|
+
}
|
|
503
|
+
QComboBox::drop-down {
|
|
504
|
+
subcontrol-origin: padding;
|
|
505
|
+
subcontrol-position: top right;
|
|
506
|
+
border-left: 1px solid $border_strong;
|
|
507
|
+
border-top-right-radius: 3px;
|
|
508
|
+
border-bottom-right-radius: 3px;
|
|
509
|
+
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
|
510
|
+
stop:0 $button_top, stop:1 $button_bottom);
|
|
511
|
+
width: 24px;
|
|
512
|
+
}
|
|
513
|
+
QComboBox::down-arrow {
|
|
514
|
+
width: 9px;
|
|
515
|
+
height: 9px;
|
|
516
|
+
}
|
|
517
|
+
QComboBox QAbstractItemView {
|
|
518
|
+
background-color: $surface;
|
|
519
|
+
border: 1px solid $border;
|
|
520
|
+
border-radius: 6px;
|
|
521
|
+
padding: 4px;
|
|
522
|
+
selection-background-color: $accent_soft;
|
|
523
|
+
selection-color: $text;
|
|
524
|
+
outline: none;
|
|
525
|
+
}
|
|
526
|
+
QSpinBox::up-button, QDoubleSpinBox::up-button, QDateTimeEdit::up-button,
|
|
527
|
+
QSpinBox::down-button, QDoubleSpinBox::down-button, QDateTimeEdit::down-button {
|
|
528
|
+
subcontrol-origin: border;
|
|
529
|
+
border-left: 1px solid $border_strong;
|
|
530
|
+
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
|
531
|
+
stop:0 $button_top, stop:1 $button_bottom);
|
|
532
|
+
width: 20px;
|
|
533
|
+
}
|
|
534
|
+
QSpinBox::up-button, QDoubleSpinBox::up-button, QDateTimeEdit::up-button {
|
|
535
|
+
subcontrol-position: top right;
|
|
536
|
+
border-bottom: 1px solid $border;
|
|
537
|
+
border-top-right-radius: 3px;
|
|
538
|
+
}
|
|
539
|
+
QSpinBox::down-button, QDoubleSpinBox::down-button, QDateTimeEdit::down-button {
|
|
540
|
+
subcontrol-position: bottom right;
|
|
541
|
+
border-bottom-right-radius: 3px;
|
|
542
|
+
}
|
|
543
|
+
QSpinBox::up-arrow, QDoubleSpinBox::up-arrow, QDateTimeEdit::up-arrow,
|
|
544
|
+
QSpinBox::down-arrow, QDoubleSpinBox::down-arrow, QDateTimeEdit::down-arrow {
|
|
545
|
+
width: 8px;
|
|
546
|
+
height: 6px;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/* ---------------- lists, trees, tables ---------------- */
|
|
550
|
+
QListView, QListWidget, QTreeView, QTableView, QTreeWidget {
|
|
551
|
+
background-color: $surface;
|
|
552
|
+
border: 1px solid $border;
|
|
553
|
+
border-radius: 6px;
|
|
554
|
+
alternate-background-color: $surface_alt;
|
|
555
|
+
outline: none;
|
|
556
|
+
}
|
|
557
|
+
QListView::item, QListWidget::item, QTreeView::item, QTableView::item {
|
|
558
|
+
padding: 4px 6px;
|
|
559
|
+
border-radius: 0px;
|
|
560
|
+
}
|
|
561
|
+
QListView::item:selected, QListWidget::item:selected,
|
|
562
|
+
QTreeView::item:selected, QTableView::item:selected {
|
|
563
|
+
background: $accent_soft;
|
|
564
|
+
color: $text;
|
|
565
|
+
}
|
|
566
|
+
QListView::item:hover, QListWidget::item:hover,
|
|
567
|
+
QTreeView::item:hover, QTableView::item:hover {
|
|
568
|
+
background: $surface_alt;
|
|
569
|
+
}
|
|
570
|
+
QHeaderView::section {
|
|
571
|
+
background: $surface_alt;
|
|
572
|
+
color: $text_secondary;
|
|
573
|
+
padding: 5px 8px;
|
|
574
|
+
border: none;
|
|
575
|
+
border-bottom: 1px solid $border;
|
|
576
|
+
font-weight: 600;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/* ---------------- scroll areas & bars ---------------- */
|
|
580
|
+
QScrollArea {
|
|
581
|
+
border: none;
|
|
582
|
+
background: transparent;
|
|
583
|
+
}
|
|
584
|
+
QScrollBar:horizontal {
|
|
585
|
+
background: transparent;
|
|
586
|
+
height: 12px;
|
|
587
|
+
border-radius: 0px;
|
|
588
|
+
margin: 0;
|
|
589
|
+
}
|
|
590
|
+
QScrollBar::handle:horizontal {
|
|
591
|
+
background: $scroll_handle;
|
|
592
|
+
border-radius: 6px;
|
|
593
|
+
min-width: 32px;
|
|
594
|
+
}
|
|
595
|
+
QScrollBar::handle:horizontal:hover {
|
|
596
|
+
background: $scroll_handle_hover;
|
|
597
|
+
}
|
|
598
|
+
QScrollBar:vertical {
|
|
599
|
+
background: transparent;
|
|
600
|
+
width: 12px;
|
|
601
|
+
border-radius: 0px;
|
|
602
|
+
margin: 0;
|
|
603
|
+
}
|
|
604
|
+
QScrollBar::handle:vertical {
|
|
605
|
+
background: $scroll_handle;
|
|
606
|
+
border-radius: 6px;
|
|
607
|
+
min-height: 32px;
|
|
608
|
+
}
|
|
609
|
+
QScrollBar::handle:vertical:hover {
|
|
610
|
+
background: $scroll_handle_hover;
|
|
611
|
+
}
|
|
612
|
+
QScrollBar::add-line, QScrollBar::sub-line {
|
|
613
|
+
width: 0;
|
|
614
|
+
height: 0;
|
|
615
|
+
}
|
|
616
|
+
QScrollBar::add-page, QScrollBar::sub-page {
|
|
617
|
+
background: transparent;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/* ---------------- sidebar ---------------- */
|
|
621
|
+
#Sidebar, #SidebarScroll, #SidebarScroll > QWidget {
|
|
622
|
+
background: $surface;
|
|
623
|
+
}
|
|
624
|
+
QToolButton#SidebarSectionHeader {
|
|
625
|
+
background: transparent;
|
|
626
|
+
border: none;
|
|
627
|
+
border-bottom: 1px solid $border;
|
|
628
|
+
border-radius: 0;
|
|
629
|
+
text-align: left;
|
|
630
|
+
padding: 7px 8px;
|
|
631
|
+
font-weight: 600;
|
|
632
|
+
font-size: 9pt;
|
|
633
|
+
color: $text_secondary;
|
|
634
|
+
}
|
|
635
|
+
QToolButton#SidebarSectionHeader:hover {
|
|
636
|
+
background: $accent_soft;
|
|
637
|
+
color: $text;
|
|
638
|
+
}
|
|
639
|
+
QToolButton#SidebarSectionHeader:pressed {
|
|
640
|
+
background: $accent_soft_2;
|
|
641
|
+
}
|
|
642
|
+
QToolButton#SidebarSectionHeader:checked {
|
|
643
|
+
color: $accent;
|
|
644
|
+
}
|
|
645
|
+
QMainWindow::separator {
|
|
646
|
+
background: $border;
|
|
647
|
+
width: 1px;
|
|
648
|
+
height: 1px;
|
|
649
|
+
}
|
|
650
|
+
QSplitter::handle {
|
|
651
|
+
background: $border;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/* ---------------- groups & tabs ---------------- */
|
|
655
|
+
QGroupBox {
|
|
656
|
+
border: 1px solid $border;
|
|
657
|
+
border-radius: 8px;
|
|
658
|
+
margin-top: 10px;
|
|
659
|
+
padding-top: 8px;
|
|
660
|
+
background: transparent;
|
|
661
|
+
}
|
|
662
|
+
QGroupBox::title {
|
|
663
|
+
subcontrol-origin: margin;
|
|
664
|
+
left: 10px;
|
|
665
|
+
padding: 0 6px;
|
|
666
|
+
color: $accent;
|
|
667
|
+
font-weight: 600;
|
|
668
|
+
}
|
|
669
|
+
QTabWidget::pane {
|
|
670
|
+
border: 1px solid $border;
|
|
671
|
+
border-radius: 8px;
|
|
672
|
+
background: $surface;
|
|
673
|
+
top: -1px;
|
|
674
|
+
}
|
|
675
|
+
QTabBar::tab {
|
|
676
|
+
background: transparent;
|
|
677
|
+
border: none;
|
|
678
|
+
border-bottom: 2px solid transparent;
|
|
679
|
+
padding: 6px 16px;
|
|
680
|
+
color: $text_secondary;
|
|
681
|
+
font-weight: 500;
|
|
682
|
+
}
|
|
683
|
+
QTabBar::tab:selected {
|
|
684
|
+
color: $accent;
|
|
685
|
+
border-bottom: 2px solid $accent;
|
|
686
|
+
font-weight: 600;
|
|
687
|
+
}
|
|
688
|
+
QTabBar::tab:hover:!selected {
|
|
689
|
+
color: $text;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/* ---------------- sliders ---------------- */
|
|
693
|
+
QSlider::groove:horizontal {
|
|
694
|
+
height: 4px;
|
|
695
|
+
background: $slider_groove;
|
|
696
|
+
border-radius: 0px;
|
|
697
|
+
}
|
|
698
|
+
QSlider::sub-page:horizontal {
|
|
699
|
+
background: $accent;
|
|
700
|
+
border-radius: 0px;
|
|
701
|
+
}
|
|
702
|
+
QSlider::handle:horizontal {
|
|
703
|
+
width: 14px;
|
|
704
|
+
margin: -5px 0;
|
|
705
|
+
border-radius: 0px;
|
|
706
|
+
background: $surface;
|
|
707
|
+
border: 2px solid $accent;
|
|
708
|
+
}
|
|
709
|
+
QSlider::handle:horizontal:hover {
|
|
710
|
+
background: $accent;
|
|
711
|
+
}
|
|
712
|
+
QSlider::groove:vertical {
|
|
713
|
+
width: 4px;
|
|
714
|
+
background: $slider_groove;
|
|
715
|
+
border-radius: 0px;
|
|
716
|
+
}
|
|
717
|
+
QSlider::sub-page:vertical {
|
|
718
|
+
background: $accent;
|
|
719
|
+
border-radius: 0px;
|
|
720
|
+
}
|
|
721
|
+
QSlider::handle:vertical {
|
|
722
|
+
height: 14px;
|
|
723
|
+
margin: 0 -5px;
|
|
724
|
+
border-radius: 0px;
|
|
725
|
+
background: $surface;
|
|
726
|
+
border: 2px solid $accent;
|
|
727
|
+
}
|
|
728
|
+
QSlider::handle:vertical:hover {
|
|
729
|
+
background: $accent;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/* ---------------- checkboxes & radios ---------------- */
|
|
733
|
+
QCheckBox, QRadioButton {
|
|
734
|
+
spacing: 6px;
|
|
735
|
+
}
|
|
736
|
+
/* Keep QCheckBox native: Fusion draws a real check mark (✓). Painting the
|
|
737
|
+
whole checked indicator with QSS hides that glyph on Windows. */
|
|
738
|
+
QRadioButton::indicator {
|
|
739
|
+
width: 16px;
|
|
740
|
+
height: 16px;
|
|
741
|
+
border: 1px solid $border_strong;
|
|
742
|
+
background: $surface;
|
|
743
|
+
}
|
|
744
|
+
QRadioButton::indicator {
|
|
745
|
+
border-radius: 0px;
|
|
746
|
+
}
|
|
747
|
+
QRadioButton::indicator:hover {
|
|
748
|
+
border-color: $accent;
|
|
749
|
+
}
|
|
750
|
+
QRadioButton::indicator:checked {
|
|
751
|
+
background: $accent;
|
|
752
|
+
border-color: $accent;
|
|
753
|
+
}
|
|
754
|
+
QRadioButton::indicator:disabled {
|
|
755
|
+
border-color: $border;
|
|
756
|
+
background: $disabled_bg;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/* ---------------- misc ---------------- */
|
|
760
|
+
QStatusBar {
|
|
761
|
+
background: $surface;
|
|
762
|
+
border-top: 1px solid $border;
|
|
763
|
+
}
|
|
764
|
+
QStatusBar::item {
|
|
765
|
+
border: none;
|
|
766
|
+
}
|
|
767
|
+
QToolTip {
|
|
768
|
+
background: $tooltip_bg;
|
|
769
|
+
color: $tooltip_text;
|
|
770
|
+
border: none;
|
|
771
|
+
padding: 4px 8px;
|
|
772
|
+
border-radius: 0px;
|
|
773
|
+
font-size: 9pt;
|
|
774
|
+
}
|
|
775
|
+
QProgressBar {
|
|
776
|
+
border: 1px solid $border;
|
|
777
|
+
border-radius: 6px;
|
|
778
|
+
text-align: center;
|
|
779
|
+
background: $surface;
|
|
780
|
+
color: $text;
|
|
781
|
+
}
|
|
782
|
+
QProgressBar::chunk {
|
|
783
|
+
background: $accent;
|
|
784
|
+
border-radius: 5px;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/* ---------------- calendar popup ---------------- */
|
|
788
|
+
QCalendarWidget QWidget#qt_calendar_navigationbar {
|
|
789
|
+
background: $surface_alt;
|
|
790
|
+
}
|
|
791
|
+
QCalendarWidget QToolButton {
|
|
792
|
+
background: transparent;
|
|
793
|
+
border: none;
|
|
794
|
+
border-radius: 0px;
|
|
795
|
+
padding: 2px 6px;
|
|
796
|
+
color: $text;
|
|
797
|
+
}
|
|
798
|
+
QCalendarWidget QToolButton:hover {
|
|
799
|
+
background: $accent_soft;
|
|
800
|
+
}
|
|
801
|
+
QCalendarWidget QAbstractItemView {
|
|
802
|
+
background: $surface;
|
|
803
|
+
color: $text;
|
|
804
|
+
selection-background-color: $accent;
|
|
805
|
+
selection-color: #ffffff;
|
|
806
|
+
outline: none;
|
|
807
|
+
}
|
|
808
|
+
QCalendarWidget QSpinBox {
|
|
809
|
+
background: $input;
|
|
810
|
+
}
|
|
811
|
+
""")
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
def build_stylesheet(theme_name: str = "light", tone_name: str = "black") -> str:
|
|
815
|
+
"""Return the Qt stylesheet for the given theme name."""
|
|
816
|
+
theme_name = theme_name if theme_name in THEMES else "light"
|
|
817
|
+
palette = resolved_theme(theme_name, tone_name)
|
|
818
|
+
palette["_primary"] = _PRIMARY_SELECTOR
|
|
819
|
+
palette["_primary_hover"] = _state_selector(_PRIMARY_BUTTONS, "hover")
|
|
820
|
+
palette["_primary_pressed"] = _state_selector(_PRIMARY_BUTTONS, "pressed")
|
|
821
|
+
palette["_primary_disabled"] = _state_selector(_PRIMARY_BUTTONS, "disabled")
|
|
822
|
+
palette["_danger"] = _DANGER_SELECTOR
|
|
823
|
+
palette["_danger_hover"] = _state_selector(_DANGER_BUTTONS, "hover")
|
|
824
|
+
palette["_danger_pressed"] = _state_selector(_DANGER_BUTTONS, "pressed")
|
|
825
|
+
return _STYLESHEET_TEMPLATE.substitute(palette)
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
# ---------------------------------------------------------------------------
|
|
829
|
+
# Fonts
|
|
830
|
+
# ---------------------------------------------------------------------------
|
|
831
|
+
|
|
832
|
+
def _setup_app_font(app: QApplication) -> None:
|
|
833
|
+
"""Set a per-platform font family so text scales with the display DPI."""
|
|
834
|
+
font = QFont()
|
|
835
|
+
font.setFamilies(font_families())
|
|
836
|
+
font.setPointSizeF(9.5)
|
|
837
|
+
app.setFont(font)
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
# ---------------------------------------------------------------------------
|
|
841
|
+
# Matplotlib theming
|
|
842
|
+
# ---------------------------------------------------------------------------
|
|
843
|
+
|
|
844
|
+
_MPL_SHARED = {
|
|
845
|
+
"figure.dpi": 100,
|
|
846
|
+
"savefig.dpi": 200,
|
|
847
|
+
"font.size": 9.0,
|
|
848
|
+
"axes.titlesize": 10.0,
|
|
849
|
+
"axes.labelsize": 9.0,
|
|
850
|
+
"xtick.labelsize": 8.0,
|
|
851
|
+
"ytick.labelsize": 8.0,
|
|
852
|
+
"legend.fontsize": 8.5,
|
|
853
|
+
"axes.linewidth": 0.8,
|
|
854
|
+
"axes.spines.top": False,
|
|
855
|
+
"axes.spines.right": False,
|
|
856
|
+
"lines.linewidth": 1.0,
|
|
857
|
+
"grid.linewidth": 0.8,
|
|
858
|
+
"image.cmap": "jet",
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def build_palette(theme_name: str = "light", tone_name: str = "black"):
|
|
863
|
+
"""Return a QPalette matching the theme.
|
|
864
|
+
|
|
865
|
+
The stylesheet covers most widgets, but a few things are drawn by the
|
|
866
|
+
style itself (combo/spin arrows, check indicators in menus, separators,
|
|
867
|
+
the calendar popup, ...) and pick their colors from the palette - so
|
|
868
|
+
both themes need a consistent palette or those elements stay light (and
|
|
869
|
+
nearly invisible) in dark mode.
|
|
870
|
+
"""
|
|
871
|
+
from PySide6.QtGui import QPalette
|
|
872
|
+
from PySide6.QtGui import QColor
|
|
873
|
+
|
|
874
|
+
theme_name = theme_name if theme_name in THEMES else "light"
|
|
875
|
+
p = resolved_theme(theme_name, tone_name)
|
|
876
|
+
|
|
877
|
+
pal = QPalette()
|
|
878
|
+
pal.setColor(QPalette.ColorRole.Window, QColor(p["window"]))
|
|
879
|
+
pal.setColor(QPalette.ColorRole.WindowText, QColor(p["text"]))
|
|
880
|
+
pal.setColor(QPalette.ColorRole.Base, QColor(p["input"]))
|
|
881
|
+
pal.setColor(QPalette.ColorRole.AlternateBase, QColor(p["surface_alt"]))
|
|
882
|
+
pal.setColor(QPalette.ColorRole.ToolTipBase, QColor(p["tooltip_bg"]))
|
|
883
|
+
pal.setColor(QPalette.ColorRole.ToolTipText, QColor(p["tooltip_text"]))
|
|
884
|
+
pal.setColor(QPalette.ColorRole.Text, QColor(p["text"]))
|
|
885
|
+
pal.setColor(QPalette.ColorRole.PlaceholderText, QColor(p["text_disabled"]))
|
|
886
|
+
pal.setColor(QPalette.ColorRole.Button, QColor(p["surface"]))
|
|
887
|
+
pal.setColor(QPalette.ColorRole.ButtonText, QColor(p["text"]))
|
|
888
|
+
pal.setColor(QPalette.ColorRole.BrightText, QColor("#ffffff"))
|
|
889
|
+
pal.setColor(QPalette.ColorRole.Highlight, QColor(p["accent"]))
|
|
890
|
+
pal.setColor(QPalette.ColorRole.HighlightedText, QColor("#ffffff"))
|
|
891
|
+
pal.setColor(QPalette.ColorRole.Link, QColor(p["accent"]))
|
|
892
|
+
pal.setColor(QPalette.ColorRole.Mid, QColor(p["border"]))
|
|
893
|
+
pal.setColor(QPalette.ColorRole.Midlight, QColor(p["border"]))
|
|
894
|
+
pal.setColor(QPalette.ColorRole.Dark, QColor(p["border_strong"]))
|
|
895
|
+
pal.setColor(QPalette.ColorRole.Shadow, QColor(p["border_strong"]))
|
|
896
|
+
pal.setColor(QPalette.ColorRole.Light, QColor(p["surface"]))
|
|
897
|
+
|
|
898
|
+
disabled = QPalette.ColorGroup.Disabled
|
|
899
|
+
pal.setColor(disabled, QPalette.ColorRole.WindowText, QColor(p["text_disabled"]))
|
|
900
|
+
pal.setColor(disabled, QPalette.ColorRole.Text, QColor(p["text_disabled"]))
|
|
901
|
+
pal.setColor(disabled, QPalette.ColorRole.ButtonText, QColor(p["text_disabled"]))
|
|
902
|
+
pal.setColor(disabled, QPalette.ColorRole.Base, QColor(p["disabled_bg"]))
|
|
903
|
+
pal.setColor(disabled, QPalette.ColorRole.Button, QColor(p["disabled_bg"]))
|
|
904
|
+
return pal
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
def apply_matplotlib_theme(theme_name: str = "light", tone_name: str = "black") -> None:
|
|
908
|
+
"""Apply the theme colors to the matplotlib defaults (affects new figures)."""
|
|
909
|
+
import matplotlib as mpl
|
|
910
|
+
from matplotlib import font_manager
|
|
911
|
+
|
|
912
|
+
theme_name = theme_name if theme_name in THEMES else "light"
|
|
913
|
+
mpl.rcParams.update(_MPL_SHARED)
|
|
914
|
+
# Use the first font family that is actually installed, so matplotlib
|
|
915
|
+
# does not warn about every missing CJK fallback on each text draw.
|
|
916
|
+
for family in font_families():
|
|
917
|
+
try:
|
|
918
|
+
font_manager.findfont(family, fallback_to_default=False)
|
|
919
|
+
mpl.rcParams["font.family"] = family
|
|
920
|
+
break
|
|
921
|
+
except Exception:
|
|
922
|
+
continue
|
|
923
|
+
mpl.rcParams.update(resolved_theme(theme_name, tone_name)["mpl"])
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
def retheme_figures(theme_name: str = "light", tone_name: str = "black") -> None:
|
|
927
|
+
"""Restyle every currently open matplotlib figure for ``theme_name``.
|
|
928
|
+
|
|
929
|
+
Used after a live theme toggle so existing canvases (signal area,
|
|
930
|
+
hypnogram, spectrum window, ...) follow the new colors without
|
|
931
|
+
recreating them.
|
|
932
|
+
"""
|
|
933
|
+
import matplotlib.pyplot as plt
|
|
934
|
+
|
|
935
|
+
theme_name = theme_name if theme_name in THEMES else "light"
|
|
936
|
+
mpl_colors = resolved_theme(theme_name, tone_name)["mpl"]
|
|
937
|
+
for num in plt.get_fignums():
|
|
938
|
+
fig = plt.figure(num)
|
|
939
|
+
try:
|
|
940
|
+
fig.patch.set_facecolor(mpl_colors["figure.facecolor"])
|
|
941
|
+
for ax in fig.axes:
|
|
942
|
+
ax.set_facecolor(mpl_colors["axes.facecolor"])
|
|
943
|
+
for spine in ax.spines.values():
|
|
944
|
+
spine.set_edgecolor(mpl_colors["axes.edgecolor"])
|
|
945
|
+
ax.tick_params(colors=mpl_colors["xtick.color"], which="both")
|
|
946
|
+
if ax.xaxis.label is not None:
|
|
947
|
+
ax.xaxis.label.set_color(mpl_colors["axes.labelcolor"])
|
|
948
|
+
if ax.yaxis.label is not None:
|
|
949
|
+
ax.yaxis.label.set_color(mpl_colors["axes.labelcolor"])
|
|
950
|
+
fig.canvas.draw_idle()
|
|
951
|
+
except Exception: # pragma: no cover - a figure may be mid-close
|
|
952
|
+
continue
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
# ---------------------------------------------------------------------------
|
|
956
|
+
# Public API
|
|
957
|
+
# ---------------------------------------------------------------------------
|
|
958
|
+
|
|
959
|
+
def apply_theme(app: QApplication, theme_name: str = "light",
|
|
960
|
+
tone_name: str = "black") -> str:
|
|
961
|
+
"""Apply a full MiSleep theme (style + fonts + matplotlib) to ``app``.
|
|
962
|
+
|
|
963
|
+
Parameters
|
|
964
|
+
----------
|
|
965
|
+
app : QApplication
|
|
966
|
+
The application to style.
|
|
967
|
+
theme_name : str
|
|
968
|
+
``"light"`` or ``"dark"``. Unknown names fall back to ``"light"``.
|
|
969
|
+
|
|
970
|
+
Returns
|
|
971
|
+
-------
|
|
972
|
+
str
|
|
973
|
+
The theme name that was actually applied.
|
|
974
|
+
"""
|
|
975
|
+
theme_name = theme_name if theme_name in THEMES else "light"
|
|
976
|
+
|
|
977
|
+
# Fusion renders identically on every platform, so the QSS look is
|
|
978
|
+
# predictable across Windows / macOS / Linux (native styles would
|
|
979
|
+
# override parts of the stylesheet).
|
|
980
|
+
try:
|
|
981
|
+
app.setStyle("Fusion")
|
|
982
|
+
except Exception: # pragma: no cover
|
|
983
|
+
pass
|
|
984
|
+
|
|
985
|
+
_setup_app_font(app)
|
|
986
|
+
app.setPalette(build_palette(theme_name, tone_name))
|
|
987
|
+
app.setStyleSheet(build_stylesheet(theme_name, tone_name))
|
|
988
|
+
apply_matplotlib_theme(theme_name, tone_name)
|
|
989
|
+
return theme_name
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
def apply_app_style(app: QApplication) -> None:
|
|
993
|
+
"""Backward-compatible alias: apply the default (light) theme."""
|
|
994
|
+
apply_theme(app, "light")
|