setiastrosuitepro 1.6.0__py3-none-any.whl → 1.6.4.post1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/__main__.py +228 -67
- setiastro/saspro/_generated/build_info.py +2 -1
- setiastro/saspro/abe.py +76 -25
- setiastro/saspro/aberration_ai.py +14 -14
- setiastro/saspro/add_stars.py +15 -12
- setiastro/saspro/astrobin_exporter.py +61 -58
- setiastro/saspro/astrospike_python.py +3 -1
- setiastro/saspro/autostretch.py +4 -2
- setiastro/saspro/backgroundneutral.py +65 -14
- setiastro/saspro/batch_convert.py +8 -5
- setiastro/saspro/batch_renamer.py +39 -36
- setiastro/saspro/blemish_blaster.py +15 -12
- setiastro/saspro/blink_comparator_pro.py +605 -379
- setiastro/saspro/cheat_sheet.py +62 -17
- setiastro/saspro/clahe.py +34 -8
- setiastro/saspro/comet_stacking.py +103 -38
- setiastro/saspro/common_tr.py +107 -0
- setiastro/saspro/continuum_subtract.py +7 -7
- setiastro/saspro/convo.py +12 -9
- setiastro/saspro/copyastro.py +3 -0
- setiastro/saspro/cosmicclarity.py +77 -52
- setiastro/saspro/crop_dialog_pro.py +80 -45
- setiastro/saspro/curve_editor_pro.py +51 -33
- setiastro/saspro/debayer.py +6 -3
- setiastro/saspro/doc_manager.py +49 -19
- setiastro/saspro/exoplanet_detector.py +11 -11
- setiastro/saspro/fitsmodifier.py +48 -44
- setiastro/saspro/fix_bom.py +32 -0
- setiastro/saspro/frequency_separation.py +18 -12
- setiastro/saspro/function_bundle.py +18 -16
- setiastro/saspro/generate_translations.py +3092 -0
- setiastro/saspro/ghs_dialog_pro.py +19 -16
- setiastro/saspro/graxpert.py +3 -0
- setiastro/saspro/gui/main_window.py +471 -126
- setiastro/saspro/gui/mixins/dock_mixin.py +123 -11
- setiastro/saspro/gui/mixins/file_mixin.py +25 -20
- setiastro/saspro/gui/mixins/geometry_mixin.py +115 -15
- setiastro/saspro/gui/mixins/header_mixin.py +6 -6
- setiastro/saspro/gui/mixins/mask_mixin.py +8 -8
- setiastro/saspro/gui/mixins/menu_mixin.py +62 -33
- setiastro/saspro/gui/mixins/toolbar_mixin.py +382 -226
- setiastro/saspro/gui/mixins/update_mixin.py +26 -26
- setiastro/saspro/gui/statistics_dialog.py +47 -0
- setiastro/saspro/halobgon.py +29 -3
- setiastro/saspro/header_viewer.py +21 -18
- setiastro/saspro/histogram.py +29 -26
- setiastro/saspro/history_explorer.py +2 -0
- setiastro/saspro/i18n.py +168 -0
- setiastro/saspro/image_combine.py +3 -0
- setiastro/saspro/image_peeker_pro.py +52 -44
- 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 +180 -22
- setiastro/saspro/mfdeconv.py +5 -0
- setiastro/saspro/morphology.py +38 -13
- setiastro/saspro/multiscale_decomp.py +713 -256
- setiastro/saspro/nbtorgb_stars.py +12 -2
- setiastro/saspro/numba_utils.py +149 -48
- setiastro/saspro/ops/scripts.py +77 -17
- setiastro/saspro/ops/settings.py +177 -100
- setiastro/saspro/perfect_palette_picker.py +25 -7
- setiastro/saspro/pixelmath.py +114 -110
- setiastro/saspro/plate_solver.py +118 -108
- setiastro/saspro/remove_green.py +24 -7
- setiastro/saspro/remove_stars.py +136 -162
- setiastro/saspro/remove_stars_preset.py +55 -13
- setiastro/saspro/resources.py +46 -15
- setiastro/saspro/rgb_combination.py +19 -18
- setiastro/saspro/rgbalign.py +11 -11
- setiastro/saspro/save_options.py +5 -4
- setiastro/saspro/selective_color.py +84 -25
- setiastro/saspro/sfcc.py +119 -72
- setiastro/saspro/shortcuts.py +345 -36
- setiastro/saspro/signature_insert.py +4 -1
- setiastro/saspro/stacking_suite.py +2066 -1119
- setiastro/saspro/star_alignment.py +291 -331
- setiastro/saspro/star_spikes.py +137 -53
- setiastro/saspro/star_stretch.py +47 -10
- setiastro/saspro/stat_stretch.py +52 -16
- setiastro/saspro/status_log_dock.py +1 -1
- setiastro/saspro/subwindow.py +97 -36
- setiastro/saspro/supernovaasteroidhunter.py +68 -61
- 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 +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 +20 -17
- setiastro/saspro/wavescale_hdr.py +54 -33
- setiastro/saspro/wavescale_hdr_preset.py +6 -5
- setiastro/saspro/wavescalede.py +54 -31
- setiastro/saspro/wavescalede_preset.py +9 -7
- setiastro/saspro/whitebalance.py +58 -22
- setiastro/saspro/widgets/common_utilities.py +12 -11
- 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 +8 -8
- 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 +7996 -0
- setiastro/saspro/wims.py +578 -0
- setiastro/saspro/window_shelf.py +2 -2
- {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/METADATA +15 -3
- setiastrosuitepro-1.6.4.post1.dist-info/RECORD +368 -0
- setiastrosuitepro-1.6.0.dist-info/RECORD +0 -174
- {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/licenses/license.txt +0 -0
setiastro/saspro/shortcuts.py
CHANGED
|
@@ -31,6 +31,7 @@ import os # ← NEW
|
|
|
31
31
|
SASS_KIND = "sas.shortcuts"
|
|
32
32
|
SASS_VER = 1
|
|
33
33
|
|
|
34
|
+
TOOLBAR_REORDER_MIME = "application/x-saspro-toolbar-reorder"
|
|
34
35
|
# Accept these endings (case-insensitive)
|
|
35
36
|
OPENABLE_ENDINGS = (
|
|
36
37
|
".png", ".jpg", ".jpeg",
|
|
@@ -115,11 +116,7 @@ class DraggableToolBar(QToolBar):
|
|
|
115
116
|
self._press_had_mod: dict[QToolButton, bool] = {}
|
|
116
117
|
self._suppress_release: set[QToolButton] = set()
|
|
117
118
|
self._settings_key: str | None = None
|
|
118
|
-
|
|
119
|
-
# NEW: called by main window / mixin
|
|
120
|
-
def setSettingsKey(self, key: str):
|
|
121
|
-
"""Set the settings key for persisting toolbar state."""
|
|
122
|
-
self._settings_key = str(key)
|
|
119
|
+
self.setAcceptDrops(True)
|
|
123
120
|
|
|
124
121
|
def _mods_ok(self, mods: Qt.KeyboardModifiers) -> bool:
|
|
125
122
|
return bool(mods & (
|
|
@@ -128,6 +125,101 @@ class DraggableToolBar(QToolBar):
|
|
|
128
125
|
Qt.KeyboardModifier.ShiftModifier
|
|
129
126
|
))
|
|
130
127
|
|
|
128
|
+
# NEW: called by main window / mixin
|
|
129
|
+
def setSettingsKey(self, key: str):
|
|
130
|
+
self._settings_key = str(key)
|
|
131
|
+
|
|
132
|
+
def _settings(self):
|
|
133
|
+
mw = self.window()
|
|
134
|
+
s = getattr(mw, "settings", None)
|
|
135
|
+
if s is None:
|
|
136
|
+
from PyQt6.QtCore import QSettings
|
|
137
|
+
s = QSettings()
|
|
138
|
+
return s
|
|
139
|
+
|
|
140
|
+
def _action_id(self, act: QAction) -> str | None:
|
|
141
|
+
cid = act.property("command_id") or act.objectName()
|
|
142
|
+
return str(cid) if cid else None
|
|
143
|
+
|
|
144
|
+
def _hidden_key(self) -> str | None:
|
|
145
|
+
if not self._settings_key:
|
|
146
|
+
return None
|
|
147
|
+
return f"{self._settings_key}/Hidden"
|
|
148
|
+
|
|
149
|
+
def _load_hidden_set(self) -> set[str]:
|
|
150
|
+
k = self._hidden_key()
|
|
151
|
+
if not k:
|
|
152
|
+
return set()
|
|
153
|
+
s = self._settings()
|
|
154
|
+
raw = s.value(k, [], type=list)
|
|
155
|
+
# QSettings sometimes returns strings instead of list depending on backend
|
|
156
|
+
if isinstance(raw, str):
|
|
157
|
+
return {raw}
|
|
158
|
+
return {str(x) for x in (raw or [])}
|
|
159
|
+
|
|
160
|
+
def _save_hidden_set(self, hidden: set[str]):
|
|
161
|
+
k = self._hidden_key()
|
|
162
|
+
if not k:
|
|
163
|
+
return
|
|
164
|
+
s = self._settings()
|
|
165
|
+
s.setValue(k, sorted(hidden))
|
|
166
|
+
|
|
167
|
+
def _set_action_hidden(self, act: QAction, hide: bool):
|
|
168
|
+
cid = self._action_id(act)
|
|
169
|
+
if not cid:
|
|
170
|
+
return
|
|
171
|
+
hidden = self._load_hidden_set()
|
|
172
|
+
if hide:
|
|
173
|
+
hidden.add(cid)
|
|
174
|
+
act.setVisible(False)
|
|
175
|
+
else:
|
|
176
|
+
hidden.discard(cid)
|
|
177
|
+
act.setVisible(True)
|
|
178
|
+
self._save_hidden_set(hidden)
|
|
179
|
+
|
|
180
|
+
def apply_hidden_state(self):
|
|
181
|
+
"""Call after toolbar is populated / order restored."""
|
|
182
|
+
hidden = self._load_hidden_set()
|
|
183
|
+
if not hidden:
|
|
184
|
+
return
|
|
185
|
+
for act in self.actions():
|
|
186
|
+
cid = self._action_id(act)
|
|
187
|
+
if cid and cid in hidden:
|
|
188
|
+
act.setVisible(False)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _persist_order(self):
|
|
192
|
+
"""Persist current action order (by command_id/objectName) to QSettings."""
|
|
193
|
+
if not self._settings_key:
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
# Prefer main-window settings if available
|
|
197
|
+
mw = self.window()
|
|
198
|
+
s = getattr(mw, "settings", None)
|
|
199
|
+
if s is None:
|
|
200
|
+
from PyQt6.QtCore import QSettings
|
|
201
|
+
s = QSettings()
|
|
202
|
+
|
|
203
|
+
ids: list[str] = []
|
|
204
|
+
for act in self.actions():
|
|
205
|
+
cid = act.property("command_id") or act.objectName()
|
|
206
|
+
if cid:
|
|
207
|
+
ids.append(str(cid))
|
|
208
|
+
|
|
209
|
+
s.setValue(self._settings_key, ids)
|
|
210
|
+
|
|
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
|
+
|
|
131
223
|
# install/remove our event filter when actions are added/removed
|
|
132
224
|
def actionEvent(self, e):
|
|
133
225
|
super().actionEvent(e)
|
|
@@ -163,32 +255,56 @@ class DraggableToolBar(QToolBar):
|
|
|
163
255
|
self._show_toolbutton_context_menu(obj, act, ev.globalPos())
|
|
164
256
|
return True
|
|
165
257
|
return False
|
|
258
|
+
|
|
166
259
|
# L-press: remember start + whether a drag-modifier was held
|
|
167
260
|
if ev.type() == QEvent.Type.MouseButtonPress and ev.button() == Qt.MouseButton.LeftButton:
|
|
168
261
|
self._press_pos[obj] = ev.globalPosition().toPoint()
|
|
169
262
|
self._press_had_mod[obj] = self._mods_ok(QApplication.keyboardModifiers())
|
|
170
263
|
return False # allow normal press visuals
|
|
171
264
|
|
|
172
|
-
# Move with L held:
|
|
265
|
+
# Move with L held:
|
|
173
266
|
if ev.type() == QEvent.Type.MouseMove and (ev.buttons() & Qt.MouseButton.LeftButton):
|
|
174
267
|
start = self._press_pos.get(obj)
|
|
175
268
|
if start is not None:
|
|
176
269
|
delta = ev.globalPosition().toPoint() - start
|
|
177
|
-
if
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
270
|
+
if delta.manhattanLength() > QApplication.startDragDistance():
|
|
271
|
+
mods_now = QApplication.keyboardModifiers()
|
|
272
|
+
had_mod = self._press_had_mod.get(obj, False)
|
|
273
|
+
|
|
274
|
+
# CASE 1: had/has modifiers → create desktop shortcut / function-bundle drag (existing behavior)
|
|
275
|
+
if had_mod or self._mods_ok(mods_now):
|
|
276
|
+
act = self._find_action_for_button(obj)
|
|
277
|
+
if act:
|
|
278
|
+
self._start_drag_for_action(act)
|
|
279
|
+
self._suppress_release.add(obj)
|
|
280
|
+
self._press_pos.pop(obj, None)
|
|
281
|
+
self._press_had_mod.pop(obj, None)
|
|
282
|
+
return True # consume
|
|
283
|
+
else:
|
|
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
|
+
|
|
299
|
+
self._start_reorder_drag_for_button(obj)
|
|
184
300
|
self._suppress_release.add(obj)
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
301
|
+
self._press_pos.pop(obj, None)
|
|
302
|
+
self._press_had_mod.pop(obj, None)
|
|
303
|
+
return True # consume
|
|
304
|
+
|
|
189
305
|
return False
|
|
190
306
|
|
|
191
|
-
# Release: if we started
|
|
307
|
+
# Release: if we started any drag, swallow the release so click won't fire
|
|
192
308
|
if ev.type() == QEvent.Type.MouseButtonRelease and ev.button() == Qt.MouseButton.LeftButton:
|
|
193
309
|
self._press_pos.pop(obj, None)
|
|
194
310
|
self._press_had_mod.pop(obj, None)
|
|
@@ -199,6 +315,7 @@ class DraggableToolBar(QToolBar):
|
|
|
199
315
|
|
|
200
316
|
return super().eventFilter(obj, ev)
|
|
201
317
|
|
|
318
|
+
|
|
202
319
|
def _start_drag_for_action(self, act: QAction):
|
|
203
320
|
act_id = act.property("command_id") or act.objectName()
|
|
204
321
|
if not act_id:
|
|
@@ -235,6 +352,31 @@ class DraggableToolBar(QToolBar):
|
|
|
235
352
|
drag.setHotSpot(pm.rect().center())
|
|
236
353
|
drag.exec(Qt.DropAction.CopyAction)
|
|
237
354
|
|
|
355
|
+
def _start_reorder_drag_for_button(self, btn: QToolButton):
|
|
356
|
+
"""
|
|
357
|
+
Start a drag whose only purpose is to reorder actions within THIS toolbar.
|
|
358
|
+
No presets, no desktop shortcuts.
|
|
359
|
+
"""
|
|
360
|
+
act = self._find_action_for_button(btn)
|
|
361
|
+
if act is None:
|
|
362
|
+
return
|
|
363
|
+
|
|
364
|
+
md = QMimeData()
|
|
365
|
+
# Tag this as an internal toolbar-reorder drag
|
|
366
|
+
md.setData(TOOLBAR_REORDER_MIME, b"1")
|
|
367
|
+
|
|
368
|
+
drag = QDrag(btn)
|
|
369
|
+
drag.setMimeData(md)
|
|
370
|
+
|
|
371
|
+
pm = act.icon().pixmap(32, 32) if not act.icon().isNull() else QPixmap(32, 32)
|
|
372
|
+
if pm.isNull():
|
|
373
|
+
pm = QPixmap(32, 32)
|
|
374
|
+
pm.fill(Qt.GlobalColor.darkGray)
|
|
375
|
+
drag.setPixmap(pm)
|
|
376
|
+
drag.setHotSpot(pm.rect().center())
|
|
377
|
+
drag.exec(Qt.DropAction.MoveAction)
|
|
378
|
+
|
|
379
|
+
|
|
238
380
|
def _find_action_for_button(self, btn: QToolButton) -> QAction | None:
|
|
239
381
|
# Find the QAction that owns this toolbutton
|
|
240
382
|
for a in self.actions():
|
|
@@ -242,6 +384,85 @@ class DraggableToolBar(QToolBar):
|
|
|
242
384
|
return a
|
|
243
385
|
return None
|
|
244
386
|
|
|
387
|
+
def dragEnterEvent(self, e):
|
|
388
|
+
# Accept toolbar-reorder drags from any DraggableToolBar
|
|
389
|
+
if e.mimeData().hasFormat(TOOLBAR_REORDER_MIME):
|
|
390
|
+
src = e.source()
|
|
391
|
+
if isinstance(src, QToolButton) and isinstance(src.parent(), DraggableToolBar):
|
|
392
|
+
e.acceptProposedAction()
|
|
393
|
+
return
|
|
394
|
+
super().dragEnterEvent(e)
|
|
395
|
+
|
|
396
|
+
def dragMoveEvent(self, e):
|
|
397
|
+
if e.mimeData().hasFormat(TOOLBAR_REORDER_MIME):
|
|
398
|
+
src = e.source()
|
|
399
|
+
if isinstance(src, QToolButton) and isinstance(src.parent(), DraggableToolBar):
|
|
400
|
+
e.acceptProposedAction()
|
|
401
|
+
return
|
|
402
|
+
super().dragMoveEvent(e)
|
|
403
|
+
|
|
404
|
+
def dropEvent(self, e):
|
|
405
|
+
if e.mimeData().hasFormat(TOOLBAR_REORDER_MIME):
|
|
406
|
+
src_btn = e.source()
|
|
407
|
+
if isinstance(src_btn, QToolButton):
|
|
408
|
+
src_tb = src_btn.parent()
|
|
409
|
+
if isinstance(src_tb, DraggableToolBar):
|
|
410
|
+
# Find the QAction that belongs to the dragged button
|
|
411
|
+
src_act = src_tb._find_action_for_button(src_btn)
|
|
412
|
+
if src_act is None:
|
|
413
|
+
e.ignore()
|
|
414
|
+
return
|
|
415
|
+
|
|
416
|
+
pos = e.position().toPoint()
|
|
417
|
+
target_act = self.actionAt(pos)
|
|
418
|
+
|
|
419
|
+
# Remove from source toolbar and insert into this one
|
|
420
|
+
src_tb.removeAction(src_act)
|
|
421
|
+
if target_act is None:
|
|
422
|
+
self.addAction(src_act)
|
|
423
|
+
else:
|
|
424
|
+
self.insertAction(target_act, src_act)
|
|
425
|
+
|
|
426
|
+
# Persist order for both toolbars
|
|
427
|
+
self._persist_order()
|
|
428
|
+
if src_tb is not self:
|
|
429
|
+
src_tb._persist_order()
|
|
430
|
+
# Also persist the cross-toolbar assignment
|
|
431
|
+
self._update_assignment_for_action(src_act)
|
|
432
|
+
|
|
433
|
+
e.acceptProposedAction()
|
|
434
|
+
return
|
|
435
|
+
|
|
436
|
+
super().dropEvent(e)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _update_assignment_for_action(self, act: QAction):
|
|
440
|
+
"""
|
|
441
|
+
Persist that this action now belongs to this toolbar (cross-toolbar move).
|
|
442
|
+
Stored as: Toolbar/Assignments → JSON {command_id: settings_key}.
|
|
443
|
+
"""
|
|
444
|
+
if not self._settings_key:
|
|
445
|
+
return
|
|
446
|
+
|
|
447
|
+
cid = act.property("command_id") or act.objectName()
|
|
448
|
+
if not cid:
|
|
449
|
+
return
|
|
450
|
+
|
|
451
|
+
mw = self.window()
|
|
452
|
+
s = getattr(mw, "settings", None)
|
|
453
|
+
if s is None:
|
|
454
|
+
s = QSettings()
|
|
455
|
+
|
|
456
|
+
raw = s.value("Toolbar/Assignments", "", type=str) or ""
|
|
457
|
+
try:
|
|
458
|
+
mapping = json.loads(raw) if raw else {}
|
|
459
|
+
except Exception:
|
|
460
|
+
mapping = {}
|
|
461
|
+
|
|
462
|
+
mapping[str(cid)] = self._settings_key
|
|
463
|
+
s.setValue("Toolbar/Assignments", json.dumps(mapping))
|
|
464
|
+
|
|
465
|
+
|
|
245
466
|
def _add_shortcut_for_action(self, act: QAction):
|
|
246
467
|
# Resolve command id
|
|
247
468
|
act_id = act.property("command_id") or act.objectName()
|
|
@@ -265,12 +486,67 @@ class DraggableToolBar(QToolBar):
|
|
|
265
486
|
|
|
266
487
|
def _show_toolbutton_context_menu(self, btn: QToolButton, act: QAction, gpos: QPoint):
|
|
267
488
|
m = QMenu(btn)
|
|
268
|
-
|
|
489
|
+
|
|
490
|
+
m.addAction(self.tr("Create Desktop Shortcut"), lambda: self._add_shortcut_for_action(act))
|
|
491
|
+
|
|
492
|
+
# Hide this icon
|
|
493
|
+
cid = self._action_id(act)
|
|
494
|
+
if cid:
|
|
495
|
+
m.addSeparator()
|
|
496
|
+
m.addAction(self.tr("Hide this icon"), lambda: self._set_action_hidden(act, True))
|
|
497
|
+
|
|
269
498
|
# (Optional) teach users about Alt+Drag:
|
|
270
499
|
m.addSeparator()
|
|
271
|
-
m.addAction("Tip: Alt+Drag to create"
|
|
500
|
+
tip = m.addAction(self.tr("Tip: Alt+Drag to create"))
|
|
501
|
+
tip.setEnabled(False)
|
|
502
|
+
|
|
272
503
|
m.exec(gpos)
|
|
273
504
|
|
|
505
|
+
|
|
506
|
+
def contextMenuEvent(self, ev):
|
|
507
|
+
# Right-click on empty toolbar area
|
|
508
|
+
m = QMenu(self)
|
|
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
|
+
|
|
523
|
+
# Submenu listing hidden actions for this toolbar
|
|
524
|
+
hidden = self._load_hidden_set()
|
|
525
|
+
sub = m.addMenu(self.tr("Show hidden…"))
|
|
526
|
+
|
|
527
|
+
# Build list from actions that are currently invisible
|
|
528
|
+
any_hidden = False
|
|
529
|
+
for act in self.actions():
|
|
530
|
+
cid = self._action_id(act)
|
|
531
|
+
if cid and (cid in hidden) and (not act.isVisible()):
|
|
532
|
+
any_hidden = True
|
|
533
|
+
sub.addAction(act.text() or cid, lambda a=act: self._set_action_hidden(a, False))
|
|
534
|
+
|
|
535
|
+
if not any_hidden:
|
|
536
|
+
sub.setEnabled(False)
|
|
537
|
+
|
|
538
|
+
m.addSeparator()
|
|
539
|
+
m.addAction(self.tr("Reset hidden icons"), self._reset_hidden_icons)
|
|
540
|
+
|
|
541
|
+
m.exec(ev.globalPos())
|
|
542
|
+
|
|
543
|
+
def _reset_hidden_icons(self):
|
|
544
|
+
# Show everything and clear hidden list
|
|
545
|
+
for act in self.actions():
|
|
546
|
+
act.setVisible(True)
|
|
547
|
+
self._save_hidden_set(set())
|
|
548
|
+
|
|
549
|
+
|
|
274
550
|
_PRESET_UI_IDS = {
|
|
275
551
|
"stat_stretch","star_stretch","crop","curves","ghs","abe","graxpert",
|
|
276
552
|
"remove_stars","cosmic_clarity","cosmic","cosmicclarity",
|
|
@@ -279,7 +555,7 @@ _PRESET_UI_IDS = {
|
|
|
279
555
|
"remove_green","star_align","background_neutral","white_balance","clahe",
|
|
280
556
|
"morphology","pixel_math","rgb_align","signature_insert","signature_adder",
|
|
281
557
|
"signature","halo_b_gon","geom_rescale","rescale","debayer","image_combine",
|
|
282
|
-
"star_spikes","diffraction_spikes", "multiscale_decomp",
|
|
558
|
+
"star_spikes","diffraction_spikes", "multiscale_decomp","geom_rotate_any",
|
|
283
559
|
}
|
|
284
560
|
|
|
285
561
|
def _has_preset_editor_for_command(command_id: str) -> bool:
|
|
@@ -312,6 +588,12 @@ def _open_preset_editor_for_command(parent, command_id: str, initial: dict | Non
|
|
|
312
588
|
})
|
|
313
589
|
return dlg.result_dict() if dlg.exec() == QDialog.DialogCode.Accepted else None
|
|
314
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
|
+
|
|
315
597
|
if command_id == "curves":
|
|
316
598
|
dlg = _CurvesPresetDialog(parent, initial=cur or {"shape":"linear","amount":0.5,"mode":"K (Brightness)"})
|
|
317
599
|
return dlg.result_dict() if dlg.exec() == QDialog.DialogCode.Accepted else None
|
|
@@ -505,18 +787,18 @@ class ShortcutButton(QToolButton):
|
|
|
505
787
|
# --- Context menu (run / preset / delete) ----------------------------
|
|
506
788
|
def _context_menu(self, pos):
|
|
507
789
|
m = QMenu(self)
|
|
508
|
-
m.addAction("Run", lambda: self._mgr.trigger(self.command_id))
|
|
790
|
+
m.addAction(self.tr("Run"), lambda: self._mgr.trigger(self.command_id))
|
|
509
791
|
m.addSeparator()
|
|
510
|
-
m.addAction("Edit Preset…", self._edit_preset_ui)
|
|
511
|
-
m.addAction("Clear Preset", lambda: self._save_preset(None))
|
|
512
|
-
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
|
|
513
795
|
m.addSeparator()
|
|
514
|
-
m.addAction("Delete", self._delete)
|
|
796
|
+
m.addAction(self.tr("Delete"), self._delete)
|
|
515
797
|
m.exec(self.mapToGlobal(pos))
|
|
516
798
|
|
|
517
799
|
def _rename(self):
|
|
518
800
|
current = self.text()
|
|
519
|
-
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)
|
|
520
802
|
if not ok or not new_name.strip():
|
|
521
803
|
return
|
|
522
804
|
self.setText(new_name.strip())
|
|
@@ -528,21 +810,21 @@ class ShortcutButton(QToolButton):
|
|
|
528
810
|
result = _open_preset_editor_for_command(self, cid, cur)
|
|
529
811
|
if result is not None:
|
|
530
812
|
self._save_preset(result)
|
|
531
|
-
QMessageBox.information(self, "Preset saved", "Preset stored on shortcut.")
|
|
813
|
+
QMessageBox.information(self, self.tr("Preset saved"), self.tr("Preset stored on shortcut."))
|
|
532
814
|
return
|
|
533
815
|
|
|
534
816
|
# Fallback: JSON editor
|
|
535
817
|
raw = json.dumps(cur or {}, indent=2)
|
|
536
|
-
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)
|
|
537
819
|
if ok:
|
|
538
820
|
try:
|
|
539
821
|
preset = json.loads(text or "{}")
|
|
540
822
|
if not isinstance(preset, dict):
|
|
541
|
-
raise ValueError("Preset must be a JSON object")
|
|
823
|
+
raise ValueError(self.tr("Preset must be a JSON object"))
|
|
542
824
|
self._save_preset(preset)
|
|
543
|
-
QMessageBox.information(self, "Preset saved", "Preset stored on shortcut.")
|
|
825
|
+
QMessageBox.information(self, self.tr("Preset saved"), self.tr("Preset stored on shortcut."))
|
|
544
826
|
except Exception as e:
|
|
545
|
-
QMessageBox.warning(self, "Invalid JSON", str(e))
|
|
827
|
+
QMessageBox.warning(self, self.tr("Invalid JSON"), str(e))
|
|
546
828
|
|
|
547
829
|
|
|
548
830
|
def _start_command_drag(self):
|
|
@@ -830,11 +1112,11 @@ class ShortcutCanvas(QWidget):
|
|
|
830
1112
|
def contextMenuEvent(self, e):
|
|
831
1113
|
menu = QMenu(self)
|
|
832
1114
|
has_sel = bool(self._mgr.selected)
|
|
833
|
-
a_del = menu.addAction("Delete Selected", self._mgr.delete_selected); a_del.setEnabled(has_sel)
|
|
834
|
-
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)
|
|
835
1117
|
menu.addSeparator()
|
|
836
|
-
a_vb = menu.addAction("View Bundles…", lambda: _open_view_bundles_from_canvas(self))
|
|
837
|
-
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))
|
|
838
1120
|
menu.exec(e.globalPos())
|
|
839
1121
|
|
|
840
1122
|
|
|
@@ -2805,3 +3087,30 @@ class _RGBAlignPresetDialog(QDialog):
|
|
|
2805
3087
|
"new_doc": bool(self.chk_new.isChecked()),
|
|
2806
3088
|
}
|
|
2807
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
|
+
}
|
|
@@ -362,7 +362,10 @@ class SignatureInsertDialogPro(QDialog):
|
|
|
362
362
|
"""
|
|
363
363
|
def __init__(self, parent, doc, icon: QIcon | None = None):
|
|
364
364
|
super().__init__(parent)
|
|
365
|
-
self.setWindowTitle("Signature / Insert")
|
|
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:
|