setiastrosuitepro 1.6.1.post1__py3-none-any.whl → 1.6.4__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 (139) hide show
  1. setiastro/images/Background_startup.jpg +0 -0
  2. setiastro/images/rotatearbitrary.png +0 -0
  3. setiastro/qml/ResourceMonitor.qml +126 -0
  4. setiastro/saspro/__main__.py +162 -25
  5. setiastro/saspro/_generated/build_info.py +2 -1
  6. setiastro/saspro/abe.py +62 -11
  7. setiastro/saspro/aberration_ai.py +3 -3
  8. setiastro/saspro/add_stars.py +5 -2
  9. setiastro/saspro/astrobin_exporter.py +3 -0
  10. setiastro/saspro/astrospike_python.py +3 -1
  11. setiastro/saspro/autostretch.py +4 -2
  12. setiastro/saspro/backgroundneutral.py +60 -9
  13. setiastro/saspro/batch_convert.py +3 -0
  14. setiastro/saspro/batch_renamer.py +3 -0
  15. setiastro/saspro/blemish_blaster.py +3 -0
  16. setiastro/saspro/blink_comparator_pro.py +474 -251
  17. setiastro/saspro/cheat_sheet.py +50 -15
  18. setiastro/saspro/clahe.py +27 -1
  19. setiastro/saspro/comet_stacking.py +103 -38
  20. setiastro/saspro/convo.py +3 -0
  21. setiastro/saspro/copyastro.py +3 -0
  22. setiastro/saspro/cosmicclarity.py +70 -45
  23. setiastro/saspro/crop_dialog_pro.py +28 -1
  24. setiastro/saspro/curve_editor_pro.py +18 -0
  25. setiastro/saspro/debayer.py +3 -0
  26. setiastro/saspro/doc_manager.py +40 -17
  27. setiastro/saspro/fitsmodifier.py +3 -0
  28. setiastro/saspro/frequency_separation.py +8 -2
  29. setiastro/saspro/function_bundle.py +18 -16
  30. setiastro/saspro/generate_translations.py +715 -1
  31. setiastro/saspro/ghs_dialog_pro.py +3 -0
  32. setiastro/saspro/graxpert.py +3 -0
  33. setiastro/saspro/gui/main_window.py +364 -92
  34. setiastro/saspro/gui/mixins/dock_mixin.py +119 -7
  35. setiastro/saspro/gui/mixins/file_mixin.py +7 -0
  36. setiastro/saspro/gui/mixins/geometry_mixin.py +105 -5
  37. setiastro/saspro/gui/mixins/menu_mixin.py +29 -0
  38. setiastro/saspro/gui/mixins/toolbar_mixin.py +33 -10
  39. setiastro/saspro/gui/statistics_dialog.py +47 -0
  40. setiastro/saspro/halobgon.py +29 -3
  41. setiastro/saspro/histogram.py +3 -0
  42. setiastro/saspro/history_explorer.py +2 -0
  43. setiastro/saspro/i18n.py +22 -10
  44. setiastro/saspro/image_combine.py +3 -0
  45. setiastro/saspro/image_peeker_pro.py +3 -0
  46. setiastro/saspro/imageops/stretch.py +5 -13
  47. setiastro/saspro/isophote.py +3 -0
  48. setiastro/saspro/legacy/numba_utils.py +64 -47
  49. setiastro/saspro/linear_fit.py +3 -0
  50. setiastro/saspro/live_stacking.py +13 -2
  51. setiastro/saspro/mask_creation.py +3 -0
  52. setiastro/saspro/mfdeconv.py +5 -0
  53. setiastro/saspro/morphology.py +30 -5
  54. setiastro/saspro/multiscale_decomp.py +713 -256
  55. setiastro/saspro/nbtorgb_stars.py +12 -2
  56. setiastro/saspro/numba_utils.py +148 -47
  57. setiastro/saspro/ops/scripts.py +77 -17
  58. setiastro/saspro/ops/settings.py +1 -43
  59. setiastro/saspro/perfect_palette_picker.py +1 -0
  60. setiastro/saspro/pixelmath.py +6 -2
  61. setiastro/saspro/plate_solver.py +1 -0
  62. setiastro/saspro/remove_green.py +18 -1
  63. setiastro/saspro/remove_stars.py +136 -162
  64. setiastro/saspro/remove_stars_preset.py +55 -13
  65. setiastro/saspro/resources.py +36 -10
  66. setiastro/saspro/rgb_combination.py +1 -0
  67. setiastro/saspro/rgbalign.py +4 -4
  68. setiastro/saspro/save_options.py +1 -0
  69. setiastro/saspro/selective_color.py +79 -20
  70. setiastro/saspro/sfcc.py +50 -8
  71. setiastro/saspro/shortcuts.py +94 -21
  72. setiastro/saspro/signature_insert.py +3 -0
  73. setiastro/saspro/stacking_suite.py +924 -446
  74. setiastro/saspro/star_alignment.py +291 -331
  75. setiastro/saspro/star_spikes.py +116 -32
  76. setiastro/saspro/star_stretch.py +38 -1
  77. setiastro/saspro/stat_stretch.py +35 -3
  78. setiastro/saspro/status_log_dock.py +1 -1
  79. setiastro/saspro/subwindow.py +63 -2
  80. setiastro/saspro/supernovaasteroidhunter.py +3 -0
  81. setiastro/saspro/swap_manager.py +77 -42
  82. setiastro/saspro/translations/all_source_strings.json +4726 -0
  83. setiastro/saspro/translations/ar_translations.py +4096 -0
  84. setiastro/saspro/translations/de_translations.py +441 -446
  85. setiastro/saspro/translations/es_translations.py +278 -32
  86. setiastro/saspro/translations/fr_translations.py +280 -32
  87. setiastro/saspro/translations/hi_translations.py +3803 -0
  88. setiastro/saspro/translations/integrate_translations.py +38 -1
  89. setiastro/saspro/translations/it_translations.py +1211 -145
  90. setiastro/saspro/translations/ja_translations.py +556 -307
  91. setiastro/saspro/translations/pt_translations.py +3316 -3322
  92. setiastro/saspro/translations/ru_translations.py +3082 -0
  93. setiastro/saspro/translations/saspro_ar.qm +0 -0
  94. setiastro/saspro/translations/saspro_ar.ts +16019 -0
  95. setiastro/saspro/translations/saspro_de.qm +0 -0
  96. setiastro/saspro/translations/saspro_de.ts +14428 -133
  97. setiastro/saspro/translations/saspro_es.qm +0 -0
  98. setiastro/saspro/translations/saspro_es.ts +11503 -7821
  99. setiastro/saspro/translations/saspro_fr.qm +0 -0
  100. setiastro/saspro/translations/saspro_fr.ts +11168 -7812
  101. setiastro/saspro/translations/saspro_hi.qm +0 -0
  102. setiastro/saspro/translations/saspro_hi.ts +14855 -0
  103. setiastro/saspro/translations/saspro_it.qm +0 -0
  104. setiastro/saspro/translations/saspro_it.ts +14347 -7821
  105. setiastro/saspro/translations/saspro_ja.qm +0 -0
  106. setiastro/saspro/translations/saspro_ja.ts +14860 -137
  107. setiastro/saspro/translations/saspro_pt.qm +0 -0
  108. setiastro/saspro/translations/saspro_pt.ts +14904 -137
  109. setiastro/saspro/translations/saspro_ru.qm +0 -0
  110. setiastro/saspro/translations/saspro_ru.ts +11835 -0
  111. setiastro/saspro/translations/saspro_sw.qm +0 -0
  112. setiastro/saspro/translations/saspro_sw.ts +15237 -0
  113. setiastro/saspro/translations/saspro_uk.qm +0 -0
  114. setiastro/saspro/translations/saspro_uk.ts +15248 -0
  115. setiastro/saspro/translations/saspro_zh.qm +0 -0
  116. setiastro/saspro/translations/saspro_zh.ts +10581 -7812
  117. setiastro/saspro/translations/sw_translations.py +3897 -0
  118. setiastro/saspro/translations/uk_translations.py +3929 -0
  119. setiastro/saspro/translations/zh_translations.py +283 -32
  120. setiastro/saspro/versioning.py +36 -5
  121. setiastro/saspro/view_bundle.py +20 -17
  122. setiastro/saspro/wavescale_hdr.py +22 -1
  123. setiastro/saspro/wavescalede.py +23 -1
  124. setiastro/saspro/whitebalance.py +39 -3
  125. setiastro/saspro/widgets/minigame/game.js +991 -0
  126. setiastro/saspro/widgets/minigame/index.html +53 -0
  127. setiastro/saspro/widgets/minigame/style.css +241 -0
  128. setiastro/saspro/widgets/resource_monitor.py +263 -0
  129. setiastro/saspro/widgets/spinboxes.py +18 -0
  130. setiastro/saspro/widgets/wavelet_utils.py +52 -20
  131. setiastro/saspro/wimi.py +100 -80
  132. setiastro/saspro/wims.py +33 -33
  133. setiastro/saspro/window_shelf.py +2 -2
  134. {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/METADATA +15 -4
  135. {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/RECORD +139 -115
  136. {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/WHEEL +0 -0
  137. {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/entry_points.txt +0 -0
  138. {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/licenses/LICENSE +0 -0
  139. {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/licenses/license.txt +0 -0
@@ -128,7 +128,8 @@ from PyQt6.QtGui import (QPixmap, QColor, QIcon, QKeySequence, QShortcut,
128
128
  )
129
129
 
130
130
  # ----- QtCore -----
131
- from PyQt6.QtCore import (Qt, pyqtSignal, QCoreApplication, QTimer, QSize, QSignalBlocker, QModelIndex, QThread, QUrl, QSettings, QEvent, QByteArray, QObject
131
+ from PyQt6.QtCore import (Qt, pyqtSignal, QCoreApplication, QTimer, QSize, QSignalBlocker, QModelIndex, QThread, QUrl, QSettings, QEvent, QByteArray, QObject,
132
+ QPropertyAnimation, QEasingCurve, QElapsedTimer
132
133
  )
133
134
 
134
135
  from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
@@ -186,7 +187,7 @@ from setiastro.saspro.resources import (
186
187
  platesolve_path, psf_path, supernova_path, starregistration_path,
187
188
  stacking_path, pedestal_icon_path, starspike_path, aperture_path,
188
189
  jwstpupil_path, signature_icon_path, livestacking_path, hrdiagram_path,
189
- convoicon_path, spcc_icon_path, sasp_data_path, exoicon_path, peeker_icon,
190
+ convoicon_path, spcc_icon_path, sasp_data_path, exoicon_path, peeker_icon,rotatearbitrary_path,
190
191
  dse_icon_path, astrobin_filters_csv_path, isophote_path, statstretch_path,
191
192
  starstretch_path, curves_path, disk_path, uhs_path, blink_path, ppp_path,
192
193
  nbtorgb_path, freqsep_path, contsub_path, halo_path, cosmic_path,
@@ -295,11 +296,21 @@ class AstroSuiteProMainWindow(
295
296
  def __init__(self, image_manager=None, parent=None,
296
297
  version: str = "dev", build_timestamp: str = "dev"):
297
298
  super().__init__(parent)
299
+ # Prevent white flash: start strictly transparent and force dark bg
300
+ self.setWindowOpacity(0.0)
301
+ self.setStyleSheet("QMainWindow { background-color: #0F0F19; }")
302
+
303
+ # --- Usage Stats ---
304
+ self._session_start_time = time.time()
305
+ self._stats_timer = QTimer(self)
306
+ self._stats_timer.timeout.connect(self._update_usage_stats)
307
+ self._stats_timer.start(60000) # Update every minute
308
+
298
309
  from setiastro.saspro.doc_manager import DocManager
299
310
  from setiastro.saspro.window_shelf import WindowShelf, MinimizeInterceptor
300
311
  from setiastro.saspro.imageops.mdi_snap import MdiSnapController
301
312
  from setiastro.saspro.ops.scripts import ScriptManager
302
- self._version = "1.6.1"
313
+ self._version = version
303
314
  self._build_timestamp = build_timestamp
304
315
  self.setWindowTitle(f"Seti Astro Suite Pro v{self._version}")
305
316
  self.resize(1400, 900)
@@ -425,6 +436,7 @@ class AstroSuiteProMainWindow(
425
436
  self._init_console_dock()
426
437
  self._init_header_viewer_dock()
427
438
  self._init_layers_dock()
439
+ self._init_resource_monitor_overlay()
428
440
  self._shutting_down = False
429
441
  self._init_status_log_dock()
430
442
  self._init_log_dock()
@@ -455,26 +467,23 @@ class AstroSuiteProMainWindow(
455
467
  self.mdi.linkViewDropped.connect(self._on_linkview_drop)
456
468
 
457
469
  self.doc_manager.set_mdi_area(self.mdi)
458
-
470
+ # Coalesce undo/redo label refreshes
471
+ self._undo_redo_refresh_pending = False
472
+ self._undo_redo_refresh_timer = QTimer(self)
473
+ self._undo_redo_refresh_timer.setSingleShot(True)
474
+ self._undo_redo_refresh_timer.timeout.connect(self._do_undo_redo_label_refresh)
459
475
  # Keep the toolbar in sync whenever anything relevant changes
460
- self.doc_manager.documentAdded.connect(lambda *_: self.update_undo_redo_action_labels())
461
- self.doc_manager.documentRemoved.connect(lambda *_: self.update_undo_redo_action_labels())
462
- self.doc_manager.imageRegionUpdated.connect(lambda *_: self.update_undo_redo_action_labels())
463
- self.doc_manager.previewRepaintRequested.connect(lambda *_: self.update_undo_redo_action_labels())
464
-
465
- # Also refresh when the active subwindow changes
466
- try:
467
- self.mdi.subWindowActivated.connect(lambda *_: self.update_undo_redo_action_labels())
468
- except Exception:
469
- pass
470
-
471
- try:
472
- QApplication.instance().focusChanged.connect(
473
- lambda *_: QTimer.singleShot(0, self.update_undo_redo_action_labels)
474
- )
475
- except Exception:
476
- pass
477
-
476
+ self.doc_manager.documentAdded.connect(lambda *_: self._schedule_undo_redo_label_refresh())
477
+ self.doc_manager.documentRemoved.connect(lambda *_: self._schedule_undo_redo_label_refresh())
478
+ self.doc_manager.imageRegionUpdated.connect(lambda *_: self._schedule_undo_redo_label_refresh())
479
+ self.doc_manager.previewRepaintRequested.connect(lambda *_: self._schedule_undo_redo_label_refresh())
480
+ self.mdi.subWindowActivated.connect(lambda *_: self._schedule_undo_redo_label_refresh())
481
+
482
+ # optional: keep, but schedule (or remove entirely)
483
+ #try:
484
+ # QApplication.instance().focusChanged.connect(lambda *_: self._schedule_undo_redo_label_refresh())
485
+ #except Exception:
486
+ # pass
478
487
  self.shortcuts.load_shortcuts()
479
488
  self._ensure_persistent_names()
480
489
  self._restore_window_placement()
@@ -558,6 +567,22 @@ class AstroSuiteProMainWindow(
558
567
 
559
568
  # _init_log_dock, _hook_stdout_stderr, and _append_log_text are now in DockMixin
560
569
 
570
+ def _schedule_undo_redo_label_refresh(self):
571
+ # Coalesce many triggers into one UI update
572
+ if getattr(self, "_undo_redo_refresh_pending", False):
573
+ return
574
+ self._undo_redo_refresh_pending = True
575
+ # 0ms is fine *if* it’s a real attribute timer (not a local)
576
+ self._undo_redo_refresh_timer.start(0)
577
+
578
+ def _do_undo_redo_label_refresh(self):
579
+ self._undo_redo_refresh_pending = False
580
+ try:
581
+ self.update_undo_redo_action_labels()
582
+ except Exception:
583
+ pass
584
+
585
+
561
586
  def _rebuild_menus_for_language(self):
562
587
  """Rebuild menus after language change to apply new translations."""
563
588
  try:
@@ -568,6 +593,19 @@ class AstroSuiteProMainWindow(
568
593
  except Exception:
569
594
  pass
570
595
 
596
+ def createPopupMenu(self):
597
+ """Override to add System Monitor to the toolbar/dock context menu."""
598
+ # Get the default popup menu from QMainWindow
599
+ menu = super().createPopupMenu()
600
+ if menu is None:
601
+ menu = QMenu(self)
602
+
603
+ # Add System Monitor toggle if available
604
+ if hasattr(self, "act_toggle_monitor") and self.act_toggle_monitor is not None:
605
+ menu.addSeparator()
606
+ menu.addAction(self.act_toggle_monitor)
607
+
608
+ return menu
571
609
 
572
610
  def _on_sw_activated(self, sw):
573
611
  if not sw:
@@ -583,7 +621,7 @@ class AstroSuiteProMainWindow(
583
621
  doc.changed.connect(self.update_undo_redo_action_labels)
584
622
  except Exception:
585
623
  pass
586
- self.update_undo_redo_action_labels()
624
+ self._schedule_undo_redo_label_refresh()
587
625
 
588
626
  def _promote_roi_preview_to_real_doc(self, st: dict, preview_doc) -> None:
589
627
  """
@@ -1181,11 +1219,11 @@ class AstroSuiteProMainWindow(
1181
1219
  global_pos = lw.viewport().mapToGlobal(pos)
1182
1220
 
1183
1221
  menu = QMenu(lw)
1184
- act_copy_selected = menu.addAction("Copy Selected")
1185
- act_copy_all = menu.addAction("Copy All")
1222
+ act_copy_selected = menu.addAction(self.tr("Copy Selected"))
1223
+ act_copy_all = menu.addAction(self.tr("Copy All"))
1186
1224
  menu.addSeparator()
1187
- act_select_all = menu.addAction("Select All Lines")
1188
- act_clear = menu.addAction("Clear Console")
1225
+ act_select_all = menu.addAction(self.tr("Select All Lines"))
1226
+ act_clear = menu.addAction(self.tr("Clear Console"))
1189
1227
 
1190
1228
  action = menu.exec(global_pos)
1191
1229
  if action is None:
@@ -1779,7 +1817,7 @@ class AstroSuiteProMainWindow(
1779
1817
 
1780
1818
  show_view_bundles(self)
1781
1819
  except Exception as e:
1782
- QMessageBox.warning(self, "View Bundles", f"Open failed:\n{e}")
1820
+ QMessageBox.warning(self, self.tr("View Bundles"), f"Open failed:\n{e}")
1783
1821
 
1784
1822
  def _open_function_bundles(self):
1785
1823
  from setiastro.saspro.function_bundle import show_function_bundles
@@ -1787,7 +1825,7 @@ class AstroSuiteProMainWindow(
1787
1825
 
1788
1826
  show_function_bundles(self)
1789
1827
  except Exception as e:
1790
- QMessageBox.warning(self, "Function Bundles", f"Open failed:\n{e}")
1828
+ QMessageBox.warning(self, self.tr("Function Bundles"), f"Open failed:\n{e}")
1791
1829
 
1792
1830
  def _open_scripts_folder(self):
1793
1831
  if hasattr(self, "scriptman"):
@@ -1823,6 +1861,7 @@ class AstroSuiteProMainWindow(
1823
1861
  actions = self._collect_all_qactions()
1824
1862
  except Exception:
1825
1863
  actions = self.findChildren(QAction)
1864
+
1826
1865
  for act in actions:
1827
1866
  for seq in _seqs_for_action(act):
1828
1867
  rows.append((_qs_to_str(seq), _describe_action(act), _where_for_action(act)))
@@ -1833,6 +1872,12 @@ class AstroSuiteProMainWindow(
1833
1872
  if seq and not seq.isEmpty():
1834
1873
  rows.append((_qs_to_str(seq), _describe_shortcut(sc), _where_for_shortcut(sc)))
1835
1874
 
1875
+ # 3) App-level shortcuts not represented by QAction/QShortcut
1876
+ try:
1877
+ add_extra_shortcuts(rows) # ✅ Ctrl+K, Ctrl+Alt+M, etc.
1878
+ except Exception:
1879
+ pass
1880
+
1836
1881
  # De-duplicate and sort by shortcut text
1837
1882
  rows = _uniq_keep_order(rows)
1838
1883
  rows.sort(key=lambda r: (r[0].lower(), r[1].lower()))
@@ -1842,43 +1887,43 @@ class AstroSuiteProMainWindow(
1842
1887
  # Manual list (extend anytime). Format: (Gesture, Context, Effect)
1843
1888
  rows = [
1844
1889
  # Command search
1845
- ("A", "Display Stretch", "Toggle Display Auto-Stretch"),
1846
- ("Ctrl+I", "Invert", "Invert the Image"),
1847
- ("Ctrl+Shift+P", "Command Search", "Focus the command search bar; Enter runs first match"),
1890
+ ("A", "Display Stretch", self.tr("Toggle Display Auto-Stretch")),
1891
+ ("Ctrl+I", "Invert", self.tr("Invert the Image")),
1892
+ ("Ctrl+Shift+P", "Command Search", self.tr("Focus the command search bar; Enter runs first match")),
1848
1893
 
1849
1894
  # View Icon
1850
- ("Drag view -> Off to Canvas", "View", "Duplicate Image"),
1851
- ("Drag view -> On to Other Image", "View", "Copy Zoom and Pan"),
1852
- ("Shift+Drag -> On to Other Image", "View", "Apply that image to the other as a mask"),
1853
- ("Ctrl+Drag -> On to Other Image", "View", "Copy Astrometric Solution"),
1895
+ ("Drag view -> Off to Canvas", "View", self.tr("Duplicate Image")),
1896
+ ("Drag view -> On to Other Image", "View", self.tr("Copy Zoom and Pan")),
1897
+ ("Shift+Drag -> On to Other Image", "View", self.tr("Apply that image to the other as a mask")),
1898
+ ("Ctrl+Drag -> On to Other Image", "View", self.tr("Copy Astrometric Solution")),
1854
1899
 
1855
1900
  # View zoom
1856
- ("Ctrl+1", "View", "Zoom to 100% (1:1)"),
1857
- ("Ctrl+0", "View", "Fit image to current window"),
1858
- ("Ctrl++", "View", "Zoom In"),
1859
- ("Ctrl+-", "View", "Zoom Out"),
1901
+ ("Ctrl+1", "View", self.tr("Zoom to 100% (1:1)")),
1902
+ ("Ctrl+0", "View", self.tr("Fit image to current window")),
1903
+ ("Ctrl++", "View", self.tr("Zoom In")),
1904
+ ("Ctrl+-", "View", self.tr("Zoom Out")),
1860
1905
 
1861
1906
  # Window switching
1862
- ("Ctrl+PgDown", "MDI", "Switch to previously active view"),
1863
- ("Ctrl+PgUp", "MDI", "Switch to next active view"),
1907
+ ("Ctrl+PgDown", "MDI", self.tr("Switch to previously active view")),
1908
+ ("Ctrl+PgUp", "MDI", self.tr("Switch to next active view")),
1864
1909
 
1865
1910
  # Shortcuts canvas + buttons
1866
- ("Alt+Drag (toolbar button)", "Toolbar", "Create a desktop shortcut for that action"),
1867
- ("Alt+Drag (shortcut button -> view)", "Shortcuts", "Headless apply the shortcut's command/preset to a view"),
1868
- ("Ctrl/Shift+Click", "Shortcuts", "Multi-select shortcut buttons"),
1869
- ("Drag (selection)", "Shortcuts", "Move selected shortcut buttons"),
1870
- ("Delete / Backspace", "Shortcuts", "Delete selected shortcut buttons"),
1871
- ("Ctrl+A", "Shortcuts", "Select all shortcut buttons"),
1872
- ("Double-click empty area", "MDI background", "Open files dialog"),
1911
+ ("Alt+Drag (toolbar button)", "Toolbar", self.tr("Create a desktop shortcut for that action")),
1912
+ ("Alt+Drag (shortcut button -> view)", "Shortcuts", self.tr("Headless apply the shortcut's command/preset to a view")),
1913
+ ("Ctrl/Shift+Click", "Shortcuts", self.tr("Multi-select shortcut buttons")),
1914
+ ("Drag (selection)", "Shortcuts", self.tr("Move selected shortcut buttons")),
1915
+ ("Delete / Backspace", "Shortcuts", self.tr("Delete selected shortcut buttons")),
1916
+ ("Ctrl+A", "Shortcuts", self.tr("Select all shortcut buttons")),
1917
+ ("Double-click empty area", "MDI background", self.tr("Open files dialog")),
1873
1918
 
1874
1919
  # Layers dock
1875
- ("Drag view -> Layers list", "Layers", "Add dragged view as a new layer (on top)"),
1876
- ("Shift+Drag mask -> Layers list", "Layers", "Attach dragged image as mask to the selected layer"),
1920
+ ("Drag view -> Layers list", "Layers", self.tr("Add dragged view as a new layer (on top)")),
1921
+ ("Shift+Drag mask -> Layers list", "Layers", self.tr("Attach dragged image as mask to the selected layer")),
1877
1922
 
1878
1923
  # Crop tool
1879
- ("Click-drag", "Crop Tool", "Draw a crop rectangle"),
1880
- ("Drag corner handles", "Crop Tool", "Resize crop rectangle"),
1881
- ("Shift+Drag on box", "Crop Tool", "Rotate crop rectangle"),
1924
+ ("Click-drag", "Crop Tool", self.tr("Draw a crop rectangle")),
1925
+ ("Drag corner handles", "Crop Tool", self.tr("Resize crop rectangle")),
1926
+ ("Shift+Drag on box", "Crop Tool", self.tr("Rotate crop rectangle")),
1882
1927
  ]
1883
1928
  return rows
1884
1929
 
@@ -1995,7 +2040,7 @@ class AstroSuiteProMainWindow(
1995
2040
  if getattr(self, "doc_manager", None) and self.doc_manager._docs:
1996
2041
  if not self._confirm_discard(
1997
2042
  title=title,
1998
- msg=(
2043
+ msg=self.tr(
1999
2044
  "Loading a project will close current views and replace desktop shortcuts.\n"
2000
2045
  "Continue?"
2001
2046
  ),
@@ -5510,6 +5555,10 @@ class AstroSuiteProMainWindow(
5510
5555
  "rotate_180": "geom_rotate_180",
5511
5556
  "geom_rotate_180": "geom_rotate_180",
5512
5557
 
5558
+ "rotate_any": "geom_rotate_any",
5559
+ "rotate_arbitrary": "geom_rotate_any",
5560
+ "geom_rotate_any": "geom_rotate_any",
5561
+
5513
5562
  "invert": "geom_invert",
5514
5563
  "geom_invert": "geom_invert",
5515
5564
 
@@ -6510,6 +6559,17 @@ class AstroSuiteProMainWindow(
6510
6559
  QMessageBox.warning(self, "Rotate 180Â deg", str(e))
6511
6560
  return
6512
6561
 
6562
+ if cid == "geom_rotate_any":
6563
+ try:
6564
+ angle = float(preset.get("angle_deg", preset.get("angle", 0.0)))
6565
+ called = _call_any(["_apply_geom_rot_any_to_doc"], doc, angle_deg=angle)
6566
+ if not called:
6567
+ raise RuntimeError("No rotate-any apply method found")
6568
+ self._log(f"Rotate ({angle:g}°) applied to '{target_sw.windowTitle()}'")
6569
+ except Exception as e:
6570
+ QMessageBox.warning(self, "Rotate...", str(e))
6571
+ return
6572
+
6513
6573
  if cid == "geom_rescale":
6514
6574
  try:
6515
6575
  factor = float(preset.get("factor", 1.0))
@@ -7315,7 +7375,7 @@ class AstroSuiteProMainWindow(
7315
7375
  self._search_dock = None
7316
7376
 
7317
7377
  # --- Right-side mini dock with the search box ---
7318
- self._search_dock = QDockWidget("Command Search", self)
7378
+ self._search_dock = QDockWidget(self.tr("Command Search"), self)
7319
7379
  self._search_dock.setObjectName("CommandSearchDock")
7320
7380
  # âœ... Allow moving/closing like other panels
7321
7381
  self._search_dock.setAllowedAreas(
@@ -7530,7 +7590,7 @@ class AstroSuiteProMainWindow(
7530
7590
  except Exception:
7531
7591
  pass
7532
7592
 
7533
- try: self.update_undo_redo_action_labels()
7593
+ try: self._schedule_undo_redo_label_refresh()
7534
7594
  except Exception as e:
7535
7595
  import logging
7536
7596
  logging.debug(f"Exception suppressed: {type(e).__name__}: {e}")
@@ -7790,6 +7850,90 @@ class AstroSuiteProMainWindow(
7790
7850
  flags |= Qt.WindowType.WindowMaximizeButtonHint
7791
7851
  sw.setWindowFlags(flags)
7792
7852
 
7853
+ # -------------------------------------------------------------------------
7854
+ # Explicitly size the window to valid dimensions so it doesn't default
7855
+ # to "maximized" or "full MDI area" if the previous window was large.
7856
+ # We target ~60% of the viewport height, clamped to sane bounds.
7857
+ # -------------------------------------------------------------------------
7858
+ vp = self.mdi.viewport()
7859
+ area = vp.rect() if vp else self.mdi.rect()
7860
+
7861
+ # Determine aspect ratio
7862
+ img_w = img_h = None
7863
+ try:
7864
+ img_w, img_h = self._infer_image_size(view)
7865
+ except Exception:
7866
+ pass
7867
+
7868
+ if not img_w or not img_h:
7869
+ aspect = 1.0
7870
+ else:
7871
+ aspect = float(img_w) / float(img_h)
7872
+
7873
+ # Clamp aspect
7874
+ aspect = max(0.3, min(aspect, 4.0))
7875
+
7876
+ target_h = int(area.height() * 0.6)
7877
+ target_w = int(target_h * aspect)
7878
+
7879
+ # Ensure it fits within the area (with some margin)
7880
+ max_w = int(area.width() * 0.9)
7881
+ max_h = int(area.height() * 0.9)
7882
+
7883
+ if target_w > max_w:
7884
+ target_w = max_w
7885
+ # Recalculate height to preserve aspect, if possible
7886
+ target_h = int(target_w / aspect)
7887
+
7888
+ if target_h > max_h:
7889
+ target_h = max_h
7890
+
7891
+ # Enforce minimums
7892
+ target_w = max(200, target_w)
7893
+ target_h = max(200, target_h)
7894
+
7895
+ sw.resize(target_w, target_h)
7896
+ sw.showNormal() # CRITICAL: clears any "maximized" flag from previous active window
7897
+
7898
+ # -------------------------------------------------------------------------
7899
+ # Smart Cascade: Position relative to the *currently active* window
7900
+ # (before we make the new one active).
7901
+ # -------------------------------------------------------------------------
7902
+ new_x, new_y = 0, 0
7903
+
7904
+ # Get dominant/active window *before* we activate the new one
7905
+ active = self.mdi.activeSubWindow()
7906
+ if active and active.isVisible() and not (active.windowState() & Qt.WindowState.WindowMinimized):
7907
+ # Cascade from the active window
7908
+ geo = active.geometry()
7909
+ new_x = geo.x() + 30
7910
+ new_y = geo.y() + 30
7911
+ else:
7912
+ # Fallback: try to find the "last added" visible window to cascade from
7913
+ # (useful if active is None but windows exist)
7914
+ try:
7915
+ subs = [s for s in self.mdi.subWindowList() if s.isVisible() and s is not sw]
7916
+ if subs:
7917
+ # simplistic "last created" might be at end of list
7918
+ last = subs[-1]
7919
+ geo = last.geometry()
7920
+ new_x = geo.x() + 30
7921
+ new_y = geo.y() + 30
7922
+ except Exception:
7923
+ pass
7924
+
7925
+ # Bounds check: don't let it drift completely off-screen
7926
+ # (allow valid title bar to be visible at least)
7927
+ if (new_x + target_w > area.width() + 50) or (new_y + 50 > area.height()):
7928
+ new_x = 0
7929
+ new_y = 0
7930
+
7931
+ # Clamp to 0 if negative for some reason
7932
+ new_x = max(0, new_x)
7933
+ new_y = max(0, new_y)
7934
+
7935
+ sw.move(new_x, new_y)
7936
+
7793
7937
  # ❌ removed the "fill MDI viewport" block - we *don't* want full-monitor first window
7794
7938
 
7795
7939
  # Show / activate
@@ -7934,7 +8078,7 @@ class AstroSuiteProMainWindow(
7934
8078
  # If no subwindows remain, clear all "active doc" UI bits, including header
7935
8079
  if not self.mdi.subWindowList():
7936
8080
  self.currentDocumentChanged.emit(None) # drives HeaderViewerDock.set_document(None)
7937
- self.update_undo_redo_action_labels()
8081
+ self._schedule_undo_redo_label_refresh()
7938
8082
  self._hdr_refresh_timer.start(0) # belt-and-suspenders for manual widgets
7939
8083
  # If your dock has its own set_document, call it explicitly too
7940
8084
  hv = getattr(self, "header_viewer", None)
@@ -8276,19 +8420,20 @@ class AstroSuiteProMainWindow(
8276
8420
 
8277
8421
  # Misc UI refreshes (guarded)
8278
8422
  try:
8279
- self.update_undo_redo_action_labels()
8280
- except Exception:
8281
- pass
8282
- try:
8283
- if hasattr(self, "_hdr_refresh_timer") and self._hdr_refresh_timer is not None:
8284
- self._hdr_refresh_timer.start(0)
8423
+ self._schedule_undo_redo_label_refresh()
8285
8424
  except Exception:
8286
8425
  pass
8426
+ #try:
8427
+ # if hasattr(self, "_hdr_refresh_timer") and self._hdr_refresh_timer is not None:
8428
+ # self._hdr_refresh_timer.start(0)
8429
+ #except Exception:
8430
+ # pass
8287
8431
  try:
8288
8432
  self._refresh_mask_action_states()
8289
8433
  except Exception:
8290
8434
  pass
8291
8435
 
8436
+
8292
8437
  def _sync_docman_active(self, doc):
8293
8438
  dm = self.doc_manager
8294
8439
  try:
@@ -8449,6 +8594,40 @@ class AstroSuiteProMainWindow(
8449
8594
  if self._suspend_dock_sync:
8450
8595
  QTimer.singleShot(0, lambda: self.changeEvent(QEvent(QEvent.Type.WindowStateChange)))
8451
8596
 
8597
+ def resizeEvent(self, event):
8598
+ super().resizeEvent(event)
8599
+ # Update floating resource monitor position if it exists (from DockMixin)
8600
+ if hasattr(self, "_update_monitor_position"):
8601
+ self._update_monitor_position()
8602
+
8603
+ def moveEvent(self, event):
8604
+ super().moveEvent(event)
8605
+ # Update floating resource monitor position if it exists (from DockMixin)
8606
+ if hasattr(self, "_update_monitor_position"):
8607
+ self._update_monitor_position()
8608
+
8609
+ def changeEvent(self, event):
8610
+ super().changeEvent(event)
8611
+ # 1. Existing logic for dock sync (re-instated from showEvent logic if needed, but usually changeEvent is enough)
8612
+ # (The snippet viewed previously showed showEvent firing a oneshot to call changeEvent)
8613
+
8614
+ # 2. Resource Monitor Sync
8615
+ if event.type() == QEvent.Type.WindowStateChange:
8616
+ if self.windowState() & Qt.WindowState.WindowMinimized:
8617
+ # App minimized -> hide overlay
8618
+ if hasattr(self, "resource_monitor") and self.resource_monitor:
8619
+ self.resource_monitor.hide()
8620
+ elif not (self.windowState() & Qt.WindowState.WindowMinimized):
8621
+ # Only auto-show if the initial fade-in is done
8622
+ if getattr(self, "_fade_in_complete", False):
8623
+ # App restored -> show overlay if enabled in settings
8624
+ if hasattr(self, "resource_monitor") and self.resource_monitor:
8625
+ if self.settings.value("ui/resource_monitor_visible", True, type=bool):
8626
+ self.resource_monitor.show()
8627
+ # Ensure position is correct upon restore
8628
+ if hasattr(self, "_update_monitor_position"):
8629
+ self._update_monitor_position()
8630
+
8452
8631
  def save_ui_state(self):
8453
8632
  """Save window geometry, state, and shortcuts to settings."""
8454
8633
  self._ensure_persistent_names()
@@ -8478,12 +8657,69 @@ class AstroSuiteProMainWindow(
8478
8657
 
8479
8658
  self.settings.sync()
8480
8659
 
8660
+ def on_fade_in_complete(self):
8661
+ """Called when main window fade-in is finished."""
8662
+ self._fade_in_complete = True
8663
+ # Sync Monitor Visibility
8664
+ if hasattr(self, "resource_monitor") and self.resource_monitor:
8665
+ if not self.isMinimized() and self.settings.value("ui/resource_monitor_visible", True, type=bool):
8666
+ # Delay show to ensure visually pleasing sequence (monitor appears AFTER app)
8667
+ QTimer.singleShot(500, self.resource_monitor.show)
8668
+ # Ensure position
8669
+ QTimer.singleShot(600, self._update_monitor_position)
8670
+
8671
+ def keyPressEvent(self, event):
8672
+ """Handle key press events for secret shortcuts."""
8673
+ if (event.modifiers() == (Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.AltModifier) and
8674
+ event.key() == Qt.Key.Key_M):
8675
+
8676
+ # Secret minigame launcher
8677
+ # __file__ is in .../saspro/gui/main_window.py
8678
+ # We want to go up to .../saspro/
8679
+ base_pkg = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
8680
+ minigame_path = os.path.join(base_pkg, "widgets", "minigame", "index.html")
8681
+
8682
+ if os.path.exists(minigame_path):
8683
+ QDesktopServices.openUrl(QUrl.fromLocalFile(minigame_path))
8684
+ event.accept()
8685
+ return
8686
+
8687
+ super().keyPressEvent(event)
8688
+
8689
+ def _update_usage_stats(self):
8690
+ try:
8691
+ now = time.time()
8692
+ elapsed = now - self._session_start_time
8693
+ self._session_start_time = now # Reset session start to avoid double counting
8694
+
8695
+ total = self.settings.value("stats/total_time_seconds", 0.0, type=float)
8696
+ self.settings.setValue("stats/total_time_seconds", total + elapsed)
8697
+ except Exception:
8698
+ pass
8699
+
8700
+ def _on_tool_triggered(self):
8701
+ """Slot to track tool usage count."""
8702
+ try:
8703
+ count = self.settings.value("stats/opened_tools_count", 0, type=int)
8704
+ self.settings.setValue("stats/opened_tools_count", count + 1)
8705
+ except Exception:
8706
+ pass
8707
+
8481
8708
  def closeEvent(self, e):
8709
+ self._update_usage_stats()
8710
+
8711
+
8482
8712
  # Optimization: If restarting (e.g. language change), bypass confirmation and close immediately
8483
8713
  if getattr(self, "_is_restarting", False):
8484
8714
  e.accept()
8485
8715
  return
8486
-
8716
+
8717
+ # Check if we have already faded out
8718
+ if getattr(self, "_fade_out_complete", False):
8719
+ # Proceed with shutdown
8720
+ self._do_shutdown_steps(e)
8721
+ return
8722
+
8487
8723
  try:
8488
8724
  if hasattr(self, "_orig_stdout") and self._orig_stdout is not None:
8489
8725
  sys.stdout = self._orig_stdout
@@ -8491,6 +8727,8 @@ class AstroSuiteProMainWindow(
8491
8727
  sys.stderr = self._orig_stderr
8492
8728
  except Exception:
8493
8729
  pass
8730
+
8731
+ # --- Confirmation Logic ---
8494
8732
  self._shutting_down = True
8495
8733
  # Gather open docs
8496
8734
  docs = []
@@ -8501,35 +8739,68 @@ class AstroSuiteProMainWindow(
8501
8739
  docs.append(d)
8502
8740
 
8503
8741
  edited = [d for d in docs if self._document_has_edits(d)]
8504
- msg = self.tr("Exit Seti Astro Suite Pro?")
8505
- detail = []
8506
- if docs:
8507
- detail.append(self.tr("Open images:") + f" {len(docs)}")
8508
- if edited:
8509
- detail.append(self.tr("Edited since open:") + f" {len(edited)}")
8510
- if detail:
8511
- msg += "\n\n" + "\n".join(detail)
8512
-
8513
- # --- stay-on-top message box ---
8514
- mbox = QMessageBox(self)
8515
- mbox.setIcon(QMessageBox.Icon.Question)
8516
- mbox.setWindowTitle(self.tr("Confirm Exit"))
8517
- mbox.setText(msg)
8518
- mbox.setStandardButtons(
8519
- QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
8520
- )
8521
- mbox.setDefaultButton(QMessageBox.StandardButton.No)
8522
- # ðŸ'‡ key line
8523
- mbox.setWindowFlag(Qt.WindowType.WindowStaysOnTopHint, True)
8524
- mbox.raise_()
8525
- mbox.activateWindow()
8526
- btn = mbox.exec()
8742
+ # If user has disabled exit confirmation (optional setting, but default is confirm)
8743
+ confirm = True
8744
+
8745
+ if confirm:
8746
+ msg = self.tr("Exit Seti Astro Suite Pro?")
8747
+ detail = []
8748
+ if docs:
8749
+ detail.append(self.tr("Open images:") + f" {len(docs)}")
8750
+ if edited:
8751
+ detail.append(self.tr("Edited since open:") + f" {len(edited)}")
8752
+ if detail:
8753
+ msg += "\n\n" + "\n".join(detail)
8754
+
8755
+ # --- stay-on-top message box ---
8756
+ mbox = QMessageBox(self)
8757
+ mbox.setIcon(QMessageBox.Icon.Question)
8758
+ mbox.setWindowTitle(self.tr("Confirm Exit"))
8759
+ mbox.setText(msg)
8760
+ mbox.setStandardButtons(
8761
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
8762
+ )
8763
+ mbox.setDefaultButton(QMessageBox.StandardButton.No)
8764
+ mbox.setWindowFlag(Qt.WindowType.WindowStaysOnTopHint, True)
8765
+ mbox.raise_()
8766
+ mbox.activateWindow()
8767
+ btn = mbox.exec()
8768
+
8769
+ if btn != QMessageBox.StandardButton.Yes:
8770
+ e.ignore()
8771
+ self._shutting_down = False
8772
+ return
8527
8773
 
8528
- if btn != QMessageBox.StandardButton.Yes:
8529
- e.ignore()
8530
- return
8774
+ # --- User confirmed (or no confirm needed) ---
8775
+ # Start Fade Out Animation
8776
+ e.ignore() # Defer close until animation completes
8777
+ self.setEnabled(False) # Prevent further interaction
8778
+
8779
+ # Hide monitor immediately when fade starts (user preference)
8780
+ if hasattr(self, "resource_monitor") and self.resource_monitor:
8781
+ try:
8782
+ if hasattr(self.resource_monitor, "backend"):
8783
+ self.resource_monitor.backend.stop()
8784
+ except Exception:
8785
+ pass
8786
+ self.resource_monitor.hide()
8787
+ self.resource_monitor.close()
8788
+
8789
+ self._anim_close = QPropertyAnimation(self, b"windowOpacity")
8790
+ self._anim_close.setDuration(800)
8791
+ self._anim_close.setStartValue(1.0)
8792
+ self._anim_close.setEndValue(0.0)
8793
+ self._anim_close.setEasingCurve(QEasingCurve.Type.OutQuad)
8794
+ self._anim_close.finished.connect(self._on_fade_out_finished)
8795
+ self._anim_close.start()
8796
+
8797
+ def _on_fade_out_finished(self):
8798
+ """Called when close animation completes."""
8799
+ self._fade_out_complete = True
8800
+ self.close()
8531
8801
 
8532
- # User confirmed: prevent per-subwindow prompts and proceed
8802
+ def _do_shutdown_steps(self, e):
8803
+ """Actual shutdown logic after verification and animation."""
8533
8804
  self._force_close_all = True
8534
8805
  self._shutting_down = True
8535
8806
 
@@ -8553,6 +8824,7 @@ class AstroSuiteProMainWindow(
8553
8824
  # CheatSheet dialog and helper functions imported from setiastro.saspro.cheat_sheet
8554
8825
  from setiastro.saspro.cheat_sheet import (
8555
8826
  CheatSheetDialog as _CheatSheetDialog,
8827
+ add_extra_shortcuts,
8556
8828
  _qs_to_str,
8557
8829
  _clean_text,
8558
8830
  _uniq_keep_order,