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,554 @@
1
+ # pro/stat_stretch.py
2
+ from __future__ import annotations
3
+ from PyQt6.QtCore import Qt, QSize, QEvent
4
+ from PyQt6.QtWidgets import (
5
+ QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QLabel, QDoubleSpinBox,
6
+ QCheckBox, QPushButton, QScrollArea, QWidget, QMessageBox, QSlider, QToolBar, QToolButton
7
+ )
8
+ from PyQt6.QtGui import QImage, QPixmap, QMouseEvent, QCursor
9
+ import numpy as np
10
+ from PyQt6 import sip
11
+
12
+ from .doc_manager import ImageDocument
13
+ # use your existing stretch code
14
+ from setiastro.saspro.imageops.stretch import stretch_mono_image, stretch_color_image
15
+ from setiastro.saspro.widgets.themed_buttons import themed_toolbtn
16
+
17
+ class StatisticalStretchDialog(QDialog):
18
+ """
19
+ Non-destructive preview; Apply commits to the document image.
20
+ """
21
+ def __init__(self, parent, document: ImageDocument):
22
+ super().__init__(parent)
23
+ self.setWindowTitle(self.tr("Statistical Stretch"))
24
+
25
+ # --- IMPORTANT: avoid “attached modal” behavior on some Linux WMs ---
26
+ # Make this a proper top-level window (tool-style) rather than an attached sheet.
27
+ self.setWindowFlag(Qt.WindowType.Window, True)
28
+ # Non-modal: allow user to switch between images while dialog is open
29
+ self.setWindowModality(Qt.WindowModality.NonModal)
30
+ # Don’t let the generic modal flag override the explicit modality
31
+ self.setModal(False)
32
+ try:
33
+ self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
34
+ except Exception:
35
+ pass # older PyQt6 versions
36
+ self._main = parent
37
+ self.doc = document
38
+ self._last_preview = None
39
+
40
+ self._follow_conn = None
41
+ if hasattr(self._main, "currentDocumentChanged"):
42
+ try:
43
+ # store connection so we can cleanly disconnect
44
+ self._main.currentDocumentChanged.connect(self._on_active_doc_changed)
45
+ self._follow_conn = True
46
+ except Exception:
47
+ self._follow_conn = None
48
+ self._panning = False
49
+ self._pan_last = None # QPoint
50
+ self._preview_scale = 1.0 # NEW: zoom factor for preview
51
+ self._preview_qimg = None # NEW: store unscaled QImage for clean scaling
52
+ self._suppress_replay_record = False
53
+
54
+ # --- Controls ---
55
+ self.spin_target = QDoubleSpinBox()
56
+ self.spin_target.setRange(0.01, 0.99)
57
+ self.spin_target.setSingleStep(0.01)
58
+ self.spin_target.setValue(0.25)
59
+ self.spin_target.setDecimals(3)
60
+
61
+ self.chk_linked = QCheckBox(self.tr("Linked channels"))
62
+ self.chk_linked.setChecked(False)
63
+
64
+ self.chk_normalize = QCheckBox(self.tr("Normalize to [0..1]"))
65
+ self.chk_normalize.setChecked(False)
66
+
67
+ # NEW: Curves boost
68
+ self.chk_curves = QCheckBox(self.tr("Curves boost"))
69
+ self.chk_curves.setChecked(False)
70
+
71
+ self.curves_row = QWidget()
72
+ cr_lay = QHBoxLayout(self.curves_row); cr_lay.setContentsMargins(0,0,0,0)
73
+ cr_lay.setSpacing(8)
74
+ cr_lay.addWidget(QLabel(self.tr("Strength:")))
75
+ self.sld_curves = QSlider(Qt.Orientation.Horizontal)
76
+ self.sld_curves.setRange(0, 100) # 0.00 … 1.00 mapped to 0…100
77
+ self.sld_curves.setSingleStep(1)
78
+ self.sld_curves.setPageStep(5)
79
+ self.sld_curves.setValue(20) # default 0.20
80
+ self.lbl_curves_val = QLabel("0.20")
81
+ self.sld_curves.valueChanged.connect(lambda v: self.lbl_curves_val.setText(f"{v/100:.2f}"))
82
+ cr_lay.addWidget(self.sld_curves, 1)
83
+ cr_lay.addWidget(self.lbl_curves_val)
84
+ self.curves_row.setEnabled(False) # disabled until checkbox is ticked
85
+ self.chk_curves.toggled.connect(self.curves_row.setEnabled)
86
+
87
+ # Preview area
88
+ self.preview_label = QLabel(alignment=Qt.AlignmentFlag.AlignCenter)
89
+ self.preview_label.setMinimumSize(QSize(320, 240))
90
+ self.preview_label.setScaledContents(False)
91
+ self.preview_scroll = QScrollArea()
92
+ self.preview_scroll.setWidgetResizable(False) # <- was True; we manage size
93
+ self.preview_scroll.setWidget(self.preview_label)
94
+ self.preview_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
95
+ self.preview_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
96
+
97
+ self._fit_mode = True # NEW: start in Fit mode
98
+
99
+ # --- Zoom buttons row (place before the main layout or right above preview) ---
100
+ # --- Zoom buttons row ---
101
+ zoom_row = QHBoxLayout()
102
+
103
+ # Use themed tool buttons (consistent with the rest of SASpro)
104
+ self.btn_zoom_out = themed_toolbtn("zoom-out", "Zoom Out")
105
+ self.btn_zoom_in = themed_toolbtn("zoom-in", "Zoom In")
106
+ self.btn_zoom_100 = themed_toolbtn("zoom-original", "1:1")
107
+ self.btn_zoom_fit = themed_toolbtn("zoom-fit-best", "Fit")
108
+
109
+
110
+ zoom_row.addStretch(1)
111
+ for b in (self.btn_zoom_out, self.btn_zoom_in, self.btn_zoom_100, self.btn_zoom_fit):
112
+ zoom_row.addWidget(b)
113
+ zoom_row.addStretch(1)
114
+
115
+ # Buttons
116
+ self.btn_preview = QPushButton(self.tr("Preview"))
117
+ self.btn_apply = QPushButton(self.tr("Apply"))
118
+ self.btn_close = QPushButton(self.tr("Close"))
119
+
120
+ self.btn_preview.clicked.connect(self._do_preview)
121
+ self.btn_apply.clicked.connect(self._do_apply)
122
+ self.btn_close.clicked.connect(self.close)
123
+
124
+ # --- Layout ---
125
+ form = QFormLayout()
126
+ form.addRow(self.tr("Target median:"), self.spin_target)
127
+ form.addRow("", self.chk_linked)
128
+ form.addRow("", self.chk_normalize)
129
+ form.addRow("", self.chk_curves)
130
+ form.addRow("", self.curves_row)
131
+
132
+ left = QVBoxLayout()
133
+ left.addLayout(form)
134
+ row = QHBoxLayout()
135
+ row.addWidget(self.btn_preview)
136
+ row.addWidget(self.btn_apply)
137
+ row.addStretch(1)
138
+ left.addLayout(row)
139
+ left.addStretch(1)
140
+
141
+ main = QHBoxLayout(self)
142
+ main.addLayout(left, 0)
143
+
144
+ # NEW: right column with zoom row + preview
145
+ right = QVBoxLayout()
146
+ right.addLayout(zoom_row) # ← actually add the zoom controls
147
+ right.addWidget(self.preview_scroll, 1) # preview below the buttons
148
+ main.addLayout(right, 1)
149
+
150
+ self.btn_zoom_in.clicked.connect(lambda: self._zoom_by(1.25))
151
+ self.btn_zoom_out.clicked.connect(lambda: self._zoom_by(1/1.25))
152
+ self.btn_zoom_100.clicked.connect(self._zoom_reset_100)
153
+ self.btn_zoom_fit.clicked.connect(self._fit_preview)
154
+
155
+ self.preview_scroll.viewport().installEventFilter(self)
156
+ self.preview_label.installEventFilter(self)
157
+
158
+ self._populate_initial_preview()
159
+
160
+ # ----- helpers -----
161
+ def _get_source_float(self) -> np.ndarray:
162
+ """
163
+ Return a float32 array scaled into ~[0..1] for stretching.
164
+ """
165
+ src = np.asarray(self.doc.image)
166
+ if src is None or src.size == 0:
167
+ return None
168
+
169
+ if np.issubdtype(src.dtype, np.integer):
170
+ # Assume 16-bit astro sources by default; adjust if you prefer
171
+ scale = 65535.0 if src.dtype.itemsize >= 2 else 255.0
172
+ return (src.astype(np.float32) / scale).clip(0, 1)
173
+ else:
174
+ f = src.astype(np.float32)
175
+ # If values are way above 1 (linear calibrated data), compress softly
176
+ mx = float(f.max()) if f.size else 1.0
177
+ if mx > 5.0:
178
+ f = f / mx
179
+ return f
180
+
181
+ def _apply_current_zoom(self):
182
+ """Apply the current zoom mode (fit or manual) to the preview image."""
183
+ if self._preview_qimg is None:
184
+ return
185
+ if self._fit_mode:
186
+ self._fit_preview()
187
+ else:
188
+ self._update_preview_scaled()
189
+
190
+ def _fit_preview(self):
191
+ """Fit the image into the visible scroll viewport."""
192
+ if self._preview_qimg is None:
193
+ return
194
+ vp = self.preview_scroll.viewport().size()
195
+ if vp.width() <= 1 or vp.height() <= 1:
196
+ return
197
+ iw, ih = self._preview_qimg.width(), self._preview_qimg.height()
198
+ if iw <= 0 or ih <= 0:
199
+ return
200
+ # compute scale to fit
201
+ sx = vp.width() / iw
202
+ sy = vp.height() / ih
203
+ self._preview_scale = max(0.05, min(sx, sy))
204
+ self._fit_mode = True
205
+ self._update_preview_scaled()
206
+
207
+ def _zoom_reset_100(self):
208
+ """Set zoom to 100% (1:1)."""
209
+ self._fit_mode = False
210
+ self._preview_scale = 1.0
211
+ self._update_preview_scaled()
212
+
213
+ def _zoom_by(self, factor: float):
214
+ """Incremental zoom around the current center; exits Fit mode."""
215
+ self._fit_mode = False
216
+ new_scale = self._preview_scale * float(factor)
217
+ self._preview_scale = max(0.05, min(new_scale, 8.0))
218
+ self._update_preview_scaled()
219
+
220
+
221
+ # --- MASK helpers ----------------------------------------------------
222
+ def _active_mask_array(self) -> np.ndarray | None:
223
+ """Return active mask as float32 [H,W] in 0..1, resized to doc image."""
224
+ try:
225
+ mid = getattr(self.doc, "active_mask_id", None)
226
+ if not mid:
227
+ return None
228
+ layer = getattr(self.doc, "masks", {}).get(mid)
229
+ if layer is None:
230
+ return None
231
+
232
+ m = np.asarray(getattr(layer, "data", None))
233
+ if m is None or m.size == 0:
234
+ return None
235
+
236
+ # squeeze to 2D
237
+ if m.ndim == 3 and m.shape[2] == 1:
238
+ m = m[..., 0]
239
+ elif m.ndim == 3: # RGB/whatever → luminance
240
+ m = (0.2126*m[...,0] + 0.7152*m[...,1] + 0.0722*m[...,2])
241
+
242
+ m = m.astype(np.float32, copy=False)
243
+ # normalize if integer / out-of-range
244
+ if m.dtype.kind in "ui":
245
+ m /= float(np.iinfo(m.dtype).max)
246
+ m = np.clip(m, 0.0, 1.0)
247
+
248
+ th, tw = self.doc.image.shape[:2]
249
+ sh, sw = m.shape[:2]
250
+ if (sh, sw) != (th, tw):
251
+ yi = (np.linspace(0, sh-1, th)).astype(np.int32)
252
+ xi = (np.linspace(0, sw-1, tw)).astype(np.int32)
253
+ m = m[yi][:, xi]
254
+
255
+ # honor opacity if present
256
+ opacity = float(getattr(layer, "opacity", 1.0) or 1.0)
257
+ if opacity < 1.0:
258
+ m *= opacity
259
+ return m
260
+ except Exception:
261
+ return None
262
+
263
+ def _blend_with_mask(self, base: np.ndarray, out: np.ndarray, mask: np.ndarray) -> np.ndarray:
264
+ """base/out can be mono or 3ch; mask is [H,W] in 0..1."""
265
+ if out.ndim == 3 and out.shape[2] == 3:
266
+ m = mask[..., None]
267
+ else:
268
+ m = mask
269
+ return base * (1.0 - m) + out * m
270
+
271
+
272
+ def _run_stretch(self) -> np.ndarray | None:
273
+ imgf = self._get_source_float()
274
+ if imgf is None:
275
+ return None
276
+
277
+ target = float(self.spin_target.value())
278
+ linked = bool(self.chk_linked.isChecked())
279
+ normalize = bool(self.chk_normalize.isChecked())
280
+ apply_curves = bool(self.chk_curves.isChecked())
281
+ curves_boost = float(self.sld_curves.value()) / 100.0
282
+
283
+ if imgf.ndim == 2 or (imgf.ndim == 3 and imgf.shape[2] == 1):
284
+ out = stretch_mono_image(
285
+ imgf.squeeze(),
286
+ target_median=target,
287
+ normalize=normalize,
288
+ apply_curves=apply_curves,
289
+ curves_boost=curves_boost,
290
+ )
291
+ else:
292
+ out = stretch_color_image(
293
+ imgf,
294
+ target_median=target,
295
+ linked=linked,
296
+ normalize=normalize,
297
+ apply_curves=apply_curves,
298
+ curves_boost=curves_boost,
299
+ )
300
+
301
+ # ✅ If a mask is active, blend stretched result with original
302
+ m = self._active_mask_array()
303
+ if m is not None:
304
+ base = imgf.astype(np.float32, copy=False)
305
+ out = self._blend_with_mask(base, out, m)
306
+
307
+ return out
308
+
309
+
310
+ def _set_preview_pixmap(self, arr: np.ndarray):
311
+ vis = arr
312
+ if vis is None or vis.size == 0:
313
+ self.preview_label.clear()
314
+ return
315
+
316
+ # Ensure 3 channels for display
317
+ if vis.ndim == 2:
318
+ vis3 = np.stack([vis] * 3, axis=-1)
319
+ elif vis.ndim == 3 and vis.shape[2] == 1:
320
+ vis3 = np.repeat(vis, 3, axis=2)
321
+ else:
322
+ vis3 = vis
323
+
324
+ # Convert to 8-bit RGB
325
+ if vis3.dtype == np.uint8:
326
+ buf8 = vis3
327
+ elif vis3.dtype == np.uint16:
328
+ buf8 = (vis3.astype(np.float32) / 65535.0 * 255.0).clip(0, 255).astype(np.uint8)
329
+ else:
330
+ buf8 = (np.clip(vis3, 0.0, 1.0) * 255.0).astype(np.uint8)
331
+
332
+ # Must be C-contiguous for QImage
333
+ buf8 = np.ascontiguousarray(buf8)
334
+ h, w, _ = buf8.shape
335
+ bytes_per_line = buf8.strides[0]
336
+
337
+ # Build QImage from raw pointer; keep references alive
338
+ self._last_preview = buf8 # keep backing store alive
339
+ ptr = sip.voidptr(self._last_preview.ctypes.data)
340
+ qimg = QImage(ptr, w, h, bytes_per_line, QImage.Format.Format_RGB888)
341
+
342
+ self._preview_qimg = qimg
343
+ self._apply_current_zoom()
344
+
345
+ # ----- active document change -----
346
+ def _on_active_doc_changed(self, doc):
347
+ """Called when user clicks a different image window."""
348
+ if doc is None or getattr(doc, "image", None) is None:
349
+ return
350
+ self.doc = doc
351
+ self._populate_initial_preview()
352
+
353
+ # ----- slots -----
354
+ def _populate_initial_preview(self):
355
+ # show the current (unstretched) image as baseline
356
+ src = self._get_source_float()
357
+ if src is not None:
358
+ self._set_preview_pixmap(np.clip(src, 0, 1))
359
+
360
+ def _do_preview(self):
361
+ try:
362
+ out = self._run_stretch()
363
+ if out is None:
364
+ QMessageBox.information(self, "No image", "No image is loaded in the active document.")
365
+ return
366
+ self._set_preview_pixmap(out)
367
+ except Exception as e:
368
+ QMessageBox.warning(self, "Preview failed", str(e))
369
+
370
+ def _do_apply(self):
371
+ try:
372
+ out = self._run_stretch()
373
+ if out is None:
374
+ QMessageBox.information(self, "No image", "No image is loaded in the active document.")
375
+ return
376
+
377
+ # Preserve mono vs color shape
378
+ if out.ndim == 3 and out.shape[2] == 3 and (self.doc.image.ndim == 2 or self.doc.image.shape[-1] == 1):
379
+ out = out[..., 0]
380
+
381
+ # --- Gather current UI state ------------------------------------
382
+ target = float(self.spin_target.value())
383
+ linked = bool(self.chk_linked.isChecked())
384
+ normalize = bool(self.chk_normalize.isChecked())
385
+ apply_curves = bool(getattr(self, "chk_curves", None) and self.chk_curves.isChecked())
386
+ curves_boost = 0.0
387
+ if getattr(self, "sld_curves", None) is not None:
388
+ curves_boost = float(self.sld_curves.value()) / 100.0
389
+
390
+ # Build human-readable step name
391
+ parts = [f"target={target:.2f}", "linked" if linked else "unlinked"]
392
+ if normalize:
393
+ parts.append("norm")
394
+ if apply_curves:
395
+ parts.append(f"curves={curves_boost:.2f}")
396
+ if self._active_mask_array() is not None:
397
+ parts.append("masked")
398
+ step_name = f"Statistical Stretch ({', '.join(parts)})"
399
+
400
+ # Apply to document
401
+ self.doc.apply_edit(out.astype(np.float32, copy=False), step_name=step_name)
402
+
403
+ # Turn off display stretch on the active view, if any
404
+ mw = self.parent()
405
+ if hasattr(mw, "mdi") and mw.mdi.activeSubWindow():
406
+ view = mw.mdi.activeSubWindow().widget()
407
+ if getattr(view, "autostretch_enabled", False):
408
+ view.set_autostretch(False)
409
+
410
+ # Existing logging, now using the same values as above
411
+ if hasattr(mw, "_log"):
412
+ curves_on = apply_curves
413
+ boost_val = curves_boost if curves_on else 0.0
414
+ mw._log(
415
+ "Applied Statistical Stretch "
416
+ f"(target={target:.3f}, linked={linked}, normalize={normalize}, "
417
+ f"curves={'ON' if curves_on else 'OFF'}"
418
+ f"{', boost='+str(round(boost_val,2)) if curves_on else ''}, "
419
+ f"mask={'ON' if self._active_mask_array() is not None else 'OFF'})"
420
+ )
421
+
422
+ # --- Build preset for headless replay ---------------------------
423
+ # --- Build preset for headless replay ---------------------------
424
+ preset = {
425
+ "target_median": target,
426
+ "linked": linked,
427
+ "normalize": normalize,
428
+ "apply_curves": apply_curves,
429
+ "curves_boost": curves_boost,
430
+ }
431
+
432
+ # ✅ Remember this as the last headless-style command
433
+ # (unless we are in a headless/suppressed call)
434
+ suppress = bool(getattr(self, "_suppress_replay_record", False))
435
+ if not suppress:
436
+ from PyQt6.QtWidgets import QMainWindow
437
+ try:
438
+ mw2 = self.parent()
439
+ while mw2 is not None and not isinstance(mw2, QMainWindow):
440
+ mw2 = mw2.parent()
441
+
442
+ if mw2 is not None and hasattr(mw2, "remember_last_headless_command"):
443
+ mw2.remember_last_headless_command(
444
+ command_id="stat_stretch",
445
+ preset=preset,
446
+ description="Statistical Stretch",
447
+ )
448
+ print(f"Remembered Statistical Stretch last headless command: {preset}")
449
+ else:
450
+ print("No main window with remember_last_headless_command; cannot store stat_stretch preset")
451
+ except Exception as e:
452
+ print(f"Failed to remember Statistical Stretch last headless command: {e}")
453
+ else:
454
+ # optional debug
455
+ print("Statistical Stretch: replay recording suppressed for this apply()")
456
+
457
+ self.close()
458
+ return
459
+
460
+
461
+ except Exception as e:
462
+ QMessageBox.critical(self, "Apply failed", str(e))
463
+
464
+ def _refresh_document_from_active(self):
465
+ """
466
+ Refresh the dialog's document reference to the currently active document.
467
+ This allows reusing the same dialog on different images.
468
+ """
469
+ try:
470
+ main = self.parent()
471
+ if main and hasattr(main, "_active_doc"):
472
+ new_doc = main._active_doc()
473
+ if new_doc is not None and new_doc is not self.doc:
474
+ self.doc = new_doc
475
+ # Reset preview state for new document
476
+ self._last_preview = None
477
+ self._preview_qimg = None
478
+ except Exception:
479
+ pass
480
+
481
+ def closeEvent(self, ev):
482
+ # disconnect the “follow active document” hook
483
+ try:
484
+ if self._follow_conn and hasattr(self._main, "currentDocumentChanged"):
485
+ self._main.currentDocumentChanged.disconnect(self._on_active_doc_changed)
486
+ except Exception:
487
+ pass
488
+ super().closeEvent(ev)
489
+
490
+
491
+ def _update_preview_scaled(self):
492
+ if self._preview_qimg is None:
493
+ self.preview_label.clear()
494
+ return
495
+ sw = max(1, int(self._preview_qimg.width() * self._preview_scale))
496
+ sh = max(1, int(self._preview_qimg.height() * self._preview_scale))
497
+ scaled = self._preview_qimg.scaled(
498
+ sw, sh,
499
+ Qt.AspectRatioMode.KeepAspectRatio,
500
+ Qt.TransformationMode.SmoothTransformation
501
+ )
502
+ self.preview_label.setPixmap(QPixmap.fromImage(scaled))
503
+ self.preview_label.resize(scaled.size()) # <- crucial for scrollbars
504
+
505
+ def resizeEvent(self, ev):
506
+ super().resizeEvent(ev)
507
+ if self._fit_mode:
508
+ self._fit_preview()
509
+
510
+ def eventFilter(self, obj, ev):
511
+ # Ctrl+wheel zoom
512
+ if ev.type() == QEvent.Type.Wheel and (obj is self.preview_scroll.viewport() or obj is self.preview_label):
513
+ if ev.modifiers() & Qt.KeyboardModifier.ControlModifier:
514
+ factor = 1.25 if ev.angleDelta().y() > 0 else 1/1.25
515
+ self._fit_mode = False # ← ensure we exit Fit mode
516
+ self._preview_scale = max(0.05, min(self._preview_scale * factor, 8.0))
517
+ self._update_preview_scaled()
518
+ return True
519
+ return False
520
+
521
+ # Click+drag pan (left or middle mouse)
522
+ if obj is self.preview_scroll.viewport() or obj is self.preview_label:
523
+ if ev.type() == QEvent.Type.MouseButtonPress:
524
+ if ev.buttons() & (Qt.MouseButton.LeftButton | Qt.MouseButton.MiddleButton):
525
+ self._panning = True
526
+ self._pan_last = ev.position().toPoint()
527
+ # show a "grab" cursor where the drag begins
528
+ if obj is self.preview_label:
529
+ self.preview_label.setCursor(Qt.CursorShape.ClosedHandCursor)
530
+ else:
531
+ self.preview_scroll.viewport().setCursor(Qt.CursorShape.ClosedHandCursor)
532
+ return True
533
+
534
+ elif ev.type() == QEvent.Type.MouseMove and self._panning:
535
+ pos = ev.position().toPoint()
536
+ delta = pos - self._pan_last
537
+ self._pan_last = pos
538
+
539
+ hsb = self.preview_scroll.horizontalScrollBar()
540
+ vsb = self.preview_scroll.verticalScrollBar()
541
+ hsb.setValue(hsb.value() - delta.x())
542
+ vsb.setValue(vsb.value() - delta.y())
543
+ return True
544
+
545
+ elif ev.type() == QEvent.Type.MouseButtonRelease and self._panning:
546
+ self._panning = False
547
+ self._pan_last = None
548
+ # restore cursor
549
+ self.preview_label.unsetCursor()
550
+ self.preview_scroll.viewport().unsetCursor()
551
+ return True
552
+
553
+ return super().eventFilter(obj, ev)
554
+
@@ -0,0 +1,78 @@
1
+ # pro/status_log_dock.py
2
+ from PyQt6.QtCore import Qt, pyqtSlot
3
+ from PyQt6.QtGui import QTextCursor
4
+ from PyQt6.QtWidgets import (
5
+ QDockWidget, QWidget, QVBoxLayout, QPlainTextEdit, QPushButton, QHBoxLayout
6
+ )
7
+
8
+ class StatusLogDock(QDockWidget):
9
+ MAX_BLOCKS = 2000
10
+
11
+ def __init__(self, parent=None):
12
+ super().__init__(self.tr("Stacking Log"), parent)
13
+ self.setObjectName("StackingLogDock")
14
+ self.setAllowedAreas(
15
+ Qt.DockWidgetArea.BottomDockWidgetArea
16
+ | Qt.DockWidgetArea.LeftDockWidgetArea
17
+ | Qt.DockWidgetArea.RightDockWidgetArea
18
+ )
19
+
20
+ w = QWidget(self)
21
+ lay = QVBoxLayout(w); lay.setContentsMargins(6,6,6,6)
22
+
23
+ self.view = QPlainTextEdit(w)
24
+ self.view.setReadOnly(True)
25
+ self.view.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
26
+ self.view.setStyleSheet(
27
+ "background-color: black; color: white; font-family: Monospace; padding: 6px;"
28
+ )
29
+ lay.addWidget(self.view, 1)
30
+
31
+ row = QHBoxLayout()
32
+ btn_clear = QPushButton("Clear", w)
33
+ btn_clear.clicked.connect(self.view.clear)
34
+ row.addWidget(btn_clear)
35
+ row.addStretch(1)
36
+ lay.addLayout(row)
37
+
38
+ self.setWidget(w)
39
+
40
+ @pyqtSlot(str)
41
+ def append_line(self, message: str):
42
+ doc = self.view.document()
43
+
44
+ # coalesce “Normalizing …” lines (replace last if same prefix)
45
+ if message.startswith("🔄 Normalizing") and doc.blockCount() > 0:
46
+ last = doc.findBlockByNumber(doc.blockCount() - 1)
47
+ if last.isValid() and last.text().startswith("🔄 Normalizing"):
48
+ cur = self.view.textCursor()
49
+ cur.movePosition(QTextCursor.MoveOperation.End)
50
+ cur.movePosition(QTextCursor.MoveOperation.StartOfBlock,
51
+ QTextCursor.MoveMode.KeepAnchor)
52
+ cur.removeSelectedText()
53
+ cur.insertText(message)
54
+ self.view.setTextCursor(cur)
55
+ else:
56
+ self.view.appendPlainText(message)
57
+ else:
58
+ self.view.appendPlainText(message)
59
+
60
+ # trim earliest lines
61
+ if doc.blockCount() > self.MAX_BLOCKS:
62
+ extra = doc.blockCount() - self.MAX_BLOCKS
63
+ cur = self.view.textCursor()
64
+ cur.movePosition(QTextCursor.MoveOperation.Start)
65
+ cur.movePosition(QTextCursor.MoveOperation.Down,
66
+ QTextCursor.MoveMode.KeepAnchor, extra)
67
+ cur.removeSelectedText()
68
+ self.view.setTextCursor(self.view.textCursor())
69
+
70
+ # autoscroll
71
+ sb = self.view.verticalScrollBar()
72
+ sb.setValue(sb.maximum())
73
+
74
+ def show_raise(self):
75
+ self.setVisible(True)
76
+ self.raise_()
77
+ if self.widget():
78
+ self.widget().setFocus()