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.
- 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/Background_startup.jpg +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/qml/ResourceMonitor.qml +126 -0
- setiastro/saspro/__init__.py +20 -0
- setiastro/saspro/__main__.py +945 -0
- setiastro/saspro/_generated/__init__.py +7 -0
- setiastro/saspro/_generated/build_info.py +3 -0
- setiastro/saspro/abe.py +1346 -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 +624 -0
- setiastro/saspro/astrobin_exporter.py +1010 -0
- setiastro/saspro/astrospike.py +153 -0
- setiastro/saspro/astrospike_python.py +1841 -0
- setiastro/saspro/autostretch.py +198 -0
- setiastro/saspro/backgroundneutral.py +602 -0
- setiastro/saspro/batch_convert.py +328 -0
- setiastro/saspro/batch_renamer.py +522 -0
- setiastro/saspro/blemish_blaster.py +491 -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 +213 -0
- setiastro/saspro/clahe.py +368 -0
- setiastro/saspro/comet_stacking.py +1442 -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 +1400 -0
- setiastro/saspro/convo_preset.py +414 -0
- setiastro/saspro/copyastro.py +190 -0
- setiastro/saspro/cosmicclarity.py +1589 -0
- setiastro/saspro/cosmicclarity_preset.py +407 -0
- setiastro/saspro/crop_dialog_pro.py +973 -0
- setiastro/saspro/crop_preset.py +189 -0
- setiastro/saspro/curve_editor_pro.py +2562 -0
- setiastro/saspro/curves_preset.py +375 -0
- setiastro/saspro/debayer.py +673 -0
- setiastro/saspro/debug_utils.py +29 -0
- setiastro/saspro/dnd_mime.py +35 -0
- setiastro/saspro/doc_manager.py +2664 -0
- setiastro/saspro/exoplanet_detector.py +2166 -0
- setiastro/saspro/file_utils.py +284 -0
- setiastro/saspro/fitsmodifier.py +748 -0
- setiastro/saspro/fix_bom.py +32 -0
- setiastro/saspro/free_torch_memory.py +48 -0
- setiastro/saspro/frequency_separation.py +1349 -0
- setiastro/saspro/function_bundle.py +1596 -0
- setiastro/saspro/generate_translations.py +3092 -0
- setiastro/saspro/ghs_dialog_pro.py +663 -0
- setiastro/saspro/ghs_preset.py +284 -0
- setiastro/saspro/graxpert.py +637 -0
- setiastro/saspro/graxpert_preset.py +287 -0
- setiastro/saspro/gui/__init__.py +0 -0
- setiastro/saspro/gui/main_window.py +8810 -0
- setiastro/saspro/gui/mixins/__init__.py +33 -0
- setiastro/saspro/gui/mixins/dock_mixin.py +362 -0
- setiastro/saspro/gui/mixins/file_mixin.py +450 -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 +389 -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/gui/statistics_dialog.py +47 -0
- setiastro/saspro/halobgon.py +488 -0
- setiastro/saspro/header_viewer.py +448 -0
- setiastro/saspro/headless_utils.py +88 -0
- setiastro/saspro/histogram.py +756 -0
- setiastro/saspro/history_explorer.py +941 -0
- setiastro/saspro/i18n.py +168 -0
- setiastro/saspro/image_combine.py +417 -0
- setiastro/saspro/image_peeker_pro.py +1604 -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 +236 -0
- setiastro/saspro/isophote.py +1182 -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 +3676 -0
- setiastro/saspro/legacy/xisf.py +1071 -0
- setiastro/saspro/linear_fit.py +537 -0
- setiastro/saspro/live_stacking.py +1841 -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 +931 -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 +3831 -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 +407 -0
- setiastro/saspro/multiscale_decomp.py +1293 -0
- setiastro/saspro/nbtorgb_stars.py +541 -0
- setiastro/saspro/numba_utils.py +3145 -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 +1473 -0
- setiastro/saspro/ops/settings.py +637 -0
- setiastro/saspro/parallel_utils.py +554 -0
- setiastro/saspro/pedestal.py +121 -0
- setiastro/saspro/perfect_palette_picker.py +1071 -0
- setiastro/saspro/pipeline.py +110 -0
- setiastro/saspro/pixelmath.py +1604 -0
- setiastro/saspro/plate_solver.py +2445 -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 +331 -0
- setiastro/saspro/remove_stars.py +1599 -0
- setiastro/saspro/remove_stars_preset.py +404 -0
- setiastro/saspro/resources.py +501 -0
- setiastro/saspro/rgb_combination.py +208 -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 +73 -0
- setiastro/saspro/selective_color.py +1552 -0
- setiastro/saspro/sfcc.py +1472 -0
- setiastro/saspro/shortcuts.py +3043 -0
- setiastro/saspro/signature_insert.py +1102 -0
- setiastro/saspro/stacking_suite.py +18470 -0
- setiastro/saspro/star_alignment.py +7435 -0
- setiastro/saspro/star_alignment_preset.py +329 -0
- setiastro/saspro/star_metrics.py +49 -0
- setiastro/saspro/star_spikes.py +765 -0
- setiastro/saspro/star_stretch.py +507 -0
- setiastro/saspro/stat_stretch.py +538 -0
- setiastro/saspro/status_log_dock.py +78 -0
- setiastro/saspro/subwindow.py +3328 -0
- setiastro/saspro/supernovaasteroidhunter.py +1719 -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/all_source_strings.json +3654 -0
- setiastro/saspro/translations/ar_translations.py +3865 -0
- setiastro/saspro/translations/de_translations.py +3749 -0
- setiastro/saspro/translations/es_translations.py +3939 -0
- setiastro/saspro/translations/fr_translations.py +3858 -0
- setiastro/saspro/translations/hi_translations.py +3571 -0
- setiastro/saspro/translations/integrate_translations.py +270 -0
- setiastro/saspro/translations/it_translations.py +3678 -0
- setiastro/saspro/translations/ja_translations.py +3601 -0
- setiastro/saspro/translations/pt_translations.py +3869 -0
- setiastro/saspro/translations/ru_translations.py +2848 -0
- setiastro/saspro/translations/saspro_ar.qm +0 -0
- setiastro/saspro/translations/saspro_ar.ts +255 -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_hi.qm +0 -0
- setiastro/saspro/translations/saspro_hi.ts +257 -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_ru.qm +0 -0
- setiastro/saspro/translations/saspro_ru.ts +237 -0
- setiastro/saspro/translations/saspro_sw.qm +0 -0
- setiastro/saspro/translations/saspro_sw.ts +257 -0
- setiastro/saspro/translations/saspro_uk.qm +0 -0
- setiastro/saspro/translations/saspro_uk.ts +10771 -0
- setiastro/saspro/translations/saspro_zh.qm +0 -0
- setiastro/saspro/translations/saspro_zh.ts +12520 -0
- setiastro/saspro/translations/sw_translations.py +3671 -0
- setiastro/saspro/translations/uk_translations.py +3700 -0
- setiastro/saspro/translations/zh_translations.py +3675 -0
- setiastro/saspro/versioning.py +77 -0
- setiastro/saspro/view_bundle.py +1558 -0
- setiastro/saspro/wavescale_hdr.py +645 -0
- setiastro/saspro/wavescale_hdr_preset.py +101 -0
- setiastro/saspro/wavescalede.py +680 -0
- setiastro/saspro/wavescalede_preset.py +230 -0
- setiastro/saspro/wcs_update.py +374 -0
- setiastro/saspro/whitebalance.py +492 -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/minigame/game.js +986 -0
- setiastro/saspro/widgets/minigame/index.html +53 -0
- setiastro/saspro/widgets/minigame/style.css +241 -0
- setiastro/saspro/widgets/preview_dialogs.py +280 -0
- setiastro/saspro/widgets/resource_monitor.py +237 -0
- setiastro/saspro/widgets/spinboxes.py +275 -0
- setiastro/saspro/widgets/themed_buttons.py +13 -0
- setiastro/saspro/widgets/wavelet_utils.py +331 -0
- setiastro/saspro/wimi.py +7996 -0
- setiastro/saspro/wims.py +578 -0
- setiastro/saspro/window_shelf.py +185 -0
- setiastro/saspro/xisf.py +1123 -0
- setiastrosuitepro-1.6.2.post1.dist-info/METADATA +278 -0
- setiastrosuitepro-1.6.2.post1.dist-info/RECORD +367 -0
- setiastrosuitepro-1.6.2.post1.dist-info/WHEEL +4 -0
- setiastrosuitepro-1.6.2.post1.dist-info/entry_points.txt +6 -0
- setiastrosuitepro-1.6.2.post1.dist-info/licenses/LICENSE +674 -0
- setiastrosuitepro-1.6.2.post1.dist-info/licenses/license.txt +2580 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
# pro/header_viewer.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import os
|
|
4
|
+
import csv
|
|
5
|
+
from typing import Optional, Dict, Any
|
|
6
|
+
|
|
7
|
+
from PyQt6.QtWidgets import (
|
|
8
|
+
QDockWidget, QWidget, QVBoxLayout, QTreeWidget, QTreeWidgetItem,
|
|
9
|
+
QPushButton, QFileDialog, QMessageBox
|
|
10
|
+
)
|
|
11
|
+
from PyQt6.QtCore import Qt
|
|
12
|
+
|
|
13
|
+
from astropy.io import fits
|
|
14
|
+
try:
|
|
15
|
+
from astropy.io.fits.verify import VerifyError
|
|
16
|
+
except Exception:
|
|
17
|
+
class VerifyError(Exception):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
from setiastro.saspro.xisf import XISF
|
|
21
|
+
|
|
22
|
+
# we’ll reuse your loader helper for FITS headers
|
|
23
|
+
from setiastro.saspro.legacy.image_manager import get_valid_header, _drop_invalid_cards
|
|
24
|
+
from setiastro.saspro.doc_manager import ImageDocument
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class HeaderViewerDock(QDockWidget):
|
|
28
|
+
"""
|
|
29
|
+
Dock that shows metadata for the currently active ImageDocument.
|
|
30
|
+
Supports FITS headers and XISF file & image metadata.
|
|
31
|
+
"""
|
|
32
|
+
def __init__(self, parent=None):
|
|
33
|
+
super().__init__(self.tr("Header Viewer"), parent)
|
|
34
|
+
self._doc: Optional[ImageDocument] = None
|
|
35
|
+
self._doc_conn = False
|
|
36
|
+
|
|
37
|
+
self._tree = QTreeWidget()
|
|
38
|
+
self._tree.setHeaderLabels([self.tr("Key"), self.tr("Value")])
|
|
39
|
+
self._tree.setColumnWidth(0, 220)
|
|
40
|
+
|
|
41
|
+
self._save_btn = QPushButton(self.tr("Save Metadata…"))
|
|
42
|
+
self._save_btn.clicked.connect(self._save_metadata)
|
|
43
|
+
self._dm = None # <-- NEW: DocManager to query "active"
|
|
44
|
+
self._follow_hover = False # <-- optional toggle if you ever want hover-follow
|
|
45
|
+
w = QWidget(self)
|
|
46
|
+
lay = QVBoxLayout(w)
|
|
47
|
+
lay.setContentsMargins(6, 6, 6, 6)
|
|
48
|
+
lay.addWidget(self._tree)
|
|
49
|
+
lay.addWidget(self._save_btn)
|
|
50
|
+
self.setWidget(w)
|
|
51
|
+
|
|
52
|
+
def _same_base(self, a, b) -> bool:
|
|
53
|
+
return self._unwrap_base_doc(a) is self._unwrap_base_doc(b)
|
|
54
|
+
|
|
55
|
+
def attach_doc_manager(self, dm):
|
|
56
|
+
self._dm = dm
|
|
57
|
+
try:
|
|
58
|
+
# When docs are added/removed, re-evaluate the focused base
|
|
59
|
+
dm.documentAdded.connect(lambda _doc: self._maybe_refresh_for_active())
|
|
60
|
+
dm.documentRemoved.connect(lambda _doc: self._maybe_refresh_for_active())
|
|
61
|
+
|
|
62
|
+
# DO NOT use imageRegionUpdated to retarget; it can fire from hover-driven previews.
|
|
63
|
+
# If you want to repaint the same doc on region changes, _on_doc_changed handles that.
|
|
64
|
+
|
|
65
|
+
mdi = getattr(dm, "_mdi", None)
|
|
66
|
+
if mdi and hasattr(mdi, "subWindowActivated"):
|
|
67
|
+
mdi.subWindowActivated.connect(lambda _sw: self._maybe_refresh_for_active())
|
|
68
|
+
|
|
69
|
+
# NEW: snap to truly active base (sticky, click-activated only)
|
|
70
|
+
if hasattr(dm, "activeBaseChanged"):
|
|
71
|
+
dm.activeBaseChanged.connect(lambda _doc: self._maybe_refresh_for_active())
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
self._maybe_refresh_for_active()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def set_follow_hover(self, enabled: bool):
|
|
79
|
+
self._follow_hover = bool(enabled)
|
|
80
|
+
|
|
81
|
+
# Prefer base (true) doc over transient wrappers/proxies
|
|
82
|
+
def _unwrap_base_doc(self, d):
|
|
83
|
+
if d is None:
|
|
84
|
+
return None
|
|
85
|
+
# ROI preview wrapper → parent
|
|
86
|
+
p = getattr(d, "_parent_doc", None)
|
|
87
|
+
if isinstance(p, ImageDocument):
|
|
88
|
+
return p
|
|
89
|
+
# LiveViewDocument proxy → base
|
|
90
|
+
b = getattr(d, "_base", None)
|
|
91
|
+
if isinstance(b, ImageDocument):
|
|
92
|
+
return b
|
|
93
|
+
return d
|
|
94
|
+
|
|
95
|
+
def _active_base_doc(self):
|
|
96
|
+
if not self._dm:
|
|
97
|
+
return None
|
|
98
|
+
# Prefer DocManager’s sticky focused base if available
|
|
99
|
+
if hasattr(self._dm, "get_focused_base_document"):
|
|
100
|
+
try:
|
|
101
|
+
return self._dm.get_focused_base_document()
|
|
102
|
+
except Exception:
|
|
103
|
+
pass
|
|
104
|
+
# Fallback: unwrap whatever get_active_document returns
|
|
105
|
+
try:
|
|
106
|
+
cur = self._dm.get_active_document()
|
|
107
|
+
return self._unwrap_base_doc(cur)
|
|
108
|
+
except Exception:
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _maybe_refresh_for_active(self):
|
|
113
|
+
"""Rebuild only if our bound document == current active base document."""
|
|
114
|
+
active_base = self._active_base_doc()
|
|
115
|
+
if active_base is None:
|
|
116
|
+
return
|
|
117
|
+
# If we already show the same base doc, just ignore
|
|
118
|
+
if self._unwrap_base_doc(self._doc) is active_base:
|
|
119
|
+
return
|
|
120
|
+
# Else bind to the active base doc
|
|
121
|
+
self.set_document(active_base)
|
|
122
|
+
|
|
123
|
+
# ---- public API ----
|
|
124
|
+
def set_document(self, doc: Optional[ImageDocument]):
|
|
125
|
+
"""
|
|
126
|
+
Hard-lock behavior:
|
|
127
|
+
- If attached to a DocManager AND hover-follow is OFF, ignore the caller's 'doc'
|
|
128
|
+
and always bind to the DocManager's *active base* doc.
|
|
129
|
+
- Otherwise, behave like a normal setter.
|
|
130
|
+
"""
|
|
131
|
+
if self._dm and not self._follow_hover:
|
|
132
|
+
# Caller cannot hijack focus: resolve from DM every time
|
|
133
|
+
doc = self._active_base_doc()
|
|
134
|
+
|
|
135
|
+
# Always resolve to base (true) document for internal storage
|
|
136
|
+
base_doc = self._unwrap_base_doc(doc)
|
|
137
|
+
|
|
138
|
+
# No-op if unchanged
|
|
139
|
+
if self._same_base(self._doc, base_doc):
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
# Disconnect old
|
|
143
|
+
if self._doc and hasattr(self._doc, "changed"):
|
|
144
|
+
try:
|
|
145
|
+
self._doc.changed.disconnect(self._on_doc_changed)
|
|
146
|
+
except Exception:
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
self._doc = base_doc
|
|
150
|
+
|
|
151
|
+
# Listen for internal changes on the *bound* doc
|
|
152
|
+
if self._doc and hasattr(self._doc, "changed"):
|
|
153
|
+
try:
|
|
154
|
+
self._doc.changed.connect(self._on_doc_changed)
|
|
155
|
+
except Exception:
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
self._rebuild()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _on_doc_changed(self):
|
|
162
|
+
"""
|
|
163
|
+
Only rebuild if our bound doc is STILL the active base doc.
|
|
164
|
+
Prevents spurious rebuilds when focus changed between signal emit and slot run.
|
|
165
|
+
"""
|
|
166
|
+
if self._dm and not self._follow_hover:
|
|
167
|
+
active_base = self._active_base_doc()
|
|
168
|
+
if not self._same_base(self._doc, active_base):
|
|
169
|
+
# We got a change from an old/hover doc — ignore and snap to active.
|
|
170
|
+
self._maybe_refresh_for_active()
|
|
171
|
+
return
|
|
172
|
+
self._rebuild()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# --- helpers ---------------------------------------------------------
|
|
176
|
+
def _populate_header_dict(self, d: dict, title="Header (dict)"):
|
|
177
|
+
# We translate the default title if it's the default, but often title is passed in.
|
|
178
|
+
# If title is passed in English from other methods, we should translate it at the call site or here if possible.
|
|
179
|
+
# Since title is variable, we'll leave it as is, but ensure call sites pass translated strings.
|
|
180
|
+
root = QTreeWidgetItem([title])
|
|
181
|
+
self._tree.addTopLevelItem(root)
|
|
182
|
+
for k, v in d.items():
|
|
183
|
+
root.addChild(QTreeWidgetItem([str(k), str(v)]))
|
|
184
|
+
|
|
185
|
+
def _populate_header_snapshot(self, snap: dict):
|
|
186
|
+
fmt = (snap or {}).get("format", "")
|
|
187
|
+
if fmt == "fits-cards":
|
|
188
|
+
cards = snap.get("cards") or []
|
|
189
|
+
hdr = fits.Header()
|
|
190
|
+
for k, v, c in cards:
|
|
191
|
+
try:
|
|
192
|
+
hdr[str(k)] = (v, c)
|
|
193
|
+
except Exception:
|
|
194
|
+
# extremely defensive: skip bad card entries
|
|
195
|
+
pass
|
|
196
|
+
try:
|
|
197
|
+
hdr = _drop_invalid_cards(hdr)
|
|
198
|
+
except Exception:
|
|
199
|
+
pass
|
|
200
|
+
self._populate_fits_header(hdr)
|
|
201
|
+
elif fmt == "dict":
|
|
202
|
+
self._populate_header_dict(snap.get("items") or {}, self.tr("Header (snapshot)"))
|
|
203
|
+
else:
|
|
204
|
+
# generic repr fallback
|
|
205
|
+
txt = (snap or {}).get("text", "")
|
|
206
|
+
node = QTreeWidgetItem([self.tr("Header (snapshot)")])
|
|
207
|
+
self._tree.addTopLevelItem(node)
|
|
208
|
+
node.addChild(QTreeWidgetItem(["repr", str(txt)]))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _try_populate_from_doc(self, meta: dict) -> bool:
|
|
212
|
+
"""Return True if we showed any header from the document metadata."""
|
|
213
|
+
# 1) direct astropy header
|
|
214
|
+
hdr = meta.get("original_header") or meta.get("fits_header") or meta.get("header")
|
|
215
|
+
if isinstance(hdr, fits.Header):
|
|
216
|
+
try:
|
|
217
|
+
hdr = _drop_invalid_cards(hdr.copy())
|
|
218
|
+
except Exception:
|
|
219
|
+
pass
|
|
220
|
+
self._populate_fits_header(hdr)
|
|
221
|
+
return True
|
|
222
|
+
|
|
223
|
+
# 2) dict-style header (e.g., XISF-style properties captured as dict)
|
|
224
|
+
if isinstance(hdr, dict):
|
|
225
|
+
self._populate_header_dict(hdr, self.tr("Header (dict from document)"))
|
|
226
|
+
return True
|
|
227
|
+
|
|
228
|
+
# 3) JSON-safe snapshot captured by DocManager
|
|
229
|
+
snap = meta.get("__header_snapshot__")
|
|
230
|
+
if isinstance(snap, dict):
|
|
231
|
+
self._populate_header_snapshot(snap)
|
|
232
|
+
return True
|
|
233
|
+
|
|
234
|
+
# 4) XISF properties stored in metadata (common keys)
|
|
235
|
+
for k in ("xisf_header", "xisf_properties"):
|
|
236
|
+
if isinstance(meta.get(k), dict):
|
|
237
|
+
self._populate_header_dict(meta[k], self.tr("XISF Properties (document)"))
|
|
238
|
+
return True
|
|
239
|
+
|
|
240
|
+
return False
|
|
241
|
+
|
|
242
|
+
def _try_populate_from_file(self, path: str, meta: dict) -> bool:
|
|
243
|
+
"""Return True if we read & showed header from the backing file."""
|
|
244
|
+
if not path:
|
|
245
|
+
return False
|
|
246
|
+
p = path.lower()
|
|
247
|
+
|
|
248
|
+
# FITS (and MEF and .fz) via legacy helper
|
|
249
|
+
if p.endswith((".fits", ".fit", ".fz", ".fits.fz", ".fit.fz")):
|
|
250
|
+
# prefer the on-disk header if not already in meta
|
|
251
|
+
file_hdr = meta.get("original_header")
|
|
252
|
+
if isinstance(file_hdr, fits.Header):
|
|
253
|
+
try:
|
|
254
|
+
file_hdr = _drop_invalid_cards(file_hdr.copy())
|
|
255
|
+
except Exception:
|
|
256
|
+
pass
|
|
257
|
+
else:
|
|
258
|
+
file_hdr, _ = get_valid_header(path)
|
|
259
|
+
|
|
260
|
+
if isinstance(file_hdr, fits.Header):
|
|
261
|
+
self._populate_fits_header(file_hdr)
|
|
262
|
+
return True
|
|
263
|
+
|
|
264
|
+
# XISF: try to open and show basic properties if available
|
|
265
|
+
if p.endswith(".xisf"):
|
|
266
|
+
try:
|
|
267
|
+
xisf = XISF(path)
|
|
268
|
+
props = getattr(xisf, "properties", None)
|
|
269
|
+
if isinstance(props, dict):
|
|
270
|
+
self._populate_header_dict(props, self.tr("XISF Properties"))
|
|
271
|
+
return True
|
|
272
|
+
except Exception:
|
|
273
|
+
pass
|
|
274
|
+
|
|
275
|
+
return False
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# --- main ------------------------------------------------------------
|
|
279
|
+
def _rebuild(self):
|
|
280
|
+
self._tree.clear()
|
|
281
|
+
base_doc = self._unwrap_base_doc(self._doc)
|
|
282
|
+
if not base_doc:
|
|
283
|
+
self.setWindowTitle(self.tr("Header Viewer"))
|
|
284
|
+
return
|
|
285
|
+
self._doc = base_doc
|
|
286
|
+
|
|
287
|
+
meta = self._doc.metadata or {}
|
|
288
|
+
path = (meta.get("file_path") or "") if isinstance(meta.get("file_path"), str) else ""
|
|
289
|
+
base = os.path.basename(path) if path else (meta.get("display_name") or self.tr("Untitled"))
|
|
290
|
+
self.setWindowTitle(self.tr("Header: {0}").format(base))
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
# 1) Prefer header data already stored with the document
|
|
294
|
+
shown_any = self._try_populate_from_doc(meta)
|
|
295
|
+
|
|
296
|
+
# 2) If we didn't render anything yet, fall back to the file on disk
|
|
297
|
+
if not shown_any:
|
|
298
|
+
shown_any = self._try_populate_from_file(path, meta)
|
|
299
|
+
|
|
300
|
+
# 3) If there is a real astropy.wcs.WCS object, render it as key/value rows
|
|
301
|
+
try:
|
|
302
|
+
from astropy.wcs import WCS as _WCS
|
|
303
|
+
wcs_obj = meta.get("wcs")
|
|
304
|
+
if isinstance(wcs_obj, _WCS):
|
|
305
|
+
self._populate_wcs(wcs_obj)
|
|
306
|
+
except Exception:
|
|
307
|
+
pass
|
|
308
|
+
|
|
309
|
+
# 4) Always show remaining lightweight metadata (skip heavy blobs we already rendered)
|
|
310
|
+
info_root = QTreeWidgetItem([self.tr("Metadata")])
|
|
311
|
+
self._tree.addTopLevelItem(info_root)
|
|
312
|
+
for k, v in meta.items():
|
|
313
|
+
if k in ("original_header", "fits_header", "header", "wcs", "__header_snapshot__", "xisf_header", "xisf_properties"):
|
|
314
|
+
continue
|
|
315
|
+
info_root.addChild(QTreeWidgetItem([str(k), str(v)]))
|
|
316
|
+
|
|
317
|
+
self._tree.expandAll()
|
|
318
|
+
|
|
319
|
+
except Exception:
|
|
320
|
+
# per request: fail silently on final exception
|
|
321
|
+
pass
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# ---- population helpers ----
|
|
325
|
+
def _populate_fits_header(self, header: Any):
|
|
326
|
+
root = QTreeWidgetItem([self.tr("FITS Header")])
|
|
327
|
+
self._tree.addTopLevelItem(root)
|
|
328
|
+
|
|
329
|
+
# FITS Header: sanitize and iterate cards defensively
|
|
330
|
+
if isinstance(header, fits.Header):
|
|
331
|
+
try:
|
|
332
|
+
header = _drop_invalid_cards(header)
|
|
333
|
+
except Exception:
|
|
334
|
+
pass
|
|
335
|
+
|
|
336
|
+
for card in header.cards:
|
|
337
|
+
try:
|
|
338
|
+
k = str(card.keyword)
|
|
339
|
+
v = str(card.value)
|
|
340
|
+
except VerifyError as e:
|
|
341
|
+
# Skip invalid/unparsable card
|
|
342
|
+
print(f"[HeaderViewer] Skipping invalid FITS card {getattr(card, 'keyword', '?')!r}: {e}")
|
|
343
|
+
continue
|
|
344
|
+
except Exception as e:
|
|
345
|
+
print(f"[HeaderViewer] Error reading FITS card: {e}")
|
|
346
|
+
continue
|
|
347
|
+
root.addChild(QTreeWidgetItem([k, v]))
|
|
348
|
+
|
|
349
|
+
# Plain dict fallback (e.g., XISF-style dict)
|
|
350
|
+
elif isinstance(header, dict):
|
|
351
|
+
for k, v in header.items():
|
|
352
|
+
try:
|
|
353
|
+
root.addChild(QTreeWidgetItem([str(k), str(v)]))
|
|
354
|
+
except Exception:
|
|
355
|
+
continue
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _populate_wcs(self, wcs_obj):
|
|
359
|
+
"""Show a real astropy.wcs.WCS as header-like key/values."""
|
|
360
|
+
root = QTreeWidgetItem([self.tr("WCS")])
|
|
361
|
+
self._tree.addTopLevelItem(root)
|
|
362
|
+
try:
|
|
363
|
+
# Use relax=True so SIP/etc. are included if present.
|
|
364
|
+
wcs_hdr = wcs_obj.to_header(relax=True)
|
|
365
|
+
for k, v in wcs_hdr.items():
|
|
366
|
+
root.addChild(QTreeWidgetItem([str(k), str(v)]))
|
|
367
|
+
except Exception:
|
|
368
|
+
# Fallback: parse the repr into lines (better than a single blob).
|
|
369
|
+
for line in str(wcs_obj).splitlines():
|
|
370
|
+
s = line.strip()
|
|
371
|
+
if not s:
|
|
372
|
+
continue
|
|
373
|
+
if ":" in s:
|
|
374
|
+
a, b = s.split(":", 1)
|
|
375
|
+
root.addChild(QTreeWidgetItem([a.strip(), b.strip()]))
|
|
376
|
+
else:
|
|
377
|
+
root.addChild(QTreeWidgetItem(["", s]))
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _populate_from_xisf(self, path: str):
|
|
381
|
+
x = XISF(path)
|
|
382
|
+
file_meta: Dict[str, Any] = x.get_file_metadata()
|
|
383
|
+
img_meta_list = x.get_images_metadata()
|
|
384
|
+
img_meta: Dict[str, Any] = img_meta_list[0] if img_meta_list else {}
|
|
385
|
+
|
|
386
|
+
# File-level metadata
|
|
387
|
+
froot = QTreeWidgetItem([self.tr("XISF File Metadata")])
|
|
388
|
+
self._tree.addTopLevelItem(froot)
|
|
389
|
+
for k, v in file_meta.items():
|
|
390
|
+
vstr = v.get("value", "") if isinstance(v, dict) else v
|
|
391
|
+
froot.addChild(QTreeWidgetItem([str(k), str(vstr)]))
|
|
392
|
+
|
|
393
|
+
# Image-level metadata
|
|
394
|
+
iroot = QTreeWidgetItem([self.tr("XISF Image Metadata")])
|
|
395
|
+
self._tree.addTopLevelItem(iroot)
|
|
396
|
+
|
|
397
|
+
# FITS-like keywords (nested)
|
|
398
|
+
if "FITSKeywords" in img_meta:
|
|
399
|
+
fits_item = QTreeWidgetItem(["FITSKeywords"])
|
|
400
|
+
iroot.addChild(fits_item)
|
|
401
|
+
for kw, entries in img_meta["FITSKeywords"].items():
|
|
402
|
+
for ent in entries:
|
|
403
|
+
fits_item.addChild(QTreeWidgetItem([kw, str(ent.get("value", ""))]))
|
|
404
|
+
|
|
405
|
+
# XISFProperties (nested)
|
|
406
|
+
if "XISFProperties" in img_meta:
|
|
407
|
+
props_item = QTreeWidgetItem(["XISFProperties"])
|
|
408
|
+
iroot.addChild(props_item)
|
|
409
|
+
for prop_name, prop in img_meta["XISFProperties"].items():
|
|
410
|
+
props_item.addChild(QTreeWidgetItem([prop_name, str(prop.get("value", ""))]))
|
|
411
|
+
|
|
412
|
+
# Any remaining flat fields
|
|
413
|
+
for k, v in img_meta.items():
|
|
414
|
+
if k in ("FITSKeywords", "XISFProperties"):
|
|
415
|
+
continue
|
|
416
|
+
iroot.addChild(QTreeWidgetItem([k, str(v)]))
|
|
417
|
+
|
|
418
|
+
self._tree.expandAll()
|
|
419
|
+
|
|
420
|
+
# ---- export ----
|
|
421
|
+
def _save_metadata(self):
|
|
422
|
+
if not self._doc:
|
|
423
|
+
return
|
|
424
|
+
path, _ = QFileDialog.getSaveFileName(self, self.tr("Save Metadata"), "", self.tr("CSV (*.csv)"))
|
|
425
|
+
if not path:
|
|
426
|
+
return
|
|
427
|
+
|
|
428
|
+
# Flatten the QTreeWidget contents into key/value rows
|
|
429
|
+
rows = []
|
|
430
|
+
def walk(item: QTreeWidgetItem, prefix: str = ""):
|
|
431
|
+
key = item.text(0)
|
|
432
|
+
val = item.text(1)
|
|
433
|
+
full = f"{prefix}.{key}" if prefix else key
|
|
434
|
+
if key and val:
|
|
435
|
+
rows.append((full, val))
|
|
436
|
+
for i in range(item.childCount()):
|
|
437
|
+
walk(item.child(i), full)
|
|
438
|
+
|
|
439
|
+
for i in range(self._tree.topLevelItemCount()):
|
|
440
|
+
walk(self._tree.topLevelItem(i))
|
|
441
|
+
|
|
442
|
+
try:
|
|
443
|
+
with open(path, "w", newline="", encoding="utf-8") as f:
|
|
444
|
+
w = csv.writer(f)
|
|
445
|
+
w.writerow(["Key", "Value"])
|
|
446
|
+
w.writerows(rows)
|
|
447
|
+
except Exception as e:
|
|
448
|
+
QMessageBox.critical(self, self.tr("Save Metadata"), self.tr("Failed to save:\n{0}").format(e))
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# pro/headless_utils.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
def unwrap_docproxy(x, max_depth: int = 8):
|
|
5
|
+
"""
|
|
6
|
+
Safely unwrap live/roi/doc proxies to a real ImageDocument when possible.
|
|
7
|
+
- Recurses a few levels.
|
|
8
|
+
- Understands LiveViewDocument (_current/_base) and ROI wrappers (_parent_doc).
|
|
9
|
+
- Never unwraps to None unless input was None.
|
|
10
|
+
"""
|
|
11
|
+
if x is None:
|
|
12
|
+
return None
|
|
13
|
+
|
|
14
|
+
seen = set()
|
|
15
|
+
y = x
|
|
16
|
+
|
|
17
|
+
for _ in range(max_depth):
|
|
18
|
+
if y is None or id(y) in seen:
|
|
19
|
+
break
|
|
20
|
+
seen.add(id(y))
|
|
21
|
+
|
|
22
|
+
# LiveViewDocument / similar: prefer its resolver
|
|
23
|
+
cur = getattr(y, "_current", None)
|
|
24
|
+
if callable(cur):
|
|
25
|
+
try:
|
|
26
|
+
z = cur()
|
|
27
|
+
if z is not None and z is not y:
|
|
28
|
+
y = z
|
|
29
|
+
continue
|
|
30
|
+
except Exception:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
# Common doc proxy fields (ordered)
|
|
34
|
+
for attr in (
|
|
35
|
+
"_base", "base",
|
|
36
|
+
"_parent_doc", "parent_doc",
|
|
37
|
+
"base_document", "_base_document",
|
|
38
|
+
"_target", "target",
|
|
39
|
+
"_doc", "doc",
|
|
40
|
+
"_obj", "obj",
|
|
41
|
+
"_proxied", "proxied",
|
|
42
|
+
"_wrapped", "wrapped",
|
|
43
|
+
):
|
|
44
|
+
try:
|
|
45
|
+
z = getattr(y, attr, None)
|
|
46
|
+
except Exception:
|
|
47
|
+
z = None
|
|
48
|
+
if z is not None and z is not y:
|
|
49
|
+
y = z
|
|
50
|
+
break
|
|
51
|
+
else:
|
|
52
|
+
break
|
|
53
|
+
|
|
54
|
+
return y
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def normalize_headless_main(main_or_ctx, target_doc=None):
|
|
59
|
+
"""
|
|
60
|
+
Returns (main_window, doc, doc_manager)
|
|
61
|
+
Ensures doc + dm are fully unwrapped and ROI-aware.
|
|
62
|
+
"""
|
|
63
|
+
ctx = None
|
|
64
|
+
main = main_or_ctx
|
|
65
|
+
|
|
66
|
+
if hasattr(main_or_ctx, "app") and hasattr(main_or_ctx, "active_document"):
|
|
67
|
+
ctx = main_or_ctx
|
|
68
|
+
main = getattr(ctx, "app", None)
|
|
69
|
+
if target_doc is None:
|
|
70
|
+
try:
|
|
71
|
+
# Prefer dm.get_active_document() if possible (ROI-aware, real doc type)
|
|
72
|
+
dm0 = getattr(main, "doc_manager", None) or getattr(main, "dm", None)
|
|
73
|
+
dm0 = unwrap_docproxy(dm0)
|
|
74
|
+
if dm0 is not None and hasattr(dm0, "get_active_document"):
|
|
75
|
+
target_doc = dm0.get_active_document()
|
|
76
|
+
else:
|
|
77
|
+
target_doc = ctx.active_document()
|
|
78
|
+
except Exception:
|
|
79
|
+
target_doc = None
|
|
80
|
+
|
|
81
|
+
doc = unwrap_docproxy(target_doc)
|
|
82
|
+
|
|
83
|
+
dm = None
|
|
84
|
+
if main is not None:
|
|
85
|
+
dm = getattr(main, "doc_manager", None) or getattr(main, "dm", None)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
return main, doc, dm
|