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,1324 @@
1
+ # pro/gui/mixins/toolbar_mixin.py
2
+ """
3
+ Toolbar and action management mixin for AstroSuiteProMainWindow.
4
+ """
5
+ from __future__ import annotations
6
+ import json
7
+ from typing import TYPE_CHECKING
8
+
9
+ from PyQt6.QtCore import Qt, QTimer, QUrl
10
+ from PyQt6.QtGui import QAction, QActionGroup, QIcon, QKeySequence, QDesktopServices
11
+ from PyQt6.QtWidgets import QMenu, QToolButton
12
+
13
+ if TYPE_CHECKING:
14
+ pass
15
+
16
+ # Import icon paths - these are needed at runtime
17
+ from setiastro.saspro.resources import (
18
+ icon_path, green_path, neutral_path, whitebalance_path,
19
+ morpho_path, clahe_path, starnet_path, staradd_path, LExtract_path,
20
+ LInsert_path, rgbcombo_path, rgbextract_path, graxperticon_path,
21
+ cropicon_path, openfile_path, abeicon_path, undoicon_path, redoicon_path,
22
+ blastericon_path, hdr_path, invert_path, fliphorizontal_path,
23
+ flipvertical_path, rotateclockwise_path, rotatecounterclockwise_path,
24
+ rotate180_path, maskcreate_path, maskapply_path, maskremove_path,
25
+ pixelmath_path, histogram_path, mosaic_path, rescale_path, staralign_path,
26
+ platesolve_path, psf_path, supernova_path, starregistration_path,
27
+ stacking_path, pedestal_icon_path, starspike_path, astrospike_path,
28
+ signature_icon_path, livestacking_path, convoicon_path, spcc_icon_path,
29
+ exoicon_path, peeker_icon, dse_icon_path, isophote_path, statstretch_path,
30
+ starstretch_path, curves_path, disk_path, uhs_path, blink_path, ppp_path,
31
+ nbtorgb_path, freqsep_path, multiscale_decomp_path, contsub_path, halo_path, cosmic_path,
32
+ satellite_path, imagecombine_path, wims_path, wimi_path, linearfit_path,
33
+ debayer_path, aberration_path, functionbundles_path, viewbundles_path,
34
+ selectivecolor_path, rgbalign_path,
35
+ )
36
+
37
+ # Import shortcuts module
38
+ from setiastro.saspro.shortcuts import DraggableToolBar, ShortcutManager
39
+
40
+
41
+ class ToolbarMixin:
42
+ """
43
+ Mixin for toolbar and action management.
44
+
45
+ Provides methods for creating and managing toolbars and actions.
46
+ """
47
+
48
+ # Placeholder methods for tool openers (implemented in main window)
49
+
50
+
51
+ def _sync_link_action_state(self):
52
+ """Synchronize the link views action state."""
53
+ if not hasattr(self, "_link_views_enabled"):
54
+ return
55
+
56
+ if hasattr(self, "action_link_views"):
57
+ self.action_link_views.setChecked(self._link_views_enabled)
58
+
59
+ def _find_action_by_cid(self, command_id: str) -> QAction | None:
60
+ """
61
+ Find an action by its command ID.
62
+
63
+ Args:
64
+ command_id: The command identifier string
65
+
66
+ Returns:
67
+ The QAction if found, None otherwise
68
+ """
69
+ for action in self.findChildren(QAction):
70
+ if getattr(action, "command_id", None) == command_id:
71
+ return action
72
+ return None
73
+
74
+ def _init_toolbar(self):
75
+ # View toolbar (Undo / Redo / Display-Stretch)
76
+ tb = DraggableToolBar("View", self)
77
+ tb.setSettingsKey("Toolbar/View")
78
+ self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb)
79
+
80
+ tb.addAction(self.act_open)
81
+ tb.addAction(self.act_save)
82
+ tb.addSeparator()
83
+ tb.addAction(self.act_undo)
84
+ tb.addAction(self.act_redo)
85
+ tb.addSeparator()
86
+
87
+ # Put Display-Stretch on the bar first so we can attach a menu to its button
88
+ tb.addAction(self.act_autostretch)
89
+ tb.addAction(self.act_zoom_out)
90
+ tb.addAction(self.act_zoom_in)
91
+ tb.addAction(self.act_zoom_1_1)
92
+ tb.addAction(self.act_zoom_fit)
93
+
94
+ # Style the autostretch button + add menu
95
+ btn = tb.widgetForAction(self.act_autostretch)
96
+ if isinstance(btn, QToolButton):
97
+ menu = QMenu(btn)
98
+ menu.addAction(self.act_stretch_linked)
99
+ menu.addAction(self.act_hardstretch)
100
+
101
+ # NEW: advanced controls + presets
102
+ menu.addSeparator()
103
+ menu.addAction(self.act_display_target)
104
+ menu.addAction(self.act_display_sigma)
105
+
106
+ presets = QMenu("Presets", menu)
107
+ a_norm = presets.addAction("Normal (target 0.30, σ 5)")
108
+ a_midy = presets.addAction("Mid (target 0.40, σ 3)")
109
+ a_hard = presets.addAction("Hard (target 0.50, σ 2)")
110
+ menu.addMenu(presets)
111
+ menu.addSeparator()
112
+ menu.addAction(self.act_bake_display_stretch)
113
+
114
+ # push numbers to the active view and (optionally) turn on autostretch
115
+ def _apply_preset(t, s, also_enable=True):
116
+ self.settings.setValue("display/target", float(t))
117
+ self.settings.setValue("display/sigma", float(s))
118
+ sw = self.mdi.activeSubWindow()
119
+ if not sw:
120
+ return
121
+ view = sw.widget()
122
+ if hasattr(view, "set_autostretch_target"):
123
+ view.set_autostretch_target(float(t))
124
+ if hasattr(view, "set_autostretch_sigma"):
125
+ view.set_autostretch_sigma(float(s))
126
+ if also_enable and not getattr(view, "autostretch_enabled", False):
127
+ if hasattr(view, "set_autostretch"):
128
+ view.set_autostretch(True)
129
+ self._sync_autostretch_action(True)
130
+
131
+ a_norm.triggered.connect(lambda: _apply_preset(0.30, 5.0))
132
+ a_midy.triggered.connect(lambda: _apply_preset(0.40, 3.0))
133
+ a_hard.triggered.connect(lambda: _apply_preset(0.50, 2.0))
134
+
135
+ btn.setMenu(menu)
136
+ btn.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
137
+
138
+ btn.setStyleSheet("""
139
+ QToolButton { color: #dcdcdc; }
140
+ QToolButton:checked { color: #DAA520; font-weight: 600; }
141
+ """)
142
+
143
+ btn_fit = tb.widgetForAction(self.act_zoom_fit)
144
+ if isinstance(btn_fit, QToolButton):
145
+ fit_menu = QMenu(btn_fit)
146
+
147
+ self.act_auto_fit = fit_menu.addAction("Auto-fit on Resize")
148
+ self.act_auto_fit.setCheckable(True)
149
+ self.act_auto_fit.setChecked(self._auto_fit_on_resize)
150
+ self.act_auto_fit.toggled.connect(self._toggle_auto_fit_on_resize)
151
+
152
+ btn_fit.setMenu(fit_menu)
153
+ btn_fit.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
154
+
155
+ # Same style concept as Display-Stretch
156
+ btn_fit.setStyleSheet("""
157
+ QToolButton { color: #dcdcdc; }
158
+ QToolButton:checked { color: #DAA520; font-weight: 600; }
159
+ """)
160
+
161
+ # Make sure the visual state matches the flag at startup
162
+ self._sync_fit_auto_visual()
163
+ self._restore_toolbar_order(tb, "Toolbar/View")
164
+ # Apply hidden state immediately after order restore (prevents flash)
165
+ try:
166
+ tb.apply_hidden_state()
167
+ except Exception:
168
+ pass
169
+
170
+ # Functions toolbar
171
+ tb_fn = DraggableToolBar("Functions", self)
172
+ tb_fn.setSettingsKey("Toolbar/Functions")
173
+ self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_fn)
174
+
175
+ tb_fn.addAction(self.act_crop)
176
+ tb_fn.addAction(self.act_histogram)
177
+ tb_fn.addAction(self.act_pedestal)
178
+ tb_fn.addAction(self.act_linear_fit)
179
+ tb_fn.addAction(self.act_stat_stretch)
180
+ tb_fn.addAction(self.act_star_stretch)
181
+ tb_fn.addAction(self.act_curves)
182
+ tb_fn.addAction(self.act_ghs)
183
+ tb_fn.addAction(self.act_abe)
184
+ tb_fn.addAction(self.act_graxpert)
185
+ tb_fn.addAction(self.act_remove_stars)
186
+ tb_fn.addAction(self.act_add_stars)
187
+ tb_fn.addAction(self.act_background_neutral)
188
+ tb_fn.addAction(self.act_white_balance)
189
+ tb_fn.addAction(self.act_sfcc)
190
+ tb_fn.addAction(self.act_remove_green)
191
+ tb_fn.addAction(self.act_convo)
192
+ tb_fn.addAction(self.act_extract_luma)
193
+
194
+ btn_luma = tb_fn.widgetForAction(self.act_extract_luma)
195
+ if isinstance(btn_luma, QToolButton):
196
+ luma_menu = QMenu(btn_luma)
197
+ luma_menu.addActions(self._luma_group.actions())
198
+ btn_luma.setMenu(luma_menu)
199
+ btn_luma.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
200
+ btn_luma.setStyleSheet("""
201
+ QToolButton { color: #dcdcdc; }
202
+ QToolButton:pressed, QToolButton:checked { color: #DAA520; font-weight: 600; }
203
+ """)
204
+
205
+ tb_fn.addAction(self.act_recombine_luma)
206
+ tb_fn.addAction(self.act_rgb_extract)
207
+ tb_fn.addAction(self.act_rgb_combine)
208
+ tb_fn.addAction(self.act_blemish)
209
+ tb_fn.addAction(self.act_wavescale_hdr)
210
+ tb_fn.addAction(self.act_wavescale_de)
211
+ tb_fn.addAction(self.act_clahe)
212
+ tb_fn.addAction(self.act_morphology)
213
+ tb_fn.addAction(self.act_pixelmath)
214
+ tb_fn.addAction(self.act_signature)
215
+ tb_fn.addAction(self.act_halobgon)
216
+
217
+ self._restore_toolbar_order(tb_fn, "Toolbar/Functions")
218
+ try:
219
+ tb_fn.apply_hidden_state()
220
+ except Exception:
221
+ pass
222
+
223
+ tbCosmic = DraggableToolBar("Cosmic Clarity", self)
224
+ tbCosmic.setSettingsKey("Toolbar/Cosmic")
225
+ self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tbCosmic)
226
+
227
+ tbCosmic.addAction(self.actAberrationAI)
228
+ tbCosmic.addAction(self.actCosmicUI)
229
+ tbCosmic.addAction(self.actCosmicSat)
230
+
231
+ self._restore_toolbar_order(tbCosmic, "Toolbar/Cosmic")
232
+ try:
233
+ tbCosmic.apply_hidden_state()
234
+ except Exception:
235
+ pass
236
+
237
+ tb_tl = DraggableToolBar("Tools", self)
238
+ tb_tl.setSettingsKey("Toolbar/Tools")
239
+ self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_tl)
240
+
241
+ tb_tl.addAction(self.act_blink) # Tools start here; Blink shows with QIcon(blink_path)
242
+ tb_tl.addAction(self.act_ppp) # Perfect Palette Picker
243
+ tb_tl.addAction(self.act_nbtorgb)
244
+ tb_tl.addAction(self.act_selective_color)
245
+ tb_tl.addAction(self.act_freqsep)
246
+ tb_tl.addAction(self.act_multiscale_decomp)
247
+ tb_tl.addAction(self.act_contsub)
248
+ tb_tl.addAction(self.act_image_combine)
249
+
250
+ self._restore_toolbar_order(tb_tl, "Toolbar/Tools")
251
+ try:
252
+ tb_tl.apply_hidden_state()
253
+ except Exception:
254
+ pass
255
+
256
+ tb_geom = DraggableToolBar("Geometry", self)
257
+ tb_geom.setSettingsKey("Toolbar/Geometry")
258
+ self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_geom)
259
+
260
+ tb_geom.addAction(self.act_geom_invert)
261
+ tb_geom.addSeparator()
262
+ tb_geom.addAction(self.act_geom_flip_h)
263
+ tb_geom.addAction(self.act_geom_flip_v)
264
+ tb_geom.addSeparator()
265
+ tb_geom.addAction(self.act_geom_rot_cw)
266
+ tb_geom.addAction(self.act_geom_rot_ccw)
267
+ tb_geom.addAction(self.act_geom_rot_180)
268
+ tb_geom.addSeparator()
269
+ tb_geom.addAction(self.act_geom_rescale)
270
+ tb_geom.addSeparator()
271
+ tb_geom.addAction(self.act_debayer)
272
+
273
+ self._restore_toolbar_order(tb_geom, "Toolbar/Geometry")
274
+ try:
275
+ tb_geom.apply_hidden_state()
276
+ except Exception:
277
+ pass
278
+
279
+ tb_star = DraggableToolBar("Star Stuff", self)
280
+ tb_star.setSettingsKey("Toolbar/StarStuff")
281
+ self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_star)
282
+
283
+ tb_star.addAction(self.act_image_peeker)
284
+ tb_star.addAction(self.act_psf_viewer)
285
+ tb_star.addAction(self.act_stacking_suite)
286
+ tb_star.addAction(self.act_live_stacking)
287
+ tb_star.addAction(self.act_plate_solve)
288
+ tb_star.addAction(self.act_star_align)
289
+ tb_star.addAction(self.act_star_register)
290
+ tb_star.addAction(self.act_rgb_align)
291
+ tb_star.addAction(self.act_mosaic_master)
292
+ tb_star.addAction(self.act_supernova_hunter)
293
+ tb_star.addAction(self.act_star_spikes)
294
+ tb_star.addAction(self.act_astrospike)
295
+ tb_star.addAction(self.act_exo_detector)
296
+ tb_star.addAction(self.act_isophote)
297
+
298
+ self._restore_toolbar_order(tb_star, "Toolbar/StarStuff")
299
+ try:
300
+ tb_star.apply_hidden_state()
301
+ except Exception:
302
+ pass
303
+
304
+ tb_msk = DraggableToolBar("Masks", self)
305
+ tb_msk.setSettingsKey("Toolbar/Masks")
306
+ self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_msk)
307
+
308
+ tb_msk.addAction(self.act_create_mask)
309
+ tb_msk.addAction(self.act_apply_mask)
310
+ tb_msk.addAction(self.act_remove_mask)
311
+
312
+ self._restore_toolbar_order(tb_msk, "Toolbar/Masks")
313
+ try:
314
+ tb_msk.apply_hidden_state()
315
+ except Exception:
316
+ pass
317
+
318
+ tb_wim = DraggableToolBar("What's In My...", self)
319
+ tb_wim.setSettingsKey("Toolbar/WhatsInMy")
320
+ self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_wim)
321
+
322
+ tb_wim.addAction(self.act_whats_in_my_sky)
323
+ tb_wim.addAction(self.act_wimi)
324
+
325
+ self._restore_toolbar_order(tb_wim, "Toolbar/WhatsInMy")
326
+ try:
327
+ tb_wim.apply_hidden_state()
328
+ except Exception:
329
+ pass
330
+
331
+ tb_bundle = DraggableToolBar("Bundles", self)
332
+ tb_bundle.setSettingsKey("Toolbar/Bundles")
333
+ self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_bundle)
334
+
335
+ tb_bundle.addAction(self.act_view_bundles)
336
+ tb_bundle.addAction(self.act_function_bundles)
337
+
338
+ self._restore_toolbar_order(tb_bundle, "Toolbar/Bundles")
339
+ try:
340
+ tb_bundle.apply_hidden_state()
341
+ except Exception:
342
+ pass
343
+
344
+ # This can move actions between toolbars, so do it after each toolbar has its base order restored.
345
+ self._restore_toolbar_memberships()
346
+
347
+ # Re-apply hidden state AFTER memberships (actions may have moved toolbars).
348
+ # This also guarantees correctness even if any toolbar was rebuilt/adjusted internally.
349
+ for _tb in self.findChildren(DraggableToolBar):
350
+ try:
351
+ _tb.apply_hidden_state()
352
+ except Exception:
353
+ pass
354
+
355
+
356
+ def _create_actions(self):
357
+ # File actions
358
+ self.act_open = QAction(QIcon(openfile_path), "Open...", self)
359
+ self.act_open.setIconVisibleInMenu(True)
360
+ self.act_open.setShortcut(QKeySequence.StandardKey.Open)
361
+ self.act_open.setStatusTip("Open image(s)")
362
+ self.act_open.triggered.connect(self.open_files)
363
+
364
+
365
+ self.act_project_new = QAction("New Project", self)
366
+ self.act_project_save = QAction("Save Project...", self)
367
+ self.act_project_load = QAction("Load Project...", self)
368
+
369
+ self.act_project_new.setStatusTip("Close all views and clear shortcuts")
370
+ self.act_project_save.setStatusTip("Save all views, histories, and shortcuts to a .sas file")
371
+ self.act_project_load.setStatusTip("Load a .sas project (views, histories, shortcuts)")
372
+
373
+ self.act_project_new.triggered.connect(self._new_project)
374
+ self.act_project_save.triggered.connect(self._save_project)
375
+ self.act_project_load.triggered.connect(self._load_project)
376
+
377
+ self.act_clear_views = QAction("Clear All Views", self)
378
+ self.act_clear_views.setStatusTip("Close all views and documents, keep desktop shortcuts")
379
+ # optional shortcut (pick anything you like or omit)
380
+ # self.act_clear_views.setShortcut(QKeySequence("Ctrl+Shift+W"))
381
+ self.act_clear_views.triggered.connect(self._clear_views_keep_shortcuts)
382
+
383
+ self.act_save = QAction(QIcon(disk_path), "Save As...", self)
384
+ self.act_save.setIconVisibleInMenu(True)
385
+ self.act_save.setShortcut(QKeySequence.StandardKey.SaveAs)
386
+ self.act_save.setStatusTip("Save the active image")
387
+ self.act_save.triggered.connect(self.save_active)
388
+
389
+ self.act_exit = QAction("&Exit", self)
390
+ self.act_exit.setShortcut(QKeySequence.StandardKey.Quit) # Cmd+Q / Ctrl+Q
391
+ # Make it appear under the app menu on macOS automatically:
392
+ self.act_exit.setMenuRole(QAction.MenuRole.QuitRole)
393
+ self.act_exit.triggered.connect(self._on_exit)
394
+
395
+ self.act_cascade = QAction("Cascade Views", self)
396
+ self.act_cascade.setStatusTip("Cascade all subwindows")
397
+ self.act_cascade.setShortcut(QKeySequence("Ctrl+Shift+C"))
398
+ self.act_cascade.triggered.connect(self._cascade_views)
399
+
400
+ self.act_tile = QAction("Tile Views", self)
401
+ self.act_tile.setStatusTip("Tile all subwindows")
402
+ self.act_tile.setShortcut(QKeySequence("Ctrl+Shift+T"))
403
+ self.act_tile.triggered.connect(self._tile_views)
404
+
405
+ self.act_tile_vert = QAction("Tile Vertically", self)
406
+ self.act_tile_vert.setStatusTip("Split the workspace into equal vertical columns")
407
+ self.act_tile_vert.triggered.connect(lambda: self._tile_views_direction("v"))
408
+
409
+ self.act_tile_horiz = QAction("Tile Horizontally", self)
410
+ self.act_tile_horiz.setStatusTip("Split the workspace into equal horizontal rows")
411
+ self.act_tile_horiz.triggered.connect(lambda: self._tile_views_direction("h"))
412
+
413
+ self.act_tile_grid = QAction("Smart Grid", self)
414
+ self.act_tile_grid.setStatusTip("Arrange subwindows in a near-square grid")
415
+ self.act_tile_grid.triggered.connect(self._tile_views_grid)
416
+
417
+ self.act_link_group = QAction("Link Pan/Zoom", self)
418
+ self.act_link_group.setCheckable(True) # checked when in any group
419
+ self.act_link_group.triggered.connect(self._cycle_group_for_active) # << add
420
+
421
+ self.act_undo = QAction(QIcon(undoicon_path), "Undo", self)
422
+ self.act_redo = QAction(QIcon(redoicon_path), "Redo", self)
423
+ self.act_undo.setShortcut(QKeySequence.StandardKey.Undo) # Ctrl+Z
424
+ self.act_redo.setShortcuts([QKeySequence.StandardKey.Redo, "Ctrl+Y"]) # Shift+Ctrl+Z / Ctrl+Y
425
+ self.act_undo.setIconVisibleInMenu(True)
426
+ self.act_redo.setIconVisibleInMenu(True)
427
+ self.act_undo.triggered.connect(self._undo_active)
428
+ self.act_redo.triggered.connect(self._redo_active)
429
+
430
+ # View-ish action (toolbar toggle)
431
+ self.act_autostretch = QAction("Display-Stretch", self, checkable=True)
432
+ self.act_autostretch.setStatusTip("Toggle display auto-stretch for the active window")
433
+ self.act_autostretch.setShortcut(QKeySequence("A")) # optional: mirror the view shortcut
434
+ self.act_autostretch.toggled.connect(self._toggle_autostretch)
435
+
436
+ self.act_hardstretch = QAction("Hard-Display-Stretch", self, checkable=True)
437
+ self.addAction(self.act_hardstretch)
438
+ self.act_hardstretch.setShortcut(QKeySequence("H"))
439
+ self.act_hardstretch.setStatusTip("Toggle hard profile for Display-Stretch (H)")
440
+
441
+ # use toggled(bool), not triggered()
442
+ self.act_hardstretch.toggled.connect(self._set_hard_autostretch_from_action)
443
+
444
+ # NEW: Linked/Unlinked toggle (global default via QSettings, per-view runtime)
445
+ self.act_stretch_linked = QAction("Link RGB channels", self, checkable=True)
446
+ self.act_stretch_linked.setStatusTip("Apply the same stretch to all RGB channels")
447
+ self.act_stretch_linked.setShortcut(QKeySequence("Ctrl+Shift+L"))
448
+ self.act_stretch_linked.setChecked(
449
+ self.settings.value("display/stretch_linked", False, type=bool)
450
+ )
451
+ self.act_stretch_linked.toggled.connect(self._set_linked_stretch_from_action)
452
+
453
+ self.act_display_target = QAction("Set Target Median...", self)
454
+ self.act_display_target.setStatusTip("Set the target median for Display-Stretch (e.g., 0.30)")
455
+ self.act_display_target.triggered.connect(self._edit_display_target)
456
+
457
+ self.act_display_sigma = QAction("Set Sigma...", self)
458
+ self.act_display_sigma.setStatusTip("Set the sigma for Display-Stretch (e.g., 5.0)")
459
+ self.act_display_sigma.triggered.connect(self._edit_display_sigma)
460
+
461
+ # Defaults if not already present
462
+ if self.settings.value("display/target", None) is None:
463
+ self.settings.setValue("display/target", 0.30)
464
+ if self.settings.value("display/sigma", None) is None:
465
+ self.settings.setValue("display/sigma", 5.0)
466
+
467
+ self.act_bake_display_stretch = QAction("Make Display-Stretch Permanent", self)
468
+ self.act_bake_display_stretch.setStatusTip(
469
+ "Apply the current Display-Stretch to the image and add an undo step"
470
+ )
471
+ # choose any shortcut you like; avoid Ctrl+A etc
472
+ self.act_bake_display_stretch.setShortcut(QKeySequence("Shift+A"))
473
+ self.act_bake_display_stretch.triggered.connect(self._bake_display_stretch)
474
+
475
+ # --- Zoom controls ---
476
+ # --- Zoom controls (themed icons) ---
477
+ self.act_zoom_out = QAction(QIcon.fromTheme("zoom-out"), "Zoom Out", self)
478
+ self.act_zoom_out.setStatusTip("Zoom out")
479
+ self.act_zoom_out.setShortcuts([QKeySequence("Ctrl+-")])
480
+ self.act_zoom_out.triggered.connect(lambda: self._zoom_step_active(-1))
481
+
482
+ self.act_zoom_in = QAction(QIcon.fromTheme("zoom-in"), "Zoom In", self)
483
+ self.act_zoom_in.setStatusTip("Zoom in")
484
+ self.act_zoom_in.setShortcuts([
485
+ QKeySequence("Ctrl++"), # Ctrl + (Shift + = on many keyboards)
486
+ QKeySequence("Ctrl+="), # fallback
487
+ ])
488
+ self.act_zoom_in.triggered.connect(lambda: self._zoom_step_active(+1))
489
+
490
+ self.act_zoom_1_1 = QAction(QIcon.fromTheme("zoom-original"), "1:1", self)
491
+ self.act_zoom_1_1.setStatusTip("Zoom to 100% (pixel-for-pixel)")
492
+ self.act_zoom_1_1.setShortcut(QKeySequence("Ctrl+1"))
493
+ self.act_zoom_1_1.triggered.connect(self._zoom_active_1_1)
494
+
495
+ self.act_zoom_fit = QAction(QIcon.fromTheme("zoom-fit-best"), "Fit", self)
496
+ self.act_zoom_fit.setStatusTip("Fit image to current window")
497
+ self.act_zoom_fit.setShortcut(QKeySequence("Ctrl+0"))
498
+ self.act_zoom_fit.triggered.connect(self._zoom_active_fit)
499
+ self.act_zoom_fit.setCheckable(True)
500
+
501
+ self.act_auto_fit_resize = QAction("Auto-fit on Resize", self)
502
+ self.act_auto_fit_resize.setCheckable(True)
503
+
504
+ auto_on = self.settings.value("view/auto_fit_on_resize", False, type=bool)
505
+ self._auto_fit_on_resize = bool(auto_on)
506
+ self.act_auto_fit_resize.setChecked(self._auto_fit_on_resize)
507
+
508
+ self.act_auto_fit_resize.toggled.connect(self._toggle_auto_fit_on_resize)
509
+
510
+ # View state copy/paste (optional quick commands)
511
+ self._copied_view_state = None
512
+ self.act_copy_view = QAction("Copy View (zoom/pan)", self)
513
+ self.act_paste_view = QAction("Paste View", self)
514
+ self.act_copy_view.setShortcut("Ctrl+Shift+C")
515
+ self.act_paste_view.setShortcut("Ctrl+Shift+V")
516
+ self.act_copy_view.triggered.connect(self._copy_active_view)
517
+ self.act_paste_view.triggered.connect(self._paste_active_view)
518
+
519
+ # Functions
520
+ self.act_crop = QAction(QIcon(cropicon_path), "Crop...", self)
521
+ self.act_crop.setStatusTip("Crop / rotate with handles")
522
+ self.act_crop.setIconVisibleInMenu(True)
523
+ self.act_crop.triggered.connect(self._open_crop_dialog)
524
+
525
+ self.act_histogram = QAction(QIcon(histogram_path), "Histogram...", self)
526
+ self.act_histogram.setStatusTip("View histogram and basic stats for the active image")
527
+ self.act_histogram.setIconVisibleInMenu(True)
528
+ self.act_histogram.triggered.connect(self._open_histogram)
529
+
530
+ self.act_stat_stretch = QAction(QIcon(statstretch_path), "Statistical Stretch...", self)
531
+ self.act_stat_stretch.setStatusTip("Stretch the image using median/SD statistics")
532
+ self.act_stat_stretch.setIconVisibleInMenu(True)
533
+ self.act_stat_stretch.triggered.connect(self._open_statistical_stretch)
534
+
535
+ self.act_star_stretch = QAction(QIcon(starstretch_path), "Star Stretch...", self)
536
+ self.act_star_stretch.setStatusTip("Arcsinh star stretch with optional SCNR and color boost")
537
+ self.act_star_stretch.setIconVisibleInMenu(True)
538
+ self.act_star_stretch.triggered.connect(self._open_star_stretch)
539
+
540
+ self.act_curves = QAction(QIcon(curves_path), "Curves Editor...", self) # add an icon later if you want
541
+ self.act_curves.setStatusTip("Open the Curves Editor for the active image")
542
+ self.act_curves.setIconVisibleInMenu(True)
543
+ self.act_curves.triggered.connect(self._open_curves_editor)
544
+
545
+ self.act_ghs = QAction(QIcon(uhs_path), "Hyperbolic Stretch...", self)
546
+ self.act_ghs.setStatusTip("Generalized hyperbolic stretch (α/beta/gamma, LP/HP, pivot)")
547
+ self.act_ghs.setIconVisibleInMenu(True)
548
+ self.act_ghs.triggered.connect(self._open_hyperbolic)
549
+
550
+ self.act_abe = QAction(QIcon(abeicon_path), "ABE...", self)
551
+ self.act_abe.setStatusTip("Automatic Background Extraction")
552
+ self.act_abe.setIconVisibleInMenu(True)
553
+ self.act_abe.triggered.connect(self._open_abe_tool)
554
+
555
+ self.act_graxpert = QAction(QIcon(graxperticon_path), "Remove Gradient (GraXpert)...", self)
556
+ self.act_graxpert.setIconVisibleInMenu(True)
557
+ self.act_graxpert.setStatusTip("Run GraXpert background extraction on the active image")
558
+ self.act_graxpert.triggered.connect(self._open_graxpert)
559
+
560
+ self.act_remove_stars = QAction(QIcon(starnet_path), "Remove Stars...", self)
561
+ self.act_remove_stars.setIconVisibleInMenu(True)
562
+ self.act_remove_stars.setStatusTip("Run star removal on the active image")
563
+ self.act_remove_stars.triggered.connect(lambda: self._remove_stars())
564
+
565
+ self.act_add_stars = QAction(QIcon(staradd_path), "Add Stars...", self)
566
+ self.act_add_stars.setStatusTip("Blend a starless view with a stars-only view")
567
+ self.act_add_stars.setIconVisibleInMenu(True)
568
+ self.act_add_stars.triggered.connect(lambda: self._add_stars())
569
+
570
+ self.act_pedestal = QAction(QIcon(pedestal_icon_path), "Remove Pedestal", self)
571
+ self.act_pedestal.setToolTip("Subtract per-channel minimum.\nClick: active view\nAlt+Drag: drop onto a view")
572
+ self.act_pedestal.setShortcut("Ctrl+P")
573
+ self.act_pedestal.triggered.connect(self._on_remove_pedestal)
574
+
575
+ self.act_linear_fit = QAction(QIcon(linearfit_path),"Linear Fit...", self)
576
+ self.act_linear_fit.setIconVisibleInMenu(True)
577
+ self.act_linear_fit.setStatusTip("Match image levels using Linear Fit")
578
+ # optional shortcut; change if you already use it elsewhere
579
+ self.act_linear_fit.setShortcut("Ctrl+L")
580
+ self.act_linear_fit.triggered.connect(self._open_linear_fit)
581
+
582
+ self.act_remove_green = QAction(QIcon(green_path), "Remove Green...", self)
583
+ self.act_remove_green.setToolTip("SCNR-style green channel removal.")
584
+ self.act_remove_green.setIconVisibleInMenu(True)
585
+ self.act_remove_green.triggered.connect(self._open_remove_green)
586
+
587
+ self.act_background_neutral = QAction(QIcon(neutral_path), "Background Neutralization...", self)
588
+ self.act_background_neutral.setStatusTip("Neutralize background color balance using a sampled region")
589
+ self.act_background_neutral.setIconVisibleInMenu(True)
590
+ self.act_background_neutral.triggered.connect(self._open_background_neutral)
591
+
592
+ self.act_white_balance = QAction(QIcon(whitebalance_path), "White Balance...", self)
593
+ self.act_white_balance.setStatusTip("Apply white balance (Star-Based, Manual, or Auto)")
594
+ self.act_white_balance.triggered.connect(self._open_white_balance)
595
+
596
+ self.act_sfcc = QAction(QIcon(spcc_icon_path), "Spectral Flux Color Calibration...", self)
597
+ self.act_sfcc.setObjectName("sfcc")
598
+ self.act_sfcc.setToolTip("Open SFCC (Pickles + Filters + Sensor QE)")
599
+ self.act_sfcc.triggered.connect(self.SFCC_show)
600
+
601
+ self.act_convo = QAction(QIcon(convoicon_path), "Convolution / Deconvolution...", self)
602
+ self.act_convo.setObjectName("convo_deconvo")
603
+ self.act_convo.setToolTip("Open Convolution / Deconvolution")
604
+ self.act_convo.triggered.connect(self.show_convo_deconvo)
605
+
606
+ self.act_multiscale_decomp = QAction(QIcon(multiscale_decomp_path), "Multiscale Decomposition...", self)
607
+ self.act_multiscale_decomp.setStatusTip("Multiscale detail/residual decomposition with per-layer controls")
608
+ self.act_multiscale_decomp.setIconVisibleInMenu(True)
609
+ self.act_multiscale_decomp.triggered.connect(self._open_multiscale_decomp)
610
+
611
+
612
+
613
+
614
+ # --- Extract Luminance main action ---
615
+ self.act_extract_luma = QAction(QIcon(LExtract_path), "Extract Luminance", self)
616
+ self.act_extract_luma.setStatusTip("Create a new mono document using the selected luminance method")
617
+ self.act_extract_luma.setIconVisibleInMenu(True)
618
+ self.act_extract_luma.triggered.connect(lambda: self._extract_luminance(doc=None))
619
+
620
+ # --- Luminance method actions (checkable group) ---
621
+ self.luma_method = getattr(self, "luma_method", "rec709") # default
622
+ self._luma_group = QActionGroup(self)
623
+ self._luma_group.setExclusive(True)
624
+
625
+ def _mk(method_key, text):
626
+ act = QAction(text, self, checkable=True)
627
+ act.setData(method_key)
628
+ self._luma_group.addAction(act)
629
+ return act
630
+
631
+ self.act_luma_rec709 = _mk("rec709", "Broadband RGB (Rec.709)")
632
+ self.act_luma_max = _mk("max", "Narrowband mappings (Max)")
633
+ self.act_luma_snr = _mk("snr", "Unequal Noise (SNR)")
634
+ self.act_luma_rec601 = _mk("rec601", "Rec.601")
635
+ self.act_luma_rec2020 = _mk("rec2020", "Rec.2020")
636
+
637
+ # restore selection
638
+ for a in self._luma_group.actions():
639
+ a.setChecked(a.data() == self.luma_method)
640
+
641
+ # update method when user picks from the menu
642
+ def _on_luma_pick(act):
643
+ self.luma_method = act.data()
644
+ # (optional) persist
645
+ try:
646
+ self.settings.setValue("ui/luminance_method", self.luma_method)
647
+ except Exception:
648
+ pass
649
+
650
+ self._luma_group.triggered.connect(_on_luma_pick)
651
+
652
+ self.act_recombine_luma = QAction(QIcon(LInsert_path), "Recombine Luminance...", self)
653
+ self.act_recombine_luma.setStatusTip("Replace the active image's luminance from another view")
654
+ self.act_recombine_luma.setIconVisibleInMenu(True)
655
+ self.act_recombine_luma.triggered.connect(lambda: self._recombine_luminance_ui(target_doc=None))
656
+
657
+ self.act_rgb_extract = QAction(QIcon(rgbextract_path), "RGB Extract", self)
658
+ self.act_rgb_extract.setIconVisibleInMenu(True)
659
+ self.act_rgb_extract.setStatusTip("Extract R/G/B as three mono documents")
660
+ self.act_rgb_extract.triggered.connect(self._rgb_extract_active)
661
+
662
+ self.act_rgb_combine = QAction(QIcon(rgbcombo_path), "RGB Combination...", self)
663
+ self.act_rgb_combine.setIconVisibleInMenu(True)
664
+ self.act_rgb_combine.setStatusTip("Combine three mono images into RGB")
665
+ self.act_rgb_combine.triggered.connect(self._open_rgb_combination)
666
+
667
+ self.act_blemish = QAction(QIcon(blastericon_path), "Blemish Blaster...", self)
668
+ self.act_blemish.setIconVisibleInMenu(True)
669
+ self.act_blemish.setStatusTip("Interactive blemish removal on the active view")
670
+ self.act_blemish.triggered.connect(self._open_blemish_blaster)
671
+
672
+ self.act_wavescale_hdr = QAction(QIcon(hdr_path), "WaveScale HDR...", self)
673
+ self.act_wavescale_hdr.setStatusTip("Wave-scale HDR with luminance-masked starlet")
674
+ self.act_wavescale_hdr.setIconVisibleInMenu(True)
675
+ self.act_wavescale_hdr.triggered.connect(self._open_wavescale_hdr)
676
+
677
+ self.act_wavescale_de = QAction(QIcon(dse_icon_path), "WaveScale Dark Enhancer...", self)
678
+ self.act_wavescale_de.setStatusTip("Enhance faint/dark structures with wavelet-guided masking")
679
+ self.act_wavescale_de.setIconVisibleInMenu(True)
680
+ self.act_wavescale_de.triggered.connect(self._open_wavescale_dark_enhance)
681
+
682
+ self.act_clahe = QAction(QIcon(clahe_path), "CLAHE...", self)
683
+ self.act_clahe.setStatusTip("Contrast Limited Adaptive Histogram Equalization")
684
+ self.act_clahe.setIconVisibleInMenu(True)
685
+ self.act_clahe.triggered.connect(self._open_clahe)
686
+
687
+ self.act_morphology = QAction(QIcon(morpho_path), "Morphological Operations...", self)
688
+ self.act_morphology.setStatusTip("Erosion, dilation, opening, and closing.")
689
+ self.act_morphology.setIconVisibleInMenu(True)
690
+ self.act_morphology.triggered.connect(self._open_morphology)
691
+
692
+ self.act_pixelmath = QAction(QIcon(pixelmath_path), "Pixel Math...", self)
693
+ self.act_pixelmath.setStatusTip("Evaluate expressions using open view names")
694
+ self.act_pixelmath.setIconVisibleInMenu(True)
695
+ self.act_pixelmath.triggered.connect(self._open_pixel_math)
696
+
697
+ self.act_signature = QAction(QIcon(signature_icon_path), "Signature / Insert...", self)
698
+ self.act_signature.setIconVisibleInMenu(True)
699
+ self.act_signature.setStatusTip("Add signatures/overlays and bake them into the active image")
700
+ self.act_signature.triggered.connect(self._open_signature_insert)
701
+
702
+ self.act_halobgon = QAction(QIcon(halo_path), "Halo-B-Gon...", self)
703
+ self.act_halobgon.setIconVisibleInMenu(True)
704
+ self.act_halobgon.setStatusTip("Remove those pesky halos around your stars")
705
+ self.act_halobgon.triggered.connect(self._open_halo_b_gon)
706
+
707
+ self.act_image_combine = QAction(QIcon(imagecombine_path), "Image Combine...", self)
708
+ self.act_image_combine.setIconVisibleInMenu(True)
709
+ self.act_image_combine.setStatusTip("Blend two open images (replace A or create new)")
710
+ self.act_image_combine.triggered.connect(self._open_image_combine)
711
+
712
+ # --- Geometry ---
713
+ self.act_geom_invert = QAction(QIcon(invert_path), "Invert", self)
714
+ self.act_geom_invert.setIconVisibleInMenu(True)
715
+ self.act_geom_invert.setStatusTip("Invert image colors")
716
+ self.act_geom_invert.triggered.connect(self._exec_geom_invert)
717
+
718
+ self.act_geom_flip_h = QAction(QIcon(fliphorizontal_path), "Flip Horizontal", self)
719
+ self.act_geom_flip_h.setIconVisibleInMenu(True)
720
+ self.act_geom_flip_h.setStatusTip("Flip image left<->right")
721
+ self.act_geom_flip_h.triggered.connect(self._exec_geom_flip_h)
722
+
723
+ self.act_geom_flip_v = QAction(QIcon(flipvertical_path), "Flip Vertical", self)
724
+ self.act_geom_flip_v.setIconVisibleInMenu(True)
725
+ self.act_geom_flip_v.setStatusTip("Flip image top<->bottom")
726
+ self.act_geom_flip_v.triggered.connect(self._exec_geom_flip_v)
727
+
728
+ self.act_geom_rot_cw = QAction(QIcon(rotateclockwise_path), "Rotate 90Â deg Clockwise", self)
729
+ self.act_geom_rot_cw.setIconVisibleInMenu(True)
730
+ self.act_geom_rot_cw.setStatusTip("Rotate image 90Â deg clockwise")
731
+ self.act_geom_rot_cw.triggered.connect(self._exec_geom_rot_cw)
732
+
733
+ self.act_geom_rot_ccw = QAction(QIcon(rotatecounterclockwise_path), "Rotate 90Â deg Counterclockwise", self)
734
+ self.act_geom_rot_ccw.setIconVisibleInMenu(True)
735
+ self.act_geom_rot_ccw.setStatusTip("Rotate image 90Â deg counterclockwise")
736
+ self.act_geom_rot_ccw.triggered.connect(self._exec_geom_rot_ccw)
737
+
738
+ self.act_geom_rot_180 = QAction(QIcon(rotate180_path), "Rotate 180Â deg", self)
739
+ self.act_geom_rot_180.setIconVisibleInMenu(True)
740
+ self.act_geom_rot_180.setStatusTip("Rotate image 180Â deg")
741
+ self.act_geom_rot_180.triggered.connect(self._exec_geom_rot_180)
742
+
743
+ self.act_geom_rescale = QAction(QIcon(rescale_path), "Rescale...", self)
744
+ self.act_geom_rescale.setIconVisibleInMenu(True)
745
+ self.act_geom_rescale.setStatusTip("Rescale image by a factor")
746
+ self.act_geom_rescale.triggered.connect(self._exec_geom_rescale)
747
+
748
+ self.act_debayer = QAction(QIcon(debayer_path), "Debayer...", self)
749
+ self.act_debayer.setObjectName("debayer")
750
+ self.act_debayer.setProperty("command_id", "debayer")
751
+ self.act_debayer.setStatusTip("Demosaic a Bayer-mosaic mono image to RGB")
752
+ self.act_debayer.triggered.connect(self._open_debayer)
753
+
754
+ # (Optional example shortcuts; uncomment if you want)
755
+ self.act_geom_invert.setShortcut("Ctrl+I")
756
+
757
+ # self.act_geom_flip_h.setShortcut("H")
758
+ # self.act_geom_flip_v.setShortcut("V")
759
+ # self.act_geom_rot_cw.setShortcut("]")
760
+ # self.act_geom_rot_ccw.setShortcut("[")
761
+ # self.act_geom_rescale.setShortcut("Ctrl+R")
762
+
763
+
764
+ # actions (use your actual icon paths if you have them)
765
+ try:
766
+ cosmic_icon = QIcon(cosmic_path) # define cosmic_path like your other icons (same pattern as halo_path)
767
+ except Exception:
768
+ cosmic_icon = QIcon()
769
+
770
+ try:
771
+ sat_icon = QIcon(satellite_path) # optional icon for satellite
772
+ except Exception:
773
+ sat_icon = QIcon()
774
+
775
+ self.actCosmicUI = QAction(cosmic_icon, "Cosmic Clarity UI...", self)
776
+ self.actCosmicSat = QAction(sat_icon, "Cosmic Clarity Satellite...", self)
777
+
778
+ self.actCosmicUI.triggered.connect(self._open_cosmic_clarity_ui)
779
+ self.actCosmicSat.triggered.connect(self._open_cosmic_clarity_satellite)
780
+
781
+
782
+ ab_icon = QIcon(aberration_path) # falls back if file missing
783
+
784
+ self.actAberrationAI = QAction(ab_icon, "Aberration Correction (AI)...", self)
785
+ self.actAberrationAI.triggered.connect(self._open_aberration_ai)
786
+
787
+
788
+
789
+ #Tools
790
+ self.act_blink = QAction(QIcon(blink_path), "Blink Comparator...", self)
791
+ self.act_blink.setStatusTip("Compare a stack of images by blinking")
792
+ self.act_blink.triggered.connect(self._open_blink_tool)
793
+
794
+ self.act_ppp = QAction(QIcon(ppp_path), "Perfect Palette Picker...", self)
795
+ self.act_ppp.setStatusTip("Pick the perfect palette for your image")
796
+ self.act_ppp.triggered.connect(self._open_ppp_tool)
797
+
798
+ self.act_nbtorgb = QAction(QIcon(nbtorgb_path), "NB->RGB Stars...", self)
799
+ self.act_nbtorgb.setStatusTip("Combine narrowband to RGB with optional OSC stars")
800
+ self.act_nbtorgb.setIconVisibleInMenu(True)
801
+ self.act_nbtorgb.triggered.connect(self._open_nbtorgb_tool)
802
+
803
+ self.act_selective_color = QAction(QIcon(selectivecolor_path), "Selective Color Correction...", self)
804
+ self.act_selective_color.setStatusTip("Adjust specific hue ranges with CMY/RGB controls")
805
+ self.act_selective_color.triggered.connect(self._open_selective_color_tool)
806
+
807
+ # NEW: Frequency Separation
808
+ self.act_freqsep = QAction(QIcon(freqsep_path), "Frequency Separation...", self)
809
+ self.act_freqsep.setStatusTip("Split into LF/HF and enhance HF (scale, wavelet, denoise)")
810
+ self.act_freqsep.setIconVisibleInMenu(True)
811
+ self.act_freqsep.triggered.connect(self._open_freqsep_tool)
812
+
813
+ self.act_contsub = QAction(QIcon(contsub_path), "Continuum Subtract...", self)
814
+ self.act_contsub.setStatusTip("Continuum subtract (NB - scaled broadband)")
815
+ self.act_contsub.setIconVisibleInMenu(True)
816
+ self.act_contsub.triggered.connect(self._open_contsub_tool)
817
+
818
+ # History
819
+ self.act_history_explorer = QAction("History Explorer...", self)
820
+ self.act_history_explorer.setStatusTip("Inspect and restore from the slot's history")
821
+ self.act_history_explorer.triggered.connect(self._open_history_explorer)
822
+
823
+
824
+ #STAR STUFF
825
+ self.act_image_peeker = QAction(QIcon(peeker_icon), "Image Peeker...", self)
826
+ self.act_image_peeker.setIconVisibleInMenu(True)
827
+ self.act_image_peeker.setStatusTip("Image Inspector and Focal Plane Analysis")
828
+ self.act_image_peeker.triggered.connect(self._open_image_peeker)
829
+
830
+ self.act_psf_viewer = QAction(QIcon(psf_path), "PSF Viewer...", self)
831
+ self.act_psf_viewer.setIconVisibleInMenu(True)
832
+ self.act_psf_viewer.setStatusTip("Inspect star PSF/HFR and flux histograms (SEP)")
833
+ self.act_psf_viewer.triggered.connect(self._open_psf_viewer)
834
+
835
+ self.act_stacking_suite = QAction(QIcon(stacking_path), "Stacking Suite...", self)
836
+ self.act_stacking_suite.setIconVisibleInMenu(True)
837
+ self.act_stacking_suite.setStatusTip("Stacking! Darks, Flats, Lights, Calibration, Drizzle, and more!!")
838
+ self.act_stacking_suite.triggered.connect(self._open_stacking_suite)
839
+
840
+ self.act_live_stacking = QAction(QIcon(livestacking_path), "Live Stacking...", self)
841
+ self.act_live_stacking.setIconVisibleInMenu(True)
842
+ self.act_live_stacking.setStatusTip("Live monitor and stack incoming frames")
843
+ self.act_live_stacking.triggered.connect(self._open_live_stacking)
844
+
845
+ self.act_plate_solve = QAction(QIcon(platesolve_path), "Plate Solver...", self)
846
+ self.act_plate_solve.setIconVisibleInMenu(True)
847
+ self.act_plate_solve.setStatusTip("Solve WCS/SIP for the active image or a file")
848
+ self.act_plate_solve.triggered.connect(self._open_plate_solver)
849
+
850
+ self.act_star_align = QAction(QIcon(staralign_path), "Stellar Alignment...", self)
851
+ self.act_star_align.setIconVisibleInMenu(True)
852
+ self.act_star_align.setStatusTip("Align images via astroalign / triangles")
853
+ self.act_star_align.triggered.connect(self._open_stellar_alignment)
854
+
855
+ self.act_star_register = QAction(QIcon(starregistration_path), "Stellar Register...", self)
856
+ self.act_star_register.setIconVisibleInMenu(True)
857
+ self.act_star_register.setStatusTip("Batch-align frames to a reference")
858
+ self.act_star_register.triggered.connect(self._open_stellar_registration)
859
+
860
+ self.act_mosaic_master = QAction(QIcon(mosaic_path), "Mosaic Master...", self)
861
+ self.act_mosaic_master.setIconVisibleInMenu(True)
862
+ self.act_mosaic_master.setStatusTip("Build mosaics from overlapping frames")
863
+ self.act_mosaic_master.triggered.connect(self._open_mosaic_master)
864
+
865
+ self.act_supernova_hunter = QAction(QIcon(supernova_path), "Supernova / Asteroid Hunter...", self)
866
+ self.act_supernova_hunter.setIconVisibleInMenu(True)
867
+ self.act_supernova_hunter.setStatusTip("Find transients/anomalies across frames")
868
+ self.act_supernova_hunter.triggered.connect(self._open_supernova_hunter)
869
+
870
+ self.act_star_spikes = QAction(QIcon(starspike_path), "Diffraction Spikes...", self)
871
+ self.act_star_spikes.setIconVisibleInMenu(True)
872
+ self.act_star_spikes.setStatusTip("Add diffraction spikes to detected stars")
873
+ self.act_star_spikes.triggered.connect(self._open_star_spikes)
874
+
875
+ self.act_astrospike = QAction(QIcon(astrospike_path), "AstroSpike...", self)
876
+ self.act_astrospike.setIconVisibleInMenu(True)
877
+ self.act_astrospike.setStatusTip("Advanced diffraction spikes with halos, flares and rainbow effects")
878
+ self.act_astrospike.triggered.connect(self._open_astrospike)
879
+
880
+ self.act_exo_detector = QAction(QIcon(exoicon_path), "Exoplanet Detector...", self)
881
+ self.act_exo_detector.setIconVisibleInMenu(True)
882
+ self.act_exo_detector.setStatusTip("Detect exoplanet transits from time-series subs")
883
+ self.act_exo_detector.triggered.connect(self._open_exo_detector)
884
+
885
+ self.act_isophote = QAction(QIcon(isophote_path), "GLIMR -- Isophote Modeler...", self)
886
+ self.act_isophote.setIconVisibleInMenu(True)
887
+ self.act_isophote.setStatusTip("Fit galaxy isophotes and reveal residuals")
888
+ self.act_isophote.triggered.connect(self._open_isophote)
889
+
890
+ self.act_rgb_align = QAction(QIcon(rgbalign_path), "RGB Align...", self)
891
+ self.act_rgb_align.setIconVisibleInMenu(True)
892
+ self.act_rgb_align.setStatusTip("Align R and B channels to G using astroalign (affine/homography/poly)")
893
+ self.act_rgb_align.triggered.connect(self._open_rgb_align)
894
+
895
+ self.act_whats_in_my_sky = QAction(QIcon(wims_path), "What's In My Sky...", self)
896
+ self.act_whats_in_my_sky.setIconVisibleInMenu(True)
897
+ self.act_whats_in_my_sky.setStatusTip("Plan targets by altitude, transit time, and lunar separation")
898
+ self.act_whats_in_my_sky.triggered.connect(self._open_whats_in_my_sky)
899
+
900
+ self.act_wimi = QAction(QIcon(wimi_path), "What's In My Image...", self)
901
+ self.act_wimi.setIconVisibleInMenu(True)
902
+ self.act_wimi.setStatusTip("Identify objects in a plate-solved frame")
903
+ self.act_wimi.triggered.connect(self._open_wimi)
904
+
905
+ # --- Scripts actions ---
906
+ self.act_open_scripts_folder = QAction("Open Scripts Folder...", self)
907
+ self.act_open_scripts_folder.setStatusTip("Open the SASpro user scripts folder")
908
+ self.act_open_scripts_folder.triggered.connect(self._open_scripts_folder)
909
+
910
+ self.act_reload_scripts = QAction("Reload Scripts", self)
911
+ self.act_reload_scripts.setStatusTip("Rescan the scripts folder and reload .py files")
912
+ self.act_reload_scripts.triggered.connect(self._reload_scripts)
913
+
914
+ self.act_create_sample_script = QAction("Create Sample Scripts...", self)
915
+ self.act_create_sample_script.setStatusTip("Write a ready-to-edit sample script into the scripts folder")
916
+ self.act_create_sample_script.triggered.connect(self._create_sample_script)
917
+
918
+ self.act_script_editor = QAction("Script Editor...", self)
919
+ self.act_script_editor.setStatusTip("Open the built-in script editor")
920
+ self.act_script_editor.triggered.connect(self._show_script_editor)
921
+
922
+ self.act_open_user_scripts_github = QAction("Open User Scripts (GitHub)...", self)
923
+ self.act_open_user_scripts_github.triggered.connect(self._open_user_scripts_github)
924
+
925
+ self.act_open_scripts_discord = QAction("Open Scripts Forum (Discord)...", self)
926
+ self.act_open_scripts_discord.triggered.connect(self._open_scripts_discord_forum)
927
+
928
+ # --- FITS Header Modifier action ---
929
+ self.act_fits_modifier = QAction("FITS Header Modifier...", self)
930
+ # self.act_fits_modifier.setIcon(QIcon(path_to_icon)) # (optional) icon goes here later
931
+ self.act_fits_modifier.setIconVisibleInMenu(True)
932
+ self.act_fits_modifier.setStatusTip("View/Edit FITS headers")
933
+ self.act_fits_modifier.triggered.connect(self._open_fits_modifier)
934
+
935
+ self.act_fits_batch_modifier = QAction("FITS Header Batch Modifier...", self)
936
+ # self.act_fits_modifier.setIcon(QIcon(path_to_icon)) # (optional) icon goes here later
937
+ self.act_fits_batch_modifier.setIconVisibleInMenu(True)
938
+ self.act_fits_batch_modifier.setStatusTip("Batch Modify FITS Headers")
939
+ self.act_fits_batch_modifier.triggered.connect(self._open_fits_batch_modifier)
940
+
941
+ self.act_batch_renamer = QAction("Batch Rename from FITS...", self)
942
+ # self.act_batch_renamer.setIcon(QIcon(batch_renamer_icon_path)) # (optional icon)
943
+ self.act_batch_renamer.triggered.connect(self._open_batch_renamer)
944
+
945
+ self.act_astrobin_exporter = QAction("AstroBin Exporter...", self)
946
+ # self.act_astrobin_exporter.setIcon(QIcon(astrobin_icon_path)) # optional icon
947
+ self.act_astrobin_exporter.triggered.connect(self._open_astrobin_exporter)
948
+
949
+ self.act_batch_convert = QAction("Batch Converter...", self)
950
+ # self.act_batch_convert.setIcon(QIcon("path/to/icon.svg")) # optional later
951
+ self.act_batch_convert.triggered.connect(self._open_batch_convert)
952
+
953
+ self.act_copy_astrometry = QAction("Copy Astrometric Solution...", self)
954
+ self.act_copy_astrometry.triggered.connect(self._open_copy_astrometry)
955
+
956
+ # Create Mask
957
+ self.act_create_mask = QAction(QIcon(maskcreate_path), "Create Mask...", self)
958
+ self.act_create_mask.setIconVisibleInMenu(True)
959
+ self.act_create_mask.setStatusTip("Create a mask from the active image")
960
+ self.act_create_mask.triggered.connect(self._action_create_mask)
961
+
962
+ # --- Masks ---
963
+ self.act_apply_mask = QAction(QIcon(maskapply_path), "Apply Mask", self)
964
+ self.act_apply_mask.setStatusTip("Apply a mask document to the active image")
965
+ self.act_apply_mask.triggered.connect(self._apply_mask_menu)
966
+
967
+ self.act_remove_mask = QAction(QIcon(maskremove_path), "Remove Active Mask", self)
968
+ self.act_remove_mask.setStatusTip("Remove the active mask from the active image")
969
+ self.act_remove_mask.triggered.connect(self._remove_mask_menu)
970
+
971
+ self.act_show_mask = QAction("Show Mask Overlay", self)
972
+ self.act_hide_mask = QAction("Hide Mask Overlay", self)
973
+ self.act_show_mask.triggered.connect(self._show_mask_overlay)
974
+ self.act_hide_mask.triggered.connect(self._hide_mask_overlay)
975
+
976
+ self.act_invert_mask = QAction("Invert Mask", self)
977
+ self.act_invert_mask.triggered.connect(self._invert_mask)
978
+ self.act_invert_mask.setShortcut("Ctrl+Shift+I")
979
+
980
+ self.act_check_updates = QAction("Check for Updates...", self)
981
+ self.act_check_updates.triggered.connect(self.check_for_updates_now)
982
+
983
+ self.act_docs = QAction("Documentation...", self)
984
+ self.act_docs.setStatusTip("Open the Seti Astro Suite Pro online documentation")
985
+ self.act_docs.triggered.connect(
986
+ lambda: QDesktopServices.openUrl(QUrl("https://github.com/setiastro/setiastrosuitepro/wiki"))
987
+ )
988
+
989
+ # Qt6-safe shortcut for Help/Docs (F1)
990
+ try:
991
+ # Qt6 enum lives under StandardKey
992
+ self.act_docs.setShortcut(QKeySequence(QKeySequence.StandardKey.HelpContents))
993
+ except Exception:
994
+ # Fallback works everywhere
995
+ self.act_docs.setShortcut(QKeySequence("F1"))
996
+
997
+ self.act_view_bundles = QAction(QIcon(viewbundles_path), "View Bundles...", self)
998
+ self.act_view_bundles.setStatusTip("Create bundles of views; drop shortcuts to apply to all")
999
+ self.act_view_bundles.triggered.connect(self._open_view_bundles)
1000
+
1001
+ self.act_function_bundles = QAction(QIcon(functionbundles_path), "Function Bundles...", self)
1002
+ self.act_function_bundles.setStatusTip("Create and run bundles of functions/shortcuts")
1003
+ self.act_function_bundles.triggered.connect(self._open_function_bundles)
1004
+
1005
+ # give each action a stable id and register
1006
+ def reg(cid, act):
1007
+ act.setProperty("command_id", cid)
1008
+ act.setObjectName(cid) # also becomes default if we ever need it
1009
+ self.shortcuts.register_action(cid, act)
1010
+
1011
+ # create manager once MDI exists
1012
+ if not hasattr(self, "shortcuts"):
1013
+ # self.mdi is your QMdiArea used elsewhere (stat_stretch uses it)
1014
+ self.shortcuts = ShortcutManager(self.mdi, self)
1015
+
1016
+ # register whatever you want draggable/launchable
1017
+ reg("open", self.act_open)
1018
+ reg("save_as", self.act_save)
1019
+ reg("undo", self.act_undo)
1020
+ reg("redo", self.act_redo)
1021
+ reg("autostretch", self.act_autostretch)
1022
+ reg("zoom_1_1", self.act_zoom_1_1)
1023
+ reg("crop", self.act_crop)
1024
+ reg("histogram", self.act_histogram)
1025
+ reg("stat_stretch", self.act_stat_stretch)
1026
+ reg("star_stretch", self.act_star_stretch)
1027
+ reg("curves", self.act_curves)
1028
+ reg("ghs", self.act_ghs)
1029
+ reg("blink", self.act_blink)
1030
+ reg("ppp", self.act_ppp)
1031
+ reg("nbtorgb", self.act_nbtorgb)
1032
+ reg("freqsep", self.act_freqsep)
1033
+ reg("selective_color", self.act_selective_color)
1034
+ reg("contsub", self.act_contsub)
1035
+ reg("abe", self.act_abe)
1036
+ reg("create_mask", self.act_create_mask)
1037
+ reg("graxpert", self.act_graxpert)
1038
+ reg("remove_stars", self.act_remove_stars)
1039
+ reg("add_stars", self.act_add_stars)
1040
+ reg("pedestal", self.act_pedestal)
1041
+ reg("remove_green", self.act_remove_green)
1042
+ reg("background_neutral", self.act_background_neutral)
1043
+ reg("white_balance", self.act_white_balance)
1044
+ reg("sfcc", self.act_sfcc)
1045
+ reg("convo", self.act_convo)
1046
+ reg("extract_luminance", self.act_extract_luma)
1047
+ reg("recombine_luminance", self.act_recombine_luma)
1048
+ reg("rgb_extract", self.act_rgb_extract)
1049
+ reg("rgb_combine", self.act_rgb_combine)
1050
+ reg("blemish_blaster", self.act_blemish)
1051
+ reg("wavescale_hdr", self.act_wavescale_hdr)
1052
+ reg("wavescale_dark_enhance", self.act_wavescale_de)
1053
+ reg("clahe", self.act_clahe)
1054
+ reg("morphology", self.act_morphology)
1055
+ reg("pixel_math", self.act_pixelmath)
1056
+ reg("signature_insert", self.act_signature)
1057
+ reg("halo_b_gon", self.act_halobgon)
1058
+
1059
+ reg("multiscale_decomp", self.act_multiscale_decomp)
1060
+ reg("geom_invert", self.act_geom_invert)
1061
+ reg("geom_flip_horizontal", self.act_geom_flip_h)
1062
+ reg("geom_flip_vertical", self.act_geom_flip_v)
1063
+ reg("geom_rotate_clockwise", self.act_geom_rot_cw)
1064
+ reg("geom_rotate_counterclockwise",self.act_geom_rot_ccw)
1065
+ reg("geom_rotate_180", self.act_geom_rot_180)
1066
+ reg("geom_rescale", self.act_geom_rescale)
1067
+ reg("project_new", self.act_project_new)
1068
+ reg("project_save", self.act_project_save)
1069
+ reg("project_load", self.act_project_load)
1070
+ reg("image_combine", self.act_image_combine)
1071
+ reg("psf_viewer", self.act_psf_viewer)
1072
+ reg("plate_solve", self.act_plate_solve)
1073
+ reg("star_align", self.act_star_align)
1074
+ reg("star_register", self.act_star_register)
1075
+ reg("mosaic_master", self.act_mosaic_master)
1076
+ reg("image_peeker", self.act_image_peeker)
1077
+ reg("live_stacking", self.act_live_stacking)
1078
+ reg("stacking_suite", self.act_stacking_suite)
1079
+ reg("supernova_hunter", self.act_supernova_hunter)
1080
+ reg("star_spikes", self.act_star_spikes)
1081
+ reg("astrospike", self.act_astrospike)
1082
+ reg("exo_detector", self.act_exo_detector)
1083
+ reg("isophote", self.act_isophote)
1084
+ reg("rgb_align", self.act_rgb_align)
1085
+ reg("whats_in_my_sky", self.act_whats_in_my_sky)
1086
+ reg("whats_in_my_image", self.act_wimi)
1087
+ reg("linear_fit", self.act_linear_fit)
1088
+ reg("debayer", self.act_debayer)
1089
+ reg("cosmicclarity", self.actCosmicUI)
1090
+ reg("cosmicclaritysat", self.actCosmicSat)
1091
+ reg("aberrationai", self.actAberrationAI)
1092
+ reg("view_bundles", self.act_view_bundles)
1093
+ reg("function_bundles", self.act_function_bundles)
1094
+
1095
+ def _restore_toolbar_order(self, tb, settings_key: str):
1096
+ """
1097
+ Restore toolbar action order from QSettings, using command_id/objectName.
1098
+ Unknown actions and separators keep their relative order at the end.
1099
+ """
1100
+ if not hasattr(self, "settings"):
1101
+ return
1102
+
1103
+ order = self.settings.value(settings_key, None)
1104
+ if not order:
1105
+ return
1106
+
1107
+ # QSettings may return QVariantList or str; normalize to Python list[str]
1108
+ if isinstance(order, str):
1109
+ # if you ever decide to JSON-encode, you could json.loads here
1110
+ order = [order]
1111
+ try:
1112
+ order_list = list(order)
1113
+ except Exception:
1114
+ return
1115
+
1116
+ actions = list(tb.actions())
1117
+
1118
+ def _cid(act):
1119
+ return act.property("command_id") or act.objectName() or ""
1120
+
1121
+ rank = {str(cid): i for i, cid in enumerate(order_list)}
1122
+ big = len(rank) + len(actions) + 10
1123
+
1124
+ indexed = list(enumerate(actions))
1125
+ indexed.sort(
1126
+ key=lambda pair: (
1127
+ rank.get(str(_cid(pair[1])), big),
1128
+ pair[0],
1129
+ )
1130
+ )
1131
+
1132
+ tb.clear()
1133
+ for _, act in indexed:
1134
+ tb.addAction(act)
1135
+
1136
+ def _restore_toolbar_memberships(self):
1137
+ """
1138
+ Restore which toolbar each action belongs to, based on Toolbar/Assignments.
1139
+
1140
+ We:
1141
+ - Read JSON {command_id: settings_key}.
1142
+ - Collect all DraggableToolBar instances and their settings keys.
1143
+ - Collect all QActions by command_id/objectName.
1144
+ - Move each assigned action to its target toolbar.
1145
+ - Re-apply per-toolbar ordering via _restore_toolbar_order.
1146
+ """
1147
+ if not hasattr(self, "settings"):
1148
+ return
1149
+
1150
+ try:
1151
+ raw = self.settings.value("Toolbar/Assignments", "", type=str) or ""
1152
+ except Exception:
1153
+ return
1154
+
1155
+ try:
1156
+ mapping = json.loads(raw) if raw else {}
1157
+ except Exception:
1158
+ return
1159
+
1160
+ if not mapping:
1161
+ return
1162
+
1163
+ # Gather all DraggableToolBar instances
1164
+ from setiastro.saspro.shortcuts import DraggableToolBar
1165
+ toolbars: list[DraggableToolBar] = [
1166
+ tb for tb in self.findChildren(DraggableToolBar)
1167
+ ]
1168
+
1169
+ tb_by_key: dict[str, DraggableToolBar] = {}
1170
+ for tb in toolbars:
1171
+ key = getattr(tb, "_settings_key", None)
1172
+ if key:
1173
+ tb_by_key[str(key)] = tb
1174
+
1175
+ if not tb_by_key:
1176
+ return
1177
+
1178
+ # Map command_id → QAction
1179
+ from PyQt6.QtGui import QAction
1180
+ acts_by_id: dict[str, QAction] = {}
1181
+ for act in self.findChildren(QAction):
1182
+ cid = act.property("command_id") or act.objectName()
1183
+ if cid:
1184
+ acts_by_id[str(cid)] = act
1185
+
1186
+ # Move actions to their assigned toolbars
1187
+ for cid, key in mapping.items():
1188
+ act = acts_by_id.get(str(cid))
1189
+ tb = tb_by_key.get(str(key))
1190
+ if not act or not tb:
1191
+ continue
1192
+
1193
+ # Remove from any toolbar that currently contains it
1194
+ for t in toolbars:
1195
+ if act in t.actions():
1196
+ t.removeAction(act)
1197
+ # Add to the desired toolbar
1198
+ tb.addAction(act)
1199
+
1200
+ # Re-apply per-toolbar order now that memberships are correct
1201
+ for tb in toolbars:
1202
+ key = getattr(tb, "_settings_key", None)
1203
+ if key:
1204
+ self._restore_toolbar_order(tb, str(key))
1205
+
1206
+
1207
+ def update_undo_redo_action_labels(self):
1208
+ if not hasattr(self, "act_undo"): # not built yet
1209
+ return
1210
+
1211
+ # Always compute against the history root
1212
+ doc = self._active_history_doc()
1213
+
1214
+ if doc:
1215
+ try:
1216
+ can_u = bool(doc.can_undo()) if hasattr(doc, "can_undo") else False
1217
+ except Exception:
1218
+ can_u = False
1219
+ try:
1220
+ can_r = bool(doc.can_redo()) if hasattr(doc, "can_redo") else False
1221
+ except Exception:
1222
+ can_r = False
1223
+
1224
+ undo_name = None
1225
+ redo_name = None
1226
+ try:
1227
+ undo_name = doc.last_undo_name() if hasattr(doc, "last_undo_name") else None
1228
+ except Exception:
1229
+ pass
1230
+ try:
1231
+ redo_name = doc.last_redo_name() if hasattr(doc, "last_redo_name") else None
1232
+ except Exception:
1233
+ pass
1234
+
1235
+ self.act_undo.setText(f"Undo {undo_name}" if (can_u and undo_name) else "Undo")
1236
+ self.act_redo.setText(f"Redo {redo_name}" if (can_r and redo_name) else "Redo")
1237
+
1238
+ self.act_undo.setToolTip("Nothing to undo" if not can_u else (f"Undo: {undo_name}" if undo_name else "Undo last action"))
1239
+ self.act_redo.setToolTip("Nothing to redo" if not can_r else (f"Redo: {redo_name}" if redo_name else "Redo last action"))
1240
+
1241
+ self.act_undo.setStatusTip(self.act_undo.toolTip())
1242
+ self.act_redo.setStatusTip(self.act_redo.toolTip())
1243
+
1244
+ self.act_undo.setEnabled(can_u)
1245
+ self.act_redo.setEnabled(can_r)
1246
+ else:
1247
+ # No active doc
1248
+ for a, tip in ((self.act_undo, "Nothing to undo"),
1249
+ (self.act_redo, "Nothing to redo")):
1250
+ # Normalize label to plain "Undo"/"Redo"
1251
+ base = "Undo" if "Undo" in a.text() else ("Redo" if "Redo" in a.text() else a.text())
1252
+ a.setText(base)
1253
+ a.setToolTip(tip)
1254
+ a.setStatusTip(tip)
1255
+ a.setEnabled(False)
1256
+
1257
+ def _sync_link_action_state(self):
1258
+ g = self._current_group_of_active()
1259
+ self.act_link_group.blockSignals(True)
1260
+ try:
1261
+ self.act_link_group.setChecked(bool(g))
1262
+ self.act_link_group.setText(f"Link Pan/Zoom{'' if not g else f' ({g})'}")
1263
+ try:
1264
+ if getattr(self, "_link_btn", None):
1265
+ self._link_btn.setText(self.act_link_group.text())
1266
+ except Exception:
1267
+ pass
1268
+ finally:
1269
+ self.act_link_group.blockSignals(False)
1270
+
1271
+ def _undo_active(self):
1272
+ doc = self._active_history_doc()
1273
+ if doc and getattr(doc, "can_undo", lambda: False)():
1274
+ # Ensure the correct view is active so Qt routes shortcut focus correctly
1275
+ sw = self._subwindow_for_history_doc(doc)
1276
+ if sw is not None:
1277
+ try:
1278
+ self.mdi.setActiveSubWindow(sw)
1279
+ except Exception:
1280
+ pass
1281
+ name = doc.undo()
1282
+ if name:
1283
+ self._log(f"Undo: {name}")
1284
+ # Defer label refresh to end of event loop (lets views repaint first)
1285
+ QTimer.singleShot(0, self.update_undo_redo_action_labels)
1286
+
1287
+ def _redo_active(self):
1288
+ doc = self._active_history_doc()
1289
+ if doc and getattr(doc, "can_redo", lambda: False)():
1290
+ sw = self._subwindow_for_history_doc(doc)
1291
+ if sw is not None:
1292
+ try:
1293
+ self.mdi.setActiveSubWindow(sw)
1294
+ except Exception:
1295
+ pass
1296
+ name = doc.redo()
1297
+ if name:
1298
+ self._log(f"Redo: {name}")
1299
+ QTimer.singleShot(0, self.update_undo_redo_action_labels)
1300
+
1301
+ def _refresh_mask_action_states(self):
1302
+ active_doc = self._active_doc()
1303
+
1304
+ can_apply = bool(active_doc and self._list_candidate_mask_sources(exclude_doc=active_doc))
1305
+ can_remove = bool(active_doc and getattr(active_doc, "active_mask_id", None))
1306
+
1307
+ if hasattr(self, "act_apply_mask"):
1308
+ self.act_apply_mask.setEnabled(can_apply)
1309
+ if hasattr(self, "act_remove_mask"):
1310
+ self.act_remove_mask.setEnabled(can_remove)
1311
+
1312
+ # NEW: enable/disable Invert
1313
+ if hasattr(self, "act_invert_mask"):
1314
+ self.act_invert_mask.setEnabled(can_remove)
1315
+
1316
+ vw = self._active_view()
1317
+ overlay_on = bool(getattr(vw, "show_mask_overlay", False)) if vw else False
1318
+ has_mask = bool(active_doc and getattr(active_doc, "active_mask_id", None))
1319
+
1320
+ if hasattr(self, "act_show_mask"):
1321
+ self.act_show_mask.setEnabled(has_mask and not overlay_on)
1322
+ if hasattr(self, "act_hide_mask"):
1323
+ self.act_hide_mask.setEnabled(has_mask and overlay_on)
1324
+