setiastrosuitepro 1.6.4__py3-none-any.whl → 1.7.1.post2__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 (132) hide show
  1. setiastro/images/TextureClarity.svg +56 -0
  2. setiastro/images/abeicon.svg +16 -0
  3. setiastro/images/acv_icon.png +0 -0
  4. setiastro/images/colorwheel.svg +97 -0
  5. setiastro/images/cosmic.svg +40 -0
  6. setiastro/images/cosmicsat.svg +24 -0
  7. setiastro/images/first_quarter.png +0 -0
  8. setiastro/images/full_moon.png +0 -0
  9. setiastro/images/graxpert.svg +19 -0
  10. setiastro/images/last_quarter.png +0 -0
  11. setiastro/images/linearfit.svg +32 -0
  12. setiastro/images/narrowbandnormalization.png +0 -0
  13. setiastro/images/new_moon.png +0 -0
  14. setiastro/images/pixelmath.svg +42 -0
  15. setiastro/images/planetarystacker.png +0 -0
  16. setiastro/images/waning_crescent_1.png +0 -0
  17. setiastro/images/waning_crescent_2.png +0 -0
  18. setiastro/images/waning_crescent_3.png +0 -0
  19. setiastro/images/waning_crescent_4.png +0 -0
  20. setiastro/images/waning_crescent_5.png +0 -0
  21. setiastro/images/waning_gibbous_1.png +0 -0
  22. setiastro/images/waning_gibbous_2.png +0 -0
  23. setiastro/images/waning_gibbous_3.png +0 -0
  24. setiastro/images/waning_gibbous_4.png +0 -0
  25. setiastro/images/waning_gibbous_5.png +0 -0
  26. setiastro/images/waxing_crescent_1.png +0 -0
  27. setiastro/images/waxing_crescent_2.png +0 -0
  28. setiastro/images/waxing_crescent_3.png +0 -0
  29. setiastro/images/waxing_crescent_4.png +0 -0
  30. setiastro/images/waxing_crescent_5.png +0 -0
  31. setiastro/images/waxing_gibbous_1.png +0 -0
  32. setiastro/images/waxing_gibbous_2.png +0 -0
  33. setiastro/images/waxing_gibbous_3.png +0 -0
  34. setiastro/images/waxing_gibbous_4.png +0 -0
  35. setiastro/images/waxing_gibbous_5.png +0 -0
  36. setiastro/qml/ResourceMonitor.qml +84 -82
  37. setiastro/saspro/__main__.py +20 -1
  38. setiastro/saspro/_generated/build_info.py +2 -2
  39. setiastro/saspro/abe.py +37 -4
  40. setiastro/saspro/aberration_ai.py +364 -33
  41. setiastro/saspro/aberration_ai_preset.py +29 -3
  42. setiastro/saspro/acv_exporter.py +379 -0
  43. setiastro/saspro/add_stars.py +33 -6
  44. setiastro/saspro/astrospike_python.py +45 -3
  45. setiastro/saspro/backgroundneutral.py +108 -40
  46. setiastro/saspro/blemish_blaster.py +4 -1
  47. setiastro/saspro/blink_comparator_pro.py +150 -55
  48. setiastro/saspro/clahe.py +4 -1
  49. setiastro/saspro/continuum_subtract.py +4 -1
  50. setiastro/saspro/convo.py +13 -7
  51. setiastro/saspro/cosmicclarity.py +129 -18
  52. setiastro/saspro/crop_dialog_pro.py +123 -7
  53. setiastro/saspro/curve_editor_pro.py +181 -64
  54. setiastro/saspro/curves_preset.py +249 -47
  55. setiastro/saspro/doc_manager.py +245 -15
  56. setiastro/saspro/exoplanet_detector.py +120 -28
  57. setiastro/saspro/frequency_separation.py +1158 -204
  58. setiastro/saspro/ghs_dialog_pro.py +81 -16
  59. setiastro/saspro/graxpert.py +1 -0
  60. setiastro/saspro/gui/main_window.py +706 -264
  61. setiastro/saspro/gui/mixins/dock_mixin.py +245 -24
  62. setiastro/saspro/gui/mixins/file_mixin.py +35 -16
  63. setiastro/saspro/gui/mixins/menu_mixin.py +35 -1
  64. setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
  65. setiastro/saspro/gui/mixins/toolbar_mixin.py +499 -24
  66. setiastro/saspro/gui/mixins/update_mixin.py +138 -36
  67. setiastro/saspro/gui/mixins/view_mixin.py +42 -0
  68. setiastro/saspro/halobgon.py +4 -0
  69. setiastro/saspro/histogram.py +184 -8
  70. setiastro/saspro/image_combine.py +4 -0
  71. setiastro/saspro/image_peeker_pro.py +4 -0
  72. setiastro/saspro/imageops/narrowband_normalization.py +816 -0
  73. setiastro/saspro/imageops/serloader.py +1345 -0
  74. setiastro/saspro/imageops/starbasedwhitebalance.py +23 -52
  75. setiastro/saspro/imageops/stretch.py +582 -62
  76. setiastro/saspro/isophote.py +4 -0
  77. setiastro/saspro/layers.py +13 -9
  78. setiastro/saspro/layers_dock.py +183 -3
  79. setiastro/saspro/legacy/image_manager.py +154 -20
  80. setiastro/saspro/legacy/numba_utils.py +68 -48
  81. setiastro/saspro/legacy/xisf.py +240 -98
  82. setiastro/saspro/live_stacking.py +203 -82
  83. setiastro/saspro/luminancerecombine.py +228 -27
  84. setiastro/saspro/mask_creation.py +174 -15
  85. setiastro/saspro/mfdeconv.py +113 -35
  86. setiastro/saspro/mfdeconvcudnn.py +119 -70
  87. setiastro/saspro/mfdeconvsport.py +112 -35
  88. setiastro/saspro/morphology.py +4 -0
  89. setiastro/saspro/multiscale_decomp.py +81 -29
  90. setiastro/saspro/narrowband_normalization.py +1618 -0
  91. setiastro/saspro/numba_utils.py +72 -57
  92. setiastro/saspro/ops/commands.py +18 -18
  93. setiastro/saspro/ops/script_editor.py +10 -2
  94. setiastro/saspro/ops/scripts.py +122 -0
  95. setiastro/saspro/perfect_palette_picker.py +37 -3
  96. setiastro/saspro/plate_solver.py +84 -49
  97. setiastro/saspro/psf_viewer.py +119 -37
  98. setiastro/saspro/remove_green.py +1 -1
  99. setiastro/saspro/resources.py +73 -0
  100. setiastro/saspro/rgbalign.py +460 -12
  101. setiastro/saspro/selective_color.py +4 -1
  102. setiastro/saspro/ser_stack_config.py +82 -0
  103. setiastro/saspro/ser_stacker.py +2321 -0
  104. setiastro/saspro/ser_stacker_dialog.py +1838 -0
  105. setiastro/saspro/ser_tracking.py +206 -0
  106. setiastro/saspro/serviewer.py +1625 -0
  107. setiastro/saspro/sfcc.py +662 -216
  108. setiastro/saspro/shortcuts.py +171 -33
  109. setiastro/saspro/signature_insert.py +692 -33
  110. setiastro/saspro/stacking_suite.py +1347 -485
  111. setiastro/saspro/star_alignment.py +247 -123
  112. setiastro/saspro/star_spikes.py +4 -0
  113. setiastro/saspro/star_stretch.py +38 -3
  114. setiastro/saspro/stat_stretch.py +892 -129
  115. setiastro/saspro/subwindow.py +787 -363
  116. setiastro/saspro/supernovaasteroidhunter.py +1 -1
  117. setiastro/saspro/texture_clarity.py +593 -0
  118. setiastro/saspro/wavescale_hdr.py +4 -1
  119. setiastro/saspro/wavescalede.py +4 -1
  120. setiastro/saspro/whitebalance.py +84 -12
  121. setiastro/saspro/widgets/common_utilities.py +28 -21
  122. setiastro/saspro/widgets/resource_monitor.py +209 -111
  123. setiastro/saspro/widgets/spinboxes.py +10 -13
  124. setiastro/saspro/wimi.py +27 -656
  125. setiastro/saspro/wims.py +13 -3
  126. setiastro/saspro/xisf.py +101 -11
  127. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/METADATA +4 -2
  128. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/RECORD +132 -87
  129. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/WHEEL +0 -0
  130. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/entry_points.txt +0 -0
  131. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/licenses/LICENSE +0 -0
  132. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/licenses/license.txt +0 -0
