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,541 @@
1
+ # pro/nbtorgb_stars.py
2
+ from __future__ import annotations
3
+ import os
4
+ import numpy as np
5
+
6
+ from PyQt6.QtCore import (
7
+ Qt, QSize, QEvent, QTimer, QPoint, QThread, pyqtSignal
8
+ )
9
+ from PyQt6.QtWidgets import (
10
+ QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QScrollArea,
11
+ QFileDialog, QInputDialog, QMessageBox, QGridLayout, QCheckBox, QSizePolicy,
12
+ QSlider
13
+ )
14
+ from PyQt6.QtGui import (
15
+ QPixmap, QImage, QIcon, QPainter, QPen, QColor, QFont, QFontMetrics,
16
+ QCursor, QMovie
17
+ )
18
+
19
+ # Legacy I/O (same used elsewhere in SASpro)
20
+ from setiastro.saspro.legacy.image_manager import load_image as legacy_load_image
21
+
22
+ from setiastro.saspro.legacy.numba_utils import applySCNR_numba, adjust_saturation_numba
23
+ from setiastro.saspro.widgets.themed_buttons import themed_toolbtn
24
+
25
+
26
+ # Optional: your stretch helpers (only used if you’d like to pre-stretch inputs)
27
+ # from imageops.stretch import stretch_mono_image, stretch_color_image
28
+
29
+
30
+ class NBtoRGBStars(QWidget):
31
+ """
32
+ SASpro version of NB→RGB Stars:
33
+ - Ha/OIII/SII mono (any subset) and/or OSC stars image
34
+ - Ha↔OIII ratio
35
+ - Optional "star stretch"
36
+ - Live preview on the right (with PPP-style zoom/pan/fit)
37
+ - Push final to a new view via DocManager
38
+ """
39
+ THUMB_ICON_SIZE = QSize(22, 22) # just for button decoration if icon_path provided
40
+
41
+ def __init__(self, doc_manager=None, parent=None, icon_path: str | None = None):
42
+ super().__init__(parent)
43
+ self.doc_manager = doc_manager
44
+ self.setWindowTitle("NB→RGB Stars")
45
+
46
+ if icon_path:
47
+ try:
48
+ self.setWindowIcon(QIcon(icon_path))
49
+ except Exception:
50
+ pass
51
+
52
+ # raw inputs (float32 ~[0..1])
53
+ self.ha : np.ndarray | None = None
54
+ self.oiii : np.ndarray | None = None
55
+ self.sii : np.ndarray | None = None
56
+ self.osc : np.ndarray | None = None # 3-channel stars-only (optional)
57
+
58
+ # filenames / metadata (best-effort)
59
+ self._file_ha = None
60
+ self._file_oiii = None
61
+ self._file_sii = None
62
+ self._file_osc = None
63
+
64
+ # output
65
+ self.final: np.ndarray | None = None
66
+
67
+ # preview pixmap/zoom state
68
+ self._base_pm: QPixmap | None = None
69
+ self._zoom = 1.0
70
+ self._min_zoom = 0.05
71
+ self._max_zoom = 20.0
72
+ self._panning = False
73
+ self._pan_last: QPoint | None = None
74
+
75
+ self._build_ui()
76
+
77
+ # ---------------- UI ----------------
78
+ def _build_ui(self):
79
+ root = QHBoxLayout(self)
80
+
81
+ # -------- left controls
82
+ left = QVBoxLayout()
83
+ left_host = QWidget(self); left_host.setLayout(left); left_host.setFixedWidth(320)
84
+
85
+ left.addWidget(QLabel(
86
+ "<b>NB→RGB Stars</b><br>"
87
+ "Load Ha / OIII / (optional SII) and/or OSC stars.<br>"
88
+ "Tune ratio and preview; push to a new view."
89
+ ))
90
+
91
+ # Load buttons + status labels
92
+ self.btn_ha = QPushButton("Load Ha…"); self.btn_ha.clicked.connect(lambda: self._load_channel("Ha"))
93
+ self.btn_oiii = QPushButton("Load OIII…"); self.btn_oiii.clicked.connect(lambda: self._load_channel("OIII"))
94
+ self.btn_sii = QPushButton("Load SII (optional)…"); self.btn_sii.clicked.connect(lambda: self._load_channel("SII"))
95
+ self.btn_osc = QPushButton("Load OSC stars (optional)…"); self.btn_osc.clicked.connect(lambda: self._load_channel("OSC"))
96
+
97
+ self.lbl_ha = QLabel("No Ha loaded.")
98
+ self.lbl_oiii = QLabel("No OIII loaded.")
99
+ self.lbl_sii = QLabel("No SII loaded.")
100
+ self.lbl_osc = QLabel("No OSC stars loaded.")
101
+
102
+ for lab in (self.lbl_ha, self.lbl_oiii, self.lbl_sii, self.lbl_osc):
103
+ lab.setWordWrap(True); lab.setStyleSheet("color:#888; margin-left:8px;")
104
+
105
+ for btn, lab in ((self.btn_ha, self.lbl_ha),
106
+ (self.btn_oiii, self.lbl_oiii),
107
+ (self.btn_sii, self.lbl_sii),
108
+ (self.btn_osc, self.lbl_osc)):
109
+ left.addWidget(btn); left.addWidget(lab)
110
+
111
+ # Ratio (Ha to OIII)
112
+ row = QHBoxLayout()
113
+ self.lbl_ratio = QLabel("Ha:OIII ratio = 0.30")
114
+ self.sld_ratio = QSlider(Qt.Orientation.Horizontal); self.sld_ratio.setRange(0, 100); self.sld_ratio.setValue(30)
115
+ self.sld_ratio.valueChanged.connect(lambda v: self.lbl_ratio.setText(f"Ha:OIII ratio = {v/100:.2f}"))
116
+ row.addWidget(self.lbl_ratio); left.addLayout(row)
117
+ left.addWidget(self.sld_ratio)
118
+
119
+ # Star Stretch
120
+ self.chk_star_stretch = QCheckBox("Enable star stretch"); self.chk_star_stretch.setChecked(True)
121
+ left.addWidget(self.chk_star_stretch)
122
+
123
+ row2 = QHBoxLayout()
124
+ self.lbl_stretch = QLabel("Stretch factor = 5.00")
125
+ self.sld_stretch = QSlider(Qt.Orientation.Horizontal); self.sld_stretch.setRange(0, 800); self.sld_stretch.setValue(500)
126
+ self.sld_stretch.valueChanged.connect(lambda v: self.lbl_stretch.setText(f"Stretch factor = {v/100:.2f}"))
127
+ row2.addWidget(self.lbl_stretch); left.addLayout(row2)
128
+ left.addWidget(self.sld_stretch)
129
+
130
+ row3 = QHBoxLayout()
131
+ self.lbl_sat = QLabel("Saturation = 1.00×")
132
+ self.sld_sat = QSlider(Qt.Orientation.Horizontal)
133
+ self.sld_sat.setRange(0, 300) # 0.00× … 3.00×
134
+ self.sld_sat.setValue(100) # 1.00× by default
135
+ self.sld_sat.valueChanged.connect(lambda v: self.lbl_sat.setText(f"Saturation = {v/100:.2f}×"))
136
+ row3.addWidget(self.lbl_sat)
137
+ left.addLayout(row3)
138
+ left.addWidget(self.sld_sat)
139
+
140
+ # Actions
141
+ act = QHBoxLayout()
142
+ self.btn_preview = QPushButton("Preview Combine"); self.btn_preview.clicked.connect(self._preview_combine)
143
+ self.btn_push = QPushButton("Push Final to New View"); self.btn_push.clicked.connect(self._push_final)
144
+ act.addWidget(self.btn_preview); act.addWidget(self.btn_push)
145
+ left.addLayout(act)
146
+
147
+ self.btn_clear = QPushButton("Clear Inputs"); self.btn_clear.clicked.connect(self._clear_inputs)
148
+ left.addWidget(self.btn_clear)
149
+
150
+ # Spinner (optional)
151
+ self.spinner = QLabel(alignment=Qt.AlignmentFlag.AlignCenter)
152
+ self.spinner_movie = QMovie(os.path.join(os.path.dirname(__file__), "spinner.gif"))
153
+ self.spinner.setMovie(self.spinner_movie); self.spinner.hide()
154
+ left.addWidget(self.spinner)
155
+
156
+ left.addStretch(1)
157
+ root.addWidget(left_host, 0)
158
+
159
+ # -------- right: preview (zoom/pan like PPP)
160
+ right = QVBoxLayout()
161
+
162
+ tools = QHBoxLayout()
163
+
164
+ self.btn_zoom_in = themed_toolbtn("zoom-in", "Zoom In")
165
+ self.btn_zoom_out = themed_toolbtn("zoom-out", "Zoom Out")
166
+ self.btn_fit = themed_toolbtn("zoom-fit-best", "Fit to Preview")
167
+
168
+ self.btn_zoom_in.clicked.connect(lambda: self._zoom_at(1.25))
169
+ self.btn_zoom_out.clicked.connect(lambda: self._zoom_at(0.8))
170
+ self.btn_fit.clicked.connect(self._fit_to_preview)
171
+
172
+ tools.addWidget(self.btn_zoom_in)
173
+ tools.addWidget(self.btn_zoom_out)
174
+ tools.addWidget(self.btn_fit)
175
+ right.addLayout(tools)
176
+
177
+
178
+ self.scroll = QScrollArea(self); self.scroll.setWidgetResizable(True)
179
+ self.scroll.setAlignment(Qt.AlignmentFlag.AlignCenter)
180
+ self.preview = QLabel(alignment=Qt.AlignmentFlag.AlignCenter)
181
+ self.scroll.setWidget(self.preview)
182
+ self.preview.setMouseTracking(True)
183
+
184
+ # Intercept wheel everywhere to prevent scroll while zooming; pan via drag
185
+ for obj in (self.preview, self.scroll, self.scroll.viewport(),
186
+ self.scroll.horizontalScrollBar(), self.scroll.verticalScrollBar()):
187
+ obj.installEventFilter(self)
188
+
189
+ right.addWidget(self.scroll, 1)
190
+ self.status = QLabel(""); right.addWidget(self.status, 0)
191
+
192
+ right_host = QWidget(self); right_host.setLayout(right)
193
+ root.addWidget(right_host, 1)
194
+
195
+ self.setLayout(root)
196
+ self.setMinimumSize(980, 640)
197
+
198
+ def showEvent(self, e):
199
+ super().showEvent(e)
200
+ QTimer.singleShot(0, self._center_scrollbars)
201
+
202
+ # ---------- file/view loading ----------
203
+ def _set_status_label(self, which: str, text: str | None):
204
+ lab = getattr(self, f"lbl_{which.lower()}")
205
+ if text:
206
+ lab.setText(text); lab.setStyleSheet("color:#2a7; font-weight:600; margin-left:8px;")
207
+ else:
208
+ lab.setText(f"No {which} loaded."); lab.setStyleSheet("color:#888; margin-left:8px;")
209
+
210
+ def _load_channel(self, which: str):
211
+ src, ok = QInputDialog.getItem(self, f"Load {which}", "Source:", ["From View", "From File"], 0, False)
212
+ if not ok: return
213
+
214
+ out = self._load_from_view(which) if src == "From View" else self._load_from_file(which)
215
+ if out is None: return
216
+ img, header, bit_depth, is_mono, path, label = out
217
+
218
+ # Normalize to floats in [0,1]; collapse mono to 2D; ensure OSC is RGB
219
+ if which in ("Ha","OIII","SII"):
220
+ if img.ndim == 3: img = img[...,0]
221
+ setattr(self, which.lower(), self._as_float01(img))
222
+ else: # OSC
223
+ # Optimization: Store mono OSC as-is (2D) to save memory
224
+ # The combine step will handle expansion.
225
+ if img.ndim == 3 and img.shape[2] == 1:
226
+ img = img[..., 0]
227
+ setattr(self, which.lower(), self._as_float01(img))
228
+
229
+ setattr(self, f"_file_{which.lower()}", path)
230
+ self._set_status_label(which, label)
231
+ self.status.setText(f"{which} loaded ({'mono' if img.ndim==2 else 'RGB'}) shape={img.shape}")
232
+
233
+ def _load_from_view(self, which):
234
+ views = self._list_open_views()
235
+ if not views:
236
+ QMessageBox.warning(self, "No Views", "No open image views found."); return None
237
+ labels = [lab for lab, _ in views]
238
+ choice, ok = QInputDialog.getItem(self, f"Select View for {which}", "Choose a view:", labels, 0, False)
239
+ if not ok or not choice: return None
240
+ sw = dict(views)[choice]
241
+ doc = getattr(sw, "document", None)
242
+ if doc is None or getattr(doc, "image", None) is None:
243
+ QMessageBox.warning(self, "Empty View", "Selected view has no image."); return None
244
+ img = doc.image
245
+ meta = getattr(doc, "metadata", {}) or {}
246
+ header = meta.get("original_header", None)
247
+ bit_depth = meta.get("bit_depth", "Unknown")
248
+ is_mono = (img.ndim == 2) or (img.ndim == 3 and img.shape[2] == 1)
249
+ path = meta.get("file_path", None)
250
+ return img, header, bit_depth, is_mono, path, f"From View: {choice}"
251
+
252
+ def _load_from_file(self, which):
253
+ filt = "Images (*.png *.tif *.tiff *.fits *.fit *.xisf)"
254
+ path, _ = QFileDialog.getOpenFileName(self, f"Select {which} File", "", filt)
255
+ if not path: return None
256
+ img, header, bit_depth, is_mono = legacy_load_image(path)
257
+ if img is None:
258
+ QMessageBox.critical(self, "Load Error", f"Could not load {os.path.basename(path)}"); return None
259
+ return img, header, bit_depth, is_mono, path, f"From File: {os.path.basename(path)}"
260
+
261
+ # ---------- combine / preview ----------
262
+ def _preview_combine(self):
263
+ if (self.osc is None) and not (self.ha is not None and self.oiii is not None):
264
+ QMessageBox.warning(self, "Missing Images", "Load OSC, or Ha+OIII (SII optional).")
265
+ return
266
+
267
+ self.spinner.show(); self.spinner_movie.start()
268
+
269
+ ratio = self.sld_ratio.value() / 100.0
270
+ stretch_enabled = self.chk_star_stretch.isChecked()
271
+ stretch_factor = self.sld_stretch.value() / 100.0
272
+ sat_factor = self.sld_sat.value() / 100.0
273
+
274
+ try:
275
+ # 1) combine (no SCNR here)
276
+ rgb = self._combine_nb_rgb(ratio, stretch_enabled, stretch_factor)
277
+
278
+ # 2) ensure float32 & contiguous for numba
279
+ rgb = np.ascontiguousarray(rgb.astype(np.float32))
280
+
281
+ # 3) SCNR (numba)
282
+ rgb = applySCNR_numba(rgb)
283
+
284
+ # 4) Saturation (numba)
285
+ if abs(sat_factor - 1.0) > 1e-3:
286
+ rgb = adjust_saturation_numba(rgb, sat_factor)
287
+
288
+ self.final = np.clip(rgb, 0.0, 1.0)
289
+
290
+ except Exception as e:
291
+ self.spinner.hide(); self.spinner_movie.stop()
292
+ QMessageBox.critical(self, "Combine Error", str(e))
293
+ return
294
+
295
+ self._set_preview_image(self._to_qimage(self.final))
296
+ self.status.setText("Preview updated.")
297
+ self.spinner.hide(); self.spinner_movie.stop()
298
+
299
+
300
+ def _combine_nb_rgb(self, ratio: float, star_stretch: bool, stretch_k: float) -> np.ndarray:
301
+ """
302
+ Combine to RGB:
303
+ - If OSC present: use channels from OSC, optionally blend Ha/SII/OO into them.
304
+ - Else NB-only: R ~ 0.5*(Ha+SII), G ~ mix(Ha, OIII) via ratio, B ~ OIII.
305
+ Shapes must match.
306
+ """
307
+ # Ensure shapes
308
+ shapes = [x.shape[:2] for x in (self.ha, self.oiii, self.sii) if x is not None]
309
+ if self.osc is not None:
310
+ shapes.append(self.osc.shape[:2])
311
+ if shapes and len(set(shapes)) != 1:
312
+ raise ValueError(f"Channel sizes differ: {set(shapes)}")
313
+
314
+ if self.osc is not None:
315
+ if self.osc.ndim == 2:
316
+ r = self.osc; g = self.osc; b = self.osc
317
+ elif self.osc.ndim == 3 and self.osc.shape[2] >= 3:
318
+ r = self.osc[...,0]; g = self.osc[...,1]; b = self.osc[...,2]
319
+ else:
320
+ # fallback for unexpected shapes (e.g. 3D but 1-channel)
321
+ r = self.osc.squeeze(); g = r; b = r
322
+
323
+ sii = self.sii if self.sii is not None else r
324
+ ha = self.ha if self.ha is not None else r
325
+ oiii= self.oiii if self.oiii is not None else b
326
+
327
+ r_out = 0.5*r + 0.5*sii
328
+ g_out = ratio*ha + (1.0 - ratio)*g
329
+ b_out = oiii
330
+ else:
331
+ if self.ha is None or self.oiii is None:
332
+ raise ValueError("Need Ha and OIII if no OSC image is provided.")
333
+ ha = self.ha
334
+ sii = self.sii if self.sii is not None else ha
335
+ oiii = self.oiii
336
+ r_out = 0.5*ha + 0.5*sii
337
+ g_out = ratio*ha + (1.0 - ratio)*oiii
338
+ b_out = oiii
339
+
340
+ rgb = np.stack([r_out, g_out, b_out], axis=2).astype(np.float32)
341
+ rgb = np.clip(rgb, 0, 1)
342
+
343
+ if star_stretch:
344
+ # Simple non-linear boost; bounded and monotonic
345
+ # ((3^k)*x) / ((3^k - 1)*x + 1)
346
+ t = 3.0 ** float(stretch_k)
347
+ rgb = (t*rgb) / ((t - 1.0)*rgb + 1.0)
348
+ rgb = np.clip(rgb, 0, 1)
349
+
350
+ return rgb
351
+
352
+
353
+ # ---------- preview helpers + zoom/pan ----------
354
+ def _set_preview_image(self, qimg: QImage):
355
+ self._base_pm = QPixmap.fromImage(qimg)
356
+ self._zoom = 1.0
357
+ self._update_preview_pixmap()
358
+ QTimer.singleShot(0, self._center_scrollbars)
359
+
360
+ def _update_preview_pixmap(self):
361
+ if self._base_pm is None: return
362
+ scaled = self._base_pm.scaled(
363
+ self._base_pm.size() * self._zoom,
364
+ Qt.AspectRatioMode.KeepAspectRatio,
365
+ Qt.TransformationMode.SmoothTransformation
366
+ )
367
+ self.preview.setPixmap(scaled)
368
+ self.preview.resize(scaled.size())
369
+
370
+ def _set_zoom(self, new_zoom: float):
371
+ self._zoom = max(self._min_zoom, min(self._max_zoom, new_zoom))
372
+ self._update_preview_pixmap()
373
+
374
+ def _zoom_at(self, factor: float = 1.25, anchor_vp: QPoint | None = None):
375
+ if self._base_pm is None: return
376
+
377
+ old_zoom = self._zoom
378
+ new_zoom = max(self._min_zoom, min(self._max_zoom, old_zoom * factor))
379
+ ratio = new_zoom / max(1e-6, old_zoom)
380
+
381
+ vp = self.scroll.viewport()
382
+ if anchor_vp is None:
383
+ anchor_vp = QPoint(vp.width() // 2, vp.height() // 2) # center of view
384
+
385
+ hbar = self.scroll.horizontalScrollBar()
386
+ vbar = self.scroll.verticalScrollBar()
387
+
388
+ content_x = hbar.value() + anchor_vp.x()
389
+ content_y = vbar.value() + anchor_vp.y()
390
+
391
+ self._set_zoom(new_zoom)
392
+
393
+ if self.preview.width() <= vp.width():
394
+ hbar.setValue((hbar.maximum() + hbar.minimum()) // 2)
395
+ else:
396
+ new_h = int(content_x * ratio - anchor_vp.x())
397
+ hbar.setValue(max(hbar.minimum(), min(hbar.maximum(), new_h)))
398
+
399
+ if self.preview.height() <= vp.height():
400
+ vbar.setValue((vbar.maximum() + vbar.minimum()) // 2)
401
+ else:
402
+ new_v = int(content_y * ratio - anchor_vp.y())
403
+ vbar.setValue(max(vbar.minimum(), min(vbar.maximum(), new_v)))
404
+
405
+ def _fit_to_preview(self):
406
+ if self._base_pm is None: return
407
+ vp = self.scroll.viewport().size()
408
+ pm = self._base_pm.size()
409
+ if pm.width() == 0 or pm.height() == 0: return
410
+ k = min(vp.width() / pm.width(), vp.height() / pm.height())
411
+ self._set_zoom(max(self._min_zoom, min(self._max_zoom, k)))
412
+ self._center_scrollbars()
413
+
414
+ def _center_scrollbars(self):
415
+ h = self.scroll.horizontalScrollBar()
416
+ v = self.scroll.verticalScrollBar()
417
+ h.setValue((h.maximum() + h.minimum()) // 2)
418
+ v.setValue((v.maximum() + v.minimum()) // 2)
419
+
420
+ # ---------- utilities ----------
421
+ def _clear_inputs(self):
422
+ self.ha = self.oiii = self.sii = self.osc = None
423
+ self._file_ha = self._file_oiii = self._file_sii = self._file_osc = None
424
+ self.final = None
425
+ self.preview.clear(); self._base_pm = None
426
+ for which in ("Ha","OIII","SII","OSC"):
427
+ self._set_status_label(which, None)
428
+ self.status.setText("Cleared inputs.")
429
+
430
+ @staticmethod
431
+ def _as_float01(arr):
432
+ a = np.asarray(arr)
433
+ if a.dtype == np.uint8: return a.astype(np.float32)/255.0
434
+ if a.dtype == np.uint16: return a.astype(np.float32)/65535.0
435
+ return np.clip(a.astype(np.float32), 0.0, 1.0)
436
+
437
+ @staticmethod
438
+ def _to_qimage(arr):
439
+ a = np.clip(arr, 0, 1)
440
+ if a.ndim == 2:
441
+ u = (a * 255).astype(np.uint8); h, w = u.shape
442
+ return QImage(u.data, w, h, w, QImage.Format.Format_Grayscale8).copy()
443
+ if a.ndim == 3 and a.shape[2] == 3:
444
+ u = (a * 255).astype(np.uint8); h, w, _ = u.shape
445
+ return QImage(u.data, w, h, w*3, QImage.Format.Format_RGB888).copy()
446
+ raise ValueError(f"Unexpected image shape: {a.shape}")
447
+
448
+ def _find_main_window(self):
449
+ w = self
450
+ from PyQt6.QtWidgets import QMainWindow, QApplication
451
+ while w is not None and not isinstance(w, QMainWindow):
452
+ w = w.parentWidget()
453
+ if w: return w
454
+ for tlw in QApplication.topLevelWidgets():
455
+ if isinstance(tlw, QMainWindow):
456
+ return tlw
457
+ return None
458
+
459
+ def _list_open_views(self):
460
+ mw = self._find_main_window()
461
+ if not mw: return []
462
+ try:
463
+ from setiastro.saspro.subwindow import ImageSubWindow
464
+ subs = mw.findChildren(ImageSubWindow)
465
+ except Exception:
466
+ subs = []
467
+ out = []
468
+ for sw in subs:
469
+ title = getattr(sw, "view_title", None) or sw.windowTitle() or getattr(sw.document, "display_name", lambda: "Untitled")()
470
+ out.append((str(title), sw))
471
+ return out
472
+
473
+ def _push_final(self):
474
+ if self.final is None:
475
+ QMessageBox.warning(self, "No Image", "Preview first, then push."); return
476
+ mw = self._find_main_window()
477
+ dm = getattr(mw, "docman", None)
478
+ if not mw or not dm:
479
+ QMessageBox.critical(self, "UI", "Main window or DocManager not available."); return
480
+ title = "NB→RGB Stars"
481
+ try:
482
+ if hasattr(dm, "open_array"):
483
+ doc = dm.open_array(self.final, metadata={"is_mono": False}, title=title)
484
+ elif hasattr(dm, "create_document"):
485
+ doc = dm.create_document(image=self.final, metadata={"is_mono": False}, name=title)
486
+ else:
487
+ raise RuntimeError("DocManager lacks open_array/create_document")
488
+ if hasattr(mw, "_spawn_subwindow_for"):
489
+ mw._spawn_subwindow_for(doc)
490
+ else:
491
+ from setiastro.saspro.subwindow import ImageSubWindow
492
+ sw = ImageSubWindow(doc, parent=mw); sw.setWindowTitle(title); sw.show()
493
+ self.status.setText("Opened final composite in a new view.")
494
+ except Exception as e:
495
+ QMessageBox.critical(self, "Error", f"Failed to open new view:\n{e}")
496
+
497
+ # ---------- event filter (zoom/pan like PPP) ----------
498
+ def eventFilter(self, obj, ev):
499
+ # Ctrl+wheel zoom at mouse (prevent scrolling); wheel without Ctrl: eat it (no scroll)
500
+ if ev.type() == QEvent.Type.Wheel and (
501
+ obj is self.preview
502
+ or obj is self.scroll
503
+ or obj is self.scroll.viewport()
504
+ or obj is self.scroll.horizontalScrollBar()
505
+ or obj is self.scroll.verticalScrollBar()
506
+ ):
507
+ if ev.modifiers() & Qt.KeyboardModifier.ControlModifier:
508
+ factor = 1.25 if ev.angleDelta().y() > 0 else 0.8
509
+ # safer: compute anchor in viewport coords via global position
510
+ try:
511
+ anchor_vp = self.scroll.viewport().mapFromGlobal(ev.globalPosition().toPoint())
512
+ except Exception:
513
+ vp = self.scroll.viewport()
514
+ anchor_vp = QPoint(vp.width()//2, vp.height()//2)
515
+ self._zoom_at(factor, anchor_vp)
516
+ ev.accept()
517
+ return True
518
+
519
+ # click-drag pan on viewport
520
+ if obj is self.scroll.viewport():
521
+ if ev.type() == QEvent.Type.MouseButtonPress and ev.button() == Qt.MouseButton.LeftButton:
522
+ self._panning = True
523
+ self._pan_last = ev.position().toPoint()
524
+ self.scroll.viewport().setCursor(QCursor(Qt.CursorShape.ClosedHandCursor))
525
+ return True
526
+ if ev.type() == QEvent.Type.MouseMove and self._panning:
527
+ cur = ev.position().toPoint()
528
+ delta = cur - (self._pan_last or cur)
529
+ self._pan_last = cur
530
+ h = self.scroll.horizontalScrollBar()
531
+ v = self.scroll.verticalScrollBar()
532
+ h.setValue(h.value() - delta.x())
533
+ v.setValue(v.value() - delta.y())
534
+ return True
535
+ if ev.type() == QEvent.Type.MouseButtonRelease and ev.button() == Qt.MouseButton.LeftButton:
536
+ self._panning = False
537
+ self._pan_last = None
538
+ self.scroll.viewport().setCursor(QCursor(Qt.CursorShape.ArrowCursor))
539
+ return True
540
+
541
+ return super().eventFilter(obj, ev)