setiastrosuitepro 1.6.1.post1__py3-none-any.whl → 1.6.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of setiastrosuitepro might be problematic. Click here for more details.
- setiastro/images/Background_startup.jpg +0 -0
- setiastro/images/rotatearbitrary.png +0 -0
- setiastro/qml/ResourceMonitor.qml +126 -0
- setiastro/saspro/__main__.py +162 -25
- setiastro/saspro/_generated/build_info.py +2 -1
- setiastro/saspro/abe.py +62 -11
- setiastro/saspro/aberration_ai.py +3 -3
- setiastro/saspro/add_stars.py +5 -2
- setiastro/saspro/astrobin_exporter.py +3 -0
- setiastro/saspro/astrospike_python.py +3 -1
- setiastro/saspro/autostretch.py +4 -2
- setiastro/saspro/backgroundneutral.py +60 -9
- setiastro/saspro/batch_convert.py +3 -0
- setiastro/saspro/batch_renamer.py +3 -0
- setiastro/saspro/blemish_blaster.py +3 -0
- setiastro/saspro/blink_comparator_pro.py +474 -251
- setiastro/saspro/cheat_sheet.py +50 -15
- setiastro/saspro/clahe.py +27 -1
- setiastro/saspro/comet_stacking.py +103 -38
- setiastro/saspro/convo.py +3 -0
- setiastro/saspro/copyastro.py +3 -0
- setiastro/saspro/cosmicclarity.py +70 -45
- setiastro/saspro/crop_dialog_pro.py +28 -1
- setiastro/saspro/curve_editor_pro.py +18 -0
- setiastro/saspro/debayer.py +3 -0
- setiastro/saspro/doc_manager.py +40 -17
- setiastro/saspro/fitsmodifier.py +3 -0
- setiastro/saspro/frequency_separation.py +8 -2
- setiastro/saspro/function_bundle.py +18 -16
- setiastro/saspro/generate_translations.py +715 -1
- setiastro/saspro/ghs_dialog_pro.py +3 -0
- setiastro/saspro/graxpert.py +3 -0
- setiastro/saspro/gui/main_window.py +364 -92
- setiastro/saspro/gui/mixins/dock_mixin.py +119 -7
- setiastro/saspro/gui/mixins/file_mixin.py +7 -0
- setiastro/saspro/gui/mixins/geometry_mixin.py +105 -5
- setiastro/saspro/gui/mixins/menu_mixin.py +29 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +33 -10
- setiastro/saspro/gui/statistics_dialog.py +47 -0
- setiastro/saspro/halobgon.py +29 -3
- setiastro/saspro/histogram.py +3 -0
- setiastro/saspro/history_explorer.py +2 -0
- setiastro/saspro/i18n.py +22 -10
- setiastro/saspro/image_combine.py +3 -0
- setiastro/saspro/image_peeker_pro.py +3 -0
- setiastro/saspro/imageops/stretch.py +5 -13
- setiastro/saspro/isophote.py +3 -0
- setiastro/saspro/legacy/numba_utils.py +64 -47
- setiastro/saspro/linear_fit.py +3 -0
- setiastro/saspro/live_stacking.py +13 -2
- setiastro/saspro/mask_creation.py +3 -0
- setiastro/saspro/mfdeconv.py +5 -0
- setiastro/saspro/morphology.py +30 -5
- setiastro/saspro/multiscale_decomp.py +713 -256
- setiastro/saspro/nbtorgb_stars.py +12 -2
- setiastro/saspro/numba_utils.py +148 -47
- setiastro/saspro/ops/scripts.py +77 -17
- setiastro/saspro/ops/settings.py +1 -43
- setiastro/saspro/perfect_palette_picker.py +1 -0
- setiastro/saspro/pixelmath.py +6 -2
- setiastro/saspro/plate_solver.py +1 -0
- setiastro/saspro/remove_green.py +18 -1
- setiastro/saspro/remove_stars.py +136 -162
- setiastro/saspro/remove_stars_preset.py +55 -13
- setiastro/saspro/resources.py +36 -10
- setiastro/saspro/rgb_combination.py +1 -0
- setiastro/saspro/rgbalign.py +4 -4
- setiastro/saspro/save_options.py +1 -0
- setiastro/saspro/selective_color.py +79 -20
- setiastro/saspro/sfcc.py +50 -8
- setiastro/saspro/shortcuts.py +94 -21
- setiastro/saspro/signature_insert.py +3 -0
- setiastro/saspro/stacking_suite.py +924 -446
- setiastro/saspro/star_alignment.py +291 -331
- setiastro/saspro/star_spikes.py +116 -32
- setiastro/saspro/star_stretch.py +38 -1
- setiastro/saspro/stat_stretch.py +35 -3
- setiastro/saspro/status_log_dock.py +1 -1
- setiastro/saspro/subwindow.py +63 -2
- setiastro/saspro/supernovaasteroidhunter.py +3 -0
- setiastro/saspro/swap_manager.py +77 -42
- setiastro/saspro/translations/all_source_strings.json +4726 -0
- setiastro/saspro/translations/ar_translations.py +4096 -0
- setiastro/saspro/translations/de_translations.py +441 -446
- setiastro/saspro/translations/es_translations.py +278 -32
- setiastro/saspro/translations/fr_translations.py +280 -32
- setiastro/saspro/translations/hi_translations.py +3803 -0
- setiastro/saspro/translations/integrate_translations.py +38 -1
- setiastro/saspro/translations/it_translations.py +1211 -145
- setiastro/saspro/translations/ja_translations.py +556 -307
- setiastro/saspro/translations/pt_translations.py +3316 -3322
- 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 +14428 -133
- setiastro/saspro/translations/saspro_es.qm +0 -0
- setiastro/saspro/translations/saspro_es.ts +11503 -7821
- setiastro/saspro/translations/saspro_fr.qm +0 -0
- setiastro/saspro/translations/saspro_fr.ts +11168 -7812
- 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 +14347 -7821
- setiastro/saspro/translations/saspro_ja.qm +0 -0
- setiastro/saspro/translations/saspro_ja.ts +14860 -137
- setiastro/saspro/translations/saspro_pt.qm +0 -0
- setiastro/saspro/translations/saspro_pt.ts +14904 -137
- 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 +10581 -7812
- setiastro/saspro/translations/sw_translations.py +3897 -0
- setiastro/saspro/translations/uk_translations.py +3929 -0
- setiastro/saspro/translations/zh_translations.py +283 -32
- setiastro/saspro/versioning.py +36 -5
- setiastro/saspro/view_bundle.py +20 -17
- setiastro/saspro/wavescale_hdr.py +22 -1
- setiastro/saspro/wavescalede.py +23 -1
- setiastro/saspro/whitebalance.py +39 -3
- 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/resource_monitor.py +263 -0
- setiastro/saspro/widgets/spinboxes.py +18 -0
- setiastro/saspro/widgets/wavelet_utils.py +52 -20
- setiastro/saspro/wimi.py +100 -80
- setiastro/saspro/wims.py +33 -33
- setiastro/saspro/window_shelf.py +2 -2
- {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/METADATA +15 -4
- {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/RECORD +139 -115
- {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/licenses/license.txt +0 -0
|
@@ -467,7 +467,7 @@ class SelectiveColorCorrection(QDialog):
|
|
|
467
467
|
|
|
468
468
|
self.img = np.clip(self.document.image.astype(np.float32), 0.0, 1.0)
|
|
469
469
|
self.preview_img = self.img.copy()
|
|
470
|
-
|
|
470
|
+
self._syncing_hue = False
|
|
471
471
|
self._imported_mask_full = None # full-res mask (H x W) float32 0..1
|
|
472
472
|
self._imported_mask_name = None # nice label to show in UI
|
|
473
473
|
self._use_imported_mask = False # checkbox state mirror
|
|
@@ -555,6 +555,36 @@ class SelectiveColorCorrection(QDialog):
|
|
|
555
555
|
self.hue_wheel.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum)
|
|
556
556
|
gl.addWidget(self.hue_wheel, 1, 0, 7, 2)
|
|
557
557
|
|
|
558
|
+
# Wheel -> sliders/spins (so dragging wheel updates UI and mask)
|
|
559
|
+
def _wheel_to_sliders(s: int, e: int):
|
|
560
|
+
# If user is dragging the wheel, we’re now custom
|
|
561
|
+
if not self._setting_preset and self.dd_preset.currentText() != "Custom":
|
|
562
|
+
self.dd_preset.blockSignals(True)
|
|
563
|
+
self.dd_preset.setCurrentText("Custom")
|
|
564
|
+
self.dd_preset.blockSignals(False)
|
|
565
|
+
|
|
566
|
+
# Update BOTH sliders and spins, without ping-pong
|
|
567
|
+
self._syncing_hue = True
|
|
568
|
+
try:
|
|
569
|
+
s = int(s) % 360
|
|
570
|
+
e = int(e) % 360
|
|
571
|
+
|
|
572
|
+
for w, val in (
|
|
573
|
+
(self.sl_h1, s), (self.sp_h1, s),
|
|
574
|
+
(self.sl_h2, e), (self.sp_h2, e),
|
|
575
|
+
):
|
|
576
|
+
w.blockSignals(True)
|
|
577
|
+
w.setValue(val)
|
|
578
|
+
w.blockSignals(False)
|
|
579
|
+
finally:
|
|
580
|
+
self._syncing_hue = False
|
|
581
|
+
|
|
582
|
+
self._schedule_mask()
|
|
583
|
+
|
|
584
|
+
self.hue_wheel.rangeChanged.connect(_wheel_to_sliders)
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
|
|
558
588
|
# Helper: integer slider + spin (0..360)
|
|
559
589
|
def _deg_pair(grid: QGridLayout, label: str, row: int):
|
|
560
590
|
grid.addWidget(QLabel(label), row, 2)
|
|
@@ -574,7 +604,8 @@ class SelectiveColorCorrection(QDialog):
|
|
|
574
604
|
|
|
575
605
|
# Row 3: chroma + lightness
|
|
576
606
|
gl.addWidget(QLabel("Min chroma:"), 3, 2)
|
|
577
|
-
self.ds_minC = QDoubleSpinBox(); self.ds_minC.setRange(0,1); self.ds_minC.setSingleStep(0.05); self.ds_minC.setValue(0.
|
|
607
|
+
self.ds_minC = QDoubleSpinBox(); self.ds_minC.setRange(0,1); self.ds_minC.setSingleStep(0.05); self.ds_minC.setValue(0.05)
|
|
608
|
+
|
|
578
609
|
self.ds_minC.valueChanged.connect(self._recompute_mask_and_preview)
|
|
579
610
|
gl.addWidget(self.ds_minC, 3, 3)
|
|
580
611
|
|
|
@@ -741,10 +772,12 @@ class SelectiveColorCorrection(QDialog):
|
|
|
741
772
|
right.addLayout(zoom_row)
|
|
742
773
|
|
|
743
774
|
self.lbl_help = QLabel(
|
|
744
|
-
"🖱️ <b>Click</b>:
|
|
775
|
+
"🖱️ <b>Click</b>: show hue • "
|
|
776
|
+
"<b>Shift + Click</b>: select that color • "
|
|
745
777
|
"<b>Ctrl + Click & Drag</b>: pan • "
|
|
746
778
|
"<b>Ctrl + Wheel</b>: zoom"
|
|
747
779
|
)
|
|
780
|
+
|
|
748
781
|
self.lbl_help.setWordWrap(True)
|
|
749
782
|
self.lbl_help.setTextFormat(Qt.TextFormat.RichText)
|
|
750
783
|
self.lbl_help.setStyleSheet("color: #888; font-size: 11px;")
|
|
@@ -797,12 +830,20 @@ class SelectiveColorCorrection(QDialog):
|
|
|
797
830
|
w.valueChanged.connect(self._schedule_adjustments)
|
|
798
831
|
|
|
799
832
|
def _sliders_to_wheel(_=None):
|
|
833
|
+
if getattr(self, "_syncing_hue", False):
|
|
834
|
+
return
|
|
835
|
+
|
|
800
836
|
if not self._setting_preset and self.dd_preset.currentText() != "Custom":
|
|
837
|
+
self.dd_preset.blockSignals(True)
|
|
801
838
|
self.dd_preset.setCurrentText("Custom")
|
|
802
|
-
|
|
839
|
+
self.dd_preset.blockSignals(False)
|
|
840
|
+
|
|
841
|
+
s = int(self.sp_h1.value())
|
|
842
|
+
e = int(self.sp_h2.value())
|
|
803
843
|
self.hue_wheel.setRange(s, e, notify=False)
|
|
804
844
|
self._schedule_mask()
|
|
805
845
|
|
|
846
|
+
|
|
806
847
|
self.sp_h1.valueChanged.connect(_sliders_to_wheel)
|
|
807
848
|
self.sp_h2.valueChanged.connect(_sliders_to_wheel)
|
|
808
849
|
self.sl_h1.valueChanged.connect(_sliders_to_wheel)
|
|
@@ -973,7 +1014,7 @@ class SelectiveColorCorrection(QDialog):
|
|
|
973
1014
|
w.setValue(int(val))
|
|
974
1015
|
w.blockSignals(False)
|
|
975
1016
|
|
|
976
|
-
setv(self.ds_minC, 0.
|
|
1017
|
+
setv(self.ds_minC, 0.05)
|
|
977
1018
|
setv(self.ds_minL, 0.0)
|
|
978
1019
|
setv(self.ds_maxL, 1.0)
|
|
979
1020
|
setv(self.ds_smooth, 10.0)
|
|
@@ -1018,23 +1059,27 @@ class SelectiveColorCorrection(QDialog):
|
|
|
1018
1059
|
self._recompute_mask_and_preview()
|
|
1019
1060
|
|
|
1020
1061
|
|
|
1021
|
-
def _schedule_adjustments(self, delay_ms: int | None = None):
|
|
1062
|
+
def _schedule_adjustments(self, *_, delay_ms: int | None = None):
|
|
1022
1063
|
if delay_ms is None:
|
|
1023
1064
|
delay_ms = getattr(self, "_adj_delay_ms", 200)
|
|
1024
|
-
|
|
1065
|
+
|
|
1025
1066
|
if not hasattr(self, "_adj_timer"):
|
|
1026
1067
|
return
|
|
1027
|
-
self._adj_timer.stop()
|
|
1028
|
-
self._adj_timer.start(int(delay_ms))
|
|
1029
1068
|
|
|
1069
|
+
ms = max(1, int(delay_ms)) # never allow 0/negative
|
|
1070
|
+
self._adj_timer.stop()
|
|
1071
|
+
self._adj_timer.start(ms)
|
|
1030
1072
|
|
|
1031
|
-
def _schedule_mask(self, delay_ms: int | None = None):
|
|
1032
|
-
"""Debounce mask recomputation for hue changes."""
|
|
1073
|
+
def _schedule_mask(self, *_, delay_ms: int | None = None):
|
|
1033
1074
|
if delay_ms is None:
|
|
1034
|
-
delay_ms = self
|
|
1035
|
-
|
|
1075
|
+
delay_ms = getattr(self, "_mask_delay_ms", 200)
|
|
1076
|
+
|
|
1077
|
+
if not hasattr(self, "_mask_timer"):
|
|
1078
|
+
return
|
|
1079
|
+
|
|
1080
|
+
ms = max(1, int(delay_ms))
|
|
1036
1081
|
self._mask_timer.stop()
|
|
1037
|
-
self._mask_timer.start(
|
|
1082
|
+
self._mask_timer.start(ms)
|
|
1038
1083
|
|
|
1039
1084
|
|
|
1040
1085
|
def _sample_hue_deg_from_base(self, x: int, y: int) -> float | None:
|
|
@@ -1190,16 +1235,30 @@ class SelectiveColorCorrection(QDialog):
|
|
|
1190
1235
|
intervals = _PRESETS.get(txt, [])
|
|
1191
1236
|
if intervals:
|
|
1192
1237
|
lo, hi = (intervals[0][0], intervals[-1][1]) if len(intervals) > 1 else intervals[0]
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
self.
|
|
1196
|
-
|
|
1197
|
-
|
|
1238
|
+
|
|
1239
|
+
# --- NEW: keep wheel + sliders + spins all in sync ---
|
|
1240
|
+
self._syncing_hue = True
|
|
1241
|
+
try:
|
|
1242
|
+
# update wheel silently
|
|
1243
|
+
self.hue_wheel.setRange(int(lo), int(hi), notify=False)
|
|
1244
|
+
|
|
1245
|
+
# update both sliders and spins (no ping-pong)
|
|
1246
|
+
for w, val in (
|
|
1247
|
+
(self.sl_h1, int(lo)), (self.sp_h1, int(lo)),
|
|
1248
|
+
(self.sl_h2, int(hi)), (self.sp_h2, int(hi)),
|
|
1249
|
+
):
|
|
1250
|
+
w.blockSignals(True)
|
|
1251
|
+
w.setValue(val)
|
|
1252
|
+
w.blockSignals(False)
|
|
1253
|
+
|
|
1254
|
+
self.hue_wheel.update() # ensure repaint
|
|
1255
|
+
finally:
|
|
1256
|
+
self._syncing_hue = False
|
|
1257
|
+
|
|
1198
1258
|
self._recompute_mask_and_preview()
|
|
1199
1259
|
finally:
|
|
1200
1260
|
self._setting_preset = False
|
|
1201
1261
|
|
|
1202
|
-
|
|
1203
1262
|
def _downsample(self, img, max_dim=1024):
|
|
1204
1263
|
h, w = img.shape[:2]
|
|
1205
1264
|
s = max(h, w)
|
setiastro/saspro/sfcc.py
CHANGED
|
@@ -346,6 +346,9 @@ class SFCCDialog(QDialog):
|
|
|
346
346
|
def __init__(self, doc_manager, sasp_data_path, parent=None):
|
|
347
347
|
super().__init__(parent)
|
|
348
348
|
self.setWindowTitle(self.tr("Spectral Flux Color Calibration"))
|
|
349
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
350
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
351
|
+
self.setModal(False)
|
|
349
352
|
self.setMinimumSize(800, 600)
|
|
350
353
|
|
|
351
354
|
self.doc_manager = doc_manager
|
|
@@ -1155,18 +1158,56 @@ class SFCCDialog(QDialog):
|
|
|
1155
1158
|
diag_meas_BG, diag_exp_BG = [], []
|
|
1156
1159
|
enriched = []
|
|
1157
1160
|
|
|
1161
|
+
# --- Optimization: Pre-calculate integrals for unique templates ---
|
|
1162
|
+
unique_simbad_types = set(m["template"] for m in raw_matches)
|
|
1163
|
+
|
|
1164
|
+
# Map simbad_type -> pickles_template_name
|
|
1165
|
+
simbad_to_pickles = {}
|
|
1166
|
+
pickles_templates_needed = set()
|
|
1167
|
+
|
|
1168
|
+
for sp in unique_simbad_types:
|
|
1169
|
+
cands = pickles_match_for_simbad(sp, getattr(self, "pickles_templates", []))
|
|
1170
|
+
if cands:
|
|
1171
|
+
pickles_name = cands[0]
|
|
1172
|
+
simbad_to_pickles[sp] = pickles_name
|
|
1173
|
+
pickles_templates_needed.add(pickles_name)
|
|
1174
|
+
|
|
1175
|
+
# Pre-calc integrals for each unique Pickles template
|
|
1176
|
+
# Cache structure: template_name -> (S_sr, S_sg, S_sb)
|
|
1177
|
+
template_integrals = {}
|
|
1178
|
+
|
|
1179
|
+
# Cache for load_sed to avoid re-reading even across different calls if desired,
|
|
1180
|
+
# but here we just optimize the loop.
|
|
1181
|
+
|
|
1182
|
+
for pname in pickles_templates_needed:
|
|
1183
|
+
try:
|
|
1184
|
+
wl_s, fl_s = load_sed(pname)
|
|
1185
|
+
fs_i = np.interp(wl_grid, wl_s, fl_s, left=0., right=0.)
|
|
1186
|
+
|
|
1187
|
+
S_sr = np.trapezoid(fs_i * T_sys_R, x=wl_grid)
|
|
1188
|
+
S_sg = np.trapezoid(fs_i * T_sys_G, x=wl_grid)
|
|
1189
|
+
S_sb = np.trapezoid(fs_i * T_sys_B, x=wl_grid)
|
|
1190
|
+
|
|
1191
|
+
template_integrals[pname] = (S_sr, S_sg, S_sb)
|
|
1192
|
+
except Exception as e:
|
|
1193
|
+
print(f"[SFCC] Warning: failed to load/integrate template {pname}: {e}")
|
|
1194
|
+
|
|
1195
|
+
# --- Main Match Loop ---
|
|
1158
1196
|
for m in raw_matches:
|
|
1159
1197
|
xi, yi, sp = m["x_pix"], m["y_pix"], m["template"]
|
|
1160
1198
|
Rm = float(base[yi, xi, 0]); Gm = float(base[yi, xi, 1]); Bm = float(base[yi, xi, 2])
|
|
1161
1199
|
if Gm <= 0: continue
|
|
1162
1200
|
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1201
|
+
# 1. Resolve Simbad -> Pickles
|
|
1202
|
+
pname = simbad_to_pickles.get(sp)
|
|
1203
|
+
if not pname: continue
|
|
1204
|
+
|
|
1205
|
+
# 2. Retrieve pre-calced integrals
|
|
1206
|
+
integrals = template_integrals.get(pname)
|
|
1207
|
+
if not integrals: continue
|
|
1208
|
+
|
|
1209
|
+
S_sr, S_sg, S_sb = integrals
|
|
1210
|
+
|
|
1170
1211
|
if S_sg <= 0: continue
|
|
1171
1212
|
|
|
1172
1213
|
exp_RG = S_sr / S_sg; exp_BG = S_sb / S_sg
|
|
@@ -1180,7 +1221,8 @@ class SFCCDialog(QDialog):
|
|
|
1180
1221
|
"S_star_R": S_sr, "S_star_G": S_sg, "S_star_B": S_sb,
|
|
1181
1222
|
"exp_RG": exp_RG, "exp_BG": exp_BG
|
|
1182
1223
|
})
|
|
1183
|
-
|
|
1224
|
+
|
|
1225
|
+
self._last_matched = enriched
|
|
1184
1226
|
diag_meas_RG = np.array(diag_meas_RG); diag_exp_RG = np.array(diag_exp_RG)
|
|
1185
1227
|
diag_meas_BG = np.array(diag_meas_BG); diag_exp_BG = np.array(diag_exp_BG)
|
|
1186
1228
|
if diag_meas_RG.size == 0 or diag_meas_BG.size == 0:
|
setiastro/saspro/shortcuts.py
CHANGED
|
@@ -209,6 +209,17 @@ class DraggableToolBar(QToolBar):
|
|
|
209
209
|
s.setValue(self._settings_key, ids)
|
|
210
210
|
|
|
211
211
|
|
|
212
|
+
def _is_locked(self) -> bool:
|
|
213
|
+
"""Check if toolbar icon movement is locked globally."""
|
|
214
|
+
s = self._settings()
|
|
215
|
+
# Default to False (unlocked)
|
|
216
|
+
return s.value("UI/ToolbarLocked", False, type=bool)
|
|
217
|
+
|
|
218
|
+
def _set_locked(self, locked: bool):
|
|
219
|
+
"""Set the global lock state."""
|
|
220
|
+
s = self._settings()
|
|
221
|
+
s.setValue("UI/ToolbarLocked", locked)
|
|
222
|
+
|
|
212
223
|
# install/remove our event filter when actions are added/removed
|
|
213
224
|
def actionEvent(self, e):
|
|
214
225
|
super().actionEvent(e)
|
|
@@ -259,6 +270,7 @@ class DraggableToolBar(QToolBar):
|
|
|
259
270
|
if delta.manhattanLength() > QApplication.startDragDistance():
|
|
260
271
|
mods_now = QApplication.keyboardModifiers()
|
|
261
272
|
had_mod = self._press_had_mod.get(obj, False)
|
|
273
|
+
|
|
262
274
|
# CASE 1: had/has modifiers → create desktop shortcut / function-bundle drag (existing behavior)
|
|
263
275
|
if had_mod or self._mods_ok(mods_now):
|
|
264
276
|
act = self._find_action_for_button(obj)
|
|
@@ -270,6 +282,20 @@ class DraggableToolBar(QToolBar):
|
|
|
270
282
|
return True # consume
|
|
271
283
|
else:
|
|
272
284
|
# CASE 2: plain drag (no modifiers) → reorder within this toolbar
|
|
285
|
+
# CHECK LOCK STATE FIRST
|
|
286
|
+
if self._is_locked():
|
|
287
|
+
# Lock is active: DO NOT start drag.
|
|
288
|
+
# Should we consume the event?
|
|
289
|
+
# If we consume it, the button won't feel "pressed" anymore if the user keeps dragging?
|
|
290
|
+
# Actually, if we just return False, standard QToolButton behavior applies (it might think it's being pressed).
|
|
291
|
+
# However, we want to prevent the *reorder* logic.
|
|
292
|
+
# So simply doing nothing here is enough to prevent the reorder drag from starting.
|
|
293
|
+
|
|
294
|
+
# But we might want to let the user know, or just silently fail distinctively?
|
|
295
|
+
# Silently failing distinctively is what the user asked for (prevent involuntary move).
|
|
296
|
+
# If we return False, the button keeps tracking the mouse, which is fine (it won't click unless released inside).
|
|
297
|
+
return False
|
|
298
|
+
|
|
273
299
|
self._start_reorder_drag_for_button(obj)
|
|
274
300
|
self._suppress_release.add(obj)
|
|
275
301
|
self._press_pos.pop(obj, None)
|
|
@@ -461,28 +487,42 @@ class DraggableToolBar(QToolBar):
|
|
|
461
487
|
def _show_toolbutton_context_menu(self, btn: QToolButton, act: QAction, gpos: QPoint):
|
|
462
488
|
m = QMenu(btn)
|
|
463
489
|
|
|
464
|
-
m.addAction("Create Desktop Shortcut", lambda: self._add_shortcut_for_action(act))
|
|
490
|
+
m.addAction(self.tr("Create Desktop Shortcut"), lambda: self._add_shortcut_for_action(act))
|
|
465
491
|
|
|
466
492
|
# Hide this icon
|
|
467
493
|
cid = self._action_id(act)
|
|
468
494
|
if cid:
|
|
469
495
|
m.addSeparator()
|
|
470
|
-
m.addAction("Hide this icon", lambda: self._set_action_hidden(act, True))
|
|
496
|
+
m.addAction(self.tr("Hide this icon"), lambda: self._set_action_hidden(act, True))
|
|
471
497
|
|
|
472
498
|
# (Optional) teach users about Alt+Drag:
|
|
473
499
|
m.addSeparator()
|
|
474
|
-
tip = m.addAction("Tip: Alt+Drag to create")
|
|
500
|
+
tip = m.addAction(self.tr("Tip: Alt+Drag to create"))
|
|
475
501
|
tip.setEnabled(False)
|
|
476
502
|
|
|
477
503
|
m.exec(gpos)
|
|
478
504
|
|
|
505
|
+
|
|
479
506
|
def contextMenuEvent(self, ev):
|
|
480
507
|
# Right-click on empty toolbar area
|
|
481
508
|
m = QMenu(self)
|
|
482
509
|
|
|
510
|
+
# 1. Lock/Unlock Action
|
|
511
|
+
is_locked = self._is_locked()
|
|
512
|
+
act_lock = m.addAction(self.tr("Lock Toolbar Icons"))
|
|
513
|
+
act_lock.setCheckable(True)
|
|
514
|
+
act_lock.setChecked(is_locked)
|
|
515
|
+
|
|
516
|
+
def _toggle_lock(checked):
|
|
517
|
+
self._set_locked(checked)
|
|
518
|
+
|
|
519
|
+
act_lock.triggered.connect(_toggle_lock)
|
|
520
|
+
|
|
521
|
+
m.addSeparator()
|
|
522
|
+
|
|
483
523
|
# Submenu listing hidden actions for this toolbar
|
|
484
524
|
hidden = self._load_hidden_set()
|
|
485
|
-
sub = m.addMenu("Show hidden…")
|
|
525
|
+
sub = m.addMenu(self.tr("Show hidden…"))
|
|
486
526
|
|
|
487
527
|
# Build list from actions that are currently invisible
|
|
488
528
|
any_hidden = False
|
|
@@ -496,7 +536,7 @@ class DraggableToolBar(QToolBar):
|
|
|
496
536
|
sub.setEnabled(False)
|
|
497
537
|
|
|
498
538
|
m.addSeparator()
|
|
499
|
-
m.addAction("Reset hidden icons", self._reset_hidden_icons)
|
|
539
|
+
m.addAction(self.tr("Reset hidden icons"), self._reset_hidden_icons)
|
|
500
540
|
|
|
501
541
|
m.exec(ev.globalPos())
|
|
502
542
|
|
|
@@ -515,7 +555,7 @@ _PRESET_UI_IDS = {
|
|
|
515
555
|
"remove_green","star_align","background_neutral","white_balance","clahe",
|
|
516
556
|
"morphology","pixel_math","rgb_align","signature_insert","signature_adder",
|
|
517
557
|
"signature","halo_b_gon","geom_rescale","rescale","debayer","image_combine",
|
|
518
|
-
"star_spikes","diffraction_spikes", "multiscale_decomp",
|
|
558
|
+
"star_spikes","diffraction_spikes", "multiscale_decomp","geom_rotate_any",
|
|
519
559
|
}
|
|
520
560
|
|
|
521
561
|
def _has_preset_editor_for_command(command_id: str) -> bool:
|
|
@@ -548,6 +588,12 @@ def _open_preset_editor_for_command(parent, command_id: str, initial: dict | Non
|
|
|
548
588
|
})
|
|
549
589
|
return dlg.result_dict() if dlg.exec() == QDialog.DialogCode.Accepted else None
|
|
550
590
|
|
|
591
|
+
if command_id == "geom_rotate_any":
|
|
592
|
+
dlg = _GeomRotateAnyPresetDialog(parent, initial=cur or {
|
|
593
|
+
"angle_deg": 0.0,
|
|
594
|
+
})
|
|
595
|
+
return dlg.result_dict() if dlg.exec() == QDialog.DialogCode.Accepted else None
|
|
596
|
+
|
|
551
597
|
if command_id == "curves":
|
|
552
598
|
dlg = _CurvesPresetDialog(parent, initial=cur or {"shape":"linear","amount":0.5,"mode":"K (Brightness)"})
|
|
553
599
|
return dlg.result_dict() if dlg.exec() == QDialog.DialogCode.Accepted else None
|
|
@@ -741,18 +787,18 @@ class ShortcutButton(QToolButton):
|
|
|
741
787
|
# --- Context menu (run / preset / delete) ----------------------------
|
|
742
788
|
def _context_menu(self, pos):
|
|
743
789
|
m = QMenu(self)
|
|
744
|
-
m.addAction("Run", lambda: self._mgr.trigger(self.command_id))
|
|
790
|
+
m.addAction(self.tr("Run"), lambda: self._mgr.trigger(self.command_id))
|
|
745
791
|
m.addSeparator()
|
|
746
|
-
m.addAction("Edit Preset…", self._edit_preset_ui)
|
|
747
|
-
m.addAction("Clear Preset", lambda: self._save_preset(None))
|
|
748
|
-
m.addAction("Rename…", self._rename) # ← NEW
|
|
792
|
+
m.addAction(self.tr("Edit Preset…"), self._edit_preset_ui)
|
|
793
|
+
m.addAction(self.tr("Clear Preset"), lambda: self._save_preset(None))
|
|
794
|
+
m.addAction(self.tr("Rename…"), self._rename) # ← NEW
|
|
749
795
|
m.addSeparator()
|
|
750
|
-
m.addAction("Delete", self._delete)
|
|
796
|
+
m.addAction(self.tr("Delete"), self._delete)
|
|
751
797
|
m.exec(self.mapToGlobal(pos))
|
|
752
798
|
|
|
753
799
|
def _rename(self):
|
|
754
800
|
current = self.text()
|
|
755
|
-
new_name, ok = QInputDialog.getText(self, "Rename Shortcut", "Name:", text=current)
|
|
801
|
+
new_name, ok = QInputDialog.getText(self, self.tr("Rename Shortcut"), self.tr("Name:"), text=current)
|
|
756
802
|
if not ok or not new_name.strip():
|
|
757
803
|
return
|
|
758
804
|
self.setText(new_name.strip())
|
|
@@ -764,21 +810,21 @@ class ShortcutButton(QToolButton):
|
|
|
764
810
|
result = _open_preset_editor_for_command(self, cid, cur)
|
|
765
811
|
if result is not None:
|
|
766
812
|
self._save_preset(result)
|
|
767
|
-
QMessageBox.information(self, "Preset saved", "Preset stored on shortcut.")
|
|
813
|
+
QMessageBox.information(self, self.tr("Preset saved"), self.tr("Preset stored on shortcut."))
|
|
768
814
|
return
|
|
769
815
|
|
|
770
816
|
# Fallback: JSON editor
|
|
771
817
|
raw = json.dumps(cur or {}, indent=2)
|
|
772
|
-
text, ok = QInputDialog.getMultiLineText(self, "Edit Preset (JSON)", "Preset:", raw)
|
|
818
|
+
text, ok = QInputDialog.getMultiLineText(self, self.tr("Edit Preset (JSON)"), self.tr("Preset:"), raw)
|
|
773
819
|
if ok:
|
|
774
820
|
try:
|
|
775
821
|
preset = json.loads(text or "{}")
|
|
776
822
|
if not isinstance(preset, dict):
|
|
777
|
-
raise ValueError("Preset must be a JSON object")
|
|
823
|
+
raise ValueError(self.tr("Preset must be a JSON object"))
|
|
778
824
|
self._save_preset(preset)
|
|
779
|
-
QMessageBox.information(self, "Preset saved", "Preset stored on shortcut.")
|
|
825
|
+
QMessageBox.information(self, self.tr("Preset saved"), self.tr("Preset stored on shortcut."))
|
|
780
826
|
except Exception as e:
|
|
781
|
-
QMessageBox.warning(self, "Invalid JSON", str(e))
|
|
827
|
+
QMessageBox.warning(self, self.tr("Invalid JSON"), str(e))
|
|
782
828
|
|
|
783
829
|
|
|
784
830
|
def _start_command_drag(self):
|
|
@@ -1066,11 +1112,11 @@ class ShortcutCanvas(QWidget):
|
|
|
1066
1112
|
def contextMenuEvent(self, e):
|
|
1067
1113
|
menu = QMenu(self)
|
|
1068
1114
|
has_sel = bool(self._mgr.selected)
|
|
1069
|
-
a_del = menu.addAction("Delete Selected", self._mgr.delete_selected); a_del.setEnabled(has_sel)
|
|
1070
|
-
a_clr = menu.addAction("Clear Selection", self._mgr.clear_selection); a_clr.setEnabled(has_sel)
|
|
1115
|
+
a_del = menu.addAction(self.tr("Delete Selected"), self._mgr.delete_selected); a_del.setEnabled(has_sel)
|
|
1116
|
+
a_clr = menu.addAction(self.tr("Clear Selection"), self._mgr.clear_selection); a_clr.setEnabled(has_sel)
|
|
1071
1117
|
menu.addSeparator()
|
|
1072
|
-
a_vb = menu.addAction("View Bundles…", lambda: _open_view_bundles_from_canvas(self))
|
|
1073
|
-
a_fb = menu.addAction("Function Bundles…", lambda: _open_function_bundles_from_canvas(self))
|
|
1118
|
+
a_vb = menu.addAction(self.tr("View Bundles…"), lambda: _open_view_bundles_from_canvas(self))
|
|
1119
|
+
a_fb = menu.addAction(self.tr("Function Bundles…"), lambda: _open_function_bundles_from_canvas(self))
|
|
1074
1120
|
menu.exec(e.globalPos())
|
|
1075
1121
|
|
|
1076
1122
|
|
|
@@ -3041,3 +3087,30 @@ class _RGBAlignPresetDialog(QDialog):
|
|
|
3041
3087
|
"new_doc": bool(self.chk_new.isChecked()),
|
|
3042
3088
|
}
|
|
3043
3089
|
|
|
3090
|
+
class _GeomRotateAnyPresetDialog(QDialog):
|
|
3091
|
+
def __init__(self, parent=None, initial: dict | None = None):
|
|
3092
|
+
super().__init__(parent)
|
|
3093
|
+
self.setWindowTitle("Arbitrary Rotation — Preset")
|
|
3094
|
+
init = dict(initial or {})
|
|
3095
|
+
|
|
3096
|
+
self.spin_angle = QDoubleSpinBox()
|
|
3097
|
+
self.spin_angle.setRange(-360.0, 360.0)
|
|
3098
|
+
self.spin_angle.setDecimals(2)
|
|
3099
|
+
self.spin_angle.setSingleStep(0.25)
|
|
3100
|
+
self.spin_angle.setValue(float(init.get("angle_deg", init.get("angle", 0.0))))
|
|
3101
|
+
|
|
3102
|
+
form = QFormLayout(self)
|
|
3103
|
+
form.addRow("Angle (degrees):", self.spin_angle)
|
|
3104
|
+
|
|
3105
|
+
btns = QDialogButtonBox(
|
|
3106
|
+
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel,
|
|
3107
|
+
parent=self
|
|
3108
|
+
)
|
|
3109
|
+
btns.accepted.connect(self.accept)
|
|
3110
|
+
btns.rejected.connect(self.reject)
|
|
3111
|
+
form.addRow(btns)
|
|
3112
|
+
|
|
3113
|
+
def result_dict(self) -> dict:
|
|
3114
|
+
return {
|
|
3115
|
+
"angle_deg": float(self.spin_angle.value()),
|
|
3116
|
+
}
|
|
@@ -363,6 +363,9 @@ class SignatureInsertDialogPro(QDialog):
|
|
|
363
363
|
def __init__(self, parent, doc, icon: QIcon | None = None):
|
|
364
364
|
super().__init__(parent)
|
|
365
365
|
self.setWindowTitle(self.tr("Signature / Insert"))
|
|
366
|
+
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
367
|
+
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
368
|
+
self.setModal(False)
|
|
366
369
|
if icon:
|
|
367
370
|
try: self.setWindowIcon(icon)
|
|
368
371
|
except Exception as e:
|