setiastrosuitepro 1.6.2__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.
- setiastro/images/rotatearbitrary.png +0 -0
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/backgroundneutral.py +10 -1
- setiastro/saspro/blink_comparator_pro.py +474 -251
- setiastro/saspro/crop_dialog_pro.py +11 -1
- setiastro/saspro/doc_manager.py +1 -1
- setiastro/saspro/function_bundle.py +16 -16
- setiastro/saspro/gui/main_window.py +93 -64
- setiastro/saspro/gui/mixins/dock_mixin.py +31 -18
- setiastro/saspro/gui/mixins/geometry_mixin.py +105 -5
- setiastro/saspro/gui/mixins/menu_mixin.py +1 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +33 -10
- setiastro/saspro/multiscale_decomp.py +710 -256
- setiastro/saspro/remove_stars_preset.py +55 -13
- setiastro/saspro/resources.py +30 -11
- setiastro/saspro/selective_color.py +79 -20
- setiastro/saspro/shortcuts.py +94 -21
- setiastro/saspro/stacking_suite.py +296 -107
- setiastro/saspro/star_alignment.py +275 -330
- setiastro/saspro/status_log_dock.py +1 -1
- setiastro/saspro/swap_manager.py +77 -42
- setiastro/saspro/translations/all_source_strings.json +1588 -516
- setiastro/saspro/translations/ar_translations.py +915 -684
- setiastro/saspro/translations/de_translations.py +442 -463
- setiastro/saspro/translations/es_translations.py +277 -47
- setiastro/saspro/translations/fr_translations.py +279 -47
- setiastro/saspro/translations/hi_translations.py +253 -21
- setiastro/saspro/translations/integrate_translations.py +3 -2
- setiastro/saspro/translations/it_translations.py +1211 -161
- setiastro/saspro/translations/ja_translations.py +3340 -3107
- setiastro/saspro/translations/pt_translations.py +3315 -3337
- setiastro/saspro/translations/ru_translations.py +351 -117
- setiastro/saspro/translations/saspro_ar.qm +0 -0
- setiastro/saspro/translations/saspro_ar.ts +15902 -138
- 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 +14733 -135
- 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 +11766 -168
- setiastro/saspro/translations/saspro_sw.qm +0 -0
- setiastro/saspro/translations/saspro_sw.ts +15115 -135
- setiastro/saspro/translations/saspro_uk.qm +0 -0
- setiastro/saspro/translations/saspro_uk.ts +11206 -6729
- setiastro/saspro/translations/saspro_zh.qm +0 -0
- setiastro/saspro/translations/saspro_zh.ts +10581 -7812
- setiastro/saspro/translations/sw_translations.py +282 -56
- setiastro/saspro/translations/uk_translations.py +264 -35
- setiastro/saspro/translations/zh_translations.py +282 -47
- setiastro/saspro/view_bundle.py +17 -17
- setiastro/saspro/widgets/minigame/game.js +11 -6
- setiastro/saspro/widgets/resource_monitor.py +26 -0
- setiastro/saspro/widgets/spinboxes.py +18 -0
- setiastro/saspro/wimi.py +65 -65
- setiastro/saspro/wims.py +33 -33
- setiastro/saspro/window_shelf.py +2 -2
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.4.dist-info}/METADATA +7 -7
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.4.dist-info}/RECORD +72 -71
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.4.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.4.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.4.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.4.dist-info}/licenses/license.txt +0 -0
|
@@ -12,7 +12,7 @@ from setiastro.saspro.legacy.image_manager import save_image, load_image
|
|
|
12
12
|
# Reuse helpers & plumbing from the interactive module
|
|
13
13
|
from .remove_stars import (
|
|
14
14
|
_ProcThread, _ProcDialog,
|
|
15
|
-
|
|
15
|
+
_mtf_params_unlinked, _apply_mtf_unlinked_rgb, _invert_mtf_unlinked_rgb,
|
|
16
16
|
_active_mask3_from_doc, _mask_blend_with_doc_mask, _push_as_new_doc,
|
|
17
17
|
_ensure_exec_bit,
|
|
18
18
|
)
|
|
@@ -125,24 +125,48 @@ def _run_starnet_headless(main, doc, p):
|
|
|
125
125
|
processing_image = processing_image.astype(np.float32, copy=False)
|
|
126
126
|
|
|
127
127
|
is_linear = bool(p.get("linear", True))
|
|
128
|
-
did_stretch =
|
|
129
|
-
|
|
128
|
+
did_stretch = is_linear
|
|
129
|
+
|
|
130
|
+
# sanitize + normalize if needed (keep exactly like interactive)
|
|
131
|
+
processing_image = np.nan_to_num(processing_image, nan=0.0, posinf=0.0, neginf=0.0).astype(np.float32, copy=False)
|
|
132
|
+
|
|
133
|
+
scale_factor = float(np.max(processing_image)) if processing_image.size else 1.0
|
|
134
|
+
processing_norm = (processing_image / scale_factor) if scale_factor > 1.0 else processing_image
|
|
135
|
+
processing_norm = np.clip(processing_norm, 0.0, 1.0)
|
|
136
|
+
|
|
137
|
+
img_for_starnet = processing_norm
|
|
138
|
+
|
|
130
139
|
if is_linear:
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
140
|
+
mtf_params = _mtf_params_unlinked(processing_norm, shadows_clipping=-2.8, targetbg=0.25)
|
|
141
|
+
img_for_starnet = _apply_mtf_unlinked_rgb(processing_norm, mtf_params)
|
|
142
|
+
|
|
143
|
+
# stash for inverse step (same keys as interactive)
|
|
144
|
+
try:
|
|
145
|
+
setattr(main, "_starnet_stat_meta", {
|
|
146
|
+
"scheme": "siril_mtf",
|
|
147
|
+
"s": np.asarray(mtf_params["s"], dtype=np.float32),
|
|
148
|
+
"m": np.asarray(mtf_params["m"], dtype=np.float32),
|
|
149
|
+
"h": np.asarray(mtf_params["h"], dtype=np.float32),
|
|
150
|
+
"scale": float(scale_factor),
|
|
151
|
+
})
|
|
152
|
+
except Exception:
|
|
153
|
+
pass
|
|
134
154
|
else:
|
|
135
|
-
|
|
136
|
-
|
|
155
|
+
try:
|
|
156
|
+
if hasattr(main, "_starnet_stat_meta"):
|
|
157
|
+
delattr(main, "_starnet_stat_meta")
|
|
158
|
+
except Exception:
|
|
159
|
+
pass
|
|
160
|
+
|
|
137
161
|
|
|
138
162
|
starnet_dir = os.path.dirname(exe) or os.getcwd()
|
|
139
163
|
in_path = os.path.join(starnet_dir, "imagetoremovestars.tif")
|
|
140
164
|
out_path = os.path.join(starnet_dir, "starless.tif")
|
|
141
165
|
|
|
142
166
|
try:
|
|
143
|
-
save_image(
|
|
144
|
-
|
|
145
|
-
|
|
167
|
+
save_image(img_for_starnet, in_path, original_format="tif",
|
|
168
|
+
bit_depth="16-bit", original_header=None, is_mono=False,
|
|
169
|
+
image_meta=None, file_meta=None)
|
|
146
170
|
except Exception as e:
|
|
147
171
|
QMessageBox.critical(main, "StarNet", f"Failed to write input TIFF:\n{e}")
|
|
148
172
|
return
|
|
@@ -179,12 +203,30 @@ def _finish_starnet(main, doc, rc, dlg, in_path, out_path, did_stretch):
|
|
|
179
203
|
starless_rgb = starless_rgb.astype(np.float32, copy=False)
|
|
180
204
|
|
|
181
205
|
if did_stretch:
|
|
206
|
+
meta = getattr(main, "_starnet_stat_meta", None)
|
|
207
|
+
if isinstance(meta, dict) and meta.get("scheme") == "siril_mtf":
|
|
208
|
+
try:
|
|
209
|
+
p = {
|
|
210
|
+
"s": np.asarray(meta.get("s"), dtype=np.float32),
|
|
211
|
+
"m": np.asarray(meta.get("m"), dtype=np.float32),
|
|
212
|
+
"h": np.asarray(meta.get("h"), dtype=np.float32),
|
|
213
|
+
}
|
|
214
|
+
inv = _invert_mtf_unlinked_rgb(starless_rgb, p)
|
|
215
|
+
sc = float(meta.get("scale", 1.0))
|
|
216
|
+
if sc > 1.0:
|
|
217
|
+
inv *= sc
|
|
218
|
+
starless_rgb = np.clip(inv, 0.0, 1.0).astype(np.float32, copy=False)
|
|
219
|
+
except Exception:
|
|
220
|
+
pass
|
|
221
|
+
|
|
222
|
+
# cleanup so it can't leak
|
|
182
223
|
try:
|
|
183
|
-
|
|
184
|
-
|
|
224
|
+
if hasattr(main, "_starnet_stat_meta"):
|
|
225
|
+
delattr(main, "_starnet_stat_meta")
|
|
185
226
|
except Exception:
|
|
186
227
|
pass
|
|
187
228
|
|
|
229
|
+
|
|
188
230
|
# original as RGB
|
|
189
231
|
orig = np.asarray(doc.image)
|
|
190
232
|
if orig.ndim == 2: original_rgb = np.stack([orig]*3, axis=-1)
|
setiastro/saspro/resources.py
CHANGED
|
@@ -123,17 +123,31 @@ def _get_base_path() -> str:
|
|
|
123
123
|
|
|
124
124
|
|
|
125
125
|
def _resource_path(filename: str) -> str:
|
|
126
|
-
"""Get full path to a resource file."""
|
|
127
126
|
base = _get_base_path()
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
127
|
+
fn = filename
|
|
128
|
+
|
|
129
|
+
is_img = fn.lower().endswith(('.png','.ico','.gif','.icns','.svg','.jpg','.jpeg','.bmp'))
|
|
130
|
+
if is_img:
|
|
131
|
+
candidates = [
|
|
132
|
+
os.path.join(base, 'images', fn),
|
|
133
|
+
os.path.join(base, 'setiastro', 'images', fn),
|
|
134
|
+
os.path.join(base, 'setiastro', 'saspro', 'images', fn),
|
|
135
|
+
]
|
|
136
|
+
for p in candidates:
|
|
137
|
+
if os.path.exists(p):
|
|
138
|
+
return p
|
|
139
|
+
|
|
140
|
+
# data / other files
|
|
141
|
+
candidates = [
|
|
142
|
+
os.path.join(base, fn),
|
|
143
|
+
os.path.join(base, 'setiastro', fn),
|
|
144
|
+
os.path.join(base, 'setiastro', 'saspro', fn),
|
|
145
|
+
]
|
|
146
|
+
for p in candidates:
|
|
147
|
+
if os.path.exists(p):
|
|
148
|
+
return p
|
|
149
|
+
|
|
150
|
+
return os.path.join(base, fn)
|
|
137
151
|
|
|
138
152
|
|
|
139
153
|
class Icons:
|
|
@@ -210,6 +224,7 @@ class Icons:
|
|
|
210
224
|
ROTATE_CW = property(lambda self: _resource_path('rotateclockwise.png'))
|
|
211
225
|
ROTATE_CCW = property(lambda self: _resource_path('rotatecounterclockwise.png'))
|
|
212
226
|
ROTATE_180 = property(lambda self: _resource_path('rotate180.png'))
|
|
227
|
+
ROTATE_ANY = property(lambda self: _resource_path('rotatearbitrary.png'))
|
|
213
228
|
RESCALE = property(lambda self: _resource_path('rescale.png'))
|
|
214
229
|
|
|
215
230
|
# Masks
|
|
@@ -395,6 +410,7 @@ def _init_legacy_paths():
|
|
|
395
410
|
'rotateclockwise_path': get_icon_path('rotateclockwise.png'),
|
|
396
411
|
'rotatecounterclockwise_path': get_icon_path('rotatecounterclockwise.png'),
|
|
397
412
|
'rotate180_path': get_icon_path('rotate180.png'),
|
|
413
|
+
'rotatearbitrary_path': get_icon_path('rotatearbitrary.png'),
|
|
398
414
|
'maskcreate_path': get_icon_path('maskcreate.png'),
|
|
399
415
|
'maskapply_path': get_icon_path('maskapply.png'),
|
|
400
416
|
'maskremove_path': get_icon_path('maskremove.png'),
|
|
@@ -471,9 +487,12 @@ globals().update(_legacy)
|
|
|
471
487
|
|
|
472
488
|
|
|
473
489
|
# Background for startup
|
|
474
|
-
background_startup_path =
|
|
490
|
+
background_startup_path = _resource_path('Background_startup.jpg')
|
|
475
491
|
_legacy['background_startup_path'] = background_startup_path
|
|
476
492
|
|
|
493
|
+
# QML helper
|
|
494
|
+
resource_monitor_qml = _resource_path(os.path.join("qml", "ResourceMonitor.qml"))
|
|
495
|
+
|
|
477
496
|
# Export list for `from setiastro.saspro.resources import *`
|
|
478
497
|
__all__ = [
|
|
479
498
|
'Icons', 'Resources',
|
|
@@ -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/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
|
+
}
|