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