setiastrosuitepro 1.6.4__py3-none-any.whl → 1.6.12__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 (115) hide show
  1. setiastro/images/abeicon.svg +16 -0
  2. setiastro/images/acv_icon.png +0 -0
  3. setiastro/images/colorwheel.svg +97 -0
  4. setiastro/images/cosmic.svg +40 -0
  5. setiastro/images/cosmicsat.svg +24 -0
  6. setiastro/images/first_quarter.png +0 -0
  7. setiastro/images/full_moon.png +0 -0
  8. setiastro/images/graxpert.svg +19 -0
  9. setiastro/images/last_quarter.png +0 -0
  10. setiastro/images/linearfit.svg +32 -0
  11. setiastro/images/new_moon.png +0 -0
  12. setiastro/images/pixelmath.svg +42 -0
  13. setiastro/images/waning_crescent_1.png +0 -0
  14. setiastro/images/waning_crescent_2.png +0 -0
  15. setiastro/images/waning_crescent_3.png +0 -0
  16. setiastro/images/waning_crescent_4.png +0 -0
  17. setiastro/images/waning_crescent_5.png +0 -0
  18. setiastro/images/waning_gibbous_1.png +0 -0
  19. setiastro/images/waning_gibbous_2.png +0 -0
  20. setiastro/images/waning_gibbous_3.png +0 -0
  21. setiastro/images/waning_gibbous_4.png +0 -0
  22. setiastro/images/waning_gibbous_5.png +0 -0
  23. setiastro/images/waxing_crescent_1.png +0 -0
  24. setiastro/images/waxing_crescent_2.png +0 -0
  25. setiastro/images/waxing_crescent_3.png +0 -0
  26. setiastro/images/waxing_crescent_4.png +0 -0
  27. setiastro/images/waxing_crescent_5.png +0 -0
  28. setiastro/images/waxing_gibbous_1.png +0 -0
  29. setiastro/images/waxing_gibbous_2.png +0 -0
  30. setiastro/images/waxing_gibbous_3.png +0 -0
  31. setiastro/images/waxing_gibbous_4.png +0 -0
  32. setiastro/images/waxing_gibbous_5.png +0 -0
  33. setiastro/qml/ResourceMonitor.qml +84 -82
  34. setiastro/saspro/__main__.py +20 -1
  35. setiastro/saspro/_generated/build_info.py +2 -2
  36. setiastro/saspro/abe.py +37 -4
  37. setiastro/saspro/aberration_ai.py +237 -21
  38. setiastro/saspro/acv_exporter.py +379 -0
  39. setiastro/saspro/add_stars.py +33 -6
  40. setiastro/saspro/backgroundneutral.py +108 -40
  41. setiastro/saspro/blemish_blaster.py +4 -1
  42. setiastro/saspro/blink_comparator_pro.py +74 -24
  43. setiastro/saspro/clahe.py +4 -1
  44. setiastro/saspro/continuum_subtract.py +4 -1
  45. setiastro/saspro/convo.py +13 -7
  46. setiastro/saspro/cosmicclarity.py +129 -18
  47. setiastro/saspro/crop_dialog_pro.py +123 -7
  48. setiastro/saspro/curve_editor_pro.py +109 -42
  49. setiastro/saspro/doc_manager.py +245 -15
  50. setiastro/saspro/exoplanet_detector.py +120 -28
  51. setiastro/saspro/frequency_separation.py +1158 -204
  52. setiastro/saspro/ghs_dialog_pro.py +81 -16
  53. setiastro/saspro/graxpert.py +1 -0
  54. setiastro/saspro/gui/main_window.py +429 -228
  55. setiastro/saspro/gui/mixins/dock_mixin.py +245 -24
  56. setiastro/saspro/gui/mixins/menu_mixin.py +27 -1
  57. setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
  58. setiastro/saspro/gui/mixins/toolbar_mixin.py +384 -18
  59. setiastro/saspro/gui/mixins/update_mixin.py +138 -36
  60. setiastro/saspro/gui/mixins/view_mixin.py +42 -0
  61. setiastro/saspro/halobgon.py +4 -0
  62. setiastro/saspro/histogram.py +5 -1
  63. setiastro/saspro/image_combine.py +4 -0
  64. setiastro/saspro/image_peeker_pro.py +4 -0
  65. setiastro/saspro/imageops/starbasedwhitebalance.py +23 -52
  66. setiastro/saspro/imageops/stretch.py +582 -62
  67. setiastro/saspro/isophote.py +4 -0
  68. setiastro/saspro/layers.py +13 -9
  69. setiastro/saspro/layers_dock.py +183 -3
  70. setiastro/saspro/legacy/image_manager.py +154 -20
  71. setiastro/saspro/legacy/numba_utils.py +67 -47
  72. setiastro/saspro/legacy/xisf.py +240 -98
  73. setiastro/saspro/live_stacking.py +180 -79
  74. setiastro/saspro/luminancerecombine.py +228 -27
  75. setiastro/saspro/mask_creation.py +174 -15
  76. setiastro/saspro/mfdeconv.py +113 -35
  77. setiastro/saspro/mfdeconvcudnn.py +119 -70
  78. setiastro/saspro/mfdeconvsport.py +112 -35
  79. setiastro/saspro/morphology.py +4 -0
  80. setiastro/saspro/multiscale_decomp.py +51 -12
  81. setiastro/saspro/numba_utils.py +72 -57
  82. setiastro/saspro/ops/commands.py +18 -18
  83. setiastro/saspro/ops/script_editor.py +10 -2
  84. setiastro/saspro/ops/scripts.py +122 -0
  85. setiastro/saspro/perfect_palette_picker.py +37 -3
  86. setiastro/saspro/plate_solver.py +84 -49
  87. setiastro/saspro/psf_viewer.py +119 -37
  88. setiastro/saspro/resources.py +67 -0
  89. setiastro/saspro/rgbalign.py +4 -0
  90. setiastro/saspro/selective_color.py +4 -1
  91. setiastro/saspro/sfcc.py +364 -152
  92. setiastro/saspro/shortcuts.py +160 -29
  93. setiastro/saspro/signature_insert.py +692 -33
  94. setiastro/saspro/stacking_suite.py +1331 -484
  95. setiastro/saspro/star_alignment.py +247 -123
  96. setiastro/saspro/star_spikes.py +4 -0
  97. setiastro/saspro/star_stretch.py +38 -3
  98. setiastro/saspro/stat_stretch.py +743 -128
  99. setiastro/saspro/subwindow.py +786 -360
  100. setiastro/saspro/supernovaasteroidhunter.py +1 -1
  101. setiastro/saspro/wavescale_hdr.py +4 -1
  102. setiastro/saspro/wavescalede.py +4 -1
  103. setiastro/saspro/whitebalance.py +84 -12
  104. setiastro/saspro/widgets/common_utilities.py +28 -21
  105. setiastro/saspro/widgets/resource_monitor.py +109 -59
  106. setiastro/saspro/widgets/spinboxes.py +10 -13
  107. setiastro/saspro/wimi.py +27 -656
  108. setiastro/saspro/wims.py +13 -3
  109. setiastro/saspro/xisf.py +101 -11
  110. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/METADATA +2 -1
  111. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/RECORD +115 -82
  112. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/WHEEL +0 -0
  113. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/entry_points.txt +0 -0
  114. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/LICENSE +0 -0
  115. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/license.txt +0 -0
