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.
- setiastro/__init__.py +2 -0
- setiastro/data/SASP_data.fits +0 -0
- setiastro/data/catalogs/List_of_Galaxies_with_Distances_Gly.csv +488 -0
- setiastro/data/catalogs/astrobin_filters.csv +890 -0
- setiastro/data/catalogs/astrobin_filters_page1_local.csv +51 -0
- setiastro/data/catalogs/cali2.csv +63 -0
- setiastro/data/catalogs/cali2color.csv +65 -0
- setiastro/data/catalogs/celestial_catalog - original.csv +16471 -0
- setiastro/data/catalogs/celestial_catalog.csv +24031 -0
- setiastro/data/catalogs/detected_stars.csv +24784 -0
- setiastro/data/catalogs/fits_header_data.csv +46 -0
- setiastro/data/catalogs/test.csv +8 -0
- setiastro/data/catalogs/updated_celestial_catalog.csv +16471 -0
- setiastro/images/Astro_Spikes.png +0 -0
- setiastro/images/HRDiagram.png +0 -0
- setiastro/images/LExtract.png +0 -0
- setiastro/images/LInsert.png +0 -0
- setiastro/images/Oxygenation-atm-2.svg.png +0 -0
- setiastro/images/RGB080604.png +0 -0
- setiastro/images/abeicon.png +0 -0
- setiastro/images/aberration.png +0 -0
- setiastro/images/andromedatry.png +0 -0
- setiastro/images/andromedatry_satellited.png +0 -0
- setiastro/images/annotated.png +0 -0
- setiastro/images/aperture.png +0 -0
- setiastro/images/astrosuite.ico +0 -0
- setiastro/images/astrosuite.png +0 -0
- setiastro/images/astrosuitepro.icns +0 -0
- setiastro/images/astrosuitepro.ico +0 -0
- setiastro/images/astrosuitepro.png +0 -0
- setiastro/images/background.png +0 -0
- setiastro/images/background2.png +0 -0
- setiastro/images/benchmark.png +0 -0
- setiastro/images/big_moon_stabilizer_timeline.png +0 -0
- setiastro/images/big_moon_stabilizer_timeline_clean.png +0 -0
- setiastro/images/blaster.png +0 -0
- setiastro/images/blink.png +0 -0
- setiastro/images/clahe.png +0 -0
- setiastro/images/collage.png +0 -0
- setiastro/images/colorwheel.png +0 -0
- setiastro/images/contsub.png +0 -0
- setiastro/images/convo.png +0 -0
- setiastro/images/copyslot.png +0 -0
- setiastro/images/cosmic.png +0 -0
- setiastro/images/cosmicsat.png +0 -0
- setiastro/images/crop1.png +0 -0
- setiastro/images/cropicon.png +0 -0
- setiastro/images/curves.png +0 -0
- setiastro/images/cvs.png +0 -0
- setiastro/images/debayer.png +0 -0
- setiastro/images/denoise_cnn_custom.png +0 -0
- setiastro/images/denoise_cnn_graph.png +0 -0
- setiastro/images/disk.png +0 -0
- setiastro/images/dse.png +0 -0
- setiastro/images/exoicon.png +0 -0
- setiastro/images/eye.png +0 -0
- setiastro/images/fliphorizontal.png +0 -0
- setiastro/images/flipvertical.png +0 -0
- setiastro/images/font.png +0 -0
- setiastro/images/freqsep.png +0 -0
- setiastro/images/functionbundle.png +0 -0
- setiastro/images/graxpert.png +0 -0
- setiastro/images/green.png +0 -0
- setiastro/images/gridicon.png +0 -0
- setiastro/images/halo.png +0 -0
- setiastro/images/hdr.png +0 -0
- setiastro/images/histogram.png +0 -0
- setiastro/images/hubble.png +0 -0
- setiastro/images/imagecombine.png +0 -0
- setiastro/images/invert.png +0 -0
- setiastro/images/isophote.png +0 -0
- setiastro/images/isophote_demo_figure.png +0 -0
- setiastro/images/isophote_demo_image.png +0 -0
- setiastro/images/isophote_demo_model.png +0 -0
- setiastro/images/isophote_demo_residual.png +0 -0
- setiastro/images/jwstpupil.png +0 -0
- setiastro/images/linearfit.png +0 -0
- setiastro/images/livestacking.png +0 -0
- setiastro/images/mask.png +0 -0
- setiastro/images/maskapply.png +0 -0
- setiastro/images/maskcreate.png +0 -0
- setiastro/images/maskremove.png +0 -0
- setiastro/images/morpho.png +0 -0
- setiastro/images/mosaic.png +0 -0
- setiastro/images/multiscale_decomp.png +0 -0
- setiastro/images/nbtorgb.png +0 -0
- setiastro/images/neutral.png +0 -0
- setiastro/images/nuke.png +0 -0
- setiastro/images/openfile.png +0 -0
- setiastro/images/pedestal.png +0 -0
- setiastro/images/pen.png +0 -0
- setiastro/images/pixelmath.png +0 -0
- setiastro/images/platesolve.png +0 -0
- setiastro/images/ppp.png +0 -0
- setiastro/images/pro.png +0 -0
- setiastro/images/project.png +0 -0
- setiastro/images/psf.png +0 -0
- setiastro/images/redo.png +0 -0
- setiastro/images/redoicon.png +0 -0
- setiastro/images/rescale.png +0 -0
- setiastro/images/rgbalign.png +0 -0
- setiastro/images/rgbcombo.png +0 -0
- setiastro/images/rgbextract.png +0 -0
- setiastro/images/rotate180.png +0 -0
- setiastro/images/rotateclockwise.png +0 -0
- setiastro/images/rotatecounterclockwise.png +0 -0
- setiastro/images/satellite.png +0 -0
- setiastro/images/script.png +0 -0
- setiastro/images/selectivecolor.png +0 -0
- setiastro/images/simbad.png +0 -0
- setiastro/images/slot0.png +0 -0
- setiastro/images/slot1.png +0 -0
- setiastro/images/slot2.png +0 -0
- setiastro/images/slot3.png +0 -0
- setiastro/images/slot4.png +0 -0
- setiastro/images/slot5.png +0 -0
- setiastro/images/slot6.png +0 -0
- setiastro/images/slot7.png +0 -0
- setiastro/images/slot8.png +0 -0
- setiastro/images/slot9.png +0 -0
- setiastro/images/spcc.png +0 -0
- setiastro/images/spin_precession_vs_lunar_distance.png +0 -0
- setiastro/images/spinner.gif +0 -0
- setiastro/images/stacking.png +0 -0
- setiastro/images/staradd.png +0 -0
- setiastro/images/staralign.png +0 -0
- setiastro/images/starnet.png +0 -0
- setiastro/images/starregistration.png +0 -0
- setiastro/images/starspike.png +0 -0
- setiastro/images/starstretch.png +0 -0
- setiastro/images/statstretch.png +0 -0
- setiastro/images/supernova.png +0 -0
- setiastro/images/uhs.png +0 -0
- setiastro/images/undoicon.png +0 -0
- setiastro/images/upscale.png +0 -0
- setiastro/images/viewbundle.png +0 -0
- setiastro/images/whitebalance.png +0 -0
- setiastro/images/wimi_icon_256x256.png +0 -0
- setiastro/images/wimilogo.png +0 -0
- setiastro/images/wims.png +0 -0
- setiastro/images/wrench_icon.png +0 -0
- setiastro/images/xisfliberator.png +0 -0
- setiastro/saspro/__init__.py +20 -0
- setiastro/saspro/__main__.py +809 -0
- setiastro/saspro/_generated/__init__.py +7 -0
- setiastro/saspro/_generated/build_info.py +2 -0
- setiastro/saspro/abe.py +1295 -0
- setiastro/saspro/abe_preset.py +196 -0
- setiastro/saspro/aberration_ai.py +694 -0
- setiastro/saspro/aberration_ai_preset.py +224 -0
- setiastro/saspro/accel_installer.py +218 -0
- setiastro/saspro/accel_workers.py +30 -0
- setiastro/saspro/add_stars.py +621 -0
- setiastro/saspro/astrobin_exporter.py +1007 -0
- setiastro/saspro/astrospike.py +153 -0
- setiastro/saspro/astrospike_python.py +1839 -0
- setiastro/saspro/autostretch.py +196 -0
- setiastro/saspro/backgroundneutral.py +560 -0
- setiastro/saspro/batch_convert.py +325 -0
- setiastro/saspro/batch_renamer.py +519 -0
- setiastro/saspro/blemish_blaster.py +488 -0
- setiastro/saspro/blink_comparator_pro.py +2926 -0
- setiastro/saspro/bundles.py +61 -0
- setiastro/saspro/bundles_dock.py +114 -0
- setiastro/saspro/cheat_sheet.py +178 -0
- setiastro/saspro/clahe.py +342 -0
- setiastro/saspro/comet_stacking.py +1377 -0
- setiastro/saspro/common_tr.py +107 -0
- setiastro/saspro/config.py +38 -0
- setiastro/saspro/config_bootstrap.py +40 -0
- setiastro/saspro/config_manager.py +316 -0
- setiastro/saspro/continuum_subtract.py +1617 -0
- setiastro/saspro/convo.py +1397 -0
- setiastro/saspro/convo_preset.py +414 -0
- setiastro/saspro/copyastro.py +187 -0
- setiastro/saspro/cosmicclarity.py +1564 -0
- setiastro/saspro/cosmicclarity_preset.py +407 -0
- setiastro/saspro/crop_dialog_pro.py +956 -0
- setiastro/saspro/crop_preset.py +189 -0
- setiastro/saspro/curve_editor_pro.py +2544 -0
- setiastro/saspro/curves_preset.py +375 -0
- setiastro/saspro/debayer.py +670 -0
- setiastro/saspro/debug_utils.py +29 -0
- setiastro/saspro/dnd_mime.py +35 -0
- setiastro/saspro/doc_manager.py +2641 -0
- setiastro/saspro/exoplanet_detector.py +2166 -0
- setiastro/saspro/file_utils.py +284 -0
- setiastro/saspro/fitsmodifier.py +745 -0
- setiastro/saspro/fix_bom.py +32 -0
- setiastro/saspro/free_torch_memory.py +48 -0
- setiastro/saspro/frequency_separation.py +1343 -0
- setiastro/saspro/function_bundle.py +1594 -0
- setiastro/saspro/generate_translations.py +2378 -0
- setiastro/saspro/ghs_dialog_pro.py +660 -0
- setiastro/saspro/ghs_preset.py +284 -0
- setiastro/saspro/graxpert.py +634 -0
- setiastro/saspro/graxpert_preset.py +287 -0
- setiastro/saspro/gui/__init__.py +0 -0
- setiastro/saspro/gui/main_window.py +8567 -0
- setiastro/saspro/gui/mixins/__init__.py +33 -0
- setiastro/saspro/gui/mixins/dock_mixin.py +263 -0
- setiastro/saspro/gui/mixins/file_mixin.py +443 -0
- setiastro/saspro/gui/mixins/geometry_mixin.py +403 -0
- setiastro/saspro/gui/mixins/header_mixin.py +441 -0
- setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
- setiastro/saspro/gui/mixins/menu_mixin.py +361 -0
- setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +1457 -0
- setiastro/saspro/gui/mixins/update_mixin.py +309 -0
- setiastro/saspro/gui/mixins/view_mixin.py +435 -0
- setiastro/saspro/halobgon.py +462 -0
- setiastro/saspro/header_viewer.py +448 -0
- setiastro/saspro/headless_utils.py +88 -0
- setiastro/saspro/histogram.py +753 -0
- setiastro/saspro/history_explorer.py +939 -0
- setiastro/saspro/i18n.py +156 -0
- setiastro/saspro/image_combine.py +414 -0
- setiastro/saspro/image_peeker_pro.py +1601 -0
- setiastro/saspro/imageops/__init__.py +37 -0
- setiastro/saspro/imageops/mdi_snap.py +292 -0
- setiastro/saspro/imageops/scnr.py +36 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
- setiastro/saspro/imageops/stretch.py +244 -0
- setiastro/saspro/isophote.py +1179 -0
- setiastro/saspro/layers.py +208 -0
- setiastro/saspro/layers_dock.py +714 -0
- setiastro/saspro/lazy_imports.py +193 -0
- setiastro/saspro/legacy/__init__.py +2 -0
- setiastro/saspro/legacy/image_manager.py +2226 -0
- setiastro/saspro/legacy/numba_utils.py +3659 -0
- setiastro/saspro/legacy/xisf.py +1071 -0
- setiastro/saspro/linear_fit.py +534 -0
- setiastro/saspro/live_stacking.py +1830 -0
- setiastro/saspro/log_bus.py +5 -0
- setiastro/saspro/logging_config.py +460 -0
- setiastro/saspro/luminancerecombine.py +309 -0
- setiastro/saspro/main_helpers.py +201 -0
- setiastro/saspro/mask_creation.py +928 -0
- setiastro/saspro/masks_core.py +56 -0
- setiastro/saspro/mdi_widgets.py +353 -0
- setiastro/saspro/memory_utils.py +666 -0
- setiastro/saspro/metadata_patcher.py +75 -0
- setiastro/saspro/mfdeconv.py +3826 -0
- setiastro/saspro/mfdeconv_earlystop.py +71 -0
- setiastro/saspro/mfdeconvcudnn.py +3263 -0
- setiastro/saspro/mfdeconvsport.py +2382 -0
- setiastro/saspro/minorbodycatalog.py +567 -0
- setiastro/saspro/morphology.py +382 -0
- setiastro/saspro/multiscale_decomp.py +1290 -0
- setiastro/saspro/nbtorgb_stars.py +531 -0
- setiastro/saspro/numba_utils.py +3044 -0
- setiastro/saspro/numba_warmup.py +141 -0
- setiastro/saspro/ops/__init__.py +9 -0
- setiastro/saspro/ops/command_help_dialog.py +623 -0
- setiastro/saspro/ops/command_runner.py +217 -0
- setiastro/saspro/ops/commands.py +1594 -0
- setiastro/saspro/ops/script_editor.py +1102 -0
- setiastro/saspro/ops/scripts.py +1413 -0
- setiastro/saspro/ops/settings.py +679 -0
- setiastro/saspro/parallel_utils.py +554 -0
- setiastro/saspro/pedestal.py +121 -0
- setiastro/saspro/perfect_palette_picker.py +1070 -0
- setiastro/saspro/pipeline.py +110 -0
- setiastro/saspro/pixelmath.py +1600 -0
- setiastro/saspro/plate_solver.py +2444 -0
- setiastro/saspro/project_io.py +797 -0
- setiastro/saspro/psf_utils.py +136 -0
- setiastro/saspro/psf_viewer.py +549 -0
- setiastro/saspro/pyi_rthook_astroquery.py +95 -0
- setiastro/saspro/remove_green.py +314 -0
- setiastro/saspro/remove_stars.py +1625 -0
- setiastro/saspro/remove_stars_preset.py +404 -0
- setiastro/saspro/resources.py +477 -0
- setiastro/saspro/rgb_combination.py +207 -0
- setiastro/saspro/rgb_extract.py +19 -0
- setiastro/saspro/rgbalign.py +723 -0
- setiastro/saspro/runtime_imports.py +7 -0
- setiastro/saspro/runtime_torch.py +754 -0
- setiastro/saspro/save_options.py +72 -0
- setiastro/saspro/selective_color.py +1552 -0
- setiastro/saspro/sfcc.py +1430 -0
- setiastro/saspro/shortcuts.py +3043 -0
- setiastro/saspro/signature_insert.py +1099 -0
- setiastro/saspro/stacking_suite.py +18181 -0
- setiastro/saspro/star_alignment.py +7420 -0
- setiastro/saspro/star_alignment_preset.py +329 -0
- setiastro/saspro/star_metrics.py +49 -0
- setiastro/saspro/star_spikes.py +681 -0
- setiastro/saspro/star_stretch.py +470 -0
- setiastro/saspro/stat_stretch.py +506 -0
- setiastro/saspro/status_log_dock.py +78 -0
- setiastro/saspro/subwindow.py +3267 -0
- setiastro/saspro/supernovaasteroidhunter.py +1716 -0
- setiastro/saspro/swap_manager.py +99 -0
- setiastro/saspro/torch_backend.py +89 -0
- setiastro/saspro/torch_rejection.py +434 -0
- setiastro/saspro/translations/de_translations.py +3733 -0
- setiastro/saspro/translations/es_translations.py +3923 -0
- setiastro/saspro/translations/fr_translations.py +3842 -0
- setiastro/saspro/translations/integrate_translations.py +234 -0
- setiastro/saspro/translations/it_translations.py +3662 -0
- setiastro/saspro/translations/ja_translations.py +3585 -0
- setiastro/saspro/translations/pt_translations.py +3853 -0
- setiastro/saspro/translations/saspro_de.qm +0 -0
- setiastro/saspro/translations/saspro_de.ts +253 -0
- setiastro/saspro/translations/saspro_es.qm +0 -0
- setiastro/saspro/translations/saspro_es.ts +12520 -0
- setiastro/saspro/translations/saspro_fr.qm +0 -0
- setiastro/saspro/translations/saspro_fr.ts +12514 -0
- setiastro/saspro/translations/saspro_it.qm +0 -0
- setiastro/saspro/translations/saspro_it.ts +12520 -0
- setiastro/saspro/translations/saspro_ja.qm +0 -0
- setiastro/saspro/translations/saspro_ja.ts +257 -0
- setiastro/saspro/translations/saspro_pt.qm +0 -0
- setiastro/saspro/translations/saspro_pt.ts +257 -0
- setiastro/saspro/translations/saspro_zh.qm +0 -0
- setiastro/saspro/translations/saspro_zh.ts +12520 -0
- setiastro/saspro/translations/zh_translations.py +3659 -0
- setiastro/saspro/versioning.py +71 -0
- setiastro/saspro/view_bundle.py +1555 -0
- setiastro/saspro/wavescale_hdr.py +624 -0
- setiastro/saspro/wavescale_hdr_preset.py +101 -0
- setiastro/saspro/wavescalede.py +658 -0
- setiastro/saspro/wavescalede_preset.py +230 -0
- setiastro/saspro/wcs_update.py +374 -0
- setiastro/saspro/whitebalance.py +456 -0
- setiastro/saspro/widgets/__init__.py +48 -0
- setiastro/saspro/widgets/common_utilities.py +306 -0
- setiastro/saspro/widgets/graphics_views.py +122 -0
- setiastro/saspro/widgets/image_utils.py +518 -0
- setiastro/saspro/widgets/preview_dialogs.py +280 -0
- setiastro/saspro/widgets/spinboxes.py +275 -0
- setiastro/saspro/widgets/themed_buttons.py +13 -0
- setiastro/saspro/widgets/wavelet_utils.py +299 -0
- setiastro/saspro/window_shelf.py +185 -0
- setiastro/saspro/xisf.py +1123 -0
- setiastrosuitepro-1.6.1.dist-info/METADATA +267 -0
- setiastrosuitepro-1.6.1.dist-info/RECORD +342 -0
- setiastrosuitepro-1.6.1.dist-info/WHEEL +4 -0
- setiastrosuitepro-1.6.1.dist-info/entry_points.txt +6 -0
- setiastrosuitepro-1.6.1.dist-info/licenses/LICENSE +674 -0
- 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")
|