setiastrosuitepro 1.6.2.post1__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 (367) hide show
  1. setiastro/__init__.py +2 -0
  2. setiastro/data/SASP_data.fits +0 -0
  3. setiastro/data/catalogs/List_of_Galaxies_with_Distances_Gly.csv +488 -0
  4. setiastro/data/catalogs/astrobin_filters.csv +890 -0
  5. setiastro/data/catalogs/astrobin_filters_page1_local.csv +51 -0
  6. setiastro/data/catalogs/cali2.csv +63 -0
  7. setiastro/data/catalogs/cali2color.csv +65 -0
  8. setiastro/data/catalogs/celestial_catalog - original.csv +16471 -0
  9. setiastro/data/catalogs/celestial_catalog.csv +24031 -0
  10. setiastro/data/catalogs/detected_stars.csv +24784 -0
  11. setiastro/data/catalogs/fits_header_data.csv +46 -0
  12. setiastro/data/catalogs/test.csv +8 -0
  13. setiastro/data/catalogs/updated_celestial_catalog.csv +16471 -0
  14. setiastro/images/Astro_Spikes.png +0 -0
  15. setiastro/images/Background_startup.jpg +0 -0
  16. setiastro/images/HRDiagram.png +0 -0
  17. setiastro/images/LExtract.png +0 -0
  18. setiastro/images/LInsert.png +0 -0
  19. setiastro/images/Oxygenation-atm-2.svg.png +0 -0
  20. setiastro/images/RGB080604.png +0 -0
  21. setiastro/images/abeicon.png +0 -0
  22. setiastro/images/aberration.png +0 -0
  23. setiastro/images/andromedatry.png +0 -0
  24. setiastro/images/andromedatry_satellited.png +0 -0
  25. setiastro/images/annotated.png +0 -0
  26. setiastro/images/aperture.png +0 -0
  27. setiastro/images/astrosuite.ico +0 -0
  28. setiastro/images/astrosuite.png +0 -0
  29. setiastro/images/astrosuitepro.icns +0 -0
  30. setiastro/images/astrosuitepro.ico +0 -0
  31. setiastro/images/astrosuitepro.png +0 -0
  32. setiastro/images/background.png +0 -0
  33. setiastro/images/background2.png +0 -0
  34. setiastro/images/benchmark.png +0 -0
  35. setiastro/images/big_moon_stabilizer_timeline.png +0 -0
  36. setiastro/images/big_moon_stabilizer_timeline_clean.png +0 -0
  37. setiastro/images/blaster.png +0 -0
  38. setiastro/images/blink.png +0 -0
  39. setiastro/images/clahe.png +0 -0
  40. setiastro/images/collage.png +0 -0
  41. setiastro/images/colorwheel.png +0 -0
  42. setiastro/images/contsub.png +0 -0
  43. setiastro/images/convo.png +0 -0
  44. setiastro/images/copyslot.png +0 -0
  45. setiastro/images/cosmic.png +0 -0
  46. setiastro/images/cosmicsat.png +0 -0
  47. setiastro/images/crop1.png +0 -0
  48. setiastro/images/cropicon.png +0 -0
  49. setiastro/images/curves.png +0 -0
  50. setiastro/images/cvs.png +0 -0
  51. setiastro/images/debayer.png +0 -0
  52. setiastro/images/denoise_cnn_custom.png +0 -0
  53. setiastro/images/denoise_cnn_graph.png +0 -0
  54. setiastro/images/disk.png +0 -0
  55. setiastro/images/dse.png +0 -0
  56. setiastro/images/exoicon.png +0 -0
  57. setiastro/images/eye.png +0 -0
  58. setiastro/images/fliphorizontal.png +0 -0
  59. setiastro/images/flipvertical.png +0 -0
  60. setiastro/images/font.png +0 -0
  61. setiastro/images/freqsep.png +0 -0
  62. setiastro/images/functionbundle.png +0 -0
  63. setiastro/images/graxpert.png +0 -0
  64. setiastro/images/green.png +0 -0
  65. setiastro/images/gridicon.png +0 -0
  66. setiastro/images/halo.png +0 -0
  67. setiastro/images/hdr.png +0 -0
  68. setiastro/images/histogram.png +0 -0
  69. setiastro/images/hubble.png +0 -0
  70. setiastro/images/imagecombine.png +0 -0
  71. setiastro/images/invert.png +0 -0
  72. setiastro/images/isophote.png +0 -0
  73. setiastro/images/isophote_demo_figure.png +0 -0
  74. setiastro/images/isophote_demo_image.png +0 -0
  75. setiastro/images/isophote_demo_model.png +0 -0
  76. setiastro/images/isophote_demo_residual.png +0 -0
  77. setiastro/images/jwstpupil.png +0 -0
  78. setiastro/images/linearfit.png +0 -0
  79. setiastro/images/livestacking.png +0 -0
  80. setiastro/images/mask.png +0 -0
  81. setiastro/images/maskapply.png +0 -0
  82. setiastro/images/maskcreate.png +0 -0
  83. setiastro/images/maskremove.png +0 -0
  84. setiastro/images/morpho.png +0 -0
  85. setiastro/images/mosaic.png +0 -0
  86. setiastro/images/multiscale_decomp.png +0 -0
  87. setiastro/images/nbtorgb.png +0 -0
  88. setiastro/images/neutral.png +0 -0
  89. setiastro/images/nuke.png +0 -0
  90. setiastro/images/openfile.png +0 -0
  91. setiastro/images/pedestal.png +0 -0
  92. setiastro/images/pen.png +0 -0
  93. setiastro/images/pixelmath.png +0 -0
  94. setiastro/images/platesolve.png +0 -0
  95. setiastro/images/ppp.png +0 -0
  96. setiastro/images/pro.png +0 -0
  97. setiastro/images/project.png +0 -0
  98. setiastro/images/psf.png +0 -0
  99. setiastro/images/redo.png +0 -0
  100. setiastro/images/redoicon.png +0 -0
  101. setiastro/images/rescale.png +0 -0
  102. setiastro/images/rgbalign.png +0 -0
  103. setiastro/images/rgbcombo.png +0 -0
  104. setiastro/images/rgbextract.png +0 -0
  105. setiastro/images/rotate180.png +0 -0
  106. setiastro/images/rotateclockwise.png +0 -0
  107. setiastro/images/rotatecounterclockwise.png +0 -0
  108. setiastro/images/satellite.png +0 -0
  109. setiastro/images/script.png +0 -0
  110. setiastro/images/selectivecolor.png +0 -0
  111. setiastro/images/simbad.png +0 -0
  112. setiastro/images/slot0.png +0 -0
  113. setiastro/images/slot1.png +0 -0
  114. setiastro/images/slot2.png +0 -0
  115. setiastro/images/slot3.png +0 -0
  116. setiastro/images/slot4.png +0 -0
  117. setiastro/images/slot5.png +0 -0
  118. setiastro/images/slot6.png +0 -0
  119. setiastro/images/slot7.png +0 -0
  120. setiastro/images/slot8.png +0 -0
  121. setiastro/images/slot9.png +0 -0
  122. setiastro/images/spcc.png +0 -0
  123. setiastro/images/spin_precession_vs_lunar_distance.png +0 -0
  124. setiastro/images/spinner.gif +0 -0
  125. setiastro/images/stacking.png +0 -0
  126. setiastro/images/staradd.png +0 -0
  127. setiastro/images/staralign.png +0 -0
  128. setiastro/images/starnet.png +0 -0
  129. setiastro/images/starregistration.png +0 -0
  130. setiastro/images/starspike.png +0 -0
  131. setiastro/images/starstretch.png +0 -0
  132. setiastro/images/statstretch.png +0 -0
  133. setiastro/images/supernova.png +0 -0
  134. setiastro/images/uhs.png +0 -0
  135. setiastro/images/undoicon.png +0 -0
  136. setiastro/images/upscale.png +0 -0
  137. setiastro/images/viewbundle.png +0 -0
  138. setiastro/images/whitebalance.png +0 -0
  139. setiastro/images/wimi_icon_256x256.png +0 -0
  140. setiastro/images/wimilogo.png +0 -0
  141. setiastro/images/wims.png +0 -0
  142. setiastro/images/wrench_icon.png +0 -0
  143. setiastro/images/xisfliberator.png +0 -0
  144. setiastro/qml/ResourceMonitor.qml +126 -0
  145. setiastro/saspro/__init__.py +20 -0
  146. setiastro/saspro/__main__.py +945 -0
  147. setiastro/saspro/_generated/__init__.py +7 -0
  148. setiastro/saspro/_generated/build_info.py +3 -0
  149. setiastro/saspro/abe.py +1346 -0
  150. setiastro/saspro/abe_preset.py +196 -0
  151. setiastro/saspro/aberration_ai.py +694 -0
  152. setiastro/saspro/aberration_ai_preset.py +224 -0
  153. setiastro/saspro/accel_installer.py +218 -0
  154. setiastro/saspro/accel_workers.py +30 -0
  155. setiastro/saspro/add_stars.py +624 -0
  156. setiastro/saspro/astrobin_exporter.py +1010 -0
  157. setiastro/saspro/astrospike.py +153 -0
  158. setiastro/saspro/astrospike_python.py +1841 -0
  159. setiastro/saspro/autostretch.py +198 -0
  160. setiastro/saspro/backgroundneutral.py +602 -0
  161. setiastro/saspro/batch_convert.py +328 -0
  162. setiastro/saspro/batch_renamer.py +522 -0
  163. setiastro/saspro/blemish_blaster.py +491 -0
  164. setiastro/saspro/blink_comparator_pro.py +2926 -0
  165. setiastro/saspro/bundles.py +61 -0
  166. setiastro/saspro/bundles_dock.py +114 -0
  167. setiastro/saspro/cheat_sheet.py +213 -0
  168. setiastro/saspro/clahe.py +368 -0
  169. setiastro/saspro/comet_stacking.py +1442 -0
  170. setiastro/saspro/common_tr.py +107 -0
  171. setiastro/saspro/config.py +38 -0
  172. setiastro/saspro/config_bootstrap.py +40 -0
  173. setiastro/saspro/config_manager.py +316 -0
  174. setiastro/saspro/continuum_subtract.py +1617 -0
  175. setiastro/saspro/convo.py +1400 -0
  176. setiastro/saspro/convo_preset.py +414 -0
  177. setiastro/saspro/copyastro.py +190 -0
  178. setiastro/saspro/cosmicclarity.py +1589 -0
  179. setiastro/saspro/cosmicclarity_preset.py +407 -0
  180. setiastro/saspro/crop_dialog_pro.py +973 -0
  181. setiastro/saspro/crop_preset.py +189 -0
  182. setiastro/saspro/curve_editor_pro.py +2562 -0
  183. setiastro/saspro/curves_preset.py +375 -0
  184. setiastro/saspro/debayer.py +673 -0
  185. setiastro/saspro/debug_utils.py +29 -0
  186. setiastro/saspro/dnd_mime.py +35 -0
  187. setiastro/saspro/doc_manager.py +2664 -0
  188. setiastro/saspro/exoplanet_detector.py +2166 -0
  189. setiastro/saspro/file_utils.py +284 -0
  190. setiastro/saspro/fitsmodifier.py +748 -0
  191. setiastro/saspro/fix_bom.py +32 -0
  192. setiastro/saspro/free_torch_memory.py +48 -0
  193. setiastro/saspro/frequency_separation.py +1349 -0
  194. setiastro/saspro/function_bundle.py +1596 -0
  195. setiastro/saspro/generate_translations.py +3092 -0
  196. setiastro/saspro/ghs_dialog_pro.py +663 -0
  197. setiastro/saspro/ghs_preset.py +284 -0
  198. setiastro/saspro/graxpert.py +637 -0
  199. setiastro/saspro/graxpert_preset.py +287 -0
  200. setiastro/saspro/gui/__init__.py +0 -0
  201. setiastro/saspro/gui/main_window.py +8810 -0
  202. setiastro/saspro/gui/mixins/__init__.py +33 -0
  203. setiastro/saspro/gui/mixins/dock_mixin.py +362 -0
  204. setiastro/saspro/gui/mixins/file_mixin.py +450 -0
  205. setiastro/saspro/gui/mixins/geometry_mixin.py +403 -0
  206. setiastro/saspro/gui/mixins/header_mixin.py +441 -0
  207. setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
  208. setiastro/saspro/gui/mixins/menu_mixin.py +389 -0
  209. setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
  210. setiastro/saspro/gui/mixins/toolbar_mixin.py +1457 -0
  211. setiastro/saspro/gui/mixins/update_mixin.py +309 -0
  212. setiastro/saspro/gui/mixins/view_mixin.py +435 -0
  213. setiastro/saspro/gui/statistics_dialog.py +47 -0
  214. setiastro/saspro/halobgon.py +488 -0
  215. setiastro/saspro/header_viewer.py +448 -0
  216. setiastro/saspro/headless_utils.py +88 -0
  217. setiastro/saspro/histogram.py +756 -0
  218. setiastro/saspro/history_explorer.py +941 -0
  219. setiastro/saspro/i18n.py +168 -0
  220. setiastro/saspro/image_combine.py +417 -0
  221. setiastro/saspro/image_peeker_pro.py +1604 -0
  222. setiastro/saspro/imageops/__init__.py +37 -0
  223. setiastro/saspro/imageops/mdi_snap.py +292 -0
  224. setiastro/saspro/imageops/scnr.py +36 -0
  225. setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
  226. setiastro/saspro/imageops/stretch.py +236 -0
  227. setiastro/saspro/isophote.py +1182 -0
  228. setiastro/saspro/layers.py +208 -0
  229. setiastro/saspro/layers_dock.py +714 -0
  230. setiastro/saspro/lazy_imports.py +193 -0
  231. setiastro/saspro/legacy/__init__.py +2 -0
  232. setiastro/saspro/legacy/image_manager.py +2226 -0
  233. setiastro/saspro/legacy/numba_utils.py +3676 -0
  234. setiastro/saspro/legacy/xisf.py +1071 -0
  235. setiastro/saspro/linear_fit.py +537 -0
  236. setiastro/saspro/live_stacking.py +1841 -0
  237. setiastro/saspro/log_bus.py +5 -0
  238. setiastro/saspro/logging_config.py +460 -0
  239. setiastro/saspro/luminancerecombine.py +309 -0
  240. setiastro/saspro/main_helpers.py +201 -0
  241. setiastro/saspro/mask_creation.py +931 -0
  242. setiastro/saspro/masks_core.py +56 -0
  243. setiastro/saspro/mdi_widgets.py +353 -0
  244. setiastro/saspro/memory_utils.py +666 -0
  245. setiastro/saspro/metadata_patcher.py +75 -0
  246. setiastro/saspro/mfdeconv.py +3831 -0
  247. setiastro/saspro/mfdeconv_earlystop.py +71 -0
  248. setiastro/saspro/mfdeconvcudnn.py +3263 -0
  249. setiastro/saspro/mfdeconvsport.py +2382 -0
  250. setiastro/saspro/minorbodycatalog.py +567 -0
  251. setiastro/saspro/morphology.py +407 -0
  252. setiastro/saspro/multiscale_decomp.py +1293 -0
  253. setiastro/saspro/nbtorgb_stars.py +541 -0
  254. setiastro/saspro/numba_utils.py +3145 -0
  255. setiastro/saspro/numba_warmup.py +141 -0
  256. setiastro/saspro/ops/__init__.py +9 -0
  257. setiastro/saspro/ops/command_help_dialog.py +623 -0
  258. setiastro/saspro/ops/command_runner.py +217 -0
  259. setiastro/saspro/ops/commands.py +1594 -0
  260. setiastro/saspro/ops/script_editor.py +1102 -0
  261. setiastro/saspro/ops/scripts.py +1473 -0
  262. setiastro/saspro/ops/settings.py +637 -0
  263. setiastro/saspro/parallel_utils.py +554 -0
  264. setiastro/saspro/pedestal.py +121 -0
  265. setiastro/saspro/perfect_palette_picker.py +1071 -0
  266. setiastro/saspro/pipeline.py +110 -0
  267. setiastro/saspro/pixelmath.py +1604 -0
  268. setiastro/saspro/plate_solver.py +2445 -0
  269. setiastro/saspro/project_io.py +797 -0
  270. setiastro/saspro/psf_utils.py +136 -0
  271. setiastro/saspro/psf_viewer.py +549 -0
  272. setiastro/saspro/pyi_rthook_astroquery.py +95 -0
  273. setiastro/saspro/remove_green.py +331 -0
  274. setiastro/saspro/remove_stars.py +1599 -0
  275. setiastro/saspro/remove_stars_preset.py +404 -0
  276. setiastro/saspro/resources.py +501 -0
  277. setiastro/saspro/rgb_combination.py +208 -0
  278. setiastro/saspro/rgb_extract.py +19 -0
  279. setiastro/saspro/rgbalign.py +723 -0
  280. setiastro/saspro/runtime_imports.py +7 -0
  281. setiastro/saspro/runtime_torch.py +754 -0
  282. setiastro/saspro/save_options.py +73 -0
  283. setiastro/saspro/selective_color.py +1552 -0
  284. setiastro/saspro/sfcc.py +1472 -0
  285. setiastro/saspro/shortcuts.py +3043 -0
  286. setiastro/saspro/signature_insert.py +1102 -0
  287. setiastro/saspro/stacking_suite.py +18470 -0
  288. setiastro/saspro/star_alignment.py +7435 -0
  289. setiastro/saspro/star_alignment_preset.py +329 -0
  290. setiastro/saspro/star_metrics.py +49 -0
  291. setiastro/saspro/star_spikes.py +765 -0
  292. setiastro/saspro/star_stretch.py +507 -0
  293. setiastro/saspro/stat_stretch.py +538 -0
  294. setiastro/saspro/status_log_dock.py +78 -0
  295. setiastro/saspro/subwindow.py +3328 -0
  296. setiastro/saspro/supernovaasteroidhunter.py +1719 -0
  297. setiastro/saspro/swap_manager.py +99 -0
  298. setiastro/saspro/torch_backend.py +89 -0
  299. setiastro/saspro/torch_rejection.py +434 -0
  300. setiastro/saspro/translations/all_source_strings.json +3654 -0
  301. setiastro/saspro/translations/ar_translations.py +3865 -0
  302. setiastro/saspro/translations/de_translations.py +3749 -0
  303. setiastro/saspro/translations/es_translations.py +3939 -0
  304. setiastro/saspro/translations/fr_translations.py +3858 -0
  305. setiastro/saspro/translations/hi_translations.py +3571 -0
  306. setiastro/saspro/translations/integrate_translations.py +270 -0
  307. setiastro/saspro/translations/it_translations.py +3678 -0
  308. setiastro/saspro/translations/ja_translations.py +3601 -0
  309. setiastro/saspro/translations/pt_translations.py +3869 -0
  310. setiastro/saspro/translations/ru_translations.py +2848 -0
  311. setiastro/saspro/translations/saspro_ar.qm +0 -0
  312. setiastro/saspro/translations/saspro_ar.ts +255 -0
  313. setiastro/saspro/translations/saspro_de.qm +0 -0
  314. setiastro/saspro/translations/saspro_de.ts +253 -0
  315. setiastro/saspro/translations/saspro_es.qm +0 -0
  316. setiastro/saspro/translations/saspro_es.ts +12520 -0
  317. setiastro/saspro/translations/saspro_fr.qm +0 -0
  318. setiastro/saspro/translations/saspro_fr.ts +12514 -0
  319. setiastro/saspro/translations/saspro_hi.qm +0 -0
  320. setiastro/saspro/translations/saspro_hi.ts +257 -0
  321. setiastro/saspro/translations/saspro_it.qm +0 -0
  322. setiastro/saspro/translations/saspro_it.ts +12520 -0
  323. setiastro/saspro/translations/saspro_ja.qm +0 -0
  324. setiastro/saspro/translations/saspro_ja.ts +257 -0
  325. setiastro/saspro/translations/saspro_pt.qm +0 -0
  326. setiastro/saspro/translations/saspro_pt.ts +257 -0
  327. setiastro/saspro/translations/saspro_ru.qm +0 -0
  328. setiastro/saspro/translations/saspro_ru.ts +237 -0
  329. setiastro/saspro/translations/saspro_sw.qm +0 -0
  330. setiastro/saspro/translations/saspro_sw.ts +257 -0
  331. setiastro/saspro/translations/saspro_uk.qm +0 -0
  332. setiastro/saspro/translations/saspro_uk.ts +10771 -0
  333. setiastro/saspro/translations/saspro_zh.qm +0 -0
  334. setiastro/saspro/translations/saspro_zh.ts +12520 -0
  335. setiastro/saspro/translations/sw_translations.py +3671 -0
  336. setiastro/saspro/translations/uk_translations.py +3700 -0
  337. setiastro/saspro/translations/zh_translations.py +3675 -0
  338. setiastro/saspro/versioning.py +77 -0
  339. setiastro/saspro/view_bundle.py +1558 -0
  340. setiastro/saspro/wavescale_hdr.py +645 -0
  341. setiastro/saspro/wavescale_hdr_preset.py +101 -0
  342. setiastro/saspro/wavescalede.py +680 -0
  343. setiastro/saspro/wavescalede_preset.py +230 -0
  344. setiastro/saspro/wcs_update.py +374 -0
  345. setiastro/saspro/whitebalance.py +492 -0
  346. setiastro/saspro/widgets/__init__.py +48 -0
  347. setiastro/saspro/widgets/common_utilities.py +306 -0
  348. setiastro/saspro/widgets/graphics_views.py +122 -0
  349. setiastro/saspro/widgets/image_utils.py +518 -0
  350. setiastro/saspro/widgets/minigame/game.js +986 -0
  351. setiastro/saspro/widgets/minigame/index.html +53 -0
  352. setiastro/saspro/widgets/minigame/style.css +241 -0
  353. setiastro/saspro/widgets/preview_dialogs.py +280 -0
  354. setiastro/saspro/widgets/resource_monitor.py +237 -0
  355. setiastro/saspro/widgets/spinboxes.py +275 -0
  356. setiastro/saspro/widgets/themed_buttons.py +13 -0
  357. setiastro/saspro/widgets/wavelet_utils.py +331 -0
  358. setiastro/saspro/wimi.py +7996 -0
  359. setiastro/saspro/wims.py +578 -0
  360. setiastro/saspro/window_shelf.py +185 -0
  361. setiastro/saspro/xisf.py +1123 -0
  362. setiastrosuitepro-1.6.2.post1.dist-info/METADATA +278 -0
  363. setiastrosuitepro-1.6.2.post1.dist-info/RECORD +367 -0
  364. setiastrosuitepro-1.6.2.post1.dist-info/WHEEL +4 -0
  365. setiastrosuitepro-1.6.2.post1.dist-info/entry_points.txt +6 -0
  366. setiastrosuitepro-1.6.2.post1.dist-info/licenses/LICENSE +674 -0
  367. setiastrosuitepro-1.6.2.post1.dist-info/licenses/license.txt +2580 -0