@@ -12,13 +12,15 @@ from PyQt6.QtWidgets import QMenu, QToolButton
12
12
 
13
13
  from PyQt6.QtCore import QElapsedTimer
14
14
 
15
+ import sys
16
+ import os
15
17
 
16
18
  if TYPE_CHECKING:
17
19
  pass
18
20
 
19
21
  # Import icon paths - these are needed at runtime
20
22
  from setiastro.saspro.resources import (
21
- icon_path, green_path, neutral_path, whitebalance_path,
23
+ icon_path, green_path, neutral_path, whitebalance_path, texture_clarity_path,
22
24
  morpho_path, clahe_path, starnet_path, staradd_path, LExtract_path,
23
25
  LInsert_path, rgbcombo_path, rgbextract_path, graxperticon_path,
24
26
  cropicon_path, openfile_path, abeicon_path, undoicon_path, redoicon_path,
@@ -30,10 +32,10 @@ from setiastro.saspro.resources import (
30
32
  stacking_path, pedestal_icon_path, starspike_path, astrospike_path,
31
33
  signature_icon_path, livestacking_path, convoicon_path, spcc_icon_path,
32
34
  exoicon_path, peeker_icon, dse_icon_path, isophote_path, statstretch_path,
33
- starstretch_path, curves_path, disk_path, uhs_path, blink_path, ppp_path,
35
+ starstretch_path, curves_path, disk_path, uhs_path, blink_path, ppp_path, narrowbandnormalization_path,
34
36
  nbtorgb_path, freqsep_path, multiscale_decomp_path, contsub_path, halo_path, cosmic_path,
35
37
  satellite_path, imagecombine_path, wims_path, wimi_path, linearfit_path,
36
- debayer_path, aberration_path, functionbundles_path, viewbundles_path,
38
+ debayer_path, aberration_path, functionbundles_path, viewbundles_path, planetarystacker_path,
37
39
  selectivecolor_path, rgbalign_path,
38
40
  )
39
41
 
@@ -203,16 +205,16 @@ class ToolbarMixin:
203
205
  tb_fn.addAction(self.act_convo)
204
206
  tb_fn.addAction(self.act_extract_luma)
205
207
 
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
- """)
208
+ #btn_luma = tb_fn.widgetForAction(self.act_extract_luma)
209
+ #if isinstance(btn_luma, QToolButton):
210
+ # luma_menu = QMenu(btn_luma)
211
+ # luma_menu.addActions(self._luma_group.actions())
212
+ # btn_luma.setMenu(luma_menu)
213
+ # btn_luma.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
214
+ # btn_luma.setStyleSheet("""
215
+ # QToolButton { color: #dcdcdc; }
216
+ # QToolButton:pressed, QToolButton:checked { color: #DAA520; font-weight: 600; }
217
+ # """)
216
218
 
217
219
  tb_fn.addAction(self.act_recombine_luma)
218
220
  tb_fn.addAction(self.act_rgb_extract)
@@ -221,6 +223,7 @@ class ToolbarMixin:
221
223
  tb_fn.addAction(self.act_wavescale_hdr)
222
224
  tb_fn.addAction(self.act_wavescale_de)
223
225
  tb_fn.addAction(self.act_clahe)
226
+ tb_fn.addAction(self.act_texture_clarity)
224
227
  tb_fn.addAction(self.act_morphology)
225
228
  tb_fn.addAction(self.act_pixelmath)
226
229
  tb_fn.addAction(self.act_signature)
@@ -255,6 +258,8 @@ class ToolbarMixin:
255
258
  tb_tl.addAction(self.act_blink) # Tools start here; Blink shows with QIcon(blink_path)
256
259
  tb_tl.addAction(self.act_ppp) # Perfect Palette Picker
257
260
  tb_tl.addAction(self.act_nbtorgb)
261
+ tb_tl.addAction(self.act_narrowband_normalization)
262
+
258
263
  tb_tl.addAction(self.act_selective_color)
259
264
  tb_tl.addAction(self.act_freqsep)
260
265
  tb_tl.addAction(self.act_multiscale_decomp)
@@ -301,6 +306,7 @@ class ToolbarMixin:
301
306
  tb_star.addAction(self.act_psf_viewer)
302
307
  tb_star.addAction(self.act_stacking_suite)
303
308
  tb_star.addAction(self.act_live_stacking)
309
+ tb_star.addAction(self.act_planetary_stacker)
304
310
  tb_star.addAction(self.act_plate_solve)
305
311
  tb_star.addAction(self.act_star_align)
306
312
  tb_star.addAction(self.act_star_register)
@@ -361,9 +367,26 @@ class ToolbarMixin:
361
367
  except Exception:
362
368
  pass
363
369
 
370
+
371
+ tb_hidden = DraggableToolBar(self.tr("Hidden"), self)
372
+ tb_hidden.setObjectName("Hidden")
373
+ tb_hidden.setSettingsKey("Toolbar/Hidden")
374
+ self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_hidden)
375
+ #tb_hidden.addAction(self.act_narrowband_normalization)
376
+ tb_hidden.setVisible(False) # <- always hidden
377
+
364
378
  # This can move actions between toolbars, so do it after each toolbar has its base order restored.
