setiastrosuitepro 1.6.2__py3-none-any.whl → 1.6.12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of setiastrosuitepro might be problematic. Click here for more details.

Files changed (162) hide show
  1. setiastro/images/abeicon.svg +16 -0
  2. setiastro/images/acv_icon.png +0 -0
  3. setiastro/images/colorwheel.svg +97 -0
  4. setiastro/images/cosmic.svg +40 -0
  5. setiastro/images/cosmicsat.svg +24 -0
  6. setiastro/images/first_quarter.png +0 -0
  7. setiastro/images/full_moon.png +0 -0
  8. setiastro/images/graxpert.svg +19 -0
  9. setiastro/images/last_quarter.png +0 -0
  10. setiastro/images/linearfit.svg +32 -0
  11. setiastro/images/new_moon.png +0 -0
  12. setiastro/images/pixelmath.svg +42 -0
  13. setiastro/images/rotatearbitrary.png +0 -0
  14. setiastro/images/waning_crescent_1.png +0 -0
  15. setiastro/images/waning_crescent_2.png +0 -0
  16. setiastro/images/waning_crescent_3.png +0 -0
  17. setiastro/images/waning_crescent_4.png +0 -0
  18. setiastro/images/waning_crescent_5.png +0 -0
  19. setiastro/images/waning_gibbous_1.png +0 -0
  20. setiastro/images/waning_gibbous_2.png +0 -0
  21. setiastro/images/waning_gibbous_3.png +0 -0
  22. setiastro/images/waning_gibbous_4.png +0 -0
  23. setiastro/images/waning_gibbous_5.png +0 -0
  24. setiastro/images/waxing_crescent_1.png +0 -0
  25. setiastro/images/waxing_crescent_2.png +0 -0
  26. setiastro/images/waxing_crescent_3.png +0 -0
  27. setiastro/images/waxing_crescent_4.png +0 -0
  28. setiastro/images/waxing_crescent_5.png +0 -0
  29. setiastro/images/waxing_gibbous_1.png +0 -0
  30. setiastro/images/waxing_gibbous_2.png +0 -0
  31. setiastro/images/waxing_gibbous_3.png +0 -0
  32. setiastro/images/waxing_gibbous_4.png +0 -0
  33. setiastro/images/waxing_gibbous_5.png +0 -0
  34. setiastro/qml/ResourceMonitor.qml +84 -82
  35. setiastro/saspro/__main__.py +20 -1
  36. setiastro/saspro/_generated/build_info.py +2 -2
  37. setiastro/saspro/abe.py +37 -4
  38. setiastro/saspro/aberration_ai.py +237 -21
  39. setiastro/saspro/acv_exporter.py +379 -0
  40. setiastro/saspro/add_stars.py +33 -6
  41. setiastro/saspro/backgroundneutral.py +114 -37
  42. setiastro/saspro/blemish_blaster.py +4 -1
  43. setiastro/saspro/blink_comparator_pro.py +548 -275
  44. setiastro/saspro/clahe.py +4 -1
  45. setiastro/saspro/continuum_subtract.py +4 -1
  46. setiastro/saspro/convo.py +13 -7
  47. setiastro/saspro/cosmicclarity.py +129 -18
  48. setiastro/saspro/crop_dialog_pro.py +134 -8
  49. setiastro/saspro/curve_editor_pro.py +109 -42
  50. setiastro/saspro/doc_manager.py +246 -16
  51. setiastro/saspro/exoplanet_detector.py +120 -28
  52. setiastro/saspro/frequency_separation.py +1158 -204
  53. setiastro/saspro/function_bundle.py +16 -16
  54. setiastro/saspro/ghs_dialog_pro.py +81 -16
  55. setiastro/saspro/graxpert.py +1 -0
  56. setiastro/saspro/gui/main_window.py +519 -289
  57. setiastro/saspro/gui/mixins/dock_mixin.py +276 -42
  58. setiastro/saspro/gui/mixins/geometry_mixin.py +105 -5
  59. setiastro/saspro/gui/mixins/menu_mixin.py +28 -1
  60. setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
  61. setiastro/saspro/gui/mixins/toolbar_mixin.py +416 -27
  62. setiastro/saspro/gui/mixins/update_mixin.py +138 -36
  63. setiastro/saspro/gui/mixins/view_mixin.py +42 -0
  64. setiastro/saspro/halobgon.py +4 -0
  65. setiastro/saspro/histogram.py +5 -1
  66. setiastro/saspro/image_combine.py +4 -0
  67. setiastro/saspro/image_peeker_pro.py +4 -0
  68. setiastro/saspro/imageops/starbasedwhitebalance.py +23 -52
  69. setiastro/saspro/imageops/stretch.py +582 -62
  70. setiastro/saspro/isophote.py +4 -0
  71. setiastro/saspro/layers.py +13 -9
  72. setiastro/saspro/layers_dock.py +183 -3
  73. setiastro/saspro/legacy/image_manager.py +154 -20
  74. setiastro/saspro/legacy/numba_utils.py +67 -47
  75. setiastro/saspro/legacy/xisf.py +240 -98
  76. setiastro/saspro/live_stacking.py +180 -79
  77. setiastro/saspro/luminancerecombine.py +228 -27
  78. setiastro/saspro/mask_creation.py +174 -15
  79. setiastro/saspro/mfdeconv.py +113 -35
  80. setiastro/saspro/mfdeconvcudnn.py +119 -70
  81. setiastro/saspro/mfdeconvsport.py +112 -35
  82. setiastro/saspro/morphology.py +4 -0
  83. setiastro/saspro/multiscale_decomp.py +748 -255
  84. setiastro/saspro/numba_utils.py +72 -57
  85. setiastro/saspro/ops/commands.py +18 -18
  86. setiastro/saspro/ops/script_editor.py +10 -2
  87. setiastro/saspro/ops/scripts.py +122 -0
  88. setiastro/saspro/perfect_palette_picker.py +37 -3
  89. setiastro/saspro/plate_solver.py +84 -49
  90. setiastro/saspro/psf_viewer.py +119 -37
  91. setiastro/saspro/remove_stars_preset.py +55 -13
  92. setiastro/saspro/resources.py +97 -11
  93. setiastro/saspro/rgbalign.py +4 -0
  94. setiastro/saspro/selective_color.py +83 -21
  95. setiastro/saspro/sfcc.py +364 -152
  96. setiastro/saspro/shortcuts.py +253 -49
  97. setiastro/saspro/signature_insert.py +692 -33
  98. setiastro/saspro/stacking_suite.py +1610 -574
  99. setiastro/saspro/star_alignment.py +522 -453
  100. setiastro/saspro/star_spikes.py +4 -0
  101. setiastro/saspro/star_stretch.py +38 -3
  102. setiastro/saspro/stat_stretch.py +743 -128
  103. setiastro/saspro/status_log_dock.py +1 -1
  104. setiastro/saspro/subwindow.py +786 -360
  105. setiastro/saspro/supernovaasteroidhunter.py +1 -1
  106. setiastro/saspro/swap_manager.py +77 -42
  107. setiastro/saspro/translations/all_source_strings.json +1588 -516
  108. setiastro/saspro/translations/ar_translations.py +915 -684
  109. setiastro/saspro/translations/de_translations.py +442 -463
  110. setiastro/saspro/translations/es_translations.py +277 -47
  111. setiastro/saspro/translations/fr_translations.py +279 -47
  112. setiastro/saspro/translations/hi_translations.py +253 -21
  113. setiastro/saspro/translations/integrate_translations.py +3 -2
  114. setiastro/saspro/translations/it_translations.py +1211 -161
  115. setiastro/saspro/translations/ja_translations.py +3340 -3107
  116. setiastro/saspro/translations/pt_translations.py +3315 -3337
  117. setiastro/saspro/translations/ru_translations.py +351 -117
  118. setiastro/saspro/translations/saspro_ar.qm +0 -0
  119. setiastro/saspro/translations/saspro_ar.ts +15902 -138
  120. setiastro/saspro/translations/saspro_de.qm +0 -0
  121. setiastro/saspro/translations/saspro_de.ts +14428 -133
  122. setiastro/saspro/translations/saspro_es.qm +0 -0
  123. setiastro/saspro/translations/saspro_es.ts +11503 -7821
  124. setiastro/saspro/translations/saspro_fr.qm +0 -0
  125. setiastro/saspro/translations/saspro_fr.ts +11168 -7812
  126. setiastro/saspro/translations/saspro_hi.qm +0 -0
  127. setiastro/saspro/translations/saspro_hi.ts +14733 -135
  128. setiastro/saspro/translations/saspro_it.qm +0 -0
  129. setiastro/saspro/translations/saspro_it.ts +14347 -7821
  130. setiastro/saspro/translations/saspro_ja.qm +0 -0
  131. setiastro/saspro/translations/saspro_ja.ts +14860 -137
  132. setiastro/saspro/translations/saspro_pt.qm +0 -0
  133. setiastro/saspro/translations/saspro_pt.ts +14904 -137
  134. setiastro/saspro/translations/saspro_ru.qm +0 -0
  135. setiastro/saspro/translations/saspro_ru.ts +11766 -168
  136. setiastro/saspro/translations/saspro_sw.qm +0 -0
  137. setiastro/saspro/translations/saspro_sw.ts +15115 -135
  138. setiastro/saspro/translations/saspro_uk.qm +0 -0
  139. setiastro/saspro/translations/saspro_uk.ts +11206 -6729
  140. setiastro/saspro/translations/saspro_zh.qm +0 -0
  141. setiastro/saspro/translations/saspro_zh.ts +10581 -7812
  142. setiastro/saspro/translations/sw_translations.py +282 -56
  143. setiastro/saspro/translations/uk_translations.py +264 -35
  144. setiastro/saspro/translations/zh_translations.py +282 -47
  145. setiastro/saspro/view_bundle.py +17 -17
  146. setiastro/saspro/wavescale_hdr.py +4 -1
  147. setiastro/saspro/wavescalede.py +4 -1
  148. setiastro/saspro/whitebalance.py +84 -12
  149. setiastro/saspro/widgets/common_utilities.py +28 -21
  150. setiastro/saspro/widgets/minigame/game.js +11 -6
  151. setiastro/saspro/widgets/resource_monitor.py +133 -57
  152. setiastro/saspro/widgets/spinboxes.py +28 -13
  153. setiastro/saspro/wimi.py +92 -721
  154. setiastro/saspro/wims.py +46 -36
  155. setiastro/saspro/window_shelf.py +2 -2
  156. setiastro/saspro/xisf.py +101 -11
  157. {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/METADATA +8 -7
  158. {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/RECORD +162 -128
  159. {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/WHEEL +0 -0
  160. {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/entry_points.txt +0 -0
  161. {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/LICENSE +0 -0
  162. {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/license.txt +0 -0
@@ -10,6 +10,10 @@ from PyQt6.QtCore import Qt, QTimer, QUrl
10
10
  from PyQt6.QtGui import QAction, QActionGroup, QIcon, QKeySequence, QDesktopServices
11
11
  from PyQt6.QtWidgets import QMenu, QToolButton
12
12
 
13
+ from PyQt6.QtCore import QElapsedTimer
14
+
15
+ import sys
16
+
13
17
  if TYPE_CHECKING:
14
18
  pass
15
19
 
@@ -20,7 +24,7 @@ from setiastro.saspro.resources import (
20
24
  LInsert_path, rgbcombo_path, rgbextract_path, graxperticon_path,
21
25
  cropicon_path, openfile_path, abeicon_path, undoicon_path, redoicon_path,
22
26
  blastericon_path, hdr_path, invert_path, fliphorizontal_path,
23
- flipvertical_path, rotateclockwise_path, rotatecounterclockwise_path,
27
+ flipvertical_path, rotateclockwise_path, rotatecounterclockwise_path,rotatearbitrary_path,
24
28
  rotate180_path, maskcreate_path, maskapply_path, maskremove_path,
25
29
  pixelmath_path, histogram_path, mosaic_path, rescale_path, staralign_path,
26
30
  platesolve_path, psf_path, supernova_path, starregistration_path,
@@ -73,7 +77,8 @@ class ToolbarMixin:
73
77
 
74
78
  def _init_toolbar(self):
75
79
  # View toolbar (Undo / Redo / Display-Stretch)
76
- tb = DraggableToolBar("View", self)
80
+ tb = DraggableToolBar(self.tr("View"), self)
81
+ tb.setObjectName("View")
77
82
  tb.setSettingsKey("Toolbar/View")
78
83
  self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb)
79
84
 
@@ -175,7 +180,8 @@ class ToolbarMixin:
175
180
  pass
176
181
 
177
182
  # Functions toolbar
178
- tb_fn = DraggableToolBar("Functions", self)
183
+ tb_fn = DraggableToolBar(self.tr("Functions"), self)
184
+ tb_fn.setObjectName("Functions")
179
185
  tb_fn.setSettingsKey("Toolbar/Functions")
180
186
  self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_fn)
181
187
 
@@ -198,16 +204,16 @@ class ToolbarMixin:
198
204
  tb_fn.addAction(self.act_convo)
199
205
  tb_fn.addAction(self.act_extract_luma)
200
206
 
201
- btn_luma = tb_fn.widgetForAction(self.act_extract_luma)
202
- if isinstance(btn_luma, QToolButton):
203
- luma_menu = QMenu(btn_luma)
204
- luma_menu.addActions(self._luma_group.actions())
205
- btn_luma.setMenu(luma_menu)
206
- btn_luma.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
207
- btn_luma.setStyleSheet("""
208
- QToolButton { color: #dcdcdc; }
209
- QToolButton:pressed, QToolButton:checked { color: #DAA520; font-weight: 600; }
210
- """)
207
+ #btn_luma = tb_fn.widgetForAction(self.act_extract_luma)
208
+ #if isinstance(btn_luma, QToolButton):
209
+ # luma_menu = QMenu(btn_luma)
210
+ # luma_menu.addActions(self._luma_group.actions())
211
+ # btn_luma.setMenu(luma_menu)
212
+ # btn_luma.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
213
+ # btn_luma.setStyleSheet("""
214
+ # QToolButton { color: #dcdcdc; }
215
+ # QToolButton:pressed, QToolButton:checked { color: #DAA520; font-weight: 600; }
216
+ # """)
211
217
 
212
218
  tb_fn.addAction(self.act_recombine_luma)
213
219
  tb_fn.addAction(self.act_rgb_extract)
@@ -227,7 +233,8 @@ class ToolbarMixin:
227
233
  except Exception:
228
234
  pass
229
235
 
230
- tbCosmic = DraggableToolBar("Cosmic Clarity", self)
236
+ tbCosmic = DraggableToolBar(self.tr("Cosmic Clarity"), self)
237
+ tbCosmic.setObjectName("Cosmic Clarity")
231
238
  tbCosmic.setSettingsKey("Toolbar/Cosmic")
232
239
  self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tbCosmic)
233
240
 
@@ -241,7 +248,8 @@ class ToolbarMixin:
241
248
  except Exception:
242
249
  pass
243
250
 
244
- tb_tl = DraggableToolBar("Tools", self)
251
+ tb_tl = DraggableToolBar(self.tr("Tools"), self)
252
+ tb_tl.setObjectName("Tools")
245
253
  tb_tl.setSettingsKey("Toolbar/Tools")
246
254
  self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_tl)
247
255
 
@@ -260,7 +268,8 @@ class ToolbarMixin:
260
268
  except Exception:
261
269
  pass
262
270
 
263
- tb_geom = DraggableToolBar("Geometry", self)
271
+ tb_geom = DraggableToolBar(self.tr("Geometry"), self)
272
+ tb_geom.setObjectName("Geometry")
264
273
  tb_geom.setSettingsKey("Toolbar/Geometry")
265
274
  self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_geom)
266
275
 
@@ -272,6 +281,7 @@ class ToolbarMixin:
272
281
  tb_geom.addAction(self.act_geom_rot_cw)
273
282
  tb_geom.addAction(self.act_geom_rot_ccw)
274
283
  tb_geom.addAction(self.act_geom_rot_180)
284
+ tb_geom.addAction(self.act_geom_rot_any)
275
285
  tb_geom.addSeparator()
276
286
  tb_geom.addAction(self.act_geom_rescale)
277
287
  tb_geom.addSeparator()
@@ -283,7 +293,8 @@ class ToolbarMixin:
283
293
  except Exception:
284
294
  pass
285
295
 
286
- tb_star = DraggableToolBar("Star Stuff", self)
296
+ tb_star = DraggableToolBar(self.tr("Star Stuff"), self)
297
+ tb_star.setObjectName("Star Stuff")
287
298
  tb_star.setSettingsKey("Toolbar/StarStuff")
288
299
  self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_star)
289
300
 
@@ -308,7 +319,8 @@ class ToolbarMixin:
308
319
  except Exception:
309
320
  pass
310
321
 
311
- tb_msk = DraggableToolBar("Masks", self)
322
+ tb_msk = DraggableToolBar(self.tr("Masks"), self)
323
+ tb_msk.setObjectName("Masks")
312
324
  tb_msk.setSettingsKey("Toolbar/Masks")
313
325
  self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_msk)
