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,2665 @@
1
+ from PyQt5.QtWidgets import (
2
+ QDialog,
3
+ QVBoxLayout,
4
+ QHBoxLayout,
5
+ QGroupBox,
6
+ QRadioButton,
7
+ QLineEdit,
8
+ QLabel,
9
+ QPushButton,
10
+ QSpinBox,
11
+ QDoubleSpinBox,
12
+ QGridLayout,
13
+ QFormLayout,
14
+ QDialogButtonBox,
15
+ QComboBox,
16
+ QCheckBox,
17
+ QFileDialog,
18
+ QMessageBox,
19
+ QListWidget,
20
+ QListWidgetItem,
21
+ QPlainTextEdit,
22
+ QButtonGroup,
23
+ QWidget,
24
+ QProgressDialog,
25
+ QFrame,
26
+ )
27
+ from PyQt5.QtGui import QIcon
28
+ from PyQt5.QtCore import Qt, QSize
29
+ import pkg_resources
30
+ import numpy as np
31
+ import os
32
+ import multiprocessing
33
+ import glob
34
+ from PyQt5.QtWidgets import QApplication
35
+ import uuid
36
+ import traceback
37
+ import time
38
+
39
+
40
+ # Standalone function for multiprocessing
41
+ def process_single_file_hpc(args):
42
+ """Process a single file for HPC conversion - standalone function for multiprocessing
43
+
44
+ Parameters:
45
+ -----------
46
+ args : tuple
47
+ Tuple containing (input_file, output_path, stokes, process_id)
48
+
49
+ Returns:
50
+ --------
51
+ dict
52
+ Result dictionary with processing outcome
53
+ """
54
+ input_file, output_path, stokes, process_id = args
55
+
56
+ try:
57
+ result = {
58
+ "input_file": input_file,
59
+ "output_path": output_path,
60
+ "stokes": stokes,
61
+ "success": False,
62
+ "error": None,
63
+ }
64
+
65
+ # Import the function here to ensure we have it in the subprocess
66
+ from .helioprojective import convert_and_save_hpc
67
+
68
+ # Generate a unique file suffix for this process to avoid conflicts
69
+ temp_suffix = f"_proc_{process_id}_{uuid.uuid4().hex[:8]}"
70
+
71
+ # Convert file with unique temp file handling
72
+ success = convert_and_save_hpc(
73
+ input_file,
74
+ output_path,
75
+ Stokes=stokes,
76
+ overwrite=True,
77
+ temp_suffix=temp_suffix,
78
+ )
79
+
80
+ result["success"] = success
81
+ return result
82
+ except Exception as e:
83
+ result["error"] = str(e)
84
+ result["traceback"] = traceback.format_exc()
85
+ return result
86
+
87
+
88
+ class ContourSettingsDialog(QDialog):
89
+ """Dialog for configuring contour settings with a more compact layout."""
90
+
91
+ def __init__(self, parent=None, settings=None):
92
+ super().__init__(parent)
93
+ self.setWindowTitle("Contour Settings")
94
+ self.settings = settings.copy() if settings else {}
95
+
96
+ # Set stylesheet BEFORE creating widgets so styles apply correctly
97
+ self.setStyleSheet(
98
+ """
99
+ QGroupBox {
100
+ border: 1px solid #666666;
101
+ border-radius: 25px;
102
+ margin-top: 16px;
103
+ padding: 15px;
104
+ padding-top: 10px;
105
+ font-weight: bold;
106
+ }
107
+ QGroupBox::title {
108
+ subcontrol-origin: margin;
109
+ subcontrol-position: top left;
110
+ left: 15px;
111
+ top: 2px;
112
+ padding: 2px 12px;
113
+ background-color: palette(window);
114
+ border-radius: 15px;
115
+ }
116
+ QLineEdit {
117
+ padding: 5px;
118
+ border: 1px solid #666666;
119
+ border-radius: 8px;
120
+ }
121
+ QLineEdit:disabled {
122
+ background-color: #555555;
123
+ color: #888888;
124
+ }
125
+ QComboBox {
126
+ padding: 5px;
127
+ border: 1px solid #666666;
128
+ border-radius: 8px;
129
+ }
130
+ QComboBox:disabled {
131
+ background-color: #555555;
132
+ color: #888888;
133
+ }
134
+ QRadioButton:disabled {
135
+ color: #888888;
136
+ }
137
+ QSpinBox, QDoubleSpinBox {
138
+ border-radius: 8px;
139
+ }
140
+ QSpinBox:disabled, QDoubleSpinBox:disabled {
141
+ background-color: #555555;
142
+ color: #888888;
143
+ }
144
+ """
145
+ )
146
+
147
+ self.setup_ui()
148
+
149
+ def setup_ui(self):
150
+
151
+ main_layout = QVBoxLayout(self)
152
+ main_layout.setSpacing(10)
153
+ main_layout.setContentsMargins(10, 10, 10, 10)
154
+
155
+ # Top row: Source selection and Stokes parameter side by side
156
+ top_layout = QHBoxLayout()
157
+ top_layout.setSpacing(10)
158
+
159
+ # Source selection group
160
+ source_group = QGroupBox("Contour Source")
161
+ source_layout = QVBoxLayout(source_group)
162
+ source_layout.setSpacing(10)
163
+ source_layout.setContentsMargins(10, 15, 10, 10)
164
+
165
+ # Main radio buttons for source selection
166
+ source_radio_layout = QHBoxLayout()
167
+ source_radio_layout.setSpacing(20)
168
+ self.same_image_radio = QRadioButton("Current Image")
169
+ self.external_image_radio = QRadioButton("External Image")
170
+ if self.settings.get("source") == "external":
171
+ self.external_image_radio.setChecked(True)
172
+ else:
173
+ self.same_image_radio.setChecked(True)
174
+ source_radio_layout.addWidget(self.same_image_radio)
175
+ source_radio_layout.addWidget(self.external_image_radio)
176
+ source_radio_layout.addStretch()
177
+ source_layout.addLayout(source_radio_layout)
178
+
179
+ # External image options in a subgroup
180
+ self.external_group = (
181
+ QWidget()
182
+ ) # Changed from QGroupBox to QWidget for better visual
183
+ external_layout = QVBoxLayout(self.external_group)
184
+ external_layout.setSpacing(8)
185
+ external_layout.setContentsMargins(20, 0, 0, 0) # Add left indent
186
+
187
+ # Radio buttons for file type selection
188
+ file_type_layout = QHBoxLayout()
189
+ file_type_layout.setSpacing(20)
190
+ self.radio_casa_image = QRadioButton("CASA Image")
191
+ self.radio_fits_file = QRadioButton("FITS File")
192
+ self.radio_casa_image.setChecked(True)
193
+ file_type_layout.addWidget(self.radio_casa_image)
194
+ file_type_layout.addWidget(self.radio_fits_file)
195
+ file_type_layout.addStretch()
196
+ external_layout.addLayout(file_type_layout)
197
+
198
+ # Browse layout
199
+ browse_layout = QHBoxLayout()
200
+ browse_layout.setSpacing(8)
201
+ self.file_path_edit = QLineEdit(self.settings.get("external_image", ""))
202
+ self.file_path_edit.setPlaceholderText("Select CASA image directory...")
203
+ self.file_path_edit.setMinimumWidth(
204
+ 250
205
+ ) # Set minimum width for better appearance
206
+
207
+ self.browse_button = QPushButton()
208
+ self.browse_button.setObjectName("IconOnlyNBGButton")
209
+ self.browse_button.setIcon(
210
+ QIcon(
211
+ pkg_resources.resource_filename(
212
+ "solar_radio_image_viewer", "assets/browse.png"
213
+ )
214
+ )
215
+ )
216
+ self.browse_button.setIconSize(QSize(24, 24))
217
+ self.browse_button.setToolTip("Browse")
218
+ self.browse_button.setFixedSize(32, 32)
219
+ self.browse_button.clicked.connect(self.browse_file)
220
+
221
+ # Store both icon variants for theme switching
222
+ self.browse_icon_light = QIcon(
223
+ pkg_resources.resource_filename(
224
+ "solar_radio_image_viewer", "assets/browse.png"
225
+ )
226
+ )
227
+ self.browse_icon_dark = QIcon(
228
+ pkg_resources.resource_filename(
229
+ "solar_radio_image_viewer", "assets/browse_light.png"
230
+ )
231
+ )
232
+
233
+ # Set initial icon based on palette
234
+ self._update_browse_icon()
235
+
236
+ self.browse_button.setStyleSheet(
237
+ """
238
+ QPushButton {
239
+ background-color: transparent;
240
+ border: none;
241
+ padding: 4px;
242
+ }
243
+ QPushButton:hover {
244
+ background-color: #484848;
245
+ }
246
+ QPushButton:pressed {
247
+ background-color: #303030;
248
+ }
249
+ QPushButton:disabled {
250
+ background-color: transparent;
251
+ }
252
+ """
253
+ )
254
+
255
+ browse_layout.addWidget(self.file_path_edit)
256
+ browse_layout.addWidget(self.browse_button)
257
+ external_layout.addLayout(browse_layout)
258
+
259
+ source_layout.addWidget(self.external_group)
260
+ top_layout.addWidget(source_group)
261
+
262
+ # Stokes parameter group
263
+ stokes_group = QGroupBox("Stokes Parameter")
264
+ stokes_layout = QHBoxLayout(stokes_group)
265
+ stokes_layout.setContentsMargins(10, 15, 10, 10)
266
+
267
+ stokes_label = QLabel("Stokes:")
268
+ stokes_label.setMinimumWidth(55) # Minimum width to prevent cutoff
269
+ self.stokes_combo = QComboBox()
270
+ self.stokes_combo.addItems(
271
+ ["I", "Q", "U", "V", "Q/I", "U/I", "V/I", "L", "Lfrac", "PANG"]
272
+ )
273
+ self.stokes_combo.setFixedWidth(80)
274
+ current_stokes = self.settings.get("stokes", "I")
275
+ self.stokes_combo.setCurrentText(current_stokes)
276
+
277
+ stokes_layout.addWidget(stokes_label)
278
+ stokes_layout.addWidget(self.stokes_combo)
279
+ stokes_layout.addStretch()
280
+
281
+ # Set fixed size for stokes group to match source group height
282
+ stokes_group.setFixedHeight(source_group.sizeHint().height())
283
+ stokes_group.setMinimumWidth(200) # Set minimum width
284
+ top_layout.addWidget(stokes_group)
285
+
286
+ main_layout.addLayout(top_layout)
287
+
288
+ # Create button group for CASA/FITS selection
289
+ self.file_type_button_group = QButtonGroup()
290
+ self.file_type_button_group.addButton(self.radio_casa_image)
291
+ self.file_type_button_group.addButton(self.radio_fits_file)
292
+
293
+ # Connect signals for enabling/disabling external options
294
+ self.external_image_radio.toggled.connect(self.update_external_options)
295
+ self.radio_casa_image.toggled.connect(self.update_placeholder_text)
296
+ self.radio_fits_file.toggled.connect(self.update_placeholder_text)
297
+
298
+ # Initially update states
299
+ self.update_external_options(self.external_image_radio.isChecked())
300
+ self.update_placeholder_text()
301
+
302
+ # Middle row: Contour Levels and Appearance side by side
303
+ mid_layout = QHBoxLayout()
304
+
305
+ # Contour Levels group with a form layout
306
+ levels_group = QGroupBox("Contour Levels")
307
+ levels_layout = QFormLayout(levels_group)
308
+ self.level_type_combo = QComboBox()
309
+ self.level_type_combo.addItems(["fraction", "absolute", "sigma"])
310
+ current_level_type = self.settings.get("level_type", "fraction")
311
+ self.level_type_combo.setCurrentText(current_level_type)
312
+ levels_layout.addRow("Level Type:", self.level_type_combo)
313
+ self.pos_levels_edit = QLineEdit(
314
+ ", ".join(
315
+ str(level)
316
+ for level in self.settings.get("pos_levels", [0.1, 0.3, 0.5, 0.7, 0.9])
317
+ )
318
+ )
319
+ levels_layout.addRow("Positive Levels:", self.pos_levels_edit)
320
+ self.neg_levels_edit = QLineEdit(
321
+ ", ".join(
322
+ str(level)
323
+ for level in self.settings.get("neg_levels", [0.1, 0.3, 0.5, 0.7, 0.9])
324
+ )
325
+ )
326
+ levels_layout.addRow("Negative Levels:", self.neg_levels_edit)
327
+
328
+ # Connect level type change to update default levels
329
+ self.level_type_combo.currentTextChanged.connect(self.on_level_type_changed)
330
+
331
+ mid_layout.addWidget(levels_group)
332
+
333
+
334
+ # Appearance group with a form layout
335
+ appearance_group = QGroupBox("Appearance")
336
+ appearance_layout = QFormLayout(appearance_group)
337
+ self.color_combo = QComboBox()
338
+ self.color_combo.addItems(
339
+ ["white", "black", "red", "green", "blue", "yellow", "cyan", "magenta"]
340
+ )
341
+ current_color = self.settings.get("color", "white")
342
+ self.color_combo.setCurrentText(current_color)
343
+ appearance_layout.addRow("Color:", self.color_combo)
344
+ self.linewidth_spin = QDoubleSpinBox()
345
+ self.linewidth_spin.setRange(0.1, 5.0)
346
+ self.linewidth_spin.setSingleStep(0.1)
347
+ self.linewidth_spin.setValue(self.settings.get("linewidth", 1.0))
348
+ appearance_layout.addRow("Line Width:", self.linewidth_spin)
349
+ self.pos_linestyle_combo = QComboBox()
350
+ self.pos_linestyle_combo.addItems(["-", "--", "-.", ":"])
351
+ current_pos_linestyle = self.settings.get("pos_linestyle", "-")
352
+ self.pos_linestyle_combo.setCurrentText(current_pos_linestyle)
353
+ appearance_layout.addRow("Positive Style:", self.pos_linestyle_combo)
354
+ self.neg_linestyle_combo = QComboBox()
355
+ self.neg_linestyle_combo.addItems(["-", "--", "-.", ":"])
356
+ current_neg_linestyle = self.settings.get("neg_linestyle", "--")
357
+ self.neg_linestyle_combo.setCurrentText(current_neg_linestyle)
358
+ appearance_layout.addRow("Negative Style:", self.neg_linestyle_combo)
359
+ mid_layout.addWidget(appearance_group)
360
+
361
+ main_layout.addLayout(mid_layout)
362
+
363
+ # Bottom row: RMS Calculation Region in a compact grid layout
364
+ rms_group = QGroupBox("RMS Calculation Region")
365
+ rms_layout = QGridLayout(rms_group)
366
+ self.use_default_rms_box = QCheckBox("Use default RMS region")
367
+ self.use_default_rms_box.setChecked(
368
+ self.settings.get("use_default_rms_region", True)
369
+ )
370
+ self.use_default_rms_box.stateChanged.connect(self.toggle_rms_inputs)
371
+ rms_layout.addWidget(self.use_default_rms_box, 0, 0, 1, 4)
372
+ # Arrange X min and Y min side by side, then X max and Y max
373
+ rms_layout.addWidget(QLabel("X min:"), 1, 0)
374
+ self.rms_xmin = QSpinBox()
375
+ self.rms_xmin.setRange(0, 10000)
376
+ self.rms_xmin.setValue(self.settings.get("rms_box", (0, 200, 0, 130))[0])
377
+ rms_layout.addWidget(self.rms_xmin, 1, 1)
378
+ rms_layout.addWidget(QLabel("Y min:"), 1, 2)
379
+ self.rms_ymin = QSpinBox()
380
+ self.rms_ymin.setRange(0, 10000)
381
+ self.rms_ymin.setValue(self.settings.get("rms_box", (0, 200, 0, 130))[2])
382
+ rms_layout.addWidget(self.rms_ymin, 1, 3)
383
+ rms_layout.addWidget(QLabel("X max:"), 2, 0)
384
+ self.rms_xmax = QSpinBox()
385
+ self.rms_xmax.setRange(0, 10000)
386
+ self.rms_xmax.setValue(self.settings.get("rms_box", (0, 200, 0, 130))[1])
387
+ rms_layout.addWidget(self.rms_xmax, 2, 1)
388
+ rms_layout.addWidget(QLabel("Y max:"), 2, 2)
389
+ self.rms_ymax = QSpinBox()
390
+ self.rms_ymax.setRange(0, 10000)
391
+ self.rms_ymax.setValue(self.settings.get("rms_box", (0, 200, 0, 130))[3])
392
+ rms_layout.addWidget(self.rms_ymax, 2, 3)
393
+ main_layout.addWidget(rms_group)
394
+
395
+ # Initialize RMS inputs state
396
+ self.toggle_rms_inputs()
397
+
398
+ # Button box at the bottom
399
+ button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
400
+ button_box.accepted.connect(self.accept)
401
+ button_box.rejected.connect(self.reject)
402
+ main_layout.addWidget(button_box)
403
+
404
+ def toggle_rms_inputs(self):
405
+ """Update the enabled state and visual appearance of RMS inputs."""
406
+ enabled = not self.use_default_rms_box.isChecked()
407
+
408
+ # Create a widget list for consistent state management
409
+ rms_inputs = [self.rms_xmin, self.rms_xmax, self.rms_ymin, self.rms_ymax]
410
+
411
+ # Update enabled state for all inputs
412
+ for widget in rms_inputs:
413
+ widget.setEnabled(enabled)
414
+
415
+ # Clear any custom styles - let palette handle colors
416
+ for widget in rms_inputs:
417
+ widget.setStyleSheet("")
418
+
419
+ def update_external_options(self, enabled):
420
+ """Update the enabled state and visual appearance of external options."""
421
+ #print(f"update_external_options called with enabled={enabled}")
422
+
423
+ # Explicitly disable/enable each widget
424
+ self.radio_casa_image.setEnabled(enabled)
425
+ self.radio_fits_file.setEnabled(enabled)
426
+ self.file_path_edit.setEnabled(enabled)
427
+ self.browse_button.setEnabled(enabled)
428
+
429
+ # Also set the parent group
430
+ self.external_group.setEnabled(enabled)
431
+
432
+ #print(f" radio_casa_image.isEnabled() = {self.radio_casa_image.isEnabled()}")
433
+ #print(f" radio_fits_file.isEnabled() = {self.radio_fits_file.isEnabled()}")
434
+
435
+
436
+ def _update_browse_icon(self):
437
+ """Update browse button icon based on current palette (light/dark mode)."""
438
+ # Check if we're in light or dark mode by examining window color
439
+ palette = self.palette()
440
+ window_color = palette.color(palette.Window)
441
+ # If window color is light (high luminance), use dark icon
442
+ luminance = 0.299 * window_color.red() + 0.587 * window_color.green() + 0.114 * window_color.blue()
443
+ if luminance > 128:
444
+ # Light mode - use dark icon
445
+ if hasattr(self, 'browse_icon_dark'):
446
+ self.browse_button.setIcon(self.browse_icon_dark)
447
+ else:
448
+ # Dark mode - use light icon
449
+ if hasattr(self, 'browse_icon_light'):
450
+ self.browse_button.setIcon(self.browse_icon_light)
451
+
452
+ def on_level_type_changed(self, level_type):
453
+ """Update default levels when level type changes."""
454
+ # Define default levels for each type
455
+ defaults = {
456
+ "fraction": {
457
+ "pos": [0.1, 0.3, 0.5, 0.7, 0.9],
458
+ "neg": [0.1, 0.3, 0.5, 0.7, 0.9]
459
+ },
460
+ "sigma": {
461
+ "pos": [3, 6, 9, 12, 15, 20, 25, 30],
462
+ "neg": [3, 6, 9, 12, 15, 20, 25, 30]
463
+ },
464
+ "absolute": {
465
+ "pos": [50, 100, 500, 1000, 5000, 10000],
466
+ "neg": [50, 100, 500, 1000, 5000, 10000]
467
+ }
468
+ }
469
+
470
+ if level_type in defaults:
471
+ pos_levels = defaults[level_type]["pos"]
472
+ neg_levels = defaults[level_type]["neg"]
473
+ self.pos_levels_edit.setText(", ".join(str(l) for l in pos_levels))
474
+ self.neg_levels_edit.setText(", ".join(str(l) for l in neg_levels))
475
+
476
+ def update_placeholder_text(self):
477
+
478
+ if self.radio_casa_image.isChecked():
479
+ self.file_path_edit.setPlaceholderText("Select CASA image directory...")
480
+ else:
481
+ self.file_path_edit.setPlaceholderText("Select FITS file...")
482
+
483
+ def browse_file(self):
484
+ if self.radio_casa_image.isChecked():
485
+ # Select CASA image directory
486
+ directory = QFileDialog.getExistingDirectory(
487
+ self, "Select a CASA Image Directory"
488
+ )
489
+ if directory:
490
+ self.file_path_edit.setText(directory)
491
+ else:
492
+ # Select FITS file
493
+ file_path, _ = QFileDialog.getOpenFileName(
494
+ self, "Select a FITS file", "", "FITS files (*.fits);;All files (*)"
495
+ )
496
+ if file_path:
497
+ self.file_path_edit.setText(file_path)
498
+
499
+ def get_settings(self):
500
+ settings = {}
501
+ settings["source"] = (
502
+ "external" if self.external_image_radio.isChecked() else "same"
503
+ )
504
+ settings["external_image"] = self.file_path_edit.text()
505
+ settings["stokes"] = self.stokes_combo.currentText()
506
+ settings["level_type"] = self.level_type_combo.currentText()
507
+ try:
508
+ pos_levels_text = self.pos_levels_edit.text()
509
+ settings["pos_levels"] = [
510
+ float(level.strip())
511
+ for level in pos_levels_text.split(",")
512
+ if level.strip()
513
+ ]
514
+ except ValueError:
515
+ settings["pos_levels"] = [0.1, 0.3, 0.5, 0.7, 0.9]
516
+ try:
517
+ neg_levels_text = self.neg_levels_edit.text()
518
+ settings["neg_levels"] = [
519
+ float(level.strip())
520
+ for level in neg_levels_text.split(",")
521
+ if level.strip()
522
+ ]
523
+ except ValueError:
524
+ settings["neg_levels"] = [0.1, 0.3, 0.5, 0.7, 0.9]
525
+ settings["levels"] = settings["pos_levels"]
526
+ settings["use_default_rms_region"] = self.use_default_rms_box.isChecked()
527
+ settings["rms_box"] = (
528
+ self.rms_xmin.value(),
529
+ self.rms_xmax.value(),
530
+ self.rms_ymin.value(),
531
+ self.rms_ymax.value(),
532
+ )
533
+ settings["color"] = self.color_combo.currentText()
534
+ settings["linewidth"] = self.linewidth_spin.value()
535
+ settings["pos_linestyle"] = self.pos_linestyle_combo.currentText()
536
+ settings["neg_linestyle"] = self.neg_linestyle_combo.currentText()
537
+ settings["linestyle"] = settings["pos_linestyle"]
538
+ if "contour_data" in self.settings:
539
+ settings["contour_data"] = self.settings["contour_data"]
540
+ else:
541
+ settings["contour_data"] = None
542
+ return settings
543
+
544
+
545
+ class BatchProcessDialog(QDialog):
546
+ def __init__(self, parent=None):
547
+ super().__init__(parent)
548
+ self.setWindowTitle("Batch Processing")
549
+ self.setMinimumWidth(500)
550
+ self.setStyleSheet("background-color: #484848; color: #ffffff;")
551
+ self.image_list = QListWidget()
552
+ self.add_button = QPushButton("Add Image")
553
+ self.remove_button = QPushButton("Remove Selected")
554
+ self.threshold_spin = QSpinBox()
555
+ self.threshold_spin.setRange(1, 9999)
556
+ self.threshold_spin.setValue(10)
557
+ lbl_thresh = QLabel("Threshold:")
558
+ self.run_button = QPushButton("Run Process")
559
+ button_box = QDialogButtonBox(QDialogButtonBox.Close)
560
+ button_box.rejected.connect(self.reject)
561
+ layout = QVBoxLayout(self)
562
+ layout.addWidget(self.image_list)
563
+ ctrl_layout = QHBoxLayout()
564
+ ctrl_layout.addWidget(self.add_button)
565
+ ctrl_layout.addWidget(self.remove_button)
566
+ layout.addLayout(ctrl_layout)
567
+ thr_layout = QHBoxLayout()
568
+ thr_layout.addWidget(lbl_thresh)
569
+ thr_layout.addWidget(self.threshold_spin)
570
+ layout.addLayout(thr_layout)
571
+ layout.addWidget(self.run_button)
572
+ layout.addWidget(button_box)
573
+ self.add_button.clicked.connect(self.add_image)
574
+ self.remove_button.clicked.connect(self.remove_image)
575
+ self.run_button.clicked.connect(self.run_process)
576
+
577
+ def add_image(self):
578
+ directory = QFileDialog.getExistingDirectory(
579
+ self, "Select a CASA Image Directory"
580
+ )
581
+ if directory:
582
+ self.image_list.addItem(directory)
583
+
584
+ def remove_image(self):
585
+ for item in self.image_list.selectedItems():
586
+ self.image_list.takeItem(self.image_list.row(item))
587
+
588
+ def run_process(self):
589
+ threshold = self.threshold_spin.value()
590
+ results = []
591
+ for i in range(self.image_list.count()):
592
+ imagename = self.image_list.item(i).text()
593
+ try:
594
+ from .utils import get_pixel_values_from_image
595
+
596
+ pix, _, _ = get_pixel_values_from_image(imagename, "I", threshold)
597
+ flux = float(np.sum(pix))
598
+ results.append(f"{imagename}: threshold={threshold}, flux={flux:.2f}")
599
+ except Exception as e:
600
+ results.append(f"{imagename}: ERROR - {str(e)}")
601
+ QMessageBox.information(self, "Batch Results", "\n".join(results))
602
+
603
+
604
+ class ImageInfoDialog(QDialog):
605
+ def __init__(self, parent=None, info_text=""):
606
+ super().__init__(parent)
607
+ self.setWindowTitle("Image Metadata / Info")
608
+ self.setMinimumSize(500, 400)
609
+ layout = QVBoxLayout(self)
610
+ self.text_area = QPlainTextEdit()
611
+ self.text_area.setReadOnly(True)
612
+ self.text_area.setPlainText(info_text)
613
+ button_box = QDialogButtonBox(QDialogButtonBox.Ok)
614
+ button_box.accepted.connect(self.accept)
615
+ layout.addWidget(self.text_area)
616
+ layout.addWidget(button_box)
617
+ self.setLayout(layout)
618
+
619
+
620
+ class PhaseShiftDialog(QDialog):
621
+ """Dialog for configuring and executing solar phase center shifting."""
622
+
623
+ def __init__(self, parent=None, imagename=None):
624
+ super().__init__(parent)
625
+ self.setWindowTitle("Solar Phase Center Shifting")
626
+ self.setMinimumSize(1000, 800)
627
+ self.imagename = imagename
628
+
629
+ # Set the dialog size to match the parent window if available
630
+ """if parent and parent.size().isValid():
631
+ self.resize(parent.size())
632
+ # Center the dialog relative to the parent
633
+ self.move(
634
+ parent.frameGeometry().topLeft()
635
+ + parent.rect().center()
636
+ - self.rect().center()
637
+ )"""
638
+
639
+ self.setup_ui()
640
+
641
+ def setup_ui(self):
642
+ from .move_phasecenter import SolarPhaseCenter
643
+
644
+ main_layout = QVBoxLayout(self)
645
+ main_layout.setSpacing(15)
646
+ main_layout.setContentsMargins(15, 15, 15, 15)
647
+
648
+ # Add a description at the top
649
+ description = QLabel(
650
+ "This tool shifts the coordinate system so that the solar center aligns with the image phase center. "
651
+ "This is useful for properly aligning solar observations in heliographic coordinates."
652
+ )
653
+ description.setWordWrap(True)
654
+ description.setStyleSheet("color: #BBB; font-style: italic;")
655
+ main_layout.addWidget(description)
656
+
657
+ # Mode selection: Single file or batch processing
658
+ mode_container = QWidget()
659
+ mode_container_layout = QHBoxLayout(mode_container)
660
+ mode_container_layout.setContentsMargins(0, 0, 0, 0)
661
+
662
+ mode_group = QGroupBox("Processing Mode")
663
+ mode_layout = QHBoxLayout(mode_group)
664
+ mode_layout.setContentsMargins(10, 15, 10, 10)
665
+
666
+ self.single_mode_radio = QRadioButton("Single File")
667
+ self.batch_mode_radio = QRadioButton("Batch Processing")
668
+ self.single_mode_radio.setChecked(True)
669
+
670
+ mode_layout.addWidget(self.single_mode_radio)
671
+ mode_layout.addWidget(self.batch_mode_radio)
672
+ mode_layout.addStretch(1)
673
+
674
+ # Stokes parameter selection - moved next to mode selection
675
+ stokes_group = QGroupBox("Stokes Parameter")
676
+ stokes_group_layout = QVBoxLayout(stokes_group)
677
+ stokes_group_layout.setContentsMargins(10, 15, 10, 10)
678
+
679
+ # Add radio buttons for Stokes mode selection
680
+ stokes_mode_layout = QHBoxLayout()
681
+ self.single_stokes_radio = QRadioButton("Single Stokes")
682
+ self.full_stokes_radio = QRadioButton("Full Stokes")
683
+ self.single_stokes_radio.setChecked(True)
684
+ stokes_mode_layout.addWidget(self.single_stokes_radio)
685
+ stokes_mode_layout.addWidget(self.full_stokes_radio)
686
+ stokes_mode_layout.addStretch(1)
687
+ stokes_group_layout.addLayout(stokes_mode_layout)
688
+
689
+ # Add stokes combo box for selection
690
+ stokes_select_layout = QHBoxLayout()
691
+ self.stokes_combo = QComboBox()
692
+ self.stokes_combo.addItems(["I", "Q", "U", "V"])
693
+ stokes_select_layout.addWidget(self.stokes_combo)
694
+ stokes_select_layout.addStretch(1)
695
+ stokes_group_layout.addLayout(stokes_select_layout)
696
+
697
+ # Connect stokes mode radios to update UI
698
+ self.single_stokes_radio.toggled.connect(self.update_stokes_mode)
699
+ self.full_stokes_radio.toggled.connect(self.update_stokes_mode)
700
+
701
+ # Add the two groups to the container
702
+ mode_container_layout.addWidget(mode_group, 1)
703
+ mode_container_layout.addWidget(stokes_group, 1)
704
+ main_layout.addWidget(mode_container)
705
+
706
+ # Connect mode radios to update UI
707
+ self.single_mode_radio.toggled.connect(self.update_mode_ui)
708
+ self.batch_mode_radio.toggled.connect(self.update_mode_ui)
709
+
710
+ # Input and Output options in two columns
711
+ io_container = QWidget()
712
+ io_layout = QHBoxLayout(io_container)
713
+ io_layout.setContentsMargins(0, 0, 0, 0)
714
+ io_layout.setSpacing(15)
715
+
716
+ # Input options group (left column)
717
+ self.input_group = QGroupBox("Input Settings")
718
+ input_layout = QVBoxLayout(self.input_group)
719
+ input_layout.setSpacing(10)
720
+ input_layout.setContentsMargins(10, 15, 10, 10)
721
+
722
+ # Single file mode controls
723
+ self.single_file_widget = QWidget()
724
+ single_file_layout = QFormLayout(self.single_file_widget)
725
+ single_file_layout.setContentsMargins(0, 0, 0, 0)
726
+ single_file_layout.setVerticalSpacing(8)
727
+
728
+ # Image selection
729
+ image_layout = QHBoxLayout()
730
+ self.image_path_edit = QLineEdit(self.imagename or "")
731
+ self.image_path_edit.setReadOnly(True)
732
+ self.browse_button = QPushButton("Browse...")
733
+ self.browse_button.clicked.connect(self.browse_image)
734
+ image_layout.addWidget(self.image_path_edit, 1)
735
+ image_layout.addWidget(self.browse_button)
736
+ single_file_layout.addRow("Image:", image_layout)
737
+
738
+ # Batch mode controls
739
+ self.batch_file_widget = QWidget()
740
+ batch_file_layout = QFormLayout(self.batch_file_widget)
741
+ batch_file_layout.setContentsMargins(0, 0, 0, 0)
742
+ batch_file_layout.setVerticalSpacing(8)
743
+
744
+ # Reference image selection for batch mode
745
+ reference_image_layout = QHBoxLayout()
746
+ self.reference_image_edit = QLineEdit("")
747
+ self.reference_image_edit.setReadOnly(True)
748
+ self.reference_image_edit.setPlaceholderText(
749
+ "Select reference image for phase center calculation"
750
+ )
751
+ self.reference_browse_button = QPushButton("Browse...")
752
+ self.reference_browse_button.clicked.connect(self.browse_reference_image)
753
+ reference_image_layout.addWidget(self.reference_image_edit, 1)
754
+ reference_image_layout.addWidget(self.reference_browse_button)
755
+ batch_file_layout.addRow("Reference Image:", reference_image_layout)
756
+
757
+ # Input pattern selection
758
+ input_pattern_layout = QHBoxLayout()
759
+ self.input_pattern_edit = QLineEdit("")
760
+ self.input_pattern_edit.setPlaceholderText("e.g., /path/to/images/*.fits")
761
+ self.input_pattern_button = QPushButton("Browse...")
762
+ self.input_pattern_button.clicked.connect(self.browse_input_pattern)
763
+ input_pattern_layout.addWidget(self.input_pattern_edit, 1)
764
+ input_pattern_layout.addWidget(self.input_pattern_button)
765
+ batch_file_layout.addRow("Apply To Pattern:", input_pattern_layout)
766
+
767
+ # MS File selection (optional) - common for both modes
768
+ ms_layout = QHBoxLayout()
769
+ self.ms_path_edit = QLineEdit("")
770
+ self.ms_path_edit.setPlaceholderText(
771
+ "Optional MS file for phase center calculation"
772
+ )
773
+ self.ms_browse_button = QPushButton("Browse...")
774
+ self.ms_browse_button.clicked.connect(self.browse_ms)
775
+ ms_layout.addWidget(self.ms_path_edit, 1)
776
+ ms_layout.addWidget(self.ms_browse_button)
777
+
778
+ # Add widgets to input layout
779
+ input_layout.addWidget(self.single_file_widget)
780
+ input_layout.addWidget(self.batch_file_widget)
781
+ self.batch_file_widget.setVisible(False)
782
+
783
+ # Add MS file row directly to the input layout
784
+ ms_form_container = QWidget()
785
+ ms_form_layout = QFormLayout(ms_form_container)
786
+ ms_form_layout.setContentsMargins(0, 0, 0, 0)
787
+ ms_form_layout.setVerticalSpacing(8)
788
+ ms_form_layout.addRow("MS File (optional):", ms_layout)
789
+ input_layout.addWidget(ms_form_container)
790
+
791
+ # Output options group (right column)
792
+ output_group = QGroupBox("Output Settings")
793
+ output_layout = QVBoxLayout(output_group)
794
+ output_layout.setSpacing(10)
795
+ output_layout.setContentsMargins(10, 15, 10, 10)
796
+
797
+ # Single file output
798
+ self.single_output_widget = QWidget()
799
+ single_output_layout = QFormLayout(self.single_output_widget)
800
+ single_output_layout.setContentsMargins(0, 0, 0, 0)
801
+ single_output_layout.setVerticalSpacing(8)
802
+
803
+ # Output file selection for single file
804
+ output_file_layout = QHBoxLayout()
805
+ self.output_path_edit = QLineEdit("")
806
+ self.output_path_edit.setPlaceholderText("Leave empty to modify input image")
807
+ self.output_browse_button = QPushButton("Browse...")
808
+ self.output_browse_button.clicked.connect(self.browse_output)
809
+ output_file_layout.addWidget(self.output_path_edit, 1)
810
+ output_file_layout.addWidget(self.output_browse_button)
811
+ single_output_layout.addRow("Output File:", output_file_layout)
812
+ output_layout.addWidget(self.single_output_widget)
813
+
814
+ # Batch file output
815
+ self.batch_output_widget = QWidget()
816
+ batch_output_layout = QVBoxLayout(self.batch_output_widget)
817
+ batch_output_layout.setContentsMargins(0, 0, 0, 0)
818
+ batch_output_layout.setSpacing(8)
819
+
820
+ # Output pattern for batch mode
821
+ output_pattern_form = QFormLayout()
822
+ output_pattern_form.setVerticalSpacing(8)
823
+ output_pattern_layout = QHBoxLayout()
824
+ self.output_pattern_edit = QLineEdit("shifted_*.fits")
825
+ self.output_pattern_edit.setPlaceholderText("e.g., shifted_*.fits")
826
+ self.output_pattern_button = QPushButton("Browse Directory...")
827
+ self.output_pattern_button.clicked.connect(self.browse_output_dir)
828
+ output_pattern_layout.addWidget(self.output_pattern_edit, 1)
829
+ output_pattern_layout.addWidget(self.output_pattern_button)
830
+ output_pattern_form.addRow("Output Pattern:", output_pattern_layout)
831
+ batch_output_layout.addLayout(output_pattern_form)
832
+
833
+ # Add a help text for pattern
834
+ pattern_help = QLabel(
835
+ "Use * in the pattern as a placeholder for the original filename."
836
+ )
837
+ pattern_help.setStyleSheet("color: #BBB; font-style: italic;")
838
+ batch_output_layout.addWidget(pattern_help)
839
+
840
+ output_layout.addWidget(self.batch_output_widget)
841
+ self.batch_output_widget.setVisible(False)
842
+
843
+ # Add the input and output groups to the container
844
+ io_layout.addWidget(self.input_group, 1)
845
+ io_layout.addWidget(output_group, 1)
846
+ main_layout.addWidget(io_container)
847
+
848
+ # Method settings and Visual centering in one row
849
+ method_container = QWidget()
850
+ method_container_layout = QHBoxLayout(method_container)
851
+ method_container_layout.setContentsMargins(0, 0, 0, 0)
852
+ method_container_layout.setSpacing(15)
853
+
854
+ # Method options group
855
+ method_group = QGroupBox("Method Settings")
856
+ method_layout = QVBoxLayout(method_group)
857
+ method_layout.setSpacing(10)
858
+ method_layout.setContentsMargins(10, 15, 10, 10)
859
+
860
+ # Gaussian fitting option
861
+ self.fit_gaussian_check = QCheckBox("Use Gaussian fitting for solar center")
862
+ self.fit_gaussian_check.setChecked(False)
863
+ method_layout.addWidget(self.fit_gaussian_check)
864
+
865
+ # Sigma threshold for center-of-mass
866
+ sigma_layout = QHBoxLayout()
867
+ sigma_layout.addWidget(QLabel("Sigma threshold for center-of-mass:"))
868
+ self.sigma_spinbox = QDoubleSpinBox()
869
+ self.sigma_spinbox.setRange(1.0, 20.0)
870
+ self.sigma_spinbox.setValue(10.0)
871
+ self.sigma_spinbox.setSingleStep(0.5)
872
+ sigma_layout.addWidget(self.sigma_spinbox)
873
+ sigma_layout.addStretch()
874
+ method_layout.addLayout(sigma_layout)
875
+
876
+ # Visual centering option
877
+ self.visual_center_check = QCheckBox(
878
+ "Create a visually centered image (moves pixel data)"
879
+ )
880
+ self.visual_center_check.setChecked(False)
881
+ method_layout.addWidget(self.visual_center_check)
882
+
883
+ # Multiprocessing option for batch mode
884
+ self.multiprocessing_check = QCheckBox(
885
+ "Use multiprocessing for batch operations (faster)"
886
+ )
887
+ self.multiprocessing_check.setChecked(True)
888
+ self.multiprocessing_check.setToolTip(
889
+ "Enable parallel processing for batch operations"
890
+ )
891
+ method_layout.addWidget(self.multiprocessing_check)
892
+
893
+ # CPU cores selection
894
+ cores_layout = QHBoxLayout()
895
+ cores_layout.addWidget(QLabel("Number of CPU cores to use:"))
896
+ self.cores_spinbox = QSpinBox()
897
+ self.cores_spinbox.setRange(1, multiprocessing.cpu_count())
898
+ self.cores_spinbox.setValue(
899
+ max(1, multiprocessing.cpu_count() - 1)
900
+ ) # Default to N-1 cores
901
+ self.cores_spinbox.setSingleStep(1)
902
+ self.cores_spinbox.setToolTip(
903
+ f"Maximum: {multiprocessing.cpu_count()} cores available"
904
+ )
905
+ cores_layout.addWidget(self.cores_spinbox)
906
+ cores_layout.addStretch()
907
+ method_layout.addLayout(cores_layout)
908
+
909
+ # Connect multiprocessing checkbox to enable/disable cores spinbox
910
+ self.multiprocessing_check.toggled.connect(self.cores_spinbox.setEnabled)
911
+
912
+ # Add the method group to the container (full width)
913
+ method_container_layout.addWidget(method_group)
914
+ main_layout.addWidget(method_container)
915
+
916
+ # Add a status text area
917
+ status_group = QGroupBox("Status / Results")
918
+ status_layout = QVBoxLayout(status_group)
919
+ status_layout.setContentsMargins(10, 15, 10, 10)
920
+
921
+ self.status_text = QPlainTextEdit()
922
+ self.status_text.setReadOnly(True)
923
+ self.status_text.setPlaceholderText("Status and results will appear here")
924
+ self.status_text.setMinimumHeight(100)
925
+ status_layout.addWidget(self.status_text)
926
+
927
+ main_layout.addWidget(status_group)
928
+
929
+ # Add dialog buttons
930
+ button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
931
+ button_box.accepted.connect(self.apply_phase_shift)
932
+ button_box.rejected.connect(self.reject)
933
+ main_layout.addWidget(button_box)
934
+
935
+ # Set the Ok button text based on mode
936
+ self.ok_button = button_box.button(QDialogButtonBox.Ok)
937
+ self.ok_button.setText("Apply Shift")
938
+
939
+ # Apply consistent styling to the dialog
940
+ self.setStyleSheet(
941
+ """
942
+ QGroupBox {
943
+ border: 1px solid #555555;
944
+ border-radius: 3px;
945
+ margin-top: 0.5em;
946
+ padding-top: 0.5em;
947
+ }
948
+ QGroupBox::title {
949
+ subcontrol-origin: margin;
950
+ left: 10px;
951
+ padding: 0 3px 0 3px;
952
+ }
953
+ QLabel {
954
+ margin-top: 2px;
955
+ margin-bottom: 2px;
956
+ }
957
+ QRadioButton, QCheckBox {
958
+ min-height: 20px;
959
+ }
960
+ """
961
+ )
962
+
963
+ def update_mode_ui(self):
964
+ """Update UI components based on the selected mode"""
965
+ single_mode = self.single_mode_radio.isChecked()
966
+
967
+ # Update visibility of widgets
968
+ self.single_file_widget.setVisible(single_mode)
969
+ self.batch_file_widget.setVisible(not single_mode)
970
+ self.single_output_widget.setVisible(single_mode)
971
+ self.batch_output_widget.setVisible(not single_mode)
972
+
973
+ # Update button text
974
+ if single_mode:
975
+ self.ok_button.setText("Apply Shift")
976
+ else:
977
+ self.ok_button.setText("Apply Batch Shift")
978
+
979
+ def update_stokes_mode(self):
980
+ """Update UI based on selected Stokes mode"""
981
+ single_stokes = self.single_stokes_radio.isChecked()
982
+ self.stokes_combo.setEnabled(single_stokes)
983
+
984
+ def browse_image(self):
985
+ """Browse for input image file"""
986
+ file_path, _ = QFileDialog.getOpenFileName(
987
+ self, "Select Image File", "", "FITS Files (*.fits);;CASA Images (*)"
988
+ )
989
+ if file_path:
990
+ self.image_path_edit.setText(file_path)
991
+ self.imagename = file_path
992
+
993
+ # Set default output filename pattern
994
+ if not self.output_path_edit.text():
995
+ file_dir = os.path.dirname(file_path)
996
+ file_name = os.path.basename(file_path)
997
+ output_path = os.path.join(file_dir, f"shifted_{file_name}")
998
+ self.output_path_edit.setText(output_path)
999
+
1000
+ def browse_input_pattern(self):
1001
+ """Browse for directory and help set input pattern"""
1002
+ dir_path = QFileDialog.getExistingDirectory(
1003
+ self, "Select Directory for Input Files"
1004
+ )
1005
+ if dir_path:
1006
+ # Set a default pattern in the selected directory
1007
+ self.input_pattern_edit.setText(os.path.join(dir_path, "*.fits"))
1008
+
1009
+ def browse_output_dir(self):
1010
+ """Browse for output directory for batch processing"""
1011
+ dir_path = QFileDialog.getExistingDirectory(
1012
+ self, "Select Directory for Output Files"
1013
+ )
1014
+ if dir_path:
1015
+ # Preserve the filename pattern but update the directory
1016
+ pattern = os.path.basename(self.output_pattern_edit.text())
1017
+ if not pattern:
1018
+ pattern = "shifted_*.fits"
1019
+ self.output_pattern_edit.setText(os.path.join(dir_path, pattern))
1020
+
1021
+ def browse_ms(self):
1022
+ """Browse for MS file"""
1023
+ dir_path = QFileDialog.getExistingDirectory(
1024
+ self, "Select Measurement Set Directory"
1025
+ )
1026
+ if dir_path:
1027
+ self.ms_path_edit.setText(dir_path)
1028
+
1029
+ def browse_output(self):
1030
+ """Browse for output file location"""
1031
+ file_path, _ = QFileDialog.getSaveFileName(
1032
+ self, "Save Output As", "", "FITS Files (*.fits);;CASA Images (*)"
1033
+ )
1034
+ if file_path:
1035
+ self.output_path_edit.setText(file_path)
1036
+
1037
+ def browse_reference_image(self):
1038
+ """Browse for reference image file for batch processing"""
1039
+ file_path, _ = QFileDialog.getOpenFileName(
1040
+ self, "Select Reference Image", "", "FITS Files (*.fits);;CASA Images (*)"
1041
+ )
1042
+ if file_path:
1043
+ self.reference_image_edit.setText(file_path)
1044
+
1045
+ # Set default input pattern in the same directory
1046
+ if not self.input_pattern_edit.text():
1047
+ file_dir = os.path.dirname(file_path)
1048
+ self.input_pattern_edit.setText(os.path.join(file_dir, "*.fits"))
1049
+
1050
+ def apply_phase_shift(self):
1051
+ """Apply the phase shift to the image(s)"""
1052
+ import os
1053
+ from .move_phasecenter import SolarPhaseCenter
1054
+
1055
+ # Check if we're in batch mode or single file mode
1056
+ batch_mode = self.batch_mode_radio.isChecked()
1057
+
1058
+ # Check if we're processing full Stokes
1059
+ full_stokes = self.full_stokes_radio.isChecked()
1060
+
1061
+ # Validate inputs
1062
+ if batch_mode:
1063
+ if not self.reference_image_edit.text():
1064
+ QMessageBox.warning(
1065
+ self,
1066
+ "Input Error",
1067
+ "Please select a reference image for phase center calculation",
1068
+ )
1069
+ return
1070
+ if not self.input_pattern_edit.text():
1071
+ QMessageBox.warning(
1072
+ self, "Input Error", "Please specify a pattern for files to process"
1073
+ )
1074
+ return
1075
+ else:
1076
+ if not self.image_path_edit.text():
1077
+ QMessageBox.warning(self, "Input Error", "Please select an input image")
1078
+ return
1079
+
1080
+ try:
1081
+ # Get common parameters
1082
+ msname = self.ms_path_edit.text() or None
1083
+
1084
+ # Create SolarPhaseCenter instance - removing cellsize and imsize parameters
1085
+ spc = SolarPhaseCenter(msname=msname)
1086
+
1087
+ # Determine Stokes parameter to use
1088
+ if full_stokes:
1089
+ stokes_list = ["I", "Q", "U", "V"]
1090
+ self.status_text.appendPlainText(
1091
+ "Processing all Stokes parameters: I, Q, U, V"
1092
+ )
1093
+ else:
1094
+ stokes_list = [self.stokes_combo.currentText()]
1095
+ self.status_text.appendPlainText(
1096
+ f"Processing Stokes {self.stokes_combo.currentText()}"
1097
+ )
1098
+
1099
+ if batch_mode:
1100
+ # Batch processing mode
1101
+ reference_image = self.reference_image_edit.text()
1102
+ input_pattern = self.input_pattern_edit.text()
1103
+ output_pattern = (
1104
+ self.output_pattern_edit.text()
1105
+ if self.output_pattern_edit.text()
1106
+ else None
1107
+ )
1108
+
1109
+ self.status_text.appendPlainText(
1110
+ f"Using reference image: {reference_image}"
1111
+ )
1112
+ self.status_text.appendPlainText(
1113
+ f"Processing files matching pattern: {input_pattern}"
1114
+ )
1115
+ if output_pattern:
1116
+ self.status_text.appendPlainText(
1117
+ f"Output pattern: {output_pattern}"
1118
+ )
1119
+ else:
1120
+ self.status_text.appendPlainText(
1121
+ f"Will modify input files in-place"
1122
+ )
1123
+
1124
+ # First calculate phase shift from the reference image
1125
+ self.status_text.appendPlainText(
1126
+ f"Calculating solar center position using reference image: {reference_image}"
1127
+ )
1128
+
1129
+ # Check if any files match the pattern
1130
+ matching_files = glob.glob(input_pattern)
1131
+ if not matching_files:
1132
+ QMessageBox.warning(
1133
+ self,
1134
+ "Input Error",
1135
+ f"No files found matching pattern: {input_pattern}",
1136
+ )
1137
+ return
1138
+
1139
+ self.status_text.appendPlainText(
1140
+ f"Found {len(matching_files)} files matching the pattern"
1141
+ )
1142
+
1143
+ # Calculate phase shift based on the reference image
1144
+ ra, dec, needs_shift = spc.cal_solar_phaseshift(
1145
+ imagename=reference_image,
1146
+ fit_gaussian=self.fit_gaussian_check.isChecked(),
1147
+ sigma=self.sigma_spinbox.value(),
1148
+ )
1149
+
1150
+ self.status_text.appendPlainText(
1151
+ f"Calculated solar center: RA = {ra} deg, DEC = {dec} deg"
1152
+ )
1153
+
1154
+ if not needs_shift:
1155
+ self.status_text.appendPlainText(
1156
+ "No phase shift needed. Solar center is already aligned with phase center."
1157
+ )
1158
+ result = QMessageBox.question(
1159
+ self,
1160
+ "No Shift Needed",
1161
+ "No phase shift is needed as the solar center is already aligned. Proceed anyway?",
1162
+ QMessageBox.Yes | QMessageBox.No,
1163
+ QMessageBox.No,
1164
+ )
1165
+ if result == QMessageBox.No:
1166
+ return
1167
+
1168
+ # Apply to all files
1169
+ visual_center = self.visual_center_check.isChecked()
1170
+ use_multiprocessing = self.multiprocessing_check.isChecked()
1171
+ max_processes = (
1172
+ self.cores_spinbox.value() if use_multiprocessing else None
1173
+ )
1174
+
1175
+ for stokes in stokes_list:
1176
+ self.status_text.appendPlainText(f"\nProcessing Stokes {stokes}...")
1177
+
1178
+ if use_multiprocessing:
1179
+ self.status_text.appendPlainText(
1180
+ f"Using multiprocessing with {max_processes} CPU cores"
1181
+ )
1182
+
1183
+ results = spc.apply_shift_to_multiple_fits(
1184
+ ra=ra,
1185
+ dec=dec,
1186
+ input_pattern=input_pattern,
1187
+ output_pattern=output_pattern,
1188
+ stokes=stokes,
1189
+ visual_center=visual_center,
1190
+ use_multiprocessing=use_multiprocessing,
1191
+ max_processes=max_processes,
1192
+ )
1193
+
1194
+ if visual_center:
1195
+ self.status_text.appendPlainText(
1196
+ "Visually centered images were also created with '_centered' suffix."
1197
+ )
1198
+
1199
+ self.status_text.appendPlainText(
1200
+ f"Successfully processed {results[0]} out of {results[1]} files for Stokes {stokes}"
1201
+ )
1202
+
1203
+ QMessageBox.information(
1204
+ self,
1205
+ "Success",
1206
+ f"Batch processing completed: {results[0]} out of {results[1]} files processed successfully.",
1207
+ )
1208
+ self.accept()
1209
+
1210
+ else:
1211
+ # Single file mode
1212
+ imagename = self.image_path_edit.text()
1213
+
1214
+ # Calculate phase shift
1215
+ self.status_text.appendPlainText("Calculating solar center position...")
1216
+ ra, dec, needs_shift = spc.cal_solar_phaseshift(
1217
+ imagename=imagename,
1218
+ fit_gaussian=self.fit_gaussian_check.isChecked(),
1219
+ sigma=self.sigma_spinbox.value(),
1220
+ )
1221
+
1222
+ self.status_text.appendPlainText(
1223
+ f"Calculated solar center: RA = {ra} deg, DEC = {dec} deg"
1224
+ )
1225
+
1226
+ if not needs_shift:
1227
+ self.status_text.appendPlainText(
1228
+ "No phase shift needed. Solar center is already aligned with phase center."
1229
+ )
1230
+ result = QMessageBox.question(
1231
+ self,
1232
+ "No Shift Needed",
1233
+ "No phase shift is needed as the solar center is already aligned. Proceed anyway?",
1234
+ QMessageBox.Yes | QMessageBox.No,
1235
+ QMessageBox.No,
1236
+ )
1237
+ if result == QMessageBox.No:
1238
+ return
1239
+
1240
+ # Process all requested Stokes parameters
1241
+ for stokes_param in stokes_list:
1242
+ output_file = self.output_path_edit.text() or imagename
1243
+
1244
+ # For multi-Stokes mode, append stokes parameter to filename if output is specified
1245
+ if (
1246
+ full_stokes
1247
+ and self.output_path_edit.text()
1248
+ and len(stokes_list) > 1
1249
+ ):
1250
+ base, ext = os.path.splitext(output_file)
1251
+ stokes_output_file = f"{base}_{stokes_param}{ext}"
1252
+ else:
1253
+ stokes_output_file = output_file
1254
+
1255
+ self.status_text.appendPlainText(
1256
+ f"\nProcessing Stokes {stokes_param}..."
1257
+ )
1258
+ self.status_text.appendPlainText(
1259
+ f"Output file: {stokes_output_file}"
1260
+ )
1261
+
1262
+ # If output is different from input, make a copy
1263
+ if stokes_output_file != imagename:
1264
+ import shutil
1265
+
1266
+ if os.path.isdir(imagename):
1267
+ os.system(f"rm -rf {stokes_output_file}")
1268
+ os.system(f"cp -r {imagename} {stokes_output_file}")
1269
+ else:
1270
+ shutil.copy(imagename, stokes_output_file)
1271
+ target = stokes_output_file
1272
+ else:
1273
+ target = imagename
1274
+
1275
+ self.status_text.appendPlainText(
1276
+ f"Applying phase shift to {target}..."
1277
+ )
1278
+
1279
+ result = spc.shift_phasecenter(
1280
+ imagename=target, ra=ra, dec=dec, stokes=stokes_param
1281
+ )
1282
+
1283
+ if result == 0:
1284
+ self.status_text.appendPlainText(
1285
+ "Phase shift successfully applied."
1286
+ )
1287
+
1288
+ # Create visually centered image if requested
1289
+ if self.visual_center_check.isChecked():
1290
+ # Generate output filename for visually centered image
1291
+ if stokes_output_file == imagename:
1292
+ # If modifying in place, create a separate centered file
1293
+ base_path = os.path.splitext(target)[0]
1294
+ ext = os.path.splitext(target)[1]
1295
+ visual_output = f"{base_path}_centered{ext}"
1296
+ else:
1297
+ # If already creating a new file, derive from that filename
1298
+ base_path = os.path.splitext(stokes_output_file)[0]
1299
+ ext = os.path.splitext(stokes_output_file)[1]
1300
+ visual_output = f"{base_path}_centered{ext}"
1301
+
1302
+ try:
1303
+ # Get the reference pixel values from the shifted image
1304
+ from astropy.io import fits
1305
+
1306
+ header = fits.getheader(target)
1307
+ crpix1 = int(header["CRPIX1"])
1308
+ crpix2 = int(header["CRPIX2"])
1309
+
1310
+ self.status_text.appendPlainText(
1311
+ f"Creating visually centered image: {visual_output}"
1312
+ )
1313
+
1314
+ # Create the visually centered image
1315
+ success = spc.visually_center_image(
1316
+ target, visual_output, crpix1, crpix2
1317
+ )
1318
+
1319
+ if success:
1320
+ self.status_text.appendPlainText(
1321
+ "Visually centered image created successfully."
1322
+ )
1323
+ else:
1324
+ self.status_text.appendPlainText(
1325
+ "Failed to create visually centered image."
1326
+ )
1327
+ except Exception as vis_error:
1328
+ self.status_text.appendPlainText(
1329
+ f"Error creating visually centered image: {str(vis_error)}"
1330
+ )
1331
+ elif result == 1:
1332
+ self.status_text.appendPlainText("Phase shift not needed.")
1333
+ else:
1334
+ self.status_text.appendPlainText(
1335
+ f"Error applying phase shift for Stokes {stokes_param}."
1336
+ )
1337
+
1338
+ QMessageBox.information(
1339
+ self,
1340
+ "Success",
1341
+ f"Solar phase center shift completed successfully for {len(stokes_list)} Stokes parameters.",
1342
+ )
1343
+ self.accept()
1344
+
1345
+ except Exception as e:
1346
+ import traceback
1347
+
1348
+ self.status_text.appendPlainText(f"Error: {str(e)}")
1349
+ self.status_text.appendPlainText(traceback.format_exc())
1350
+ QMessageBox.critical(self, "Error", f"An error occurred: {str(e)}")
1351
+
1352
+ def showEvent(self, event):
1353
+ """Handle the show event to ensure correct sizing"""
1354
+ super().showEvent(event)
1355
+
1356
+ # Ensure the dialog size matches the parent when shown
1357
+ if self.parent() and self.parent().size().isValid():
1358
+ # Set size to match parent
1359
+ # self.resize(self.parent().size())
1360
+
1361
+ # Center relative to parent
1362
+ self.move(
1363
+ self.parent().frameGeometry().topLeft()
1364
+ + self.parent().rect().center()
1365
+ - self.rect().center()
1366
+ )
1367
+
1368
+
1369
+ class HPCBatchConversionDialog(QDialog):
1370
+ """Dialog for batch conversion of images to helioprojective coordinates."""
1371
+
1372
+ def __init__(self, parent=None, current_file=None):
1373
+ super().__init__(parent)
1374
+ self.setWindowTitle("Batch Conversion to Helioprojective Coordinates")
1375
+ self.setMinimumSize(900, 600)
1376
+ self.parent = parent
1377
+ self.current_file = current_file
1378
+ self.setup_ui()
1379
+
1380
+ def setup_ui(self):
1381
+ """Set up the dialog UI with a two-column layout."""
1382
+ main_layout = QVBoxLayout(self)
1383
+ main_layout.setSpacing(15)
1384
+ main_layout.setContentsMargins(15, 15, 15, 15)
1385
+
1386
+ # Add a description at the top
1387
+ description = QLabel(
1388
+ "This tool converts multiple images to helioprojective coordinates in batch. "
1389
+ "Select a pattern of files to convert and specify the output pattern."
1390
+ )
1391
+ description.setWordWrap(True)
1392
+ description.setStyleSheet("color: #BBB; font-style: italic;")
1393
+ main_layout.addWidget(description)
1394
+
1395
+ # Create two-column layout
1396
+ columns_layout = QHBoxLayout()
1397
+ columns_layout.setSpacing(15)
1398
+
1399
+ # ===== LEFT COLUMN =====
1400
+ left_column = QVBoxLayout()
1401
+ left_column.setSpacing(10)
1402
+
1403
+ # Input section
1404
+ input_group = QGroupBox("Input Settings")
1405
+ input_layout = QVBoxLayout(input_group)
1406
+ input_layout.setSpacing(10)
1407
+ input_layout.setContentsMargins(10, 15, 10, 10)
1408
+
1409
+ # Directory selection
1410
+ dir_layout = QHBoxLayout()
1411
+ self.dir_label = QLabel("Input Directory:")
1412
+ self.dir_edit = QLineEdit()
1413
+ if self.current_file:
1414
+ self.dir_edit.setText(os.path.dirname(self.current_file))
1415
+ self.dir_browse_btn = QPushButton("Browse...")
1416
+ self.dir_browse_btn.clicked.connect(self.browse_directory)
1417
+ dir_layout.addWidget(self.dir_label)
1418
+ dir_layout.addWidget(self.dir_edit, 1)
1419
+ dir_layout.addWidget(self.dir_browse_btn)
1420
+ input_layout.addLayout(dir_layout)
1421
+
1422
+ # File pattern
1423
+ pattern_layout = QHBoxLayout()
1424
+ self.pattern_label = QLabel("File Pattern:")
1425
+ self.pattern_edit = QLineEdit()
1426
+ if self.current_file:
1427
+ file_ext = os.path.splitext(self.current_file)[1]
1428
+ self.pattern_edit.setText(f"*{file_ext}")
1429
+ else:
1430
+ self.pattern_edit.setText("*.fits")
1431
+ self.pattern_edit.setPlaceholderText("e.g., *.fits")
1432
+ pattern_layout.addWidget(self.pattern_label)
1433
+ pattern_layout.addWidget(self.pattern_edit, 1)
1434
+ input_layout.addLayout(pattern_layout)
1435
+
1436
+ # Preview button
1437
+ preview_btn = QPushButton("Preview Files")
1438
+ preview_btn.clicked.connect(self.preview_files)
1439
+ input_layout.addWidget(preview_btn)
1440
+
1441
+ # Files list
1442
+ self.files_label = QLabel("Files to be processed:")
1443
+ input_layout.addWidget(self.files_label)
1444
+
1445
+ self.files_list = QListWidget()
1446
+ self.files_list.setSelectionMode(QListWidget.ExtendedSelection)
1447
+ self.files_list.setMinimumHeight(150)
1448
+ input_layout.addWidget(self.files_list)
1449
+
1450
+ left_column.addWidget(input_group)
1451
+
1452
+ # Stokes and Processing Settings group (combined for better space usage)
1453
+ options_group = QGroupBox("Processing Options")
1454
+ options_layout = QVBoxLayout(options_group)
1455
+ options_layout.setSpacing(10)
1456
+ options_layout.setContentsMargins(10, 15, 10, 10)
1457
+
1458
+ # Stokes parameter selection
1459
+ stokes_form = QFormLayout()
1460
+ stokes_form.setVerticalSpacing(10)
1461
+ stokes_form.setHorizontalSpacing(15)
1462
+
1463
+ # Mode selection layout
1464
+ stokes_mode_layout = QHBoxLayout()
1465
+ self.single_stokes_radio = QRadioButton("Single Stokes")
1466
+ self.full_stokes_radio = QRadioButton("Full Stokes")
1467
+ self.single_stokes_radio.setChecked(True)
1468
+ stokes_mode_layout.addWidget(self.single_stokes_radio)
1469
+ stokes_mode_layout.addWidget(self.full_stokes_radio)
1470
+ stokes_mode_layout.addStretch(1)
1471
+ stokes_form.addRow("Mode:", stokes_mode_layout)
1472
+
1473
+ # Stokes combo
1474
+ self.stokes_combo = QComboBox()
1475
+ self.stokes_combo.addItems(["I", "Q", "U", "V"])
1476
+ stokes_form.addRow("Parameter:", self.stokes_combo)
1477
+
1478
+ # Connect stokes mode radios to update UI
1479
+ self.single_stokes_radio.toggled.connect(self.update_stokes_mode)
1480
+ self.full_stokes_radio.toggled.connect(self.update_stokes_mode)
1481
+
1482
+ options_layout.addLayout(stokes_form)
1483
+
1484
+ # Add a separator line
1485
+ line = QFrame()
1486
+ line.setFrameShape(QFrame.HLine)
1487
+ line.setFrameShadow(QFrame.Sunken)
1488
+ options_layout.addWidget(line)
1489
+
1490
+ # Multiprocessing options
1491
+ self.multiprocessing_check = QCheckBox("Use multiprocessing (faster)")
1492
+ self.multiprocessing_check.setChecked(True)
1493
+ options_layout.addWidget(self.multiprocessing_check)
1494
+
1495
+ # CPU cores selection
1496
+ cores_layout = QHBoxLayout()
1497
+ cores_layout.addWidget(QLabel("CPU cores:"))
1498
+ self.cores_spinbox = QSpinBox()
1499
+ self.cores_spinbox.setRange(1, multiprocessing.cpu_count())
1500
+ self.cores_spinbox.setValue(max(1, multiprocessing.cpu_count() - 1))
1501
+ cores_layout.addWidget(self.cores_spinbox)
1502
+ cores_layout.addStretch()
1503
+ options_layout.addLayout(cores_layout)
1504
+
1505
+ # Connect multiprocessing checkbox to enable/disable cores spinbox
1506
+ self.multiprocessing_check.toggled.connect(self.cores_spinbox.setEnabled)
1507
+
1508
+ left_column.addWidget(options_group)
1509
+
1510
+ # ===== RIGHT COLUMN =====
1511
+ right_column = QVBoxLayout()
1512
+ right_column.setSpacing(10)
1513
+
1514
+ # Output section
1515
+ output_group = QGroupBox("Output Settings")
1516
+ output_layout = QVBoxLayout(output_group)
1517
+ output_layout.setSpacing(10)
1518
+ output_layout.setContentsMargins(10, 15, 10, 10)
1519
+
1520
+ # Output directory and pattern
1521
+ output_dir_layout = QHBoxLayout()
1522
+ self.output_dir_label = QLabel("Output Directory:")
1523
+ self.output_dir_edit = QLineEdit()
1524
+ if self.current_file:
1525
+ self.output_dir_edit.setText(os.path.dirname(self.current_file))
1526
+ self.output_dir_btn = QPushButton("Browse...")
1527
+ self.output_dir_btn.clicked.connect(self.browse_output_directory)
1528
+ output_dir_layout.addWidget(self.output_dir_label)
1529
+ output_dir_layout.addWidget(self.output_dir_edit, 1)
1530
+ output_dir_layout.addWidget(self.output_dir_btn)
1531
+ output_layout.addLayout(output_dir_layout)
1532
+
1533
+ output_pattern_layout = QHBoxLayout()
1534
+ self.output_pattern_label = QLabel("Output Pattern:")
1535
+ self.output_pattern_edit = QLineEdit("hpc_*.fits")
1536
+ self.output_pattern_edit.setPlaceholderText("e.g., hpc_*.fits")
1537
+ output_pattern_layout.addWidget(self.output_pattern_label)
1538
+ output_pattern_layout.addWidget(self.output_pattern_edit, 1)
1539
+ output_layout.addLayout(output_pattern_layout)
1540
+
1541
+ # Add a help text for pattern
1542
+ pattern_help = QLabel(
1543
+ "Use * in the pattern as a placeholder for the original filename."
1544
+ )
1545
+ pattern_help.setStyleSheet("color: #BBB; font-style: italic;")
1546
+ output_layout.addWidget(pattern_help)
1547
+
1548
+ # Add example section
1549
+ example_group = QVBoxLayout()
1550
+ example_title = QLabel("Example:")
1551
+ example_title.setStyleSheet("font-weight: bold;")
1552
+ example_label = QLabel("Input: myimage.fits → Output: hpc_myimage.fits")
1553
+ example_label.setStyleSheet("color: #AAA; font-style: italic;")
1554
+ example_group.addWidget(example_title)
1555
+ example_group.addWidget(example_label)
1556
+ output_layout.addLayout(example_group)
1557
+
1558
+ right_column.addWidget(output_group)
1559
+
1560
+ # Status text area
1561
+ status_group = QGroupBox("Status / Results")
1562
+ status_layout = QVBoxLayout(status_group)
1563
+ status_layout.setContentsMargins(10, 15, 10, 10)
1564
+
1565
+ self.status_text = QPlainTextEdit()
1566
+ self.status_text.setReadOnly(True)
1567
+ self.status_text.setPlaceholderText("Status and results will appear here")
1568
+ self.status_text.setMinimumHeight(250) # Increased height for better visibility
1569
+ status_layout.addWidget(self.status_text)
1570
+
1571
+ right_column.addWidget(status_group)
1572
+
1573
+ # Add columns to the layout
1574
+ columns_layout.addLayout(left_column, 1) # 1 is the stretch factor
1575
+ columns_layout.addLayout(right_column, 1) # 1 is the stretch factor
1576
+
1577
+ main_layout.addLayout(columns_layout)
1578
+
1579
+ # Dialog buttons
1580
+ button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
1581
+ self.ok_button = button_box.button(QDialogButtonBox.Ok)
1582
+ self.ok_button.setText("Convert")
1583
+ button_box.accepted.connect(self.convert_files)
1584
+ button_box.rejected.connect(self.reject)
1585
+ main_layout.addWidget(button_box)
1586
+
1587
+ # Apply consistent styling to the dialog
1588
+ self.setStyleSheet(
1589
+ """
1590
+ QGroupBox {
1591
+ border: 1px solid #555555;
1592
+ border-radius: 3px;
1593
+ margin-top: 0.5em;
1594
+ padding-top: 0.5em;
1595
+ }
1596
+ QGroupBox::title {
1597
+ subcontrol-origin: margin;
1598
+ left: 10px;
1599
+ padding: 0 3px 0 3px;
1600
+ }
1601
+ QLabel {
1602
+ margin-top: 2px;
1603
+ margin-bottom: 2px;
1604
+ }
1605
+ QRadioButton, QCheckBox {
1606
+ min-height: 20px;
1607
+ }
1608
+ """
1609
+ )
1610
+
1611
+ def browse_directory(self):
1612
+ """Browse for input directory"""
1613
+ current_dir = self.dir_edit.text()
1614
+ if not current_dir and self.current_file:
1615
+ current_dir = os.path.dirname(self.current_file)
1616
+ if not current_dir:
1617
+ current_dir = os.path.expanduser("~")
1618
+
1619
+ directory = QFileDialog.getExistingDirectory(
1620
+ self, "Select Input Directory", current_dir
1621
+ )
1622
+
1623
+ if directory:
1624
+ self.dir_edit.setText(directory)
1625
+
1626
+ # Set output directory to match if not already set
1627
+ if not self.output_dir_edit.text():
1628
+ self.output_dir_edit.setText(directory)
1629
+
1630
+ # Preview files if pattern is already set
1631
+ self.preview_files()
1632
+
1633
+ def browse_output_directory(self):
1634
+ """Browse for output directory"""
1635
+ current_dir = self.output_dir_edit.text()
1636
+ if not current_dir:
1637
+ current_dir = self.dir_edit.text()
1638
+ if not current_dir and self.current_file:
1639
+ current_dir = os.path.dirname(self.current_file)
1640
+ if not current_dir:
1641
+ current_dir = os.path.expanduser("~")
1642
+
1643
+ directory = QFileDialog.getExistingDirectory(
1644
+ self, "Select Output Directory", current_dir
1645
+ )
1646
+
1647
+ if directory:
1648
+ self.output_dir_edit.setText(directory)
1649
+
1650
+ def update_stokes_mode(self):
1651
+ """Update UI based on selected Stokes mode"""
1652
+ single_stokes = self.single_stokes_radio.isChecked()
1653
+ self.stokes_combo.setEnabled(single_stokes)
1654
+
1655
+ def preview_files(self):
1656
+ """Show files that match the pattern in the list widget"""
1657
+ self.files_list.clear()
1658
+
1659
+ input_dir = self.dir_edit.text()
1660
+ pattern = self.pattern_edit.text()
1661
+
1662
+ if not input_dir:
1663
+ self.status_text.setPlainText("Please select an input directory.")
1664
+ return
1665
+
1666
+ try:
1667
+ # Get matching files
1668
+ input_pattern = os.path.join(input_dir, pattern)
1669
+ matching_files = glob.glob(input_pattern)
1670
+
1671
+ if not matching_files:
1672
+ self.status_text.setPlainText(
1673
+ f"No files found matching pattern: {input_pattern}"
1674
+ )
1675
+ return
1676
+
1677
+ # Add files to list, showing only basenames but storing full paths as item data
1678
+ for file_path in sorted(matching_files):
1679
+ basename = os.path.basename(file_path)
1680
+ item = QListWidgetItem(basename)
1681
+ item.setToolTip(file_path) # Show full path on hover
1682
+ item.setData(Qt.UserRole, file_path) # Store full path as data
1683
+ self.files_list.addItem(item)
1684
+
1685
+ self.status_text.setPlainText(
1686
+ f"Found {len(matching_files)} files matching the pattern."
1687
+ )
1688
+ except Exception as e:
1689
+ self.status_text.setPlainText(f"Error previewing files: {str(e)}")
1690
+ # Print the full error to console for debugging
1691
+ traceback.print_exc()
1692
+
1693
+ def convert_files(self):
1694
+ """Convert the selected files to helioprojective coordinates"""
1695
+ # Get input files
1696
+ if self.files_list.count() == 0:
1697
+ QMessageBox.warning(
1698
+ self,
1699
+ "No Files Found",
1700
+ "No files match the pattern. Please check your input settings.",
1701
+ )
1702
+ return
1703
+
1704
+ # Get selected files or use all if none selected
1705
+ selected_items = self.files_list.selectedItems()
1706
+ if selected_items:
1707
+ # Get full paths from item data
1708
+ files_to_process = [item.data(Qt.UserRole) for item in selected_items]
1709
+ self.status_text.appendPlainText(
1710
+ f"Processing {len(files_to_process)} selected files."
1711
+ )
1712
+ else:
1713
+ # Get full paths from item data for all items
1714
+ files_to_process = [
1715
+ self.files_list.item(i).data(Qt.UserRole)
1716
+ for i in range(self.files_list.count())
1717
+ ]
1718
+ self.status_text.appendPlainText(
1719
+ f"Processing all {len(files_to_process)} files."
1720
+ )
1721
+
1722
+ # Get output directory and pattern
1723
+ output_dir = self.output_dir_edit.text()
1724
+ output_pattern = self.output_pattern_edit.text()
1725
+
1726
+ if not output_dir:
1727
+ QMessageBox.warning(
1728
+ self,
1729
+ "Output Directory Missing",
1730
+ "Please specify an output directory.",
1731
+ )
1732
+ return
1733
+
1734
+ # Get processing options
1735
+ use_multiprocessing = self.multiprocessing_check.isChecked()
1736
+ max_cores = self.cores_spinbox.value() if use_multiprocessing else 1
1737
+ full_stokes = self.full_stokes_radio.isChecked()
1738
+ stokes_param = self.stokes_combo.currentText() if not full_stokes else None
1739
+
1740
+ # Prepare progress dialog
1741
+ progress_dialog = QProgressDialog(
1742
+ "Converting files to helioprojective coordinates...",
1743
+ "Cancel",
1744
+ 0,
1745
+ len(files_to_process),
1746
+ self,
1747
+ )
1748
+ progress_dialog.setWindowTitle("Batch Conversion")
1749
+ progress_dialog.setWindowModality(Qt.WindowModal)
1750
+ progress_dialog.show()
1751
+
1752
+ # Import modules needed for processing
1753
+ import multiprocessing
1754
+ import time
1755
+ from .helioprojective import convert_and_save_hpc
1756
+
1757
+ # Use a worker thread or process for conversion
1758
+ try:
1759
+ self.status_text.appendPlainText("Starting batch conversion...")
1760
+ self.ok_button.setEnabled(False)
1761
+ QApplication.processEvents()
1762
+
1763
+ # Initialize counters
1764
+ success_count = 0
1765
+ error_count = 0
1766
+ completed_count = 0
1767
+ pool = None
1768
+ results = []
1769
+
1770
+ # Multi-stokes requires different handling
1771
+ if full_stokes:
1772
+ stokes_list = ["I", "Q", "U", "V"]
1773
+
1774
+ if use_multiprocessing and len(files_to_process) > 1:
1775
+ # Prepare arguments for multiprocessing
1776
+ self.status_text.appendPlainText(
1777
+ f"Using multiprocessing with {max_cores} cores"
1778
+ )
1779
+
1780
+ # Create task list - each task is (input_file, output_path, stokes, process_id)
1781
+ tasks = []
1782
+ for i, input_file in enumerate(files_to_process):
1783
+ base_filename = os.path.basename(input_file)
1784
+ process_id = i # Use file index as part of process ID
1785
+
1786
+ if "*" in output_pattern:
1787
+ output_filename = output_pattern.replace(
1788
+ "*", os.path.splitext(base_filename)[0]
1789
+ )
1790
+ else:
1791
+ output_filename = (
1792
+ f"{os.path.splitext(base_filename)[0]}_{output_pattern}"
1793
+ )
1794
+
1795
+ output_path = os.path.join(output_dir, output_filename)
1796
+
1797
+ # Create tasks for each stokes parameter
1798
+ for stokes in stokes_list:
1799
+ stokes_output = output_path.replace(
1800
+ ".fits", f"_{stokes}.fits"
1801
+ )
1802
+ task = (
1803
+ input_file,
1804
+ stokes_output,
1805
+ stokes,
1806
+ f"{process_id}_{stokes}",
1807
+ )
1808
+ tasks.append(task)
1809
+
1810
+ # Set up progress tracking
1811
+ total_tasks = len(tasks)
1812
+ progress_dialog.setMaximum(total_tasks)
1813
+
1814
+ # Create process pool and start processing
1815
+ pool = multiprocessing.Pool(processes=max_cores)
1816
+
1817
+ # Start asynchronous processing with our standalone function
1818
+ result_objects = pool.map_async(process_single_file_hpc, tasks)
1819
+ pool.close() # No more tasks will be submitted
1820
+
1821
+ # Monitor progress while processing
1822
+ while not result_objects.ready():
1823
+ if progress_dialog.wasCanceled():
1824
+ pool.terminate()
1825
+ self.status_text.appendPlainText(
1826
+ "Operation canceled by user."
1827
+ )
1828
+ break
1829
+ time.sleep(0.1) # Short sleep to prevent UI blocking
1830
+ QApplication.processEvents()
1831
+
1832
+ # Get results if not canceled
1833
+ if not progress_dialog.wasCanceled():
1834
+ results = result_objects.get()
1835
+
1836
+ # Process results
1837
+ file_results = {} # Group results by input file
1838
+
1839
+ for result in results:
1840
+ input_file = result["input_file"]
1841
+ basename = os.path.basename(input_file)
1842
+
1843
+ if basename not in file_results:
1844
+ file_results[basename] = {"success": 0, "errors": []}
1845
+
1846
+ if result["success"]:
1847
+ file_results[basename]["success"] += 1
1848
+ self.status_text.appendPlainText(
1849
+ f" - Stokes {result['stokes']}: Converted successfully"
1850
+ )
1851
+ else:
1852
+ error_msg = result["error"] or "Unknown error"
1853
+ file_results[basename]["errors"].append(
1854
+ f"Stokes {result['stokes']}: {error_msg}"
1855
+ )
1856
+ self.status_text.appendPlainText(
1857
+ f" - Stokes {result['stokes']}: Error: {error_msg}"
1858
+ )
1859
+
1860
+ # Count overall successes
1861
+ for basename, res in file_results.items():
1862
+ if res["success"] == len(stokes_list):
1863
+ success_count += 1
1864
+ elif res["success"] > 0:
1865
+ success_count += 0.5 # Partial success
1866
+ error_count += 0.5
1867
+ else:
1868
+ error_count += 1
1869
+
1870
+ # Log each file's summary
1871
+ self.status_text.appendPlainText(
1872
+ f"File {basename}: {res['success']}/{len(stokes_list)} stokes parameters processed successfully"
1873
+ )
1874
+ if res["errors"]:
1875
+ for err in res["errors"]:
1876
+ self.status_text.appendPlainText(
1877
+ f" - Error: {err}"
1878
+ )
1879
+
1880
+ # Update progress to completion
1881
+ progress_dialog.setValue(total_tasks)
1882
+ else:
1883
+ # Sequential processing for multi-stokes
1884
+ for i, input_file in enumerate(files_to_process):
1885
+ # Check if canceled
1886
+ if progress_dialog.wasCanceled():
1887
+ self.status_text.appendPlainText(
1888
+ "Operation canceled by user."
1889
+ )
1890
+ break
1891
+
1892
+ # Get output filename
1893
+ base_filename = os.path.basename(input_file)
1894
+ if "*" in output_pattern:
1895
+ output_filename = output_pattern.replace(
1896
+ "*", os.path.splitext(base_filename)[0]
1897
+ )
1898
+ else:
1899
+ output_filename = (
1900
+ f"{os.path.splitext(base_filename)[0]}_{output_pattern}"
1901
+ )
1902
+
1903
+ output_path = os.path.join(output_dir, output_filename)
1904
+
1905
+ # Update progress dialog
1906
+ progress_dialog.setValue(i)
1907
+ progress_dialog.setLabelText(f"Converting: {base_filename}")
1908
+ QApplication.processEvents()
1909
+
1910
+ self.status_text.appendPlainText(
1911
+ f"Processing {i+1}/{len(files_to_process)}: {base_filename}"
1912
+ )
1913
+
1914
+ stokes_success = 0
1915
+ for stokes in stokes_list:
1916
+ # Create stokes-specific output filename
1917
+ stokes_output = output_path.replace(
1918
+ ".fits", f"_{stokes}.fits"
1919
+ )
1920
+
1921
+ try:
1922
+ # Convert file with a unique temp suffix
1923
+ temp_suffix = f"_seq_{i}_{stokes}"
1924
+ result = process_single_file_hpc(
1925
+ (
1926
+ input_file,
1927
+ stokes_output,
1928
+ stokes,
1929
+ f"_seq_{i}_{stokes}",
1930
+ )
1931
+ )
1932
+ success = result["success"]
1933
+
1934
+ if success:
1935
+ stokes_success += 1
1936
+ self.status_text.appendPlainText(
1937
+ f" - Stokes {stokes}: Converted successfully"
1938
+ )
1939
+ else:
1940
+ self.status_text.appendPlainText(
1941
+ f" - Stokes {stokes}: Conversion failed"
1942
+ )
1943
+
1944
+ except Exception as e:
1945
+ self.status_text.appendPlainText(
1946
+ f" - Stokes {stokes}: Error: {str(e)}"
1947
+ )
1948
+
1949
+ if stokes_success == len(stokes_list):
1950
+ success_count += 1
1951
+ elif stokes_success > 0:
1952
+ success_count += 0.5 # Partial success
1953
+ error_count += 0.5
1954
+ else:
1955
+ error_count += 1
1956
+ else:
1957
+ # Single stokes processing
1958
+ if use_multiprocessing and len(files_to_process) > 1:
1959
+ # Prepare arguments for multiprocessing
1960
+ self.status_text.appendPlainText(
1961
+ f"Using multiprocessing with {max_cores} cores"
1962
+ )
1963
+
1964
+ # Create task list - each task is (input_file, output_path, stokes, process_id)
1965
+ tasks = []
1966
+ for i, input_file in enumerate(files_to_process):
1967
+ base_filename = os.path.basename(input_file)
1968
+
1969
+ if "*" in output_pattern:
1970
+ output_filename = output_pattern.replace(
1971
+ "*", os.path.splitext(base_filename)[0]
1972
+ )
1973
+ else:
1974
+ output_filename = (
1975
+ f"{os.path.splitext(base_filename)[0]}_{output_pattern}"
1976
+ )
1977
+
1978
+ output_path = os.path.join(output_dir, output_filename)
1979
+ task = (input_file, output_path, stokes_param, i)
1980
+ tasks.append(task)
1981
+
1982
+ # Set up progress tracking
1983
+ total_tasks = len(tasks)
1984
+ progress_dialog.setMaximum(total_tasks)
1985
+
1986
+ # Create process pool
1987
+ pool = multiprocessing.Pool(processes=max_cores)
1988
+
1989
+ # Start asynchronous processing
1990
+ result_objects = pool.map_async(process_single_file_hpc, tasks)
1991
+ pool.close() # No more tasks will be submitted
1992
+
1993
+ # Monitor progress while processing
1994
+ while not result_objects.ready():
1995
+ if progress_dialog.wasCanceled():
1996
+ pool.terminate()
1997
+ self.status_text.appendPlainText(
1998
+ "Operation canceled by user."
1999
+ )
2000
+ break
2001
+ time.sleep(0.1) # Short sleep to prevent UI blocking
2002
+ QApplication.processEvents()
2003
+
2004
+ # Process results if not canceled
2005
+ if not progress_dialog.wasCanceled():
2006
+ results = result_objects.get()
2007
+
2008
+ # Process results
2009
+ for result in results:
2010
+ basename = os.path.basename(result["input_file"])
2011
+
2012
+ if result["success"]:
2013
+ success_count += 1
2014
+ self.status_text.appendPlainText(
2015
+ f" - {basename}: Converted successfully"
2016
+ )
2017
+ else:
2018
+ error_count += 1
2019
+ error_msg = result["error"] or "Unknown error"
2020
+ self.status_text.appendPlainText(
2021
+ f" - {basename}: Error: {error_msg}"
2022
+ )
2023
+
2024
+ # Update progress to completion
2025
+ progress_dialog.setValue(total_tasks)
2026
+ else:
2027
+ # Sequential processing for single stokes
2028
+ for i, input_file in enumerate(files_to_process):
2029
+ # Check if canceled
2030
+ if progress_dialog.wasCanceled():
2031
+ self.status_text.appendPlainText(
2032
+ "Operation canceled by user."
2033
+ )
2034
+ break
2035
+
2036
+ # Get output filename
2037
+ base_filename = os.path.basename(input_file)
2038
+ if "*" in output_pattern:
2039
+ output_filename = output_pattern.replace(
2040
+ "*", os.path.splitext(base_filename)[0]
2041
+ )
2042
+ else:
2043
+ output_filename = (
2044
+ f"{os.path.splitext(base_filename)[0]}_{output_pattern}"
2045
+ )
2046
+
2047
+ output_path = os.path.join(output_dir, output_filename)
2048
+
2049
+ # Update progress dialog
2050
+ progress_dialog.setValue(i)
2051
+ progress_dialog.setLabelText(f"Converting: {base_filename}")
2052
+ QApplication.processEvents()
2053
+
2054
+ self.status_text.appendPlainText(
2055
+ f"Processing {i+1}/{len(files_to_process)}: {base_filename}"
2056
+ )
2057
+
2058
+ try:
2059
+ # Convert file with a unique temp suffix
2060
+ temp_suffix = f"_seq_{i}"
2061
+ result = process_single_file_hpc(
2062
+ (input_file, output_path, stokes_param, f"_seq_{i}")
2063
+ )
2064
+ success = result["success"]
2065
+
2066
+ if success:
2067
+ success_count += 1
2068
+ self.status_text.appendPlainText(
2069
+ " - Converted successfully"
2070
+ )
2071
+ else:
2072
+ error_count += 1
2073
+ self.status_text.appendPlainText(
2074
+ " - Conversion failed"
2075
+ )
2076
+
2077
+ except Exception as e:
2078
+ error_count += 1
2079
+ self.status_text.appendPlainText(f" - Error: {str(e)}")
2080
+
2081
+ # Complete the progress
2082
+ progress_dialog.setValue(progress_dialog.maximum())
2083
+
2084
+ # Show completion message
2085
+ summary = (
2086
+ f"Batch conversion completed:\n"
2087
+ f"Total files: {len(files_to_process)}\n"
2088
+ f"Successfully converted: {success_count}\n"
2089
+ f"Failed: {error_count}"
2090
+ )
2091
+
2092
+ self.status_text.appendPlainText("\n" + summary)
2093
+ QMessageBox.information(self, "Conversion Complete", summary)
2094
+
2095
+ except Exception as e:
2096
+ self.status_text.appendPlainText(f"Error in batch processing: {str(e)}")
2097
+ self.status_text.appendPlainText(traceback.format_exc())
2098
+ QMessageBox.critical(self, "Error", f"Error in batch processing: {str(e)}")
2099
+ finally:
2100
+ # Clean up multiprocessing pool if it exists
2101
+ if pool is not None:
2102
+ pool.terminate()
2103
+ pool.join()
2104
+
2105
+ # Close progress dialog and re-enable button
2106
+ progress_dialog.close()
2107
+ self.ok_button.setEnabled(True)
2108
+
2109
+
2110
+ class PlotCustomizationDialog(QDialog):
2111
+ """Dialog for customizing plot appearance (labels, fonts, colors)."""
2112
+
2113
+ def __init__(self, parent=None, settings=None):
2114
+ super().__init__(parent)
2115
+ self.setWindowTitle("Plot Customization")
2116
+ self.setMinimumWidth(660)
2117
+ self.setMaximumHeight(1280)
2118
+ self.settings = settings.copy() if settings else {}
2119
+ self.setup_ui()
2120
+
2121
+ def setup_ui(self):
2122
+ from PyQt5.QtWidgets import QTabWidget
2123
+
2124
+ outer_layout = QVBoxLayout(self)
2125
+ outer_layout.setSpacing(8)
2126
+ outer_layout.setContentsMargins(10, 10, 10, 10)
2127
+
2128
+ # Create tab widget for organized sections
2129
+ tab_widget = QTabWidget()
2130
+
2131
+ # ===== TAB 1: TEXT & LABELS =====
2132
+ text_tab = QWidget()
2133
+ text_layout = QVBoxLayout(text_tab)
2134
+ text_layout.setSpacing(8)
2135
+
2136
+ # Labels Section
2137
+ labels_group = QGroupBox("Labels")
2138
+ labels_layout = QGridLayout(labels_group)
2139
+ labels_layout.setSpacing(8)
2140
+
2141
+ labels_layout.addWidget(QLabel("X-Axis:"), 0, 0)
2142
+ self.xlabel_edit = QLineEdit(self.settings.get("xlabel", ""))
2143
+ self.xlabel_edit.setPlaceholderText("Auto")
2144
+ labels_layout.addWidget(self.xlabel_edit, 0, 1)
2145
+
2146
+ labels_layout.addWidget(QLabel("Y-Axis:"), 0, 2)
2147
+ self.ylabel_edit = QLineEdit(self.settings.get("ylabel", ""))
2148
+ self.ylabel_edit.setPlaceholderText("Auto")
2149
+ labels_layout.addWidget(self.ylabel_edit, 0, 3)
2150
+
2151
+ labels_layout.addWidget(QLabel("Title:"), 1, 0)
2152
+ self.title_edit = QLineEdit(self.settings.get("title", ""))
2153
+ self.title_edit.setPlaceholderText("Auto")
2154
+ labels_layout.addWidget(self.title_edit, 1, 1)
2155
+
2156
+ labels_layout.addWidget(QLabel("Colorbar:"), 1, 2)
2157
+ self.colorbar_label_edit = QLineEdit(self.settings.get("colorbar_label", ""))
2158
+ self.colorbar_label_edit.setPlaceholderText("e.g., Jy/beam")
2159
+ labels_layout.addWidget(self.colorbar_label_edit, 1, 3)
2160
+
2161
+ text_layout.addWidget(labels_group)
2162
+
2163
+ # Font Sizes Section (compact grid)
2164
+ fonts_group = QGroupBox("Font Sizes")
2165
+ fonts_layout = QGridLayout(fonts_group)
2166
+ fonts_layout.setSpacing(8)
2167
+
2168
+ fonts_layout.addWidget(QLabel("Axis Labels:"), 0, 0)
2169
+ self.axis_label_size = QSpinBox()
2170
+ self.axis_label_size.setRange(1, 50)
2171
+ self.axis_label_size.setValue(self.settings.get("axis_label_fontsize", 12))
2172
+ fonts_layout.addWidget(self.axis_label_size, 0, 1)
2173
+
2174
+ fonts_layout.addWidget(QLabel("Axis Ticks:"), 0, 2)
2175
+ self.axis_tick_size = QSpinBox()
2176
+ self.axis_tick_size.setRange(1, 50)
2177
+ self.axis_tick_size.setValue(self.settings.get("axis_tick_fontsize", 10))
2178
+ fonts_layout.addWidget(self.axis_tick_size, 0, 3)
2179
+
2180
+ fonts_layout.addWidget(QLabel("Title:"), 1, 0)
2181
+ self.title_size = QSpinBox()
2182
+ self.title_size.setRange(1, 50)
2183
+ self.title_size.setValue(self.settings.get("title_fontsize", 12))
2184
+ fonts_layout.addWidget(self.title_size, 1, 1)
2185
+
2186
+ fonts_layout.addWidget(QLabel("Colorbar:"), 1, 2)
2187
+ self.colorbar_label_size = QSpinBox()
2188
+ self.colorbar_label_size.setRange(1, 50)
2189
+ self.colorbar_label_size.setValue(self.settings.get("colorbar_label_fontsize", 10))
2190
+ fonts_layout.addWidget(self.colorbar_label_size, 1, 3)
2191
+
2192
+ fonts_layout.addWidget(QLabel("Colorbar Ticks:"), 2, 0)
2193
+ self.colorbar_tick_size = QSpinBox()
2194
+ self.colorbar_tick_size.setRange(1, 50)
2195
+ self.colorbar_tick_size.setValue(self.settings.get("colorbar_tick_fontsize", 10))
2196
+ fonts_layout.addWidget(self.colorbar_tick_size, 2, 1)
2197
+
2198
+ # Scale buttons
2199
+ scale_layout = QHBoxLayout()
2200
+ scale_layout.addWidget(QLabel("Scale All:"))
2201
+ scale_down_btn = QPushButton("-")
2202
+ scale_down_btn.setFixedWidth(30)
2203
+ scale_down_btn.clicked.connect(self._scale_fonts_down)
2204
+ scale_up_btn = QPushButton("+")
2205
+ scale_up_btn.setFixedWidth(30)
2206
+ scale_up_btn.clicked.connect(self._scale_fonts_up)
2207
+ scale_layout.addWidget(scale_down_btn)
2208
+ scale_layout.addWidget(scale_up_btn)
2209
+ scale_layout.addStretch()
2210
+ fonts_layout.addLayout(scale_layout, 2, 2, 1, 2)
2211
+
2212
+ text_layout.addWidget(fonts_group)
2213
+ text_layout.addStretch()
2214
+
2215
+ tab_widget.addTab(text_tab, "Text")
2216
+
2217
+ # ===== TAB 2: COLORS & STYLE =====
2218
+ style_tab = QWidget()
2219
+ style_layout = QVBoxLayout(style_tab)
2220
+ style_layout.setSpacing(8)
2221
+
2222
+ # Colors Section (compact 2-column grid)
2223
+ colors_group = QGroupBox("Colors")
2224
+ colors_layout = QGridLayout(colors_group)
2225
+ colors_layout.setSpacing(6)
2226
+
2227
+ # Row 0: Plot BG, Figure BG
2228
+ colors_layout.addWidget(QLabel("Plot BG:"), 0, 0)
2229
+ self.plot_bg_color = self.settings.get("plot_bg_color", "auto")
2230
+ self.plot_bg_preview = QLabel()
2231
+ self.plot_bg_preview.setFixedSize(20, 20)
2232
+ self._update_color_preview(self.plot_bg_preview, self.plot_bg_color)
2233
+ self.plot_bg_btn = QPushButton("...")
2234
+ self.plot_bg_btn.setFixedWidth(30)
2235
+ self.plot_bg_btn.clicked.connect(self._pick_plot_bg_color)
2236
+ self.plot_bg_auto_btn = QPushButton("A")
2237
+ self.plot_bg_auto_btn.setFixedWidth(25)
2238
+ self.plot_bg_auto_btn.setToolTip("Auto")
2239
+ self.plot_bg_auto_btn.clicked.connect(lambda: self._set_plot_bg_auto())
2240
+ plot_bg_row = QHBoxLayout()
2241
+ plot_bg_row.addWidget(self.plot_bg_preview)
2242
+ plot_bg_row.addWidget(self.plot_bg_btn)
2243
+ plot_bg_row.addWidget(self.plot_bg_auto_btn)
2244
+ colors_layout.addLayout(plot_bg_row, 0, 1)
2245
+
2246
+ colors_layout.addWidget(QLabel("Figure BG:"), 0, 2)
2247
+ self.figure_bg_color = self.settings.get("figure_bg_color", "auto")
2248
+ self.figure_bg_preview = QLabel()
2249
+ self.figure_bg_preview.setFixedSize(20, 20)
2250
+ self._update_color_preview(self.figure_bg_preview, self.figure_bg_color)
2251
+ self.figure_bg_btn = QPushButton("...")
2252
+ self.figure_bg_btn.setFixedWidth(30)
2253
+ self.figure_bg_btn.clicked.connect(self._pick_figure_bg_color)
2254
+ self.figure_bg_auto_btn = QPushButton("A")
2255
+ self.figure_bg_auto_btn.setFixedWidth(25)
2256
+ self.figure_bg_auto_btn.setToolTip("Auto")
2257
+ self.figure_bg_auto_btn.clicked.connect(lambda: self._set_figure_bg_auto())
2258
+ figure_bg_row = QHBoxLayout()
2259
+ figure_bg_row.addWidget(self.figure_bg_preview)
2260
+ figure_bg_row.addWidget(self.figure_bg_btn)
2261
+ figure_bg_row.addWidget(self.figure_bg_auto_btn)
2262
+ colors_layout.addLayout(figure_bg_row, 0, 3)
2263
+
2264
+ # Row 1: Text, Tick
2265
+ colors_layout.addWidget(QLabel("Text:"), 1, 0)
2266
+ self.text_color = self.settings.get("text_color", "auto")
2267
+ self.text_color_preview = QLabel()
2268
+ self.text_color_preview.setFixedSize(20, 20)
2269
+ self._update_color_preview(self.text_color_preview, self.text_color)
2270
+ self.text_color_btn = QPushButton("...")
2271
+ self.text_color_btn.setFixedWidth(30)
2272
+ self.text_color_btn.clicked.connect(self._pick_text_color)
2273
+ self.text_color_auto_btn = QPushButton("A")
2274
+ self.text_color_auto_btn.setFixedWidth(25)
2275
+ self.text_color_auto_btn.setToolTip("Auto")
2276
+ self.text_color_auto_btn.clicked.connect(self._set_text_color_auto)
2277
+ text_color_row = QHBoxLayout()
2278
+ text_color_row.addWidget(self.text_color_preview)
2279
+ text_color_row.addWidget(self.text_color_btn)
2280
+ text_color_row.addWidget(self.text_color_auto_btn)
2281
+ colors_layout.addLayout(text_color_row, 1, 1)
2282
+
2283
+ colors_layout.addWidget(QLabel("Ticks:"), 1, 2)
2284
+ self.tick_color = self.settings.get("tick_color", "auto")
2285
+ self.tick_color_preview = QLabel()
2286
+ self.tick_color_preview.setFixedSize(20, 20)
2287
+ self._update_color_preview(self.tick_color_preview, self.tick_color)
2288
+ self.tick_color_btn = QPushButton("...")
2289
+ self.tick_color_btn.setFixedWidth(30)
2290
+ self.tick_color_btn.clicked.connect(self._pick_tick_color)
2291
+ self.tick_color_auto_btn = QPushButton("A")
2292
+ self.tick_color_auto_btn.setFixedWidth(25)
2293
+ self.tick_color_auto_btn.setToolTip("Auto")
2294
+ self.tick_color_auto_btn.clicked.connect(self._set_tick_color_auto)
2295
+ tick_color_row = QHBoxLayout()
2296
+ tick_color_row.addWidget(self.tick_color_preview)
2297
+ tick_color_row.addWidget(self.tick_color_btn)
2298
+ tick_color_row.addWidget(self.tick_color_auto_btn)
2299
+ colors_layout.addLayout(tick_color_row, 1, 3)
2300
+
2301
+ # Row 2: Border color + width
2302
+ colors_layout.addWidget(QLabel("Border:"), 2, 0)
2303
+ self.border_color = self.settings.get("border_color", "auto")
2304
+ self.border_color_preview = QLabel()
2305
+ self.border_color_preview.setFixedSize(20, 20)
2306
+ self._update_color_preview(self.border_color_preview, self.border_color)
2307
+ self.border_color_btn = QPushButton("...")
2308
+ self.border_color_btn.setFixedWidth(30)
2309
+ self.border_color_btn.clicked.connect(self._pick_border_color)
2310
+ self.border_color_auto_btn = QPushButton("A")
2311
+ self.border_color_auto_btn.setFixedWidth(25)
2312
+ self.border_color_auto_btn.setToolTip("Auto")
2313
+ self.border_color_auto_btn.clicked.connect(self._set_border_color_auto)
2314
+ border_color_row = QHBoxLayout()
2315
+ border_color_row.addWidget(self.border_color_preview)
2316
+ border_color_row.addWidget(self.border_color_btn)
2317
+ border_color_row.addWidget(self.border_color_auto_btn)
2318
+ colors_layout.addLayout(border_color_row, 2, 1)
2319
+
2320
+ colors_layout.addWidget(QLabel("Border Width:"), 2, 2)
2321
+ self.border_width = QDoubleSpinBox()
2322
+ self.border_width.setRange(0.5, 5.0)
2323
+ self.border_width.setSingleStep(0.5)
2324
+ self.border_width.setValue(self.settings.get("border_width", 1.0))
2325
+ colors_layout.addWidget(self.border_width, 2, 3)
2326
+
2327
+ style_layout.addWidget(colors_group)
2328
+
2329
+ # Tick Marks Section (compact)
2330
+ ticks_group = QGroupBox("Tick Marks")
2331
+ ticks_layout = QGridLayout(ticks_group)
2332
+ ticks_layout.setSpacing(8)
2333
+
2334
+ ticks_layout.addWidget(QLabel("Direction:"), 0, 0)
2335
+ self.tick_direction = QComboBox()
2336
+ self.tick_direction.addItems(["in", "out"])
2337
+ self.tick_direction.setCurrentText(self.settings.get("tick_direction", "out"))
2338
+ ticks_layout.addWidget(self.tick_direction, 0, 1)
2339
+
2340
+ ticks_layout.addWidget(QLabel("Length:"), 0, 2)
2341
+ self.tick_length = QSpinBox()
2342
+ self.tick_length.setRange(1, 20)
2343
+ self.tick_length.setValue(self.settings.get("tick_length", 4))
2344
+ ticks_layout.addWidget(self.tick_length, 0, 3)
2345
+
2346
+ ticks_layout.addWidget(QLabel("Width:"), 1, 0)
2347
+ self.tick_width = QDoubleSpinBox()
2348
+ self.tick_width.setRange(0.5, 5.0)
2349
+ self.tick_width.setSingleStep(0.5)
2350
+ self.tick_width.setValue(self.settings.get("tick_width", 1.0))
2351
+ ticks_layout.addWidget(self.tick_width, 1, 1)
2352
+
2353
+ style_layout.addWidget(ticks_group)
2354
+ style_layout.addStretch()
2355
+
2356
+ tab_widget.addTab(style_tab, "Style")
2357
+
2358
+ # ===== TAB 3: PADDING =====
2359
+ padding_tab = QWidget()
2360
+ padding_layout = QVBoxLayout(padding_tab)
2361
+ padding_layout.setSpacing(8)
2362
+
2363
+ # Subplot Margins Section
2364
+ margins_group = QGroupBox("Plot Margins (0.0 - 1.0)")
2365
+ margins_layout = QGridLayout(margins_group)
2366
+ margins_layout.setSpacing(10)
2367
+
2368
+ # Left
2369
+ margins_layout.addWidget(QLabel("Left:"), 0, 0)
2370
+ self.pad_left = QDoubleSpinBox()
2371
+ self.pad_left.setRange(0.0, 0.5)
2372
+ self.pad_left.setSingleStep(0.01)
2373
+ self.pad_left.setDecimals(2)
2374
+ self.pad_left.setValue(self.settings.get("pad_left", 0.135))
2375
+ margins_layout.addWidget(self.pad_left, 0, 1)
2376
+
2377
+ # Right
2378
+ margins_layout.addWidget(QLabel("Right:"), 0, 2)
2379
+ self.pad_right = QDoubleSpinBox()
2380
+ self.pad_right.setRange(0.5, 1.0)
2381
+ self.pad_right.setSingleStep(0.01)
2382
+ self.pad_right.setDecimals(2)
2383
+ self.pad_right.setValue(self.settings.get("pad_right", 1.0))
2384
+ margins_layout.addWidget(self.pad_right, 0, 3)
2385
+
2386
+ # Top
2387
+ margins_layout.addWidget(QLabel("Top:"), 1, 0)
2388
+ self.pad_top = QDoubleSpinBox()
2389
+ self.pad_top.setRange(0.5, 1.0)
2390
+ self.pad_top.setSingleStep(0.01)
2391
+ self.pad_top.setDecimals(2)
2392
+ self.pad_top.setValue(self.settings.get("pad_top", 0.95))
2393
+ margins_layout.addWidget(self.pad_top, 1, 1)
2394
+
2395
+ # Bottom
2396
+ margins_layout.addWidget(QLabel("Bottom:"), 1, 2)
2397
+ self.pad_bottom = QDoubleSpinBox()
2398
+ self.pad_bottom.setRange(0.0, 0.5)
2399
+ self.pad_bottom.setSingleStep(0.01)
2400
+ self.pad_bottom.setDecimals(2)
2401
+ self.pad_bottom.setValue(self.settings.get("pad_bottom", 0.05))
2402
+ margins_layout.addWidget(self.pad_bottom, 1, 3)
2403
+
2404
+ # Wspace (width space between subplots)
2405
+ margins_layout.addWidget(QLabel("Wspace:"), 2, 0)
2406
+ self.pad_wspace = QDoubleSpinBox()
2407
+ self.pad_wspace.setRange(0.0, 0.5)
2408
+ self.pad_wspace.setSingleStep(0.01)
2409
+ self.pad_wspace.setDecimals(2)
2410
+ self.pad_wspace.setValue(self.settings.get("pad_wspace", 0.2))
2411
+ self.pad_wspace.setToolTip("Width space between subplots")
2412
+ margins_layout.addWidget(self.pad_wspace, 2, 1)
2413
+
2414
+ # Hspace (height space between subplots)
2415
+ margins_layout.addWidget(QLabel("Hspace:"), 2, 2)
2416
+ self.pad_hspace = QDoubleSpinBox()
2417
+ self.pad_hspace.setRange(0.0, 0.5)
2418
+ self.pad_hspace.setSingleStep(0.01)
2419
+ self.pad_hspace.setDecimals(2)
2420
+ self.pad_hspace.setValue(self.settings.get("pad_hspace", 0.2))
2421
+ self.pad_hspace.setToolTip("Height space between subplots")
2422
+ margins_layout.addWidget(self.pad_hspace, 2, 3)
2423
+
2424
+ padding_layout.addWidget(margins_group)
2425
+
2426
+ # Tight Layout Option
2427
+ tight_group = QGroupBox("Layout Options")
2428
+ tight_layout_grid = QGridLayout(tight_group)
2429
+
2430
+ self.use_tight_layout = QCheckBox("Use Tight Layout")
2431
+ self.use_tight_layout.setChecked(self.settings.get("use_tight_layout", False))
2432
+ self.use_tight_layout.setToolTip("Automatically adjust margins for best fit")
2433
+ tight_layout_grid.addWidget(self.use_tight_layout, 0, 0)
2434
+
2435
+ padding_layout.addWidget(tight_group)
2436
+ padding_layout.addStretch()
2437
+
2438
+ tab_widget.addTab(padding_tab, "Padding")
2439
+
2440
+ outer_layout.addWidget(tab_widget)
2441
+
2442
+ # Bottom buttons row
2443
+ buttons_layout = QHBoxLayout()
2444
+ reset_btn = QPushButton("Reset to Defaults")
2445
+ reset_btn.clicked.connect(self.reset_to_defaults)
2446
+ buttons_layout.addWidget(reset_btn)
2447
+ buttons_layout.addStretch()
2448
+
2449
+ button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
2450
+ button_box.accepted.connect(self.accept)
2451
+ button_box.rejected.connect(self.reject)
2452
+ buttons_layout.addWidget(button_box)
2453
+
2454
+ outer_layout.addLayout(buttons_layout)
2455
+
2456
+ def _update_color_preview(self, label, color):
2457
+ """Update the color preview label."""
2458
+ if color == "auto" or color == "transparent":
2459
+ label.setStyleSheet("background-color: #888888; border: 1px solid #555555;")
2460
+ label.setText("A" if color == "auto" else "T")
2461
+ label.setAlignment(Qt.AlignCenter)
2462
+ else:
2463
+ label.setStyleSheet(f"background-color: {color}; border: 1px solid #555555;")
2464
+ label.setText("")
2465
+
2466
+ def _pick_plot_bg_color(self):
2467
+ """Open color picker for plot background."""
2468
+ from PyQt5.QtWidgets import QColorDialog, QPushButton
2469
+ from PyQt5.QtGui import QColor
2470
+
2471
+ initial = QColor(self.plot_bg_color) if self.plot_bg_color not in ("auto", "transparent") else QColor("#ffffff")
2472
+ dialog = QColorDialog(initial, self)
2473
+ dialog.setWindowTitle("Select Plot Background Color")
2474
+ dialog.setOption(QColorDialog.DontUseNativeDialog, True)
2475
+
2476
+ # Hide the "Pick Screen Color" button
2477
+ for btn in dialog.findChildren(QPushButton):
2478
+ if "pick" in btn.text().lower() or "screen" in btn.text().lower():
2479
+ btn.hide()
2480
+
2481
+ if dialog.exec_() == QColorDialog.Accepted:
2482
+ self.plot_bg_color = dialog.selectedColor().name()
2483
+ self._update_color_preview(self.plot_bg_preview, self.plot_bg_color)
2484
+
2485
+ def _pick_figure_bg_color(self):
2486
+ """Open color picker for figure background."""
2487
+ from PyQt5.QtWidgets import QColorDialog, QPushButton
2488
+ from PyQt5.QtGui import QColor
2489
+
2490
+ initial = QColor(self.figure_bg_color) if self.figure_bg_color not in ("auto", "transparent") else QColor("#ffffff")
2491
+ dialog = QColorDialog(initial, self)
2492
+ dialog.setWindowTitle("Select Figure Background Color")
2493
+ dialog.setOption(QColorDialog.DontUseNativeDialog, True)
2494
+
2495
+ # Hide the "Pick Screen Color" button
2496
+ for btn in dialog.findChildren(QPushButton):
2497
+ if "pick" in btn.text().lower() or "screen" in btn.text().lower():
2498
+ btn.hide()
2499
+
2500
+ if dialog.exec_() == QColorDialog.Accepted:
2501
+ self.figure_bg_color = dialog.selectedColor().name()
2502
+ self._update_color_preview(self.figure_bg_preview, self.figure_bg_color)
2503
+
2504
+ def _set_plot_bg_auto(self):
2505
+ """Set plot background to auto."""
2506
+ self.plot_bg_color = "auto"
2507
+ self._update_color_preview(self.plot_bg_preview, "auto")
2508
+
2509
+ def _set_figure_bg_auto(self):
2510
+ """Set figure background to auto."""
2511
+ self.figure_bg_color = "auto"
2512
+ self._update_color_preview(self.figure_bg_preview, "auto")
2513
+
2514
+ def _pick_text_color(self):
2515
+ """Open color picker for text color."""
2516
+ from PyQt5.QtWidgets import QColorDialog, QPushButton
2517
+ from PyQt5.QtGui import QColor
2518
+
2519
+ initial = QColor(self.text_color) if self.text_color not in ("auto", "transparent") else QColor("#ffffff")
2520
+ dialog = QColorDialog(initial, self)
2521
+ dialog.setWindowTitle("Select Text Color")
2522
+ dialog.setOption(QColorDialog.DontUseNativeDialog, True)
2523
+
2524
+ # Hide the "Pick Screen Color" button
2525
+ for btn in dialog.findChildren(QPushButton):
2526
+ if "pick" in btn.text().lower() or "screen" in btn.text().lower():
2527
+ btn.hide()
2528
+
2529
+ if dialog.exec_() == QColorDialog.Accepted:
2530
+ self.text_color = dialog.selectedColor().name()
2531
+ self._update_color_preview(self.text_color_preview, self.text_color)
2532
+
2533
+ def _set_text_color_auto(self):
2534
+ """Set text color to auto."""
2535
+ self.text_color = "auto"
2536
+ self._update_color_preview(self.text_color_preview, "auto")
2537
+
2538
+ def _pick_tick_color(self):
2539
+ """Open color picker for tick color."""
2540
+ from PyQt5.QtWidgets import QColorDialog, QPushButton
2541
+ from PyQt5.QtGui import QColor
2542
+
2543
+ initial = QColor(self.tick_color) if self.tick_color not in ("auto", "transparent") else QColor("#ffffff")
2544
+ dialog = QColorDialog(initial, self)
2545
+ dialog.setWindowTitle("Select Tick Color")
2546
+ dialog.setOption(QColorDialog.DontUseNativeDialog, True)
2547
+
2548
+ # Hide the "Pick Screen Color" button
2549
+ for btn in dialog.findChildren(QPushButton):
2550
+ if "pick" in btn.text().lower() or "screen" in btn.text().lower():
2551
+ btn.hide()
2552
+
2553
+ if dialog.exec_() == QColorDialog.Accepted:
2554
+ self.tick_color = dialog.selectedColor().name()
2555
+ self._update_color_preview(self.tick_color_preview, self.tick_color)
2556
+
2557
+ def _set_tick_color_auto(self):
2558
+ """Set tick color to auto."""
2559
+ self.tick_color = "auto"
2560
+ self._update_color_preview(self.tick_color_preview, "auto")
2561
+
2562
+ def _pick_border_color(self):
2563
+ """Open color picker for border color."""
2564
+ from PyQt5.QtWidgets import QColorDialog, QPushButton
2565
+ from PyQt5.QtGui import QColor
2566
+
2567
+ initial = QColor(self.border_color) if self.border_color not in ("auto", "transparent") else QColor("#ffffff")
2568
+ dialog = QColorDialog(initial, self)
2569
+ dialog.setWindowTitle("Select Border Color")
2570
+ dialog.setOption(QColorDialog.DontUseNativeDialog, True)
2571
+
2572
+ # Hide the "Pick Screen Color" button
2573
+ for btn in dialog.findChildren(QPushButton):
2574
+ if "pick" in btn.text().lower() or "screen" in btn.text().lower():
2575
+ btn.hide()
2576
+
2577
+ if dialog.exec_() == QColorDialog.Accepted:
2578
+ self.border_color = dialog.selectedColor().name()
2579
+ self._update_color_preview(self.border_color_preview, self.border_color)
2580
+
2581
+ def _set_border_color_auto(self):
2582
+ """Set border color to auto."""
2583
+ self.border_color = "auto"
2584
+ self._update_color_preview(self.border_color_preview, "auto")
2585
+
2586
+ def _scale_fonts_up(self):
2587
+ """Increase all font sizes by 1."""
2588
+ self.axis_label_size.setValue(min(self.axis_label_size.value() + 1, 28))
2589
+ self.axis_tick_size.setValue(min(self.axis_tick_size.value() + 1, 24))
2590
+ self.title_size.setValue(min(self.title_size.value() + 1, 32))
2591
+ self.colorbar_label_size.setValue(min(self.colorbar_label_size.value() + 1, 24))
2592
+ self.colorbar_tick_size.setValue(min(self.colorbar_tick_size.value() + 1, 20))
2593
+
2594
+ def _scale_fonts_down(self):
2595
+ """Decrease all font sizes by 1."""
2596
+ self.axis_label_size.setValue(max(self.axis_label_size.value() - 1, 6))
2597
+ self.axis_tick_size.setValue(max(self.axis_tick_size.value() - 1, 6))
2598
+ self.title_size.setValue(max(self.title_size.value() - 1, 8))
2599
+ self.colorbar_label_size.setValue(max(self.colorbar_label_size.value() - 1, 6))
2600
+ self.colorbar_tick_size.setValue(max(self.colorbar_tick_size.value() - 1, 6))
2601
+
2602
+ def reset_to_defaults(self):
2603
+ """Reset all settings to defaults."""
2604
+ self.xlabel_edit.clear()
2605
+ self.ylabel_edit.clear()
2606
+ self.title_edit.clear()
2607
+ self.colorbar_label_edit.clear()
2608
+ self.axis_label_size.setValue(12)
2609
+ self.axis_tick_size.setValue(10)
2610
+ self.title_size.setValue(12)
2611
+ self.colorbar_label_size.setValue(10)
2612
+ self.colorbar_tick_size.setValue(10)
2613
+ self.plot_bg_color = "auto"
2614
+ self.figure_bg_color = "auto"
2615
+ self.text_color = "auto"
2616
+ self.tick_color = "auto"
2617
+ self.border_color = "auto"
2618
+ self._update_color_preview(self.plot_bg_preview, "auto")
2619
+ self._update_color_preview(self.figure_bg_preview, "auto")
2620
+ self._update_color_preview(self.text_color_preview, "auto")
2621
+ self._update_color_preview(self.tick_color_preview, "auto")
2622
+ self._update_color_preview(self.border_color_preview, "auto")
2623
+ self.tick_direction.setCurrentText("out")
2624
+ self.tick_length.setValue(4)
2625
+ self.tick_width.setValue(1.0)
2626
+ self.border_width.setValue(1.0)
2627
+ # Padding defaults
2628
+ self.pad_left.setValue(0.135)
2629
+ self.pad_right.setValue(1.0)
2630
+ self.pad_top.setValue(0.95)
2631
+ self.pad_bottom.setValue(0.05)
2632
+ self.pad_wspace.setValue(0.2)
2633
+ self.pad_hspace.setValue(0.2)
2634
+ self.use_tight_layout.setChecked(False)
2635
+
2636
+ def get_settings(self):
2637
+ """Return the current settings as a dictionary."""
2638
+ return {
2639
+ "xlabel": self.xlabel_edit.text(),
2640
+ "ylabel": self.ylabel_edit.text(),
2641
+ "title": self.title_edit.text(),
2642
+ "colorbar_label": self.colorbar_label_edit.text(),
2643
+ "axis_label_fontsize": self.axis_label_size.value(),
2644
+ "axis_tick_fontsize": self.axis_tick_size.value(),
2645
+ "title_fontsize": self.title_size.value(),
2646
+ "colorbar_label_fontsize": self.colorbar_label_size.value(),
2647
+ "colorbar_tick_fontsize": self.colorbar_tick_size.value(),
2648
+ "plot_bg_color": self.plot_bg_color,
2649
+ "figure_bg_color": self.figure_bg_color,
2650
+ "text_color": self.text_color,
2651
+ "tick_color": self.tick_color,
2652
+ "border_color": self.border_color,
2653
+ "border_width": self.border_width.value(),
2654
+ "tick_direction": self.tick_direction.currentText(),
2655
+ "tick_length": self.tick_length.value(),
2656
+ "tick_width": self.tick_width.value(),
2657
+ # Padding settings
2658
+ "pad_left": self.pad_left.value(),
2659
+ "pad_right": self.pad_right.value(),
2660
+ "pad_top": self.pad_top.value(),
2661
+ "pad_bottom": self.pad_bottom.value(),
2662
+ "pad_wspace": self.pad_wspace.value(),
2663
+ "pad_hspace": self.pad_hspace.value(),
2664
+ "use_tight_layout": self.use_tight_layout.isChecked(),
2665
+ }