365
379
  self._restore_toolbar_memberships()
366
380
 
381
+ # Migrate legacy per-toolbar Hidden lists into the new Hidden toolbar
382
+ self._migrate_old_hidden_sets_to_hidden_toolbar()
383
+
384
+ # (Optional) Re-apply per-toolbar order after migration (since we removed actions)
385
+ for _tb in self.findChildren(DraggableToolBar):
386
+ key = getattr(_tb, "_settings_key", None)
387
+ if key:
388
+ self._restore_toolbar_order(_tb, str(key))
389
+
367
390
  # Re-apply hidden state AFTER memberships (actions may have moved toolbars).
368
391
  # This also guarantees correctness even if any toolbar was rebuilt/adjusted internally.
369
392
  for _tb in self.findChildren(DraggableToolBar):
@@ -372,7 +395,9 @@ class ToolbarMixin:
372
395
  except Exception:
373
396
  pass
374
397
 
398
+ # Rebind ALL dropdowns after reorder/membership moves
375
399
  self._rebind_view_dropdowns()
400
+ self._rebind_extract_luma_dropdown()
376
401
 
377
402
  def _toolbar_containing_action(self, action: QAction):
378
403
  from setiastro.saspro.shortcuts import DraggableToolBar
@@ -381,6 +406,140 @@ class ToolbarMixin:
381
406
  return tb
382
407
  return None
383
408
 