314
326
 
@@ -322,7 +334,8 @@ class ToolbarMixin:
322
334
  except Exception:
323
335
  pass
324
336
 
325
- tb_wim = DraggableToolBar("What's In My...", self)
337
+ tb_wim = DraggableToolBar(self.tr("What's In My..."), self)
338
+ tb_wim.setObjectName("What's In My...")
326
339
  tb_wim.setSettingsKey("Toolbar/WhatsInMy")
327
340
  self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_wim)
328
341
 
@@ -335,7 +348,8 @@ class ToolbarMixin:
335
348
  except Exception:
336
349
  pass
337
350
 
338
- tb_bundle = DraggableToolBar("Bundles", self)
351
+ tb_bundle = DraggableToolBar(self.tr("Bundles"), self)
352
+ tb_bundle.setObjectName("Bundles")
339
353
  tb_bundle.setSettingsKey("Toolbar/Bundles")
340
354
  self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_bundle)
341
355
 
@@ -348,9 +362,25 @@ class ToolbarMixin:
348
362
  except Exception:
349
363
  pass
350
364
 
365
+
366
+ tb_hidden = DraggableToolBar(self.tr("Hidden"), self)
367
+ tb_hidden.setObjectName("Hidden")
368
+ tb_hidden.setSettingsKey("Toolbar/Hidden")
369
+ self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_hidden)
370
+ tb_hidden.setVisible(False) # <- always hidden
371
+
351
372
  # This can move actions between toolbars, so do it after each toolbar has its base order restored.