@@ -0,0 +1,714 @@
1
+ #pro.layers_dock.py
2
+ from __future__ import annotations
3
+ from typing import Optional
4
+ import json
5
+ import numpy as np
6
+
7
+ from PyQt6.QtCore import Qt, pyqtSignal, QByteArray, QTimer
8
+ from PyQt6.QtWidgets import (
9
+ QDockWidget, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QComboBox,
10
+ QListWidget, QListWidgetItem, QAbstractItemView, QSlider, QCheckBox,
11
+ QPushButton, QFrame, QMessageBox
12
+ )
13
+ from PyQt6.QtGui import QIcon, QDragEnterEvent, QDropEvent, QPixmap, QCursor
14
+
15
+ from setiastro.saspro.dnd_mime import MIME_VIEWSTATE, MIME_MASK
16
+ from setiastro.saspro.layers import composite_stack, ImageLayer, BLEND_MODES
17
+
18
+ # ---------- Small row widget for a layer ----------
19
+ class _LayerRow(QWidget):
20
+ changed = pyqtSignal()
21
+ requestDelete = pyqtSignal()
22
+ moveUp = pyqtSignal()
23
+ moveDown = pyqtSignal()
24
+
25
+ def __init__(self, name: str, mode: str = "Normal", opacity: float = 1.0,
26
+ visible: bool = True, parent=None, *, is_base: bool = False):
27
+ super().__init__(parent)
28
+ self._name = name
29
+ self._is_base = bool(is_base)
30
+
31
+ v = QVBoxLayout(self); v.setContentsMargins(6, 2, 6, 2)
32
+
33
+ # row 1: visibility, name, mode, opacity, reorder/delete
34
+ r1 = QHBoxLayout(); v.addLayout(r1)
35
+ self.chk = QCheckBox(); self.chk.setChecked(visible)
36
+ self.lbl = QLabel(name)
37
+ self.mode = QComboBox(); self.mode.addItems(BLEND_MODES)
38
+ try: self.mode.setCurrentIndex(max(0, BLEND_MODES.index(mode)))
39
+ except Exception: self.mode.setCurrentIndex(0)
40
+ self.sld = QSlider(Qt.Orientation.Horizontal); self.sld.setRange(0, 100); self.sld.setValue(int(round(opacity*100)))
41
+ self.btn_up = QPushButton("↑"); self.btn_up.setFixedWidth(28)
42
+ self.btn_dn = QPushButton("↓"); self.btn_dn.setFixedWidth(28)
43
+ self.btn_x = QPushButton("✕"); self.btn_x.setFixedWidth(28)
44
+
45
+ r1.addWidget(self.chk); r1.addWidget(self.lbl, 1)
46
+ r1.addWidget(self.mode); r1.addWidget(QLabel("Opacity")); r1.addWidget(self.sld, 1)
47
+ r1.addWidget(self.btn_up); r1.addWidget(self.btn_dn); r1.addWidget(self.btn_x)
48
+
49
+ # row 2: mask controls (hidden for base)
50
+ r2 = QHBoxLayout(); v.addLayout(r2)
51
+ self.mask_combo = QComboBox(); self.mask_combo.setMinimumWidth(140)
52
+ self.mask_combo.setPlaceholderText("Mask: (none)")
53
+ self.mask_invert = QCheckBox("Invert")
54
+ self.btn_clear_mask = QPushButton("Clear")
55
+ self.btn_clear_mask.setFixedWidth(52)
56
+ r2.addWidget(QLabel("Mask")); r2.addWidget(self.mask_combo, 1)
57
+ r2.addWidget(self.mask_invert); r2.addWidget(self.btn_clear_mask)
58
+
59
+ # Extra controls for some blend modes (e.g. Sigmoid)
60
+ self.sig_center_label = None
61
+ self.sig_center = None
62
+ self.sig_strength_label = None
63
+ self.sig_strength = None
64
+
65
+ if not self._is_base:
66
+ # row 3: Sigmoid parameters
67
+ r3 = QHBoxLayout(); v.addLayout(r3)
68
+
69
+ self.sig_center_label = QLabel("Sigmoid center")
70
+ from PyQt6.QtWidgets import QDoubleSpinBox
71
+ self.sig_center = QDoubleSpinBox()
72
+ self.sig_center.setRange(0.0, 1.0)
73
+ self.sig_center.setSingleStep(0.01)
74
+ self.sig_center.setDecimals(3)
75
+ self.sig_center.setValue(0.5)
76
+
77
+ self.sig_strength_label = QLabel("Strength")
78
+ self.sig_strength = QDoubleSpinBox()
79
+ self.sig_strength.setRange(0.1, 50.0)
80
+ self.sig_strength.setSingleStep(0.5)
81
+ self.sig_strength.setDecimals(2)
82
+ self.sig_strength.setValue(10.0)
83
+
84
+ r3.addWidget(self.sig_center_label)
85
+ r3.addWidget(self.sig_center)
86
+ r3.addWidget(self.sig_strength_label)
87
+ r3.addWidget(self.sig_strength)
88
+ r3.addStretch(1)
89
+
90
+ if self._is_base:
91
+ # Base row is informational only
92
+ for w in (self.chk, self.mode, self.sld, self.btn_up, self.btn_dn, self.btn_x,
93
+ self.mask_combo, self.mask_invert, self.btn_clear_mask):
94
+ w.setEnabled(False)
95
+ self.lbl.setStyleSheet("color: palette(mid);")
96
+ else:
97
+ self.chk.stateChanged.connect(self._emit)
98
+ self.mode.currentIndexChanged.connect(self._on_mode_changed)
99
+ self.sld.valueChanged.connect(self._emit)
100
+ self.mask_combo.currentIndexChanged.connect(self._emit)
101
+ self.mask_invert.stateChanged.connect(self._emit)
102
+ self.btn_clear_mask.clicked.connect(self._on_clear_mask)
103
+ self.btn_x.clicked.connect(self.requestDelete.emit)
104
+ self.btn_up.clicked.connect(self.moveUp.emit)
105
+ self.btn_dn.clicked.connect(self.moveDown.emit)
106
+
107
+ # Sigmoid controls emit change + only show for Sigmoid mode
108
+ if self.sig_center is not None:
109
+ self.sig_center.valueChanged.connect(self._emit)
110
+ if self.sig_strength is not None:
111
+ self.sig_strength.valueChanged.connect(self._emit)
112
+
113
+ self.mode.currentIndexChanged.connect(
114
+ lambda _i: self._update_extra_controls(self.mode.currentText())
115
+ )
116
+ # Initial visibility
117
+ self._update_extra_controls(self.mode.currentText())
118
+
119
+ def _on_mode_changed(self, _idx: int):
120
+ # Update which extra controls are visible
121
+ self._update_extra_controls(self.mode.currentText())
122
+ # Make our layout recompute height
123
+ lay = self.layout()
124
+ if lay is not None:
125
+ lay.invalidate()
126
+ lay.activate()
127
+
128
+ self.adjustSize()
129
+ self.updateGeometry()
130
+ # Tell the dock “something changed”
131
+ self._emit()
132
+
133
+ def _update_extra_controls(self, mode_text: str):
134
+ is_sig = (mode_text == "Sigmoid")
135
+ for w in (self.sig_center_label, self.sig_center,
136
+ self.sig_strength_label, self.sig_strength):
137
+ if w is not None:
138
+ w.setVisible(is_sig)
139
+
140
+
141
+ def _update_extra_controls(self, mode_text: str):
142
+ is_sig = (mode_text == "Sigmoid")
143
+ for w in (self.sig_center_label, self.sig_center,
144
+ self.sig_strength_label, self.sig_strength):
145
+ if w is not None:
146
+ w.setVisible(is_sig)
147
+
148
+ # Let the layout recompute our preferred height
149
+ self.adjustSize()
150
+ self.updateGeometry()
151
+
152
+ def set_sigmoid_params(self, center: float, strength: float):
153
+ if self.sig_center is None or self.sig_strength is None:
154
+ return
155
+ self.sig_center.blockSignals(True)
156
+ self.sig_strength.blockSignals(True)
157
+ self.sig_center.setValue(float(center))
158
+ self.sig_strength.setValue(float(strength))
159
+ self.sig_center.blockSignals(False)
160
+ self.sig_strength.blockSignals(False)
161
+ self._update_extra_controls(self.mode.currentText())
162
+
163
+
164
+ def _on_clear_mask(self):
165
+ # select the explicit "(none)" entry
166
+ self.mask_combo.setCurrentIndex(0)
167
+ self._emit()
168
+
169
+ def _emit(self, *_):
170
+ self.changed.emit()
171
+
172
+ def params(self):
173
+ out = {
174
+ "visible": self.chk.isChecked(),
175
+ "mode": self.mode.currentText(),
176
+ "opacity": self.sld.value() / 100.0,
177
+ "name": self._name,
178
+ # mask UI state
179
+ "mask_index": self.mask_combo.currentIndex(),
180
+ "mask_src": "Luminance",
181
+ "mask_invert": self.mask_invert.isChecked(),
182
+ }
183
+ if self.sig_center is not None and self.sig_strength is not None:
184
+ out["sigmoid_center"] = self.sig_center.value()
185
+ out["sigmoid_strength"] = self.sig_strength.value()
186
+ return out
187
+
188
+ def setName(self, name: str):
189
+ self._name = name
190
+ self.lbl.setText(name)
191
+
192
+ # ---------- The Dock ----------
193
+ class LayersDock(QDockWidget):
194
+ def __init__(self, main_window):
195
+ super().__init__("Layers", main_window)
196
+ self.setObjectName("LayersDock")
197
+ self.mw = main_window
198
+ self.docman = main_window.docman
199
+ self._wired_title_sources = set()
200
+
201
+ self._apply_timer = QTimer(self)
202
+ self._apply_timer.setSingleShot(True)
203
+ self._apply_timer.timeout.connect(self._apply_list_to_view)
204
+ self._apply_debounce_ms = 100 # tweak 60–150ms as you like
205
+
206
+ # UI
207
+ w = QWidget()
208
+ v = QVBoxLayout(w); v.setContentsMargins(8, 8, 8, 8)
209
+ top = QHBoxLayout(); v.addLayout(top)
210
+ top.addWidget(QLabel("View:"))
211
+ self.view_combo = QComboBox()
212
+ top.addWidget(self.view_combo, 1)
213
+
214
+ self.list = QListWidget()
215
+ self.list.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
216
+ self.list.setAlternatingRowColors(True)
217
+ v.addWidget(self.list, 1)
218
+
219
+ # buttons
220
+ row = QHBoxLayout(); v.addLayout(row)
221
+ self.btn_clear = QPushButton("Clear All Layers")
222
+ self.btn_merge = QPushButton("Merge Layers and Push to View")
223
+ self.btn_merge.setToolTip("Flatten the visible layers into the current view and add an undo step.")
224
+ row.addWidget(self.btn_merge)
225
+ row.addStretch(1)
226
+ row.addWidget(self.btn_clear)
227
+
228
+ self.setWidget(w)
229
+
230
+ # dnd (accept drops from views)
231
+ self.setAcceptDrops(True)
232
+
233
+ # signals
234
+ self.view_combo.currentIndexChanged.connect(self._on_pick_view)
235
+ self.btn_clear.clicked.connect(self._clear_layers)
236
+
237
+ # keep in sync with MDI/windows
238
+ self.mw.mdi.subWindowActivated.connect(lambda _sw: self._refresh_views())
239
+ self.docman.documentAdded.connect(lambda _d: self._refresh_views())
240
+ self.docman.documentRemoved.connect(lambda _d: self._refresh_views())
241
+
242
+ self.btn_merge.clicked.connect(self._merge_and_push)
243
+
244
+ # initial
245
+ self._refresh_views()
246
+
247
+ # ---------- helpers ----------
248
+ def _mask_choices(self):
249
+ out = []
250
+ for sw in self._all_subwindows():
251
+ title = sw._effective_title() or "Untitled"
252
+ out.append((title, sw.document))
253
+ return out
254
+
255
+ def _all_subwindows(self):
256
+ from setiastro.saspro.subwindow import ImageSubWindow
257
+ subs = []
258
+ for sw in self.mw.mdi.subWindowList():
259
+ w = sw.widget()
260
+ if isinstance(w, ImageSubWindow):
261
+ subs.append(w)
262
+ return subs
263
+
264
+ def _refresh_views(self):
265
+ subs = self._all_subwindows()
266
+ current = self.current_view()
267
+ self.view_combo.blockSignals(True)
268
+ self.view_combo.clear()
269
+ for w in subs:
270
+ title = w._effective_title() or "Untitled"
271
+ self.view_combo.addItem(title, userData=w)
272
+ self.view_combo.blockSignals(False)
273
+
274
+ if current and current in subs:
275
+ idx = subs.index(current)
276
+ self.view_combo.setCurrentIndex(idx)
277
+ elif subs:
278
+ self.view_combo.setCurrentIndex(0)
279
+
280
+ # NEW: listen for future title changes
281
+ self._wire_title_change_listeners(subs)
282
+
283
+ self._rebuild_list()
284
+
285
+
286
+ def _wire_title_change_listeners(self, subs):
287
+ # connect once per subwindow
288
+ for sw in subs:
289
+ if sw in self._wired_title_sources:
290
+ continue
291
+ if hasattr(sw, "viewTitleChanged"):
292
+ try:
293
+ sw.viewTitleChanged.connect(lambda *_: self._refresh_titles_only())
294
+ except Exception:
295
+ pass
296
+ self._wired_title_sources.add(sw)
297
+
298
+ def _refresh_titles_only(self):
299
+ """Update just the titles in the View dropdown, mask source lists,
300
+ and base-row label, preserving current selection and layer state."""
301
+ subs = self._all_subwindows()
302
+ if not subs:
303
+ return
304
+
305
+ # Update the View dropdown text in place
306
+ self.view_combo.blockSignals(True)
307
+ cur_idx = self.view_combo.currentIndex()
308
+ for i, sw in enumerate(subs):
309
+ t = sw._effective_title() or "Untitled"
310
+ if i < self.view_combo.count():
311
+ self.view_combo.setItemText(i, t)
312
+ else:
313
+ self.view_combo.addItem(t, userData=sw)
314
+ self.view_combo.blockSignals(False)
315
+ if 0 <= cur_idx < self.view_combo.count():
316
+ self.view_combo.setCurrentIndex(cur_idx)
317
+
318
+ # Update mask choices shown in each row (titles only)
319
+ choices = [(sw._effective_title() or "Untitled", sw.document) for sw in subs]
320
+ docs = [d for _, d in choices]
321
+
322
+ for i in range(self.list.count()):
323
+ roww = self.list.itemWidget(self.list.item(i))
324
+ if not isinstance(roww, _LayerRow):
325
+ continue
326
+
327
+ # base row label
328
+ if getattr(roww, "_is_base", False):
329
+ vw = self.current_view()
330
+ base_name = vw._effective_title() if (vw and hasattr(vw, "_effective_title")) else "Current View"
331
+ roww.setName(f"Base • {base_name}")
332
+ continue
333
+
334
+ # non-base row: update mask combo item texts without changing selection
335
+ if roww.mask_combo.count() > 0:
336
+ # index 0 is "(none)"
337
+ # build a map from doc -> title
338
+ title_for_doc = {doc: title for title, doc in choices}
339
+ for idx in range(1, roww.mask_combo.count()):
340
+ doc = roww.mask_combo.itemData(idx)
341
+ if doc in title_for_doc:
342
+ roww.mask_combo.setItemText(idx, title_for_doc[doc])
343
+
344
+ def current_view(self):
345
+ idx = self.view_combo.currentIndex()
346
+ if idx < 0:
347
+ return None
348
+ return self.view_combo.itemData(idx)
349
+
350
+ def _on_pick_view(self, _i):
351
+ self._rebuild_list()
352
+
353
+ def _rebuild_list(self):
354
+ self.list.clear()
355
+ vw = self.current_view()
356
+ if not vw:
357
+ return
358
+
359
+ choices = self._mask_choices()
360
+ docs = [d for _, d in choices]
361
+
362
+ for lyr in getattr(vw, "_layers", []):
363
+ raw_name = getattr(lyr, "name", "Layer")
364
+ name = raw_name if isinstance(raw_name, str) else str(raw_name)
365
+
366
+ # --- Optional dynamic title sync ---
367
+ try:
368
+ src_doc = getattr(lyr, "src_doc", None)
369
+ # What the document considers its "base" display name
370
+ doc_disp = None
371
+ if src_doc is not None:
372
+ dn = getattr(src_doc, "display_name", None)
373
+ doc_disp = dn() if callable(dn) else dn
374
+
375
+ # If our stored name is just the base doc name, prefer the current view title
376
+ if src_doc is not None and name == (doc_disp or name):
377
+ for sw in self._all_subwindows():
378
+ if getattr(sw, "document", None) is src_doc:
379
+ t = getattr(sw, "_effective_title", None)
380
+ if callable(t):
381
+ t = t()
382
+ if t:
383
+ name = t
384
+ break
385
+ except Exception:
386
+ pass
387
+ mode = getattr(lyr, "mode", "Normal")
388
+ opacity = float(getattr(lyr, "opacity", 1.0))
389
+ visible = bool(getattr(lyr, "visible", True))
390
+ roww = _LayerRow(name, mode, opacity, visible)
391
+ roww.mask_combo.blockSignals(True)
392
+ roww.mask_combo.clear()
393
+ roww.mask_combo.addItem("(none)", userData=None)
394
+ for title, doc in choices:
395
+ roww.mask_combo.addItem(title, userData=doc)
396
+ if getattr(lyr, "mask_doc", None) in docs:
397
+ roww.mask_combo.setCurrentIndex(1 + docs.index(lyr.mask_doc))
398
+ else:
399
+ roww.mask_combo.setCurrentIndex(0)
400
+
401
+ roww.mask_invert.setChecked(bool(getattr(lyr, "mask_invert", False)))
402
+ roww.mask_combo.blockSignals(False)
403
+ center = getattr(lyr, "sigmoid_center", 0.5)
404
+ strength = getattr(lyr, "sigmoid_strength", 10.0)
405
+ roww.set_sigmoid_params(center, strength)
406
+ self._bind_row(roww)
407
+ it = QListWidgetItem(self.list)
408
+ it.setSizeHint(roww.sizeHint())
409
+ self.list.addItem(it)
410
+ self.list.setItemWidget(it, roww)
411
+
412
+ base_name = getattr(vw, "_effective_title", None)
413
+ base_name = base_name() if callable(base_name) else "Current View"
414
+ base_label = f"Base • {base_name}"
415
+ base_row = _LayerRow(base_label, "—", 1.0, True, is_base=True)
416
+ itb = QListWidgetItem(self.list)
417
+ itb.setSizeHint(base_row.sizeHint())
418
+ self.list.addItem(itb)
419
+ self.list.setItemWidget(itb, base_row)
420
+ has_layers = bool(getattr(vw, "_layers", []))
421
+ self.btn_merge.setEnabled(has_layers)
422
+ self.btn_clear.setEnabled(has_layers)
423
+ self._refresh_row_heights()
424
+
425
+ def _layer_count(self) -> int:
426
+ vw = self.current_view()
427
+ return len(getattr(vw, "_layers", [])) if vw else 0
428
+
429
+ def _bind_row(self, roww: _LayerRow):
430
+ if getattr(roww, "_is_base", False):
431
+ return
432
+ roww.changed.connect(self._apply_list_to_view_debounced)
433
+
434
+ roww.requestDelete.connect(lambda: self._delete_row(roww))
435
+ roww.moveUp.connect(lambda: self._move_row(roww, -1))
436
+ roww.moveDown.connect(lambda: self._move_row(roww, +1))
437
+
438
+ def _apply_list_to_view_debounced(self):
439
+ # restart the timer on every slider tick
440
+ self._apply_timer.start(self._apply_debounce_ms)
441
+ # Also refresh row heights so mode-dependent controls (like Sigmoid)
442
+ # can expand/collapse the row visually.
443
+ self._refresh_row_heights()
444
+
445
+ def _refresh_row_heights(self):
446
+ """Update QListWidgetItem size hints to match current row widgets."""
447
+ try:
448
+ for i in range(self.list.count()):
449
+ item = self.list.item(i)
450
+ roww = self.list.itemWidget(item)
451
+ if roww is not None:
452
+ # Ask the row for an up-to-date size hint
453
+ item.setSizeHint(roww.sizeHint())
454
+ except Exception as ex:
455
+ print("[LayersDock] _refresh_row_heights error:", ex)
456
+
457
+
458
+
459
+ def _find_row_index(self, roww: _LayerRow) -> int:
460
+ for i in range(self.list.count()):
461
+ if self.list.itemWidget(self.list.item(i)) is roww:
462
+ return i
463
+ return -1
464
+
465
+ def _delete_row(self, roww: _LayerRow):
466
+ vw = self.current_view()
467
+ if not vw:
468
+ return
469
+ idx = self._find_row_index(roww)
470
+ if idx < 0:
471
+ return
472
+ if idx >= self._layer_count():
473
+ return
474
+ vw._layers.pop(idx)
475
+ self.list.takeItem(idx)
476
+ self._apply_list_to_view()
477
+
478
+ def _move_row(self, roww: _LayerRow, delta: int):
479
+ vw = self.current_view()
480
+ if not vw:
481
+ return
482
+ i = self._find_row_index(roww)
483
+ if i < 0 or i >= self._layer_count():
484
+ return
485
+ j = i + delta
486
+ if j < 0 or j >= self._layer_count():
487
+ return
488
+ vw._layers[i], vw._layers[j] = vw._layers[j], vw._layers[i]
489
+ self._rebuild_list()
490
+ self._apply_list_to_view()
491
+
492
+ def _apply_list_to_view(self):
493
+ vw = self.current_view()
494
+ if not vw:
495
+ return
496
+ n = self._layer_count()
497
+ rows = []
498
+ for i in range(n):
499
+ it = self.list.item(i)
500
+ rows.append(self.list.itemWidget(it))
501
+
502
+
503
+ for lyr, roww in zip(vw._layers, rows):
504
+ p = roww.params()
505
+ lyr.visible = p["visible"]
506
+ lyr.mode = p["mode"]
507
+ lyr.opacity = float(p["opacity"])
508
+ # Sigmoid parameters (if present)
509
+ if "sigmoid_center" in p:
510
+ lyr.sigmoid_center = float(p["sigmoid_center"])
511
+ if "sigmoid_strength" in p:
512
+ lyr.sigmoid_strength = float(p["sigmoid_strength"])
513
+ mi = p["mask_index"]
514
+ if mi is not None and mi > 0:
515
+ doc = roww.mask_combo.itemData(mi)
516
+ lyr.mask_doc = doc
517
+ else:
518
+ lyr.mask_doc = None
519
+
520
+ # Force luminance masks only
521
+ lyr.mask_use_luma = True
522
+ lyr.mask_invert = bool(p["mask_invert"])
523
+ vw._reinstall_layer_watchers()
524
+ vw.apply_layer_stack(vw._layers)
525
+
526
+ def _clear_layers(self):
527
+ vw = self.current_view()
528
+ if not vw: return
529
+ vw._layers = []
530
+ vw._reinstall_layer_watchers()
531
+ self._rebuild_list()
532
+ vw.apply_layer_stack([])
533
+
534
+ def dragEnterEvent(self, e: QDragEnterEvent):
535
+ md = e.mimeData()
536
+ if md.hasFormat(MIME_VIEWSTATE) or md.hasFormat(MIME_MASK):
537
+ e.acceptProposedAction()
538
+ else:
539
+ e.ignore()
540
+
541
+ def dragMoveEvent(self, e: QDragEnterEvent):
542
+ self.dragEnterEvent(e)
543
+
544
+ def dropEvent(self, e: QDropEvent):
545
+ vw = self.current_view()
546
+ if not vw:
547
+ e.ignore(); return
548
+ md = e.mimeData()
549
+ try:
550
+ if md.hasFormat(MIME_VIEWSTATE):
551
+ st = json.loads(bytes(md.data(MIME_VIEWSTATE)).decode("utf-8"))
552
+ # Try robust resolution (UIDs/file_path/ptr)
553
+ src_doc = self._resolve_doc_from_state(st)
554
+ if src_doc is None:
555
+ raise RuntimeError("Source doc gone")
556
+ layer_name = "Layer"
557
+ src_title = None
558
+ for sw in self._all_subwindows():
559
+ if getattr(sw, "document", None) is src_doc:
560
+ t = getattr(sw, "_effective_title", None)
561
+ src_title = t() if callable(t) else t
562
+ break
563
+ if src_title:
564
+ layer_name = src_title
565
+ else:
566
+ dn = getattr(src_doc, "display_name", None)
567
+ layer_name = dn() if callable(dn) else (dn or "Layer")
568
+
569
+ new_layer = ImageLayer(
570
+ name=layer_name,
571
+ src_doc=src_doc,
572
+ visible=True,
573
+ opacity=1.0,
574
+ mode="Normal",
575
+ )
576
+ if not hasattr(vw, "_layers") or vw._layers is None:
577
+ vw._layers = []
578
+ vw._layers.insert(0, new_layer)
579
+ vw._reinstall_layer_watchers()
580
+ self._rebuild_list()
581
+ vw.apply_layer_stack(vw._layers)
582
+ e.acceptProposedAction()
583
+ return
584
+
585
+ if md.hasFormat(MIME_MASK):
586
+ payload = json.loads(bytes(md.data(MIME_MASK)).decode("utf-8"))
587
+ # payload may include doc_uid/base_doc_uid/file_path/mask_doc_ptr
588
+ mask_doc = self._resolve_doc_from_state(payload)
589
+ if mask_doc is None:
590
+ raise RuntimeError("Mask doc gone")
591
+ if not getattr(vw, "_layers", None):
592
+ QMessageBox.information(self, "No Layers", "Add a layer first, then drop a mask onto it.")
593
+ e.ignore(); return
594
+ sel_row = self.list.currentRow()
595
+ if sel_row < 0:
596
+ sel_row = 0
597
+ idx = min(sel_row, len(vw._layers) - 1)
598
+ layer = vw._layers[idx]
599
+ layer.mask_doc = mask_doc
600
+ layer.mask_invert = bool(payload.get("invert", False))
601
+ try:
602
+ layer.mask_feather = float(payload.get("feather", 0.0) or 0.0)
603
+ except Exception:
604
+ layer.mask_feather = 0.0
605
+ vw._reinstall_layer_watchers()
606
+ self._rebuild_list()
607
+ vw.apply_layer_stack(vw._layers)
608
+ e.acceptProposedAction()
609
+ return
610
+
611
+ except Exception as ex:
612
+ print("[LayersDock] drop error:", ex)
613
+ e.ignore()
614
+
615
+ def _resolve_doc_ptr(self, ptr: int):
616
+ """Legacy path: resolve by Python id() pointer."""
617
+ try:
618
+ for d in self.docman.all_documents():
619
+ if id(d) == ptr:
620
+ return d
621
+ except Exception:
622
+ pass
623
+ return None
624
+
625
+ def _resolve_doc_from_state(self, st):
626
+ """
627
+ Accepts either:
628
+ - dict payload (preferred): may include doc_uid, base_doc_uid, file_path, doc_ptr/mask_doc_ptr
629
+ - int legacy pointer
630
+ Tries, in order: doc_uid → base_doc_uid → legacy ptr → file_path.
631
+ """
632
+ # If called with an int, treat it as a raw pointer
633
+ if isinstance(st, int):
634
+ return self._resolve_doc_ptr(st)
635
+
636
+ if not isinstance(st, dict):
637
+ return None
638
+
639
+ # 1) Prefer UIDs
640
+ doc_uid = st.get("doc_uid")
641
+ base_uid = st.get("base_doc_uid")
642
+ if doc_uid and hasattr(self.docman, "get_document_by_uid"):
643
+ d = self.docman.get_document_by_uid(doc_uid)
644
+ if d is not None:
645
+ return d
646
+ if base_uid and hasattr(self.docman, "get_document_by_uid"):
647
+ d = self.docman.get_document_by_uid(base_uid)
648
+ if d is not None:
649
+ return d
650
+
651
+ # 2) Legacy pointer
652
+ ptr = st.get("doc_ptr") or st.get("mask_doc_ptr") # mask payloads may use mask_doc_ptr
653
+ if isinstance(ptr, int):
654
+ d = self._resolve_doc_ptr(ptr)
655
+ if d is not None:
656
+ return d
657
+
658
+ # 3) Last-ditch: file path match
659
+ fp = (st.get("file_path") or "").strip()
660
+ if fp:
661
+ try:
662
+ for d in self.docman.all_documents():
663
+ meta = getattr(d, "metadata", {}) or {}
664
+ if meta.get("file_path") == fp:
665
+ return d
666
+ except Exception:
667
+ pass
668
+
669
+ return None
670
+
671
+
672
+ def _merge_and_push(self):
673
+ vw = self.current_view()
674
+ if not vw:
675
+ return
676
+
677
+ # No layers? Nothing to do.
678
+ layers = list(getattr(vw, "_layers", []) or [])
679
+ if not layers:
680
+ QMessageBox.information(self, "Layers", "There are no layers to merge.")
681
+ return
682
+
683
+ try:
684
+ # Base image from the current view's document
685
+ base_doc = getattr(vw, "document", None)
686
+ if base_doc is None or getattr(base_doc, "image", None) is None:
687
+ QMessageBox.warning(self, "Layers", "No base image available for this view.")
688
+ return
689
+
690
+ base_img = base_doc.image
691
+ merged = composite_stack(base_img, layers)
692
+ if merged is None:
693
+ QMessageBox.warning(self, "Layers", "Composite failed (empty result).")
694
+ return
695
+
696
+ # Push into the document as an undoable edit
697
+ # (assumes document.apply_edit accepts float [0..1] or handles dtype internally)
698
+ meta = dict(getattr(base_doc, "metadata", {}) or {})
699
+ meta["step_name"] = "Layers Merge"
700
+ base_doc.apply_edit(merged.copy(), metadata=meta, step_name="Layers Merge")
701
+
702
+ # Clear layers and update live preview
703
+ vw._layers = []
704
+ vw._reinstall_layer_watchers()
705
+ self._rebuild_list()
706
+ vw.apply_layer_stack([])
707
+
708
+ # Nice confirmation
709
+ QMessageBox.information(self, "Layers",
710
+ "Merged visible layers and pushed the result to the current view.")
711
+ except Exception as ex:
712
+ print("[LayersDock] merge error:", ex)
713
+ QMessageBox.critical(self, "Layers", f"Merge failed:\n{ex}")
714
+