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.
Files changed (82) hide show
  1. solar_radio_image_viewer/__init__.py +12 -0
  2. solar_radio_image_viewer/assets/add_tab_default.png +0 -0
  3. solar_radio_image_viewer/assets/add_tab_default_light.png +0 -0
  4. solar_radio_image_viewer/assets/add_tab_hover.png +0 -0
  5. solar_radio_image_viewer/assets/add_tab_hover_light.png +0 -0
  6. solar_radio_image_viewer/assets/browse.png +0 -0
  7. solar_radio_image_viewer/assets/browse_light.png +0 -0
  8. solar_radio_image_viewer/assets/close_tab_default.png +0 -0
  9. solar_radio_image_viewer/assets/close_tab_default_light.png +0 -0
  10. solar_radio_image_viewer/assets/close_tab_hover.png +0 -0
  11. solar_radio_image_viewer/assets/close_tab_hover_light.png +0 -0
  12. solar_radio_image_viewer/assets/ellipse_selection.png +0 -0
  13. solar_radio_image_viewer/assets/ellipse_selection_light.png +0 -0
  14. solar_radio_image_viewer/assets/icons8-ellipse-90.png +0 -0
  15. solar_radio_image_viewer/assets/icons8-ellipse-90_light.png +0 -0
  16. solar_radio_image_viewer/assets/icons8-info-90.png +0 -0
  17. solar_radio_image_viewer/assets/icons8-info-90_light.png +0 -0
  18. solar_radio_image_viewer/assets/profile.png +0 -0
  19. solar_radio_image_viewer/assets/profile_light.png +0 -0
  20. solar_radio_image_viewer/assets/rectangle_selection.png +0 -0
  21. solar_radio_image_viewer/assets/rectangle_selection_light.png +0 -0
  22. solar_radio_image_viewer/assets/reset.png +0 -0
  23. solar_radio_image_viewer/assets/reset_light.png +0 -0
  24. solar_radio_image_viewer/assets/ruler.png +0 -0
  25. solar_radio_image_viewer/assets/ruler_light.png +0 -0
  26. solar_radio_image_viewer/assets/search.png +0 -0
  27. solar_radio_image_viewer/assets/search_light.png +0 -0
  28. solar_radio_image_viewer/assets/settings.png +0 -0
  29. solar_radio_image_viewer/assets/settings_light.png +0 -0
  30. solar_radio_image_viewer/assets/splash.fits +0 -0
  31. solar_radio_image_viewer/assets/zoom_60arcmin.png +0 -0
  32. solar_radio_image_viewer/assets/zoom_60arcmin_light.png +0 -0
  33. solar_radio_image_viewer/assets/zoom_in.png +0 -0
  34. solar_radio_image_viewer/assets/zoom_in_light.png +0 -0
  35. solar_radio_image_viewer/assets/zoom_out.png +0 -0
  36. solar_radio_image_viewer/assets/zoom_out_light.png +0 -0
  37. solar_radio_image_viewer/create_video.py +1345 -0
  38. solar_radio_image_viewer/dialogs.py +2665 -0
  39. solar_radio_image_viewer/from_simpl/__init__.py +184 -0
  40. solar_radio_image_viewer/from_simpl/caltable_visualizer.py +1001 -0
  41. solar_radio_image_viewer/from_simpl/dynamic_spectra_dialog.py +332 -0
  42. solar_radio_image_viewer/from_simpl/make_dynamic_spectra.py +351 -0
  43. solar_radio_image_viewer/from_simpl/pipeline_logger_gui.py +1232 -0
  44. solar_radio_image_viewer/from_simpl/simpl_theme.py +352 -0
  45. solar_radio_image_viewer/from_simpl/utils.py +984 -0
  46. solar_radio_image_viewer/from_simpl/view_dynamic_spectra_GUI.py +1975 -0
  47. solar_radio_image_viewer/helioprojective.py +1916 -0
  48. solar_radio_image_viewer/helioprojective_viewer.py +817 -0
  49. solar_radio_image_viewer/helioviewer_browser.py +1514 -0
  50. solar_radio_image_viewer/main.py +148 -0
  51. solar_radio_image_viewer/move_phasecenter.py +1269 -0
  52. solar_radio_image_viewer/napari_viewer.py +368 -0
  53. solar_radio_image_viewer/noaa_events/__init__.py +32 -0
  54. solar_radio_image_viewer/noaa_events/noaa_events.py +430 -0
  55. solar_radio_image_viewer/noaa_events/noaa_events_gui.py +1922 -0
  56. solar_radio_image_viewer/norms.py +293 -0
  57. solar_radio_image_viewer/radio_data_downloader/__init__.py +25 -0
  58. solar_radio_image_viewer/radio_data_downloader/radio_data_downloader.py +756 -0
  59. solar_radio_image_viewer/radio_data_downloader/radio_data_downloader_gui.py +528 -0
  60. solar_radio_image_viewer/searchable_combobox.py +220 -0
  61. solar_radio_image_viewer/solar_context/__init__.py +41 -0
  62. solar_radio_image_viewer/solar_context/active_regions.py +371 -0
  63. solar_radio_image_viewer/solar_context/cme_alerts.py +234 -0
  64. solar_radio_image_viewer/solar_context/context_images.py +297 -0
  65. solar_radio_image_viewer/solar_context/realtime_data.py +528 -0
  66. solar_radio_image_viewer/solar_data_downloader/__init__.py +35 -0
  67. solar_radio_image_viewer/solar_data_downloader/solar_data_downloader.py +1667 -0
  68. solar_radio_image_viewer/solar_data_downloader/solar_data_downloader_cli.py +901 -0
  69. solar_radio_image_viewer/solar_data_downloader/solar_data_downloader_gui.py +1210 -0
  70. solar_radio_image_viewer/styles.py +643 -0
  71. solar_radio_image_viewer/utils/__init__.py +32 -0
  72. solar_radio_image_viewer/utils/rate_limiter.py +255 -0
  73. solar_radio_image_viewer/utils.py +952 -0
  74. solar_radio_image_viewer/video_dialog.py +2629 -0
  75. solar_radio_image_viewer/video_utils.py +656 -0
  76. solar_radio_image_viewer/viewer.py +11174 -0
  77. solarviewer-1.0.2.dist-info/METADATA +343 -0
  78. solarviewer-1.0.2.dist-info/RECORD +82 -0
  79. solarviewer-1.0.2.dist-info/WHEEL +5 -0
  80. solarviewer-1.0.2.dist-info/entry_points.txt +8 -0
  81. solarviewer-1.0.2.dist-info/licenses/LICENSE +21 -0
  82. 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
+ ]