@@ -116,16 +116,15 @@ def _status_popup_update(text: str):
116
116
  _STATUS_POPUP.update_text(text)
117
117
 
118
118
  def _status_popup_close():
119
- """Hide (but do not destroy) the singleton status popup if it exists."""
120
119
  global _STATUS_POPUP
120
+ if _STATUS_POPUP is None:
121
+ return
121
122
  try:
122
- if _STATUS_POPUP is not None:
123
- _STATUS_POPUP.hide()
124
- # keep instance for reuse (fast re-open)
123
+ _STATUS_POPUP.hide()
125
124
  except Exception:
126
- # Completely safe to ignore; worst case the popup was already gone.
127
125
  pass
128
126
 
127
+
129
128
  def _sleep_ui(ms: int):
130
129
  """Non-blocking sleep that keeps the UI responsive."""
131
130
  loop = QEventLoop()
@@ -137,54 +136,38 @@ def _with_events():
137
136
  QApplication.processEvents()
138
137
 
139
138
  def _set_status_ui(parent, text: str):
140
- """
141
- Update dialog/main-window status or batch log; if neither exists (headless),
142
- show/update a small modeless popup. Always pumps events for responsiveness.
143
- """
144
139
  try:
145
140
  updated_any = False
146
141
 
147
- def _do():
148
- nonlocal updated_any
149
- target = None
150
- # Dialog status label?
151
- if hasattr(parent, "status") and isinstance(getattr(parent, "status"), QLabel):
152
- target = parent.status
153
- # Named child fallback
154
- if target is None and hasattr(parent, "findChild"):
155
- target = parent.findChild(QLabel, "status_label")
156
- if target is not None:
157
- target.setText(text)
142
+ target = None
143
+ if hasattr(parent, "status") and isinstance(getattr(parent, "status"), QLabel):
144
+ target = parent.status
145
+ if target is None and hasattr(parent, "findChild"):
146
+ target = parent.findChild(QLabel, "status_label")
147
+ if target is not None:
148
+ target.setText(text)
149
+ updated_any = True
150
+
151
+ logw = getattr(parent, "log", None)
152
+ if logw and hasattr(logw, "append"):
153
+ tr_status = QCoreApplication.translate("PlateSolver", "Status:")
154
+ if text and (text.startswith("Status:") or text.startswith(tr_status) or text.startswith("▶") or text.startswith("✔") or text.startswith("❌")):
155
+ logw.append(text)
158
156
  updated_any = True