352
373
  self._restore_toolbar_memberships()
353
374
 
375
+ # Migrate legacy per-toolbar Hidden lists into the new Hidden toolbar
376
+ self._migrate_old_hidden_sets_to_hidden_toolbar()
377
+
378
+ # (Optional) Re-apply per-toolbar order after migration (since we removed actions)
379
+ for _tb in self.findChildren(DraggableToolBar):
380
+ key = getattr(_tb, "_settings_key", None)
381
+ if key:
382
+ self._restore_toolbar_order(_tb, str(key))
383
+
354
384
  # Re-apply hidden state AFTER memberships (actions may have moved toolbars).
355
385
  # This also guarantees correctness even if any toolbar was rebuilt/adjusted internally.
356
386
  for _tb in self.findChildren(DraggableToolBar):
@@ -359,7 +389,9 @@ class ToolbarMixin:
359
389
  except Exception:
360
390
  pass
361
391
 
392
+ # Rebind ALL dropdowns after reorder/membership moves
362
393
  self._rebind_view_dropdowns()
394
+ self._rebind_extract_luma_dropdown()
363
395
 
364
396
  def _toolbar_containing_action(self, action: QAction):
365
397
  from setiastro.saspro.shortcuts import DraggableToolBar
@@ -368,6 +400,140 @@ class ToolbarMixin:
368
400
  return tb
