setiastrosuitepro 1.6.1__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 (342) 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/HRDiagram.png +0 -0
  16. setiastro/images/LExtract.png +0 -0
  17. setiastro/images/LInsert.png +0 -0
  18. setiastro/images/Oxygenation-atm-2.svg.png +0 -0
  19. setiastro/images/RGB080604.png +0 -0
  20. setiastro/images/abeicon.png +0 -0
  21. setiastro/images/aberration.png +0 -0
  22. setiastro/images/andromedatry.png +0 -0
  23. setiastro/images/andromedatry_satellited.png +0 -0
  24. setiastro/images/annotated.png +0 -0
  25. setiastro/images/aperture.png +0 -0
  26. setiastro/images/astrosuite.ico +0 -0
  27. setiastro/images/astrosuite.png +0 -0
  28. setiastro/images/astrosuitepro.icns +0 -0
  29. setiastro/images/astrosuitepro.ico +0 -0
  30. setiastro/images/astrosuitepro.png +0 -0
  31. setiastro/images/background.png +0 -0
  32. setiastro/images/background2.png +0 -0
  33. setiastro/images/benchmark.png +0 -0
  34. setiastro/images/big_moon_stabilizer_timeline.png +0 -0
  35. setiastro/images/big_moon_stabilizer_timeline_clean.png +0 -0
  36. setiastro/images/blaster.png +0 -0
  37. setiastro/images/blink.png +0 -0
  38. setiastro/images/clahe.png +0 -0
  39. setiastro/images/collage.png +0 -0
  40. setiastro/images/colorwheel.png +0 -0
  41. setiastro/images/contsub.png +0 -0
  42. setiastro/images/convo.png +0 -0
  43. setiastro/images/copyslot.png +0 -0
  44. setiastro/images/cosmic.png +0 -0
  45. setiastro/images/cosmicsat.png +0 -0
  46. setiastro/images/crop1.png +0 -0
  47. setiastro/images/cropicon.png +0 -0
  48. setiastro/images/curves.png +0 -0
  49. setiastro/images/cvs.png +0 -0
  50. setiastro/images/debayer.png +0 -0
  51. setiastro/images/denoise_cnn_custom.png +0 -0
  52. setiastro/images/denoise_cnn_graph.png +0 -0
  53. setiastro/images/disk.png +0 -0
  54. setiastro/images/dse.png +0 -0
  55. setiastro/images/exoicon.png +0 -0
  56. setiastro/images/eye.png +0 -0
  57. setiastro/images/fliphorizontal.png +0 -0
  58. setiastro/images/flipvertical.png +0 -0
  59. setiastro/images/font.png +0 -0
  60. setiastro/images/freqsep.png +0 -0
  61. setiastro/images/functionbundle.png +0 -0
  62. setiastro/images/graxpert.png +0 -0
  63. setiastro/images/green.png +0 -0
  64. setiastro/images/gridicon.png +0 -0
  65. setiastro/images/halo.png +0 -0
  66. setiastro/images/hdr.png +0 -0
  67. setiastro/images/histogram.png +0 -0
  68. setiastro/images/hubble.png +0 -0
  69. setiastro/images/imagecombine.png +0 -0
  70. setiastro/images/invert.png +0 -0
  71. setiastro/images/isophote.png +0 -0
  72. setiastro/images/isophote_demo_figure.png +0 -0
  73. setiastro/images/isophote_demo_image.png +0 -0
  74. setiastro/images/isophote_demo_model.png +0 -0
  75. setiastro/images/isophote_demo_residual.png +0 -0
  76. setiastro/images/jwstpupil.png +0 -0
  77. setiastro/images/linearfit.png +0 -0
  78. setiastro/images/livestacking.png +0 -0
  79. setiastro/images/mask.png +0 -0
  80. setiastro/images/maskapply.png +0 -0
  81. setiastro/images/maskcreate.png +0 -0
  82. setiastro/images/maskremove.png +0 -0
  83. setiastro/images/morpho.png +0 -0
  84. setiastro/images/mosaic.png +0 -0
  85. setiastro/images/multiscale_decomp.png +0 -0
  86. setiastro/images/nbtorgb.png +0 -0
  87. setiastro/images/neutral.png +0 -0
  88. setiastro/images/nuke.png +0 -0
  89. setiastro/images/openfile.png +0 -0
  90. setiastro/images/pedestal.png +0 -0
  91. setiastro/images/pen.png +0 -0
  92. setiastro/images/pixelmath.png +0 -0
  93. setiastro/images/platesolve.png +0 -0
  94. setiastro/images/ppp.png +0 -0
  95. setiastro/images/pro.png +0 -0
  96. setiastro/images/project.png +0 -0
  97. setiastro/images/psf.png +0 -0
  98. setiastro/images/redo.png +0 -0
  99. setiastro/images/redoicon.png +0 -0
  100. setiastro/images/rescale.png +0 -0
  101. setiastro/images/rgbalign.png +0 -0
  102. setiastro/images/rgbcombo.png +0 -0
  103. setiastro/images/rgbextract.png +0 -0
  104. setiastro/images/rotate180.png +0 -0
  105. setiastro/images/rotateclockwise.png +0 -0
  106. setiastro/images/rotatecounterclockwise.png +0 -0
  107. setiastro/images/satellite.png +0 -0
  108. setiastro/images/script.png +0 -0
  109. setiastro/images/selectivecolor.png +0 -0
  110. setiastro/images/simbad.png +0 -0
  111. setiastro/images/slot0.png +0 -0
  112. setiastro/images/slot1.png +0 -0
  113. setiastro/images/slot2.png +0 -0
  114. setiastro/images/slot3.png +0 -0
  115. setiastro/images/slot4.png +0 -0
  116. setiastro/images/slot5.png +0 -0
  117. setiastro/images/slot6.png +0 -0
  118. setiastro/images/slot7.png +0 -0
  119. setiastro/images/slot8.png +0 -0
  120. setiastro/images/slot9.png +0 -0
  121. setiastro/images/spcc.png +0 -0
  122. setiastro/images/spin_precession_vs_lunar_distance.png +0 -0
  123. setiastro/images/spinner.gif +0 -0
  124. setiastro/images/stacking.png +0 -0
  125. setiastro/images/staradd.png +0 -0
  126. setiastro/images/staralign.png +0 -0
  127. setiastro/images/starnet.png +0 -0
  128. setiastro/images/starregistration.png +0 -0
  129. setiastro/images/starspike.png +0 -0
  130. setiastro/images/starstretch.png +0 -0
  131. setiastro/images/statstretch.png +0 -0
  132. setiastro/images/supernova.png +0 -0
  133. setiastro/images/uhs.png +0 -0
  134. setiastro/images/undoicon.png +0 -0
  135. setiastro/images/upscale.png +0 -0
  136. setiastro/images/viewbundle.png +0 -0
  137. setiastro/images/whitebalance.png +0 -0
  138. setiastro/images/wimi_icon_256x256.png +0 -0
  139. setiastro/images/wimilogo.png +0 -0
  140. setiastro/images/wims.png +0 -0
  141. setiastro/images/wrench_icon.png +0 -0
  142. setiastro/images/xisfliberator.png +0 -0
  143. setiastro/saspro/__init__.py +20 -0
  144. setiastro/saspro/__main__.py +809 -0
  145. setiastro/saspro/_generated/__init__.py +7 -0
  146. setiastro/saspro/_generated/build_info.py +2 -0
  147. setiastro/saspro/abe.py +1295 -0
  148. setiastro/saspro/abe_preset.py +196 -0
  149. setiastro/saspro/aberration_ai.py +694 -0
  150. setiastro/saspro/aberration_ai_preset.py +224 -0
  151. setiastro/saspro/accel_installer.py +218 -0
  152. setiastro/saspro/accel_workers.py +30 -0
  153. setiastro/saspro/add_stars.py +621 -0
  154. setiastro/saspro/astrobin_exporter.py +1007 -0
  155. setiastro/saspro/astrospike.py +153 -0
  156. setiastro/saspro/astrospike_python.py +1839 -0
  157. setiastro/saspro/autostretch.py +196 -0
  158. setiastro/saspro/backgroundneutral.py +560 -0
  159. setiastro/saspro/batch_convert.py +325 -0
  160. setiastro/saspro/batch_renamer.py +519 -0
  161. setiastro/saspro/blemish_blaster.py +488 -0
  162. setiastro/saspro/blink_comparator_pro.py +2926 -0
  163. setiastro/saspro/bundles.py +61 -0
  164. setiastro/saspro/bundles_dock.py +114 -0
  165. setiastro/saspro/cheat_sheet.py +178 -0
  166. setiastro/saspro/clahe.py +342 -0
  167. setiastro/saspro/comet_stacking.py +1377 -0
  168. setiastro/saspro/common_tr.py +107 -0
  169. setiastro/saspro/config.py +38 -0
  170. setiastro/saspro/config_bootstrap.py +40 -0
  171. setiastro/saspro/config_manager.py +316 -0
  172. setiastro/saspro/continuum_subtract.py +1617 -0
  173. setiastro/saspro/convo.py +1397 -0
  174. setiastro/saspro/convo_preset.py +414 -0
  175. setiastro/saspro/copyastro.py +187 -0
  176. setiastro/saspro/cosmicclarity.py +1564 -0
  177. setiastro/saspro/cosmicclarity_preset.py +407 -0
  178. setiastro/saspro/crop_dialog_pro.py +956 -0
  179. setiastro/saspro/crop_preset.py +189 -0
  180. setiastro/saspro/curve_editor_pro.py +2544 -0
  181. setiastro/saspro/curves_preset.py +375 -0
  182. setiastro/saspro/debayer.py +670 -0
  183. setiastro/saspro/debug_utils.py +29 -0
  184. setiastro/saspro/dnd_mime.py +35 -0
  185. setiastro/saspro/doc_manager.py +2641 -0
  186. setiastro/saspro/exoplanet_detector.py +2166 -0
  187. setiastro/saspro/file_utils.py +284 -0
  188. setiastro/saspro/fitsmodifier.py +745 -0
  189. setiastro/saspro/fix_bom.py +32 -0
  190. setiastro/saspro/free_torch_memory.py +48 -0
  191. setiastro/saspro/frequency_separation.py +1343 -0
  192. setiastro/saspro/function_bundle.py +1594 -0
  193. setiastro/saspro/generate_translations.py +2378 -0
  194. setiastro/saspro/ghs_dialog_pro.py +660 -0
  195. setiastro/saspro/ghs_preset.py +284 -0
  196. setiastro/saspro/graxpert.py +634 -0
  197. setiastro/saspro/graxpert_preset.py +287 -0
  198. setiastro/saspro/gui/__init__.py +0 -0
  199. setiastro/saspro/gui/main_window.py +8567 -0
  200. setiastro/saspro/gui/mixins/__init__.py +33 -0
  201. setiastro/saspro/gui/mixins/dock_mixin.py +263 -0
  202. setiastro/saspro/gui/mixins/file_mixin.py +443 -0
  203. setiastro/saspro/gui/mixins/geometry_mixin.py +403 -0
  204. setiastro/saspro/gui/mixins/header_mixin.py +441 -0
  205. setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
  206. setiastro/saspro/gui/mixins/menu_mixin.py +361 -0
  207. setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
  208. setiastro/saspro/gui/mixins/toolbar_mixin.py +1457 -0
  209. setiastro/saspro/gui/mixins/update_mixin.py +309 -0
  210. setiastro/saspro/gui/mixins/view_mixin.py +435 -0
  211. setiastro/saspro/halobgon.py +462 -0
  212. setiastro/saspro/header_viewer.py +448 -0
  213. setiastro/saspro/headless_utils.py +88 -0
  214. setiastro/saspro/histogram.py +753 -0
  215. setiastro/saspro/history_explorer.py +939 -0
  216. setiastro/saspro/i18n.py +156 -0
  217. setiastro/saspro/image_combine.py +414 -0
  218. setiastro/saspro/image_peeker_pro.py +1601 -0
  219. setiastro/saspro/imageops/__init__.py +37 -0
  220. setiastro/saspro/imageops/mdi_snap.py +292 -0
  221. setiastro/saspro/imageops/scnr.py +36 -0
  222. setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
  223. setiastro/saspro/imageops/stretch.py +244 -0
  224. setiastro/saspro/isophote.py +1179 -0
  225. setiastro/saspro/layers.py +208 -0
  226. setiastro/saspro/layers_dock.py +714 -0
  227. setiastro/saspro/lazy_imports.py +193 -0
  228. setiastro/saspro/legacy/__init__.py +2 -0
  229. setiastro/saspro/legacy/image_manager.py +2226 -0
  230. setiastro/saspro/legacy/numba_utils.py +3659 -0
  231. setiastro/saspro/legacy/xisf.py +1071 -0
  232. setiastro/saspro/linear_fit.py +534 -0
  233. setiastro/saspro/live_stacking.py +1830 -0
  234. setiastro/saspro/log_bus.py +5 -0
  235. setiastro/saspro/logging_config.py +460 -0
  236. setiastro/saspro/luminancerecombine.py +309 -0
  237. setiastro/saspro/main_helpers.py +201 -0
  238. setiastro/saspro/mask_creation.py +928 -0
  239. setiastro/saspro/masks_core.py +56 -0
  240. setiastro/saspro/mdi_widgets.py +353 -0
  241. setiastro/saspro/memory_utils.py +666 -0
  242. setiastro/saspro/metadata_patcher.py +75 -0
  243. setiastro/saspro/mfdeconv.py +3826 -0
  244. setiastro/saspro/mfdeconv_earlystop.py +71 -0
  245. setiastro/saspro/mfdeconvcudnn.py +3263 -0
  246. setiastro/saspro/mfdeconvsport.py +2382 -0
  247. setiastro/saspro/minorbodycatalog.py +567 -0
  248. setiastro/saspro/morphology.py +382 -0
  249. setiastro/saspro/multiscale_decomp.py +1290 -0
  250. setiastro/saspro/nbtorgb_stars.py +531 -0
  251. setiastro/saspro/numba_utils.py +3044 -0
  252. setiastro/saspro/numba_warmup.py +141 -0
  253. setiastro/saspro/ops/__init__.py +9 -0
  254. setiastro/saspro/ops/command_help_dialog.py +623 -0
  255. setiastro/saspro/ops/command_runner.py +217 -0
  256. setiastro/saspro/ops/commands.py +1594 -0
  257. setiastro/saspro/ops/script_editor.py +1102 -0
  258. setiastro/saspro/ops/scripts.py +1413 -0
  259. setiastro/saspro/ops/settings.py +679 -0
  260. setiastro/saspro/parallel_utils.py +554 -0
  261. setiastro/saspro/pedestal.py +121 -0
  262. setiastro/saspro/perfect_palette_picker.py +1070 -0
  263. setiastro/saspro/pipeline.py +110 -0
  264. setiastro/saspro/pixelmath.py +1600 -0
  265. setiastro/saspro/plate_solver.py +2444 -0
  266. setiastro/saspro/project_io.py +797 -0
  267. setiastro/saspro/psf_utils.py +136 -0
  268. setiastro/saspro/psf_viewer.py +549 -0
  269. setiastro/saspro/pyi_rthook_astroquery.py +95 -0
  270. setiastro/saspro/remove_green.py +314 -0
  271. setiastro/saspro/remove_stars.py +1625 -0
  272. setiastro/saspro/remove_stars_preset.py +404 -0
  273. setiastro/saspro/resources.py +477 -0
  274. setiastro/saspro/rgb_combination.py +207 -0
  275. setiastro/saspro/rgb_extract.py +19 -0
  276. setiastro/saspro/rgbalign.py +723 -0
  277. setiastro/saspro/runtime_imports.py +7 -0
  278. setiastro/saspro/runtime_torch.py +754 -0
  279. setiastro/saspro/save_options.py +72 -0
  280. setiastro/saspro/selective_color.py +1552 -0
  281. setiastro/saspro/sfcc.py +1430 -0
  282. setiastro/saspro/shortcuts.py +3043 -0
  283. setiastro/saspro/signature_insert.py +1099 -0
  284. setiastro/saspro/stacking_suite.py +18181 -0
  285. setiastro/saspro/star_alignment.py +7420 -0
  286. setiastro/saspro/star_alignment_preset.py +329 -0
  287. setiastro/saspro/star_metrics.py +49 -0
  288. setiastro/saspro/star_spikes.py +681 -0
  289. setiastro/saspro/star_stretch.py +470 -0
  290. setiastro/saspro/stat_stretch.py +506 -0
  291. setiastro/saspro/status_log_dock.py +78 -0
  292. setiastro/saspro/subwindow.py +3267 -0
  293. setiastro/saspro/supernovaasteroidhunter.py +1716 -0
  294. setiastro/saspro/swap_manager.py +99 -0
  295. setiastro/saspro/torch_backend.py +89 -0
  296. setiastro/saspro/torch_rejection.py +434 -0
  297. setiastro/saspro/translations/de_translations.py +3733 -0
  298. setiastro/saspro/translations/es_translations.py +3923 -0
  299. setiastro/saspro/translations/fr_translations.py +3842 -0
  300. setiastro/saspro/translations/integrate_translations.py +234 -0
  301. setiastro/saspro/translations/it_translations.py +3662 -0
  302. setiastro/saspro/translations/ja_translations.py +3585 -0
  303. setiastro/saspro/translations/pt_translations.py +3853 -0
  304. setiastro/saspro/translations/saspro_de.qm +0 -0
  305. setiastro/saspro/translations/saspro_de.ts +253 -0
  306. setiastro/saspro/translations/saspro_es.qm +0 -0
  307. setiastro/saspro/translations/saspro_es.ts +12520 -0
  308. setiastro/saspro/translations/saspro_fr.qm +0 -0
  309. setiastro/saspro/translations/saspro_fr.ts +12514 -0
  310. setiastro/saspro/translations/saspro_it.qm +0 -0
  311. setiastro/saspro/translations/saspro_it.ts +12520 -0
  312. setiastro/saspro/translations/saspro_ja.qm +0 -0
  313. setiastro/saspro/translations/saspro_ja.ts +257 -0
  314. setiastro/saspro/translations/saspro_pt.qm +0 -0
  315. setiastro/saspro/translations/saspro_pt.ts +257 -0
  316. setiastro/saspro/translations/saspro_zh.qm +0 -0
  317. setiastro/saspro/translations/saspro_zh.ts +12520 -0
  318. setiastro/saspro/translations/zh_translations.py +3659 -0
  319. setiastro/saspro/versioning.py +71 -0
  320. setiastro/saspro/view_bundle.py +1555 -0
  321. setiastro/saspro/wavescale_hdr.py +624 -0
  322. setiastro/saspro/wavescale_hdr_preset.py +101 -0
  323. setiastro/saspro/wavescalede.py +658 -0
  324. setiastro/saspro/wavescalede_preset.py +230 -0
  325. setiastro/saspro/wcs_update.py +374 -0
  326. setiastro/saspro/whitebalance.py +456 -0
  327. setiastro/saspro/widgets/__init__.py +48 -0
  328. setiastro/saspro/widgets/common_utilities.py +306 -0
  329. setiastro/saspro/widgets/graphics_views.py +122 -0
  330. setiastro/saspro/widgets/image_utils.py +518 -0
  331. setiastro/saspro/widgets/preview_dialogs.py +280 -0
  332. setiastro/saspro/widgets/spinboxes.py +275 -0
  333. setiastro/saspro/widgets/themed_buttons.py +13 -0
  334. setiastro/saspro/widgets/wavelet_utils.py +299 -0
  335. setiastro/saspro/window_shelf.py +185 -0
  336. setiastro/saspro/xisf.py +1123 -0
  337. setiastrosuitepro-1.6.1.dist-info/METADATA +267 -0
  338. setiastrosuitepro-1.6.1.dist-info/RECORD +342 -0
  339. setiastrosuitepro-1.6.1.dist-info/WHEEL +4 -0
  340. setiastrosuitepro-1.6.1.dist-info/entry_points.txt +6 -0
  341. setiastrosuitepro-1.6.1.dist-info/licenses/LICENSE +674 -0
  342. setiastrosuitepro-1.6.1.dist-info/licenses/license.txt +2580 -0