159
157
 
160
- # Batch log?
161
- logw = getattr(parent, "log", None)
162
- if logw and hasattr(logw, "append"):
163
- tr_status = QCoreApplication.translate("PlateSolver", "Status:")
164
- if text and (text.startswith("Status:") or text.startswith(tr_status) or text.startswith("▶") or text.startswith("✔") or text.startswith("❌")):
165
- logw.append(text)
166
- updated_any = True
167
-
168
- # If we couldn't update any inline widget, use the headless popup.
169
- if not updated_any:
170
- _status_popup_open(parent, text)
171
- else:
172
- # If inline widgets exist and popup is visible, keep it quiet.
173
- _status_popup_update(text)
174
-
175
- QApplication.processEvents()
176
-
177
- if isinstance(parent, QWidget):
178
- QTimer.singleShot(0, _do)
158
+ if not updated_any:
159
+ _status_popup_open(parent, text)
179
160
  else:
180
- _do()
161
+ _status_popup_update(text)
162
+
163
+ QApplication.processEvents()
181
164
  except Exception:
182
- # Last-resort popup if even the above failed
183
165
  try:
184
166
  _status_popup_open(parent, text)
185
167
  except Exception:
186
168
  pass
187
169
 
170
+
188
171
  def _wait_process(proc: QProcess, timeout_ms: int, parent=None) -> bool:
189
172
  """
190
173
  Incrementally wait for a QProcess while pumping UI events so the dialog stays responsive.
@@ -234,10 +217,53 @@ def _get_solvefield_exe(settings) -> str:
234
217
  return cand[0] # may be empty (used to decide web vs. local)
235
218
 
236
219
  def _get_astrometry_api_key(settings) -> str:
237
- return settings.value("astrometry/api_key", "", type=str) or ""
220
+ """
221
+ Canonical key: 'api/astrometry_key' (matches SettingsDialog).
222
+ Also check older legacy keys for backward compatibility.
223
+ """
224
+ if settings is None:
225
+ return ""
226
+
227
+ # ✅ canonical
228
+ key = settings.value("api/astrometry_key", "", type=str) or ""
229
+ key = key.strip()
230
+ if key:
231
+ return key
232
+
233
+ # 🔁 legacy fallbacks (if you ever stored them differently)
234
+ for k in (
235
+ "api/astrometry", # old guess
236
+ "astrometry/api_key",
237
+ "astrometry/key",
238
+ "astrometry_key",
239
+ "plate_solver/astrometry_key",
240
+ ):
241
+ v = settings.value(k, "", type=str) or ""
242
+ v = v.strip()
243
+ if v:
244
+ # migrate forward so it works next time
245
+ settings.setValue("api/astrometry_key", v)
246
+ try:
247
+ settings.remove(k)
248
+ except Exception:
249
+ pass
250
+ try:
251
+ settings.sync()
252
+ except Exception:
253
+ pass
254
+ return v
255
+
256
+ return ""
238
257
 
239
- def _set_astrometry_api_key(settings, key: str):
240
- settings.setValue("astrometry/api_key", key or "")
258
+
259
+ def _set_astrometry_api_key(settings, key: str) -> None:
260
+ if settings is None:
261
+ return
262
+ settings.setValue("api/astrometry_key", (key or "").strip())
263
+ try:
264
+ settings.sync()
265
+ except Exception:
266
+ pass
241
267
 
242
268
  def _wcs_header_from_astrometry_calib(calib: dict, image_shape: tuple[int, ...]) -> Header:
243
269
  """
@@ -1774,7 +1800,8 @@ def _debug_dump_meta(label: str, meta: dict):
1774
1800
  print(f" {k}: {type(v).__name__}")
1775
1801
  print("================================\n")
1776
1802
 
1777
-
1803
+ def tr(s: str) -> str:
1804
+ return QCoreApplication.translate("PlateSolver", s)
1778
1805
 
1779
1806
  def plate_solve_doc_inplace(parent, doc, settings) -> Tuple[bool, Header | str]:
1780
1807
  img = getattr(doc, "image", None)
@@ -1831,8 +1858,9 @@ def plate_solve_doc_inplace(parent, doc, settings) -> Tuple[bool, Header | str]:
1831
1858
  (hasattr(parent, "findChild") and parent.findChild(QLabel, "status_label") is not None)
1832
1859
  )
1833
1860
  if headless:
1834
- _status_popup_open(parent, QCoreApplication.translate("PlateSolver", "Status: Preparing plate solve…"))
1861
+ _status_popup_open(parent, tr("Status: Preparing plate solve…"))
1835
1862
 
1863
+ ok_solve = False
1836
1864
  try:
1837
1865
  ok, res = _solve_numpy_with_fallback(parent, settings, img, seed_h)
1838
1866
  if not ok:
@@ -1886,11 +1914,18 @@ def plate_solve_doc_inplace(parent, doc, settings) -> Tuple[bool, Header | str]:
1886
1914
  if hasattr(parent, "currentDocumentChanged"):
1887
1915
  QTimer.singleShot(0, lambda: parent.currentDocumentChanged.emit(doc))
1888
1916
 
1889
- _set_status_ui(parent, QCoreApplication.translate("PlateSolver", "Status: Plate solve completed."))
1890
- _status_popup_close()
1917
+ _set_status_ui(parent, tr("Status: Plate solve completed."))
1918
+
1919
+
1920
+ ok_solve = True
1921
+ if headless:
1922
+ QTimer.singleShot(1200, _status_popup_close)
1923
+ else:
1924
+ _status_popup_close()
1891
1925
  return True, hdr
1892
1926
  finally:
1893
- _status_popup_close()
1927
+ if not ok_solve:
1928
+ _status_popup_close()
1894
1929
 
1895
1930
 
1896
1931
 
@@ -13,7 +13,7 @@ from PyQt6.QtWidgets import (
13
13
  )
14
14
  from setiastro.saspro.widgets.themed_buttons import themed_toolbtn
15
15
 
16
- from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal, QObject
16
+ from PyQt6.QtCore import QThread, pyqtSignal, QObject
17
17
  from PyQt6.QtWidgets import QWidget
18
18
 
19
19
  class _ProcessingOverlay(QWidget):
@@ -108,7 +108,10 @@ class PSFViewer(QDialog):
108
108
  # Accept either a view (with .document) or a doc directly
109
109
  doc = getattr(view_or_doc, "document", None)
110
110
  self.doc = doc if doc is not None else view_or_doc
111
-
111
+ try:
112
+ self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
113
+ except Exception:
114
+ pass # older PyQt6 versions
112
115
  # Image + state
113
116
  self.image = self._grab_image()
114
117
  self.zoom_factor = 1.0
@@ -124,12 +127,19 @@ class PSFViewer(QDialog):
124
127
  self.threshold_timer.timeout.connect(self._applyThreshold)
125
128
 
126
129
  # Auto-update when the document changes
130
+
131
+ self._psf_thread = None
132
+ self._psf_worker = None
133
+ self._doc_conn = False
127
134
  if hasattr(self.doc, "changed"):
128
135
  try:
129
136
  self.doc.changed.connect(self._on_doc_changed)
137
+ self._doc_conn = True
130
138
  except Exception:
131
- pass
139
+ self._doc_conn = False
132
140
 
141
+ # cleanup no matter how the dialog is dismissed (accept/reject/done)
142
+ self.finished.connect(self._cleanup)
133
143
  self._build_ui()
134
144
  # Defer first compute until after the dialog is shown/layouted
135
145
  QTimer.singleShot(0, self._applyThreshold)
@@ -235,7 +245,7 @@ class PSFViewer(QDialog):
235
245
 
236
246
  # Close
237
247
  close_btn = QPushButton("Close", self)
238
- close_btn.clicked.connect(self.accept)
248
+ close_btn.clicked.connect(self.close)
239
249
  main_layout.addWidget(close_btn)
240
250
 
241
251
  self.setLayout(main_layout)
@@ -279,7 +289,6 @@ class PSFViewer(QDialog):
279
289
  self.hist_label.resize(scaled.size())
280
290
 
281
291
  def _applyThreshold(self):
282
- # kick off worker
283
292
  if self.image is None:
284
293
  self.star_list = None
285
294
  self.status_label.setText("Status: No image.")
@@ -288,46 +297,72 @@ class PSFViewer(QDialog):
288
297
 
289
298
  self._show_processing("Processing… extracting stars / PSFs")
290
299
 
291
- # kill previous run if any
292
- if hasattr(self, "_psf_thread") and self._psf_thread is not None:
293
- try:
294
- self._psf_thread.quit()
295
- self._psf_thread.wait(50)
296
- except Exception:
297
- pass
300
+ # stop any previous run cleanly
301
+ self._stop_psf_worker()
298
302
 
299
303
  self._psf_thread = QThread(self)
300
304
  self._psf_worker = _PSFWorker(self.image, self.detection_threshold)
301
305
  self._psf_worker.moveToThread(self._psf_thread)
302
306
 
303
307
  self._psf_thread.started.connect(self._psf_worker.run)
308
+ self._psf_worker.finished.connect(self._on_psf_done)
309
+ self._psf_worker.failed.connect(self._on_psf_fail)
304
310
 
305
- def _done(tbl, status):
306
- self.star_list = tbl
307
- self.status_label.setText(status)
308
- self._hide_processing()
309
- self.drawHistogram()
310
- self._psf_thread.quit()
311
- self._psf_thread.wait(100)
312
-
313
- def _fail(msg):
314
- self.star_list = None
315
- self.status_label.setText(f"Status: {msg}")
316
- self._hide_processing()
317
- self.drawHistogram()
318
- self._psf_thread.quit()
319
- self._psf_thread.wait(100)
311
+ # ensure thread quits once worker reports anything
312
+ self._psf_worker.finished.connect(lambda *_: self._stop_psf_worker(quit_only=False))
313
+ self._psf_worker.failed.connect(lambda *_: self._stop_psf_worker(quit_only=False))
320
314
 
