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,168 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Internationalization (i18n) module for Seti Astro Suite Pro.
4
+
5
+ Handles loading and managing translations using PyQt6's QTranslator.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from typing import Optional, Dict, List
11
+
12
+ from PyQt6.QtCore import QCoreApplication, QTranslator, QLocale, QSettings
13
+
14
+ # Module-level translator instance (kept alive for the app lifetime)
15
+ _translator: Optional[QTranslator] = None
16
+ _current_language: str = "en"
17
+
18
+ # Available languages with display names
19
+ AVAILABLE_LANGUAGES: Dict[str, str] = {
20
+ "en": "English",
21
+ "it": "Italiano",
22
+ "fr": "Français",
23
+ "es": "Español",
24
+ "zh": "简体中文",
25
+ "de": "Deutsch",
26
+ "pt": "Português",
27
+ "ja": "日本語",
28
+ "hi": "हिन्दी",
29
+ "sw": "Kiswahili",
30
+ "uk": "Українська",
31
+ "ru": "Русский",
32
+ "ar": "العربية",
33
+ }
34
+
35
+
36
+ def get_translations_dir() -> str:
37
+ """Get the path to the translations directory."""
38
+ # Source / installed package location
39
+ module_dir = os.path.dirname(os.path.abspath(__file__))
40
+ pkg_dir = os.path.join(module_dir, "translations")
41
+
42
+ # PyInstaller frozen builds
43
+ if hasattr(os.sys, "_MEIPASS"):
44
+ # New bundle layout (preferred)
45
+ frozen_internal = os.path.join(os.sys._MEIPASS, "_internal", "translations")
46
+ if os.path.exists(frozen_internal):
47
+ return frozen_internal
48
+
49
+ # Legacy bundle layout fallback
50
+ frozen_legacy = os.path.join(os.sys._MEIPASS, "translations")
51
+ if os.path.exists(frozen_legacy):
52
+ return frozen_legacy
53
+
54
+ return pkg_dir
55
+
56
+
57
+
58
+ def get_available_languages() -> Dict[str, str]:
59
+ """
60
+ Get available languages.
61
+
62
+ Returns:
63
+ Dict mapping language codes to display names (e.g., {"en": "English"})
64
+ """
65
+ return AVAILABLE_LANGUAGES.copy()
66
+
67
+
68
+ def get_current_language() -> str:
69
+ """Get the currently loaded language code."""
70
+ return _current_language
71
+
72
+
73
+ def get_saved_language() -> str:
74
+ """
75
+ Get the language saved in settings.
76
+
77
+ Returns:
78
+ Language code from settings, defaults to "en"
79
+ """
80
+ settings = QSettings("SetiAstro", "SetiAstroSuitePro")
81
+ return settings.value("ui/language", "en", type=str) or "en"
82
+
83
+
84
+ def save_language(lang_code: str) -> None:
85
+ """
86
+ Save the language preference to settings.
87
+
88
+ Args:
89
+ lang_code: Language code (e.g., "it", "fr", "es")
90
+ """
91
+ settings = QSettings("SetiAstro", "SetiAstroSuitePro")
92
+ settings.setValue("ui/language", lang_code)
93
+ settings.sync()
94
+
95
+
96
+ def load_language(lang_code: str = None, app: QCoreApplication = None) -> bool:
97
+ """
98
+ Load a translation for the specified language.
99
+
100
+ Must be called BEFORE creating any widgets for full effect.
101
+
102
+ Args:
103
+ lang_code: Language code (e.g., "it", "fr", "es").
104
+ If None, reads from settings.
105
+ app: QApplication instance. If None, uses QCoreApplication.instance()
106
+
107
+ Returns:
108
+ True if translation was loaded successfully, False otherwise
109
+ """
110
+ global _translator, _current_language
111
+
112
+ if app is None:
113
+ app = QCoreApplication.instance()
114
+
115
+ if app is None:
116
+ return False
117
+
118
+ # Get language from settings if not specified
119
+ if lang_code is None:
120
+ lang_code = get_saved_language()
121
+
122
+ # English is the base language, no translation needed
123
+ if lang_code == "en":
124
+ # Remove any existing translator
125
+ if _translator is not None:
126
+ app.removeTranslator(_translator)
127
+ _translator = None
128
+ _current_language = "en"
129
+ return True
130
+
131
+ # Find translation file
132
+ translations_dir = get_translations_dir()
133
+ qm_file = os.path.join(translations_dir, f"saspro_{lang_code}.qm")
134
+
135
+ if not os.path.exists(qm_file):
136
+ # Translation file doesn't exist yet
137
+ _current_language = lang_code
138
+ return False
139
+
140
+ # Remove old translator if any
141
+ if _translator is not None:
142
+ app.removeTranslator(_translator)
143
+
144
+ # Load new translator
145
+ _translator = QTranslator()
146
+ if _translator.load(qm_file):
147
+ app.installTranslator(_translator)
148
+ _current_language = lang_code
149
+ return True
150
+ else:
151
+ _translator = None
152
+ return False
153
+
154
+
155
+ def tr(text: str, context: str = "Global") -> str:
156
+ """
157
+ Translate a string outside of a QObject context.
158
+
159
+ For use in module-level code or functions that aren't methods of QObject.
160
+
161
+ Args:
162
+ text: Text to translate
163
+ context: Context for disambiguation (default: "Global")
164
+
165
+ Returns:
166
+ Translated string
167
+ """
168
+ return QCoreApplication.translate(context, text)
@@ -0,0 +1,417 @@
1
+ # pro/image_combine.py
2
+ from __future__ import annotations
3
+ import numpy as np
4
+
5
+ from PyQt6.QtCore import Qt, QPoint, QRect, QEvent
6
+ from PyQt6.QtGui import QImage, QPixmap
7
+ from PyQt6.QtWidgets import (
8
+ QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QLabel, QComboBox, QSlider,
9
+ QCheckBox, QScrollArea, QPushButton, QDialogButtonBox, QApplication, QMessageBox
10
+ )
11
+
12
+ # NEW: optional cv2 for fast gray/resize
13
+ try:
14
+ import cv2
15
+ except Exception:
16
+ cv2 = None
17
+
18
+ # Shared utilities
19
+ from setiastro.saspro.widgets.image_utils import (
20
+ to_float01 as _to_float01,
21
+ extract_mask_from_document as _active_mask_array_from_doc
22
+ )
23
+ from setiastro.saspro.widgets.themed_buttons import themed_toolbtn
24
+
25
+
26
+ _LUMA_WEIGHTS = np.array([0.2126, 0.7152, 0.0722], dtype=np.float32)
27
+
28
+ # ---------- helpers ----------
29
+ def _doc_name(d) -> str:
30
+ try: return d.display_name()
31
+ except Exception: return "Untitled"
32
+
33
+ def _rgb_to_luma(img: np.ndarray) -> np.ndarray:
34
+ f = _to_float01(img)
35
+ if f.ndim == 2: return f
36
+ if f.ndim == 3 and f.shape[2] == 1: return f[..., 0]
37
+ if f.ndim == 3 and f.shape[2] == 3:
38
+ w = _LUMA_WEIGHTS
39
+ return f[..., 0]*w[0] + f[..., 1]*w[1] + f[..., 2]*w[2]
40
+ raise ValueError(f"Unsupported image shape: {img.shape}")
41
+
42
+ def _recombine_luma_into_rgb(Y: np.ndarray, RGB: np.ndarray) -> np.ndarray:
43
+ rgb = _to_float01(RGB)
44
+ if rgb.ndim != 3 or rgb.shape[2] != 3:
45
+ raise ValueError("Recombine requires RGB target.")
46
+ w = _LUMA_WEIGHTS
47
+ orig_Y = rgb[..., 0]*w[0] + rgb[..., 1]*w[1] + rgb[..., 2]*w[2]
48
+ chroma = rgb / (orig_Y[..., None] + 1e-6)
49
+ return np.clip(chroma * Y[..., None], 0.0, 1.0)
50
+
51
+ def _blend_dispatch(A: np.ndarray, B: np.ndarray, mode: str, alpha: float) -> np.ndarray:
52
+ A = _to_float01(A); B = _to_float01(B)
53
+ if A.ndim == 2: A = A[..., None]
54
+ if B.ndim == 2: B = B[..., None]
55
+ if A.shape != B.shape:
56
+ raise ValueError("Images must have same size/channels.")
57
+
58
+ if mode == "Average": return np.clip(0.5*(A+B), 0.0, 1.0)
59
+ if mode == "Blend": return np.clip(A*(1-alpha) + B*alpha, 0.0, 1.0)
60
+ def mix(x): return np.clip(A*(1-alpha) + x*alpha, 0.0, 1.0)
61
+
62
+ eps = 1e-6
63
+ if mode == "Add": return mix(np.clip(A+B, 0.0, 1.0))
64
+ if mode == "Subtract": return mix(np.clip(A-B, 0.0, 1.0))
65
+ if mode == "Multiply": return mix(A*B)
66
+ if mode == "Divide": return mix(np.clip(A/(B+eps), 0.0, 1.0))
67
+ if mode == "Screen": return mix(1.0 - (1.0-A)*(1.0-B))
68
+ if mode == "Overlay": return mix(np.clip(np.where(A<=0.5, 2*A*B, 1-2*(1-A)*(1-B)), 0.0, 1.0))
69
+ if mode == "Difference": return mix(np.abs(A-B))
70
+ return np.clip(A*(1-alpha) + B*alpha, 0.0, 1.0)
71
+
72
+ # ---------- mask helpers ----------
73
+ def _resize_mask_nearest(m: np.ndarray, shape_hw: tuple[int,int]) -> np.ndarray:
74
+ """Resize mask to (H,W) with nearest neighbor."""
75
+ h, w = shape_hw
76
+ if m.shape == (h, w):
77
+ return m
78
+ if cv2 is not None:
79
+ return cv2.resize(m, (w, h), interpolation=cv2.INTER_NEAREST).astype(np.float32, copy=False)
80
+ # fallback NN without cv2
81
+ yi = (np.linspace(0, m.shape[0]-1, h)).astype(np.int32)
82
+ xi = (np.linspace(0, m.shape[1]-1, w)).astype(np.int32)
83
+ return m[yi][:, xi].astype(np.float32, copy=False)
84
+
85
+ # ---------- dialog ----------
86
+ class ImageCombineDialog(QDialog):
87
+ """
88
+ Views-based Image Combine with realtime preview, zoom/pan, luma-only, and mask overlay.
89
+ Output: replace A or create new view.
90
+ """
91
+ def __init__(self, main_window):
92
+ super().__init__(main_window)
93
+ self.setWindowTitle("Image Combine")
94
+ self.setWindowFlag(Qt.WindowType.Window, True)
95
+ self.setWindowModality(Qt.WindowModality.NonModal)
96
+ self.setModal(False)
97
+ self.mw = main_window
98
+ self.dm = getattr(main_window, "doc_manager", None) or getattr(main_window, "dm", None)
99
+ self.zoom = 1.0
100
+ self._pan_origin = None
101
+ self._hstart = 0; self._vstart = 0
102
+ self._pix = None # last preview QPixmap
103
+
104
+ # --- UI ---
105
+ root = QVBoxLayout(self)
106
+
107
+ frm = QFormLayout()
108
+ self.cbA = QComboBox(); self.cbB = QComboBox()
109
+ frm.addRow("Source A:", self.cbA)
110
+ frm.addRow("Source B:", self.cbB)
111
+
112
+ row = QHBoxLayout()
113
+ row.addWidget(QLabel("Mode:"))
114
+ self.cbMode = QComboBox()
115
+ self.cbMode.addItems(["Average","Add","Subtract","Blend","Multiply","Divide","Screen","Overlay","Difference"])
116
+ row.addWidget(self.cbMode, 1)
117
+ row.addWidget(QLabel("Opacity:"))
118
+ self.slAlpha = QSlider(Qt.Orientation.Horizontal); self.slAlpha.setRange(0,100); self.slAlpha.setValue(100)
119
+ row.addWidget(self.slAlpha, 2)
120
+ frm.addRow(row)
121
+
122
+ # luma-only
123
+ self.chkLuma = QCheckBox("Combine luminance only (keep A’s color)")
124
+ frm.addRow(self.chkLuma)
125
+
126
+ # mask overlay
127
+ mrow = QHBoxLayout()
128
+ self.chkOverlay = QCheckBox("Show mask overlay")
129
+ self.chkInvert = QCheckBox("Invert mask")
130
+ mrow.addWidget(self.chkOverlay)
131
+ mrow.addWidget(self.chkInvert)
132
+ mrow.addWidget(QLabel("Overlay opacity:"))
133
+ self.slOverlay = QSlider(Qt.Orientation.Horizontal); self.slOverlay.setRange(5,95); self.slOverlay.setValue(40)
134
+ mrow.addWidget(self.slOverlay, 1)
135
+ frm.addRow(mrow)
136
+ root.addLayout(frm)
137
+
138
+ # preview
139
+ self.scroll = QScrollArea(self); self.scroll.setWidgetResizable(True)
140
+ self.lbl = QLabel(""); self.lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
141
+ self.scroll.setWidget(self.lbl)
142
+ root.addWidget(self.scroll, 1)
143
+
144
+ # zoom (themed)
145
+ zrow = QHBoxLayout()
146
+
147
+ btnOut = themed_toolbtn("zoom-out", "Zoom Out")
148
+ btnFit = themed_toolbtn("zoom-fit-best", "Fit to Preview")
149
+ btnIn = themed_toolbtn("zoom-in", "Zoom In")
150
+
151
+ btnOut.clicked.connect(self._zoom_out)
152
+ btnIn .clicked.connect(self._zoom_in)
153
+ btnFit.clicked.connect(self._fit)
154
+
155
+ zrow.addWidget(btnOut)
156
+ zrow.addWidget(btnFit)
157
+ zrow.addWidget(btnIn)
158
+ root.addLayout(zrow)
159
+
160
+ # buttons
161
+ btns = QDialogButtonBox()
162
+ self.btnApply = btns.addButton("Apply", QDialogButtonBox.ButtonRole.AcceptRole)
163
+ self.btnClose = btns.addButton("Close", QDialogButtonBox.ButtonRole.RejectRole)
164
+ self.btnClose.clicked.connect(self.reject)
165
+ self.btnApply.clicked.connect(self._commit)
166
+ root.addWidget(btns)
167
+
168
+ # hooks
169
+ for w in (self.cbA, self.cbB, self.cbMode):
170
+ w.currentIndexChanged.connect(self._update_preview)
171
+ self.slAlpha.valueChanged.connect(self._update_preview)
172
+ self.chkLuma.toggled.connect(self._update_preview)
173
+ self.chkOverlay.toggled.connect(self._update_preview)
174
+ self.chkInvert.toggled.connect(self._update_preview)
175
+ self.slOverlay.valueChanged.connect(self._update_preview)
176
+ self.scroll.viewport().installEventFilter(self)
177
+
178
+ self._populate_docs()
179
+ self._update_preview()
180
+
181
+ # ---------- doc utilities ----------
182
+ def _open_docs(self) -> list:
183
+ if not self.dm: return []
184
+ docs = list(getattr(self.dm, "_docs", []) or [])
185
+ return [d for d in docs if getattr(d, "image", None) is not None]
186
+
187
+ def _active_doc(self):
188
+ if self.dm and hasattr(self.dm, "get_active_document"):
189
+ return self.dm.get_active_document()
190
+ return None
191
+
192
+ def _populate_docs(self):
193
+ docs = self._open_docs()
194
+ self.cbA.blockSignals(True); self.cbB.blockSignals(True)
195
+ self.cbA.clear(); self.cbB.clear()
196
+ for d in docs:
197
+ self.cbA.addItem(_doc_name(d), userData=d)
198
+ self.cbB.addItem(_doc_name(d), userData=d)
199
+ self.cbA.blockSignals(False); self.cbB.blockSignals(False)
200
+ if docs:
201
+ act = self._active_doc()
202
+ if act in docs:
203
+ self.cbA.setCurrentIndex(docs.index(act))
204
+ # B defaults to “other”
205
+ j = 0 if len(docs) < 2 else (1 if docs[0] is act else 0)
206
+ self.cbB.setCurrentIndex(j)
207
+
208
+ # ---------- mask helpers ----------
209
+ def _mask01_for_doc(self, doc, *, shape_hw: tuple[int,int], channels: int | None, invert_flag: bool):
210
+ """
211
+ Return mask for the given doc resized to (H,W).
212
+ If channels is 3 and mask is 2D, expand with np.repeat.
213
+ """
214
+ m = _active_mask_array_from_doc(doc)
215
+ if m is None:
216
+ # last-resort fallback to global mask manager (in case user applied a global mask)
217
+ mm = getattr(getattr(self.mw, "image_manager", None), "mask_manager", None)
218
+ if mm and hasattr(mm, "get_applied_mask"):
219
+ try:
220
+ mg = mm.get_applied_mask()
221
+ if mg is not None:
222
+ mg = np.asarray(mg).astype(np.float32)
223
+ if mg.ndim == 3:
224
+ mg = mg.mean(axis=2)
225
+ if mg.max() > 1.0:
226
+ mg /= 255.0
227
+ m = np.clip(mg, 0.0, 1.0)
228
+ except Exception:
229
+ m = None
230
+ if m is None:
231
+ return None
232
+
233
+ m = _resize_mask_nearest(m, shape_hw)
234
+ if invert_flag:
235
+ m = 1.0 - m
236
+ m = np.clip(m, 0.0, 1.0)
237
+ if channels and channels > 1 and m.ndim == 2:
238
+ m = np.repeat(m[:, :, None], channels, axis=2)
239
+ return m
240
+
241
+ def _apply_overlay(self, img, mask, opacity):
242
+ # show protected region (A) as red wash: vis = 1 - m
243
+ vis = 1.0 - np.clip(mask, 0.0, 1.0)
244
+ if img.ndim == 2:
245
+ rgb = np.stack([img, img, img], axis=-1)
246
+ else:
247
+ rgb = img
248
+ overlay = np.zeros_like(rgb, dtype=np.float32); overlay[..., 0] = 1.0
249
+ if vis.ndim == 2: vis = vis[..., None]
250
+ return np.clip(rgb*(1.0 - vis*opacity) + overlay*(vis*opacity), 0.0, 1.0)
251
+
252
+ # ---------- preview ----------
253
+ def _update_preview(self, *_):
254
+ A = self.cbA.currentData(); B = self.cbB.currentData()
255
+ if not (A and B): return
256
+ imgA = getattr(A, "image", None); imgB = getattr(B, "image", None)
257
+ if imgA is None or imgB is None: return
258
+ if imgA.shape[:2] != imgB.shape[:2]:
259
+ self.lbl.setText("Images must be the same size.")
260
+ return
261
+
262
+ alpha = self.slAlpha.value()/100.0
263
+ mode = self.cbMode.currentText()
264
+
265
+ try:
266
+ if self.chkLuma.isChecked():
267
+ if imgA.ndim != 3 or imgA.shape[2] != 3:
268
+ self.lbl.setText("Luminance mode requires RGB A."); return
269
+ YA = _rgb_to_luma(imgA)
270
+ YB = _rgb_to_luma(imgB)
271
+ Ymix = _blend_dispatch(YA[...,None], YB[...,None], mode, alpha)[...,0]
272
+
273
+ # mask from destination doc (A)
274
+ m = self._mask01_for_doc(A, shape_hw=Ymix.shape[:2], channels=None,
275
+ invert_flag=self.chkInvert.isChecked())
276
+ if m is not None:
277
+ Ymix = Ymix*m + YA*(1.0 - m)
278
+
279
+ blended = _recombine_luma_into_rgb(Ymix, imgA)
280
+
281
+ else:
282
+ A3 = imgA if imgA.ndim == 3 else imgA[..., None]
283
+ B3 = imgB if imgB.ndim == 3 else imgB[..., None]
284
+ blended = _blend_dispatch(A3, B3, mode, alpha)
285
+ if imgA.ndim == 2:
286
+ blended = blended[...,0]
287
+
288
+ # mask from destination doc (A)
289
+ m = self._mask01_for_doc(
290
+ A, shape_hw=blended.shape[:2],
291
+ channels=(blended.shape[2] if blended.ndim == 3 else 1),
292
+ invert_flag=self.chkInvert.isChecked()
293
+ )
294
+ if m is not None:
295
+ blended = np.clip(blended*m + _to_float01(imgA)*(1.0 - m), 0.0, 1.0)
296
+
297
+ # optional red overlay
298
+ if self.chkOverlay.isChecked():
299
+ m = self._mask01_for_doc(
300
+ A, shape_hw=blended.shape[:2],
301
+ channels=(blended.shape[2] if blended.ndim == 3 else 1),
302
+ invert_flag=self.chkInvert.isChecked()
303
+ )
304
+ if m is not None:
305
+ blended = self._apply_overlay(_to_float01(blended), m, self.slOverlay.value()/100.0)
306
+
307
+ # to pixmap
308
+ f = _to_float01(blended); h, w = f.shape[:2]
309
+ if f.ndim == 2:
310
+ buf = (f*255).astype(np.uint8); q = QImage(buf.data, w, h, w, QImage.Format.Format_Grayscale8)
311
+ else:
312
+ buf = (f*255).astype(np.uint8); q = QImage(buf.data, w, h, 3*w, QImage.Format.Format_RGB888)
313
+ self._pix = QPixmap.fromImage(q)
314
+ self._apply_zoom()
315
+ except Exception as e:
316
+ self.lbl.setText(f"Error: {e}")
317
+
318
+ # ---------- apply ----------
319
+ def _commit(self):
320
+ A = self.cbA.currentData(); B = self.cbB.currentData()
321
+ if not (A and B): return
322
+ imgA = getattr(A, "image", None); imgB = getattr(B, "image", None)
323
+ if imgA is None or imgB is None: return
324
+ if imgA.shape[:2] != imgB.shape[:2]:
325
+ QMessageBox.warning(self, "Image Combine", "Image sizes must match."); return
326
+
327
+ alpha = self.slAlpha.value()/100.0
328
+ mode = self.cbMode.currentText()
329
+
330
+ try:
331
+ if self.chkLuma.isChecked():
332
+ YA = _rgb_to_luma(imgA); YB = _rgb_to_luma(imgB)
333
+ Ymix = _blend_dispatch(YA[...,None], YB[...,None], mode, alpha)[...,0]
334
+
335
+ m = self._mask01_for_doc(A, shape_hw=Ymix.shape[:2], channels=None,
336
+ invert_flag=self.chkInvert.isChecked())
337
+ if m is not None:
338
+ Ymix = Ymix*m + YA*(1.0 - m)
339
+
340
+ result = _recombine_luma_into_rgb(Ymix, imgA)
341
+ step = f"Luminance {mode}"
342
+ else:
343
+ A3 = imgA if imgA.ndim == 3 else imgA[..., None]
344
+ B3 = imgB if imgB.ndim == 3 else imgB[..., None]
345
+ result = _blend_dispatch(A3, B3, mode, alpha)
346
+ if imgA.ndim == 2: result = result[...,0]
347
+
348
+ m = self._mask01_for_doc(
349
+ A, shape_hw=result.shape[:2],
350
+ channels=(result.shape[2] if result.ndim == 3 else 1),
351
+ invert_flag=self.chkInvert.isChecked()
352
+ )
353
+ if m is not None:
354
+ result = np.clip(result*m + _to_float01(imgA)*(1.0 - m), 0.0, 1.0)
355
+ step = f"{mode} Combine"
356
+
357
+ result = _to_float01(result)
358
+
359
+ # Replace A (overwrite active view) or create new?
360
+ replace = True
361
+ if replace:
362
+ if hasattr(A, "set_image"):
363
+ A.set_image(result, step_name=f"Image Combine: {step}")
364
+ else:
365
+ A.image = result
366
+ try: self.mw._log(f"Image Combine → replaced '{_doc_name(A)}' ({step})")
367
+ except Exception as e:
368
+ import logging
369
+ logging.debug(f"Exception suppressed: {type(e).__name__}: {e}")
370
+ else:
371
+ newdoc = self.dm.create_document(result, metadata={
372
+ "display_name": f"Combined ({step})",
373
+ "bit_depth": "32-bit floating point",
374
+ "is_mono": (result.ndim == 2),
375
+ "source": f"Combine: {step}",
376
+ }, name=f"Combined ({step})")
377
+ self.mw._spawn_subwindow_for(newdoc)
378
+ try: self.mw._log(f"Image Combine → new view '{_doc_name(newdoc)}' ({step})")
379
+ except Exception as e:
380
+ import logging
381
+ logging.debug(f"Exception suppressed: {type(e).__name__}: {e}")
382
+
383
+ except Exception as e:
384
+ QMessageBox.critical(self, "Image Combine", f"Failed:\n{e}")
385
+
386
+ # ---------- zoom/pan ----------
387
+ def _apply_zoom(self):
388
+ if self._pix is None: return
389
+ scaled = self._pix.scaled(self._pix.size()*self.zoom, Qt.AspectRatioMode.KeepAspectRatio,
390
+ Qt.TransformationMode.SmoothTransformation)
391
+ self.lbl.setPixmap(scaled)
392
+
393
+ def _zoom_in(self): self.zoom *= 1.25; self._apply_zoom()
394
+ def _zoom_out(self): self.zoom /= 1.25; self._apply_zoom()
395
+ def _fit(self):
396
+ if self._pix is None: return
397
+ area = self.scroll.viewport().size(); pix = self._pix.size()
398
+ sx = area.width()/max(1,pix.width()); sy = area.height()/max(1,pix.height())
399
+ self.zoom = min(sx, sy, 1.0); self._apply_zoom()
400
+
401
+ def eventFilter(self, src, ev):
402
+ if src is self.scroll.viewport():
403
+ if ev.type() == QEvent.Type.MouseButtonPress and ev.button() == Qt.MouseButton.LeftButton:
404
+ self._pan_origin = ev.pos()
405
+ self._hstart = self.scroll.horizontalScrollBar().value()
406
+ self._vstart = self.scroll.verticalScrollBar().value()
407
+ return True
408
+ if ev.type() == QEvent.Type.MouseMove and self._pan_origin is not None:
409
+ d = ev.pos() - self._pan_origin
410
+ self.scroll.horizontalScrollBar().setValue(self._hstart - d.x())
411
+ self.scroll.verticalScrollBar().setValue(self._vstart - d.y())
412
+ return True
413
+ if ev.type() == QEvent.Type.MouseButtonRelease and ev.button() == Qt.MouseButton.LeftButton:
414
+ self._pan_origin = None; return True
415
+ return False
416
+ return super().eventFilter(src, ev)
417
+