409
+ def _rebind_extract_luma_dropdown(self):
410
+ from PyQt6.QtWidgets import QMenu, QToolButton
411
+ from setiastro.saspro.luminancerecombine import LUMA_PROFILES
412
+
413
+ tb = self._toolbar_containing_action(self.act_extract_luma)
414
+ if not tb:
415
+ return
416
+
417
+ btn = tb.widgetForAction(self.act_extract_luma)
418
+ if not isinstance(btn, QToolButton):
419
+ return
420
+
421
+ menu = QMenu(btn)
422
+
423
+ # Ensure cache exists (created in _create_actions(), but be defensive)
424
+ if not hasattr(self, "_luma_sensor_actions"):
425
+ self._luma_sensor_actions = {} # key -> QAction
426
+
427
+ cur_method = str(getattr(self, "luma_method", "rec709"))
428
+
429
+ # ============================================================
430
+ # PATCH SITE #1 (Standard menu) <-- THIS IS WHERE "C" GOES
431
+ # Find your old block:
432
+ #
433
+ # # ---- Standard (use your QActionGroup, keep it simple) ----
434
+ # if getattr(self, "_luma_group", None) is not None:
435
+ # std_menu = QMenu(self.tr("Standard"), menu)
436
+ # std_menu.addActions(self._luma_group.actions())
437
+ # menu.addMenu(std_menu)
438
+ #
439
+ # Replace it with the block below.
440
+ # ============================================================
441
+ if getattr(self, "_luma_group", None) is not None:
442
+ # Sync standard checked states from self.luma_method
443
+ for a in self._luma_group.actions():
444
+ data = str(a.data() or "")
445
+ if data.startswith("sensor:"):
446
+ continue # standards only in Standard menu
447
+ a.blockSignals(True)
448
+ try:
449
+ a.setChecked(data == cur_method)
450
+ finally:
451
+ a.blockSignals(False)
452
+
453
+ # Add ONLY standard actions to the Standard menu (exclude sensors)
454
+ std_menu = QMenu(self.tr("Standard"), menu)
455
+ for a in self._luma_group.actions():
456
+ data = str(a.data() or "")
457
+ if data.startswith("sensor:"):
458
+ continue
459
+ std_menu.addAction(a)
460
+ menu.addMenu(std_menu)
461
+
462
+ # ---- Sensors (nested by category path) ----
463
+ sensors_root = QMenu(self.tr("Sensors"), menu)
464
+
465
+ # Build tree of submenus keyed by "Sensors/<path>"
466
+ submenu_cache: dict[str, QMenu] = {}
467
+
468
+ def get_or_make_path(root_menu: QMenu, path: str) -> QMenu:
469
+ parts = [p for p in path.split("/") if p.strip()]
470
+ cur = root_menu
471
+ acc = ""
472
+ for part in parts:
473
+ acc = f"{acc}/{part}" if acc else part
474
+ if acc not in submenu_cache:
475
+ sm = QMenu(self.tr(part), cur)
476
+ cur.addMenu(sm)
477
+ submenu_cache[acc] = sm
478
+ cur = submenu_cache[acc]
479
+ return cur
480
+
481
+ any_sensor = False
482
+ for key, prof in LUMA_PROFILES.items():
483
+ if not str(key).startswith("sensor:"):
484
+ continue
485
+
486
+ cat = str(prof.get("category", "Sensors/Other"))
487
+ group_path = cat.split("Sensors/", 1)[1] if "Sensors/" in cat else cat
488
+ parent_menu = get_or_make_path(sensors_root, group_path)
489
+
490
+ display_name = key.split("sensor:", 1)[1].strip()
491
+ desc = str(prof.get("description", display_name))
492
+ info = str(prof.get("info", "")).strip()
493
+
494
+ # ============================================================
495
+ # PATCH SITE #2 (Sensors become mutually exclusive with standards)
496
+ # - Cache the QAction so we don't pile up connections/actions
497
+ # - Add it to the SAME QActionGroup (exclusive) as standards
498
+ # - Do NOT use _pick_sensor; rely on QActionGroup.triggered
499
+ # ============================================================
500
+ act = self._luma_sensor_actions.get(key)
501
+ if act is None:
502
+ act = parent_menu.addAction(self.tr(display_name))
503
+ act.setCheckable(True)
504
+ act.setData(key) # IMPORTANT: enables group.triggered to set luma_method
505
+
506
+ # Put sensors into the SAME exclusive group so selecting one
507
+ # deselects everything else (standards and other sensors).
508
+ if getattr(self, "_luma_group", None) is not None:
509
+ self._luma_group.addAction(act)
510
+
511
+ self._luma_sensor_actions[key] = act
512
+ else:
513
+ # Reuse action, but re-add it to the correct submenu location
514
+ parent_menu.addAction(act)
515
+ act.setText(self.tr(display_name))
516
+
517
+ # Update UI info each bind (safe if translations change)
518
+ if info:
519
+ act.setStatusTip(info)
520
+ act.setToolTip(f"{desc}\n{info}")
521
+ else:
522
+ act.setToolTip(desc)
523
+
524
+ # Checked state reflects current selection
525
+ act.blockSignals(True)
526
+ try:
527
+ act.setChecked(cur_method == str(key))
528
+ finally:
529
+ act.blockSignals(False)
530
+
531
+ any_sensor = True
532
+
533
+ if any_sensor:
534
+ menu.addMenu(sensors_root)
535
+
536
+ btn.setMenu(menu)
537
+ btn.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
538
+ btn.setStyleSheet("""
539
+ QToolButton { color: #dcdcdc; }
540
+ QToolButton:pressed, QToolButton:checked { color: #DAA520; font-weight: 600; }
541
+ """)
542
+
384
543
 
385
544
  def _rebind_view_dropdowns(self):