369
401
  return None
370
402
 
403
+ def _rebind_extract_luma_dropdown(self):
404
+ from PyQt6.QtWidgets import QMenu, QToolButton
405
+ from setiastro.saspro.luminancerecombine import LUMA_PROFILES
406
+
407
+ tb = self._toolbar_containing_action(self.act_extract_luma)
408
+ if not tb:
409
+ return
410
+
411
+ btn = tb.widgetForAction(self.act_extract_luma)
412
+ if not isinstance(btn, QToolButton):
413
+ return
414
+
415
+ menu = QMenu(btn)
416
+
417
+ # Ensure cache exists (created in _create_actions(), but be defensive)
418
+ if not hasattr(self, "_luma_sensor_actions"):
419
+ self._luma_sensor_actions = {} # key -> QAction
420
+
421
+ cur_method = str(getattr(self, "luma_method", "rec709"))
422
+
423
+ # ============================================================
424
+ # PATCH SITE #1 (Standard menu) <-- THIS IS WHERE "C" GOES
425
+ # Find your old block:
426
+ #
427
+ # # ---- Standard (use your QActionGroup, keep it simple) ----
428
+ # if getattr(self, "_luma_group", None) is not None:
429
+ # std_menu = QMenu(self.tr("Standard"), menu)
430
+ # std_menu.addActions(self._luma_group.actions())
431
+ # menu.addMenu(std_menu)
432
+ #
433
+ # Replace it with the block below.
434
+ # ============================================================
435
+ if getattr(self, "_luma_group", None) is not None:
436
+ # Sync standard checked states from self.luma_method
437
+ for a in self._luma_group.actions():
438
+ data = str(a.data() or "")
439
+ if data.startswith("sensor:"):
440
+ continue # standards only in Standard menu
441
+ a.blockSignals(True)
442
+ try:
443
+ a.setChecked(data == cur_method)
444
+ finally:
445
+ a.blockSignals(False)
446
+
447
+ # Add ONLY standard actions to the Standard menu (exclude sensors)
448
+ std_menu = QMenu(self.tr("Standard"), menu)
449
+ for a in self._luma_group.actions():
450
+ data = str(a.data() or "")
451
+ if data.startswith("sensor:"):
452
+ continue
453
+ std_menu.addAction(a)
454
+ menu.addMenu(std_menu)
455
+
456
+ # ---- Sensors (nested by category path) ----
457
+ sensors_root = QMenu(self.tr("Sensors"), menu)
458
+
459
+ # Build tree of submenus keyed by "Sensors/<path>"
460
+ submenu_cache: dict[str, QMenu] = {}
461
+
462
+ def get_or_make_path(root_menu: QMenu, path: str) -> QMenu:
463
+ parts = [p for p in path.split("/") if p.strip()]
464
+ cur = root_menu
465
+ acc = ""
466
+ for part in parts:
467
+ acc = f"{acc}/{part}" if acc else part
468
+ if acc not in submenu_cache:
469
+ sm = QMenu(self.tr(part), cur)
470
+ cur.addMenu(sm)
471
+ submenu_cache[acc] = sm
472
+ cur = submenu_cache[acc]
473
+ return cur
474
+
475
+ any_sensor = False
476
+ for key, prof in LUMA_PROFILES.items():
477
+ if not str(key).startswith("sensor:"):
478
+ continue
479
+
480
+ cat = str(prof.get("category", "Sensors/Other"))
481
+ group_path = cat.split("Sensors/", 1)[1] if "Sensors/" in cat else cat
482
+ parent_menu = get_or_make_path(sensors_root, group_path)
483
+
484
+ display_name = key.split("sensor:", 1)[1].strip()
485
+ desc = str(prof.get("description", display_name))
486
+ info = str(prof.get("info", "")).strip()
487
+
488
+ # ============================================================
489
+ # PATCH SITE #2 (Sensors become mutually exclusive with standards)
490
+ # - Cache the QAction so we don't pile up connections/actions
491
+ # - Add it to the SAME QActionGroup (exclusive) as standards
492
+ # - Do NOT use _pick_sensor; rely on QActionGroup.triggered
493
+ # ============================================================
494
+ act = self._luma_sensor_actions.get(key)
495
+ if act is None:
496
+ act = parent_menu.addAction(self.tr(display_name))
497
+ act.setCheckable(True)
498
+ act.setData(key) # IMPORTANT: enables group.triggered to set luma_method
499
+
500
+ # Put sensors into the SAME exclusive group so selecting one
501
+ # deselects everything else (standards and other sensors).
502
+ if getattr(self, "_luma_group", None) is not None:
503
+ self._luma_group.addAction(act)
504
+
505
+ self._luma_sensor_actions[key] = act
506
+ else:
507
+ # Reuse action, but re-add it to the correct submenu location
508
+ parent_menu.addAction(act)
509
+ act.setText(self.tr(display_name))
510
+
511
+ # Update UI info each bind (safe if translations change)
512
+ if info:
513
+ act.setStatusTip(info)
514
+ act.setToolTip(f"{desc}\n{info}")
515
+ else:
516
+ act.setToolTip(desc)
517
+
518
+ # Checked state reflects current selection
519
+ act.blockSignals(True)
520
+ try:
521
+ act.setChecked(cur_method == str(key))
522
+ finally:
523
+ act.blockSignals(False)
524
+
525
+ any_sensor = True
526
+
527
+ if any_sensor:
528
+ menu.addMenu(sensors_root)
529
+
530
+ btn.setMenu(menu)
531
+ btn.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
532
+ btn.setStyleSheet("""
533
+ QToolButton { color: #dcdcdc; }
534
+ QToolButton:pressed, QToolButton:checked { color: #DAA520; font-weight: 600; }
535
+ """)
536
+
371
537
 
