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,1619 @@
|
|
|
1
|
+
# pro/gui/mixins/toolbar_mixin.py
|
|
2
|
+
"""
|
|
3
|
+
Toolbar and action management mixin for AstroSuiteProMainWindow.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
import json
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from PyQt6.QtCore import Qt, QTimer, QUrl
|
|
10
|
+
from PyQt6.QtGui import QAction, QActionGroup, QIcon, QKeySequence, QDesktopServices
|
|
11
|
+
from PyQt6.QtWidgets import QMenu, QToolButton
|
|
12
|
+
|
|
13
|
+
from PyQt6.QtCore import QElapsedTimer
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
# Import icon paths - these are needed at runtime
|
|
20
|
+
from setiastro.saspro.resources import (
|
|
21
|
+
icon_path, green_path, neutral_path, whitebalance_path,
|
|
22
|
+
morpho_path, clahe_path, starnet_path, staradd_path, LExtract_path,
|
|
23
|
+
LInsert_path, rgbcombo_path, rgbextract_path, graxperticon_path,
|
|
24
|
+
cropicon_path, openfile_path, abeicon_path, undoicon_path, redoicon_path,
|
|
25
|
+
blastericon_path, hdr_path, invert_path, fliphorizontal_path,
|
|
26
|
+
flipvertical_path, rotateclockwise_path, rotatecounterclockwise_path,rotatearbitrary_path,
|
|
27
|
+
rotate180_path, maskcreate_path, maskapply_path, maskremove_path,
|
|
28
|
+
pixelmath_path, histogram_path, mosaic_path, rescale_path, staralign_path,
|
|
29
|
+
platesolve_path, psf_path, supernova_path, starregistration_path,
|
|
30
|
+
stacking_path, pedestal_icon_path, starspike_path, astrospike_path,
|
|
31
|
+
signature_icon_path, livestacking_path, convoicon_path, spcc_icon_path,
|
|
32
|
+
exoicon_path, peeker_icon, dse_icon_path, isophote_path, statstretch_path,
|
|
33
|
+
starstretch_path, curves_path, disk_path, uhs_path, blink_path, ppp_path,
|
|
34
|
+
nbtorgb_path, freqsep_path, multiscale_decomp_path, contsub_path, halo_path, cosmic_path,
|
|
35
|
+
satellite_path, imagecombine_path, wims_path, wimi_path, linearfit_path,
|
|
36
|
+
debayer_path, aberration_path, functionbundles_path, viewbundles_path,
|
|
37
|
+
selectivecolor_path, rgbalign_path,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Import shortcuts module
|
|
41
|
+
from setiastro.saspro.shortcuts import DraggableToolBar, ShortcutManager
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ToolbarMixin:
|
|
45
|
+
"""
|
|
46
|
+
Mixin for toolbar and action management.
|
|
47
|
+
|
|
48
|
+
Provides methods for creating and managing toolbars and actions.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
# Placeholder methods for tool openers (implemented in main window)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _sync_link_action_state(self):
|
|
55
|
+
"""Synchronize the link views action state."""
|
|
56
|
+
if not hasattr(self, "_link_views_enabled"):
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
if hasattr(self, "action_link_views"):
|
|
60
|
+
self.action_link_views.setChecked(self._link_views_enabled)
|
|
61
|
+
|
|
62
|
+
def _find_action_by_cid(self, command_id: str) -> QAction | None:
|
|
63
|
+
"""
|
|
64
|
+
Find an action by its command ID.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
command_id: The command identifier string
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
The QAction if found, None otherwise
|
|
71
|
+
"""
|
|
72
|
+
for action in self.findChildren(QAction):
|
|
73
|
+
if getattr(action, "command_id", None) == command_id:
|
|
74
|
+
return action
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
def _init_toolbar(self):
|
|
78
|
+
# View toolbar (Undo / Redo / Display-Stretch)
|
|
79
|
+
tb = DraggableToolBar(self.tr("View"), self)
|
|
80
|
+
tb.setObjectName("View")
|
|
81
|
+
tb.setSettingsKey("Toolbar/View")
|
|
82
|
+
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb)
|
|
83
|
+
|
|
84
|
+
tb.addAction(self.act_open)
|
|
85
|
+
tb.addAction(self.act_save)
|
|
86
|
+
tb.addSeparator()
|
|
87
|
+
tb.addAction(self.act_undo)
|
|
88
|
+
tb.addAction(self.act_redo)
|
|
89
|
+
tb.addSeparator()
|
|
90
|
+
|
|
91
|
+
# Put Display-Stretch on the bar first so we can attach a menu to its button
|
|
92
|
+
tb.addAction(self.act_autostretch)
|
|
93
|
+
tb.addAction(self.act_zoom_out)
|
|
94
|
+
tb.addAction(self.act_zoom_in)
|
|
95
|
+
tb.addAction(self.act_zoom_1_1)
|
|
96
|
+
tb.addAction(self.act_zoom_fit)
|
|
97
|
+
|
|
98
|
+
# Style the autostretch button + add menu
|
|
99
|
+
btn = tb.widgetForAction(self.act_autostretch)
|
|
100
|
+
if isinstance(btn, QToolButton):
|
|
101
|
+
menu = QMenu(btn)
|
|
102
|
+
menu.addAction(self.act_stretch_linked)
|
|
103
|
+
menu.addAction(self.act_hardstretch)
|
|
104
|
+
|
|
105
|
+
# NEW: advanced controls + presets
|
|
106
|
+
menu.addSeparator()
|
|
107
|
+
menu.addAction(self.act_display_target)
|
|
108
|
+
menu.addAction(self.act_display_sigma)
|
|
109
|
+
|
|
110
|
+
presets = QMenu("Presets", menu)
|
|
111
|
+
a_norm = presets.addAction("Normal (target 0.30, σ 5)")
|
|
112
|
+
a_midy = presets.addAction("Mid (target 0.40, σ 3)")
|
|
113
|
+
a_hard = presets.addAction("Hard (target 0.50, σ 2)")
|
|
114
|
+
menu.addMenu(presets)
|
|
115
|
+
menu.addSeparator()
|
|
116
|
+
menu.addAction(self.act_bake_display_stretch)
|
|
117
|
+
|
|
118
|
+
# push numbers to the active view and (optionally) turn on autostretch
|
|
119
|
+
def _apply_preset(t, s, also_enable=True):
|
|
120
|
+
self.settings.setValue("display/target", float(t))
|
|
121
|
+
self.settings.setValue("display/sigma", float(s))
|
|
122
|
+
sw = self.mdi.activeSubWindow()
|
|
123
|
+
if not sw:
|
|
124
|
+
return
|
|
125
|
+
view = sw.widget()
|
|
126
|
+
if hasattr(view, "set_autostretch_target"):
|
|
127
|
+
view.set_autostretch_target(float(t))
|
|
128
|
+
if hasattr(view, "set_autostretch_sigma"):
|
|
129
|
+
view.set_autostretch_sigma(float(s))
|
|
130
|
+
if also_enable and not getattr(view, "autostretch_enabled", False):
|
|
131
|
+
if hasattr(view, "set_autostretch"):
|
|
132
|
+
view.set_autostretch(True)
|
|
133
|
+
self._sync_autostretch_action(True)
|
|
134
|
+
|
|
135
|
+
a_norm.triggered.connect(lambda: _apply_preset(0.30, 5.0))
|
|
136
|
+
a_midy.triggered.connect(lambda: _apply_preset(0.40, 3.0))
|
|
137
|
+
a_hard.triggered.connect(lambda: _apply_preset(0.50, 2.0))
|
|
138
|
+
|
|
139
|
+
btn.setMenu(menu)
|
|
140
|
+
btn.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
|
|
141
|
+
|
|
142
|
+
btn.setStyleSheet("""
|
|
143
|
+
QToolButton { color: #dcdcdc; }
|
|
144
|
+
QToolButton:checked { color: #DAA520; font-weight: 600; }
|
|
145
|
+
""")
|
|
146
|
+
|
|
147
|
+
btn_fit = tb.widgetForAction(self.act_zoom_fit)
|
|
148
|
+
if isinstance(btn_fit, QToolButton):
|
|
149
|
+
fit_menu = QMenu(btn_fit)
|
|
150
|
+
|
|
151
|
+
# Use the existing action created in _create_actions()
|
|
152
|
+
fit_menu.addAction(self.act_auto_fit_resize)
|
|
153
|
+
|
|
154
|
+
# (Optional) make sure it reflects current flag at startup
|
|
155
|
+
self.act_auto_fit_resize.blockSignals(True)
|
|
156
|
+
try:
|
|
157
|
+
self.act_auto_fit_resize.setChecked(bool(self._auto_fit_on_resize))
|
|
158
|
+
finally:
|
|
159
|
+
self.act_auto_fit_resize.blockSignals(False)
|
|
160
|
+
|
|
161
|
+
btn_fit.setMenu(fit_menu)
|
|
162
|
+
btn_fit.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
|
|
163
|
+
|
|
164
|
+
btn_fit.setStyleSheet("""
|
|
165
|
+
QToolButton { color: #dcdcdc; }
|
|
166
|
+
QToolButton:checked { color: #DAA520; font-weight: 600; }
|
|
167
|
+
""")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# Make sure the visual state matches the flag at startup
|
|
171
|
+
self._restore_toolbar_order(tb, "Toolbar/View")
|
|
172
|
+
self._restore_toolbar_memberships() # (or keep your existing placement, but the bind must be AFTER it)
|
|
173
|
+
self._bind_view_toolbar_menus(tb)
|
|
174
|
+
self._sync_fit_auto_visual()
|
|
175
|
+
# Apply hidden state immediately after order restore (prevents flash)
|
|
176
|
+
try:
|
|
177
|
+
tb.apply_hidden_state()
|
|
178
|
+
except Exception:
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
# Functions toolbar
|
|
182
|
+
tb_fn = DraggableToolBar(self.tr("Functions"), self)
|
|
183
|
+
tb_fn.setObjectName("Functions")
|
|
184
|
+
tb_fn.setSettingsKey("Toolbar/Functions")
|
|
185
|
+
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_fn)
|
|
186
|
+
|
|
187
|
+
tb_fn.addAction(self.act_crop)
|
|
188
|
+
tb_fn.addAction(self.act_histogram)
|
|
189
|
+
tb_fn.addAction(self.act_pedestal)
|
|
190
|
+
tb_fn.addAction(self.act_linear_fit)
|
|
191
|
+
tb_fn.addAction(self.act_stat_stretch)
|
|
192
|
+
tb_fn.addAction(self.act_star_stretch)
|
|
193
|
+
tb_fn.addAction(self.act_curves)
|
|
194
|
+
tb_fn.addAction(self.act_ghs)
|
|
195
|
+
tb_fn.addAction(self.act_abe)
|
|
196
|
+
tb_fn.addAction(self.act_graxpert)
|
|
197
|
+
tb_fn.addAction(self.act_remove_stars)
|
|
198
|
+
tb_fn.addAction(self.act_add_stars)
|
|
199
|
+
tb_fn.addAction(self.act_background_neutral)
|
|
200
|
+
tb_fn.addAction(self.act_white_balance)
|
|
201
|
+
tb_fn.addAction(self.act_sfcc)
|
|
202
|
+
tb_fn.addAction(self.act_remove_green)
|
|
203
|
+
tb_fn.addAction(self.act_convo)
|
|
204
|
+
tb_fn.addAction(self.act_extract_luma)
|
|
205
|
+
|
|
206
|
+
#btn_luma = tb_fn.widgetForAction(self.act_extract_luma)
|
|
207
|
+
#if isinstance(btn_luma, QToolButton):
|
|
208
|
+
# luma_menu = QMenu(btn_luma)
|
|
209
|
+
# luma_menu.addActions(self._luma_group.actions())
|
|
210
|
+
# btn_luma.setMenu(luma_menu)
|
|
211
|
+
# btn_luma.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
|
|
212
|
+
# btn_luma.setStyleSheet("""
|
|
213
|
+
# QToolButton { color: #dcdcdc; }
|
|
214
|
+
# QToolButton:pressed, QToolButton:checked { color: #DAA520; font-weight: 600; }
|
|
215
|
+
# """)
|
|
216
|
+
|
|
217
|
+
tb_fn.addAction(self.act_recombine_luma)
|
|
218
|
+
tb_fn.addAction(self.act_rgb_extract)
|
|
219
|
+
tb_fn.addAction(self.act_rgb_combine)
|
|
220
|
+
tb_fn.addAction(self.act_blemish)
|
|
221
|
+
tb_fn.addAction(self.act_wavescale_hdr)
|
|
222
|
+
tb_fn.addAction(self.act_wavescale_de)
|
|
223
|
+
tb_fn.addAction(self.act_clahe)
|
|
224
|
+
tb_fn.addAction(self.act_morphology)
|
|
225
|
+
tb_fn.addAction(self.act_pixelmath)
|
|
226
|
+
tb_fn.addAction(self.act_signature)
|
|
227
|
+
tb_fn.addAction(self.act_halobgon)
|
|
228
|
+
|
|
229
|
+
self._restore_toolbar_order(tb_fn, "Toolbar/Functions")
|
|
230
|
+
try:
|
|
231
|
+
tb_fn.apply_hidden_state()
|
|
232
|
+
except Exception:
|
|
233
|
+
pass
|
|
234
|
+
|
|
235
|
+
tbCosmic = DraggableToolBar(self.tr("Cosmic Clarity"), self)
|
|
236
|
+
tbCosmic.setObjectName("Cosmic Clarity")
|
|
237
|
+
tbCosmic.setSettingsKey("Toolbar/Cosmic")
|
|
238
|
+
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tbCosmic)
|
|
239
|
+
|
|
240
|
+
tbCosmic.addAction(self.actAberrationAI)
|
|
241
|
+
tbCosmic.addAction(self.actCosmicUI)
|
|
242
|
+
tbCosmic.addAction(self.actCosmicSat)
|
|
243
|
+
|
|
244
|
+
self._restore_toolbar_order(tbCosmic, "Toolbar/Cosmic")
|
|
245
|
+
try:
|
|
246
|
+
tbCosmic.apply_hidden_state()
|
|
247
|
+
except Exception:
|
|
248
|
+
pass
|
|
249
|
+
|
|
250
|
+
tb_tl = DraggableToolBar(self.tr("Tools"), self)
|
|
251
|
+
tb_tl.setObjectName("Tools")
|
|
252
|
+
tb_tl.setSettingsKey("Toolbar/Tools")
|
|
253
|
+
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_tl)
|
|
254
|
+
|
|
255
|
+
tb_tl.addAction(self.act_blink) # Tools start here; Blink shows with QIcon(blink_path)
|
|
256
|
+
tb_tl.addAction(self.act_ppp) # Perfect Palette Picker
|
|
257
|
+
tb_tl.addAction(self.act_nbtorgb)
|
|
258
|
+
tb_tl.addAction(self.act_selective_color)
|
|
259
|
+
tb_tl.addAction(self.act_freqsep)
|
|
260
|
+
tb_tl.addAction(self.act_multiscale_decomp)
|
|
261
|
+
tb_tl.addAction(self.act_contsub)
|
|
262
|
+
tb_tl.addAction(self.act_image_combine)
|
|
263
|
+
|
|
264
|
+
self._restore_toolbar_order(tb_tl, "Toolbar/Tools")
|
|
265
|
+
try:
|
|
266
|
+
tb_tl.apply_hidden_state()
|
|
267
|
+
except Exception:
|
|
268
|
+
pass
|
|
269
|
+
|
|
270
|
+
tb_geom = DraggableToolBar(self.tr("Geometry"), self)
|
|
271
|
+
tb_geom.setObjectName("Geometry")
|
|
272
|
+
tb_geom.setSettingsKey("Toolbar/Geometry")
|
|
273
|
+
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_geom)
|
|
274
|
+
|
|
275
|
+
tb_geom.addAction(self.act_geom_invert)
|
|
276
|
+
tb_geom.addSeparator()
|
|
277
|
+
tb_geom.addAction(self.act_geom_flip_h)
|
|
278
|
+
tb_geom.addAction(self.act_geom_flip_v)
|
|
279
|
+
tb_geom.addSeparator()
|
|
280
|
+
tb_geom.addAction(self.act_geom_rot_cw)
|
|
281
|
+
tb_geom.addAction(self.act_geom_rot_ccw)
|
|
282
|
+
tb_geom.addAction(self.act_geom_rot_180)
|
|
283
|
+
tb_geom.addAction(self.act_geom_rot_any)
|
|
284
|
+
tb_geom.addSeparator()
|
|
285
|
+
tb_geom.addAction(self.act_geom_rescale)
|
|
286
|
+
tb_geom.addSeparator()
|
|
287
|
+
tb_geom.addAction(self.act_debayer)
|
|
288
|
+
|
|
289
|
+
self._restore_toolbar_order(tb_geom, "Toolbar/Geometry")
|
|
290
|
+
try:
|
|
291
|
+
tb_geom.apply_hidden_state()
|
|
292
|
+
except Exception:
|
|
293
|
+
pass
|
|
294
|
+
|
|
295
|
+
tb_star = DraggableToolBar(self.tr("Star Stuff"), self)
|
|
296
|
+
tb_star.setObjectName("Star Stuff")
|
|
297
|
+
tb_star.setSettingsKey("Toolbar/StarStuff")
|
|
298
|
+
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_star)
|
|
299
|
+
|
|
300
|
+
tb_star.addAction(self.act_image_peeker)
|
|
301
|
+
tb_star.addAction(self.act_psf_viewer)
|
|
302
|
+
tb_star.addAction(self.act_stacking_suite)
|
|
303
|
+
tb_star.addAction(self.act_live_stacking)
|
|
304
|
+
tb_star.addAction(self.act_plate_solve)
|
|
305
|
+
tb_star.addAction(self.act_star_align)
|
|
306
|
+
tb_star.addAction(self.act_star_register)
|
|
307
|
+
tb_star.addAction(self.act_rgb_align)
|
|
308
|
+
tb_star.addAction(self.act_mosaic_master)
|
|
309
|
+
tb_star.addAction(self.act_supernova_hunter)
|
|
310
|
+
tb_star.addAction(self.act_star_spikes)
|
|
311
|
+
tb_star.addAction(self.act_astrospike)
|
|
312
|
+
tb_star.addAction(self.act_exo_detector)
|
|
313
|
+
tb_star.addAction(self.act_isophote)
|
|
314
|
+
|
|
315
|
+
self._restore_toolbar_order(tb_star, "Toolbar/StarStuff")
|
|
316
|
+
try:
|
|
317
|
+
tb_star.apply_hidden_state()
|
|
318
|
+
except Exception:
|
|
319
|
+
pass
|
|
320
|
+
|
|
321
|
+
tb_msk = DraggableToolBar(self.tr("Masks"), self)
|
|
322
|
+
tb_msk.setObjectName("Masks")
|
|
323
|
+
tb_msk.setSettingsKey("Toolbar/Masks")
|
|
324
|
+
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_msk)
|
|
325
|
+
|
|
326
|
+
tb_msk.addAction(self.act_create_mask)
|
|
327
|
+
tb_msk.addAction(self.act_apply_mask)
|
|
328
|
+
tb_msk.addAction(self.act_remove_mask)
|
|
329
|
+
|
|
330
|
+
self._restore_toolbar_order(tb_msk, "Toolbar/Masks")
|
|
331
|
+
try:
|
|
332
|
+
tb_msk.apply_hidden_state()
|
|
333
|
+
except Exception:
|
|
334
|
+
pass
|
|
335
|
+
|
|
336
|
+
tb_wim = DraggableToolBar(self.tr("What's In My..."), self)
|
|
337
|
+
tb_wim.setObjectName("What's In My...")
|
|
338
|
+
tb_wim.setSettingsKey("Toolbar/WhatsInMy")
|
|
339
|
+
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_wim)
|
|
340
|
+
|
|
341
|
+
tb_wim.addAction(self.act_whats_in_my_sky)
|
|
342
|
+
tb_wim.addAction(self.act_wimi)
|
|
343
|
+
|
|
344
|
+
self._restore_toolbar_order(tb_wim, "Toolbar/WhatsInMy")
|
|
345
|
+
try:
|
|
346
|
+
tb_wim.apply_hidden_state()
|
|
347
|
+
except Exception:
|
|
348
|
+
pass
|
|
349
|
+
|
|
350
|
+
tb_bundle = DraggableToolBar(self.tr("Bundles"), self)
|
|
351
|
+
tb_bundle.setObjectName("Bundles")
|
|
352
|
+
tb_bundle.setSettingsKey("Toolbar/Bundles")
|
|
353
|
+
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_bundle)
|
|
354
|
+
|
|
355
|
+
tb_bundle.addAction(self.act_view_bundles)
|
|
356
|
+
tb_bundle.addAction(self.act_function_bundles)
|
|
357
|
+
|
|
358
|
+
self._restore_toolbar_order(tb_bundle, "Toolbar/Bundles")
|
|
359
|
+
try:
|
|
360
|
+
tb_bundle.apply_hidden_state()
|
|
361
|
+
except Exception:
|
|
362
|
+
pass
|
|
363
|
+
|
|
364
|
+
# This can move actions between toolbars, so do it after each toolbar has its base order restored.
|
|
365
|
+
self._restore_toolbar_memberships()
|
|
366
|
+
|
|
367
|
+
# Re-apply hidden state AFTER memberships (actions may have moved toolbars).
|
|
368
|
+
# This also guarantees correctness even if any toolbar was rebuilt/adjusted internally.
|
|
369
|
+
for _tb in self.findChildren(DraggableToolBar):
|
|
370
|
+
try:
|
|
371
|
+
_tb.apply_hidden_state()
|
|
372
|
+
except Exception:
|
|
373
|
+
pass
|
|
374
|
+
|
|
375
|
+
# Rebind ALL dropdowns after reorder/membership moves
|
|
376
|
+
self._rebind_view_dropdowns()
|
|
377
|
+
self._rebind_extract_luma_dropdown()
|
|
378
|
+
|
|
379
|
+
def _toolbar_containing_action(self, action: QAction):
|
|
380
|
+
from setiastro.saspro.shortcuts import DraggableToolBar
|
|
381
|
+
for tb in self.findChildren(DraggableToolBar):
|
|
382
|
+
if action in tb.actions():
|
|
383
|
+
return tb
|
|
384
|
+
return None
|
|
385
|
+
|
|
386
|
+
def _rebind_extract_luma_dropdown(self):
|
|
387
|
+
from PyQt6.QtWidgets import QMenu, QToolButton
|
|
388
|
+
from setiastro.saspro.luminancerecombine import LUMA_PROFILES
|
|
389
|
+
|
|
390
|
+
tb = self._toolbar_containing_action(self.act_extract_luma)
|
|
391
|
+
if not tb:
|
|
392
|
+
return
|
|
393
|
+
|
|
394
|
+
btn = tb.widgetForAction(self.act_extract_luma)
|
|
395
|
+
if not isinstance(btn, QToolButton):
|
|
396
|
+
return
|
|
397
|
+
|
|
398
|
+
menu = QMenu(btn)
|
|
399
|
+
|
|
400
|
+
# Ensure cache exists (created in _create_actions(), but be defensive)
|
|
401
|
+
if not hasattr(self, "_luma_sensor_actions"):
|
|
402
|
+
self._luma_sensor_actions = {} # key -> QAction
|
|
403
|
+
|
|
404
|
+
cur_method = str(getattr(self, "luma_method", "rec709"))
|
|
405
|
+
|
|
406
|
+
# ============================================================
|
|
407
|
+
# PATCH SITE #1 (Standard menu) <-- THIS IS WHERE "C" GOES
|
|
408
|
+
# Find your old block:
|
|
409
|
+
#
|
|
410
|
+
# # ---- Standard (use your QActionGroup, keep it simple) ----
|
|
411
|
+
# if getattr(self, "_luma_group", None) is not None:
|
|
412
|
+
# std_menu = QMenu(self.tr("Standard"), menu)
|
|
413
|
+
# std_menu.addActions(self._luma_group.actions())
|
|
414
|
+
# menu.addMenu(std_menu)
|
|
415
|
+
#
|
|
416
|
+
# Replace it with the block below.
|
|
417
|
+
# ============================================================
|
|
418
|
+
if getattr(self, "_luma_group", None) is not None:
|
|
419
|
+
# Sync standard checked states from self.luma_method
|
|
420
|
+
for a in self._luma_group.actions():
|
|
421
|
+
data = str(a.data() or "")
|
|
422
|
+
if data.startswith("sensor:"):
|
|
423
|
+
continue # standards only in Standard menu
|
|
424
|
+
a.blockSignals(True)
|
|
425
|
+
try:
|
|
426
|
+
a.setChecked(data == cur_method)
|
|
427
|
+
finally:
|
|
428
|
+
a.blockSignals(False)
|
|
429
|
+
|
|
430
|
+
# Add ONLY standard actions to the Standard menu (exclude sensors)
|
|
431
|
+
std_menu = QMenu(self.tr("Standard"), menu)
|
|
432
|
+
for a in self._luma_group.actions():
|
|
433
|
+
data = str(a.data() or "")
|
|
434
|
+
if data.startswith("sensor:"):
|
|
435
|
+
continue
|
|
436
|
+
std_menu.addAction(a)
|
|
437
|
+
menu.addMenu(std_menu)
|
|
438
|
+
|
|
439
|
+
# ---- Sensors (nested by category path) ----
|
|
440
|
+
sensors_root = QMenu(self.tr("Sensors"), menu)
|
|
441
|
+
|
|
442
|
+
# Build tree of submenus keyed by "Sensors/<path>"
|
|
443
|
+
submenu_cache: dict[str, QMenu] = {}
|
|
444
|
+
|
|
445
|
+
def get_or_make_path(root_menu: QMenu, path: str) -> QMenu:
|
|
446
|
+
parts = [p for p in path.split("/") if p.strip()]
|
|
447
|
+
cur = root_menu
|
|
448
|
+
acc = ""
|
|
449
|
+
for part in parts:
|
|
450
|
+
acc = f"{acc}/{part}" if acc else part
|
|
451
|
+
if acc not in submenu_cache:
|
|
452
|
+
sm = QMenu(self.tr(part), cur)
|
|
453
|
+
cur.addMenu(sm)
|
|
454
|
+
submenu_cache[acc] = sm
|
|
455
|
+
cur = submenu_cache[acc]
|
|
456
|
+
return cur
|
|
457
|
+
|
|
458
|
+
any_sensor = False
|
|
459
|
+
for key, prof in LUMA_PROFILES.items():
|
|
460
|
+
if not str(key).startswith("sensor:"):
|
|
461
|
+
continue
|
|
462
|
+
|
|
463
|
+
cat = str(prof.get("category", "Sensors/Other"))
|
|
464
|
+
group_path = cat.split("Sensors/", 1)[1] if "Sensors/" in cat else cat
|
|
465
|
+
parent_menu = get_or_make_path(sensors_root, group_path)
|
|
466
|
+
|
|
467
|
+
display_name = key.split("sensor:", 1)[1].strip()
|
|
468
|
+
desc = str(prof.get("description", display_name))
|
|
469
|
+
info = str(prof.get("info", "")).strip()
|
|
470
|
+
|
|
471
|
+
# ============================================================
|
|
472
|
+
# PATCH SITE #2 (Sensors become mutually exclusive with standards)
|
|
473
|
+
# - Cache the QAction so we don't pile up connections/actions
|
|
474
|
+
# - Add it to the SAME QActionGroup (exclusive) as standards
|
|
475
|
+
# - Do NOT use _pick_sensor; rely on QActionGroup.triggered
|
|
476
|
+
# ============================================================
|
|
477
|
+
act = self._luma_sensor_actions.get(key)
|
|
478
|
+
if act is None:
|
|
479
|
+
act = parent_menu.addAction(self.tr(display_name))
|
|
480
|
+
act.setCheckable(True)
|
|
481
|
+
act.setData(key) # IMPORTANT: enables group.triggered to set luma_method
|
|
482
|
+
|
|
483
|
+
# Put sensors into the SAME exclusive group so selecting one
|
|
484
|
+
# deselects everything else (standards and other sensors).
|
|
485
|
+
if getattr(self, "_luma_group", None) is not None:
|
|
486
|
+
self._luma_group.addAction(act)
|
|
487
|
+
|
|
488
|
+
self._luma_sensor_actions[key] = act
|
|
489
|
+
else:
|
|
490
|
+
# Reuse action, but re-add it to the correct submenu location
|
|
491
|
+
parent_menu.addAction(act)
|
|
492
|
+
act.setText(self.tr(display_name))
|
|
493
|
+
|
|
494
|
+
# Update UI info each bind (safe if translations change)
|
|
495
|
+
if info:
|
|
496
|
+
act.setStatusTip(info)
|
|
497
|
+
act.setToolTip(f"{desc}\n{info}")
|
|
498
|
+
else:
|
|
499
|
+
act.setToolTip(desc)
|
|
500
|
+
|
|
501
|
+
# Checked state reflects current selection
|
|
502
|
+
act.blockSignals(True)
|
|
503
|
+
try:
|
|
504
|
+
act.setChecked(cur_method == str(key))
|
|
505
|
+
finally:
|
|
506
|
+
act.blockSignals(False)
|
|
507
|
+
|
|
508
|
+
any_sensor = True
|
|
509
|
+
|
|
510
|
+
if any_sensor:
|
|
511
|
+
menu.addMenu(sensors_root)
|
|
512
|
+
|
|
513
|
+
btn.setMenu(menu)
|
|
514
|
+
btn.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
|
|
515
|
+
btn.setStyleSheet("""
|
|
516
|
+
QToolButton { color: #dcdcdc; }
|
|
517
|
+
QToolButton:pressed, QToolButton:checked { color: #DAA520; font-weight: 600; }
|
|
518
|
+
""")
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def _rebind_view_dropdowns(self):
|
|
522
|
+
"""
|
|
523
|
+
Rebind dropdown menus for Display-Stretch + Fit buttons
|
|
524
|
+
on whatever toolbar those actions currently live in.
|
|
525
|
+
Call this AFTER all restore/reorder/membership moves.
|
|
526
|
+
"""
|
|
527
|
+
# ---- Display-Stretch dropdown ----
|
|
528
|
+
tb = self._toolbar_containing_action(self.act_autostretch)
|
|
529
|
+
if tb:
|
|
530
|
+
btn = tb.widgetForAction(self.act_autostretch)
|
|
531
|
+
if isinstance(btn, QToolButton):
|
|
532
|
+
menu = QMenu(btn)
|
|
533
|
+
menu.addAction(self.act_stretch_linked)
|
|
534
|
+
menu.addAction(self.act_hardstretch)
|
|
535
|
+
|
|
536
|
+
menu.addSeparator()
|
|
537
|
+
menu.addAction(self.act_display_target)
|
|
538
|
+
menu.addAction(self.act_display_sigma)
|
|
539
|
+
|
|
540
|
+
presets = QMenu(self.tr("Presets"), menu)
|
|
541
|
+
a_norm = presets.addAction(self.tr("Normal (target 0.30, σ 5)"))
|
|
542
|
+
a_midy = presets.addAction(self.tr("Mid (target 0.40, σ 3)"))
|
|
543
|
+
a_hard = presets.addAction(self.tr("Hard (target 0.50, σ 2)"))
|
|
544
|
+
menu.addMenu(presets)
|
|
545
|
+
|
|
546
|
+
menu.addSeparator()
|
|
547
|
+
menu.addAction(self.act_bake_display_stretch)
|
|
548
|
+
|
|
549
|
+
def _apply_preset(t, s, also_enable=True):
|
|
550
|
+
self.settings.setValue("display/target", float(t))
|
|
551
|
+
self.settings.setValue("display/sigma", float(s))
|
|
552
|
+
sw = self.mdi.activeSubWindow()
|
|
553
|
+
if not sw:
|
|
554
|
+
return
|
|
555
|
+
view = sw.widget()
|
|
556
|
+
if hasattr(view, "set_autostretch_target"):
|
|
557
|
+
view.set_autostretch_target(float(t))
|
|
558
|
+
if hasattr(view, "set_autostretch_sigma"):
|
|
559
|
+
view.set_autostretch_sigma(float(s))
|
|
560
|
+
if also_enable and not getattr(view, "autostretch_enabled", False):
|
|
561
|
+
if hasattr(view, "set_autostretch"):
|
|
562
|
+
view.set_autostretch(True)
|
|
563
|
+
self._sync_autostretch_action(True)
|
|
564
|
+
|
|
565
|
+
a_norm.triggered.connect(lambda: _apply_preset(0.30, 5.0))
|
|
566
|
+
a_midy.triggered.connect(lambda: _apply_preset(0.40, 3.0))
|
|
567
|
+
a_hard.triggered.connect(lambda: _apply_preset(0.50, 2.0))
|
|
568
|
+
|
|
569
|
+
btn.setMenu(menu)
|
|
570
|
+
btn.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
|
|
571
|
+
|
|
572
|
+
# ---- Fit dropdown ----
|
|
573
|
+
tb_fit = self._toolbar_containing_action(self.act_zoom_fit)
|
|
574
|
+
if tb_fit:
|
|
575
|
+
btn_fit = tb_fit.widgetForAction(self.act_zoom_fit)
|
|
576
|
+
if isinstance(btn_fit, QToolButton):
|
|
577
|
+
fit_menu = QMenu(btn_fit)
|
|
578
|
+
fit_menu.addAction(self.act_auto_fit_resize) # use the real action
|
|
579
|
+
btn_fit.setMenu(fit_menu)
|
|
580
|
+
btn_fit.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def _bind_view_toolbar_menus(self, tb: DraggableToolBar):
|
|
584
|
+
# --- Display-Stretch menu ---
|
|
585
|
+
btn = tb.widgetForAction(self.act_autostretch)
|
|
586
|
+
if isinstance(btn, QToolButton):
|
|
587
|
+
menu = QMenu(btn)
|
|
588
|
+
menu.addAction(self.act_stretch_linked)
|
|
589
|
+
menu.addAction(self.act_hardstretch)
|
|
590
|
+
|
|
591
|
+
menu.addSeparator()
|
|
592
|
+
menu.addAction(self.act_display_target)
|
|
593
|
+
menu.addAction(self.act_display_sigma)
|
|
594
|
+
|
|
595
|
+
presets = QMenu(self.tr("Presets"), menu)
|
|
596
|
+
a_norm = presets.addAction(self.tr("Normal (target 0.30, σ 5)"))
|
|
597
|
+
a_midy = presets.addAction(self.tr("Mid (target 0.40, σ 3)"))
|
|
598
|
+
a_hard = presets.addAction(self.tr("Hard (target 0.50, σ 2)"))
|
|
599
|
+
menu.addMenu(presets)
|
|
600
|
+
menu.addSeparator()
|
|
601
|
+
menu.addAction(self.act_bake_display_stretch)
|
|
602
|
+
|
|
603
|
+
def _apply_preset(t, s, also_enable=True):
|
|
604
|
+
self.settings.setValue("display/target", float(t))
|
|
605
|
+
self.settings.setValue("display/sigma", float(s))
|
|
606
|
+
sw = self.mdi.activeSubWindow()
|
|
607
|
+
if not sw:
|
|
608
|
+
return
|
|
609
|
+
view = sw.widget()
|
|
610
|
+
if hasattr(view, "set_autostretch_target"):
|
|
611
|
+
view.set_autostretch_target(float(t))
|
|
612
|
+
if hasattr(view, "set_autostretch_sigma"):
|
|
613
|
+
view.set_autostretch_sigma(float(s))
|
|
614
|
+
if also_enable and not getattr(view, "autostretch_enabled", False):
|
|
615
|
+
if hasattr(view, "set_autostretch"):
|
|
616
|
+
view.set_autostretch(True)
|
|
617
|
+
self._sync_autostretch_action(True)
|
|
618
|
+
|
|
619
|
+
a_norm.triggered.connect(lambda: _apply_preset(0.30, 5.0))
|
|
620
|
+
a_midy.triggered.connect(lambda: _apply_preset(0.40, 3.0))
|
|
621
|
+
a_hard.triggered.connect(lambda: _apply_preset(0.50, 2.0))
|
|
622
|
+
|
|
623
|
+
btn.setMenu(menu)
|
|
624
|
+
btn.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
|
|
625
|
+
|
|
626
|
+
# --- Fit menu (Auto-fit checkbox) ---
|
|
627
|
+
btn_fit = tb.widgetForAction(self.act_zoom_fit)
|
|
628
|
+
if isinstance(btn_fit, QToolButton):
|
|
629
|
+
fit_menu = QMenu(btn_fit)
|
|
630
|
+
|
|
631
|
+
# IMPORTANT: use your existing action (don’t create a new one)
|
|
632
|
+
fit_menu.addAction(self.act_auto_fit_resize)
|
|
633
|
+
|
|
634
|
+
btn_fit.setMenu(fit_menu)
|
|
635
|
+
btn_fit.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def _create_actions(self):
|
|
639
|
+
# File actions
|
|
640
|
+
self.act_open = QAction(QIcon(openfile_path), self.tr("Open..."), self)
|
|
641
|
+
self.act_open.setIconVisibleInMenu(True)
|
|
642
|
+
self.act_open.setShortcut(QKeySequence.StandardKey.Open)
|
|
643
|
+
self.act_open.setStatusTip(self.tr("Open image(s)"))
|
|
644
|
+
self.act_open.triggered.connect(self.open_files)
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
self.act_project_new = QAction(self.tr("New Project"), self)
|
|
648
|
+
self.act_project_save = QAction(self.tr("Save Project..."), self)
|
|
649
|
+
self.act_project_load = QAction(self.tr("Load Project..."), self)
|
|
650
|
+
|
|
651
|
+
self.act_project_new.setStatusTip(self.tr("Close all views and clear shortcuts"))
|
|
652
|
+
self.act_project_save.setStatusTip(self.tr("Save all views, histories, and shortcuts to a .sas file"))
|
|
653
|
+
self.act_project_load.setStatusTip(self.tr("Load a .sas project (views, histories, shortcuts)"))
|
|
654
|
+
|
|
655
|
+
self.act_project_new.triggered.connect(self._new_project)
|
|
656
|
+
self.act_project_save.triggered.connect(self._save_project)
|
|
657
|
+
self.act_project_load.triggered.connect(self._load_project)
|
|
658
|
+
|
|
659
|
+
self.act_clear_views = QAction(self.tr("Clear All Views"), self)
|
|
660
|
+
self.act_clear_views.setStatusTip(self.tr("Close all views and documents, keep desktop shortcuts"))
|
|
661
|
+
# optional shortcut (pick anything you like or omit)
|
|
662
|
+
# self.act_clear_views.setShortcut(QKeySequence("Ctrl+Shift+W"))
|
|
663
|
+
self.act_clear_views.triggered.connect(self._clear_views_keep_shortcuts)
|
|
664
|
+
|
|
665
|
+
self.act_save = QAction(QIcon(disk_path), self.tr("Save As..."), self)
|
|
666
|
+
self.act_save.setIconVisibleInMenu(True)
|
|
667
|
+
self.act_save.setShortcut(QKeySequence.StandardKey.SaveAs)
|
|
668
|
+
self.act_save.setStatusTip(self.tr("Save the active image"))
|
|
669
|
+
self.act_save.triggered.connect(self.save_active)
|
|
670
|
+
|
|
671
|
+
self.act_exit = QAction(self.tr("&Exit"), self)
|
|
672
|
+
self.act_exit.setShortcut(QKeySequence.StandardKey.Quit) # Cmd+Q / Ctrl+Q
|
|
673
|
+
# Make it appear under the app menu on macOS automatically:
|
|
674
|
+
self.act_exit.setMenuRole(QAction.MenuRole.QuitRole)
|
|
675
|
+
self.act_exit.triggered.connect(self._on_exit)
|
|
676
|
+
|
|
677
|
+
self.act_cascade = QAction(self.tr("Cascade Views"), self)
|
|
678
|
+
self.act_cascade.setStatusTip(self.tr("Cascade all subwindows"))
|
|
679
|
+
self.act_cascade.setShortcut(QKeySequence("Ctrl+Shift+C"))
|
|
680
|
+
self.act_cascade.triggered.connect(self._cascade_views)
|
|
681
|
+
|
|
682
|
+
self.act_tile = QAction(self.tr("Tile Views"), self)
|
|
683
|
+
self.act_tile.setStatusTip(self.tr("Tile all subwindows"))
|
|
684
|
+
self.act_tile.setShortcut(QKeySequence("Ctrl+Shift+T"))
|
|
685
|
+
self.act_tile.triggered.connect(self._tile_views)
|
|
686
|
+
|
|
687
|
+
self.act_tile_vert = QAction(self.tr("Tile Vertically"), self)
|
|
688
|
+
self.act_tile_vert.setStatusTip(self.tr("Split the workspace into equal vertical columns"))
|
|
689
|
+
self.act_tile_vert.triggered.connect(lambda: self._tile_views_direction("v"))
|
|
690
|
+
|
|
691
|
+
self.act_tile_horiz = QAction(self.tr("Tile Horizontally"), self)
|
|
692
|
+
self.act_tile_horiz.setStatusTip(self.tr("Split the workspace into equal horizontal rows"))
|
|
693
|
+
self.act_tile_horiz.triggered.connect(lambda: self._tile_views_direction("h"))
|
|
694
|
+
|
|
695
|
+
self.act_tile_grid = QAction(self.tr("Smart Grid"), self)
|
|
696
|
+
self.act_tile_grid.setStatusTip(self.tr("Arrange subwindows in a near-square grid"))
|
|
697
|
+
self.act_tile_grid.triggered.connect(self._tile_views_grid)
|
|
698
|
+
|
|
699
|
+
self.act_link_group = QAction(self.tr("Link Pan/Zoom"), self)
|
|
700
|
+
self.act_link_group.setCheckable(True) # checked when in any group
|
|
701
|
+
self.act_link_group.triggered.connect(self._cycle_group_for_active) # << add
|
|
702
|
+
|
|
703
|
+
self.act_undo = QAction(QIcon(undoicon_path), self.tr("Undo"), self)
|
|
704
|
+
self.act_redo = QAction(QIcon(redoicon_path), self.tr("Redo"), self)
|
|
705
|
+
self.act_undo.setShortcut(QKeySequence.StandardKey.Undo) # Ctrl+Z
|
|
706
|
+
self.act_redo.setShortcuts([QKeySequence.StandardKey.Redo, "Ctrl+Y"]) # Shift+Ctrl+Z / Ctrl+Y
|
|
707
|
+
self.act_undo.setIconVisibleInMenu(True)
|
|
708
|
+
self.act_redo.setIconVisibleInMenu(True)
|
|
709
|
+
self.act_undo.triggered.connect(self._undo_active)
|
|
710
|
+
self.act_redo.triggered.connect(self._redo_active)
|
|
711
|
+
|
|
712
|
+
# View-ish action (toolbar toggle)
|
|
713
|
+
self.act_autostretch = QAction(self.tr("Display-Stretch"), self, checkable=True)
|
|
714
|
+
self.act_autostretch.setStatusTip(self.tr("Toggle display auto-stretch for the active window"))
|
|
715
|
+
self.act_autostretch.setShortcut(QKeySequence("A")) # optional: mirror the view shortcut
|
|
716
|
+
self.act_autostretch.toggled.connect(self._toggle_autostretch)
|
|
717
|
+
|
|
718
|
+
self.act_hardstretch = QAction(self.tr("Hard-Display-Stretch"), self, checkable=True)
|
|
719
|
+
self.addAction(self.act_hardstretch)
|
|
720
|
+
self.act_hardstretch.setShortcut(QKeySequence("H"))
|
|
721
|
+
self.act_hardstretch.setStatusTip(self.tr("Toggle hard profile for Display-Stretch (H)"))
|
|
722
|
+
|
|
723
|
+
# use toggled(bool), not triggered()
|
|
724
|
+
self.act_hardstretch.toggled.connect(self._set_hard_autostretch_from_action)
|
|
725
|
+
|
|
726
|
+
# NEW: Linked/Unlinked toggle (global default via QSettings, per-view runtime)
|
|
727
|
+
self.act_stretch_linked = QAction(self.tr("Link RGB channels"), self, checkable=True)
|
|
728
|
+
self.act_stretch_linked.setStatusTip(self.tr("Apply the same stretch to all RGB channels"))
|
|
729
|
+
self.act_stretch_linked.setShortcut(QKeySequence("Ctrl+Shift+L"))
|
|
730
|
+
self.act_stretch_linked.setChecked(
|
|
731
|
+
self.settings.value("display/stretch_linked", False, type=bool)
|
|
732
|
+
)
|
|
733
|
+
self.act_stretch_linked.toggled.connect(self._set_linked_stretch_from_action)
|
|
734
|
+
|
|
735
|
+
self.act_display_target = QAction(self.tr("Set Target Median..."), self)
|
|
736
|
+
self.act_display_target.setStatusTip(self.tr("Set the target median for Display-Stretch (e.g., 0.30)"))
|
|
737
|
+
self.act_display_target.triggered.connect(self._edit_display_target)
|
|
738
|
+
|
|
739
|
+
self.act_display_sigma = QAction(self.tr("Set Sigma..."), self)
|
|
740
|
+
self.act_display_sigma.setStatusTip(self.tr("Set the sigma for Display-Stretch (e.g., 5.0)"))
|
|
741
|
+
self.act_display_sigma.triggered.connect(self._edit_display_sigma)
|
|
742
|
+
|
|
743
|
+
# Defaults if not already present
|
|
744
|
+
if self.settings.value("display/target", None) is None:
|
|
745
|
+
self.settings.setValue("display/target", 0.30)
|
|
746
|
+
if self.settings.value("display/sigma", None) is None:
|
|
747
|
+
self.settings.setValue("display/sigma", 5.0)
|
|
748
|
+
|
|
749
|
+
self.act_bake_display_stretch = QAction(self.tr("Make Display-Stretch Permanent"), self)
|
|
750
|
+
self.act_bake_display_stretch.setStatusTip(
|
|
751
|
+
self.tr("Apply the current Display-Stretch to the image and add an undo step")
|
|
752
|
+
)
|
|
753
|
+
# choose any shortcut you like; avoid Ctrl+A etc
|
|
754
|
+
self.act_bake_display_stretch.setShortcut(QKeySequence("Shift+A"))
|
|
755
|
+
self.act_bake_display_stretch.triggered.connect(self._bake_display_stretch)
|
|
756
|
+
|
|
757
|
+
# --- Zoom controls ---
|
|
758
|
+
# --- Zoom controls (themed icons) ---
|
|
759
|
+
self.act_zoom_out = QAction(QIcon.fromTheme("zoom-out"), self.tr("Zoom Out"), self)
|
|
760
|
+
self.act_zoom_out.setStatusTip(self.tr("Zoom out"))
|
|
761
|
+
self.act_zoom_out.setShortcuts([QKeySequence("Ctrl+-")])
|
|
762
|
+
self.act_zoom_out.triggered.connect(lambda: self._zoom_step_active(-1))
|
|
763
|
+
|
|
764
|
+
self.act_zoom_in = QAction(QIcon.fromTheme("zoom-in"), self.tr("Zoom In"), self)
|
|
765
|
+
self.act_zoom_in.setStatusTip(self.tr("Zoom in"))
|
|
766
|
+
self.act_zoom_in.setShortcuts([
|
|
767
|
+
QKeySequence("Ctrl++"), # Ctrl + (Shift + = on many keyboards)
|
|
768
|
+
QKeySequence("Ctrl+="), # fallback
|
|
769
|
+
])
|
|
770
|
+
self.act_zoom_in.triggered.connect(lambda: self._zoom_step_active(+1))
|
|
771
|
+
|
|
772
|
+
self.act_zoom_1_1 = QAction(QIcon.fromTheme("zoom-original"), self.tr("1:1"), self)
|
|
773
|
+
self.act_zoom_1_1.setStatusTip(self.tr("Zoom to 100% (pixel-for-pixel)"))
|
|
774
|
+
self.act_zoom_1_1.setShortcut(QKeySequence("Ctrl+1"))
|
|
775
|
+
self.act_zoom_1_1.triggered.connect(self._zoom_active_1_1)
|
|
776
|
+
|
|
777
|
+
self.act_zoom_fit = QAction(QIcon.fromTheme("zoom-fit-best"), self.tr("Fit"), self)
|
|
778
|
+
self.act_zoom_fit.setStatusTip(self.tr("Fit image to current window"))
|
|
779
|
+
self.act_zoom_fit.setShortcut(QKeySequence("Ctrl+0"))
|
|
780
|
+
self.act_zoom_fit.triggered.connect(self._zoom_active_fit)
|
|
781
|
+
self.act_zoom_fit.setCheckable(True)
|
|
782
|
+
|
|
783
|
+
self.act_auto_fit_resize = QAction(self.tr("Auto-fit on Resize"), self)
|
|
784
|
+
self.act_auto_fit_resize.setCheckable(True)
|
|
785
|
+
|
|
786
|
+
auto_on = self.settings.value("view/auto_fit_on_resize", False, type=bool)
|
|
787
|
+
self._auto_fit_on_resize = bool(auto_on)
|
|
788
|
+
self.act_auto_fit_resize.setChecked(self._auto_fit_on_resize)
|
|
789
|
+
|
|
790
|
+
self.act_auto_fit_resize.toggled.connect(self._toggle_auto_fit_on_resize)
|
|
791
|
+
|
|
792
|
+
# View state copy/paste (optional quick commands)
|
|
793
|
+
self._copied_view_state = None
|
|
794
|
+
self.act_copy_view = QAction(self.tr("Copy View (zoom/pan)"), self)
|
|
795
|
+
self.act_paste_view = QAction(self.tr("Paste View"), self)
|
|
796
|
+
self.act_copy_view.setShortcut("Ctrl+Shift+C")
|
|
797
|
+
self.act_paste_view.setShortcut("Ctrl+Shift+V")
|
|
798
|
+
self.act_copy_view.triggered.connect(self._copy_active_view)
|
|
799
|
+
self.act_paste_view.triggered.connect(self._paste_active_view)
|
|
800
|
+
|
|
801
|
+
# Functions
|
|
802
|
+
self.act_crop = QAction(QIcon(cropicon_path), self.tr("Crop..."), self)
|
|
803
|
+
self.act_crop.setStatusTip(self.tr("Crop / rotate with handles"))
|
|
804
|
+
self.act_crop.setIconVisibleInMenu(True)
|
|
805
|
+
self.act_crop.triggered.connect(self._open_crop_dialog)
|
|
806
|
+
|
|
807
|
+
self.act_histogram = QAction(QIcon(histogram_path), self.tr("Histogram..."), self)
|
|
808
|
+
self.act_histogram.setStatusTip(self.tr("View histogram and basic stats for the active image"))
|
|
809
|
+
self.act_histogram.setIconVisibleInMenu(True)
|
|
810
|
+
self.act_histogram.triggered.connect(self._open_histogram)
|
|
811
|
+
|
|
812
|
+
self.act_stat_stretch = QAction(QIcon(statstretch_path), self.tr("Statistical Stretch..."), self)
|
|
813
|
+
self.act_stat_stretch.setStatusTip(self.tr("Stretch the image using median/SD statistics"))
|
|
814
|
+
self.act_stat_stretch.setIconVisibleInMenu(True)
|
|
815
|
+
self.act_stat_stretch.triggered.connect(self._open_statistical_stretch)
|
|
816
|
+
|
|
817
|
+
self.act_star_stretch = QAction(QIcon(starstretch_path), self.tr("Star Stretch..."), self)
|
|
818
|
+
self.act_star_stretch.setStatusTip(self.tr("Arcsinh star stretch with optional SCNR and color boost"))
|
|
819
|
+
self.act_star_stretch.setIconVisibleInMenu(True)
|
|
820
|
+
self.act_star_stretch.triggered.connect(self._open_star_stretch)
|
|
821
|
+
|
|
822
|
+
self.act_curves = QAction(QIcon(curves_path), self.tr("Curves Editor..."), self)
|
|
823
|
+
self.act_curves.setStatusTip(self.tr("Open the Curves Editor for the active image"))
|
|
824
|
+
self.act_curves.setIconVisibleInMenu(True)
|
|
825
|
+
self.act_curves.triggered.connect(self._open_curves_editor)
|
|
826
|
+
|
|
827
|
+
self.act_ghs = QAction(QIcon(uhs_path), self.tr("Hyperbolic Stretch..."), self)
|
|
828
|
+
self.act_ghs.setStatusTip(self.tr("Generalized hyperbolic stretch (α/beta/gamma, LP/HP, pivot)"))
|
|
829
|
+
self.act_ghs.setIconVisibleInMenu(True)
|
|
830
|
+
self.act_ghs.triggered.connect(self._open_hyperbolic)
|
|
831
|
+
|
|
832
|
+
self.act_abe = QAction(QIcon(abeicon_path), self.tr("ABE..."), self)
|
|
833
|
+
self.act_abe.setStatusTip(self.tr("Automatic Background Extraction"))
|
|
834
|
+
self.act_abe.setIconVisibleInMenu(True)
|
|
835
|
+
self.act_abe.triggered.connect(self._open_abe_tool)
|
|
836
|
+
|
|
837
|
+
self.act_graxpert = QAction(QIcon(graxperticon_path), self.tr("Remove Gradient (GraXpert)..."), self)
|
|
838
|
+
self.act_graxpert.setIconVisibleInMenu(True)
|
|
839
|
+
self.act_graxpert.setStatusTip(self.tr("Run GraXpert background extraction on the active image"))
|
|
840
|
+
self.act_graxpert.triggered.connect(self._open_graxpert)
|
|
841
|
+
|
|
842
|
+
self.act_remove_stars = QAction(QIcon(starnet_path), self.tr("Remove Stars..."), self)
|
|
843
|
+
self.act_remove_stars.setIconVisibleInMenu(True)
|
|
844
|
+
self.act_remove_stars.setStatusTip(self.tr("Run star removal on the active image"))
|
|
845
|
+
self.act_remove_stars.triggered.connect(lambda: self._remove_stars())
|
|
846
|
+
|
|
847
|
+
self.act_add_stars = QAction(QIcon(staradd_path), self.tr("Add Stars..."), self)
|
|
848
|
+
self.act_add_stars.setStatusTip(self.tr("Blend a starless view with a stars-only view"))
|
|
849
|
+
self.act_add_stars.setIconVisibleInMenu(True)
|
|
850
|
+
self.act_add_stars.triggered.connect(lambda: self._add_stars())
|
|
851
|
+
|
|
852
|
+
self.act_pedestal = QAction(QIcon(pedestal_icon_path), self.tr("Remove Pedestal"), self)
|
|
853
|
+
self.act_pedestal.setToolTip(self.tr("Subtract per-channel minimum.\nClick: active view\nAlt+Drag: drop onto a view"))
|
|
854
|
+
self.act_pedestal.setShortcut("Ctrl+P")
|
|
855
|
+
self.act_pedestal.triggered.connect(self._on_remove_pedestal)
|
|
856
|
+
|
|
857
|
+
self.act_linear_fit = QAction(QIcon(linearfit_path), self.tr("Linear Fit..."), self)
|
|
858
|
+
self.act_linear_fit.setIconVisibleInMenu(True)
|
|
859
|
+
self.act_linear_fit.setStatusTip(self.tr("Match image levels using Linear Fit"))
|
|
860
|
+
# optional shortcut; change if you already use it elsewhere
|
|
861
|
+
self.act_linear_fit.setShortcut("Ctrl+L")
|
|
862
|
+
self.act_linear_fit.triggered.connect(self._open_linear_fit)
|
|
863
|
+
|
|
864
|
+
self.act_remove_green = QAction(QIcon(green_path), self.tr("Remove Green..."), self)
|
|
865
|
+
self.act_remove_green.setToolTip(self.tr("SCNR-style green channel removal."))
|
|
866
|
+
self.act_remove_green.setIconVisibleInMenu(True)
|
|
867
|
+
self.act_remove_green.triggered.connect(self._open_remove_green)
|
|
868
|
+
|
|
869
|
+
self.act_background_neutral = QAction(QIcon(neutral_path), self.tr("Background Neutralization..."), self)
|
|
870
|
+
self.act_background_neutral.setStatusTip(self.tr("Neutralize background color balance using a sampled region"))
|
|
871
|
+
self.act_background_neutral.setIconVisibleInMenu(True)
|
|
872
|
+
self.act_background_neutral.triggered.connect(self._open_background_neutral)
|
|
873
|
+
|
|
874
|
+
self.act_white_balance = QAction(QIcon(whitebalance_path), self.tr("White Balance..."), self)
|
|
875
|
+
self.act_white_balance.setStatusTip(self.tr("Apply white balance (Star-Based, Manual, or Auto)"))
|
|
876
|
+
self.act_white_balance.triggered.connect(self._open_white_balance)
|
|
877
|
+
|
|
878
|
+
self.act_sfcc = QAction(QIcon(spcc_icon_path), self.tr("Spectral Flux Color Calibration..."), self)
|
|
879
|
+
self.act_sfcc.setObjectName("sfcc")
|
|
880
|
+
self.act_sfcc.setToolTip(self.tr("Open SFCC (Pickles + Filters + Sensor QE)"))
|
|
881
|
+
self.act_sfcc.triggered.connect(self.SFCC_show)
|
|
882
|
+
|
|
883
|
+
self.act_convo = QAction(QIcon(convoicon_path), self.tr("Convolution / Deconvolution..."), self)
|
|
884
|
+
self.act_convo.setObjectName("convo_deconvo")
|
|
885
|
+
self.act_convo.setToolTip(self.tr("Open Convolution / Deconvolution"))
|
|
886
|
+
self.act_convo.triggered.connect(self.show_convo_deconvo)
|
|
887
|
+
|
|
888
|
+
self.act_multiscale_decomp = QAction(QIcon(multiscale_decomp_path), self.tr("Multiscale Decomposition..."), self)
|
|
889
|
+
self.act_multiscale_decomp.setStatusTip(self.tr("Multiscale detail/residual decomposition with per-layer controls"))
|
|
890
|
+
self.act_multiscale_decomp.setIconVisibleInMenu(True)
|
|
891
|
+
self.act_multiscale_decomp.triggered.connect(self._open_multiscale_decomp)
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
# --- Extract Luminance main action ---
|
|
897
|
+
self.act_extract_luma = QAction(QIcon(LExtract_path), self.tr("Extract Luminance"), self)
|
|
898
|
+
self.act_extract_luma.setStatusTip(self.tr("Create a new mono document using the selected luminance method"))
|
|
899
|
+
self.act_extract_luma.setIconVisibleInMenu(True)
|
|
900
|
+
self.act_extract_luma.triggered.connect(lambda: self._extract_luminance(doc=None))
|
|
901
|
+
|
|
902
|
+
# --- Luminance method actions (checkable group) ---
|
|
903
|
+
self.luma_method = getattr(self, "luma_method", "rec709") # default
|
|
904
|
+
self._luma_group = QActionGroup(self)
|
|
905
|
+
self._luma_group.setExclusive(True)
|
|
906
|
+
self._luma_sensor_actions = {} # key -> QAction
|
|
907
|
+
|
|
908
|
+
def _mk(method_key, text):
|
|
909
|
+
act = QAction(text, self, checkable=True)
|
|
910
|
+
act.setData(method_key)
|
|
911
|
+
self._luma_group.addAction(act)
|
|
912
|
+
return act
|
|
913
|
+
|
|
914
|
+
self.act_luma_rec709 = _mk("rec709", "Broadband RGB (Rec.709)")
|
|
915
|
+
self.act_luma_max = _mk("max", "Narrowband mappings (Max)")
|
|
916
|
+
self.act_luma_snr = _mk("snr", "Unequal Noise (SNR)")
|
|
917
|
+
self.act_luma_rec601 = _mk("rec601", "Rec.601")
|
|
918
|
+
self.act_luma_rec2020 = _mk("rec2020", "Rec.2020")
|
|
919
|
+
|
|
920
|
+
# restore selection
|
|
921
|
+
for a in self._luma_group.actions():
|
|
922
|
+
a.setChecked(a.data() == self.luma_method)
|
|
923
|
+
|
|
924
|
+
# update method when user picks from the menu
|
|
925
|
+
def _on_luma_pick(act):
|
|
926
|
+
key = act.data()
|
|
927
|
+
if key is None:
|
|
928
|
+
return
|
|
929
|
+
self.luma_method = str(key)
|
|
930
|
+
try:
|
|
931
|
+
self.settings.setValue("ui/luminance_method", self.luma_method)
|
|
932
|
+
except Exception:
|
|
933
|
+
pass
|
|
934
|
+
|
|
935
|
+
self._luma_group.triggered.connect(_on_luma_pick)
|
|
936
|
+
|
|
937
|
+
self.act_recombine_luma = QAction(QIcon(LInsert_path), self.tr("Recombine Luminance..."), self)
|
|
938
|
+
self.act_recombine_luma.setStatusTip(self.tr("Replace the active image's luminance from another view"))
|
|
939
|
+
self.act_recombine_luma.setIconVisibleInMenu(True)
|
|
940
|
+
self.act_recombine_luma.triggered.connect(lambda: self._recombine_luminance_ui(target_doc=None))
|
|
941
|
+
|
|
942
|
+
self.act_rgb_extract = QAction(QIcon(rgbextract_path), self.tr("RGB Extract"), self)
|
|
943
|
+
self.act_rgb_extract.setIconVisibleInMenu(True)
|
|
944
|
+
self.act_rgb_extract.setStatusTip(self.tr("Extract R/G/B as three mono documents"))
|
|
945
|
+
self.act_rgb_extract.triggered.connect(self._rgb_extract_active)
|
|
946
|
+
|
|
947
|
+
self.act_rgb_combine = QAction(QIcon(rgbcombo_path), self.tr("RGB Combination..."), self)
|
|
948
|
+
self.act_rgb_combine.setIconVisibleInMenu(True)
|
|
949
|
+
self.act_rgb_combine.setStatusTip(self.tr("Combine three mono images into RGB"))
|
|
950
|
+
self.act_rgb_combine.triggered.connect(self._open_rgb_combination)
|
|
951
|
+
|
|
952
|
+
self.act_blemish = QAction(QIcon(blastericon_path), self.tr("Blemish Blaster..."), self)
|
|
953
|
+
self.act_blemish.setIconVisibleInMenu(True)
|
|
954
|
+
self.act_blemish.setStatusTip(self.tr("Interactive blemish removal on the active view"))
|
|
955
|
+
self.act_blemish.triggered.connect(self._open_blemish_blaster)
|
|
956
|
+
|
|
957
|
+
self.act_wavescale_hdr = QAction(QIcon(hdr_path), self.tr("WaveScale HDR..."), self)
|
|
958
|
+
self.act_wavescale_hdr.setStatusTip(self.tr("Wave-scale HDR with luminance-masked starlet"))
|
|
959
|
+
self.act_wavescale_hdr.setIconVisibleInMenu(True)
|
|
960
|
+
self.act_wavescale_hdr.triggered.connect(self._open_wavescale_hdr)
|
|
961
|
+
|
|
962
|
+
self.act_wavescale_de = QAction(QIcon(dse_icon_path), self.tr("WaveScale Dark Enhancer..."), self)
|
|
963
|
+
self.act_wavescale_de.setStatusTip(self.tr("Enhance faint/dark structures with wavelet-guided masking"))
|
|
964
|
+
self.act_wavescale_de.setIconVisibleInMenu(True)
|
|
965
|
+
self.act_wavescale_de.triggered.connect(self._open_wavescale_dark_enhance)
|
|
966
|
+
|
|
967
|
+
self.act_clahe = QAction(QIcon(clahe_path), self.tr("CLAHE..."), self)
|
|
968
|
+
self.act_clahe.setStatusTip(self.tr("Contrast Limited Adaptive Histogram Equalization"))
|
|
969
|
+
self.act_clahe.setIconVisibleInMenu(True)
|
|
970
|
+
self.act_clahe.triggered.connect(self._open_clahe)
|
|
971
|
+
|
|
972
|
+
self.act_morphology = QAction(QIcon(morpho_path), self.tr("Morphological Operations..."), self)
|
|
973
|
+
self.act_morphology.setStatusTip(self.tr("Erosion, dilation, opening, and closing."))
|
|
974
|
+
self.act_morphology.setIconVisibleInMenu(True)
|
|
975
|
+
self.act_morphology.triggered.connect(self._open_morphology)
|
|
976
|
+
|
|
977
|
+
self.act_pixelmath = QAction(QIcon(pixelmath_path), self.tr("Pixel Math..."), self)
|
|
978
|
+
self.act_pixelmath.setStatusTip(self.tr("Evaluate expressions using open view names"))
|
|
979
|
+
self.act_pixelmath.setIconVisibleInMenu(True)
|
|
980
|
+
self.act_pixelmath.triggered.connect(self._open_pixel_math)
|
|
981
|
+
|
|
982
|
+
self.act_signature = QAction(QIcon(signature_icon_path), self.tr("Signature / Insert..."), self)
|
|
983
|
+
self.act_signature.setIconVisibleInMenu(True)
|
|
984
|
+
self.act_signature.setStatusTip(self.tr("Add signatures/overlays and bake them into the active image"))
|
|
985
|
+
self.act_signature.triggered.connect(self._open_signature_insert)
|
|
986
|
+
|
|
987
|
+
self.act_halobgon = QAction(QIcon(halo_path), self.tr("Halo-B-Gon..."), self)
|
|
988
|
+
self.act_halobgon.setIconVisibleInMenu(True)
|
|
989
|
+
self.act_halobgon.setStatusTip(self.tr("Remove those pesky halos around your stars"))
|
|
990
|
+
self.act_halobgon.triggered.connect(self._open_halo_b_gon)
|
|
991
|
+
|
|
992
|
+
self.act_image_combine = QAction(QIcon(imagecombine_path), self.tr("Image Combine..."), self)
|
|
993
|
+
self.act_image_combine.setIconVisibleInMenu(True)
|
|
994
|
+
self.act_image_combine.setStatusTip(self.tr("Blend two open images (replace A or create new)"))
|
|
995
|
+
self.act_image_combine.triggered.connect(self._open_image_combine)
|
|
996
|
+
|
|
997
|
+
# --- Geometry ---
|
|
998
|
+
self.act_geom_invert = QAction(QIcon(invert_path), self.tr("Invert"), self)
|
|
999
|
+
self.act_geom_invert.setIconVisibleInMenu(True)
|
|
1000
|
+
self.act_geom_invert.setStatusTip(self.tr("Invert image colors"))
|
|
1001
|
+
self.act_geom_invert.triggered.connect(self._exec_geom_invert)
|
|
1002
|
+
|
|
1003
|
+
self.act_geom_flip_h = QAction(QIcon(fliphorizontal_path), self.tr("Flip Horizontal"), self)
|
|
1004
|
+
self.act_geom_flip_h.setIconVisibleInMenu(True)
|
|
1005
|
+
self.act_geom_flip_h.setStatusTip(self.tr("Flip image left<->right"))
|
|
1006
|
+
self.act_geom_flip_h.triggered.connect(self._exec_geom_flip_h)
|
|
1007
|
+
|
|
1008
|
+
self.act_geom_flip_v = QAction(QIcon(flipvertical_path), self.tr("Flip Vertical"), self)
|
|
1009
|
+
self.act_geom_flip_v.setIconVisibleInMenu(True)
|
|
1010
|
+
self.act_geom_flip_v.setStatusTip(self.tr("Flip image top<->bottom"))
|
|
1011
|
+
self.act_geom_flip_v.triggered.connect(self._exec_geom_flip_v)
|
|
1012
|
+
|
|
1013
|
+
self.act_geom_rot_cw = QAction(QIcon(rotateclockwise_path), self.tr("Rotate 90° Clockwise"), self)
|
|
1014
|
+
self.act_geom_rot_cw.setIconVisibleInMenu(True)
|
|
1015
|
+
self.act_geom_rot_cw.setStatusTip(self.tr("Rotate image 90° clockwise"))
|
|
1016
|
+
self.act_geom_rot_cw.triggered.connect(self._exec_geom_rot_cw)
|
|
1017
|
+
|
|
1018
|
+
self.act_geom_rot_ccw = QAction(QIcon(rotatecounterclockwise_path), self.tr("Rotate 90° Counterclockwise"), self)
|
|
1019
|
+
self.act_geom_rot_ccw.setIconVisibleInMenu(True)
|
|
1020
|
+
self.act_geom_rot_ccw.setStatusTip(self.tr("Rotate image 90° counterclockwise"))
|
|
1021
|
+
self.act_geom_rot_ccw.triggered.connect(self._exec_geom_rot_ccw)
|
|
1022
|
+
|
|
1023
|
+
self.act_geom_rot_180 = QAction(QIcon(rotate180_path), self.tr("Rotate 180°"), self)
|
|
1024
|
+
self.act_geom_rot_180.setIconVisibleInMenu(True)
|
|
1025
|
+
self.act_geom_rot_180.setStatusTip(self.tr("Rotate image 180°"))
|
|
1026
|
+
self.act_geom_rot_180.triggered.connect(self._exec_geom_rot_180)
|
|
1027
|
+
|
|
1028
|
+
self.act_geom_rot_any = QAction(QIcon(rotatearbitrary_path), self.tr("Rotate..."), self)
|
|
1029
|
+
self.act_geom_rot_any.setIconVisibleInMenu(True)
|
|
1030
|
+
self.act_geom_rot_any.setStatusTip(self.tr("Rotate image by an arbitrary angle (degrees)"))
|
|
1031
|
+
self.act_geom_rot_any.triggered.connect(self._exec_geom_rot_any)
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
self.act_geom_rescale = QAction(QIcon(rescale_path), self.tr("Rescale..."), self)
|
|
1035
|
+
self.act_geom_rescale.setIconVisibleInMenu(True)
|
|
1036
|
+
self.act_geom_rescale.setStatusTip(self.tr("Rescale image by a factor"))
|
|
1037
|
+
self.act_geom_rescale.triggered.connect(self._exec_geom_rescale)
|
|
1038
|
+
|
|
1039
|
+
self.act_debayer = QAction(QIcon(debayer_path), self.tr("Debayer..."), self)
|
|
1040
|
+
self.act_debayer.setObjectName("debayer")
|
|
1041
|
+
self.act_debayer.setProperty("command_id", "debayer")
|
|
1042
|
+
self.act_debayer.setStatusTip(self.tr("Demosaic a Bayer-mosaic mono image to RGB"))
|
|
1043
|
+
self.act_debayer.triggered.connect(self._open_debayer)
|
|
1044
|
+
|
|
1045
|
+
# (Optional example shortcuts; uncomment if you want)
|
|
1046
|
+
self.act_geom_invert.setShortcut("Ctrl+I")
|
|
1047
|
+
|
|
1048
|
+
# self.act_geom_flip_h.setShortcut("H")
|
|
1049
|
+
# self.act_geom_flip_v.setShortcut("V")
|
|
1050
|
+
# self.act_geom_rot_cw.setShortcut("]")
|
|
1051
|
+
# self.act_geom_rot_ccw.setShortcut("[")
|
|
1052
|
+
# self.act_geom_rescale.setShortcut("Ctrl+R")
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
# actions (use your actual icon paths if you have them)
|
|
1056
|
+
try:
|
|
1057
|
+
cosmic_icon = QIcon(cosmic_path) # define cosmic_path like your other icons (same pattern as halo_path)
|
|
1058
|
+
except Exception:
|
|
1059
|
+
cosmic_icon = QIcon()
|
|
1060
|
+
|
|
1061
|
+
try:
|
|
1062
|
+
sat_icon = QIcon(satellite_path) # optional icon for satellite
|
|
1063
|
+
except Exception:
|
|
1064
|
+
sat_icon = QIcon()
|
|
1065
|
+
|
|
1066
|
+
self.actCosmicUI = QAction(cosmic_icon, self.tr("Cosmic Clarity UI..."), self)
|
|
1067
|
+
self.actCosmicSat = QAction(sat_icon, self.tr("Cosmic Clarity Satellite..."), self)
|
|
1068
|
+
|
|
1069
|
+
self.actCosmicUI.triggered.connect(self._open_cosmic_clarity_ui)
|
|
1070
|
+
self.actCosmicSat.triggered.connect(self._open_cosmic_clarity_satellite)
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
ab_icon = QIcon(aberration_path) # falls back if file missing
|
|
1074
|
+
|
|
1075
|
+
self.actAberrationAI = QAction(ab_icon, self.tr("Aberration Correction (AI)..."), self)
|
|
1076
|
+
self.actAberrationAI.triggered.connect(self._open_aberration_ai)
|
|
1077
|
+
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
#Tools
|
|
1081
|
+
self.act_blink = QAction(QIcon(blink_path), self.tr("Blink Comparator..."), self)
|
|
1082
|
+
self.act_blink.setStatusTip(self.tr("Compare a stack of images by blinking"))
|
|
1083
|
+
self.act_blink.triggered.connect(self._open_blink_tool)
|
|
1084
|
+
|
|
1085
|
+
self.act_ppp = QAction(QIcon(ppp_path), self.tr("Perfect Palette Picker..."), self)
|
|
1086
|
+
self.act_ppp.setStatusTip(self.tr("Pick the perfect palette for your image"))
|
|
1087
|
+
self.act_ppp.triggered.connect(self._open_ppp_tool)
|
|
1088
|
+
|
|
1089
|
+
self.act_nbtorgb = QAction(QIcon(nbtorgb_path), self.tr("NB->RGB Stars..."), self)
|
|
1090
|
+
self.act_nbtorgb.setStatusTip(self.tr("Combine narrowband to RGB with optional OSC stars"))
|
|
1091
|
+
self.act_nbtorgb.setIconVisibleInMenu(True)
|
|
1092
|
+
self.act_nbtorgb.triggered.connect(self._open_nbtorgb_tool)
|
|
1093
|
+
|
|
1094
|
+
self.act_selective_color = QAction(QIcon(selectivecolor_path), self.tr("Selective Color Correction..."), self)
|
|
1095
|
+
self.act_selective_color.setStatusTip(self.tr("Adjust specific hue ranges with CMY/RGB controls"))
|
|
1096
|
+
self.act_selective_color.triggered.connect(self._open_selective_color_tool)
|
|
1097
|
+
|
|
1098
|
+
# NEW: Frequency Separation
|
|
1099
|
+
self.act_freqsep = QAction(QIcon(freqsep_path), self.tr("Frequency Separation..."), self)
|
|
1100
|
+
self.act_freqsep.setStatusTip(self.tr("Split into LF/HF and enhance HF (scale, wavelet, denoise)"))
|
|
1101
|
+
self.act_freqsep.setIconVisibleInMenu(True)
|
|
1102
|
+
self.act_freqsep.triggered.connect(self._open_freqsep_tool)
|
|
1103
|
+
|
|
1104
|
+
self.act_contsub = QAction(QIcon(contsub_path), self.tr("Continuum Subtract..."), self)
|
|
1105
|
+
self.act_contsub.setStatusTip(self.tr("Continuum Subtract (NB - scaled broadband)"))
|
|
1106
|
+
self.act_contsub.setIconVisibleInMenu(True)
|
|
1107
|
+
self.act_contsub.triggered.connect(self._open_contsub_tool)
|
|
1108
|
+
|
|
1109
|
+
# History
|
|
1110
|
+
self.act_history_explorer = QAction(self.tr("History Explorer..."), self)
|
|
1111
|
+
self.act_history_explorer.setStatusTip(self.tr("Inspect and restore from the slot's history"))
|
|
1112
|
+
self.act_history_explorer.triggered.connect(self._open_history_explorer)
|
|
1113
|
+
|
|
1114
|
+
|
|
1115
|
+
#STAR STUFF
|
|
1116
|
+
self.act_image_peeker = QAction(QIcon(peeker_icon), self.tr("Image Peeker..."), self)
|
|
1117
|
+
self.act_image_peeker.setIconVisibleInMenu(True)
|
|
1118
|
+
self.act_image_peeker.setStatusTip(self.tr("Image Inspector and Focal Plane Analysis"))
|
|
1119
|
+
self.act_image_peeker.triggered.connect(self._open_image_peeker)
|
|
1120
|
+
|
|
1121
|
+
self.act_psf_viewer = QAction(QIcon(psf_path), self.tr("PSF Viewer..."), self)
|
|
1122
|
+
self.act_psf_viewer.setIconVisibleInMenu(True)
|
|
1123
|
+
self.act_psf_viewer.setStatusTip(self.tr("Inspect star PSF/HFR and flux histograms (SEP)"))
|
|
1124
|
+
self.act_psf_viewer.triggered.connect(self._open_psf_viewer)
|
|
1125
|
+
|
|
1126
|
+
self.act_stacking_suite = QAction(QIcon(stacking_path), self.tr("Stacking Suite..."), self)
|
|
1127
|
+
self.act_stacking_suite.setIconVisibleInMenu(True)
|
|
1128
|
+
self.act_stacking_suite.setStatusTip(self.tr("Stacking! Darks, Flats, Lights, Calibration, Drizzle, and more!!"))
|
|
1129
|
+
self.act_stacking_suite.triggered.connect(self._open_stacking_suite)
|
|
1130
|
+
|
|
1131
|
+
self.act_live_stacking = QAction(QIcon(livestacking_path), self.tr("Live Stacking..."), self)
|
|
1132
|
+
self.act_live_stacking.setIconVisibleInMenu(True)
|
|
1133
|
+
self.act_live_stacking.setStatusTip(self.tr("Live monitor and stack incoming frames"))
|
|
1134
|
+
self.act_live_stacking.triggered.connect(self._open_live_stacking)
|
|
1135
|
+
|
|
1136
|
+
self.act_plate_solve = QAction(QIcon(platesolve_path), self.tr("Plate Solver..."), self)
|
|
1137
|
+
self.act_plate_solve.setIconVisibleInMenu(True)
|
|
1138
|
+
self.act_plate_solve.setStatusTip(self.tr("Solve WCS/SIP for the active image or a file"))
|
|
1139
|
+
self.act_plate_solve.triggered.connect(self._open_plate_solver)
|
|
1140
|
+
|
|
1141
|
+
self.act_star_align = QAction(QIcon(staralign_path), self.tr("Stellar Alignment..."), self)
|
|
1142
|
+
self.act_star_align.setIconVisibleInMenu(True)
|
|
1143
|
+
self.act_star_align.setStatusTip(self.tr("Align images via astroalign / triangles"))
|
|
1144
|
+
self.act_star_align.triggered.connect(self._open_stellar_alignment)
|
|
1145
|
+
|
|
1146
|
+
self.act_star_register = QAction(QIcon(starregistration_path), self.tr("Stellar Register..."), self)
|
|
1147
|
+
self.act_star_register.setIconVisibleInMenu(True)
|
|
1148
|
+
self.act_star_register.setStatusTip(self.tr("Batch-align frames to a reference"))
|
|
1149
|
+
self.act_star_register.triggered.connect(self._open_stellar_registration)
|
|
1150
|
+
|
|
1151
|
+
self.act_mosaic_master = QAction(QIcon(mosaic_path), self.tr("Mosaic Master..."), self)
|
|
1152
|
+
self.act_mosaic_master.setIconVisibleInMenu(True)
|
|
1153
|
+
self.act_mosaic_master.setStatusTip(self.tr("Build mosaics from overlapping frames"))
|
|
1154
|
+
self.act_mosaic_master.triggered.connect(self._open_mosaic_master)
|
|
1155
|
+
|
|
1156
|
+
self.act_supernova_hunter = QAction(QIcon(supernova_path), self.tr("Supernova / Asteroid Hunter..."), self)
|
|
1157
|
+
self.act_supernova_hunter.setIconVisibleInMenu(True)
|
|
1158
|
+
self.act_supernova_hunter.setStatusTip(self.tr("Find transients/anomalies across frames"))
|
|
1159
|
+
self.act_supernova_hunter.triggered.connect(self._open_supernova_hunter)
|
|
1160
|
+
|
|
1161
|
+
self.act_star_spikes = QAction(QIcon(starspike_path), self.tr("Diffraction Spikes..."), self)
|
|
1162
|
+
self.act_star_spikes.setIconVisibleInMenu(True)
|
|
1163
|
+
self.act_star_spikes.setStatusTip(self.tr("Add diffraction spikes to detected stars"))
|
|
1164
|
+
self.act_star_spikes.triggered.connect(self._open_star_spikes)
|
|
1165
|
+
|
|
1166
|
+
self.act_astrospike = QAction(QIcon(astrospike_path), self.tr("AstroSpike..."), self)
|
|
1167
|
+
self.act_astrospike.setIconVisibleInMenu(True)
|
|
1168
|
+
self.act_astrospike.setStatusTip(self.tr("Advanced diffraction spikes with halos, flares and rainbow effects"))
|
|
1169
|
+
self.act_astrospike.triggered.connect(self._open_astrospike)
|
|
1170
|
+
|
|
1171
|
+
self.act_exo_detector = QAction(QIcon(exoicon_path), self.tr("Exoplanet Detector..."), self)
|
|
1172
|
+
self.act_exo_detector.setIconVisibleInMenu(True)
|
|
1173
|
+
self.act_exo_detector.setStatusTip(self.tr("Detect exoplanet transits from time-series subs"))
|
|
1174
|
+
self.act_exo_detector.triggered.connect(self._open_exo_detector)
|
|
1175
|
+
|
|
1176
|
+
self.act_isophote = QAction(QIcon(isophote_path), self.tr("GLIMR -- Isophote Modeler..."), self)
|
|
1177
|
+
self.act_isophote.setIconVisibleInMenu(True)
|
|
1178
|
+
self.act_isophote.setStatusTip(self.tr("Fit galaxy isophotes and reveal residuals"))
|
|
1179
|
+
self.act_isophote.triggered.connect(self._open_isophote)
|
|
1180
|
+
|
|
1181
|
+
self.act_rgb_align = QAction(QIcon(rgbalign_path), self.tr("RGB Align..."), self)
|
|
1182
|
+
self.act_rgb_align.setIconVisibleInMenu(True)
|
|
1183
|
+
self.act_rgb_align.setStatusTip(self.tr("Align R and B channels to G using astroalign (affine/homography/poly)"))
|
|
1184
|
+
self.act_rgb_align.triggered.connect(self._open_rgb_align)
|
|
1185
|
+
|
|
1186
|
+
self.act_whats_in_my_sky = QAction(QIcon(wims_path), self.tr("What's In My Sky..."), self)
|
|
1187
|
+
self.act_whats_in_my_sky.setIconVisibleInMenu(True)
|
|
1188
|
+
self.act_whats_in_my_sky.setStatusTip(self.tr("Plan targets by altitude, transit time, and lunar separation"))
|
|
1189
|
+
self.act_whats_in_my_sky.triggered.connect(self._open_whats_in_my_sky)
|
|
1190
|
+
|
|
1191
|
+
self.act_wimi = QAction(QIcon(wimi_path), self.tr("What's In My Image..."), self)
|
|
1192
|
+
self.act_wimi.setIconVisibleInMenu(True)
|
|
1193
|
+
self.act_wimi.setStatusTip(self.tr("Identify objects in a plate-solved frame"))
|
|
1194
|
+
self.act_wimi.triggered.connect(self._open_wimi)
|
|
1195
|
+
|
|
1196
|
+
# --- Scripts actions ---
|
|
1197
|
+
self.act_open_scripts_folder = QAction(self.tr("Open Scripts Folder..."), self)
|
|
1198
|
+
self.act_open_scripts_folder.setStatusTip(self.tr("Open the SASpro user scripts folder"))
|
|
1199
|
+
self.act_open_scripts_folder.triggered.connect(self._open_scripts_folder)
|
|
1200
|
+
|
|
1201
|
+
self.act_reload_scripts = QAction(self.tr("Reload Scripts"), self)
|
|
1202
|
+
self.act_reload_scripts.setStatusTip(self.tr("Rescan the scripts folder and reload .py files"))
|
|
1203
|
+
self.act_reload_scripts.triggered.connect(self._reload_scripts)
|
|
1204
|
+
|
|
1205
|
+
self.act_create_sample_script = QAction(self.tr("Create Sample Scripts..."), self)
|
|
1206
|
+
self.act_create_sample_script.setStatusTip(self.tr("Write a ready-to-edit sample script into the scripts folder"))
|
|
1207
|
+
self.act_create_sample_script.triggered.connect(self._create_sample_script)
|
|
1208
|
+
|
|
1209
|
+
self.act_script_editor = QAction(self.tr("Script Editor..."), self)
|
|
1210
|
+
self.act_script_editor.setStatusTip(self.tr("Open the built-in script editor"))
|
|
1211
|
+
self.act_script_editor.triggered.connect(self._show_script_editor)
|
|
1212
|
+
|
|
1213
|
+
self.act_open_user_scripts_github = QAction(self.tr("Open User Scripts (GitHub)..."), self)
|
|
1214
|
+
self.act_open_user_scripts_github.triggered.connect(self._open_user_scripts_github)
|
|
1215
|
+
|
|
1216
|
+
self.act_open_scripts_discord = QAction(self.tr("Open Scripts Forum (Discord)..."), self)
|
|
1217
|
+
self.act_open_scripts_discord.triggered.connect(self._open_scripts_discord_forum)
|
|
1218
|
+
|
|
1219
|
+
# --- FITS Header Modifier action ---
|
|
1220
|
+
self.act_fits_modifier = QAction(self.tr("FITS Header Modifier..."), self)
|
|
1221
|
+
# self.act_fits_modifier.setIcon(QIcon(path_to_icon)) # (optional) icon goes here later
|
|
1222
|
+
self.act_fits_modifier.setIconVisibleInMenu(True)
|
|
1223
|
+
self.act_fits_modifier.setStatusTip(self.tr("View/Edit FITS headers"))
|
|
1224
|
+
self.act_fits_modifier.triggered.connect(self._open_fits_modifier)
|
|
1225
|
+
|
|
1226
|
+
self.act_fits_batch_modifier = QAction(self.tr("FITS Header Batch Modifier..."), self)
|
|
1227
|
+
# self.act_fits_modifier.setIcon(QIcon(path_to_icon)) # (optional) icon goes here later
|
|
1228
|
+
self.act_fits_batch_modifier.setIconVisibleInMenu(True)
|
|
1229
|
+
self.act_fits_batch_modifier.setStatusTip(self.tr("Batch Modify FITS Headers"))
|
|
1230
|
+
self.act_fits_batch_modifier.triggered.connect(self._open_fits_batch_modifier)
|
|
1231
|
+
|
|
1232
|
+
self.act_batch_renamer = QAction(self.tr("Batch Rename from FITS..."), self)
|
|
1233
|
+
# self.act_batch_renamer.setIcon(QIcon(batch_renamer_icon_path)) # (optional icon)
|
|
1234
|
+
self.act_batch_renamer.triggered.connect(self._open_batch_renamer)
|
|
1235
|
+
|
|
1236
|
+
self.act_astrobin_exporter = QAction(self.tr("AstroBin Exporter..."), self)
|
|
1237
|
+
# self.act_astrobin_exporter.setIcon(QIcon(astrobin_icon_path)) # optional icon
|
|
1238
|
+
self.act_astrobin_exporter.triggered.connect(self._open_astrobin_exporter)
|
|
1239
|
+
|
|
1240
|
+
self.act_batch_convert = QAction(self.tr("Batch Converter..."), self)
|
|
1241
|
+
# self.act_batch_convert.setIcon(QIcon("path/to/icon.svg")) # optional later
|
|
1242
|
+
self.act_batch_convert.triggered.connect(self._open_batch_convert)
|
|
1243
|
+
|
|
1244
|
+
self.act_copy_astrometry = QAction(self.tr("Copy Astrometric Solution..."), self)
|
|
1245
|
+
self.act_copy_astrometry.triggered.connect(self._open_copy_astrometry)
|
|
1246
|
+
|
|
1247
|
+
# Create Mask
|
|
1248
|
+
self.act_create_mask = QAction(QIcon(maskcreate_path), self.tr("Create Mask..."), self)
|
|
1249
|
+
self.act_create_mask.setIconVisibleInMenu(True)
|
|
1250
|
+
self.act_create_mask.setStatusTip(self.tr("Create a mask from the active image"))
|
|
1251
|
+
self.act_create_mask.triggered.connect(self._action_create_mask)
|
|
1252
|
+
|
|
1253
|
+
# --- Masks ---
|
|
1254
|
+
self.act_apply_mask = QAction(QIcon(maskapply_path), self.tr("Apply Mask"), self)
|
|
1255
|
+
self.act_apply_mask.setStatusTip(self.tr("Apply a mask document to the active image"))
|
|
1256
|
+
self.act_apply_mask.triggered.connect(self._apply_mask_menu)
|
|
1257
|
+
|
|
1258
|
+
self.act_remove_mask = QAction(QIcon(maskremove_path), self.tr("Remove Active Mask"), self)
|
|
1259
|
+
self.act_remove_mask.setStatusTip(self.tr("Remove the active mask from the active image"))
|
|
1260
|
+
self.act_remove_mask.triggered.connect(self._remove_mask_menu)
|
|
1261
|
+
|
|
1262
|
+
self.act_show_mask = QAction(self.tr("Show Mask Overlay"), self)
|
|
1263
|
+
self.act_hide_mask = QAction(self.tr("Hide Mask Overlay"), self)
|
|
1264
|
+
self.act_show_mask.triggered.connect(self._show_mask_overlay)
|
|
1265
|
+
self.act_hide_mask.triggered.connect(self._hide_mask_overlay)
|
|
1266
|
+
|
|
1267
|
+
self.act_invert_mask = QAction(self.tr("Invert Mask"), self)
|
|
1268
|
+
self.act_invert_mask.triggered.connect(self._invert_mask)
|
|
1269
|
+
self.act_invert_mask.setShortcut("Ctrl+Shift+I")
|
|
1270
|
+
|
|
1271
|
+
self.act_check_updates = QAction(self.tr("Check for Updates..."), self)
|
|
1272
|
+
self.act_check_updates.triggered.connect(self.check_for_updates_now)
|
|
1273
|
+
|
|
1274
|
+
self.act_docs = QAction(self.tr("Documentation..."), self)
|
|
1275
|
+
self.act_docs.setStatusTip(self.tr("Open the Seti Astro Suite Pro online documentation"))
|
|
1276
|
+
self.act_docs.triggered.connect(
|
|
1277
|
+
lambda: QDesktopServices.openUrl(QUrl("https://github.com/setiastro/setiastrosuitepro/wiki"))
|
|
1278
|
+
)
|
|
1279
|
+
|
|
1280
|
+
# Qt6-safe shortcut for Help/Docs (F1)
|
|
1281
|
+
try:
|
|
1282
|
+
# Qt6 enum lives under StandardKey
|
|
1283
|
+
self.act_docs.setShortcut(QKeySequence(QKeySequence.StandardKey.HelpContents))
|
|
1284
|
+
except Exception:
|
|
1285
|
+
# Fallback works everywhere
|
|
1286
|
+
self.act_docs.setShortcut(QKeySequence("F1"))
|
|
1287
|
+
|
|
1288
|
+
self.act_view_bundles = QAction(QIcon(viewbundles_path), self.tr("View Bundles..."), self)
|
|
1289
|
+
self.act_view_bundles.setStatusTip(self.tr("Create bundles of views; drop shortcuts to apply to all"))
|
|
1290
|
+
self.act_view_bundles.triggered.connect(self._open_view_bundles)
|
|
1291
|
+
|
|
1292
|
+
self.act_function_bundles = QAction(QIcon(functionbundles_path), self.tr("Function Bundles..."), self)
|
|
1293
|
+
self.act_function_bundles.setStatusTip(self.tr("Create and run bundles of functions/shortcuts"))
|
|
1294
|
+
self.act_function_bundles.triggered.connect(self._open_function_bundles)
|
|
1295
|
+
|
|
1296
|
+
# give each action a stable id and register
|
|
1297
|
+
def reg(cid, act):
|
|
1298
|
+
act.setProperty("command_id", cid)
|
|
1299
|
+
act.setObjectName(cid) # also becomes default if we ever need it
|
|
1300
|
+
self.shortcuts.register_action(cid, act)
|
|
1301
|
+
|
|
1302
|
+
# create manager once MDI exists
|
|
1303
|
+
if not hasattr(self, "shortcuts"):
|
|
1304
|
+
# self.mdi is your QMdiArea used elsewhere (stat_stretch uses it)
|
|
1305
|
+
self.shortcuts = ShortcutManager(self.mdi, self)
|
|
1306
|
+
|
|
1307
|
+
# register whatever you want draggable/launchable
|
|
1308
|
+
reg("open", self.act_open)
|
|
1309
|
+
reg("save_as", self.act_save)
|
|
1310
|
+
reg("undo", self.act_undo)
|
|
1311
|
+
reg("redo", self.act_redo)
|
|
1312
|
+
reg("autostretch", self.act_autostretch)
|
|
1313
|
+
reg("zoom_1_1", self.act_zoom_1_1)
|
|
1314
|
+
reg("crop", self.act_crop)
|
|
1315
|
+
reg("histogram", self.act_histogram)
|
|
1316
|
+
reg("stat_stretch", self.act_stat_stretch)
|
|
1317
|
+
reg("star_stretch", self.act_star_stretch)
|
|
1318
|
+
reg("curves", self.act_curves)
|
|
1319
|
+
reg("ghs", self.act_ghs)
|
|
1320
|
+
reg("blink", self.act_blink)
|
|
1321
|
+
reg("ppp", self.act_ppp)
|
|
1322
|
+
reg("nbtorgb", self.act_nbtorgb)
|
|
1323
|
+
reg("freqsep", self.act_freqsep)
|
|
1324
|
+
reg("selective_color", self.act_selective_color)
|
|
1325
|
+
reg("contsub", self.act_contsub)
|
|
1326
|
+
reg("abe", self.act_abe)
|
|
1327
|
+
reg("create_mask", self.act_create_mask)
|
|
1328
|
+
reg("graxpert", self.act_graxpert)
|
|
1329
|
+
reg("remove_stars", self.act_remove_stars)
|
|
1330
|
+
reg("add_stars", self.act_add_stars)
|
|
1331
|
+
reg("pedestal", self.act_pedestal)
|
|
1332
|
+
reg("remove_green", self.act_remove_green)
|
|
1333
|
+
reg("background_neutral", self.act_background_neutral)
|
|
1334
|
+
reg("white_balance", self.act_white_balance)
|
|
1335
|
+
reg("sfcc", self.act_sfcc)
|
|
1336
|
+
reg("convo", self.act_convo)
|
|
1337
|
+
reg("extract_luminance", self.act_extract_luma)
|
|
1338
|
+
reg("recombine_luminance", self.act_recombine_luma)
|
|
1339
|
+
reg("rgb_extract", self.act_rgb_extract)
|
|
1340
|
+
reg("rgb_combine", self.act_rgb_combine)
|
|
1341
|
+
reg("blemish_blaster", self.act_blemish)
|
|
1342
|
+
reg("wavescale_hdr", self.act_wavescale_hdr)
|
|
1343
|
+
reg("wavescale_dark_enhance", self.act_wavescale_de)
|
|
1344
|
+
reg("clahe", self.act_clahe)
|
|
1345
|
+
reg("morphology", self.act_morphology)
|
|
1346
|
+
reg("pixel_math", self.act_pixelmath)
|
|
1347
|
+
reg("signature_insert", self.act_signature)
|
|
1348
|
+
reg("halo_b_gon", self.act_halobgon)
|
|
1349
|
+
|
|
1350
|
+
reg("multiscale_decomp", self.act_multiscale_decomp)
|
|
1351
|
+
reg("geom_invert", self.act_geom_invert)
|
|
1352
|
+
reg("geom_flip_horizontal", self.act_geom_flip_h)
|
|
1353
|
+
reg("geom_flip_vertical", self.act_geom_flip_v)
|
|
1354
|
+
reg("geom_rotate_clockwise", self.act_geom_rot_cw)
|
|
1355
|
+
reg("geom_rotate_counterclockwise",self.act_geom_rot_ccw)
|
|
1356
|
+
reg("geom_rotate_180", self.act_geom_rot_180)
|
|
1357
|
+
reg("geom_rotate_any", self.act_geom_rot_any)
|
|
1358
|
+
reg("geom_rescale", self.act_geom_rescale)
|
|
1359
|
+
reg("project_new", self.act_project_new)
|
|
1360
|
+
reg("project_save", self.act_project_save)
|
|
1361
|
+
reg("project_load", self.act_project_load)
|
|
1362
|
+
reg("image_combine", self.act_image_combine)
|
|
1363
|
+
reg("psf_viewer", self.act_psf_viewer)
|
|
1364
|
+
reg("plate_solve", self.act_plate_solve)
|
|
1365
|
+
reg("star_align", self.act_star_align)
|
|
1366
|
+
reg("star_register", self.act_star_register)
|
|
1367
|
+
reg("mosaic_master", self.act_mosaic_master)
|
|
1368
|
+
reg("image_peeker", self.act_image_peeker)
|
|
1369
|
+
reg("live_stacking", self.act_live_stacking)
|
|
1370
|
+
reg("stacking_suite", self.act_stacking_suite)
|
|
1371
|
+
reg("supernova_hunter", self.act_supernova_hunter)
|
|
1372
|
+
reg("star_spikes", self.act_star_spikes)
|
|
1373
|
+
reg("astrospike", self.act_astrospike)
|
|
1374
|
+
reg("exo_detector", self.act_exo_detector)
|
|
1375
|
+
reg("isophote", self.act_isophote)
|
|
1376
|
+
reg("rgb_align", self.act_rgb_align)
|
|
1377
|
+
reg("whats_in_my_sky", self.act_whats_in_my_sky)
|
|
1378
|
+
reg("whats_in_my_image", self.act_wimi)
|
|
1379
|
+
reg("linear_fit", self.act_linear_fit)
|
|
1380
|
+
reg("debayer", self.act_debayer)
|
|
1381
|
+
reg("cosmicclarity", self.actCosmicUI)
|
|
1382
|
+
reg("cosmicclaritysat", self.actCosmicSat)
|
|
1383
|
+
reg("aberrationai", self.actAberrationAI)
|
|
1384
|
+
reg("view_bundles", self.act_view_bundles)
|
|
1385
|
+
reg("function_bundles", self.act_function_bundles)
|
|
1386
|
+
|
|
1387
|
+
def _restore_toolbar_order(self, tb, settings_key: str):
|
|
1388
|
+
"""
|
|
1389
|
+
Restore toolbar action order from QSettings, using command_id/objectName.
|
|
1390
|
+
Unknown actions and separators keep their relative order at the end.
|
|
1391
|
+
"""
|
|
1392
|
+
if not hasattr(self, "settings"):
|
|
1393
|
+
return
|
|
1394
|
+
|
|
1395
|
+
order = self.settings.value(settings_key, None)
|
|
1396
|
+
if not order:
|
|
1397
|
+
return
|
|
1398
|
+
|
|
1399
|
+
# QSettings may return QVariantList or str; normalize to Python list[str]
|
|
1400
|
+
if isinstance(order, str):
|
|
1401
|
+
# if you ever decide to JSON-encode, you could json.loads here
|
|
1402
|
+
order = [order]
|
|
1403
|
+
try:
|
|
1404
|
+
order_list = list(order)
|
|
1405
|
+
except Exception:
|
|
1406
|
+
return
|
|
1407
|
+
|
|
1408
|
+
actions = list(tb.actions())
|
|
1409
|
+
|
|
1410
|
+
def _cid(act):
|
|
1411
|
+
return act.property("command_id") or act.objectName() or ""
|
|
1412
|
+
|
|
1413
|
+
rank = {str(cid): i for i, cid in enumerate(order_list)}
|
|
1414
|
+
big = len(rank) + len(actions) + 10
|
|
1415
|
+
|
|
1416
|
+
indexed = list(enumerate(actions))
|
|
1417
|
+
indexed.sort(
|
|
1418
|
+
key=lambda pair: (
|
|
1419
|
+
rank.get(str(_cid(pair[1])), big),
|
|
1420
|
+
pair[0],
|
|
1421
|
+
)
|
|
1422
|
+
)
|
|
1423
|
+
|
|
1424
|
+
tb.clear()
|
|
1425
|
+
for _, act in indexed:
|
|
1426
|
+
tb.addAction(act)
|
|
1427
|
+
|
|
1428
|
+
def _restore_toolbar_memberships(self):
|
|
1429
|
+
"""
|
|
1430
|
+
Restore which toolbar each action belongs to, based on Toolbar/Assignments.
|
|
1431
|
+
|
|
1432
|
+
We:
|
|
1433
|
+
- Read JSON {command_id: settings_key}.
|
|
1434
|
+
- Collect all DraggableToolBar instances and their settings keys.
|
|
1435
|
+
- Collect all QActions by command_id/objectName.
|
|
1436
|
+
- Move each assigned action to its target toolbar.
|
|
1437
|
+
- Re-apply per-toolbar ordering via _restore_toolbar_order.
|
|
1438
|
+
"""
|
|
1439
|
+
if not hasattr(self, "settings"):
|
|
1440
|
+
return
|
|
1441
|
+
|
|
1442
|
+
try:
|
|
1443
|
+
raw = self.settings.value("Toolbar/Assignments", "", type=str) or ""
|
|
1444
|
+
except Exception:
|
|
1445
|
+
return
|
|
1446
|
+
|
|
1447
|
+
try:
|
|
1448
|
+
mapping = json.loads(raw) if raw else {}
|
|
1449
|
+
except Exception:
|
|
1450
|
+
return
|
|
1451
|
+
|
|
1452
|
+
if not mapping:
|
|
1453
|
+
return
|
|
1454
|
+
|
|
1455
|
+
# Gather all DraggableToolBar instances
|
|
1456
|
+
from setiastro.saspro.shortcuts import DraggableToolBar
|
|
1457
|
+
toolbars: list[DraggableToolBar] = [
|
|
1458
|
+
tb for tb in self.findChildren(DraggableToolBar)
|
|
1459
|
+
]
|
|
1460
|
+
|
|
1461
|
+
tb_by_key: dict[str, DraggableToolBar] = {}
|
|
1462
|
+
for tb in toolbars:
|
|
1463
|
+
key = getattr(tb, "_settings_key", None)
|
|
1464
|
+
if key:
|
|
1465
|
+
tb_by_key[str(key)] = tb
|
|
1466
|
+
|
|
1467
|
+
if not tb_by_key:
|
|
1468
|
+
return
|
|
1469
|
+
|
|
1470
|
+
# Map command_id → QAction
|
|
1471
|
+
from PyQt6.QtGui import QAction
|
|
1472
|
+
acts_by_id: dict[str, QAction] = {}
|
|
1473
|
+
for act in self.findChildren(QAction):
|
|
1474
|
+
cid = act.property("command_id") or act.objectName()
|
|
1475
|
+
if cid:
|
|
1476
|
+
acts_by_id[str(cid)] = act
|
|
1477
|
+
|
|
1478
|
+
# Move actions to their assigned toolbars
|
|
1479
|
+
for cid, key in mapping.items():
|
|
1480
|
+
act = acts_by_id.get(str(cid))
|
|
1481
|
+
tb = tb_by_key.get(str(key))
|
|
1482
|
+
if not act or not tb:
|
|
1483
|
+
continue
|
|
1484
|
+
|
|
1485
|
+
# Remove from any toolbar that currently contains it
|
|
1486
|
+
for t in toolbars:
|
|
1487
|
+
if act in t.actions():
|
|
1488
|
+
t.removeAction(act)
|
|
1489
|
+
# Add to the desired toolbar
|
|
1490
|
+
tb.addAction(act)
|
|
1491
|
+
|
|
1492
|
+
# Re-apply per-toolbar order now that memberships are correct
|
|
1493
|
+
for tb in toolbars:
|
|
1494
|
+
key = getattr(tb, "_settings_key", None)
|
|
1495
|
+
if key:
|
|
1496
|
+
self._restore_toolbar_order(tb, str(key))
|
|
1497
|
+
|
|
1498
|
+
|
|
1499
|
+
def update_undo_redo_action_labels(self):
|
|
1500
|
+
if not hasattr(self, "act_undo"): # not built yet
|
|
1501
|
+
return
|
|
1502
|
+
|
|
1503
|
+
# Always compute against the history root
|
|
1504
|
+
doc = self._active_history_doc()
|
|
1505
|
+
|
|
1506
|
+
if doc:
|
|
1507
|
+
try:
|
|
1508
|
+
can_u = bool(doc.can_undo()) if hasattr(doc, "can_undo") else False
|
|
1509
|
+
except Exception:
|
|
1510
|
+
can_u = False
|
|
1511
|
+
try:
|
|
1512
|
+
can_r = bool(doc.can_redo()) if hasattr(doc, "can_redo") else False
|
|
1513
|
+
except Exception:
|
|
1514
|
+
can_r = False
|
|
1515
|
+
|
|
1516
|
+
undo_name = None
|
|
1517
|
+
redo_name = None
|
|
1518
|
+
try:
|
|
1519
|
+
undo_name = doc.last_undo_name() if hasattr(doc, "last_undo_name") else None
|
|
1520
|
+
except Exception:
|
|
1521
|
+
pass
|
|
1522
|
+
try:
|
|
1523
|
+
redo_name = doc.last_redo_name() if hasattr(doc, "last_redo_name") else None
|
|
1524
|
+
except Exception:
|
|
1525
|
+
pass
|
|
1526
|
+
|
|
1527
|
+
self.act_undo.setText(f"Undo {undo_name}" if (can_u and undo_name) else "Undo")
|
|
1528
|
+
self.act_redo.setText(f"Redo {redo_name}" if (can_r and redo_name) else "Redo")
|
|
1529
|
+
|
|
1530
|
+
self.act_undo.setToolTip("Nothing to undo" if not can_u else (f"Undo: {undo_name}" if undo_name else "Undo last action"))
|
|
1531
|
+
self.act_redo.setToolTip("Nothing to redo" if not can_r else (f"Redo: {redo_name}" if redo_name else "Redo last action"))
|
|
1532
|
+
|
|
1533
|
+
self.act_undo.setStatusTip(self.act_undo.toolTip())
|
|
1534
|
+
self.act_redo.setStatusTip(self.act_redo.toolTip())
|
|
1535
|
+
|
|
1536
|
+
self.act_undo.setEnabled(can_u)
|
|
1537
|
+
self.act_redo.setEnabled(can_r)
|
|
1538
|
+
else:
|
|
1539
|
+
# No active doc
|
|
1540
|
+
for a, tip in ((self.act_undo, "Nothing to undo"),
|
|
1541
|
+
(self.act_redo, "Nothing to redo")):
|
|
1542
|
+
# Normalize label to plain "Undo"/"Redo"
|
|
1543
|
+
base = "Undo" if "Undo" in a.text() else ("Redo" if "Redo" in a.text() else a.text())
|
|
1544
|
+
a.setText(base)
|
|
1545
|
+
a.setToolTip(tip)
|
|
1546
|
+
a.setStatusTip(tip)
|
|
1547
|
+
a.setEnabled(False)
|
|
1548
|
+
|
|
1549
|
+
|
|
1550
|
+
def _sync_link_action_state(self):
|
|
1551
|
+
g = self._current_group_of_active()
|
|
1552
|
+
self.act_link_group.blockSignals(True)
|
|
1553
|
+
try:
|
|
1554
|
+
self.act_link_group.setChecked(bool(g))
|
|
1555
|
+
self.act_link_group.setText(f"Link Pan/Zoom{'' if not g else f' ({g})'}")
|
|
1556
|
+
try:
|
|
1557
|
+
if getattr(self, "_link_btn", None):
|
|
1558
|
+
self._link_btn.setText(self.act_link_group.text())
|
|
1559
|
+
except Exception:
|
|
1560
|
+
pass
|
|
1561
|
+
finally:
|
|
1562
|
+
self.act_link_group.blockSignals(False)
|
|
1563
|
+
|
|
1564
|
+
def _undo_active(self):
|
|
1565
|
+
doc = self._active_history_doc()
|
|
1566
|
+
if doc and getattr(doc, "can_undo", lambda: False)():
|
|
1567
|
+
# Ensure the correct view is active so Qt routes shortcut focus correctly
|
|
1568
|
+
sw = self._subwindow_for_history_doc(doc)
|
|
1569
|
+
if sw is not None:
|
|
1570
|
+
try:
|
|
1571
|
+
self.mdi.setActiveSubWindow(sw)
|
|
1572
|
+
except Exception:
|
|
1573
|
+
pass
|
|
1574
|
+
name = doc.undo()
|
|
1575
|
+
if name:
|
|
1576
|
+
self._log(f"Undo: {name}")
|
|
1577
|
+
# Defer label refresh to end of event loop (lets views repaint first)
|
|
1578
|
+
QTimer.singleShot(0, self.update_undo_redo_action_labels)
|
|
1579
|
+
|
|
1580
|
+
def _redo_active(self):
|
|
1581
|
+
doc = self._active_history_doc()
|
|
1582
|
+
if doc and getattr(doc, "can_redo", lambda: False)():
|
|
1583
|
+
sw = self._subwindow_for_history_doc(doc)
|
|
1584
|
+
if sw is not None:
|
|
1585
|
+
try:
|
|
1586
|
+
self.mdi.setActiveSubWindow(sw)
|
|
1587
|
+
except Exception:
|
|
1588
|
+
pass
|
|
1589
|
+
name = doc.redo()
|
|
1590
|
+
if name:
|
|
1591
|
+
self._log(f"Redo: {name}")
|
|
1592
|
+
QTimer.singleShot(0, self.update_undo_redo_action_labels)
|
|
1593
|
+
|
|
1594
|
+
def _refresh_mask_action_states(self):
|
|
1595
|
+
|
|
1596
|
+
active_doc = self._active_doc()
|
|
1597
|
+
|
|
1598
|
+
can_apply = bool(active_doc and self._list_candidate_mask_sources(exclude_doc=active_doc))
|
|
1599
|
+
can_remove = bool(active_doc and getattr(active_doc, "active_mask_id", None))
|
|
1600
|
+
|
|
1601
|
+
if hasattr(self, "act_apply_mask"):
|
|
1602
|
+
self.act_apply_mask.setEnabled(can_apply)
|
|
1603
|
+
if hasattr(self, "act_remove_mask"):
|
|
1604
|
+
self.act_remove_mask.setEnabled(can_remove)
|
|
1605
|
+
|
|
1606
|
+
# NEW: enable/disable Invert
|
|
1607
|
+
if hasattr(self, "act_invert_mask"):
|
|
1608
|
+
self.act_invert_mask.setEnabled(can_remove)
|
|
1609
|
+
|
|
1610
|
+
vw = self._active_view()
|
|
1611
|
+
overlay_on = bool(getattr(vw, "show_mask_overlay", False)) if vw else False
|
|
1612
|
+
has_mask = bool(active_doc and getattr(active_doc, "active_mask_id", None))
|
|
1613
|
+
|
|
1614
|
+
if hasattr(self, "act_show_mask"):
|
|
1615
|
+
self.act_show_mask.setEnabled(has_mask and not overlay_on)
|
|
1616
|
+
if hasattr(self, "act_hide_mask"):
|
|
1617
|
+
self.act_hide_mask.setEnabled(has_mask and overlay_on)
|
|
1618
|
+
|
|
1619
|
+
|