321
- self._psf_worker.finished.connect(_done)
322
- self._psf_worker.failed.connect(_fail)
323
315
 
324
316
  self._psf_thread.start()
325
317
 
318
+ def _stop_psf_worker(self, quit_only: bool = False):
319
+ thr = getattr(self, "_psf_thread", None)
320
+ wkr = getattr(self, "_psf_worker", None)
321
+
322
+ if thr is None:
323
+ return
324
+
325
+ try:
326
+ thr.quit()
327
+ except Exception:
328
+ pass
329
+ try:
330
+ thr.wait(250)
331
+ except Exception:
332
+ pass
333
+
334
+ if not quit_only:
335
+ try:
336
+ if wkr is not None:
337
+ wkr.deleteLater()
338
+ except Exception:
339
+ pass
340
+ try:
341
+ thr.deleteLater()
342
+ except Exception:
343
+ pass
344
+ self._psf_worker = None
345
+ self._psf_thread = None
346
+
347
+ def _on_psf_done(self, tbl, status: str):
348
+ # tbl is an astropy Table or None
349
+ self.star_list = tbl
350
+ self.status_label.setText(status)
351
+ self._hide_processing()
352
+ self.drawHistogram()
353
+
354
+ def _on_psf_fail(self, msg: str):
355
+ self.star_list = None
356
+ self.status_label.setText(f"Status: {msg}")
357
+ self._hide_processing()
358
+ self.drawHistogram()
359
+
326
360
 
327
361
  def updateImage(self, new_image):
328
362
  self.image = np.asarray(new_image) if new_image is not None else None
329
- self.compute_star_list()
330
- self.drawHistogram()
363
+ if self.threshold_timer.isActive():
364
+ self.threshold_timer.stop()
365
+ self.threshold_timer.start()
331
366
 
332
367
  def updateZoom(self, _=None):
333
368
  self._apply_hist_zoom()
@@ -538,12 +573,59 @@ class PSFViewer(QDialog):
538
573
  it.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
539
574
  self.stats_table.setItem(ri, ci, it)
540
575
 
576
+
577
+ def _cleanup(self):
578
+ # stop debounce timer
579
+ try:
580
+ if getattr(self, "threshold_timer", None) is not None:
581
+ self.threshold_timer.stop()
582
+ except Exception:
583
+ pass
584
+
585
+ # disconnect doc listener
586
+ try:
587
+ if self._doc_conn and hasattr(self.doc, "changed"):
588
+ self.doc.changed.disconnect(self._on_doc_changed)
589
+ except Exception:
590
+ pass
591
+ self._doc_conn = False
592
+
593
+ # stop worker/thread
594
+ try:
595
+ thr = getattr(self, "_psf_thread", None)
596
+ wkr = getattr(self, "_psf_worker", None)
597
+
598
+ if wkr is not None:
599
+ try:
600
+ wkr.deleteLater()
601
+ except Exception:
602
+ pass
603
+
604
+ if thr is not None:
605
+ try:
606
+ thr.requestInterruption()
607
+ except Exception:
608
+ pass
609
+ try:
610
+ thr.quit()
611
+ except Exception:
612
+ pass
613
+ try:
614
+ thr.wait(250)
615
+ except Exception:
616
+ pass
617
+ try:
618
+ thr.deleteLater()
619
+ except Exception:
620
+ pass
621
+ except Exception:
622
+ pass
623
+
624
+ self._psf_worker = None
625
+ self._psf_thread = None
626
+
541
627
  # ---------- lifecycle ----------
542
628
  def closeEvent(self, e):
543
- # Best-effort disconnect
544
- if hasattr(self.doc, "changed"):
545
- try:
546
- self.doc.changed.disconnect(self._on_doc_changed)
547
- except Exception:
548
- pass
629
+ self._cleanup()
549
630
  super().closeEvent(e)
