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.
Files changed (293) hide show
  1. setiastro/data/SASP_data.fits +0 -0
  2. setiastro/data/catalogs/List_of_Galaxies_with_Distances_Gly.csv +488 -0
  3. setiastro/data/catalogs/astrobin_filters.csv +890 -0
  4. setiastro/data/catalogs/astrobin_filters_page1_local.csv +51 -0
  5. setiastro/data/catalogs/cali2.csv +63 -0
  6. setiastro/data/catalogs/cali2color.csv +65 -0
  7. setiastro/data/catalogs/celestial_catalog - original.csv +16471 -0
  8. setiastro/data/catalogs/celestial_catalog.csv +24031 -0
  9. setiastro/data/catalogs/detected_stars.csv +24784 -0
  10. setiastro/data/catalogs/fits_header_data.csv +46 -0
  11. setiastro/data/catalogs/test.csv +8 -0
  12. setiastro/data/catalogs/updated_celestial_catalog.csv +16471 -0
  13. setiastro/images/Astro_Spikes.png +0 -0
  14. setiastro/images/Background_startup.jpg +0 -0
  15. setiastro/images/HRDiagram.png +0 -0
  16. setiastro/images/LExtract.png +0 -0
  17. setiastro/images/LInsert.png +0 -0
  18. setiastro/images/Oxygenation-atm-2.svg.png +0 -0
  19. setiastro/images/RGB080604.png +0 -0
  20. setiastro/images/abeicon.png +0 -0
  21. setiastro/images/aberration.png +0 -0
  22. setiastro/images/andromedatry.png +0 -0
  23. setiastro/images/andromedatry_satellited.png +0 -0
  24. setiastro/images/annotated.png +0 -0
  25. setiastro/images/aperture.png +0 -0
  26. setiastro/images/astrosuite.ico +0 -0
  27. setiastro/images/astrosuite.png +0 -0
  28. setiastro/images/astrosuitepro.icns +0 -0
  29. setiastro/images/astrosuitepro.ico +0 -0
  30. setiastro/images/astrosuitepro.png +0 -0
  31. setiastro/images/background.png +0 -0
  32. setiastro/images/background2.png +0 -0
  33. setiastro/images/benchmark.png +0 -0
  34. setiastro/images/big_moon_stabilizer_timeline.png +0 -0
  35. setiastro/images/big_moon_stabilizer_timeline_clean.png +0 -0
  36. setiastro/images/blaster.png +0 -0
  37. setiastro/images/blink.png +0 -0
  38. setiastro/images/clahe.png +0 -0
  39. setiastro/images/collage.png +0 -0
  40. setiastro/images/colorwheel.png +0 -0
  41. setiastro/images/contsub.png +0 -0
  42. setiastro/images/convo.png +0 -0
  43. setiastro/images/copyslot.png +0 -0
  44. setiastro/images/cosmic.png +0 -0
  45. setiastro/images/cosmicsat.png +0 -0
  46. setiastro/images/crop1.png +0 -0
  47. setiastro/images/cropicon.png +0 -0
  48. setiastro/images/curves.png +0 -0
  49. setiastro/images/cvs.png +0 -0
  50. setiastro/images/debayer.png +0 -0
  51. setiastro/images/denoise_cnn_custom.png +0 -0
  52. setiastro/images/denoise_cnn_graph.png +0 -0
  53. setiastro/images/disk.png +0 -0
  54. setiastro/images/dse.png +0 -0
  55. setiastro/images/exoicon.png +0 -0
  56. setiastro/images/eye.png +0 -0
  57. setiastro/images/fliphorizontal.png +0 -0
  58. setiastro/images/flipvertical.png +0 -0
  59. setiastro/images/font.png +0 -0
  60. setiastro/images/freqsep.png +0 -0
  61. setiastro/images/functionbundle.png +0 -0
  62. setiastro/images/graxpert.png +0 -0
  63. setiastro/images/green.png +0 -0
  64. setiastro/images/gridicon.png +0 -0
  65. setiastro/images/halo.png +0 -0
  66. setiastro/images/hdr.png +0 -0
  67. setiastro/images/histogram.png +0 -0
  68. setiastro/images/hubble.png +0 -0
  69. setiastro/images/imagecombine.png +0 -0
  70. setiastro/images/invert.png +0 -0
  71. setiastro/images/isophote.png +0 -0
  72. setiastro/images/isophote_demo_figure.png +0 -0
  73. setiastro/images/isophote_demo_image.png +0 -0
  74. setiastro/images/isophote_demo_model.png +0 -0
  75. setiastro/images/isophote_demo_residual.png +0 -0
  76. setiastro/images/jwstpupil.png +0 -0
  77. setiastro/images/linearfit.png +0 -0
  78. setiastro/images/livestacking.png +0 -0
  79. setiastro/images/mask.png +0 -0
  80. setiastro/images/maskapply.png +0 -0
  81. setiastro/images/maskcreate.png +0 -0
  82. setiastro/images/maskremove.png +0 -0
  83. setiastro/images/morpho.png +0 -0
  84. setiastro/images/mosaic.png +0 -0
  85. setiastro/images/multiscale_decomp.png +0 -0
  86. setiastro/images/nbtorgb.png +0 -0
  87. setiastro/images/neutral.png +0 -0
  88. setiastro/images/nuke.png +0 -0
  89. setiastro/images/openfile.png +0 -0
  90. setiastro/images/pedestal.png +0 -0
  91. setiastro/images/pen.png +0 -0
  92. setiastro/images/pixelmath.png +0 -0
  93. setiastro/images/platesolve.png +0 -0
  94. setiastro/images/ppp.png +0 -0
  95. setiastro/images/pro.png +0 -0
  96. setiastro/images/project.png +0 -0
  97. setiastro/images/psf.png +0 -0
  98. setiastro/images/redo.png +0 -0
  99. setiastro/images/redoicon.png +0 -0
  100. setiastro/images/rescale.png +0 -0
  101. setiastro/images/rgbalign.png +0 -0
  102. setiastro/images/rgbcombo.png +0 -0
  103. setiastro/images/rgbextract.png +0 -0
  104. setiastro/images/rotate180.png +0 -0
  105. setiastro/images/rotatearbitrary.png +0 -0
  106. setiastro/images/rotateclockwise.png +0 -0
  107. setiastro/images/rotatecounterclockwise.png +0 -0
  108. setiastro/images/satellite.png +0 -0
  109. setiastro/images/script.png +0 -0
  110. setiastro/images/selectivecolor.png +0 -0
  111. setiastro/images/simbad.png +0 -0
  112. setiastro/images/slot0.png +0 -0
  113. setiastro/images/slot1.png +0 -0
  114. setiastro/images/slot2.png +0 -0
  115. setiastro/images/slot3.png +0 -0
  116. setiastro/images/slot4.png +0 -0
  117. setiastro/images/slot5.png +0 -0
  118. setiastro/images/slot6.png +0 -0
  119. setiastro/images/slot7.png +0 -0
  120. setiastro/images/slot8.png +0 -0
  121. setiastro/images/slot9.png +0 -0
  122. setiastro/images/spcc.png +0 -0
  123. setiastro/images/spin_precession_vs_lunar_distance.png +0 -0
  124. setiastro/images/spinner.gif +0 -0
  125. setiastro/images/stacking.png +0 -0
  126. setiastro/images/staradd.png +0 -0
  127. setiastro/images/staralign.png +0 -0
  128. setiastro/images/starnet.png +0 -0
  129. setiastro/images/starregistration.png +0 -0
  130. setiastro/images/starspike.png +0 -0
  131. setiastro/images/starstretch.png +0 -0
  132. setiastro/images/statstretch.png +0 -0
  133. setiastro/images/supernova.png +0 -0
  134. setiastro/images/uhs.png +0 -0
  135. setiastro/images/undoicon.png +0 -0
  136. setiastro/images/upscale.png +0 -0
  137. setiastro/images/viewbundle.png +0 -0
  138. setiastro/images/whitebalance.png +0 -0
  139. setiastro/images/wimi_icon_256x256.png +0 -0
  140. setiastro/images/wimilogo.png +0 -0
  141. setiastro/images/wims.png +0 -0
  142. setiastro/images/wrench_icon.png +0 -0
  143. setiastro/images/xisfliberator.png +0 -0
  144. setiastro/qml/ResourceMonitor.qml +126 -0
  145. setiastro/saspro/__main__.py +228 -67
  146. setiastro/saspro/_generated/build_info.py +2 -1
  147. setiastro/saspro/abe.py +76 -25
  148. setiastro/saspro/aberration_ai.py +14 -14
  149. setiastro/saspro/add_stars.py +15 -12
  150. setiastro/saspro/astrobin_exporter.py +61 -58
  151. setiastro/saspro/astrospike_python.py +3 -1
  152. setiastro/saspro/autostretch.py +4 -2
  153. setiastro/saspro/backgroundneutral.py +65 -14
  154. setiastro/saspro/batch_convert.py +8 -5
  155. setiastro/saspro/batch_renamer.py +39 -36
  156. setiastro/saspro/blemish_blaster.py +15 -12
  157. setiastro/saspro/blink_comparator_pro.py +605 -379
  158. setiastro/saspro/cheat_sheet.py +62 -17
  159. setiastro/saspro/clahe.py +34 -8
  160. setiastro/saspro/comet_stacking.py +103 -38
  161. setiastro/saspro/common_tr.py +107 -0
  162. setiastro/saspro/continuum_subtract.py +7 -7
  163. setiastro/saspro/convo.py +12 -9
  164. setiastro/saspro/copyastro.py +3 -0
  165. setiastro/saspro/cosmicclarity.py +77 -52
  166. setiastro/saspro/crop_dialog_pro.py +80 -45
  167. setiastro/saspro/curve_editor_pro.py +51 -33
  168. setiastro/saspro/debayer.py +6 -3
  169. setiastro/saspro/doc_manager.py +49 -19
  170. setiastro/saspro/exoplanet_detector.py +11 -11
  171. setiastro/saspro/fitsmodifier.py +48 -44
  172. setiastro/saspro/fix_bom.py +32 -0
  173. setiastro/saspro/frequency_separation.py +18 -12
  174. setiastro/saspro/function_bundle.py +18 -16
  175. setiastro/saspro/generate_translations.py +3092 -0
  176. setiastro/saspro/ghs_dialog_pro.py +19 -16
  177. setiastro/saspro/graxpert.py +3 -0
  178. setiastro/saspro/gui/main_window.py +471 -126
  179. setiastro/saspro/gui/mixins/dock_mixin.py +123 -11
  180. setiastro/saspro/gui/mixins/file_mixin.py +25 -20
  181. setiastro/saspro/gui/mixins/geometry_mixin.py +115 -15
  182. setiastro/saspro/gui/mixins/header_mixin.py +6 -6
  183. setiastro/saspro/gui/mixins/mask_mixin.py +8 -8
  184. setiastro/saspro/gui/mixins/menu_mixin.py +62 -33
  185. setiastro/saspro/gui/mixins/toolbar_mixin.py +382 -226
  186. setiastro/saspro/gui/mixins/update_mixin.py +26 -26
  187. setiastro/saspro/gui/statistics_dialog.py +47 -0
  188. setiastro/saspro/halobgon.py +29 -3
  189. setiastro/saspro/header_viewer.py +21 -18
  190. setiastro/saspro/histogram.py +29 -26
  191. setiastro/saspro/history_explorer.py +2 -0
  192. setiastro/saspro/i18n.py +168 -0
  193. setiastro/saspro/image_combine.py +3 -0
  194. setiastro/saspro/image_peeker_pro.py +52 -44
  195. setiastro/saspro/imageops/stretch.py +5 -13
  196. setiastro/saspro/isophote.py +3 -0
  197. setiastro/saspro/legacy/numba_utils.py +64 -47
  198. setiastro/saspro/linear_fit.py +3 -0
  199. setiastro/saspro/live_stacking.py +13 -2
  200. setiastro/saspro/mask_creation.py +180 -22
  201. setiastro/saspro/mfdeconv.py +5 -0
  202. setiastro/saspro/morphology.py +38 -13
  203. setiastro/saspro/multiscale_decomp.py +713 -256
  204. setiastro/saspro/nbtorgb_stars.py +12 -2
  205. setiastro/saspro/numba_utils.py +149 -48
  206. setiastro/saspro/ops/scripts.py +77 -17
  207. setiastro/saspro/ops/settings.py +177 -100
  208. setiastro/saspro/perfect_palette_picker.py +25 -7
  209. setiastro/saspro/pixelmath.py +114 -110
  210. setiastro/saspro/plate_solver.py +118 -108
  211. setiastro/saspro/remove_green.py +24 -7
  212. setiastro/saspro/remove_stars.py +136 -162
  213. setiastro/saspro/remove_stars_preset.py +55 -13
  214. setiastro/saspro/resources.py +46 -15
  215. setiastro/saspro/rgb_combination.py +19 -18
  216. setiastro/saspro/rgbalign.py +11 -11
  217. setiastro/saspro/save_options.py +5 -4
  218. setiastro/saspro/selective_color.py +84 -25
  219. setiastro/saspro/sfcc.py +119 -72
  220. setiastro/saspro/shortcuts.py +345 -36
  221. setiastro/saspro/signature_insert.py +4 -1
  222. setiastro/saspro/stacking_suite.py +2066 -1119
  223. setiastro/saspro/star_alignment.py +291 -331
  224. setiastro/saspro/star_spikes.py +137 -53
  225. setiastro/saspro/star_stretch.py +47 -10
  226. setiastro/saspro/stat_stretch.py +52 -16
  227. setiastro/saspro/status_log_dock.py +1 -1
  228. setiastro/saspro/subwindow.py +97 -36
  229. setiastro/saspro/supernovaasteroidhunter.py +68 -61
  230. setiastro/saspro/swap_manager.py +77 -42
  231. setiastro/saspro/translations/all_source_strings.json +4726 -0
  232. setiastro/saspro/translations/ar_translations.py +4096 -0
  233. setiastro/saspro/translations/de_translations.py +3728 -0
  234. setiastro/saspro/translations/es_translations.py +4169 -0
  235. setiastro/saspro/translations/fr_translations.py +4090 -0
  236. setiastro/saspro/translations/hi_translations.py +3803 -0
  237. setiastro/saspro/translations/integrate_translations.py +271 -0
  238. setiastro/saspro/translations/it_translations.py +4728 -0
  239. setiastro/saspro/translations/ja_translations.py +3834 -0
  240. setiastro/saspro/translations/pt_translations.py +3847 -0
  241. setiastro/saspro/translations/ru_translations.py +3082 -0
  242. setiastro/saspro/translations/saspro_ar.qm +0 -0
  243. setiastro/saspro/translations/saspro_ar.ts +16019 -0
  244. setiastro/saspro/translations/saspro_de.qm +0 -0
  245. setiastro/saspro/translations/saspro_de.ts +14548 -0
  246. setiastro/saspro/translations/saspro_es.qm +0 -0
  247. setiastro/saspro/translations/saspro_es.ts +16202 -0
  248. setiastro/saspro/translations/saspro_fr.qm +0 -0
  249. setiastro/saspro/translations/saspro_fr.ts +15870 -0
  250. setiastro/saspro/translations/saspro_hi.qm +0 -0
  251. setiastro/saspro/translations/saspro_hi.ts +14855 -0
  252. setiastro/saspro/translations/saspro_it.qm +0 -0
  253. setiastro/saspro/translations/saspro_it.ts +19046 -0
  254. setiastro/saspro/translations/saspro_ja.qm +0 -0
  255. setiastro/saspro/translations/saspro_ja.ts +14980 -0
  256. setiastro/saspro/translations/saspro_pt.qm +0 -0
  257. setiastro/saspro/translations/saspro_pt.ts +15024 -0
  258. setiastro/saspro/translations/saspro_ru.qm +0 -0
  259. setiastro/saspro/translations/saspro_ru.ts +11835 -0
  260. setiastro/saspro/translations/saspro_sw.qm +0 -0
  261. setiastro/saspro/translations/saspro_sw.ts +15237 -0
  262. setiastro/saspro/translations/saspro_uk.qm +0 -0
  263. setiastro/saspro/translations/saspro_uk.ts +15248 -0
  264. setiastro/saspro/translations/saspro_zh.qm +0 -0
  265. setiastro/saspro/translations/saspro_zh.ts +15289 -0
  266. setiastro/saspro/translations/sw_translations.py +3897 -0
  267. setiastro/saspro/translations/uk_translations.py +3929 -0
  268. setiastro/saspro/translations/zh_translations.py +3910 -0
  269. setiastro/saspro/versioning.py +77 -0
  270. setiastro/saspro/view_bundle.py +20 -17
  271. setiastro/saspro/wavescale_hdr.py +54 -33
  272. setiastro/saspro/wavescale_hdr_preset.py +6 -5
  273. setiastro/saspro/wavescalede.py +54 -31
  274. setiastro/saspro/wavescalede_preset.py +9 -7
  275. setiastro/saspro/whitebalance.py +58 -22
  276. setiastro/saspro/widgets/common_utilities.py +12 -11
  277. setiastro/saspro/widgets/minigame/game.js +991 -0
  278. setiastro/saspro/widgets/minigame/index.html +53 -0
  279. setiastro/saspro/widgets/minigame/style.css +241 -0
  280. setiastro/saspro/widgets/preview_dialogs.py +8 -8
  281. setiastro/saspro/widgets/resource_monitor.py +263 -0
  282. setiastro/saspro/widgets/spinboxes.py +18 -0
  283. setiastro/saspro/widgets/wavelet_utils.py +52 -20
  284. setiastro/saspro/wimi.py +7996 -0
  285. setiastro/saspro/wims.py +578 -0
  286. setiastro/saspro/window_shelf.py +2 -2
  287. {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/METADATA +15 -3
  288. setiastrosuitepro-1.6.4.post1.dist-info/RECORD +368 -0
  289. setiastrosuitepro-1.6.0.dist-info/RECORD +0 -174
  290. {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/WHEEL +0 -0
  291. {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/entry_points.txt +0 -0
  292. {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/licenses/LICENSE +0 -0
  293. {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/licenses/license.txt +0 -0
@@ -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: if (had-mod at press OR has-mod now) AND moved enough → start drag
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 ((self._press_had_mod.get(obj, False) or self._mods_ok(QApplication.keyboardModifiers()))
178
- and delta.manhattanLength() > QApplication.startDragDistance()):
179
- # find the QAction backing this button
180
- act = next((a for a in self.actions() if self.widgetForAction(a) is obj), None)
181
- if act:
182
- self._start_drag_for_action(act)
183
- # eat subsequent release so the action doesn't trigger
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
- # clear press tracking
186
- self._press_pos.pop(obj, None)
187
- self._press_had_mod.pop(obj, None)
188
- return True # consume the move (prevents click)
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 a drag, swallow the release so click won't fire
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
- m.addAction("Create Desktop Shortcut", lambda: self._add_shortcut_for_action(act))
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", lambda: None).setEnabled(False)
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: