setiastrosuitepro 1.6.5.post3__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.
- 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/rotatearbitrary.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 +958 -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 +698 -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 +611 -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 +3149 -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 +983 -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 +8792 -0
- setiastro/saspro/gui/mixins/__init__.py +33 -0
- setiastro/saspro/gui/mixins/dock_mixin.py +375 -0
- setiastro/saspro/gui/mixins/file_mixin.py +450 -0
- setiastro/saspro/gui/mixins/geometry_mixin.py +503 -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 +390 -0
- setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +1619 -0
- setiastro/saspro/gui/mixins/update_mixin.py +323 -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 +2360 -0
- setiastro/saspro/legacy/numba_utils.py +3676 -0
- setiastro/saspro/legacy/xisf.py +1213 -0
- setiastro/saspro/linear_fit.py +537 -0
- setiastro/saspro/live_stacking.py +1854 -0
- setiastro/saspro/log_bus.py +5 -0
- setiastro/saspro/logging_config.py +460 -0
- setiastro/saspro/luminancerecombine.py +510 -0
- setiastro/saspro/main_helpers.py +201 -0
- setiastro/saspro/mask_creation.py +1086 -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 +3909 -0
- setiastro/saspro/mfdeconv_earlystop.py +71 -0
- setiastro/saspro/mfdeconvcudnn.py +3312 -0
- setiastro/saspro/mfdeconvsport.py +2459 -0
- setiastro/saspro/minorbodycatalog.py +567 -0
- setiastro/saspro/morphology.py +407 -0
- setiastro/saspro/multiscale_decomp.py +1747 -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 +1105 -0
- setiastro/saspro/ops/scripts.py +1476 -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 +1105 -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 +446 -0
- setiastro/saspro/resources.py +503 -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 +1611 -0
- setiastro/saspro/sfcc.py +1472 -0
- setiastro/saspro/shortcuts.py +3116 -0
- setiastro/saspro/signature_insert.py +1102 -0
- setiastro/saspro/stacking_suite.py +19066 -0
- setiastro/saspro/star_alignment.py +7380 -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 +3407 -0
- setiastro/saspro/supernovaasteroidhunter.py +1719 -0
- setiastro/saspro/swap_manager.py +134 -0
- setiastro/saspro/torch_backend.py +89 -0
- setiastro/saspro/torch_rejection.py +434 -0
- setiastro/saspro/translations/all_source_strings.json +4726 -0
- setiastro/saspro/translations/ar_translations.py +4096 -0
- setiastro/saspro/translations/de_translations.py +3728 -0
- setiastro/saspro/translations/es_translations.py +4169 -0
- setiastro/saspro/translations/fr_translations.py +4090 -0
- setiastro/saspro/translations/hi_translations.py +3803 -0
- setiastro/saspro/translations/integrate_translations.py +271 -0
- setiastro/saspro/translations/it_translations.py +4728 -0
- setiastro/saspro/translations/ja_translations.py +3834 -0
- setiastro/saspro/translations/pt_translations.py +3847 -0
- setiastro/saspro/translations/ru_translations.py +3082 -0
- setiastro/saspro/translations/saspro_ar.qm +0 -0
- setiastro/saspro/translations/saspro_ar.ts +16019 -0
- setiastro/saspro/translations/saspro_de.qm +0 -0
- setiastro/saspro/translations/saspro_de.ts +14548 -0
- setiastro/saspro/translations/saspro_es.qm +0 -0
- setiastro/saspro/translations/saspro_es.ts +16202 -0
- setiastro/saspro/translations/saspro_fr.qm +0 -0
- setiastro/saspro/translations/saspro_fr.ts +15870 -0
- setiastro/saspro/translations/saspro_hi.qm +0 -0
- setiastro/saspro/translations/saspro_hi.ts +14855 -0
- setiastro/saspro/translations/saspro_it.qm +0 -0
- setiastro/saspro/translations/saspro_it.ts +19046 -0
- setiastro/saspro/translations/saspro_ja.qm +0 -0
- setiastro/saspro/translations/saspro_ja.ts +14980 -0
- setiastro/saspro/translations/saspro_pt.qm +0 -0
- setiastro/saspro/translations/saspro_pt.ts +15024 -0
- setiastro/saspro/translations/saspro_ru.qm +0 -0
- setiastro/saspro/translations/saspro_ru.ts +11835 -0
- setiastro/saspro/translations/saspro_sw.qm +0 -0
- setiastro/saspro/translations/saspro_sw.ts +15237 -0
- setiastro/saspro/translations/saspro_uk.qm +0 -0
- setiastro/saspro/translations/saspro_uk.ts +15248 -0
- setiastro/saspro/translations/saspro_zh.qm +0 -0
- setiastro/saspro/translations/saspro_zh.ts +15289 -0
- setiastro/saspro/translations/sw_translations.py +3897 -0
- setiastro/saspro/translations/uk_translations.py +3929 -0
- setiastro/saspro/translations/zh_translations.py +3910 -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 +513 -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 +991 -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 +263 -0
- setiastro/saspro/widgets/spinboxes.py +290 -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 +1213 -0
- setiastrosuitepro-1.6.5.post3.dist-info/METADATA +278 -0
- setiastrosuitepro-1.6.5.post3.dist-info/RECORD +368 -0
- setiastrosuitepro-1.6.5.post3.dist-info/WHEEL +4 -0
- setiastrosuitepro-1.6.5.post3.dist-info/entry_points.txt +6 -0
- setiastrosuitepro-1.6.5.post3.dist-info/licenses/LICENSE +674 -0
- setiastrosuitepro-1.6.5.post3.dist-info/licenses/license.txt +2580 -0
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
#pro.layers_dock.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import Optional
|
|
4
|
+
import json
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from PyQt6.QtCore import Qt, pyqtSignal, QByteArray, QTimer
|
|
8
|
+
from PyQt6.QtWidgets import (
|
|
9
|
+
QDockWidget, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QComboBox,
|
|
10
|
+
QListWidget, QListWidgetItem, QAbstractItemView, QSlider, QCheckBox,
|
|
11
|
+
QPushButton, QFrame, QMessageBox
|
|
12
|
+
)
|
|
13
|
+
from PyQt6.QtGui import QIcon, QDragEnterEvent, QDropEvent, QPixmap, QCursor
|
|
14
|
+
|
|
15
|
+
from setiastro.saspro.dnd_mime import MIME_VIEWSTATE, MIME_MASK
|
|
16
|
+
from setiastro.saspro.layers import composite_stack, ImageLayer, BLEND_MODES
|
|
17
|
+
|
|
18
|
+
# ---------- Small row widget for a layer ----------
|
|
19
|
+
class _LayerRow(QWidget):
|
|
20
|
+
changed = pyqtSignal()
|
|
21
|
+
requestDelete = pyqtSignal()
|
|
22
|
+
moveUp = pyqtSignal()
|
|
23
|
+
moveDown = pyqtSignal()
|
|
24
|
+
|
|
25
|
+
def __init__(self, name: str, mode: str = "Normal", opacity: float = 1.0,
|
|
26
|
+
visible: bool = True, parent=None, *, is_base: bool = False):
|
|
27
|
+
super().__init__(parent)
|
|
28
|
+
self._name = name
|
|
29
|
+
self._is_base = bool(is_base)
|
|
30
|
+
|
|
31
|
+
v = QVBoxLayout(self); v.setContentsMargins(6, 2, 6, 2)
|
|
32
|
+
|
|
33
|
+
# row 1: visibility, name, mode, opacity, reorder/delete
|
|
34
|
+
r1 = QHBoxLayout(); v.addLayout(r1)
|
|
35
|
+
self.chk = QCheckBox(); self.chk.setChecked(visible)
|
|
36
|
+
self.lbl = QLabel(name)
|
|
37
|
+
self.mode = QComboBox(); self.mode.addItems(BLEND_MODES)
|
|
38
|
+
try: self.mode.setCurrentIndex(max(0, BLEND_MODES.index(mode)))
|
|
39
|
+
except Exception: self.mode.setCurrentIndex(0)
|
|
40
|
+
self.sld = QSlider(Qt.Orientation.Horizontal); self.sld.setRange(0, 100); self.sld.setValue(int(round(opacity*100)))
|
|
41
|
+
self.btn_up = QPushButton("↑"); self.btn_up.setFixedWidth(28)
|
|
42
|
+
self.btn_dn = QPushButton("↓"); self.btn_dn.setFixedWidth(28)
|
|
43
|
+
self.btn_x = QPushButton("✕"); self.btn_x.setFixedWidth(28)
|
|
44
|
+
|
|
45
|
+
r1.addWidget(self.chk); r1.addWidget(self.lbl, 1)
|
|
46
|
+
r1.addWidget(self.mode); r1.addWidget(QLabel("Opacity")); r1.addWidget(self.sld, 1)
|
|
47
|
+
r1.addWidget(self.btn_up); r1.addWidget(self.btn_dn); r1.addWidget(self.btn_x)
|
|
48
|
+
|
|
49
|
+
# row 2: mask controls (hidden for base)
|
|
50
|
+
r2 = QHBoxLayout(); v.addLayout(r2)
|
|
51
|
+
self.mask_combo = QComboBox(); self.mask_combo.setMinimumWidth(140)
|
|
52
|
+
self.mask_combo.setPlaceholderText("Mask: (none)")
|
|
53
|
+
self.mask_invert = QCheckBox("Invert")
|
|
54
|
+
self.btn_clear_mask = QPushButton("Clear")
|
|
55
|
+
self.btn_clear_mask.setFixedWidth(52)
|
|
56
|
+
r2.addWidget(QLabel("Mask")); r2.addWidget(self.mask_combo, 1)
|
|
57
|
+
r2.addWidget(self.mask_invert); r2.addWidget(self.btn_clear_mask)
|
|
58
|
+
|
|
59
|
+
# Extra controls for some blend modes (e.g. Sigmoid)
|
|
60
|
+
self.sig_center_label = None
|
|
61
|
+
self.sig_center = None
|
|
62
|
+
self.sig_strength_label = None
|
|
63
|
+
self.sig_strength = None
|
|
64
|
+
|
|
65
|
+
if not self._is_base:
|
|
66
|
+
# row 3: Sigmoid parameters
|
|
67
|
+
r3 = QHBoxLayout(); v.addLayout(r3)
|
|
68
|
+
|
|
69
|
+
self.sig_center_label = QLabel("Sigmoid center")
|
|
70
|
+
from PyQt6.QtWidgets import QDoubleSpinBox
|
|
71
|
+
self.sig_center = QDoubleSpinBox()
|
|
72
|
+
self.sig_center.setRange(0.0, 1.0)
|
|
73
|
+
self.sig_center.setSingleStep(0.01)
|
|
74
|
+
self.sig_center.setDecimals(3)
|
|
75
|
+
self.sig_center.setValue(0.5)
|
|
76
|
+
|
|
77
|
+
self.sig_strength_label = QLabel("Strength")
|
|
78
|
+
self.sig_strength = QDoubleSpinBox()
|
|
79
|
+
self.sig_strength.setRange(0.1, 50.0)
|
|
80
|
+
self.sig_strength.setSingleStep(0.5)
|
|
81
|
+
self.sig_strength.setDecimals(2)
|
|
82
|
+
self.sig_strength.setValue(10.0)
|
|
83
|
+
|
|
84
|
+
r3.addWidget(self.sig_center_label)
|
|
85
|
+
r3.addWidget(self.sig_center)
|
|
86
|
+
r3.addWidget(self.sig_strength_label)
|
|
87
|
+
r3.addWidget(self.sig_strength)
|
|
88
|
+
r3.addStretch(1)
|
|
89
|
+
|
|
90
|
+
if self._is_base:
|
|
91
|
+
# Base row is informational only
|
|
92
|
+
for w in (self.chk, self.mode, self.sld, self.btn_up, self.btn_dn, self.btn_x,
|
|
93
|
+
self.mask_combo, self.mask_invert, self.btn_clear_mask):
|
|
94
|
+
w.setEnabled(False)
|
|
95
|
+
self.lbl.setStyleSheet("color: palette(mid);")
|
|
96
|
+
else:
|
|
97
|
+
self.chk.stateChanged.connect(self._emit)
|
|
98
|
+
self.mode.currentIndexChanged.connect(self._on_mode_changed)
|
|
99
|
+
self.sld.valueChanged.connect(self._emit)
|
|
100
|
+
self.mask_combo.currentIndexChanged.connect(self._emit)
|
|
101
|
+
self.mask_invert.stateChanged.connect(self._emit)
|
|
102
|
+
self.btn_clear_mask.clicked.connect(self._on_clear_mask)
|
|
103
|
+
self.btn_x.clicked.connect(self.requestDelete.emit)
|
|
104
|
+
self.btn_up.clicked.connect(self.moveUp.emit)
|
|
105
|
+
self.btn_dn.clicked.connect(self.moveDown.emit)
|
|
106
|
+
|
|
107
|
+
# Sigmoid controls emit change + only show for Sigmoid mode
|
|
108
|
+
if self.sig_center is not None:
|
|
109
|
+
self.sig_center.valueChanged.connect(self._emit)
|
|
110
|
+
if self.sig_strength is not None:
|
|
111
|
+
self.sig_strength.valueChanged.connect(self._emit)
|
|
112
|
+
|
|
113
|
+
self.mode.currentIndexChanged.connect(
|
|
114
|
+
lambda _i: self._update_extra_controls(self.mode.currentText())
|
|
115
|
+
)
|
|
116
|
+
# Initial visibility
|
|
117
|
+
self._update_extra_controls(self.mode.currentText())
|
|
118
|
+
|
|
119
|
+
def _on_mode_changed(self, _idx: int):
|
|
120
|
+
# Update which extra controls are visible
|
|
121
|
+
self._update_extra_controls(self.mode.currentText())
|
|
122
|
+
# Make our layout recompute height
|
|
123
|
+
lay = self.layout()
|
|
124
|
+
if lay is not None:
|
|
125
|
+
lay.invalidate()
|
|
126
|
+
lay.activate()
|
|
127
|
+
|
|
128
|
+
self.adjustSize()
|
|
129
|
+
self.updateGeometry()
|
|
130
|
+
# Tell the dock “something changed”
|
|
131
|
+
self._emit()
|
|
132
|
+
|
|
133
|
+
def _update_extra_controls(self, mode_text: str):
|
|
134
|
+
is_sig = (mode_text == "Sigmoid")
|
|
135
|
+
for w in (self.sig_center_label, self.sig_center,
|
|
136
|
+
self.sig_strength_label, self.sig_strength):
|
|
137
|
+
if w is not None:
|
|
138
|
+
w.setVisible(is_sig)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _update_extra_controls(self, mode_text: str):
|
|
142
|
+
is_sig = (mode_text == "Sigmoid")
|
|
143
|
+
for w in (self.sig_center_label, self.sig_center,
|
|
144
|
+
self.sig_strength_label, self.sig_strength):
|
|
145
|
+
if w is not None:
|
|
146
|
+
w.setVisible(is_sig)
|
|
147
|
+
|
|
148
|
+
# Let the layout recompute our preferred height
|
|
149
|
+
self.adjustSize()
|
|
150
|
+
self.updateGeometry()
|
|
151
|
+
|
|
152
|
+
def set_sigmoid_params(self, center: float, strength: float):
|
|
153
|
+
if self.sig_center is None or self.sig_strength is None:
|
|
154
|
+
return
|
|
155
|
+
self.sig_center.blockSignals(True)
|
|
156
|
+
self.sig_strength.blockSignals(True)
|
|
157
|
+
self.sig_center.setValue(float(center))
|
|
158
|
+
self.sig_strength.setValue(float(strength))
|
|
159
|
+
self.sig_center.blockSignals(False)
|
|
160
|
+
self.sig_strength.blockSignals(False)
|
|
161
|
+
self._update_extra_controls(self.mode.currentText())
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _on_clear_mask(self):
|
|
165
|
+
# select the explicit "(none)" entry
|
|
166
|
+
self.mask_combo.setCurrentIndex(0)
|
|
167
|
+
self._emit()
|
|
168
|
+
|
|
169
|
+
def _emit(self, *_):
|
|
170
|
+
self.changed.emit()
|
|
171
|
+
|
|
172
|
+
def params(self):
|
|
173
|
+
out = {
|
|
174
|
+
"visible": self.chk.isChecked(),
|
|
175
|
+
"mode": self.mode.currentText(),
|
|
176
|
+
"opacity": self.sld.value() / 100.0,
|
|
177
|
+
"name": self._name,
|
|
178
|
+
# mask UI state
|
|
179
|
+
"mask_index": self.mask_combo.currentIndex(),
|
|
180
|
+
"mask_src": "Luminance",
|
|
181
|
+
"mask_invert": self.mask_invert.isChecked(),
|
|
182
|
+
}
|
|
183
|
+
if self.sig_center is not None and self.sig_strength is not None:
|
|
184
|
+
out["sigmoid_center"] = self.sig_center.value()
|
|
185
|
+
out["sigmoid_strength"] = self.sig_strength.value()
|
|
186
|
+
return out
|
|
187
|
+
|
|
188
|
+
def setName(self, name: str):
|
|
189
|
+
self._name = name
|
|
190
|
+
self.lbl.setText(name)
|
|
191
|
+
|
|
192
|
+
# ---------- The Dock ----------
|
|
193
|
+
class LayersDock(QDockWidget):
|
|
194
|
+
def __init__(self, main_window):
|
|
195
|
+
super().__init__("Layers", main_window)
|
|
196
|
+
self.setObjectName("LayersDock")
|
|
197
|
+
self.mw = main_window
|
|
198
|
+
self.docman = main_window.docman
|
|
199
|
+
self._wired_title_sources = set()
|
|
200
|
+
|
|
201
|
+
self._apply_timer = QTimer(self)
|
|
202
|
+
self._apply_timer.setSingleShot(True)
|
|
203
|
+
self._apply_timer.timeout.connect(self._apply_list_to_view)
|
|
204
|
+
self._apply_debounce_ms = 100 # tweak 60–150ms as you like
|
|
205
|
+
|
|
206
|
+
# UI
|
|
207
|
+
w = QWidget()
|
|
208
|
+
v = QVBoxLayout(w); v.setContentsMargins(8, 8, 8, 8)
|
|
209
|
+
top = QHBoxLayout(); v.addLayout(top)
|
|
210
|
+
top.addWidget(QLabel("View:"))
|
|
211
|
+
self.view_combo = QComboBox()
|
|
212
|
+
top.addWidget(self.view_combo, 1)
|
|
213
|
+
|
|
214
|
+
self.list = QListWidget()
|
|
215
|
+
self.list.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
|
216
|
+
self.list.setAlternatingRowColors(True)
|
|
217
|
+
v.addWidget(self.list, 1)
|
|
218
|
+
|
|
219
|
+
# buttons
|
|
220
|
+
row = QHBoxLayout(); v.addLayout(row)
|
|
221
|
+
self.btn_clear = QPushButton("Clear All Layers")
|
|
222
|
+
self.btn_merge = QPushButton("Merge Layers and Push to View")
|
|
223
|
+
self.btn_merge.setToolTip("Flatten the visible layers into the current view and add an undo step.")
|
|
224
|
+
row.addWidget(self.btn_merge)
|
|
225
|
+
row.addStretch(1)
|
|
226
|
+
row.addWidget(self.btn_clear)
|
|
227
|
+
|
|
228
|
+
self.setWidget(w)
|
|
229
|
+
|
|
230
|
+
# dnd (accept drops from views)
|
|
231
|
+
self.setAcceptDrops(True)
|
|
232
|
+
|
|
233
|
+
# signals
|
|
234
|
+
self.view_combo.currentIndexChanged.connect(self._on_pick_view)
|
|
235
|
+
self.btn_clear.clicked.connect(self._clear_layers)
|
|
236
|
+
|
|
237
|
+
# keep in sync with MDI/windows
|
|
238
|
+
self.mw.mdi.subWindowActivated.connect(lambda _sw: self._refresh_views())
|
|
239
|
+
self.docman.documentAdded.connect(lambda _d: self._refresh_views())
|
|
240
|
+
self.docman.documentRemoved.connect(lambda _d: self._refresh_views())
|
|
241
|
+
|
|
242
|
+
self.btn_merge.clicked.connect(self._merge_and_push)
|
|
243
|
+
|
|
244
|
+
# initial
|
|
245
|
+
self._refresh_views()
|
|
246
|
+
|
|
247
|
+
# ---------- helpers ----------
|
|
248
|
+
def _mask_choices(self):
|
|
249
|
+
out = []
|
|
250
|
+
for sw in self._all_subwindows():
|
|
251
|
+
title = sw._effective_title() or "Untitled"
|
|
252
|
+
out.append((title, sw.document))
|
|
253
|
+
return out
|
|
254
|
+
|
|
255
|
+
def _all_subwindows(self):
|
|
256
|
+
from setiastro.saspro.subwindow import ImageSubWindow
|
|
257
|
+
subs = []
|
|
258
|
+
for sw in self.mw.mdi.subWindowList():
|
|
259
|
+
w = sw.widget()
|
|
260
|
+
if isinstance(w, ImageSubWindow):
|
|
261
|
+
subs.append(w)
|
|
262
|
+
return subs
|
|
263
|
+
|
|
264
|
+
def _refresh_views(self):
|
|
265
|
+
subs = self._all_subwindows()
|
|
266
|
+
current = self.current_view()
|
|
267
|
+
self.view_combo.blockSignals(True)
|
|
268
|
+
self.view_combo.clear()
|
|
269
|
+
for w in subs:
|
|
270
|
+
title = w._effective_title() or "Untitled"
|
|
271
|
+
self.view_combo.addItem(title, userData=w)
|
|
272
|
+
self.view_combo.blockSignals(False)
|
|
273
|
+
|
|
274
|
+
if current and current in subs:
|
|
275
|
+
idx = subs.index(current)
|
|
276
|
+
self.view_combo.setCurrentIndex(idx)
|
|
277
|
+
elif subs:
|
|
278
|
+
self.view_combo.setCurrentIndex(0)
|
|
279
|
+
|
|
280
|
+
# NEW: listen for future title changes
|
|
281
|
+
self._wire_title_change_listeners(subs)
|
|
282
|
+
|
|
283
|
+
self._rebuild_list()
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _wire_title_change_listeners(self, subs):
|
|
287
|
+
# connect once per subwindow
|
|
288
|
+
for sw in subs:
|
|
289
|
+
if sw in self._wired_title_sources:
|
|
290
|
+
continue
|
|
291
|
+
if hasattr(sw, "viewTitleChanged"):
|
|
292
|
+
try:
|
|
293
|
+
sw.viewTitleChanged.connect(lambda *_: self._refresh_titles_only())
|
|
294
|
+
except Exception:
|
|
295
|
+
pass
|
|
296
|
+
self._wired_title_sources.add(sw)
|
|
297
|
+
|
|
298
|
+
def _refresh_titles_only(self):
|
|
299
|
+
"""Update just the titles in the View dropdown, mask source lists,
|
|
300
|
+
and base-row label, preserving current selection and layer state."""
|
|
301
|
+
subs = self._all_subwindows()
|
|
302
|
+
if not subs:
|
|
303
|
+
return
|
|
304
|
+
|
|
305
|
+
# Update the View dropdown text in place
|
|
306
|
+
self.view_combo.blockSignals(True)
|
|
307
|
+
cur_idx = self.view_combo.currentIndex()
|
|
308
|
+
for i, sw in enumerate(subs):
|
|
309
|
+
t = sw._effective_title() or "Untitled"
|
|
310
|
+
if i < self.view_combo.count():
|
|
311
|
+
self.view_combo.setItemText(i, t)
|
|
312
|
+
else:
|
|
313
|
+
self.view_combo.addItem(t, userData=sw)
|
|
314
|
+
self.view_combo.blockSignals(False)
|
|
315
|
+
if 0 <= cur_idx < self.view_combo.count():
|
|
316
|
+
self.view_combo.setCurrentIndex(cur_idx)
|
|
317
|
+
|
|
318
|
+
# Update mask choices shown in each row (titles only)
|
|
319
|
+
choices = [(sw._effective_title() or "Untitled", sw.document) for sw in subs]
|
|
320
|
+
docs = [d for _, d in choices]
|
|
321
|
+
|
|
322
|
+
for i in range(self.list.count()):
|
|
323
|
+
roww = self.list.itemWidget(self.list.item(i))
|
|
324
|
+
if not isinstance(roww, _LayerRow):
|
|
325
|
+
continue
|
|
326
|
+
|
|
327
|
+
# base row label
|
|
328
|
+
if getattr(roww, "_is_base", False):
|
|
329
|
+
vw = self.current_view()
|
|
330
|
+
base_name = vw._effective_title() if (vw and hasattr(vw, "_effective_title")) else "Current View"
|
|
331
|
+
roww.setName(f"Base • {base_name}")
|
|
332
|
+
continue
|
|
333
|
+
|
|
334
|
+
# non-base row: update mask combo item texts without changing selection
|
|
335
|
+
if roww.mask_combo.count() > 0:
|
|
336
|
+
# index 0 is "(none)"
|
|
337
|
+
# build a map from doc -> title
|
|
338
|
+
title_for_doc = {doc: title for title, doc in choices}
|
|
339
|
+
for idx in range(1, roww.mask_combo.count()):
|
|
340
|
+
doc = roww.mask_combo.itemData(idx)
|
|
341
|
+
if doc in title_for_doc:
|
|
342
|
+
roww.mask_combo.setItemText(idx, title_for_doc[doc])
|
|
343
|
+
|
|
344
|
+
def current_view(self):
|
|
345
|
+
idx = self.view_combo.currentIndex()
|
|
346
|
+
if idx < 0:
|
|
347
|
+
return None
|
|
348
|
+
return self.view_combo.itemData(idx)
|
|
349
|
+
|
|
350
|
+
def _on_pick_view(self, _i):
|
|
351
|
+
self._rebuild_list()
|
|
352
|
+
|
|
353
|
+
def _rebuild_list(self):
|
|
354
|
+
self.list.clear()
|
|
355
|
+
vw = self.current_view()
|
|
356
|
+
if not vw:
|
|
357
|
+
return
|
|
358
|
+
|
|
359
|
+
choices = self._mask_choices()
|
|
360
|
+
docs = [d for _, d in choices]
|
|
361
|
+
|
|
362
|
+
for lyr in getattr(vw, "_layers", []):
|
|
363
|
+
raw_name = getattr(lyr, "name", "Layer")
|
|
364
|
+
name = raw_name if isinstance(raw_name, str) else str(raw_name)
|
|
365
|
+
|
|
366
|
+
# --- Optional dynamic title sync ---
|
|
367
|
+
try:
|
|
368
|
+
src_doc = getattr(lyr, "src_doc", None)
|
|
369
|
+
# What the document considers its "base" display name
|
|
370
|
+
doc_disp = None
|
|
371
|
+
if src_doc is not None:
|
|
372
|
+
dn = getattr(src_doc, "display_name", None)
|
|
373
|
+
doc_disp = dn() if callable(dn) else dn
|
|
374
|
+
|
|
375
|
+
# If our stored name is just the base doc name, prefer the current view title
|
|
376
|
+
if src_doc is not None and name == (doc_disp or name):
|
|
377
|
+
for sw in self._all_subwindows():
|
|
378
|
+
if getattr(sw, "document", None) is src_doc:
|
|
379
|
+
t = getattr(sw, "_effective_title", None)
|
|
380
|
+
if callable(t):
|
|
381
|
+
t = t()
|
|
382
|
+
if t:
|
|
383
|
+
name = t
|
|
384
|
+
break
|
|
385
|
+
except Exception:
|
|
386
|
+
pass
|
|
387
|
+
mode = getattr(lyr, "mode", "Normal")
|
|
388
|
+
opacity = float(getattr(lyr, "opacity", 1.0))
|
|
389
|
+
visible = bool(getattr(lyr, "visible", True))
|
|
390
|
+
roww = _LayerRow(name, mode, opacity, visible)
|
|
391
|
+
roww.mask_combo.blockSignals(True)
|
|
392
|
+
roww.mask_combo.clear()
|
|
393
|
+
roww.mask_combo.addItem("(none)", userData=None)
|
|
394
|
+
for title, doc in choices:
|
|
395
|
+
roww.mask_combo.addItem(title, userData=doc)
|
|
396
|
+
if getattr(lyr, "mask_doc", None) in docs:
|
|
397
|
+
roww.mask_combo.setCurrentIndex(1 + docs.index(lyr.mask_doc))
|
|
398
|
+
else:
|
|
399
|
+
roww.mask_combo.setCurrentIndex(0)
|
|
400
|
+
|
|
401
|
+
roww.mask_invert.setChecked(bool(getattr(lyr, "mask_invert", False)))
|
|
402
|
+
roww.mask_combo.blockSignals(False)
|
|
403
|
+
center = getattr(lyr, "sigmoid_center", 0.5)
|
|
404
|
+
strength = getattr(lyr, "sigmoid_strength", 10.0)
|
|
405
|
+
roww.set_sigmoid_params(center, strength)
|
|
406
|
+
self._bind_row(roww)
|
|
407
|
+
it = QListWidgetItem(self.list)
|
|
408
|
+
it.setSizeHint(roww.sizeHint())
|
|
409
|
+
self.list.addItem(it)
|
|
410
|
+
self.list.setItemWidget(it, roww)
|
|
411
|
+
|
|
412
|
+
base_name = getattr(vw, "_effective_title", None)
|
|
413
|
+
base_name = base_name() if callable(base_name) else "Current View"
|
|
414
|
+
base_label = f"Base • {base_name}"
|
|
415
|
+
base_row = _LayerRow(base_label, "—", 1.0, True, is_base=True)
|
|
416
|
+
itb = QListWidgetItem(self.list)
|
|
417
|
+
itb.setSizeHint(base_row.sizeHint())
|
|
418
|
+
self.list.addItem(itb)
|
|
419
|
+
self.list.setItemWidget(itb, base_row)
|
|
420
|
+
has_layers = bool(getattr(vw, "_layers", []))
|
|
421
|
+
self.btn_merge.setEnabled(has_layers)
|
|
422
|
+
self.btn_clear.setEnabled(has_layers)
|
|
423
|
+
self._refresh_row_heights()
|
|
424
|
+
|
|
425
|
+
def _layer_count(self) -> int:
|
|
426
|
+
vw = self.current_view()
|
|
427
|
+
return len(getattr(vw, "_layers", [])) if vw else 0
|
|
428
|
+
|
|
429
|
+
def _bind_row(self, roww: _LayerRow):
|
|
430
|
+
if getattr(roww, "_is_base", False):
|
|
431
|
+
return
|
|
432
|
+
roww.changed.connect(self._apply_list_to_view_debounced)
|
|
433
|
+
|
|
434
|
+
roww.requestDelete.connect(lambda: self._delete_row(roww))
|
|
435
|
+
roww.moveUp.connect(lambda: self._move_row(roww, -1))
|
|
436
|
+
roww.moveDown.connect(lambda: self._move_row(roww, +1))
|
|
437
|
+
|
|
438
|
+
def _apply_list_to_view_debounced(self):
|
|
439
|
+
# restart the timer on every slider tick
|
|
440
|
+
self._apply_timer.start(self._apply_debounce_ms)
|
|
441
|
+
# Also refresh row heights so mode-dependent controls (like Sigmoid)
|
|
442
|
+
# can expand/collapse the row visually.
|
|
443
|
+
self._refresh_row_heights()
|
|
444
|
+
|
|
445
|
+
def _refresh_row_heights(self):
|
|
446
|
+
"""Update QListWidgetItem size hints to match current row widgets."""
|
|
447
|
+
try:
|
|
448
|
+
for i in range(self.list.count()):
|
|
449
|
+
item = self.list.item(i)
|
|
450
|
+
roww = self.list.itemWidget(item)
|
|
451
|
+
if roww is not None:
|
|
452
|
+
# Ask the row for an up-to-date size hint
|
|
453
|
+
item.setSizeHint(roww.sizeHint())
|
|
454
|
+
except Exception as ex:
|
|
455
|
+
print("[LayersDock] _refresh_row_heights error:", ex)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _find_row_index(self, roww: _LayerRow) -> int:
|
|
460
|
+
for i in range(self.list.count()):
|
|
461
|
+
if self.list.itemWidget(self.list.item(i)) is roww:
|
|
462
|
+
return i
|
|
463
|
+
return -1
|
|
464
|
+
|
|
465
|
+
def _delete_row(self, roww: _LayerRow):
|
|
466
|
+
vw = self.current_view()
|
|
467
|
+
if not vw:
|
|
468
|
+
return
|
|
469
|
+
idx = self._find_row_index(roww)
|
|
470
|
+
if idx < 0:
|
|
471
|
+
return
|
|
472
|
+
if idx >= self._layer_count():
|
|
473
|
+
return
|
|
474
|
+
vw._layers.pop(idx)
|
|
475
|
+
self.list.takeItem(idx)
|
|
476
|
+
self._apply_list_to_view()
|
|
477
|
+
|
|
478
|
+
def _move_row(self, roww: _LayerRow, delta: int):
|
|
479
|
+
vw = self.current_view()
|
|
480
|
+
if not vw:
|
|
481
|
+
return
|
|
482
|
+
i = self._find_row_index(roww)
|
|
483
|
+
if i < 0 or i >= self._layer_count():
|
|
484
|
+
return
|
|
485
|
+
j = i + delta
|
|
486
|
+
if j < 0 or j >= self._layer_count():
|
|
487
|
+
return
|
|
488
|
+
vw._layers[i], vw._layers[j] = vw._layers[j], vw._layers[i]
|
|
489
|
+
self._rebuild_list()
|
|
490
|
+
self._apply_list_to_view()
|
|
491
|
+
|
|
492
|
+
def _apply_list_to_view(self):
|
|
493
|
+
vw = self.current_view()
|
|
494
|
+
if not vw:
|
|
495
|
+
return
|
|
496
|
+
n = self._layer_count()
|
|
497
|
+
rows = []
|
|
498
|
+
for i in range(n):
|
|
499
|
+
it = self.list.item(i)
|
|
500
|
+
rows.append(self.list.itemWidget(it))
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
for lyr, roww in zip(vw._layers, rows):
|
|
504
|
+
p = roww.params()
|
|
505
|
+
lyr.visible = p["visible"]
|
|
506
|
+
lyr.mode = p["mode"]
|
|
507
|
+
lyr.opacity = float(p["opacity"])
|
|
508
|
+
# Sigmoid parameters (if present)
|
|
509
|
+
if "sigmoid_center" in p:
|
|
510
|
+
lyr.sigmoid_center = float(p["sigmoid_center"])
|
|
511
|
+
if "sigmoid_strength" in p:
|
|
512
|
+
lyr.sigmoid_strength = float(p["sigmoid_strength"])
|
|
513
|
+
mi = p["mask_index"]
|
|
514
|
+
if mi is not None and mi > 0:
|
|
515
|
+
doc = roww.mask_combo.itemData(mi)
|
|
516
|
+
lyr.mask_doc = doc
|
|
517
|
+
else:
|
|
518
|
+
lyr.mask_doc = None
|
|
519
|
+
|
|
520
|
+
# Force luminance masks only
|
|
521
|
+
lyr.mask_use_luma = True
|
|
522
|
+
lyr.mask_invert = bool(p["mask_invert"])
|
|
523
|
+
vw._reinstall_layer_watchers()
|
|
524
|
+
vw.apply_layer_stack(vw._layers)
|
|
525
|
+
|
|
526
|
+
def _clear_layers(self):
|
|
527
|
+
vw = self.current_view()
|
|
528
|
+
if not vw: return
|
|
529
|
+
vw._layers = []
|
|
530
|
+
vw._reinstall_layer_watchers()
|
|
531
|
+
self._rebuild_list()
|
|
532
|
+
vw.apply_layer_stack([])
|
|
533
|
+
|
|
534
|
+
def dragEnterEvent(self, e: QDragEnterEvent):
|
|
535
|
+
md = e.mimeData()
|
|
536
|
+
if md.hasFormat(MIME_VIEWSTATE) or md.hasFormat(MIME_MASK):
|
|
537
|
+
e.acceptProposedAction()
|
|
538
|
+
else:
|
|
539
|
+
e.ignore()
|
|
540
|
+
|
|
541
|
+
def dragMoveEvent(self, e: QDragEnterEvent):
|
|
542
|
+
self.dragEnterEvent(e)
|
|
543
|
+
|
|
544
|
+
def dropEvent(self, e: QDropEvent):
|
|
545
|
+
vw = self.current_view()
|
|
546
|
+
if not vw:
|
|
547
|
+
e.ignore(); return
|
|
548
|
+
md = e.mimeData()
|
|
549
|
+
try:
|
|
550
|
+
if md.hasFormat(MIME_VIEWSTATE):
|
|
551
|
+
st = json.loads(bytes(md.data(MIME_VIEWSTATE)).decode("utf-8"))
|
|
552
|
+
# Try robust resolution (UIDs/file_path/ptr)
|
|
553
|
+
src_doc = self._resolve_doc_from_state(st)
|
|
554
|
+
if src_doc is None:
|
|
555
|
+
raise RuntimeError("Source doc gone")
|
|
556
|
+
layer_name = "Layer"
|
|
557
|
+
src_title = None
|
|
558
|
+
for sw in self._all_subwindows():
|
|
559
|
+
if getattr(sw, "document", None) is src_doc:
|
|
560
|
+
t = getattr(sw, "_effective_title", None)
|
|
561
|
+
src_title = t() if callable(t) else t
|
|
562
|
+
break
|
|
563
|
+
if src_title:
|
|
564
|
+
layer_name = src_title
|
|
565
|
+
else:
|
|
566
|
+
dn = getattr(src_doc, "display_name", None)
|
|
567
|
+
layer_name = dn() if callable(dn) else (dn or "Layer")
|
|
568
|
+
|
|
569
|
+
new_layer = ImageLayer(
|
|
570
|
+
name=layer_name,
|
|
571
|
+
src_doc=src_doc,
|
|
572
|
+
visible=True,
|
|
573
|
+
opacity=1.0,
|
|
574
|
+
mode="Normal",
|
|
575
|
+
)
|
|
576
|
+
if not hasattr(vw, "_layers") or vw._layers is None:
|
|
577
|
+
vw._layers = []
|
|
578
|
+
vw._layers.insert(0, new_layer)
|
|
579
|
+
vw._reinstall_layer_watchers()
|
|
580
|
+
self._rebuild_list()
|
|
581
|
+
vw.apply_layer_stack(vw._layers)
|
|
582
|
+
e.acceptProposedAction()
|
|
583
|
+
return
|
|
584
|
+
|
|
585
|
+
if md.hasFormat(MIME_MASK):
|
|
586
|
+
payload = json.loads(bytes(md.data(MIME_MASK)).decode("utf-8"))
|
|
587
|
+
# payload may include doc_uid/base_doc_uid/file_path/mask_doc_ptr
|
|
588
|
+
mask_doc = self._resolve_doc_from_state(payload)
|
|
589
|
+
if mask_doc is None:
|
|
590
|
+
raise RuntimeError("Mask doc gone")
|
|
591
|
+
if not getattr(vw, "_layers", None):
|
|
592
|
+
QMessageBox.information(self, "No Layers", "Add a layer first, then drop a mask onto it.")
|
|
593
|
+
e.ignore(); return
|
|
594
|
+
sel_row = self.list.currentRow()
|
|
595
|
+
if sel_row < 0:
|
|
596
|
+
sel_row = 0
|
|
597
|
+
idx = min(sel_row, len(vw._layers) - 1)
|
|
598
|
+
layer = vw._layers[idx]
|
|
599
|
+
layer.mask_doc = mask_doc
|
|
600
|
+
layer.mask_invert = bool(payload.get("invert", False))
|
|
601
|
+
try:
|
|
602
|
+
layer.mask_feather = float(payload.get("feather", 0.0) or 0.0)
|
|
603
|
+
except Exception:
|
|
604
|
+
layer.mask_feather = 0.0
|
|
605
|
+
vw._reinstall_layer_watchers()
|
|
606
|
+
self._rebuild_list()
|
|
607
|
+
vw.apply_layer_stack(vw._layers)
|
|
608
|
+
e.acceptProposedAction()
|
|
609
|
+
return
|
|
610
|
+
|
|
611
|
+
except Exception as ex:
|
|
612
|
+
print("[LayersDock] drop error:", ex)
|
|
613
|
+
e.ignore()
|
|
614
|
+
|
|
615
|
+
def _resolve_doc_ptr(self, ptr: int):
|
|
616
|
+
"""Legacy path: resolve by Python id() pointer."""
|
|
617
|
+
try:
|
|
618
|
+
for d in self.docman.all_documents():
|
|
619
|
+
if id(d) == ptr:
|
|
620
|
+
return d
|
|
621
|
+
except Exception:
|
|
622
|
+
pass
|
|
623
|
+
return None
|
|
624
|
+
|
|
625
|
+
def _resolve_doc_from_state(self, st):
|
|
626
|
+
"""
|
|
627
|
+
Accepts either:
|
|
628
|
+
- dict payload (preferred): may include doc_uid, base_doc_uid, file_path, doc_ptr/mask_doc_ptr
|
|
629
|
+
- int legacy pointer
|
|
630
|
+
Tries, in order: doc_uid → base_doc_uid → legacy ptr → file_path.
|
|
631
|
+
"""
|
|
632
|
+
# If called with an int, treat it as a raw pointer
|
|
633
|
+
if isinstance(st, int):
|
|
634
|
+
return self._resolve_doc_ptr(st)
|
|
635
|
+
|
|
636
|
+
if not isinstance(st, dict):
|
|
637
|
+
return None
|
|
638
|
+
|
|
639
|
+
# 1) Prefer UIDs
|
|
640
|
+
doc_uid = st.get("doc_uid")
|
|
641
|
+
base_uid = st.get("base_doc_uid")
|
|
642
|
+
if doc_uid and hasattr(self.docman, "get_document_by_uid"):
|
|
643
|
+
d = self.docman.get_document_by_uid(doc_uid)
|
|
644
|
+
if d is not None:
|
|
645
|
+
return d
|
|
646
|
+
if base_uid and hasattr(self.docman, "get_document_by_uid"):
|
|
647
|
+
d = self.docman.get_document_by_uid(base_uid)
|
|
648
|
+
if d is not None:
|
|
649
|
+
return d
|
|
650
|
+
|
|
651
|
+
# 2) Legacy pointer
|
|
652
|
+
ptr = st.get("doc_ptr") or st.get("mask_doc_ptr") # mask payloads may use mask_doc_ptr
|
|
653
|
+
if isinstance(ptr, int):
|
|
654
|
+
d = self._resolve_doc_ptr(ptr)
|
|
655
|
+
if d is not None:
|
|
656
|
+
return d
|
|
657
|
+
|
|
658
|
+
# 3) Last-ditch: file path match
|
|
659
|
+
fp = (st.get("file_path") or "").strip()
|
|
660
|
+
if fp:
|
|
661
|
+
try:
|
|
662
|
+
for d in self.docman.all_documents():
|
|
663
|
+
meta = getattr(d, "metadata", {}) or {}
|
|
664
|
+
if meta.get("file_path") == fp:
|
|
665
|
+
return d
|
|
666
|
+
except Exception:
|
|
667
|
+
pass
|
|
668
|
+
|
|
669
|
+
return None
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def _merge_and_push(self):
|
|
673
|
+
vw = self.current_view()
|
|
674
|
+
if not vw:
|
|
675
|
+
return
|
|
676
|
+
|
|
677
|
+
# No layers? Nothing to do.
|
|
678
|
+
layers = list(getattr(vw, "_layers", []) or [])
|
|
679
|
+
if not layers:
|
|
680
|
+
QMessageBox.information(self, "Layers", "There are no layers to merge.")
|
|
681
|
+
return
|
|
682
|
+
|
|
683
|
+
try:
|
|
684
|
+
# Base image from the current view's document
|
|
685
|
+
base_doc = getattr(vw, "document", None)
|
|
686
|
+
if base_doc is None or getattr(base_doc, "image", None) is None:
|
|
687
|
+
QMessageBox.warning(self, "Layers", "No base image available for this view.")
|
|
688
|
+
return
|
|
689
|
+
|
|
690
|
+
base_img = base_doc.image
|
|
691
|
+
merged = composite_stack(base_img, layers)
|
|
692
|
+
if merged is None:
|
|
693
|
+
QMessageBox.warning(self, "Layers", "Composite failed (empty result).")
|
|
694
|
+
return
|
|
695
|
+
|
|
696
|
+
# Push into the document as an undoable edit
|
|
697
|
+
# (assumes document.apply_edit accepts float [0..1] or handles dtype internally)
|
|
698
|
+
meta = dict(getattr(base_doc, "metadata", {}) or {})
|
|
699
|
+
meta["step_name"] = "Layers Merge"
|
|
700
|
+
base_doc.apply_edit(merged.copy(), metadata=meta, step_name="Layers Merge")
|
|
701
|
+
|
|
702
|
+
# Clear layers and update live preview
|
|
703
|
+
vw._layers = []
|
|
704
|
+
vw._reinstall_layer_watchers()
|
|
705
|
+
self._rebuild_list()
|
|
706
|
+
vw.apply_layer_stack([])
|
|
707
|
+
|
|
708
|
+
# Nice confirmation
|
|
709
|
+
QMessageBox.information(self, "Layers",
|
|
710
|
+
"Merged visible layers and pushed the result to the current view.")
|
|
711
|
+
except Exception as ex:
|
|
712
|
+
print("[LayersDock] merge error:", ex)
|
|
713
|
+
QMessageBox.critical(self, "Layers", f"Merge failed:\n{ex}")
|
|
714
|
+
|