631
+
@@ -245,6 +245,39 @@ class Icons:
245
245
  LIVE_STACKING = property(lambda self: _resource_path('livestacking.png'))
246
246
  IMAGE_COMBINE = property(lambda self: _resource_path('imagecombine.png'))
247
247
 
248
+ # Moon phase (WIMS)
249
+ MOON_NEW = property(lambda self: _resource_path('new_moon.png'))
250
+ MOON_WAXING_CRES_1 = property(lambda self: _resource_path('waxing_crescent_1.png'))
251
+ MOON_WAXING_CRES_2 = property(lambda self: _resource_path('waxing_crescent_2.png'))
252
+ MOON_WAXING_CRES_3 = property(lambda self: _resource_path('waxing_crescent_3.png'))
253
+ MOON_WAXING_CRES_4 = property(lambda self: _resource_path('waxing_crescent_4.png'))
254
+ MOON_WAXING_CRES_5 = property(lambda self: _resource_path('waxing_crescent_5.png'))
255
+
256
+ MOON_FIRST_QUARTER = property(lambda self: _resource_path('first_quarter.png'))
257
+
258
+ MOON_WAXING_GIB_1 = property(lambda self: _resource_path('waxing_gibbous_1.png'))
259
+ MOON_WAXING_GIB_2 = property(lambda self: _resource_path('waxing_gibbous_2.png'))
260
+ MOON_WAXING_GIB_3 = property(lambda self: _resource_path('waxing_gibbous_3.png'))
261
+ MOON_WAXING_GIB_4 = property(lambda self: _resource_path('waxing_gibbous_4.png'))
262
+ MOON_WAXING_GIB_5 = property(lambda self: _resource_path('waxing_gibbous_5.png'))
263
+
264
+ MOON_FULL = property(lambda self: _resource_path('full_moon.png'))
265
+
266
+ MOON_WANING_GIB_1 = property(lambda self: _resource_path('waning_gibbous_1.png'))
267
+ MOON_WANING_GIB_2 = property(lambda self: _resource_path('waning_gibbous_2.png'))
268
+ MOON_WANING_GIB_3 = property(lambda self: _resource_path('waning_gibbous_3.png'))
269
+ MOON_WANING_GIB_4 = property(lambda self: _resource_path('waning_gibbous_4.png'))
270
+ MOON_WANING_GIB_5 = property(lambda self: _resource_path('waning_gibbous_5.png'))
271
+
272
+ MOON_LAST_QUARTER = property(lambda self: _resource_path('last_quarter.png'))
273
+
274
+ MOON_WANING_CRES_1 = property(lambda self: _resource_path('waning_crescent_1.png'))
275
+ MOON_WANING_CRES_2 = property(lambda self: _resource_path('waning_crescent_2.png'))
276
+ MOON_WANING_CRES_3 = property(lambda self: _resource_path('waning_crescent_3.png'))
277
+ MOON_WANING_CRES_4 = property(lambda self: _resource_path('waning_crescent_4.png'))
278
+ MOON_WANING_CRES_5 = property(lambda self: _resource_path('waning_crescent_5.png'))
279
+
280
+
248
281
  # Special features
249
282
  SUPERNOVA = property(lambda self: _resource_path('supernova.png'))
250
283
  PEDESTAL = property(lambda self: _resource_path('pedestal.png'))
@@ -284,6 +317,7 @@ class Icons:
284
317
  CSV = property(lambda self: _resource_path('cvs.png'))
285
318
  PPP = property(lambda self: _resource_path('ppp.png'))
286
319
  SCRIPT = property(lambda self: _resource_path('script.png'))
320
+ ACV = property(lambda self: _resource_path('acv_icon.png'))
287
321
 
288
322
  # Blink & comparison
289
323
  BLINK = property(lambda self: _resource_path('blink.png'))
@@ -393,6 +427,39 @@ def _init_legacy_paths():
393
427
  'slot7_path': get_icon_path('slot7.png'),
394
428
  'slot8_path': get_icon_path('slot8.png'),
395
429
  'slot9_path': get_icon_path('slot9.png'),
430
+ 'acv_icon_path': get_icon_path('acv_icon.png'),
431
+
432
+ 'moon_new_path': get_icon_path('new_moon.png'),
433
+ 'moon_waxing_crescent_1_path': get_icon_path('waxing_crescent_1.png'),
434
+ 'moon_waxing_crescent_2_path': get_icon_path('waxing_crescent_2.png'),
435
+ 'moon_waxing_crescent_3_path': get_icon_path('waxing_crescent_3.png'),
436
+ 'moon_waxing_crescent_4_path': get_icon_path('waxing_crescent_4.png'),
437
+ 'moon_waxing_crescent_5_path': get_icon_path('waxing_crescent_5.png'),
438
+
439
+ 'moon_first_quarter_path': get_icon_path('first_quarter.png'),
440
+
441
+ 'moon_waxing_gibbous_1_path': get_icon_path('waxing_gibbous_1.png'),
442
+ 'moon_waxing_gibbous_2_path': get_icon_path('waxing_gibbous_2.png'),
443
+ 'moon_waxing_gibbous_3_path': get_icon_path('waxing_gibbous_3.png'),
444
+ 'moon_waxing_gibbous_4_path': get_icon_path('waxing_gibbous_4.png'),
445
+ 'moon_waxing_gibbous_5_path': get_icon_path('waxing_gibbous_5.png'),
446
+
447
+ 'moon_full_path': get_icon_path('full_moon.png'),
448
+
449
+ 'moon_waning_gibbous_1_path': get_icon_path('waning_gibbous_1.png'),
450
+ 'moon_waning_gibbous_2_path': get_icon_path('waning_gibbous_2.png'),
451
+ 'moon_waning_gibbous_3_path': get_icon_path('waning_gibbous_3.png'),
452
+ 'moon_waning_gibbous_4_path': get_icon_path('waning_gibbous_4.png'),
453
+ 'moon_waning_gibbous_5_path': get_icon_path('waning_gibbous_5.png'),
454
+
455
+ 'moon_last_quarter_path': get_icon_path('last_quarter.png'),
456
+
457
+ 'moon_waning_crescent_1_path': get_icon_path('waning_crescent_1.png'),
458
+ 'moon_waning_crescent_2_path': get_icon_path('waning_crescent_2.png'),
459
+ 'moon_waning_crescent_3_path': get_icon_path('waning_crescent_3.png'),
460
+ 'moon_waning_crescent_4_path': get_icon_path('waning_crescent_4.png'),
461
+ 'moon_waning_crescent_5_path': get_icon_path('waning_crescent_5.png'),
462
+
396
463
  'rgbcombo_path': get_icon_path('rgbcombo.png'),
397
464
  'rgbextract_path': get_icon_path('rgbextract.png'),
398
465
  'copyslot_path': get_icon_path('copyslot.png'),
@@ -350,6 +350,10 @@ class RGBAlignDialog(QDialog):
350
350
  self.setWindowFlag(Qt.WindowType.Window, True)
351
351
  self.setWindowModality(Qt.WindowModality.NonModal)
352
352
  self.setModal(False)
353
+ try:
354
+ self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
355
+ except Exception:
356
+ pass # older PyQt6 versions
353
357
  self.parent = parent
354
358
  # document could be a view; try to unwrap
355
359
  self.doc_view = document
@@ -458,7 +458,10 @@ class SelectiveColorCorrection(QDialog):
458
458
  self.setWindowTitle(self.tr("Selective Color Correction"))
459
459
  if window_icon:
460
460
  self.setWindowIcon(window_icon)
461
-
461
+ try:
462
+ self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
463
+ except Exception:
464
+ pass # older PyQt6 versions
462
465
  self.docman = doc_manager
463
466
  self.document = document
464
467
  if self.document is None or getattr(self.document, "image", None) is None: