setiastrosuitepro 1.6.4__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 (115) 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/waning_crescent_1.png +0 -0
  14. setiastro/images/waning_crescent_2.png +0 -0
  15. setiastro/images/waning_crescent_3.png +0 -0
  16. setiastro/images/waning_crescent_4.png +0 -0
  17. setiastro/images/waning_crescent_5.png +0 -0
  18. setiastro/images/waning_gibbous_1.png +0 -0
  19. setiastro/images/waning_gibbous_2.png +0 -0
  20. setiastro/images/waning_gibbous_3.png +0 -0
  21. setiastro/images/waning_gibbous_4.png +0 -0
  22. setiastro/images/waning_gibbous_5.png +0 -0
  23. setiastro/images/waxing_crescent_1.png +0 -0
  24. setiastro/images/waxing_crescent_2.png +0 -0
  25. setiastro/images/waxing_crescent_3.png +0 -0
  26. setiastro/images/waxing_crescent_4.png +0 -0
  27. setiastro/images/waxing_crescent_5.png +0 -0
  28. setiastro/images/waxing_gibbous_1.png +0 -0
  29. setiastro/images/waxing_gibbous_2.png +0 -0
  30. setiastro/images/waxing_gibbous_3.png +0 -0
  31. setiastro/images/waxing_gibbous_4.png +0 -0
  32. setiastro/images/waxing_gibbous_5.png +0 -0
  33. setiastro/qml/ResourceMonitor.qml +84 -82
  34. setiastro/saspro/__main__.py +20 -1
  35. setiastro/saspro/_generated/build_info.py +2 -2
  36. setiastro/saspro/abe.py +37 -4
  37. setiastro/saspro/aberration_ai.py +237 -21
  38. setiastro/saspro/acv_exporter.py +379 -0
  39. setiastro/saspro/add_stars.py +33 -6
  40. setiastro/saspro/backgroundneutral.py +108 -40
  41. setiastro/saspro/blemish_blaster.py +4 -1
  42. setiastro/saspro/blink_comparator_pro.py +74 -24
  43. setiastro/saspro/clahe.py +4 -1
  44. setiastro/saspro/continuum_subtract.py +4 -1
  45. setiastro/saspro/convo.py +13 -7
  46. setiastro/saspro/cosmicclarity.py +129 -18
  47. setiastro/saspro/crop_dialog_pro.py +123 -7
  48. setiastro/saspro/curve_editor_pro.py +109 -42
  49. setiastro/saspro/doc_manager.py +245 -15
  50. setiastro/saspro/exoplanet_detector.py +120 -28
  51. setiastro/saspro/frequency_separation.py +1158 -204
  52. setiastro/saspro/ghs_dialog_pro.py +81 -16
  53. setiastro/saspro/graxpert.py +1 -0
  54. setiastro/saspro/gui/main_window.py +429 -228
  55. setiastro/saspro/gui/mixins/dock_mixin.py +245 -24
  56. setiastro/saspro/gui/mixins/menu_mixin.py +27 -1
  57. setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
  58. setiastro/saspro/gui/mixins/toolbar_mixin.py +384 -18
  59. setiastro/saspro/gui/mixins/update_mixin.py +138 -36
  60. setiastro/saspro/gui/mixins/view_mixin.py +42 -0
  61. setiastro/saspro/halobgon.py +4 -0
  62. setiastro/saspro/histogram.py +5 -1
  63. setiastro/saspro/image_combine.py +4 -0
  64. setiastro/saspro/image_peeker_pro.py +4 -0
  65. setiastro/saspro/imageops/starbasedwhitebalance.py +23 -52
  66. setiastro/saspro/imageops/stretch.py +582 -62
  67. setiastro/saspro/isophote.py +4 -0
  68. setiastro/saspro/layers.py +13 -9
  69. setiastro/saspro/layers_dock.py +183 -3
  70. setiastro/saspro/legacy/image_manager.py +154 -20
  71. setiastro/saspro/legacy/numba_utils.py +67 -47
  72. setiastro/saspro/legacy/xisf.py +240 -98
  73. setiastro/saspro/live_stacking.py +180 -79
  74. setiastro/saspro/luminancerecombine.py +228 -27
  75. setiastro/saspro/mask_creation.py +174 -15
  76. setiastro/saspro/mfdeconv.py +113 -35
  77. setiastro/saspro/mfdeconvcudnn.py +119 -70
  78. setiastro/saspro/mfdeconvsport.py +112 -35
  79. setiastro/saspro/morphology.py +4 -0
  80. setiastro/saspro/multiscale_decomp.py +51 -12
  81. setiastro/saspro/numba_utils.py +72 -57
  82. setiastro/saspro/ops/commands.py +18 -18
  83. setiastro/saspro/ops/script_editor.py +10 -2
  84. setiastro/saspro/ops/scripts.py +122 -0
  85. setiastro/saspro/perfect_palette_picker.py +37 -3
  86. setiastro/saspro/plate_solver.py +84 -49
  87. setiastro/saspro/psf_viewer.py +119 -37
  88. setiastro/saspro/resources.py +67 -0
  89. setiastro/saspro/rgbalign.py +4 -0
  90. setiastro/saspro/selective_color.py +4 -1
  91. setiastro/saspro/sfcc.py +364 -152
  92. setiastro/saspro/shortcuts.py +160 -29
  93. setiastro/saspro/signature_insert.py +692 -33
  94. setiastro/saspro/stacking_suite.py +1331 -484
  95. setiastro/saspro/star_alignment.py +247 -123
  96. setiastro/saspro/star_spikes.py +4 -0
  97. setiastro/saspro/star_stretch.py +38 -3
  98. setiastro/saspro/stat_stretch.py +743 -128
  99. setiastro/saspro/subwindow.py +786 -360
  100. setiastro/saspro/supernovaasteroidhunter.py +1 -1
  101. setiastro/saspro/wavescale_hdr.py +4 -1
  102. setiastro/saspro/wavescalede.py +4 -1
  103. setiastro/saspro/whitebalance.py +84 -12
  104. setiastro/saspro/widgets/common_utilities.py +28 -21
  105. setiastro/saspro/widgets/resource_monitor.py +109 -59
  106. setiastro/saspro/widgets/spinboxes.py +10 -13
  107. setiastro/saspro/wimi.py +27 -656
  108. setiastro/saspro/wims.py +13 -3
  109. setiastro/saspro/xisf.py +101 -11
  110. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/METADATA +2 -1
  111. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/RECORD +115 -82
  112. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/WHEEL +0 -0
  113. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/entry_points.txt +0 -0
  114. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/LICENSE +0 -0
  115. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/license.txt +0 -0
@@ -12,6 +12,7 @@ from PyQt6.QtWidgets import QMenu, QToolButton
12
12
 
13
13
  from PyQt6.QtCore import QElapsedTimer
14
14
 
15
+ import sys
15
16
 
16
17
  if TYPE_CHECKING:
17
18
  pass
@@ -203,16 +204,16 @@ class ToolbarMixin:
203
204
  tb_fn.addAction(self.act_convo)
204
205
  tb_fn.addAction(self.act_extract_luma)
205
206
 
206
- btn_luma = tb_fn.widgetForAction(self.act_extract_luma)
207
- if isinstance(btn_luma, QToolButton):
208
- luma_menu = QMenu(btn_luma)
209
- luma_menu.addActions(self._luma_group.actions())
210
- btn_luma.setMenu(luma_menu)
211
- btn_luma.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
212
- btn_luma.setStyleSheet("""
213
- QToolButton { color: #dcdcdc; }
214
- QToolButton:pressed, QToolButton:checked { color: #DAA520; font-weight: 600; }
215
- """)
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
+ # """)
216
217
 
217
218
  tb_fn.addAction(self.act_recombine_luma)
218
219
  tb_fn.addAction(self.act_rgb_extract)
@@ -361,9 +362,25 @@ class ToolbarMixin:
361
362
  except Exception:
362
363
  pass
363
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
+
364
372
  # This can move actions between toolbars, so do it after each toolbar has its base order restored.
365
373
  self._restore_toolbar_memberships()
366
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
+
367
384
  # Re-apply hidden state AFTER memberships (actions may have moved toolbars).
368
385
  # This also guarantees correctness even if any toolbar was rebuilt/adjusted internally.
369
386
  for _tb in self.findChildren(DraggableToolBar):
@@ -372,7 +389,9 @@ class ToolbarMixin:
372
389
  except Exception:
373
390
  pass
374
391
 
392
+ # Rebind ALL dropdowns after reorder/membership moves
375
393
  self._rebind_view_dropdowns()
394
+ self._rebind_extract_luma_dropdown()
376
395
 
377
396
  def _toolbar_containing_action(self, action: QAction):
378
397
  from setiastro.saspro.shortcuts import DraggableToolBar
@@ -381,6 +400,140 @@ class ToolbarMixin:
381
400
  return tb
382
401
  return None
383
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
+
384
537
 
385
538
  def _rebind_view_dropdowns(self):
386
539
  """
@@ -442,7 +595,16 @@ class ToolbarMixin:
442
595
  fit_menu.addAction(self.act_auto_fit_resize) # use the real action
443
596
  btn_fit.setMenu(fit_menu)
444
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)
445
605
 
606
+ # IMPORTANT: re-apply style after action moves / rebind
607
+ self._style_toggle_toolbutton(btn)
446
608
 
447
609
  def _bind_view_toolbar_menus(self, tb: DraggableToolBar):
448
610
  # --- Display-Stretch menu ---
@@ -498,6 +660,12 @@ class ToolbarMixin:
498
660
  btn_fit.setMenu(fit_menu)
499
661
  btn_fit.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
500
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
501
669
 
502
670
  def _create_actions(self):
503
671
  # File actions
@@ -619,30 +787,30 @@ class ToolbarMixin:
619
787
  self.act_bake_display_stretch.triggered.connect(self._bake_display_stretch)
620
788
 
621
789
  # --- Zoom controls ---
622
- # --- Zoom controls (themed icons) ---
623
790
  self.act_zoom_out = QAction(QIcon.fromTheme("zoom-out"), self.tr("Zoom Out"), self)
624
791
  self.act_zoom_out.setStatusTip(self.tr("Zoom out"))
625
792
  self.act_zoom_out.setShortcuts([QKeySequence("Ctrl+-")])
626
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
627
795
 
628
796
  self.act_zoom_in = QAction(QIcon.fromTheme("zoom-in"), self.tr("Zoom In"), self)
629
797
  self.act_zoom_in.setStatusTip(self.tr("Zoom in"))
630
- self.act_zoom_in.setShortcuts([
631
- QKeySequence("Ctrl++"), # Ctrl + (Shift + = on many keyboards)
632
- QKeySequence("Ctrl+="), # fallback
633
- ])
798
+ self.act_zoom_in.setShortcuts([QKeySequence("Ctrl++"), QKeySequence("Ctrl+=")])
634
799
  self.act_zoom_in.triggered.connect(lambda: self._zoom_step_active(+1))
800
+ self._linux_force_text_action(self.act_zoom_in, "+")
635
801
 
636
802
  self.act_zoom_1_1 = QAction(QIcon.fromTheme("zoom-original"), self.tr("1:1"), self)
637
803
  self.act_zoom_1_1.setStatusTip(self.tr("Zoom to 100% (pixel-for-pixel)"))
638
804
  self.act_zoom_1_1.setShortcut(QKeySequence("Ctrl+1"))
639
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")
640
807
 
641
808
  self.act_zoom_fit = QAction(QIcon.fromTheme("zoom-fit-best"), self.tr("Fit"), self)
642
809
  self.act_zoom_fit.setStatusTip(self.tr("Fit image to current window"))
643
810
  self.act_zoom_fit.setShortcut(QKeySequence("Ctrl+0"))
644
811
  self.act_zoom_fit.triggered.connect(self._zoom_active_fit)
645
812
  self.act_zoom_fit.setCheckable(True)
813
+ self._linux_force_text_action(self.act_zoom_fit, "Fit")
646
814
 
647
815
  self.act_auto_fit_resize = QAction(self.tr("Auto-fit on Resize"), self)
648
816
  self.act_auto_fit_resize.setCheckable(True)
@@ -767,6 +935,7 @@ class ToolbarMixin:
767
935
  self.luma_method = getattr(self, "luma_method", "rec709") # default
768
936
  self._luma_group = QActionGroup(self)
769
937
  self._luma_group.setExclusive(True)
938
+ self._luma_sensor_actions = {} # key -> QAction
770
939
 
771
940
  def _mk(method_key, text):
772
941
  act = QAction(text, self, checkable=True)
@@ -786,8 +955,10 @@ class ToolbarMixin:
786
955
 
787
956
  # update method when user picks from the menu
788
957
  def _on_luma_pick(act):
789
- self.luma_method = act.data()
790
- # (optional) persist
958
+ key = act.data()
959
+ if key is None:
960
+ return
961
+ self.luma_method = str(key)
791
962
  try:
792
963
  self.settings.setValue("ui/luminance_method", self.luma_method)
793
964
  except Exception:
@@ -1105,6 +1276,13 @@ class ToolbarMixin:
1105
1276
  self.act_copy_astrometry = QAction(self.tr("Copy Astrometric Solution..."), self)
1106
1277
  self.act_copy_astrometry.triggered.connect(self._open_copy_astrometry)
1107
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
+
1108
1286
  # Create Mask
1109
1287
  self.act_create_mask = QAction(QIcon(maskcreate_path), self.tr("Create Mask..."), self)
1110
1288
  self.act_create_mask.setIconVisibleInMenu(True)
@@ -1356,6 +1534,188 @@ class ToolbarMixin:
1356
1534
  if key:
1357
1535
  self._restore_toolbar_order(tb, str(key))
1358
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
+
1359
1719
 
1360
1720
  def update_undo_redo_action_labels(self):
1361
1721
  if not hasattr(self, "act_undo"): # not built yet
@@ -1477,4 +1837,10 @@ class ToolbarMixin:
1477
1837
  if hasattr(self, "act_hide_mask"):
1478
1838
  self.act_hide_mask.setEnabled(has_mask and overlay_on)
1479
1839
 
1480
-
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
+ """)