386
545
  """
@@ -442,7 +601,16 @@ class ToolbarMixin:
442
601
  fit_menu.addAction(self.act_auto_fit_resize) # use the real action
443
602
  btn_fit.setMenu(fit_menu)
444
603
  btn_fit.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
604
+ tb = self._toolbar_containing_action(self.act_autostretch)
605
+ if tb:
606
+ btn = tb.widgetForAction(self.act_autostretch)
607
+ if isinstance(btn, QToolButton):
608
+ # ... build menu ...
609
+ btn.setMenu(menu)
610
+ btn.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
445
611
 
612
+ # IMPORTANT: re-apply style after action moves / rebind
613
+ self._style_toggle_toolbutton(btn)
446
614
 
447
615
  def _bind_view_toolbar_menus(self, tb: DraggableToolBar):
448
616
  # --- Display-Stretch menu ---
@@ -498,6 +666,12 @@ class ToolbarMixin:
498
666
  btn_fit.setMenu(fit_menu)
499
667
  btn_fit.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
500
668
 
669
+ def _linux_force_text_action(self, act: QAction, text: str) -> None:
670
+ """On Linux, show text-only for this action (no theme icon)."""
671
+ if not sys.platform.startswith("linux"):
672
+ return
673
+ act.setIcon(QIcon()) # remove whatever theme icon got assigned
674
+ act.setText(text) # show the glyph/text
501
675
 
502
676
  def _create_actions(self):
503
677
  # File actions
@@ -619,30 +793,30 @@ class ToolbarMixin:
619
793
  self.act_bake_display_stretch.triggered.connect(self._bake_display_stretch)
620
794
 
621
795
  # --- Zoom controls ---
622
- # --- Zoom controls (themed icons) ---
623
796
  self.act_zoom_out = QAction(QIcon.fromTheme("zoom-out"), self.tr("Zoom Out"), self)
624
797
  self.act_zoom_out.setStatusTip(self.tr("Zoom out"))
625
798
  self.act_zoom_out.setShortcuts([QKeySequence("Ctrl+-")])
626
799
  self.act_zoom_out.triggered.connect(lambda: self._zoom_step_active(-1))
800
+ self._linux_force_text_action(self.act_zoom_out, "−") # true minus
627
801
 
628
802
  self.act_zoom_in = QAction(QIcon.fromTheme("zoom-in"), self.tr("Zoom In"), self)
629
803
  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
- ])
804
+ self.act_zoom_in.setShortcuts([QKeySequence("Ctrl++"), QKeySequence("Ctrl+=")])
634
805
  self.act_zoom_in.triggered.connect(lambda: self._zoom_step_active(+1))
806
+ self._linux_force_text_action(self.act_zoom_in, "+")
635
807
 
636
808
  self.act_zoom_1_1 = QAction(QIcon.fromTheme("zoom-original"), self.tr("1:1"), self)
637
809
  self.act_zoom_1_1.setStatusTip(self.tr("Zoom to 100% (pixel-for-pixel)"))
638
810
  self.act_zoom_1_1.setShortcut(QKeySequence("Ctrl+1"))
639
811
  self.act_zoom_1_1.triggered.connect(self._zoom_active_1_1)
812
+ self._linux_force_text_action(self.act_zoom_1_1, "1:1")
640
813
 
641
814
  self.act_zoom_fit = QAction(QIcon.fromTheme("zoom-fit-best"), self.tr("Fit"), self)
642
815
  self.act_zoom_fit.setStatusTip(self.tr("Fit image to current window"))
643
816
  self.act_zoom_fit.setShortcut(QKeySequence("Ctrl+0"))
644
817
  self.act_zoom_fit.triggered.connect(self._zoom_active_fit)
645
818
  self.act_zoom_fit.setCheckable(True)
819
+ self._linux_force_text_action(self.act_zoom_fit, "Fit")
646
820
 
647
821
  self.act_auto_fit_resize = QAction(self.tr("Auto-fit on Resize"), self)
648
822
  self.act_auto_fit_resize.setCheckable(True)
@@ -661,7 +835,13 @@ class ToolbarMixin:
661
835
  self.act_paste_view.setShortcut("Ctrl+Shift+V")
662
836
  self.act_copy_view.triggered.connect(self._copy_active_view)
663
837
  self.act_paste_view.triggered.connect(self._paste_active_view)
664
-
838
+ # --- Edit: Mono -> RGB (triplicate channels) ---
839
+ self.act_mono_to_rgb = QAction(self.tr("Convert Mono to RGB"), self)
840
+ self.act_mono_to_rgb.setStatusTip(self.tr("Convert a mono image to RGB by duplicating the channel"))
841
+ self.act_mono_to_rgb.triggered.connect(self._convert_mono_to_rgb_active)
842
+ self.act_swap_rb = QAction(self.tr("Swap R and B Channels"), self)
843
+ self.act_swap_rb.setStatusTip(self.tr("Swap red and blue channels on the active RGB image"))
844
+ self.act_swap_rb.triggered.connect(self._swap_rb_active)
665
845
  # Functions
666
846
  self.act_crop = QAction(QIcon(cropicon_path), self.tr("Crop..."), self)
667
847
  self.act_crop.setStatusTip(self.tr("Crop / rotate with handles"))
@@ -725,11 +905,17 @@ class ToolbarMixin:
725
905
  self.act_linear_fit.setShortcut("Ctrl+L")
726
906
  self.act_linear_fit.triggered.connect(self._open_linear_fit)
727
907
 
728
- self.act_remove_green = QAction(QIcon(green_path), self.tr("Remove Green..."), self)
908
+ self.act_remove_green = QAction(QIcon(green_path), self.tr("Remove Green (SCNR)..."), self)
729
909
  self.act_remove_green.setToolTip(self.tr("SCNR-style green channel removal."))
730
910
  self.act_remove_green.setIconVisibleInMenu(True)
731
911
  self.act_remove_green.triggered.connect(self._open_remove_green)
732
912
 
913
+ # Texture and Clarity
914
+ self.act_texture_clarity = QAction(QIcon(texture_clarity_path), self.tr("Texture and Clarity..."), self)
915
+ self.act_texture_clarity.setToolTip(self.tr("Enhance texture and clarity using Unsharp Masking"))
916
+ self.act_texture_clarity.setIconVisibleInMenu(True)
917
+ self.act_texture_clarity.triggered.connect(self._open_texture_clarity)
918
+
733
919
  self.act_background_neutral = QAction(QIcon(neutral_path), self.tr("Background Neutralization..."), self)
734
920
  self.act_background_neutral.setStatusTip(self.tr("Neutralize background color balance using a sampled region"))
735
921
  self.act_background_neutral.setIconVisibleInMenu(True)
@@ -767,6 +953,7 @@ class ToolbarMixin:
767
953
  self.luma_method = getattr(self, "luma_method", "rec709") # default
768
954
  self._luma_group = QActionGroup(self)
769
955
  self._luma_group.setExclusive(True)
956
+ self._luma_sensor_actions = {} # key -> QAction
770
957
 
771
958
  def _mk(method_key, text):
772
959
  act = QAction(text, self, checkable=True)
@@ -786,8 +973,10 @@ class ToolbarMixin:
786
973
 
787
974
  # update method when user picks from the menu
788
975
  def _on_luma_pick(act):
789
- self.luma_method = act.data()
790
- # (optional) persist
976
+ key = act.data()
977
+ if key is None:
978
+ return
979
+ self.luma_method = str(key)
791
980
  try:
792
981
  self.settings.setValue("ui/luminance_method", self.luma_method)
793
982
  except Exception:
@@ -947,6 +1136,13 @@ class ToolbarMixin:
947
1136
  self.act_ppp.setStatusTip(self.tr("Pick the perfect palette for your image"))
948
1137
  self.act_ppp.triggered.connect(self._open_ppp_tool)
949
1138
 
1139
+ self.act_narrowband_normalization = QAction(QIcon(narrowbandnormalization_path), self.tr("Narrowband Normalization..."), self)
1140
+ self.act_narrowband_normalization.setStatusTip(
1141
+ self.tr("Normalize HOO/SHO/HSO/HOS (PixelMath port by Bill Blanshan and Mike Cranfield)")
1142
+ )
1143
+
1144
+ self.act_narrowband_normalization.triggered.connect(self._open_narrowband_normalization_tool)
1145
+
950
1146
  self.act_nbtorgb = QAction(QIcon(nbtorgb_path), self.tr("NB->RGB Stars..."), self)
951
1147
  self.act_nbtorgb.setStatusTip(self.tr("Combine narrowband to RGB with optional OSC stars"))
952
1148
  self.act_nbtorgb.setIconVisibleInMenu(True)
@@ -994,6 +1190,11 @@ class ToolbarMixin:
994
1190
  self.act_live_stacking.setStatusTip(self.tr("Live monitor and stack incoming frames"))
995
1191
  self.act_live_stacking.triggered.connect(self._open_live_stacking)
996
1192
 
1193
+ self.act_planetary_stacker = QAction(QIcon(planetarystacker_path), self.tr("Planetary Stacker..."), self)
1194
+ self.act_planetary_stacker.setIconVisibleInMenu(True)
1195
+ self.act_planetary_stacker.setStatusTip(self.tr("Stack SER videos (planetary/solar/lunar)"))
1196
+ self.act_planetary_stacker.triggered.connect(self._open_planetary_stacker)
1197
+
997
1198
  self.act_plate_solve = QAction(QIcon(platesolve_path), self.tr("Plate Solver..."), self)
998
1199
  self.act_plate_solve.setIconVisibleInMenu(True)
999
1200
  self.act_plate_solve.setStatusTip(self.tr("Solve WCS/SIP for the active image or a file"))
@@ -1105,6 +1306,13 @@ class ToolbarMixin:
1105
1306
  self.act_copy_astrometry = QAction(self.tr("Copy Astrometric Solution..."), self)
1106
1307
  self.act_copy_astrometry.triggered.connect(self._open_copy_astrometry)
1107
1308
 
1309
+ self.act_acv_exporter = QAction(self.tr("Astro Catalogue Viewer Exporter..."), self)
1310
+ self.act_acv_exporter.setIconVisibleInMenu(True)
1311
+ self.act_acv_exporter.setStatusTip(self.tr("Export current view into Astro Catalogue Viewer folders"))
1312
+ self.act_acv_exporter.triggered.connect(self._open_acv_exporter)
1313
+
1314
+
1315
+
1108
1316
  # Create Mask
1109
1317
  self.act_create_mask = QAction(QIcon(maskcreate_path), self.tr("Create Mask..."), self)
1110
1318
  self.act_create_mask.setIconVisibleInMenu(True)
@@ -1183,6 +1391,7 @@ class ToolbarMixin:
1183
1391
  reg("nbtorgb", self.act_nbtorgb)
1184
1392
  reg("freqsep", self.act_freqsep)
1185
1393
  reg("selective_color", self.act_selective_color)
1394
+ reg("narrowband_normalization", self.act_narrowband_normalization)
1186
1395
  reg("contsub", self.act_contsub)
1187
1396
  reg("abe", self.act_abe)
1188
1397
  reg("create_mask", self.act_create_mask)
@@ -1191,6 +1400,7 @@ class ToolbarMixin:
1191
1400
  reg("add_stars", self.act_add_stars)
1192
1401
  reg("pedestal", self.act_pedestal)
1193
1402
  reg("remove_green", self.act_remove_green)
1403
+ reg("texture_clarity", self.act_texture_clarity)
1194
1404
  reg("background_neutral", self.act_background_neutral)
1195
1405
  reg("white_balance", self.act_white_balance)
1196
1406
  reg("sfcc", self.act_sfcc)
@@ -1207,7 +1417,7 @@ class ToolbarMixin:
1207
1417
  reg("pixel_math", self.act_pixelmath)
1208
1418
  reg("signature_insert", self.act_signature)
1209
1419
  reg("halo_b_gon", self.act_halobgon)
1210
-
1420
+ reg("planetary_stacker", self.act_planetary_stacker)
1211
1421
  reg("multiscale_decomp", self.act_multiscale_decomp)
1212
1422
  reg("geom_invert", self.act_geom_invert)
1213
1423
  reg("geom_flip_horizontal", self.act_geom_flip_h)
@@ -1245,6 +1455,83 @@ class ToolbarMixin:
1245
1455
  reg("view_bundles", self.act_view_bundles)
1246
1456
  reg("function_bundles", self.act_function_bundles)
1247
1457
 
1458
+ def _reset_all_toolbars_to_factory(self):
1459
+ """
1460
+ Clears all persisted toolbar membership/order/hidden state so the UI
1461
+ returns to the factory layout defined in code.
1462
+ """
1463
+ if not hasattr(self, "settings"):
1464
+ return
1465
+
1466
+ # 1) Clear global toolbar persistence
1467
+ keys_to_clear = [
1468
+ "Toolbar/Assignments",
1469
+ "Toolbar/HiddenPrev",
1470
+ "Toolbar/HiddenMigrationDone",
1471
+
1472
+ # Per-toolbar order lists
1473
+ "Toolbar/View",
1474
+ "Toolbar/Functions",
1475
+ "Toolbar/Cosmic",
1476
+ "Toolbar/Tools",
1477
+ "Toolbar/Geometry",
1478
+ "Toolbar/StarStuff",
1479
+ "Toolbar/Masks",
1480
+ "Toolbar/WhatsInMy",
1481
+ "Toolbar/Bundles",
1482
+ "Toolbar/Hidden",
1483
+ ]
1484
+
1485
+ for k in keys_to_clear:
1486
+ try:
1487
+ self.settings.remove(k)
1488
+ except Exception:
1489
+ pass
1490
+
1491
+ # 2) Clear legacy hidden lists (old system) if they exist
1492
+ # (These are the ones like "Toolbar/Tools/Hidden")
1493
+ try:
1494
+ # brute-force remove known ones
1495
+ legacy_hidden = [
1496
+ "Toolbar/View/Hidden",
1497
+ "Toolbar/Functions/Hidden",
1498
+ "Toolbar/Cosmic/Hidden",
1499
+ "Toolbar/Tools/Hidden",
1500
+ "Toolbar/Geometry/Hidden",
1501
+ "Toolbar/StarStuff/Hidden",
1502
+ "Toolbar/Masks/Hidden",
1503
+ "Toolbar/WhatsInMy/Hidden",
1504
+ "Toolbar/Bundles/Hidden",
1505
+ ]
1506
+ for k in legacy_hidden:
1507
+ self.settings.remove(k)
1508
+ except Exception:
1509
+ pass
1510
+
1511
+ try:
1512
+ self.settings.sync()
1513
+ except Exception:
1514
+ pass
1515
+
1516
+ # 3) Rebuild toolbars from code defaults
1517
+ # Safest approach: remove existing toolbars and rebuild.
1518
+ from setiastro.saspro.shortcuts import DraggableToolBar
1519
+ for tb in self.findChildren(DraggableToolBar):
1520
+ try:
1521
+ self.removeToolBar(tb)
1522
+ tb.deleteLater()
1523
+ except Exception:
1524
+ pass
1525
+
1526
+ # Build fresh
1527
+ self._init_toolbar()
1528
+
1529
+ # Optional: ensure Hidden stays hidden
1530
+ tb_hidden = self._hidden_toolbar()
1531
+ if tb_hidden:
1532
+ tb_hidden.setVisible(False)
1533
+
1534
+
1248
1535
  def _restore_toolbar_order(self, tb, settings_key: str):
1249
1536
  """
