setiastrosuitepro 1.6.7__py3-none-any.whl

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

Potentially problematic release.


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

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