setiastrosuitepro 1.6.5.post3__py3-none-any.whl

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