setiastrosuitepro 1.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of setiastrosuitepro might be problematic. Click here for more details.

Files changed (174) hide show
  1. setiastro/__init__.py +2 -0
  2. setiastro/saspro/__init__.py +20 -0
  3. setiastro/saspro/__main__.py +784 -0
  4. setiastro/saspro/_generated/__init__.py +7 -0
  5. setiastro/saspro/_generated/build_info.py +2 -0
  6. setiastro/saspro/abe.py +1295 -0
  7. setiastro/saspro/abe_preset.py +196 -0
  8. setiastro/saspro/aberration_ai.py +694 -0
  9. setiastro/saspro/aberration_ai_preset.py +224 -0
  10. setiastro/saspro/accel_installer.py +218 -0
  11. setiastro/saspro/accel_workers.py +30 -0
  12. setiastro/saspro/add_stars.py +621 -0
  13. setiastro/saspro/astrobin_exporter.py +1007 -0
  14. setiastro/saspro/astrospike.py +153 -0
  15. setiastro/saspro/astrospike_python.py +1839 -0
  16. setiastro/saspro/autostretch.py +196 -0
  17. setiastro/saspro/backgroundneutral.py +560 -0
  18. setiastro/saspro/batch_convert.py +325 -0
  19. setiastro/saspro/batch_renamer.py +519 -0
  20. setiastro/saspro/blemish_blaster.py +488 -0
  21. setiastro/saspro/blink_comparator_pro.py +2923 -0
  22. setiastro/saspro/bundles.py +61 -0
  23. setiastro/saspro/bundles_dock.py +114 -0
  24. setiastro/saspro/cheat_sheet.py +168 -0
  25. setiastro/saspro/clahe.py +342 -0
  26. setiastro/saspro/comet_stacking.py +1377 -0
  27. setiastro/saspro/config.py +38 -0
  28. setiastro/saspro/config_bootstrap.py +40 -0
  29. setiastro/saspro/config_manager.py +316 -0
  30. setiastro/saspro/continuum_subtract.py +1617 -0
  31. setiastro/saspro/convo.py +1397 -0
  32. setiastro/saspro/convo_preset.py +414 -0
  33. setiastro/saspro/copyastro.py +187 -0
  34. setiastro/saspro/cosmicclarity.py +1564 -0
  35. setiastro/saspro/cosmicclarity_preset.py +407 -0
  36. setiastro/saspro/crop_dialog_pro.py +948 -0
  37. setiastro/saspro/crop_preset.py +189 -0
  38. setiastro/saspro/curve_editor_pro.py +2544 -0
  39. setiastro/saspro/curves_preset.py +375 -0
  40. setiastro/saspro/debayer.py +670 -0
  41. setiastro/saspro/debug_utils.py +29 -0
  42. setiastro/saspro/dnd_mime.py +35 -0
  43. setiastro/saspro/doc_manager.py +2634 -0
  44. setiastro/saspro/exoplanet_detector.py +2166 -0
  45. setiastro/saspro/file_utils.py +284 -0
  46. setiastro/saspro/fitsmodifier.py +744 -0
  47. setiastro/saspro/free_torch_memory.py +48 -0
  48. setiastro/saspro/frequency_separation.py +1343 -0
  49. setiastro/saspro/function_bundle.py +1594 -0
  50. setiastro/saspro/ghs_dialog_pro.py +660 -0
  51. setiastro/saspro/ghs_preset.py +284 -0
  52. setiastro/saspro/graxpert.py +634 -0
  53. setiastro/saspro/graxpert_preset.py +287 -0
  54. setiastro/saspro/gui/__init__.py +0 -0
  55. setiastro/saspro/gui/main_window.py +8494 -0
  56. setiastro/saspro/gui/mixins/__init__.py +33 -0
  57. setiastro/saspro/gui/mixins/dock_mixin.py +263 -0
  58. setiastro/saspro/gui/mixins/file_mixin.py +445 -0
  59. setiastro/saspro/gui/mixins/geometry_mixin.py +403 -0
  60. setiastro/saspro/gui/mixins/header_mixin.py +441 -0
  61. setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
  62. setiastro/saspro/gui/mixins/menu_mixin.py +361 -0
  63. setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
  64. setiastro/saspro/gui/mixins/toolbar_mixin.py +1324 -0
  65. setiastro/saspro/gui/mixins/update_mixin.py +309 -0
  66. setiastro/saspro/gui/mixins/view_mixin.py +435 -0
  67. setiastro/saspro/halobgon.py +462 -0
  68. setiastro/saspro/header_viewer.py +445 -0
  69. setiastro/saspro/headless_utils.py +88 -0
  70. setiastro/saspro/histogram.py +753 -0
  71. setiastro/saspro/history_explorer.py +939 -0
  72. setiastro/saspro/image_combine.py +414 -0
  73. setiastro/saspro/image_peeker_pro.py +1596 -0
  74. setiastro/saspro/imageops/__init__.py +37 -0
  75. setiastro/saspro/imageops/mdi_snap.py +292 -0
  76. setiastro/saspro/imageops/scnr.py +36 -0
  77. setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
  78. setiastro/saspro/imageops/stretch.py +244 -0
  79. setiastro/saspro/isophote.py +1179 -0
  80. setiastro/saspro/layers.py +208 -0
  81. setiastro/saspro/layers_dock.py +714 -0
  82. setiastro/saspro/lazy_imports.py +193 -0
  83. setiastro/saspro/legacy/__init__.py +2 -0
  84. setiastro/saspro/legacy/image_manager.py +2226 -0
  85. setiastro/saspro/legacy/numba_utils.py +3659 -0
  86. setiastro/saspro/legacy/xisf.py +1071 -0
  87. setiastro/saspro/linear_fit.py +534 -0
  88. setiastro/saspro/live_stacking.py +1830 -0
  89. setiastro/saspro/log_bus.py +5 -0
  90. setiastro/saspro/logging_config.py +460 -0
  91. setiastro/saspro/luminancerecombine.py +309 -0
  92. setiastro/saspro/main_helpers.py +201 -0
  93. setiastro/saspro/mask_creation.py +928 -0
  94. setiastro/saspro/masks_core.py +56 -0
  95. setiastro/saspro/mdi_widgets.py +353 -0
  96. setiastro/saspro/memory_utils.py +666 -0
  97. setiastro/saspro/metadata_patcher.py +75 -0
  98. setiastro/saspro/mfdeconv.py +3826 -0
  99. setiastro/saspro/mfdeconv_earlystop.py +71 -0
  100. setiastro/saspro/mfdeconvcudnn.py +3263 -0
  101. setiastro/saspro/mfdeconvsport.py +2382 -0
  102. setiastro/saspro/minorbodycatalog.py +567 -0
  103. setiastro/saspro/morphology.py +382 -0
  104. setiastro/saspro/multiscale_decomp.py +1290 -0
  105. setiastro/saspro/nbtorgb_stars.py +531 -0
  106. setiastro/saspro/numba_utils.py +3044 -0
  107. setiastro/saspro/numba_warmup.py +141 -0
  108. setiastro/saspro/ops/__init__.py +9 -0
  109. setiastro/saspro/ops/command_help_dialog.py +623 -0
  110. setiastro/saspro/ops/command_runner.py +217 -0
  111. setiastro/saspro/ops/commands.py +1594 -0
  112. setiastro/saspro/ops/script_editor.py +1102 -0
  113. setiastro/saspro/ops/scripts.py +1413 -0
  114. setiastro/saspro/ops/settings.py +560 -0
  115. setiastro/saspro/parallel_utils.py +554 -0
  116. setiastro/saspro/pedestal.py +121 -0
  117. setiastro/saspro/perfect_palette_picker.py +1053 -0
  118. setiastro/saspro/pipeline.py +110 -0
  119. setiastro/saspro/pixelmath.py +1600 -0
  120. setiastro/saspro/plate_solver.py +2435 -0
  121. setiastro/saspro/project_io.py +797 -0
  122. setiastro/saspro/psf_utils.py +136 -0
  123. setiastro/saspro/psf_viewer.py +549 -0
  124. setiastro/saspro/pyi_rthook_astroquery.py +95 -0
  125. setiastro/saspro/remove_green.py +314 -0
  126. setiastro/saspro/remove_stars.py +1625 -0
  127. setiastro/saspro/remove_stars_preset.py +404 -0
  128. setiastro/saspro/resources.py +472 -0
  129. setiastro/saspro/rgb_combination.py +207 -0
  130. setiastro/saspro/rgb_extract.py +19 -0
  131. setiastro/saspro/rgbalign.py +723 -0
  132. setiastro/saspro/runtime_imports.py +7 -0
  133. setiastro/saspro/runtime_torch.py +754 -0
  134. setiastro/saspro/save_options.py +72 -0
  135. setiastro/saspro/selective_color.py +1552 -0
  136. setiastro/saspro/sfcc.py +1425 -0
  137. setiastro/saspro/shortcuts.py +2807 -0
  138. setiastro/saspro/signature_insert.py +1099 -0
  139. setiastro/saspro/stacking_suite.py +17712 -0
  140. setiastro/saspro/star_alignment.py +7420 -0
  141. setiastro/saspro/star_alignment_preset.py +329 -0
  142. setiastro/saspro/star_metrics.py +49 -0
  143. setiastro/saspro/star_spikes.py +681 -0
  144. setiastro/saspro/star_stretch.py +470 -0
  145. setiastro/saspro/stat_stretch.py +502 -0
  146. setiastro/saspro/status_log_dock.py +78 -0
  147. setiastro/saspro/subwindow.py +3267 -0
  148. setiastro/saspro/supernovaasteroidhunter.py +1712 -0
  149. setiastro/saspro/swap_manager.py +99 -0
  150. setiastro/saspro/torch_backend.py +89 -0
  151. setiastro/saspro/torch_rejection.py +434 -0
  152. setiastro/saspro/view_bundle.py +1555 -0
  153. setiastro/saspro/wavescale_hdr.py +624 -0
  154. setiastro/saspro/wavescale_hdr_preset.py +100 -0
  155. setiastro/saspro/wavescalede.py +657 -0
  156. setiastro/saspro/wavescalede_preset.py +228 -0
  157. setiastro/saspro/wcs_update.py +374 -0
  158. setiastro/saspro/whitebalance.py +456 -0
  159. setiastro/saspro/widgets/__init__.py +48 -0
  160. setiastro/saspro/widgets/common_utilities.py +305 -0
  161. setiastro/saspro/widgets/graphics_views.py +122 -0
  162. setiastro/saspro/widgets/image_utils.py +518 -0
  163. setiastro/saspro/widgets/preview_dialogs.py +280 -0
  164. setiastro/saspro/widgets/spinboxes.py +275 -0
  165. setiastro/saspro/widgets/themed_buttons.py +13 -0
  166. setiastro/saspro/widgets/wavelet_utils.py +299 -0
  167. setiastro/saspro/window_shelf.py +185 -0
  168. setiastro/saspro/xisf.py +1123 -0
  169. setiastrosuitepro-1.6.0.dist-info/METADATA +266 -0
  170. setiastrosuitepro-1.6.0.dist-info/RECORD +174 -0
  171. setiastrosuitepro-1.6.0.dist-info/WHEEL +4 -0
  172. setiastrosuitepro-1.6.0.dist-info/entry_points.txt +6 -0
  173. setiastrosuitepro-1.6.0.dist-info/licenses/LICENSE +674 -0
  174. setiastrosuitepro-1.6.0.dist-info/licenses/license.txt +2580 -0
@@ -0,0 +1,560 @@
1
+ # ops.settings.py
2
+ from PyQt6.QtWidgets import (
3
+ QLineEdit, QDialogButtonBox, QFileDialog, QDialog, QPushButton, QFormLayout,QApplication,
4
+ QHBoxLayout, QVBoxLayout, QWidget, QCheckBox, QComboBox, QSpinBox, QDoubleSpinBox, QLabel, QColorDialog, QFontDialog, QSlider)
5
+ from PyQt6.QtCore import QSettings, Qt
6
+ import pytz # for timezone list
7
+
8
+
9
+ class SettingsDialog(QDialog):
10
+ """
11
+ Simple settings UI for external executable paths + WIMS defaults.
12
+ Values are persisted via the provided QSettings instance.
13
+ """
14
+ def __init__(self, parent, settings: QSettings):
15
+ super().__init__(parent)
16
+ self.setWindowTitle("Preferences")
17
+ self.settings = settings
18
+
19
+ # ---- Existing fields (paths, checkboxes, etc.) ----
20
+ self.le_graxpert = QLineEdit()
21
+ self.le_cosmic = QLineEdit()
22
+ self.le_starnet = QLineEdit()
23
+ self.le_astap = QLineEdit()
24
+
25
+ self.chk_updates_startup = QCheckBox("Check for updates on startup")
26
+ self.chk_updates_startup.setChecked(
27
+ self.settings.value("updates/check_on_startup", True, type=bool)
28
+ )
29
+
30
+ self.le_updates_url = QLineEdit()
31
+ self.le_updates_url.setPlaceholderText("Raw JSON URL (advanced)")
32
+ self.le_updates_url.setText(
33
+ self.settings.value(
34
+ "updates/url",
35
+ "https://raw.githubusercontent.com/setiastro/setiastrosuitepro/main/updates.json",
36
+ type=str
37
+ )
38
+ )
39
+
40
+ btn_reset_updates_url = QPushButton("Reset")
41
+ btn_reset_updates_url.setToolTip("Restore default updates URL")
42
+ btn_reset_updates_url.clicked.connect(
43
+ lambda: self.le_updates_url.setText(
44
+ "https://raw.githubusercontent.com/setiastro/setiastrosuitepro/main/updates.json"
45
+ )
46
+ )
47
+
48
+ # Optional: “Check Now…” button
49
+ self.btn_check_now = QPushButton("Check Now…")
50
+ self.btn_check_now.setToolTip("Run an update check immediately")
51
+ self.btn_check_now.setVisible(hasattr(parent, "_check_for_updates_async"))
52
+ self.btn_check_now.clicked.connect(self._check_updates_now_clicked)
53
+
54
+ # Build the updates URL row ONCE (we'll insert it later on the right column)
55
+ row_updates_url = QHBoxLayout()
56
+ row_updates_url.addWidget(self.le_updates_url, 1)
57
+ row_updates_url.addWidget(btn_reset_updates_url)
58
+ row_updates_url.addWidget(self.btn_check_now)
59
+
60
+ self.chk_save_shortcuts = QCheckBox("Save desktop shortcuts on exit")
61
+ self.chk_save_shortcuts.setChecked(
62
+ self.settings.value("shortcuts/save_on_exit", True, type=bool)
63
+ )
64
+
65
+ self.cb_theme = QComboBox()
66
+ # Order: Dark, Gray, Light, System, Custom
67
+ self.cb_theme.addItems(["Dark", "Gray", "Light", "System", "Custom"])
68
+
69
+ theme_val = (self.settings.value("ui/theme", "system", type=str) or "system").lower()
70
+ index_map = {"dark": 0, "gray": 1, "light": 2, "system": 3, "custom": 4}
71
+ self.cb_theme.setCurrentIndex(index_map.get(theme_val, 2))
72
+
73
+ # "Customize…" button for custom theme
74
+ self.btn_theme_custom = QPushButton("Customize…")
75
+ self.btn_theme_custom.setToolTip("Edit custom colors and font")
76
+ self.btn_theme_custom.setEnabled(theme_val == "custom")
77
+ self.btn_theme_custom.clicked.connect(self._open_theme_editor)
78
+
79
+ # Keep button enabled state in sync with combo
80
+ self.cb_theme.currentIndexChanged.connect(self._on_theme_changed)
81
+
82
+ self.le_graxpert.setText(self.settings.value("paths/graxpert", "", type=str))
83
+ self.le_cosmic.setText(self.settings.value("paths/cosmic_clarity", "", type=str))
84
+ self.le_starnet.setText(self.settings.value("paths/starnet", "", type=str))
85
+ self.le_astap.setText(self.settings.value("paths/astap", "", type=str))
86
+
87
+ btn_grax = QPushButton("Browse…"); btn_grax.clicked.connect(lambda: self._browse_into(self.le_graxpert))
88
+ btn_ccl = QPushButton("Browse…"); btn_ccl.clicked.connect(lambda: self._browse_dir(self.le_cosmic))
89
+ btn_star = QPushButton("Browse…"); btn_star.clicked.connect(lambda: self._browse_into(self.le_starnet))
90
+ btn_astap = QPushButton("Browse…"); btn_astap.clicked.connect(lambda: self._browse_into(self.le_astap))
91
+
92
+ row_grax = QHBoxLayout(); row_grax.addWidget(self.le_graxpert); row_grax.addWidget(btn_grax)
93
+ row_ccl = QHBoxLayout(); row_ccl.addWidget(self.le_cosmic); row_ccl.addWidget(btn_ccl)
94
+ row_star = QHBoxLayout(); row_star.addWidget(self.le_starnet); row_star.addWidget(btn_star)
95
+ row_astap = QHBoxLayout(); row_astap.addWidget(self.le_astap); row_astap.addWidget(btn_astap)
96
+
97
+ self.le_astrometry = QLineEdit()
98
+ self.le_astrometry.setEchoMode(QLineEdit.EchoMode.Password)
99
+ self.le_astrometry.setText(self.settings.value("api/astrometry_key", "", type=str))
100
+
101
+ # ---- WIMS defaults ----
102
+ self.sp_lat = QDoubleSpinBox(); self.sp_lat.setRange(-90.0, 90.0); self.sp_lat.setDecimals(6)
103
+ self.sp_lon = QDoubleSpinBox(); self.sp_lon.setRange(-180.0, 180.0); self.sp_lon.setDecimals(6)
104
+ self.le_date = QLineEdit() # YYYY-MM-DD
105
+ self.le_time = QLineEdit() # HH:MM
106
+ self.cb_tz = QComboBox(); self.cb_tz.addItems(pytz.all_timezones)
107
+ self.sp_min_alt = QDoubleSpinBox(); self.sp_min_alt.setRange(0.0, 90.0); self.sp_min_alt.setDecimals(1)
108
+ self.sp_obj_limit = QSpinBox(); self.sp_obj_limit.setRange(1, 1000)
109
+
110
+ self.sp_lat.setValue(self.settings.value("latitude", 0.0, type=float))
111
+ self.sp_lon.setValue(self.settings.value("longitude", 0.0, type=float))
112
+ self.le_date.setText(self.settings.value("date", "", type=str) or "")
113
+ self.le_time.setText(self.settings.value("time", "", type=str) or "")
114
+ tz_val = self.settings.value("timezone", "UTC", type=str) or "UTC"
115
+ idx = max(0, self.cb_tz.findText(tz_val)); self.cb_tz.setCurrentIndex(idx)
116
+ self.sp_min_alt.setValue(self.settings.value("min_altitude", 0.0, type=float))
117
+ self.sp_obj_limit.setValue(self.settings.value("object_limit", 100, type=int))
118
+
119
+ self.chk_autostretch_16bit = QCheckBox("High-quality autostretch (16-bit; better gradients)")
120
+ self.chk_autostretch_16bit.setToolTip("Compute autostretch on a 16-bit histogram (smoother gradients).")
121
+ self.chk_autostretch_16bit.setChecked(
122
+ self.settings.value("display/autostretch_16bit", True, type=bool)
123
+ )
124
+
125
+ self.slider_bg_opacity = QSlider(Qt.Orientation.Horizontal)
126
+ self.slider_bg_opacity.setRange(0, 100)
127
+ current_opacity = self.settings.value("display/bg_opacity", 50, type=int)
128
+ self.slider_bg_opacity.setValue(current_opacity)
129
+ # Keep a copy of the original opacity so we can restore it if the user cancels
130
+ self._initial_bg_opacity = int(current_opacity)
131
+
132
+ self.lbl_bg_opacity_val = QLabel(f"{current_opacity}%")
133
+ self.lbl_bg_opacity_val.setFixedWidth(40)
134
+
135
+ def _on_opacity_changed(val):
136
+ self.lbl_bg_opacity_val.setText(f"{val}%")
137
+ # Aggiorna in tempo reale il valore nei settings
138
+ self.settings.setValue("display/bg_opacity", val)
139
+ self.settings.sync()
140
+ # Richiedi al parent (main window) di aggiornare il rendering della MDI
141
+ parent = self.parent()
142
+ if parent and hasattr(parent, "mdi") and hasattr(parent.mdi, "viewport"):
143
+ parent.mdi.viewport().update()
144
+
145
+ self.slider_bg_opacity.valueChanged.connect(_on_opacity_changed)
146
+
147
+ row_bg_opacity = QHBoxLayout()
148
+ row_bg_opacity.addWidget(self.slider_bg_opacity)
149
+ row_bg_opacity.addWidget(self.lbl_bg_opacity_val)
150
+ w_bg_opacity = QWidget()
151
+ w_bg_opacity.setLayout(row_bg_opacity)
152
+
153
+ # ---- Custom background: choose/clear preview ----
154
+ self.le_bg_path = QLineEdit()
155
+ self.le_bg_path.setReadOnly(True)
156
+ # remember initial custom background so Cancel can restore it
157
+ self._initial_bg_path = self.settings.value("ui/custom_background", "", type=str) or ""
158
+ self.le_bg_path.setText(self._initial_bg_path)
159
+ btn_choose_bg = QPushButton("Choose Background…")
160
+ btn_choose_bg.setToolTip("Pick a PNG or JPG to use as the application background")
161
+ btn_choose_bg.clicked.connect(self._choose_background_clicked)
162
+ btn_clear_bg = QPushButton("Clear")
163
+ btn_clear_bg.setToolTip("Remove custom background and restore default")
164
+ btn_clear_bg.clicked.connect(self._clear_background_clicked)
165
+
166
+ row_bg_image = QHBoxLayout()
167
+ row_bg_image.addWidget(self.le_bg_path, 1)
168
+ row_bg_image.addWidget(btn_choose_bg)
169
+ row_bg_image.addWidget(btn_clear_bg)
170
+ w_bg_image = QWidget()
171
+ w_bg_image.setLayout(row_bg_image)
172
+
173
+ # ─────────────────────────────────────────────────────────────────────
174
+ # LAYOUT MUST EXIST BEFORE ANY addRow(...) — build it here
175
+ # ─────────────────────────────────────────────────────────────────────
176
+ root = QVBoxLayout(self)
177
+ cols = QHBoxLayout(); root.addLayout(cols)
178
+
179
+ left_col = QFormLayout()
180
+ right_col = QFormLayout()
181
+ for f in (left_col, right_col):
182
+ f.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
183
+ f.setRowWrapPolicy(QFormLayout.RowWrapPolicy.DontWrapRows)
184
+ f.setFormAlignment(Qt.AlignmentFlag.AlignTop)
185
+
186
+ cols.addLayout(left_col, 1)
187
+ cols.addSpacing(16)
188
+ cols.addLayout(right_col, 1)
189
+
190
+ # ---- Left column: Paths & Integrations ----
191
+ left_col.addRow(QLabel("<b>Paths & Integrations</b>"))
192
+ w = QWidget(); w.setLayout(row_grax); left_col.addRow("GraXpert executable:", w)
193
+ w = QWidget(); w.setLayout(row_ccl); left_col.addRow("Cosmic Clarity folder:", w)
194
+ w = QWidget(); w.setLayout(row_star); left_col.addRow("StarNet executable:", w)
195
+ w = QWidget(); w.setLayout(row_astap); left_col.addRow("ASTAP executable:", w)
196
+ left_col.addRow("Astrometry.net API key:", self.le_astrometry)
197
+ left_col.addRow(self.chk_save_shortcuts)
198
+ row_theme = QHBoxLayout()
199
+ row_theme.addWidget(self.cb_theme, 1)
200
+ row_theme.addWidget(self.btn_theme_custom)
201
+ w_theme = QWidget()
202
+ w_theme.setLayout(row_theme)
203
+ left_col.addRow("Theme:", w_theme)
204
+
205
+ # ---- Display (moved under Theme) ----
206
+ left_col.addRow(QLabel("<b>Display</b>"))
207
+ left_col.addRow(self.chk_autostretch_16bit)
208
+ left_col.addRow("Background Opacity:", w_bg_opacity)
209
+ left_col.addRow("Background Image:", w_bg_image)
210
+
211
+ # ---- Right column: WIMS + RA/Dec + Updates + Display ----
212
+ right_col.addRow(QLabel("<b>What's In My Sky — Defaults</b>"))
213
+ right_col.addRow("Latitude (°):", self.sp_lat)
214
+ right_col.addRow("Longitude (°):", self.sp_lon)
215
+ right_col.addRow("Date (YYYY-MM-DD):", self.le_date)
216
+ right_col.addRow("Time (HH:MM):", self.le_time)
217
+ right_col.addRow("Time Zone:", self.cb_tz)
218
+ right_col.addRow("Min Altitude (°):", self.sp_min_alt)
219
+ right_col.addRow("Object Limit:", self.sp_obj_limit)
220
+
221
+ # ---- RA/Dec Overlay ----
222
+ right_col.addRow(QLabel("<b>RA/Dec Overlay</b>"))
223
+ self.chk_wcs_enabled = QCheckBox("Show RA/Dec grid")
224
+ self.chk_wcs_enabled.setChecked(self.settings.value("wcs_grid/enabled", True, type=bool))
225
+ right_col.addRow(self.chk_wcs_enabled)
226
+
227
+ self.cb_wcs_mode = QComboBox(); self.cb_wcs_mode.addItems(["Auto", "Fixed spacing"])
228
+ self.cb_wcs_mode.setCurrentIndex(
229
+ 0 if (self.settings.value("wcs_grid/mode", "auto", type=str) == "auto") else 1
230
+ )
231
+
232
+ self.cb_wcs_unit = QComboBox(); self.cb_wcs_unit.addItems(["deg", "arcmin"])
233
+ self.cb_wcs_unit.setCurrentIndex(
234
+ 0 if (self.settings.value("wcs_grid/step_unit", "deg", type=str) == "deg") else 1
235
+ )
236
+
237
+ self.sp_wcs_step = QDoubleSpinBox()
238
+ self.sp_wcs_step.setDecimals(3); self.sp_wcs_step.setRange(0.001, 90.0)
239
+ self.sp_wcs_step.setValue(self.settings.value("wcs_grid/step_value", 1.0, type=float))
240
+ self.sp_wcs_step.setEnabled(self.cb_wcs_mode.currentIndex() == 1)
241
+
242
+ def _sync_suffix():
243
+ self.sp_wcs_step.setSuffix(" °" if self.cb_wcs_unit.currentIndex() == 0 else " arcmin")
244
+ _sync_suffix()
245
+ self.cb_wcs_unit.currentIndexChanged.connect(_sync_suffix)
246
+ self.cb_wcs_mode.currentIndexChanged.connect(lambda i: self.sp_wcs_step.setEnabled(i == 1))
247
+
248
+ row_wcs = QHBoxLayout()
249
+ row_wcs.addWidget(QLabel("Mode:")); row_wcs.addWidget(self.cb_wcs_mode)
250
+ row_wcs.addSpacing(8)
251
+ row_wcs.addWidget(QLabel("Step:")); row_wcs.addWidget(self.sp_wcs_step, 1); row_wcs.addWidget(self.cb_wcs_unit)
252
+ _w = QWidget(); _w.setLayout(row_wcs)
253
+ right_col.addRow(_w)
254
+
255
+ # ---- Updates ----
256
+ right_col.addRow(QLabel("<b>Updates</b>"))
257
+ right_col.addRow(self.chk_updates_startup)
258
+ w = QWidget(); w.setLayout(row_updates_url)
259
+ right_col.addRow("Updates JSON URL:", w)
260
+
261
+
262
+
263
+ # ---- Buttons ----
264
+ btns = QDialogButtonBox(
265
+ QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, parent=self
266
+ )
267
+ btns.accepted.connect(self._save_and_accept)
268
+ btns.rejected.connect(self.reject)
269
+ root.addWidget(btns)
270
+
271
+ def reject(self):
272
+ """User cancelled: restore the original background opacity (revert live changes)."""
273
+ try:
274
+ # Restore saved original value
275
+ self.settings.setValue("display/bg_opacity", int(self._initial_bg_opacity))
276
+ self.settings.sync()
277
+ # Ask parent to redraw with restored value
278
+ parent = self.parent()
279
+ if parent:
280
+ # restore original custom background (may be empty)
281
+ try:
282
+ # If there was an initial custom background, restore it; otherwise clear.
283
+ if self._initial_bg_path:
284
+ if hasattr(parent, "_apply_custom_background"):
285
+ parent._apply_custom_background(self._initial_bg_path)
286
+ else:
287
+ # Avoid calling _apply_custom_background("") which shows a warning
288
+ if hasattr(parent, "_clear_custom_background"):
289
+ parent._clear_custom_background()
290
+ elif hasattr(parent, "_apply_custom_background"):
291
+ parent._apply_custom_background("")
292
+ except Exception:
293
+ pass
294
+ # update MDI viewport/redraw
295
+ try:
296
+ if hasattr(parent, "mdi") and hasattr(parent.mdi, "viewport"):
297
+ parent.mdi.viewport().update()
298
+ except Exception:
299
+ pass
300
+ except Exception:
301
+ pass
302
+ super().reject()
303
+
304
+
305
+ # ----------------- helpers -----------------
306
+ def _browse_into(self, lineedit: QLineEdit):
307
+ path, _ = QFileDialog.getOpenFileName(self, "Select Executable", "", "Executables (*)")
308
+ if path:
309
+ lineedit.setText(path)
310
+
311
+ def _browse_dir(self, lineedit: QLineEdit):
312
+ path = QFileDialog.getExistingDirectory(self, "Select Folder", "")
313
+ if path:
314
+ lineedit.setText(path)
315
+
316
+ def _check_updates_now_clicked(self):
317
+ """Persist update settings, then ask the main window to run an interactive check (if available)."""
318
+ self.settings.setValue("updates/check_on_startup", self.chk_updates_startup.isChecked())
319
+ self.settings.setValue("updates/url", self.le_updates_url.text().strip())
320
+ self.settings.sync()
321
+
322
+ parent = self.parent()
323
+ if parent and hasattr(parent, "_check_for_updates_async"):
324
+ try:
325
+ parent._check_for_updates_async(interactive=True)
326
+ except Exception:
327
+ pass
328
+
329
+ def _choose_background_clicked(self):
330
+ """Open a file picker and apply a custom background image for the app."""
331
+ path, _ = QFileDialog.getOpenFileName(self, "Select background image", "", "Images (*.png *.jpg *.jpeg)")
332
+ if not path:
333
+ return
334
+ try:
335
+ # Do NOT persist yet — just update UI and preview via parent.
336
+ self.le_bg_path.setText(path)
337
+ parent = self.parent()
338
+ if parent and hasattr(parent, "_apply_custom_background"):
339
+ try:
340
+ parent._apply_custom_background(path)
341
+ except Exception:
342
+ pass
343
+ except Exception:
344
+ pass
345
+
346
+ def _clear_background_clicked(self):
347
+ """Clear persisted custom background and ask main window to restore defaults."""
348
+ try:
349
+ # Do NOT modify settings yet — clear preview and let Save apply
350
+ self.le_bg_path.setText("")
351
+ parent = self.parent()
352
+ if parent:
353
+ # request parent to clear preview/background for now
354
+ try:
355
+ if hasattr(parent, "_clear_custom_background"):
356
+ parent._clear_custom_background()
357
+ elif hasattr(parent, "_apply_custom_background"):
358
+ parent._apply_custom_background("")
359
+ except Exception:
360
+ pass
361
+ except Exception:
362
+ pass
363
+
364
+ def _on_theme_changed(self, idx: int):
365
+ # Enable the "Customize…" button only when Custom is selected
366
+ text = self.cb_theme.currentText().lower()
367
+ self.btn_theme_custom.setEnabled(text == "custom")
368
+
369
+ def _open_theme_editor(self):
370
+ from PyQt6.QtWidgets import QDialog
371
+ dlg = ThemeEditorDialog(self, self.settings)
372
+ if dlg.exec() == QDialog.DialogCode.Accepted:
373
+ # If user saved a custom theme, make sure "Custom" is selected
374
+ self.cb_theme.setCurrentIndex(4) # Custom
375
+
376
+
377
+ def _save_and_accept(self):
378
+ # Paths / Integrations
379
+ self.settings.setValue("paths/graxpert", self.le_graxpert.text().strip())
380
+ self.settings.setValue("paths/cosmic_clarity", self.le_cosmic.text().strip())
381
+ self.settings.setValue("paths/starnet", self.le_starnet.text().strip())
382
+ self.settings.setValue("paths/astap", self.le_astap.text().strip())
383
+ self.settings.setValue("shortcuts/save_on_exit", self.chk_save_shortcuts.isChecked())
384
+ self.settings.setValue("api/astrometry_key", self.le_astrometry.text().strip())
385
+
386
+ # WIMS defaults
387
+ self.settings.setValue("latitude", float(self.sp_lat.value()))
388
+ self.settings.setValue("longitude", float(self.sp_lon.value()))
389
+ self.settings.setValue("date", self.le_date.text().strip())
390
+ self.settings.setValue("time", self.le_time.text().strip())
391
+ self.settings.setValue("timezone", self.cb_tz.currentText())
392
+ self.settings.setValue("min_altitude", float(self.sp_min_alt.value()))
393
+ self.settings.setValue("object_limit", int(self.sp_obj_limit.value()))
394
+
395
+ # RA/Dec Overlay
396
+ self.settings.setValue("wcs_grid/enabled", self.chk_wcs_enabled.isChecked())
397
+ self.settings.setValue("wcs_grid/mode", "auto" if self.cb_wcs_mode.currentIndex() == 0 else "fixed")
398
+ self.settings.setValue("wcs_grid/step_unit", "deg" if self.cb_wcs_unit.currentIndex() == 0 else "arcmin")
399
+ self.settings.setValue("wcs_grid/step_value", float(self.sp_wcs_step.value()))
400
+
401
+
402
+ # Updates + Display
403
+ self.settings.setValue("updates/check_on_startup", self.chk_updates_startup.isChecked())
404
+ self.settings.setValue("updates/url", self.le_updates_url.text().strip())
405
+ self.settings.setValue("display/autostretch_16bit", self.chk_autostretch_16bit.isChecked())
406
+
407
+ # Custom background: persist the chosen path (empty -> remove)
408
+ bg_path = (self.le_bg_path.text() or "").strip()
409
+ if bg_path:
410
+ self.settings.setValue("ui/custom_background", bg_path)
411
+ else:
412
+ try:
413
+ self.settings.remove("ui/custom_background")
414
+ except Exception:
415
+ self.settings.setValue("ui/custom_background", "")
416
+
417
+ # bg_opacity is already saved in real-time by _on_opacity_changed()
418
+
419
+ # Theme
420
+ idx = max(0, self.cb_theme.currentIndex())
421
+ if idx == 0:
422
+ theme_val = "dark"
423
+ elif idx == 1:
424
+ theme_val = "gray"
425
+ elif idx == 2:
426
+ theme_val = "light"
427
+ elif idx == 3:
428
+ theme_val = "system"
429
+ else:
430
+ theme_val = "custom"
431
+ self.settings.setValue("ui/theme", theme_val)
432
+
433
+ self.settings.sync()
434
+
435
+ # Apply now if the parent knows how
436
+ p = self.parent()
437
+ if p and hasattr(p, "apply_theme_from_settings"):
438
+ try:
439
+ p.apply_theme_from_settings()
440
+ except Exception:
441
+ pass
442
+
443
+ if hasattr(p, "mdi") and hasattr(p.mdi, "viewport"):
444
+ p.mdi.viewport().update()
445
+
446
+ self.accept()
447
+
448
+ from PyQt6.QtGui import QColor, QFont
449
+
450
+
451
+ class ThemeEditorDialog(QDialog):
452
+ """
453
+ Simple "Custom Theme" editor: lets the user pick main colors and a UI font.
454
+ Colors are stored in QSettings as hex strings (e.g. '#404040').
455
+ """
456
+ def __init__(self, parent, settings: QSettings):
457
+ super().__init__(parent)
458
+ self.settings = settings
459
+ self.setWindowTitle("Custom Theme")
460
+ self.colors: dict[str, QColor] = {}
461
+ self.font_str: str = self.settings.value("ui/custom/font", "", type=str) or ""
462
+
463
+ form = QFormLayout(self)
464
+
465
+ # Helper: add color pickers for key roles
466
+ self._add_color_picker(form, "Window / Panels", "ui/custom/window", QColor(40, 40, 40))
467
+ self._add_color_picker(form, "Base (Editors)", "ui/custom/base", QColor(24, 24, 24))
468
+ self._add_color_picker(form, "Alternate Base", "ui/custom/altbase", QColor(32, 32, 32))
469
+ self._add_color_picker(form, "Text", "ui/custom/text", QColor(230, 230, 230))
470
+ self._add_color_picker(form, "Buttons", "ui/custom/button", QColor(40, 40, 40))
471
+ self._add_color_picker(form, "Highlight / Accent","ui/custom/highlight",QColor(30, 144, 255))
472
+ self._add_color_picker(form, "Link", "ui/custom/link", QColor(120, 170, 255))
473
+ self._add_color_picker(form, "Visited Link", "ui/custom/link_visited", QColor(180, 150, 255))
474
+
475
+ # Font picker
476
+ self.btn_font = QPushButton("Choose…")
477
+ self.btn_font.clicked.connect(self._pick_font)
478
+ form.addRow("UI Font:", self.btn_font)
479
+
480
+ # Buttons
481
+ btns = QDialogButtonBox(
482
+ QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel,
483
+ parent=self
484
+ )
485
+ btns.accepted.connect(self._save_and_accept)
486
+ btns.rejected.connect(self.reject)
487
+ form.addRow(btns)
488
+
489
+ # ---------- helpers ----------
490
+
491
+ def _add_color_picker(self, form: QFormLayout, label_text: str,
492
+ key: str, default: QColor):
493
+ # Load from settings or default
494
+ stored = self.settings.value(key, default.name(), type=str)
495
+ color = QColor(stored) if stored else default
496
+ self.colors[key] = color
497
+
498
+ btn = QPushButton(color.name())
499
+ btn.setMinimumWidth(90)
500
+ btn.setStyleSheet(f"background-color: {color.name()}; color: #ffffff;")
501
+ btn.clicked.connect(lambda _=False, k=key, b=btn: self._pick_color(k, b))
502
+
503
+ form.addRow(label_text + ":", btn)
504
+
505
+ def _pick_color(self, key: str, button: QPushButton):
506
+ initial = self.colors.get(key, QColor("#404040"))
507
+ col = QColorDialog.getColor(initial, self, "Select Color")
508
+ if col.isValid():
509
+ self.colors[key] = col
510
+ button.setText(col.name())
511
+ button.setStyleSheet(f"background-color: {col.name()}; color: #ffffff;")
512
+
513
+ from PyQt6.QtGui import QFont
514
+ from PyQt6.QtWidgets import QFontDialog
515
+
516
+ def _pick_font(self):
517
+ # Load previous font if we have one
518
+ base_str = self.settings.value("ui/custom_font", "", type=str)
519
+ base_font = QFont()
520
+ if base_str:
521
+ try:
522
+ base_font.fromString(base_str)
523
+ except Exception:
524
+ pass
525
+
526
+ # ✅ NOTE: (font, ok) — NOT (ok, font)
527
+ font, ok = QFontDialog.getFont(base_font, self, "Select UI Font")
528
+ if not ok:
529
+ return # user cancelled
530
+
531
+ # Store and update preview
532
+ self.font_str = font.toString()
533
+ self.settings.setValue("ui/custom_font", self.font_str)
534
+ self.settings.sync()
535
+
536
+ # If you have a label/button to show the chosen font:
537
+ try:
538
+ self.font_button.setText(f"{font.family()}, {font.pointSize()} pt")
539
+ except Exception:
540
+ pass
541
+
542
+ # Re-apply theme so the new font takes effect
543
+ parent = self.parent()
544
+ if parent and hasattr(parent, "apply_theme_from_settings"):
545
+ try:
546
+ parent.apply_theme_from_settings()
547
+ except Exception:
548
+ pass
549
+
550
+ def _save_and_accept(self):
551
+ # Persist colors
552
+ for key, col in self.colors.items():
553
+ self.settings.setValue(key, col.name())
554
+
555
+ # Persist font if chosen
556
+ if self.font_str:
557
+ self.settings.setValue("ui/custom/font", self.font_str)
558
+
559
+ self.settings.sync()
560
+ self.accept()