372
538
  def _rebind_view_dropdowns(self):
373
539
  """
@@ -429,7 +595,16 @@ class ToolbarMixin:
429
595
  fit_menu.addAction(self.act_auto_fit_resize) # use the real action
430
596
  btn_fit.setMenu(fit_menu)
431
597
  btn_fit.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
598
+ tb = self._toolbar_containing_action(self.act_autostretch)
599
+ if tb:
600
+ btn = tb.widgetForAction(self.act_autostretch)
601
+ if isinstance(btn, QToolButton):
602
+ # ... build menu ...
603
+ btn.setMenu(menu)
604
+ btn.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
432
605
 
606
+ # IMPORTANT: re-apply style after action moves / rebind
607
+ self._style_toggle_toolbutton(btn)
433
608
 
434
609
  def _bind_view_toolbar_menus(self, tb: DraggableToolBar):
435
610
  # --- Display-Stretch menu ---
@@ -485,6 +660,12 @@ class ToolbarMixin:
485
660
  btn_fit.setMenu(fit_menu)
486
661
  btn_fit.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
487
662
 
663
+ def _linux_force_text_action(self, act: QAction, text: str) -> None:
664
+ """On Linux, show text-only for this action (no theme icon)."""
665
+ if not sys.platform.startswith("linux"):
666
+ return
667
+ act.setIcon(QIcon()) # remove whatever theme icon got assigned
668
+ act.setText(text) # show the glyph/text
488
669
 
489
670
  def _create_actions(self):
490
671
  # File actions
@@ -606,30 +787,30 @@ class ToolbarMixin:
606
787
  self.act_bake_display_stretch.triggered.connect(self._bake_display_stretch)
607
788
 
608
789
  # --- Zoom controls ---
609
- # --- Zoom controls (themed icons) ---
610
790
  self.act_zoom_out = QAction(QIcon.fromTheme("zoom-out"), self.tr("Zoom Out"), self)
611
791
  self.act_zoom_out.setStatusTip(self.tr("Zoom out"))
612
792
  self.act_zoom_out.setShortcuts([QKeySequence("Ctrl+-")])
613
793
  self.act_zoom_out.triggered.connect(lambda: self._zoom_step_active(-1))
794
+ self._linux_force_text_action(self.act_zoom_out, "−") # true minus
614
795
 
615
796
  self.act_zoom_in = QAction(QIcon.fromTheme("zoom-in"), self.tr("Zoom In"), self)
616
797
  self.act_zoom_in.setStatusTip(self.tr("Zoom in"))
617
- self.act_zoom_in.setShortcuts([
618
- QKeySequence("Ctrl++"), # Ctrl + (Shift + = on many keyboards)
619
- QKeySequence("Ctrl+="), # fallback
620
- ])
798
+ self.act_zoom_in.setShortcuts([QKeySequence("Ctrl++"), QKeySequence("Ctrl+=")])
621
799
  self.act_zoom_in.triggered.connect(lambda: self._zoom_step_active(+1))
800
+ self._linux_force_text_action(self.act_zoom_in, "+")
622
801
 
623
802
  self.act_zoom_1_1 = QAction(QIcon.fromTheme("zoom-original"), self.tr("1:1"), self)
624
803
  self.act_zoom_1_1.setStatusTip(self.tr("Zoom to 100% (pixel-for-pixel)"))
625
804
  self.act_zoom_1_1.setShortcut(QKeySequence("Ctrl+1"))
626
805
  self.act_zoom_1_1.triggered.connect(self._zoom_active_1_1)
806
+ self._linux_force_text_action(self.act_zoom_1_1, "1:1")
627
807
 
628
808
  self.act_zoom_fit = QAction(QIcon.fromTheme("zoom-fit-best"), self.tr("Fit"), self)
629
809
  self.act_zoom_fit.setStatusTip(self.tr("Fit image to current window"))
630
810
  self.act_zoom_fit.setShortcut(QKeySequence("Ctrl+0"))
631
811
  self.act_zoom_fit.triggered.connect(self._zoom_active_fit)
632
812
  self.act_zoom_fit.setCheckable(True)
813
+ self._linux_force_text_action(self.act_zoom_fit, "Fit")
633
814
 
634
815
  self.act_auto_fit_resize = QAction(self.tr("Auto-fit on Resize"), self)
635
816
  self.act_auto_fit_resize.setCheckable(True)
@@ -754,6 +935,7 @@ class ToolbarMixin:
754
935
  self.luma_method = getattr(self, "luma_method", "rec709") # default
755
936
  self._luma_group = QActionGroup(self)
756
937
  self._luma_group.setExclusive(True)
938
+ self._luma_sensor_actions = {} # key -> QAction
757
939
 
758
940
  def _mk(method_key, text):
759
941
  act = QAction(text, self, checkable=True)
@@ -773,8 +955,10 @@ class ToolbarMixin:
773
955
 
774
956
  # update method when user picks from the menu
775
957
  def _on_luma_pick(act):
776
- self.luma_method = act.data()
777
- # (optional) persist
958
+ key = act.data()
959
+ if key is None:
960
+ return
961
+ self.luma_method = str(key)
778
962
  try:
779
963
  self.settings.setValue("ui/luminance_method", self.luma_method)
780
964
  except Exception:
@@ -873,6 +1057,12 @@ class ToolbarMixin:
873
1057
  self.act_geom_rot_180.setStatusTip(self.tr("Rotate image 180°"))
874
1058
  self.act_geom_rot_180.triggered.connect(self._exec_geom_rot_180)
875
1059
 
1060
+ self.act_geom_rot_any = QAction(QIcon(rotatearbitrary_path), self.tr("Rotate..."), self)
1061
+ self.act_geom_rot_any.setIconVisibleInMenu(True)
1062
+ self.act_geom_rot_any.setStatusTip(self.tr("Rotate image by an arbitrary angle (degrees)"))
1063
+ self.act_geom_rot_any.triggered.connect(self._exec_geom_rot_any)
1064
+
1065
+
876
1066
  self.act_geom_rescale = QAction(QIcon(rescale_path), self.tr("Rescale..."), self)
877
1067
  self.act_geom_rescale.setIconVisibleInMenu(True)
878
1068
  self.act_geom_rescale.setStatusTip(self.tr("Rescale image by a factor"))
@@ -1086,6 +1276,13 @@ class ToolbarMixin:
1086
1276
  self.act_copy_astrometry = QAction(self.tr("Copy Astrometric Solution..."), self)
1087
1277
  self.act_copy_astrometry.triggered.connect(self._open_copy_astrometry)
1088
1278
 
1279
+ self.act_acv_exporter = QAction(self.tr("Astro Catalogue Viewer Exporter..."), self)
1280
+ self.act_acv_exporter.setIconVisibleInMenu(True)
1281
+ self.act_acv_exporter.setStatusTip(self.tr("Export current view into Astro Catalogue Viewer folders"))
1282
+ self.act_acv_exporter.triggered.connect(self._open_acv_exporter)
1283
+
1284
+
1285
+
1089
1286
  # Create Mask
1090
1287
  self.act_create_mask = QAction(QIcon(maskcreate_path), self.tr("Create Mask..."), self)
1091
1288
  self.act_create_mask.setIconVisibleInMenu(True)
@@ -1196,6 +1393,7 @@ class ToolbarMixin:
1196
1393
  reg("geom_rotate_clockwise", self.act_geom_rot_cw)
1197
1394
  reg("geom_rotate_counterclockwise",self.act_geom_rot_ccw)
1198
1395
  reg("geom_rotate_180", self.act_geom_rot_180)
1396
+ reg("geom_rotate_any", self.act_geom_rot_any)
1199
1397
  reg("geom_rescale", self.act_geom_rescale)
1200
1398
  reg("project_new", self.act_project_new)
1201
1399
  reg("project_save", self.act_project_save)
@@ -1336,6 +1534,188 @@ class ToolbarMixin:
1336
1534
  if key:
1337
1535
  self._restore_toolbar_order(tb, str(key))
1338
1536
 
1537
+ def _hidden_toolbar(self):
1538
+ from setiastro.saspro.shortcuts import DraggableToolBar
1539
+ for tb in self.findChildren(DraggableToolBar):
1540
+ if getattr(tb, "_settings_key", None) == "Toolbar/Hidden" or tb.objectName() == "Hidden":
1541
+ return tb
1542
+ return None
1543
+
1544
+ def _toolbar_by_settings_key(self, key: str):
1545
+ from setiastro.saspro.shortcuts import DraggableToolBar
1546
+ for tb in self.findChildren(DraggableToolBar):
1547
+ if getattr(tb, "_settings_key", None) == key:
1548
+ return tb
1549
+ return None
1550
+
1551
+ def _action_cid(self, act):
1552
+ return str(act.property("command_id") or act.objectName() or "")
1553
+
1554
+ def _hide_action_to_hidden_toolbar(self, act):
1555
+ """
1556
+ Move action to the Hidden toolbar, remembering where it came from.
1557
+ """
1558
+ tb_hidden = self._hidden_toolbar()
1559
+ if not tb_hidden:
1560
+ return
1561
+
1562
+ cid = self._action_cid(act)
1563
+ if not cid:
1564
+ return
1565
+
1566
+ # Find current toolbar (if any) and remember it
1567
+ cur_tb = self._toolbar_containing_action(act)
1568
+ if cur_tb and getattr(cur_tb, "_settings_key", None) and cur_tb is not tb_hidden:
1569
+ prev_key = str(cur_tb._settings_key)
1570
+
1571
+ # Persist "previous toolbar" so unhide can restore
1572
+ try:
1573
+ raw = self.settings.value("Toolbar/HiddenPrev", "", type=str) or ""
1574
+ except Exception:
1575
+ raw = ""
1576
+ try:
1577
+ prev = json.loads(raw) if raw else {}
1578
+ except Exception:
1579
+ prev = {}
1580
+ prev[cid] = prev_key
1581
+ self.settings.setValue("Toolbar/HiddenPrev", json.dumps(prev))
1582
+
1583
+ # Remove from all toolbars, add to hidden
1584
+ from setiastro.saspro.shortcuts import DraggableToolBar
1585
+ for t in self.findChildren(DraggableToolBar):
1586
+ if act in t.actions():
1587
+ t.removeAction(act)
1588
+
1589
+ tb_hidden.addAction(act)
1590
+
1591
+ # Update assignment mapping so restore works on next launch
1592
+ tb_hidden._update_assignment_for_action(act)
1593
+
1594
+ # Persist ordering (optional but nice)
1595
+ try:
1596
+ tb_hidden._persist_order()
1597
+ except Exception:
1598
+ pass
1599
+ if cur_tb:
1600
+ try:
1601
+ cur_tb._persist_order()
1602
+ except Exception:
1603
+ pass
1604
+
1605
+ def _unhide_action_from_hidden_toolbar(self, act):
1606
+ """
1607
+ Move action out of Hidden toolbar back to its remembered toolbar.
1608
+ Falls back to Toolbar/View if missing.
1609
+ """
1610
+ tb_hidden = self._hidden_toolbar()
1611
+ if not tb_hidden:
1612
+ return
1613
+
1614
+ cid = self._action_cid(act)
1615
+ if not cid:
1616
+ return
1617
+
1618
+ # Load prev mapping
1619
+ try:
1620
+ raw = self.settings.value("Toolbar/HiddenPrev", "", type=str) or ""
1621
+ except Exception:
1622
+ raw = ""
1623
+ try:
1624
+ prev = json.loads(raw) if raw else {}
1625
+ except Exception:
1626
+ prev = {}
1627
+
1628
+ target_key = prev.get(cid) or "Toolbar/View"
1629
+ tb_target = self._toolbar_by_settings_key(target_key) or self._toolbar_by_settings_key("Toolbar/View")
1630
+ if not tb_target:
1631
+ return
1632
+
1633
+ tb_hidden.removeAction(act)
1634
+ tb_target.addAction(act)
1635
+
1636
+ # Update assignment mapping
1637
+ tb_target._update_assignment_for_action(act)
1638
+
1639
+ # Cleanup prev mapping (optional)
1640
+ if cid in prev:
1641
+ prev.pop(cid, None)
1642
+ self.settings.setValue("Toolbar/HiddenPrev", json.dumps(prev))
1643
+
1644
+ try:
1645
+ tb_target._persist_order()
1646
+ tb_hidden._persist_order()
1647
+ except Exception:
1648
+ pass
1649
+
1650
+ def _migrate_old_hidden_sets_to_hidden_toolbar(self):
1651
+ """
1652
+ One-time migration:
1653
+ Old system: per-toolbar QSettings lists at "<ToolbarKey>/Hidden" storing action IDs
1654
+ New system: move those actions into the dedicated Hidden toolbar (Toolbar/Hidden)
1655
+ and persist membership via Toolbar/Assignments.
1656
+ """
1657
+ if not hasattr(self, "settings"):
1658
+ return
1659
+
1660
+ # Run once
1661
+ if self.settings.value("Toolbar/HiddenMigrationDone", False, type=bool):
1662
+ return
1663
+
1664
+ tb_hidden = self._hidden_toolbar()
1665
+ if not tb_hidden:
1666
+ return
1667
+
1668
+ from setiastro.saspro.shortcuts import DraggableToolBar
1669
+
1670
+ # If Hidden toolbar already has actions, assume user is already on new system
1671
+ # but we still can migrate any remaining old keys.
1672
+ # (We won't early-return.)
1673
+
1674
+ for tb in self.findChildren(DraggableToolBar):
1675
+ k = getattr(tb, "_settings_key", None)
1676
+ if not k or str(k) == "Toolbar/Hidden":
1677
+ continue
1678
+
1679
+ # Old storage key
1680
+ old_key = f"{k}/Hidden"
1681
+
1682
+ # Read old hidden list (Qt backend may return list OR str)
1683
+ try:
1684
+ raw = self.settings.value(old_key, [], type=list)
1685
+ except Exception:
1686
+ raw = self.settings.value(old_key, []) # fallback
1687
+
1688
+ if not raw:
1689
+ continue
1690
+
1691
+ if isinstance(raw, str):
1692
+ raw_list = [raw]
1693
+ else:
1694
+ try:
1695
+ raw_list = list(raw)
1696
+ except Exception:
1697
+ raw_list = []
1698
+
1699
+ hidden_ids = {str(x) for x in raw_list if str(x).strip()}
1700
+ if not hidden_ids:
1701
+ # Clear junk so we don’t keep trying
1702
+ self.settings.setValue(old_key, [])
1703
+ continue
1704
+
1705
+ # Move matching actions into Hidden toolbar
1706
+ for act in list(tb.actions()):
1707
+ if act is None or act.isSeparator():
1708
+ continue
1709
+ cid = str(act.property("command_id") or act.objectName() or "")
1710
+ if cid and cid in hidden_ids:
1711
+ self._hide_action_to_hidden_toolbar(act)
1712
+
1713
+ # Clear old storage so you don't keep re-migrating
1714
+ self.settings.setValue(old_key, [])
1715
+
1716
+ # Mark migration complete
1717
+ self.settings.setValue("Toolbar/HiddenMigrationDone", True)
1718
+
1339
1719
 
1340
1720
  def update_undo_redo_action_labels(self):
1341
1721
  if not hasattr(self, "act_undo"): # not built yet
@@ -1387,6 +1767,7 @@ class ToolbarMixin:
1387
1767
  a.setStatusTip(tip)
1388
1768
  a.setEnabled(False)
1389
1769
 
1770
+
1390
1771
  def _sync_link_action_state(self):
1391
1772
  g = self._current_group_of_active()
1392
1773
  self.act_link_group.blockSignals(True)
@@ -1432,6 +1813,7 @@ class ToolbarMixin:
1432
1813
  QTimer.singleShot(0, self.update_undo_redo_action_labels)
1433
1814
 
1434
1815
  def _refresh_mask_action_states(self):
1816
+
1435
1817
  active_doc = self._active_doc()
1436
1818
 
1437
1819
  can_apply = bool(active_doc and self._list_candidate_mask_sources(exclude_doc=active_doc))
@@ -1455,3 +1837,10 @@ class ToolbarMixin:
1455
1837
  if hasattr(self, "act_hide_mask"):
1456
1838
  self.act_hide_mask.setEnabled(has_mask and overlay_on)
1457
1839
 
1840
+ def _style_toggle_toolbutton(self, btn: QToolButton):
1841
+ # Make sure the action visually shows "on" state
1842
+ btn.setCheckable(True) # safe even if already
1843
+ btn.setStyleSheet("""
1844
+ QToolButton { color: #dcdcdc; }
1845
+ QToolButton:checked { color: #DAA520; font-weight: 600; }
1846
+ """)