1250
1537
  Restore toolbar action order from QSettings, using command_id/objectName.
@@ -1356,6 +1643,188 @@ class ToolbarMixin:
1356
1643
  if key:
1357
1644
  self._restore_toolbar_order(tb, str(key))
1358
1645
 
1646
+ def _hidden_toolbar(self):
1647
+ from setiastro.saspro.shortcuts import DraggableToolBar
1648
+ for tb in self.findChildren(DraggableToolBar):
1649
+ if getattr(tb, "_settings_key", None) == "Toolbar/Hidden" or tb.objectName() == "Hidden":
1650
+ return tb
1651
+ return None
1652
+
1653
+ def _toolbar_by_settings_key(self, key: str):
1654
+ from setiastro.saspro.shortcuts import DraggableToolBar
1655
+ for tb in self.findChildren(DraggableToolBar):
1656
+ if getattr(tb, "_settings_key", None) == key:
1657
+ return tb
1658
+ return None
1659
+
1660
+ def _action_cid(self, act):
1661
+ return str(act.property("command_id") or act.objectName() or "")
1662
+
1663
+ def _hide_action_to_hidden_toolbar(self, act):
1664
+ """
1665
+ Move action to the Hidden toolbar, remembering where it came from.
1666
+ """
1667
+ tb_hidden = self._hidden_toolbar()
1668
+ if not tb_hidden:
1669
+ return
1670
+
1671
+ cid = self._action_cid(act)
1672
+ if not cid:
1673
+ return
1674
+
1675
+ # Find current toolbar (if any) and remember it
1676
+ cur_tb = self._toolbar_containing_action(act)
1677
+ if cur_tb and getattr(cur_tb, "_settings_key", None) and cur_tb is not tb_hidden:
1678
+ prev_key = str(cur_tb._settings_key)
1679
+
1680
+ # Persist "previous toolbar" so unhide can restore
1681
+ try:
1682
+ raw = self.settings.value("Toolbar/HiddenPrev", "", type=str) or ""
1683
+ except Exception:
1684
+ raw = ""
1685
+ try:
1686
+ prev = json.loads(raw) if raw else {}
1687
+ except Exception:
1688
+ prev = {}
1689
+ prev[cid] = prev_key
1690
+ self.settings.setValue("Toolbar/HiddenPrev", json.dumps(prev))
1691
+
1692
+ # Remove from all toolbars, add to hidden
1693
+ from setiastro.saspro.shortcuts import DraggableToolBar
1694
+ for t in self.findChildren(DraggableToolBar):
1695
+ if act in t.actions():
1696
+ t.removeAction(act)
1697
+
1698
+ tb_hidden.addAction(act)
1699
+
1700
+ # Update assignment mapping so restore works on next launch
1701
+ tb_hidden._update_assignment_for_action(act)
1702
+
1703
+ # Persist ordering (optional but nice)
1704
+ try:
1705
+ tb_hidden._persist_order()
1706
+ except Exception:
1707
+ pass
1708
+ if cur_tb:
1709
+ try:
1710
+ cur_tb._persist_order()
1711
+ except Exception:
1712
+ pass
1713
+
1714
+ def _unhide_action_from_hidden_toolbar(self, act):
1715
+ """
1716
+ Move action out of Hidden toolbar back to its remembered toolbar.
1717
+ Falls back to Toolbar/View if missing.
1718
+ """
1719
+ tb_hidden = self._hidden_toolbar()
1720
+ if not tb_hidden:
1721
+ return
1722
+
1723
+ cid = self._action_cid(act)
1724
+ if not cid:
1725
+ return
1726
+
1727
+ # Load prev mapping
1728
+ try:
1729
+ raw = self.settings.value("Toolbar/HiddenPrev", "", type=str) or ""
1730
+ except Exception:
1731
+ raw = ""
1732
+ try:
1733
+ prev = json.loads(raw) if raw else {}
1734
+ except Exception:
1735
+ prev = {}
1736
+
1737
+ target_key = prev.get(cid) or "Toolbar/View"
1738
+ tb_target = self._toolbar_by_settings_key(target_key) or self._toolbar_by_settings_key("Toolbar/View")
1739
+ if not tb_target:
1740
+ return
1741
+
1742
+ tb_hidden.removeAction(act)
1743
+ tb_target.addAction(act)
1744
+
1745
+ # Update assignment mapping
1746
+ tb_target._update_assignment_for_action(act)
1747
+
1748
+ # Cleanup prev mapping (optional)
1749
+ if cid in prev:
1750
+ prev.pop(cid, None)
1751
+ self.settings.setValue("Toolbar/HiddenPrev", json.dumps(prev))
1752
+
1753
+ try:
1754
+ tb_target._persist_order()
1755
+ tb_hidden._persist_order()
1756
+ except Exception:
1757
+ pass
1758
+
1759
+ def _migrate_old_hidden_sets_to_hidden_toolbar(self):
1760
+ """
1761
+ One-time migration:
1762
+ Old system: per-toolbar QSettings lists at "<ToolbarKey>/Hidden" storing action IDs
1763
+ New system: move those actions into the dedicated Hidden toolbar (Toolbar/Hidden)
1764
+ and persist membership via Toolbar/Assignments.
1765
+ """
1766
+ if not hasattr(self, "settings"):
1767
+ return
1768
+
1769
+ # Run once
1770
+ if self.settings.value("Toolbar/HiddenMigrationDone", False, type=bool):
1771
+ return
1772
+
1773
+ tb_hidden = self._hidden_toolbar()
1774
+ if not tb_hidden:
1775
+ return
1776
+
1777
+ from setiastro.saspro.shortcuts import DraggableToolBar
1778
+
1779
+ # If Hidden toolbar already has actions, assume user is already on new system
1780
+ # but we still can migrate any remaining old keys.
1781
+ # (We won't early-return.)
1782
+
1783
+ for tb in self.findChildren(DraggableToolBar):
1784
+ k = getattr(tb, "_settings_key", None)
1785
+ if not k or str(k) == "Toolbar/Hidden":
1786
+ continue
1787
+
1788
+ # Old storage key
1789
+ old_key = f"{k}/Hidden"
1790
+
1791
+ # Read old hidden list (Qt backend may return list OR str)
1792
+ try:
1793
+ raw = self.settings.value(old_key, [], type=list)
1794
+ except Exception:
1795
+ raw = self.settings.value(old_key, []) # fallback
1796
+
1797
+ if not raw:
1798
+ continue
1799
+
1800
+ if isinstance(raw, str):
1801
+ raw_list = [raw]
1802
+ else:
1803
+ try:
1804
+ raw_list = list(raw)
1805
+ except Exception:
1806
+ raw_list = []
1807
+
1808
+ hidden_ids = {str(x) for x in raw_list if str(x).strip()}
1809
+ if not hidden_ids:
1810
+ # Clear junk so we don’t keep trying
1811
+ self.settings.setValue(old_key, [])
1812
+ continue
1813
+
1814
+ # Move matching actions into Hidden toolbar
1815
+ for act in list(tb.actions()):
1816
+ if act is None or act.isSeparator():
1817
+ continue
1818
+ cid = str(act.property("command_id") or act.objectName() or "")
1819
+ if cid and cid in hidden_ids:
1820
+ self._hide_action_to_hidden_toolbar(act)
1821
+
1822
+ # Clear old storage so you don't keep re-migrating
1823
+ self.settings.setValue(old_key, [])
1824
+
1825
+ # Mark migration complete
1826
+ self.settings.setValue("Toolbar/HiddenMigrationDone", True)
1827
+
1359
1828
 
1360
1829
  def update_undo_redo_action_labels(self):
1361
1830
  if not hasattr(self, "act_undo"): # not built yet
@@ -1477,4 +1946,10 @@ class ToolbarMixin:
1477
1946
  if hasattr(self, "act_hide_mask"):
1478
1947
  self.act_hide_mask.setEnabled(has_mask and overlay_on)
1479
1948
 
1480
-
1949
+ def _style_toggle_toolbutton(self, btn: QToolButton):
1950
+ # Make sure the action visually shows "on" state
1951
+ btn.setCheckable(True) # safe even if already
1952
+ btn.setStyleSheet("""
1953
+ QToolButton { color: #dcdcdc; }
1954
+ QToolButton:checked { color: #DAA520; font-weight: 600; }
1955
+ """)