@@ -0,0 +1,435 @@
1
+ # pro/gui/mixins/view_mixin.py
2
+ """
3
+ View management mixin for AstroSuiteProMainWindow.
4
+
5
+ This mixin contains all view-related functionality: tiling, cascading,
6
+ zooming, autostretch, and view layout management.
7
+ """
8
+ from __future__ import annotations
9
+ import math
10
+ from typing import TYPE_CHECKING
11
+
12
+ from PyQt6.QtCore import Qt
13
+ from PyQt6.QtGui import QIcon
14
+
15
+ if TYPE_CHECKING:
16
+ pass
17
+
18
+
19
+ class ViewMixin:
20
+ """
21
+ Mixin for view management.
22
+
23
+ Provides methods for arranging, zooming, and managing MDI subwindows.
24
+ """
25
+
26
+ def _auto_fit_all_subwindows(self):
27
+ """Apply auto-fit to every visible subwindow when the mode is enabled."""
28
+ if not getattr(self, "_auto_fit_on_resize", False):
29
+ return
30
+
31
+ subs = self._visible_subwindows()
32
+ if not subs:
33
+ return
34
+
35
+ # Remember current active so we can restore it
36
+ prev_active = self.mdi.activeSubWindow()
37
+
38
+ for sw in subs:
39
+ # Make this subwindow active so _zoom_active_fit() works on it
40
+ self.mdi.setActiveSubWindow(sw)
41
+ self._zoom_active_fit()
42
+
43
+ # Restore previously active subwindow if still around
44
+ if prev_active and prev_active in subs:
45
+ self.mdi.setActiveSubWindow(prev_active)
46
+
47
+ def _visible_subwindows(self):
48
+ """Return list of visible, non-minimized subwindows."""
49
+ subs = [sw for sw in self.mdi.subWindowList()
50
+ if sw.isVisible() and not (sw.windowState() & Qt.WindowState.WindowMinimized)]
51
+ return subs
52
+
53
+
54
+ def _tile_views(self):
55
+ """Tile all subwindows."""
56
+ self.mdi.tileSubWindows()
57
+ self._auto_fit_all_subwindows()
58
+
59
+ def _tile_views_direction(self, direction: str):
60
+ """
61
+ Tile views in a specific direction.
62
+
63
+ Args:
64
+ direction: 'v' for vertical columns, 'h' for horizontal rows
65
+ """
66
+ subs = self._visible_subwindows()
67
+ if not subs:
68
+ return
69
+ area = self.mdi.viewport().rect()
70
+ # account for MDI viewport origin in global coords
71
+ off = self.mdi.viewport().mapTo(self.mdi, area.topLeft())
72
+ origin_x, origin_y = off.x(), off.y()
73
+
74
+ n = len(subs)
75
+ if direction == "v": # columns
76
+ col_w = max(1, area.width() // n)
77
+ for i, sw in enumerate(subs):
78
+ sw.setGeometry(origin_x + i*col_w, origin_y, col_w, area.height())
79
+ else: # rows
80
+ row_h = max(1, area.height() // n)
81
+ for i, sw in enumerate(subs):
82
+ sw.setGeometry(origin_x, origin_y + i*row_h, area.width(), row_h)
83
+
84
+ self._auto_fit_all_subwindows()
85
+
86
+ def _tile_views_grid(self):
87
+ """Arrange subwindows in a near-square grid across the MDI area."""
88
+ subs = self._visible_subwindows()
89
+ if not subs:
90
+ return
91
+ area = self.mdi.viewport().rect()
92
+ off = self.mdi.viewport().mapTo(self.mdi, area.topLeft())
93
+ origin_x, origin_y = off.x(), off.y()
94
+
95
+ n = len(subs)
96
+ # rows x cols ~ square
97
+ cols = int(max(1, math.ceil(math.sqrt(n))))
98
+ rows = int(max(1, math.ceil(n / cols)))
99
+
100
+ cell_w = max(1, area.width() // cols)
101
+ cell_h = max(1, area.height() // rows)
102
+
103
+ for idx, sw in enumerate(subs):
104
+ r = idx // cols
105
+ c = idx % cols
106
+ sw.setGeometry(origin_x + c*cell_w, origin_y + r*cell_h, cell_w, cell_h)
107
+
108
+ self._auto_fit_all_subwindows()
109
+
110
+ def _zoom_step_active(self, direction: int):
111
+ """
112
+ Zoom the active view in or out by a fixed factor.
113
+
114
+ Args:
115
+ direction: > 0 for zoom in, < 0 for zoom out
116
+ """
117
+ sw = self.mdi.activeSubWindow()
118
+ if not sw:
119
+ return
120
+
121
+ view = sw.widget()
122
+ try:
123
+ cur_scale = float(getattr(view, "scale", 1.0))
124
+ except Exception:
125
+ cur_scale = 1.0
126
+
127
+ # Reasonable step factor
128
+ step = 1.25
129
+ factor = step if direction > 0 else 1.0 / step
130
+
131
+ new_scale = cur_scale * factor
132
+ # Clamp to sane bounds
133
+ new_scale = max(1e-4, min(32.0, new_scale))
134
+
135
+ # Manual zoom -> we are no longer in a "perfect fit" state
136
+ try:
137
+ self.act_zoom_fit.setChecked(False)
138
+ except Exception:
139
+ pass
140
+
141
+ # Prefer anchor-based zoom so we keep the current scroll-center stable
142
+ if hasattr(view, "_zoom_at_anchor") and callable(view._zoom_at_anchor):
143
+ try:
144
+ rel = float(new_scale) / max(cur_scale, 1e-12)
145
+ view._zoom_at_anchor(rel)
146
+ return
147
+ except Exception:
148
+ pass
149
+
150
+ # Fallback: absolute set_scale without forcing recentering
151
+ if hasattr(view, "set_scale") and callable(view.set_scale):
152
+ try:
153
+ view.set_scale(float(new_scale))
154
+ return
155
+ except Exception:
156
+ pass
157
+
158
+ def _zoom_active_1_1(self):
159
+ """Zoom active view to 100% (1:1 pixel scale)."""
160
+ sw = self.mdi.activeSubWindow()
161
+ if not sw:
162
+ return
163
+ view = sw.widget()
164
+ if hasattr(view, "set_scale") and callable(view.set_scale):
165
+ try:
166
+ view.set_scale(1.0)
167
+ except Exception:
168
+ pass
169
+
170
+ def _zoom_active_fit(self):
171
+ """Fit the active view's image to its viewport."""
172
+ sw = self.mdi.activeSubWindow()
173
+ if not sw:
174
+ return
175
+ view = sw.widget()
176
+ self._zoom_active_1_1()
177
+
178
+ # Get sizes
179
+ img_w, img_h = self._infer_image_size(view)
180
+ if not img_w or not img_h:
181
+ return
182
+
183
+ vp = self._viewport_widget(view)
184
+ vw, vh = max(1, vp.width()), max(1, vp.height())
185
+
186
+ # Compute uniform scale (minus a hair to avoid scrollbars fighting)
187
+ scale = min((vw - 2) / img_w, (vh - 2) / img_h)
188
+ # Clamp to sane bounds
189
+ scale = max(1e-4, min(32.0, scale))
190
+ self._sync_fit_auto_visual()
191
+
192
+ # Apply using view API if available
193
+ if hasattr(view, "set_scale") and callable(view.set_scale):
194
+ try:
195
+ view.set_scale(float(scale))
196
+ self._center_view(view)
197
+ return
198
+ except Exception:
199
+ pass
200
+
201
+ # Fallback: relative zoom using _zoom_at_anchor
202
+ try:
203
+ cur = float(getattr(view, "scale", 1.0))
204
+ factor = scale / max(cur, 1e-12)
205
+ if hasattr(view, "_zoom_at_anchor") and callable(view._zoom_at_anchor):
206
+ view._zoom_at_anchor(float(factor))
207
+ self._center_view(view)
208
+ return
209
+ except Exception:
210
+ pass
211
+
212
+ def _infer_image_size(self, view):
213
+ """Return (img_w, img_h) in device-independent pixels (ints), best-effort."""
214
+ # Preferred: from the label's pixmap
215
+ try:
216
+ pm = getattr(view, "label", None).pixmap() if hasattr(view, "label") else None
217
+ if pm and not pm.isNull():
218
+ dpr = max(1.0, float(pm.devicePixelRatio()))
219
+ return int(round(pm.width() / dpr)), int(round(pm.height() / dpr))
220
+ except Exception:
221
+ pass
222
+
223
+ # Next: from the document image
224
+ try:
225
+ doc = getattr(view, "document", None)
226
+ if doc and getattr(doc, "image", None) is not None:
227
+ import numpy as np
228
+ h, w = np.asarray(doc.image).shape[:2]
229
+ return int(w), int(h)
230
+ except Exception:
231
+ pass
232
+
233
+ # Fallback: from attributes some views keep
234
+ for w_key, h_key in (("image_width", "image_height"), ("_img_w", "_img_h")):
235
+ w = getattr(view, w_key, None)
236
+ h = getattr(view, h_key, None)
237
+ if isinstance(w, (int, float)) and isinstance(h, (int, float)) and w > 0 and h > 0:
238
+ return int(w), int(h)
239
+
240
+ return None, None
241
+
242
+ def _viewport_widget(self, view):
243
+ """Return the viewport widget used to display the image."""
244
+ try:
245
+ if hasattr(view, "scroll") and hasattr(view.scroll, "viewport"):
246
+ return view.scroll.viewport()
247
+ # Some views are QGraphicsView/QAbstractScrollArea-like
248
+ if hasattr(view, "viewport"):
249
+ return view.viewport()
250
+ except Exception:
251
+ pass
252
+ # Worst case: the view itself
253
+ return view
254
+
255
+ def _center_view(self, view):
256
+ """Center the content after a zoom change, if possible."""
257
+ try:
258
+ vp = self._viewport_widget(view)
259
+ hbar = view.scroll.horizontalScrollBar() if hasattr(view, "scroll") else None
260
+ vbar = view.scroll.verticalScrollBar() if hasattr(view, "scroll") else None
261
+ lbl = getattr(view, "label", None)
262
+ if vp and hbar and vbar and lbl:
263
+ cx = max(0, lbl.width() // 2 - vp.width() // 2)
264
+ cy = max(0, lbl.height() // 2 - vp.height() // 2)
265
+ hbar.setValue(min(hbar.maximum(), cx))
266
+ vbar.setValue(min(vbar.maximum(), cy))
267
+ except Exception:
268
+ pass
269
+
270
+ def _sync_fit_auto_visual(self):
271
+ """Sync the Fit button's checked state with auto-fit mode."""
272
+ on = bool(getattr(self, "_auto_fit_on_resize", False))
273
+ if hasattr(self, "act_zoom_fit"):
274
+ self.act_zoom_fit.blockSignals(True)
275
+ try:
276
+ self.act_zoom_fit.setChecked(on)
277
+ finally:
278
+ self.act_zoom_fit.blockSignals(False)
279
+
280
+ def _toggle_auto_fit_on_resize(self, checked: bool):
281
+ """Toggle auto-fit on resize mode."""
282
+ self._auto_fit_on_resize = bool(checked)
283
+ self.settings.setValue("view/auto_fit_on_resize", self._auto_fit_on_resize)
284
+ self._sync_fit_auto_visual()
285
+ if checked:
286
+ self._zoom_active_fit()
287
+
288
+ def _on_view_resized(self):
289
+ """Called whenever an ImageSubWindow emits resized(). Debounced."""
290
+ if not getattr(self, "_auto_fit_on_resize", False):
291
+ return
292
+ if hasattr(self, "_auto_fit_timer") and self._auto_fit_timer is not None:
293
+ if self._auto_fit_timer.isActive():
294
+ self._auto_fit_timer.stop()
295
+ self._auto_fit_timer.start()
296
+
297
+ def _apply_auto_fit_resize(self):
298
+ """Run the actual Fit after the resize settles."""
299
+ if not getattr(self, "_auto_fit_on_resize", False):
300
+ return
301
+ self._zoom_active_fit()
302
+
303
+ def _toggle_autostretch(self, on: bool):
304
+ """Toggle autostretch for the active view."""
305
+ sw = self.mdi.activeSubWindow()
306
+ if sw:
307
+ sw.widget().set_autostretch(on)
308
+ self._log(f"Display-Stretch {'ON' if on else 'OFF'} -> {sw.windowTitle()}")
309
+
310
+ def _set_hard_autostretch_from_action(self, checked: bool):
311
+ """Set hard autostretch profile from toolbar action."""
312
+ from PyQt6.QtCore import QSignalBlocker
313
+
314
+ sw = self.mdi.activeSubWindow()
315
+ if not sw:
316
+ return
317
+ view = sw.widget()
318
+
319
+ # mirror the action's check to the view profile
320
+ if hasattr(view, "set_autostretch_profile"):
321
+ view.set_autostretch_profile("hard" if checked else "normal")
322
+
323
+ # ensure it's visible
324
+ if not getattr(view, "autostretch_enabled", False):
325
+ view.set_autostretch(True)
326
+ self._sync_autostretch_action(True)
327
+
328
+ self._log(f"Display-Stretch profile -> {'HARD' if checked else 'NORMAL'} ({sw.windowTitle()})")
329
+
330
+ def _toggle_hard_autostretch(self):
331
+ """Toggle between hard and normal autostretch profiles."""
332
+ from PyQt6.QtCore import QSignalBlocker
333
+
334
+ sw = self.mdi.activeSubWindow()
335
+ if not sw:
336
+ return
337
+ view = sw.widget()
338
+
339
+ # flip profile
340
+ new_profile = "hard" if not getattr(view, "is_hard_autostretch", lambda: False)() else "normal"
341
+ if hasattr(view, "set_autostretch_profile"):
342
+ view.set_autostretch_profile(new_profile)
343
+
344
+ # ensure autostretch is ON so the change is visible immediately
345
+ if not getattr(view, "autostretch_enabled", False):
346
+ view.set_autostretch(True)
347
+ self._sync_autostretch_action(True)
348
+
349
+ # reflect in toolbar button
350
+ with QSignalBlocker(self.act_hardstretch):
351
+ self.act_hardstretch.setChecked(new_profile == "hard")
352
+
353
+ self._log(f"Display-Stretch profile -> {new_profile.upper()} ({sw.windowTitle()})")
354
+
355
+ def _sync_autostretch_action(self, on: bool):
356
+ """Sync the autostretch action's checked state."""
357
+ from PyQt6.QtCore import QSignalBlocker
358
+
359
+ if hasattr(self, "act_autostretch"):
360
+ block = QSignalBlocker(self.act_autostretch)
361
+ self.act_autostretch.setChecked(bool(on))
362
+
363
+ def _edit_display_target(self):
364
+ """Open dialog to edit display stretch target median."""
365
+ from PyQt6.QtWidgets import QInputDialog
366
+
367
+ cur = float(self.settings.value("display/target", 0.30, type=float))
368
+ val, ok = QInputDialog.getDouble(
369
+ self, "Target Median", "Target (0.01 - 0.90):", cur, 0.01, 0.90, 3
370
+ )
371
+ if not ok:
372
+ return
373
+ self.settings.setValue("display/target", float(val))
374
+ sw = self.mdi.activeSubWindow()
375
+ if not sw:
376
+ return
377
+ view = sw.widget()
378
+ if hasattr(view, "set_autostretch_target"):
379
+ view.set_autostretch_target(float(val))
380
+ if not getattr(view, "autostretch_enabled", False):
381
+ if hasattr(view, "set_autostretch"):
382
+ view.set_autostretch(True)
383
+ self._sync_autostretch_action(True)
384
+
385
+ def _edit_display_sigma(self):
386
+ """Open dialog to edit display stretch sigma."""
387
+ from PyQt6.QtWidgets import QInputDialog
388
+
389
+ cur = float(self.settings.value("display/sigma", 5.0, type=float))
390
+ val, ok = QInputDialog.getDouble(
391
+ self, "Sigma", "Sigma (0.5 - 10.0):", cur, 0.5, 10.0, 2
392
+ )
393
+ if not ok:
394
+ return
395
+ self.settings.setValue("display/sigma", float(val))
396
+ sw = self.mdi.activeSubWindow()
397
+ if not sw:
398
+ return
399
+ view = sw.widget()
400
+ if hasattr(view, "set_autostretch_sigma"):
401
+ view.set_autostretch_sigma(float(val))
402
+ if not getattr(view, "autostretch_enabled", False):
403
+ if hasattr(view, "set_autostretch"):
404
+ view.set_autostretch(True)
405
+ self._sync_autostretch_action(True)
406
+
407
+ def _copy_active_view(self):
408
+ """Copy the current view state (zoom/pan) for pasting to other views."""
409
+ sw = self.mdi.activeSubWindow()
410
+ if not sw:
411
+ return
412
+ view = sw.widget()
413
+ self._copied_view_state = {
414
+ "scale": getattr(view, "scale", 1.0),
415
+ "hbar": view.scroll.horizontalScrollBar().value() if hasattr(view, "scroll") else 0,
416
+ "vbar": view.scroll.verticalScrollBar().value() if hasattr(view, "scroll") else 0,
417
+ }
418
+ self._log("View state copied")
419
+
420
+ def _paste_active_view(self):
421
+ """Paste a previously copied view state to the active view."""
422
+ if not getattr(self, "_copied_view_state", None):
423
+ return
424
+ sw = self.mdi.activeSubWindow()
425
+ if not sw:
426
+ return
427
+ view = sw.widget()
428
+ state = self._copied_view_state
429
+
430
+ if hasattr(view, "set_scale"):
431
+ view.set_scale(state.get("scale", 1.0))
432
+ if hasattr(view, "scroll"):
433
+ view.scroll.horizontalScrollBar().setValue(state.get("hbar", 0))
434
+ view.scroll.verticalScrollBar().setValue(state.get("vbar", 0))
435
+ self._log("View state pasted")