solarviewer 1.0.2__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.
- solar_radio_image_viewer/__init__.py +12 -0
- solar_radio_image_viewer/assets/add_tab_default.png +0 -0
- solar_radio_image_viewer/assets/add_tab_default_light.png +0 -0
- solar_radio_image_viewer/assets/add_tab_hover.png +0 -0
- solar_radio_image_viewer/assets/add_tab_hover_light.png +0 -0
- solar_radio_image_viewer/assets/browse.png +0 -0
- solar_radio_image_viewer/assets/browse_light.png +0 -0
- solar_radio_image_viewer/assets/close_tab_default.png +0 -0
- solar_radio_image_viewer/assets/close_tab_default_light.png +0 -0
- solar_radio_image_viewer/assets/close_tab_hover.png +0 -0
- solar_radio_image_viewer/assets/close_tab_hover_light.png +0 -0
- solar_radio_image_viewer/assets/ellipse_selection.png +0 -0
- solar_radio_image_viewer/assets/ellipse_selection_light.png +0 -0
- solar_radio_image_viewer/assets/icons8-ellipse-90.png +0 -0
- solar_radio_image_viewer/assets/icons8-ellipse-90_light.png +0 -0
- solar_radio_image_viewer/assets/icons8-info-90.png +0 -0
- solar_radio_image_viewer/assets/icons8-info-90_light.png +0 -0
- solar_radio_image_viewer/assets/profile.png +0 -0
- solar_radio_image_viewer/assets/profile_light.png +0 -0
- solar_radio_image_viewer/assets/rectangle_selection.png +0 -0
- solar_radio_image_viewer/assets/rectangle_selection_light.png +0 -0
- solar_radio_image_viewer/assets/reset.png +0 -0
- solar_radio_image_viewer/assets/reset_light.png +0 -0
- solar_radio_image_viewer/assets/ruler.png +0 -0
- solar_radio_image_viewer/assets/ruler_light.png +0 -0
- solar_radio_image_viewer/assets/search.png +0 -0
- solar_radio_image_viewer/assets/search_light.png +0 -0
- solar_radio_image_viewer/assets/settings.png +0 -0
- solar_radio_image_viewer/assets/settings_light.png +0 -0
- solar_radio_image_viewer/assets/splash.fits +0 -0
- solar_radio_image_viewer/assets/zoom_60arcmin.png +0 -0
- solar_radio_image_viewer/assets/zoom_60arcmin_light.png +0 -0
- solar_radio_image_viewer/assets/zoom_in.png +0 -0
- solar_radio_image_viewer/assets/zoom_in_light.png +0 -0
- solar_radio_image_viewer/assets/zoom_out.png +0 -0
- solar_radio_image_viewer/assets/zoom_out_light.png +0 -0
- solar_radio_image_viewer/create_video.py +1345 -0
- solar_radio_image_viewer/dialogs.py +2665 -0
- solar_radio_image_viewer/from_simpl/__init__.py +184 -0
- solar_radio_image_viewer/from_simpl/caltable_visualizer.py +1001 -0
- solar_radio_image_viewer/from_simpl/dynamic_spectra_dialog.py +332 -0
- solar_radio_image_viewer/from_simpl/make_dynamic_spectra.py +351 -0
- solar_radio_image_viewer/from_simpl/pipeline_logger_gui.py +1232 -0
- solar_radio_image_viewer/from_simpl/simpl_theme.py +352 -0
- solar_radio_image_viewer/from_simpl/utils.py +984 -0
- solar_radio_image_viewer/from_simpl/view_dynamic_spectra_GUI.py +1975 -0
- solar_radio_image_viewer/helioprojective.py +1916 -0
- solar_radio_image_viewer/helioprojective_viewer.py +817 -0
- solar_radio_image_viewer/helioviewer_browser.py +1514 -0
- solar_radio_image_viewer/main.py +148 -0
- solar_radio_image_viewer/move_phasecenter.py +1269 -0
- solar_radio_image_viewer/napari_viewer.py +368 -0
- solar_radio_image_viewer/noaa_events/__init__.py +32 -0
- solar_radio_image_viewer/noaa_events/noaa_events.py +430 -0
- solar_radio_image_viewer/noaa_events/noaa_events_gui.py +1922 -0
- solar_radio_image_viewer/norms.py +293 -0
- solar_radio_image_viewer/radio_data_downloader/__init__.py +25 -0
- solar_radio_image_viewer/radio_data_downloader/radio_data_downloader.py +756 -0
- solar_radio_image_viewer/radio_data_downloader/radio_data_downloader_gui.py +528 -0
- solar_radio_image_viewer/searchable_combobox.py +220 -0
- solar_radio_image_viewer/solar_context/__init__.py +41 -0
- solar_radio_image_viewer/solar_context/active_regions.py +371 -0
- solar_radio_image_viewer/solar_context/cme_alerts.py +234 -0
- solar_radio_image_viewer/solar_context/context_images.py +297 -0
- solar_radio_image_viewer/solar_context/realtime_data.py +528 -0
- solar_radio_image_viewer/solar_data_downloader/__init__.py +35 -0
- solar_radio_image_viewer/solar_data_downloader/solar_data_downloader.py +1667 -0
- solar_radio_image_viewer/solar_data_downloader/solar_data_downloader_cli.py +901 -0
- solar_radio_image_viewer/solar_data_downloader/solar_data_downloader_gui.py +1210 -0
- solar_radio_image_viewer/styles.py +643 -0
- solar_radio_image_viewer/utils/__init__.py +32 -0
- solar_radio_image_viewer/utils/rate_limiter.py +255 -0
- solar_radio_image_viewer/utils.py +952 -0
- solar_radio_image_viewer/video_dialog.py +2629 -0
- solar_radio_image_viewer/video_utils.py +656 -0
- solar_radio_image_viewer/viewer.py +11174 -0
- solarviewer-1.0.2.dist-info/METADATA +343 -0
- solarviewer-1.0.2.dist-info/RECORD +82 -0
- solarviewer-1.0.2.dist-info/WHEEL +5 -0
- solarviewer-1.0.2.dist-info/entry_points.txt +8 -0
- solarviewer-1.0.2.dist-info/licenses/LICENSE +21 -0
- solarviewer-1.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
# Theme palettes for the Solar Radio Image Viewer
|
|
2
|
+
# Supports both dark and light modes with modern, premium styling
|
|
3
|
+
|
|
4
|
+
DARK_PALETTE = {
|
|
5
|
+
"window": "#1a1a2e", # Deep rich dark blue
|
|
6
|
+
"base": "#16213e", # Navy undertone for inputs
|
|
7
|
+
"text": "#eeeeee", # Soft white for readability
|
|
8
|
+
"highlight": "#e94560", # Vibrant accent for primary actions
|
|
9
|
+
"highlight_hover": "#ff6b6b",
|
|
10
|
+
"button": "#0f3460", # Subtle blue-gray buttons
|
|
11
|
+
"button_hover": "#1a4a7a",
|
|
12
|
+
"button_pressed": "#0a2540",
|
|
13
|
+
"border": "#2a3f5f", # Subtle border color
|
|
14
|
+
"disabled": "#4a4a6a",
|
|
15
|
+
"success": "#2ecc71",
|
|
16
|
+
"warning": "#f39c12",
|
|
17
|
+
"error": "#e74c3c",
|
|
18
|
+
"secondary": "#533483", # Secondary accent
|
|
19
|
+
"surface": "#1f2940", # Elevated surfaces
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
LIGHT_PALETTE = {
|
|
23
|
+
#"window": "#e8e8e8", # Light grey background - proper light theme
|
|
24
|
+
"window": "#A59D84",
|
|
25
|
+
#"base": "#ffffff", # White for inputs
|
|
26
|
+
"base": "#ECEBDE",
|
|
27
|
+
"text": "#1a1a1a", # Dark text for readability on light backgrounds
|
|
28
|
+
"input_text": "#1a1a1a", # Dark text for inputs (same as text in light mode)
|
|
29
|
+
"highlight": "#0066cc", # Professional blue
|
|
30
|
+
"highlight_hover": "#0052a3",
|
|
31
|
+
"button": "#f0f0f0", # Light grey buttons
|
|
32
|
+
"button_hover": "#e0e0e0",
|
|
33
|
+
"button_pressed": "#d0d0d0",
|
|
34
|
+
"border": "#b0b0b0", # Visible border
|
|
35
|
+
"disabled": "#999999",
|
|
36
|
+
"success": "#28a745",
|
|
37
|
+
"warning": "#ffc107",
|
|
38
|
+
"error": "#dc3545",
|
|
39
|
+
"secondary": "#6c5ce7",
|
|
40
|
+
#"surface": "#f5f5f5", # Light grey surfaces
|
|
41
|
+
"surface": "#ECEBDE",
|
|
42
|
+
#"toolbar_bg": "#404040", # Dark toolbar for white icons
|
|
43
|
+
"toolbar_bg": "#D7D3BF",
|
|
44
|
+
#"plot_bg": "#ffffff",
|
|
45
|
+
"plot_bg": "#ECEBDE",
|
|
46
|
+
"plot_text": "#1a1a1a",
|
|
47
|
+
"plot_grid": "#d0d0d0",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_stylesheet(palette, is_dark=True):
|
|
52
|
+
"""Generate the complete stylesheet for the given palette."""
|
|
53
|
+
|
|
54
|
+
# Adjust some colors based on theme
|
|
55
|
+
input_bg = palette["base"]
|
|
56
|
+
input_text = palette.get("input_text", palette["text"])
|
|
57
|
+
group_border = palette["border"]
|
|
58
|
+
tab_selected_bg = palette["highlight"]
|
|
59
|
+
hover_text = "#ffffff" if is_dark else palette["text"]
|
|
60
|
+
|
|
61
|
+
return f"""
|
|
62
|
+
/* ===== GLOBAL STYLES ===== */
|
|
63
|
+
QWidget {{
|
|
64
|
+
font-family: 'Segoe UI', 'SF Pro Display', -apple-system, Arial, sans-serif;
|
|
65
|
+
font-size: 11pt;
|
|
66
|
+
color: {palette['text']};
|
|
67
|
+
}}
|
|
68
|
+
|
|
69
|
+
QMainWindow {{
|
|
70
|
+
background-color: {palette['window']};
|
|
71
|
+
}}
|
|
72
|
+
|
|
73
|
+
/* ===== GROUP BOXES ===== */
|
|
74
|
+
QGroupBox {{
|
|
75
|
+
background-color: {palette['surface']};
|
|
76
|
+
border: 1px solid {group_border};
|
|
77
|
+
border-radius: 8px;
|
|
78
|
+
margin-top: 16px;
|
|
79
|
+
padding: 12px 8px 8px 8px;
|
|
80
|
+
font-weight: 600;
|
|
81
|
+
}}
|
|
82
|
+
|
|
83
|
+
QGroupBox::title {{
|
|
84
|
+
subcontrol-origin: margin;
|
|
85
|
+
left: 14px;
|
|
86
|
+
padding: 0 6px;
|
|
87
|
+
color: {palette['text']};
|
|
88
|
+
font-weight: bold;
|
|
89
|
+
font-size: 11pt;
|
|
90
|
+
}}
|
|
91
|
+
|
|
92
|
+
/* ===== BUTTONS ===== */
|
|
93
|
+
QPushButton {{
|
|
94
|
+
background-color: {palette['button']};
|
|
95
|
+
color: {palette['text']};
|
|
96
|
+
border: 1px solid {palette['border']};
|
|
97
|
+
border-radius: 6px;
|
|
98
|
+
padding: 6px 14px;
|
|
99
|
+
min-width: 80px;
|
|
100
|
+
min-height: 28px;
|
|
101
|
+
font-size: 11pt;
|
|
102
|
+
font-weight: 500;
|
|
103
|
+
}}
|
|
104
|
+
|
|
105
|
+
QPushButton:hover {{
|
|
106
|
+
background-color: {palette['button_hover']};
|
|
107
|
+
border-color: {palette['highlight']};
|
|
108
|
+
}}
|
|
109
|
+
|
|
110
|
+
QPushButton:pressed {{
|
|
111
|
+
background-color: {palette['button_pressed']};
|
|
112
|
+
}}
|
|
113
|
+
|
|
114
|
+
QPushButton:disabled {{
|
|
115
|
+
color: {palette['disabled']};
|
|
116
|
+
background-color: {palette['button']};
|
|
117
|
+
border-color: {palette['border']};
|
|
118
|
+
}}
|
|
119
|
+
|
|
120
|
+
/* Primary action button style */
|
|
121
|
+
QPushButton#PrimaryButton {{
|
|
122
|
+
background-color: {palette['highlight']};
|
|
123
|
+
color: #ffffff;
|
|
124
|
+
border: none;
|
|
125
|
+
font-weight: 600;
|
|
126
|
+
}}
|
|
127
|
+
|
|
128
|
+
QPushButton#PrimaryButton:hover {{
|
|
129
|
+
background-color: {palette['highlight_hover']};
|
|
130
|
+
}}
|
|
131
|
+
|
|
132
|
+
QPushButton#PrimaryButton:disabled {{
|
|
133
|
+
background-color: {palette['disabled']};
|
|
134
|
+
color: {palette['border']};
|
|
135
|
+
}}
|
|
136
|
+
|
|
137
|
+
QPushButton#IconOnlyButton {{
|
|
138
|
+
min-width: 30px;
|
|
139
|
+
max-width: 30px;
|
|
140
|
+
max-height: 30px;
|
|
141
|
+
padding: 4px;
|
|
142
|
+
border-radius: 6px;
|
|
143
|
+
}}
|
|
144
|
+
|
|
145
|
+
QPushButton#IconOnlyNBGButton {{
|
|
146
|
+
background-color: transparent;
|
|
147
|
+
border: none;
|
|
148
|
+
padding: 8px;
|
|
149
|
+
margin: 0px;
|
|
150
|
+
min-width: 0px;
|
|
151
|
+
min-height: 0px;
|
|
152
|
+
border-radius: 6px;
|
|
153
|
+
}}
|
|
154
|
+
|
|
155
|
+
QPushButton#IconOnlyNBGButton:hover {{
|
|
156
|
+
background-color: {palette['button_hover']};
|
|
157
|
+
}}
|
|
158
|
+
|
|
159
|
+
QPushButton#IconOnlyNBGButton:pressed {{
|
|
160
|
+
background-color: {palette['button_pressed']};
|
|
161
|
+
}}
|
|
162
|
+
|
|
163
|
+
/* ===== INPUT FIELDS ===== */
|
|
164
|
+
QLineEdit, QSpinBox, QDoubleSpinBox {{
|
|
165
|
+
background-color: {input_bg};
|
|
166
|
+
color: {input_text};
|
|
167
|
+
border: 1px solid {palette['border']};
|
|
168
|
+
border-radius: 6px;
|
|
169
|
+
padding: 6px 10px;
|
|
170
|
+
min-height: 28px;
|
|
171
|
+
font-size: 11pt;
|
|
172
|
+
selection-background-color: {palette['highlight']};
|
|
173
|
+
}}
|
|
174
|
+
|
|
175
|
+
QLineEdit:focus, QSpinBox:focus, QDoubleSpinBox:focus {{
|
|
176
|
+
border-color: {palette['highlight']};
|
|
177
|
+
border-width: 2px;
|
|
178
|
+
}}
|
|
179
|
+
|
|
180
|
+
QLineEdit:disabled, QSpinBox:disabled, QDoubleSpinBox:disabled {{
|
|
181
|
+
background-color: {palette['surface']};
|
|
182
|
+
color: {palette['disabled']};
|
|
183
|
+
}}
|
|
184
|
+
|
|
185
|
+
QComboBox {{
|
|
186
|
+
background-color: {input_bg};
|
|
187
|
+
color: {input_text};
|
|
188
|
+
border: 1px solid {palette['border']};
|
|
189
|
+
border-radius: 6px;
|
|
190
|
+
padding: 6px 10px;
|
|
191
|
+
min-height: 28px;
|
|
192
|
+
font-size: 11pt;
|
|
193
|
+
}}
|
|
194
|
+
|
|
195
|
+
QComboBox:hover {{
|
|
196
|
+
border-color: {palette['highlight']};
|
|
197
|
+
}}
|
|
198
|
+
|
|
199
|
+
QComboBox::drop-down {{
|
|
200
|
+
border: none;
|
|
201
|
+
width: 24px;
|
|
202
|
+
}}
|
|
203
|
+
|
|
204
|
+
QComboBox::down-arrow {{
|
|
205
|
+
width: 12px;
|
|
206
|
+
height: 12px;
|
|
207
|
+
}}
|
|
208
|
+
|
|
209
|
+
QComboBox QAbstractItemView {{
|
|
210
|
+
background-color: {palette['surface']};
|
|
211
|
+
color: {palette['text']};
|
|
212
|
+
border: 1px solid {palette['border']};
|
|
213
|
+
border-radius: 6px;
|
|
214
|
+
selection-background-color: {palette['highlight']};
|
|
215
|
+
selection-color: #ffffff;
|
|
216
|
+
}}
|
|
217
|
+
|
|
218
|
+
/* ===== TAB WIDGET ===== */
|
|
219
|
+
QTabWidget::pane {{
|
|
220
|
+
border: 1px solid {palette['border']};
|
|
221
|
+
border-radius: 8px;
|
|
222
|
+
background-color: {palette['surface']};
|
|
223
|
+
}}
|
|
224
|
+
|
|
225
|
+
QTabBar::tab {{
|
|
226
|
+
background: {palette['button']};
|
|
227
|
+
color: {palette['text']};
|
|
228
|
+
padding: 10px 20px;
|
|
229
|
+
border-top-left-radius: 8px;
|
|
230
|
+
border-top-right-radius: 8px;
|
|
231
|
+
margin-right: 2px;
|
|
232
|
+
font-size: 11pt;
|
|
233
|
+
font-weight: 500;
|
|
234
|
+
}}
|
|
235
|
+
|
|
236
|
+
QTabBar::tab:selected {{
|
|
237
|
+
background: {tab_selected_bg};
|
|
238
|
+
color: #ffffff;
|
|
239
|
+
}}
|
|
240
|
+
|
|
241
|
+
QTabBar::tab:hover:!selected {{
|
|
242
|
+
background: {palette['button_hover']};
|
|
243
|
+
}}
|
|
244
|
+
|
|
245
|
+
/* ===== TABLE WIDGET ===== */
|
|
246
|
+
QTableWidget {{
|
|
247
|
+
font-size: 11pt;
|
|
248
|
+
background-color: {palette['base']};
|
|
249
|
+
alternate-background-color: {palette['surface']};
|
|
250
|
+
gridline-color: {palette['border']};
|
|
251
|
+
border: 1px solid {palette['border']};
|
|
252
|
+
border-radius: 6px;
|
|
253
|
+
}}
|
|
254
|
+
|
|
255
|
+
QTableWidget QHeaderView::section {{
|
|
256
|
+
background-color: {palette['button']};
|
|
257
|
+
color: {palette['text']};
|
|
258
|
+
font-size: 11pt;
|
|
259
|
+
font-weight: bold;
|
|
260
|
+
padding: 8px;
|
|
261
|
+
border: none;
|
|
262
|
+
border-bottom: 1px solid {palette['border']};
|
|
263
|
+
}}
|
|
264
|
+
|
|
265
|
+
QTableWidget::item {{
|
|
266
|
+
padding: 6px;
|
|
267
|
+
}}
|
|
268
|
+
|
|
269
|
+
QTableWidget::item:selected {{
|
|
270
|
+
background-color: {palette['highlight']};
|
|
271
|
+
color: #ffffff;
|
|
272
|
+
}}
|
|
273
|
+
|
|
274
|
+
/* ===== LABELS ===== */
|
|
275
|
+
QLabel {{
|
|
276
|
+
font-size: 11pt;
|
|
277
|
+
color: {palette['text']};
|
|
278
|
+
}}
|
|
279
|
+
|
|
280
|
+
/* Status label - for displaying status messages */
|
|
281
|
+
QLabel#StatusLabel {{
|
|
282
|
+
padding: 8px 12px;
|
|
283
|
+
background-color: {palette['surface']};
|
|
284
|
+
border: 1px solid {palette['border']};
|
|
285
|
+
border-radius: 6px;
|
|
286
|
+
font-size: 11pt;
|
|
287
|
+
}}
|
|
288
|
+
|
|
289
|
+
/* Secondary text - for hints and descriptions */
|
|
290
|
+
QLabel#SecondaryText {{
|
|
291
|
+
color: {palette['disabled']};
|
|
292
|
+
font-style: italic;
|
|
293
|
+
font-size: 10pt;
|
|
294
|
+
}}
|
|
295
|
+
|
|
296
|
+
/* ===== CHECKBOXES & RADIO BUTTONS ===== */
|
|
297
|
+
QCheckBox {{
|
|
298
|
+
font-size: 11pt;
|
|
299
|
+
min-height: 24px;
|
|
300
|
+
spacing: 8px;
|
|
301
|
+
}}
|
|
302
|
+
|
|
303
|
+
QCheckBox::indicator {{
|
|
304
|
+
width: 18px;
|
|
305
|
+
height: 18px;
|
|
306
|
+
border: 2px solid {palette['border']};
|
|
307
|
+
border-radius: 4px;
|
|
308
|
+
background-color: {palette['base']};
|
|
309
|
+
}}
|
|
310
|
+
|
|
311
|
+
QCheckBox::indicator:checked {{
|
|
312
|
+
background-color: {palette['highlight']};
|
|
313
|
+
border-color: {palette['highlight']};
|
|
314
|
+
}}
|
|
315
|
+
|
|
316
|
+
QCheckBox::indicator:hover {{
|
|
317
|
+
border-color: {palette['highlight']};
|
|
318
|
+
}}
|
|
319
|
+
|
|
320
|
+
QRadioButton {{
|
|
321
|
+
font-size: 11pt;
|
|
322
|
+
min-height: 24px;
|
|
323
|
+
spacing: 8px;
|
|
324
|
+
}}
|
|
325
|
+
|
|
326
|
+
QRadioButton::indicator {{
|
|
327
|
+
width: 18px;
|
|
328
|
+
height: 18px;
|
|
329
|
+
border: 2px solid {palette['border']};
|
|
330
|
+
border-radius: 9px;
|
|
331
|
+
background-color: {palette['base']};
|
|
332
|
+
}}
|
|
333
|
+
|
|
334
|
+
QRadioButton::indicator:checked {{
|
|
335
|
+
background-color: {palette['highlight']};
|
|
336
|
+
border-color: {palette['highlight']};
|
|
337
|
+
}}
|
|
338
|
+
|
|
339
|
+
/* ===== SLIDERS ===== */
|
|
340
|
+
QSlider {{
|
|
341
|
+
min-height: 28px;
|
|
342
|
+
}}
|
|
343
|
+
|
|
344
|
+
QSlider::groove:horizontal {{
|
|
345
|
+
height: 6px;
|
|
346
|
+
background: {palette['border']};
|
|
347
|
+
border-radius: 3px;
|
|
348
|
+
}}
|
|
349
|
+
|
|
350
|
+
QSlider::handle:horizontal {{
|
|
351
|
+
background: {palette['highlight']};
|
|
352
|
+
width: 18px;
|
|
353
|
+
height: 18px;
|
|
354
|
+
margin: -6px 0;
|
|
355
|
+
border-radius: 9px;
|
|
356
|
+
}}
|
|
357
|
+
|
|
358
|
+
QSlider::handle:horizontal:hover {{
|
|
359
|
+
background: {palette['highlight_hover']};
|
|
360
|
+
}}
|
|
361
|
+
|
|
362
|
+
QSlider::sub-page:horizontal {{
|
|
363
|
+
background: {palette['highlight']};
|
|
364
|
+
border-radius: 3px;
|
|
365
|
+
}}
|
|
366
|
+
|
|
367
|
+
/* ===== MENU BAR ===== */
|
|
368
|
+
QMenuBar {{
|
|
369
|
+
background-color: {palette['window']};
|
|
370
|
+
color: {palette['text']};
|
|
371
|
+
padding: 4px;
|
|
372
|
+
font-size: 11pt;
|
|
373
|
+
}}
|
|
374
|
+
|
|
375
|
+
QMenuBar::item {{
|
|
376
|
+
padding: 6px 12px;
|
|
377
|
+
border-radius: 4px;
|
|
378
|
+
}}
|
|
379
|
+
|
|
380
|
+
QMenuBar::item:selected {{
|
|
381
|
+
background-color: {palette['button_hover']};
|
|
382
|
+
}}
|
|
383
|
+
|
|
384
|
+
QMenu {{
|
|
385
|
+
background-color: {palette['surface']};
|
|
386
|
+
color: {palette['text']};
|
|
387
|
+
border: 1px solid {palette['border']};
|
|
388
|
+
border-radius: 8px;
|
|
389
|
+
padding: 6px;
|
|
390
|
+
}}
|
|
391
|
+
|
|
392
|
+
QMenu::item {{
|
|
393
|
+
padding: 8px 32px 8px 16px;
|
|
394
|
+
border-radius: 4px;
|
|
395
|
+
}}
|
|
396
|
+
|
|
397
|
+
QMenu::item:selected {{
|
|
398
|
+
background-color: {palette['highlight']};
|
|
399
|
+
color: #ffffff;
|
|
400
|
+
}}
|
|
401
|
+
|
|
402
|
+
QMenu::separator {{
|
|
403
|
+
height: 1px;
|
|
404
|
+
background: {palette['border']};
|
|
405
|
+
margin: 6px 12px;
|
|
406
|
+
}}
|
|
407
|
+
|
|
408
|
+
/* ===== TOOLBAR ===== */
|
|
409
|
+
QToolBar {{
|
|
410
|
+
background-color: {palette.get('toolbar_bg', palette['surface'])};
|
|
411
|
+
border: none;
|
|
412
|
+
padding: 4px;
|
|
413
|
+
spacing: 4px;
|
|
414
|
+
}}
|
|
415
|
+
|
|
416
|
+
QToolButton {{
|
|
417
|
+
background-color: transparent;
|
|
418
|
+
color: {"#ffffff" if not is_dark and 'toolbar_bg' in palette else palette['text']};
|
|
419
|
+
border: none;
|
|
420
|
+
border-radius: 6px;
|
|
421
|
+
padding: 6px;
|
|
422
|
+
}}
|
|
423
|
+
|
|
424
|
+
QToolButton:hover {{
|
|
425
|
+
background-color: {"#555555" if not is_dark and 'toolbar_bg' in palette else palette['button_hover']};
|
|
426
|
+
}}
|
|
427
|
+
|
|
428
|
+
QToolButton:pressed {{
|
|
429
|
+
background-color: {"#333333" if not is_dark and 'toolbar_bg' in palette else palette['button_pressed']};
|
|
430
|
+
}}
|
|
431
|
+
|
|
432
|
+
QToolButton:checked {{
|
|
433
|
+
background-color: {palette['highlight']};
|
|
434
|
+
}}
|
|
435
|
+
|
|
436
|
+
/* ===== STATUS BAR ===== */
|
|
437
|
+
QStatusBar {{
|
|
438
|
+
background-color: {palette['surface']};
|
|
439
|
+
color: {palette['text']};
|
|
440
|
+
font-size: 10pt;
|
|
441
|
+
border-top: 1px solid {palette['border']};
|
|
442
|
+
}}
|
|
443
|
+
|
|
444
|
+
/* ===== SCROLL BARS ===== */
|
|
445
|
+
QScrollBar:vertical {{
|
|
446
|
+
background: {palette['window']};
|
|
447
|
+
width: 12px;
|
|
448
|
+
border-radius: 6px;
|
|
449
|
+
margin: 0;
|
|
450
|
+
}}
|
|
451
|
+
|
|
452
|
+
QScrollBar::handle:vertical {{
|
|
453
|
+
background: {palette['button']};
|
|
454
|
+
min-height: 30px;
|
|
455
|
+
border-radius: 6px;
|
|
456
|
+
margin: 2px;
|
|
457
|
+
}}
|
|
458
|
+
|
|
459
|
+
QScrollBar::handle:vertical:hover {{
|
|
460
|
+
background: {palette['button_hover']};
|
|
461
|
+
}}
|
|
462
|
+
|
|
463
|
+
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{
|
|
464
|
+
height: 0;
|
|
465
|
+
}}
|
|
466
|
+
|
|
467
|
+
QScrollBar:horizontal {{
|
|
468
|
+
background: {palette['window']};
|
|
469
|
+
height: 12px;
|
|
470
|
+
border-radius: 6px;
|
|
471
|
+
margin: 0;
|
|
472
|
+
}}
|
|
473
|
+
|
|
474
|
+
QScrollBar::handle:horizontal {{
|
|
475
|
+
background: {palette['button']};
|
|
476
|
+
min-width: 30px;
|
|
477
|
+
border-radius: 6px;
|
|
478
|
+
margin: 2px;
|
|
479
|
+
}}
|
|
480
|
+
|
|
481
|
+
QScrollBar::handle:horizontal:hover {{
|
|
482
|
+
background: {palette['button_hover']};
|
|
483
|
+
}}
|
|
484
|
+
|
|
485
|
+
QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {{
|
|
486
|
+
width: 0;
|
|
487
|
+
}}
|
|
488
|
+
|
|
489
|
+
/* ===== DIALOGS ===== */
|
|
490
|
+
QDialog {{
|
|
491
|
+
background-color: {palette['window']};
|
|
492
|
+
}}
|
|
493
|
+
|
|
494
|
+
QDialogButtonBox QPushButton {{
|
|
495
|
+
min-width: 90px;
|
|
496
|
+
}}
|
|
497
|
+
|
|
498
|
+
/* ===== MESSAGE BOX ===== */
|
|
499
|
+
QMessageBox {{
|
|
500
|
+
background-color: {palette['window']};
|
|
501
|
+
}}
|
|
502
|
+
|
|
503
|
+
/* ===== SPLITTER ===== */
|
|
504
|
+
QSplitter::handle {{
|
|
505
|
+
background-color: {palette['border']};
|
|
506
|
+
}}
|
|
507
|
+
|
|
508
|
+
QSplitter::handle:horizontal {{
|
|
509
|
+
width: 2px;
|
|
510
|
+
}}
|
|
511
|
+
|
|
512
|
+
QSplitter::handle:vertical {{
|
|
513
|
+
height: 2px;
|
|
514
|
+
}}
|
|
515
|
+
|
|
516
|
+
/* ===== FRAME ===== */
|
|
517
|
+
QFrame {{
|
|
518
|
+
border-radius: 4px;
|
|
519
|
+
}}
|
|
520
|
+
"""
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def get_matplotlib_params(palette, is_dark=True):
|
|
524
|
+
"""Get matplotlib rcParams for the given palette."""
|
|
525
|
+
if is_dark:
|
|
526
|
+
return {
|
|
527
|
+
"figure.facecolor": palette["window"],
|
|
528
|
+
"axes.facecolor": palette["base"],
|
|
529
|
+
"axes.edgecolor": palette["text"],
|
|
530
|
+
"axes.labelcolor": palette["text"],
|
|
531
|
+
"xtick.color": palette["text"],
|
|
532
|
+
"ytick.color": palette["text"],
|
|
533
|
+
"grid.color": palette["border"],
|
|
534
|
+
"text.color": palette["text"],
|
|
535
|
+
"legend.facecolor": palette["base"],
|
|
536
|
+
"legend.edgecolor": palette["border"],
|
|
537
|
+
"figure.edgecolor": palette["border"],
|
|
538
|
+
}
|
|
539
|
+
else:
|
|
540
|
+
# Light mode - use white background, dark text
|
|
541
|
+
return {
|
|
542
|
+
"figure.facecolor": palette.get("plot_bg", "#ffffff"),
|
|
543
|
+
"axes.facecolor": palette.get("plot_bg", "#ffffff"),
|
|
544
|
+
"axes.edgecolor": palette.get("plot_text", "#1a1a1a"),
|
|
545
|
+
"axes.labelcolor": palette.get("plot_text", "#1a1a1a"),
|
|
546
|
+
"xtick.color": palette.get("plot_text", "#1a1a1a"),
|
|
547
|
+
"ytick.color": palette.get("plot_text", "#1a1a1a"),
|
|
548
|
+
"grid.color": palette.get("plot_grid", "#cccccc"),
|
|
549
|
+
"text.color": palette.get("plot_text", "#1a1a1a"),
|
|
550
|
+
"legend.facecolor": palette.get("plot_bg", "#ffffff"),
|
|
551
|
+
"legend.edgecolor": palette.get("border", "#b8b8bc"),
|
|
552
|
+
"figure.edgecolor": palette.get("border", "#b8b8bc"),
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
class ThemeManager:
|
|
557
|
+
"""Manages theme switching for the application."""
|
|
558
|
+
|
|
559
|
+
DARK = "dark"
|
|
560
|
+
LIGHT = "light"
|
|
561
|
+
|
|
562
|
+
def __init__(self):
|
|
563
|
+
self._current_theme = self.DARK
|
|
564
|
+
self._callbacks = []
|
|
565
|
+
|
|
566
|
+
@property
|
|
567
|
+
def current_theme(self):
|
|
568
|
+
return self._current_theme
|
|
569
|
+
|
|
570
|
+
@property
|
|
571
|
+
def is_dark(self):
|
|
572
|
+
return self._current_theme == self.DARK
|
|
573
|
+
|
|
574
|
+
@property
|
|
575
|
+
def palette(self):
|
|
576
|
+
return DARK_PALETTE if self.is_dark else LIGHT_PALETTE
|
|
577
|
+
|
|
578
|
+
@property
|
|
579
|
+
def stylesheet(self):
|
|
580
|
+
return get_stylesheet(self.palette, self.is_dark)
|
|
581
|
+
|
|
582
|
+
@property
|
|
583
|
+
def matplotlib_params(self):
|
|
584
|
+
return get_matplotlib_params(self.palette, self.is_dark)
|
|
585
|
+
|
|
586
|
+
def set_theme(self, theme):
|
|
587
|
+
"""Set the current theme."""
|
|
588
|
+
if theme not in (self.DARK, self.LIGHT):
|
|
589
|
+
raise ValueError(f"Invalid theme: {theme}")
|
|
590
|
+
|
|
591
|
+
if theme != self._current_theme:
|
|
592
|
+
self._current_theme = theme
|
|
593
|
+
self._notify_callbacks()
|
|
594
|
+
|
|
595
|
+
def toggle_theme(self):
|
|
596
|
+
"""Toggle between dark and light themes."""
|
|
597
|
+
new_theme = self.LIGHT if self.is_dark else self.DARK
|
|
598
|
+
self.set_theme(new_theme)
|
|
599
|
+
return new_theme
|
|
600
|
+
|
|
601
|
+
def register_callback(self, callback):
|
|
602
|
+
"""Register a callback to be called when theme changes."""
|
|
603
|
+
if callback not in self._callbacks:
|
|
604
|
+
self._callbacks.append(callback)
|
|
605
|
+
|
|
606
|
+
def unregister_callback(self, callback):
|
|
607
|
+
"""Unregister a theme change callback."""
|
|
608
|
+
if callback in self._callbacks:
|
|
609
|
+
self._callbacks.remove(callback)
|
|
610
|
+
|
|
611
|
+
def _notify_callbacks(self):
|
|
612
|
+
"""Notify all registered callbacks of theme change."""
|
|
613
|
+
for callback in self._callbacks:
|
|
614
|
+
try:
|
|
615
|
+
callback(self._current_theme)
|
|
616
|
+
except Exception as e:
|
|
617
|
+
print(f"Error in theme callback: {e}")
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
# Global theme manager instance
|
|
621
|
+
theme_manager = ThemeManager()
|
|
622
|
+
|
|
623
|
+
# For backward compatibility
|
|
624
|
+
STYLESHEET = get_stylesheet(DARK_PALETTE, is_dark=True)
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def get_icon_path(icon_name):
|
|
628
|
+
"""Get the appropriate icon path based on current theme.
|
|
629
|
+
|
|
630
|
+
For light mode, returns the _light version of the icon if it exists.
|
|
631
|
+
|
|
632
|
+
Args:
|
|
633
|
+
icon_name: Base icon filename (e.g., 'browse.png')
|
|
634
|
+
|
|
635
|
+
Returns:
|
|
636
|
+
Icon filename to use (e.g., 'browse.png' or 'browse_light.png')
|
|
637
|
+
"""
|
|
638
|
+
if theme_manager.is_dark:
|
|
639
|
+
return icon_name
|
|
640
|
+
else:
|
|
641
|
+
# Use light version for light mode
|
|
642
|
+
name, ext = icon_name.rsplit('.', 1)
|
|
643
|
+
return f"{name}_light.{ext}"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Utility modules for solarviewer."""
|
|
2
|
+
|
|
3
|
+
# Import rate limiting utilities from local module
|
|
4
|
+
from .rate_limiter import (
|
|
5
|
+
RateLimitedSession,
|
|
6
|
+
CachedSession,
|
|
7
|
+
get_global_session,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
# Re-export all utilities from parent utils.py module
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
parent_utils_path = Path(__file__).parent.parent / 'utils.py'
|
|
15
|
+
if parent_utils_path.exists():
|
|
16
|
+
import importlib.util
|
|
17
|
+
spec = importlib.util.spec_from_file_location("_parent_utils", str(parent_utils_path))
|
|
18
|
+
if spec and spec.loader:
|
|
19
|
+
_parent_utils = importlib.util.module_from_spec(spec)
|
|
20
|
+
spec.loader.exec_module(_parent_utils)
|
|
21
|
+
|
|
22
|
+
# Re-export all public functions from parent utils
|
|
23
|
+
for name in dir(_parent_utils):
|
|
24
|
+
if not name.startswith('_'):
|
|
25
|
+
globals()[name] = getattr(_parent_utils, name)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"RateLimitedSession",
|
|
29
|
+
"CachedSession",
|
|
30
|
+
"get_global_session",
|
|
31
|
+
"get_pixel_values_from_image", # From parent utils.py
|
|
32
|
+
]
|