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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. setiastro/images/Background_startup.jpg +0 -0
  2. setiastro/qml/ResourceMonitor.qml +126 -0
  3. setiastro/saspro/__main__.py +159 -23
  4. setiastro/saspro/_generated/build_info.py +2 -1
  5. setiastro/saspro/abe.py +62 -11
  6. setiastro/saspro/aberration_ai.py +3 -3
  7. setiastro/saspro/add_stars.py +5 -2
  8. setiastro/saspro/astrobin_exporter.py +3 -0
  9. setiastro/saspro/astrospike_python.py +3 -1
  10. setiastro/saspro/autostretch.py +4 -2
  11. setiastro/saspro/backgroundneutral.py +52 -10
  12. setiastro/saspro/batch_convert.py +3 -0
  13. setiastro/saspro/batch_renamer.py +3 -0
  14. setiastro/saspro/blemish_blaster.py +3 -0
  15. setiastro/saspro/cheat_sheet.py +50 -15
  16. setiastro/saspro/clahe.py +27 -1
  17. setiastro/saspro/comet_stacking.py +103 -38
  18. setiastro/saspro/convo.py +3 -0
  19. setiastro/saspro/copyastro.py +3 -0
  20. setiastro/saspro/cosmicclarity.py +70 -45
  21. setiastro/saspro/crop_dialog_pro.py +17 -0
  22. setiastro/saspro/curve_editor_pro.py +18 -0
  23. setiastro/saspro/debayer.py +3 -0
  24. setiastro/saspro/doc_manager.py +39 -16
  25. setiastro/saspro/fitsmodifier.py +3 -0
  26. setiastro/saspro/frequency_separation.py +8 -2
  27. setiastro/saspro/function_bundle.py +2 -0
  28. setiastro/saspro/generate_translations.py +715 -1
  29. setiastro/saspro/ghs_dialog_pro.py +3 -0
  30. setiastro/saspro/graxpert.py +3 -0
  31. setiastro/saspro/gui/main_window.py +275 -32
  32. setiastro/saspro/gui/mixins/dock_mixin.py +100 -1
  33. setiastro/saspro/gui/mixins/file_mixin.py +7 -0
  34. setiastro/saspro/gui/mixins/menu_mixin.py +28 -0
  35. setiastro/saspro/gui/statistics_dialog.py +47 -0
  36. setiastro/saspro/halobgon.py +29 -3
  37. setiastro/saspro/histogram.py +3 -0
  38. setiastro/saspro/history_explorer.py +2 -0
  39. setiastro/saspro/i18n.py +22 -10
  40. setiastro/saspro/image_combine.py +3 -0
  41. setiastro/saspro/image_peeker_pro.py +3 -0
  42. setiastro/saspro/imageops/stretch.py +5 -13
  43. setiastro/saspro/isophote.py +3 -0
  44. setiastro/saspro/legacy/numba_utils.py +64 -47
  45. setiastro/saspro/linear_fit.py +3 -0
  46. setiastro/saspro/live_stacking.py +13 -2
  47. setiastro/saspro/mask_creation.py +3 -0
  48. setiastro/saspro/mfdeconv.py +5 -0
  49. setiastro/saspro/morphology.py +30 -5
  50. setiastro/saspro/multiscale_decomp.py +3 -0
  51. setiastro/saspro/nbtorgb_stars.py +12 -2
  52. setiastro/saspro/numba_utils.py +148 -47
  53. setiastro/saspro/ops/scripts.py +77 -17
  54. setiastro/saspro/ops/settings.py +1 -43
  55. setiastro/saspro/perfect_palette_picker.py +1 -0
  56. setiastro/saspro/pixelmath.py +6 -2
  57. setiastro/saspro/plate_solver.py +2 -1
  58. setiastro/saspro/remove_green.py +18 -1
  59. setiastro/saspro/remove_stars.py +136 -162
  60. setiastro/saspro/resources.py +7 -0
  61. setiastro/saspro/rgb_combination.py +1 -0
  62. setiastro/saspro/rgbalign.py +4 -4
  63. setiastro/saspro/save_options.py +1 -0
  64. setiastro/saspro/sfcc.py +50 -8
  65. setiastro/saspro/signature_insert.py +3 -0
  66. setiastro/saspro/stacking_suite.py +630 -341
  67. setiastro/saspro/star_alignment.py +16 -1
  68. setiastro/saspro/star_spikes.py +116 -32
  69. setiastro/saspro/star_stretch.py +38 -1
  70. setiastro/saspro/stat_stretch.py +35 -3
  71. setiastro/saspro/subwindow.py +63 -2
  72. setiastro/saspro/supernovaasteroidhunter.py +3 -0
  73. setiastro/saspro/translations/all_source_strings.json +3654 -0
  74. setiastro/saspro/translations/ar_translations.py +3865 -0
  75. setiastro/saspro/translations/de_translations.py +16 -0
  76. setiastro/saspro/translations/es_translations.py +16 -0
  77. setiastro/saspro/translations/fr_translations.py +16 -0
  78. setiastro/saspro/translations/hi_translations.py +3571 -0
  79. setiastro/saspro/translations/integrate_translations.py +36 -0
  80. setiastro/saspro/translations/it_translations.py +16 -0
  81. setiastro/saspro/translations/ja_translations.py +16 -0
  82. setiastro/saspro/translations/pt_translations.py +16 -0
  83. setiastro/saspro/translations/ru_translations.py +2848 -0
  84. setiastro/saspro/translations/saspro_ar.qm +0 -0
  85. setiastro/saspro/translations/saspro_ar.ts +255 -0
  86. setiastro/saspro/translations/saspro_de.qm +0 -0
  87. setiastro/saspro/translations/saspro_de.ts +3 -3
  88. setiastro/saspro/translations/saspro_es.qm +0 -0
  89. setiastro/saspro/translations/saspro_es.ts +3 -3
  90. setiastro/saspro/translations/saspro_fr.qm +0 -0
  91. setiastro/saspro/translations/saspro_fr.ts +3 -3
  92. setiastro/saspro/translations/saspro_hi.qm +0 -0
  93. setiastro/saspro/translations/saspro_hi.ts +257 -0
  94. setiastro/saspro/translations/saspro_it.qm +0 -0
  95. setiastro/saspro/translations/saspro_it.ts +3 -3
  96. setiastro/saspro/translations/saspro_ja.qm +0 -0
  97. setiastro/saspro/translations/saspro_ja.ts +4 -4
  98. setiastro/saspro/translations/saspro_pt.qm +0 -0
  99. setiastro/saspro/translations/saspro_pt.ts +3 -3
  100. setiastro/saspro/translations/saspro_ru.qm +0 -0
  101. setiastro/saspro/translations/saspro_ru.ts +237 -0
  102. setiastro/saspro/translations/saspro_sw.qm +0 -0
  103. setiastro/saspro/translations/saspro_sw.ts +257 -0
  104. setiastro/saspro/translations/saspro_uk.qm +0 -0
  105. setiastro/saspro/translations/saspro_uk.ts +10771 -0
  106. setiastro/saspro/translations/saspro_zh.qm +0 -0
  107. setiastro/saspro/translations/saspro_zh.ts +3 -3
  108. setiastro/saspro/translations/sw_translations.py +3671 -0
  109. setiastro/saspro/translations/uk_translations.py +3700 -0
  110. setiastro/saspro/translations/zh_translations.py +16 -0
  111. setiastro/saspro/versioning.py +12 -6
  112. setiastro/saspro/view_bundle.py +3 -0
  113. setiastro/saspro/wavescale_hdr.py +22 -1
  114. setiastro/saspro/wavescalede.py +23 -1
  115. setiastro/saspro/whitebalance.py +39 -3
  116. setiastro/saspro/widgets/minigame/game.js +986 -0
  117. setiastro/saspro/widgets/minigame/index.html +53 -0
  118. setiastro/saspro/widgets/minigame/style.css +241 -0
  119. setiastro/saspro/widgets/resource_monitor.py +237 -0
  120. setiastro/saspro/widgets/wavelet_utils.py +52 -20
  121. setiastro/saspro/wimi.py +7996 -0
  122. setiastro/saspro/wims.py +578 -0
  123. {setiastrosuitepro-1.6.1.dist-info → setiastrosuitepro-1.6.2.dist-info}/METADATA +15 -4
  124. {setiastrosuitepro-1.6.1.dist-info → setiastrosuitepro-1.6.2.dist-info}/RECORD +128 -103
  125. {setiastrosuitepro-1.6.1.dist-info → setiastrosuitepro-1.6.2.dist-info}/WHEEL +0 -0
  126. {setiastrosuitepro-1.6.1.dist-info → setiastrosuitepro-1.6.2.dist-info}/entry_points.txt +0 -0
  127. {setiastrosuitepro-1.6.1.dist-info → setiastrosuitepro-1.6.2.dist-info}/licenses/LICENSE +0 -0
  128. {setiastrosuitepro-1.6.1.dist-info → setiastrosuitepro-1.6.2.dist-info}/licenses/license.txt +0 -0
@@ -24,6 +24,9 @@ class GhsDialogPro(QDialog):
24
24
  def __init__(self, parent, document):
25
25
  super().__init__(parent)
26
26
  self.setWindowTitle(self.tr("Hyperbolic Stretch"))
27
+ self.setWindowFlag(Qt.WindowType.Window, True)
28
+ self.setWindowModality(Qt.WindowModality.NonModal)
29
+ self.setModal(False)
27
30
  self.doc = document
28
31
  self._preview_img = None
29
32
  self._full_img = None
@@ -31,6 +31,9 @@ class GraXpertOperationDialog(QDialog):
31
31
  super().__init__(parent)
32
32
 
33
33
  self.setWindowTitle("GraXpert")
34
+ self.setWindowFlag(Qt.WindowType.Window, True)
35
+ self.setWindowModality(Qt.WindowModality.NonModal)
36
+ self.setModal(False)
34
37
  root = QVBoxLayout(self)
35
38
 
36
39
  # radios
@@ -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
132
133
  )
133
134
 
134
135
  from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
@@ -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 = version
313
+ self._version = "1.6.1"
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()
@@ -568,6 +580,19 @@ class AstroSuiteProMainWindow(
568
580
  except Exception:
569
581
  pass
570
582
 
583
+ def createPopupMenu(self):
584
+ """Override to add System Monitor to the toolbar/dock context menu."""
585
+ # Get the default popup menu from QMainWindow
586
+ menu = super().createPopupMenu()
587
+ if menu is None:
588
+ menu = QMenu(self)
589
+
590
+ # Add System Monitor toggle if available
591
+ if hasattr(self, "act_toggle_monitor") and self.act_toggle_monitor is not None:
592
+ menu.addSeparator()
593
+ menu.addAction(self.act_toggle_monitor)
594
+
595
+ return menu
571
596
 
572
597
  def _on_sw_activated(self, sw):
573
598
  if not sw:
@@ -1823,6 +1848,7 @@ class AstroSuiteProMainWindow(
1823
1848
  actions = self._collect_all_qactions()
1824
1849
  except Exception:
1825
1850
  actions = self.findChildren(QAction)
1851
+
1826
1852
  for act in actions:
1827
1853
  for seq in _seqs_for_action(act):
1828
1854
  rows.append((_qs_to_str(seq), _describe_action(act), _where_for_action(act)))
@@ -1833,6 +1859,12 @@ class AstroSuiteProMainWindow(
1833
1859
  if seq and not seq.isEmpty():
1834
1860
  rows.append((_qs_to_str(seq), _describe_shortcut(sc), _where_for_shortcut(sc)))
1835
1861
 
1862
+ # 3) App-level shortcuts not represented by QAction/QShortcut
1863
+ try:
1864
+ add_extra_shortcuts(rows) # ✅ Ctrl+K, Ctrl+Alt+M, etc.
1865
+ except Exception:
1866
+ pass
1867
+
1836
1868
  # De-duplicate and sort by shortcut text
1837
1869
  rows = _uniq_keep_order(rows)
1838
1870
  rows.sort(key=lambda r: (r[0].lower(), r[1].lower()))
@@ -4327,7 +4359,7 @@ class AstroSuiteProMainWindow(
4327
4359
  dlg.show()
4328
4360
 
4329
4361
  def _open_whats_in_my_sky(self):
4330
- from wims import WhatsInMySkyDialog
4362
+ from setiastro.saspro.wims import WhatsInMySkyDialog
4331
4363
  dlg = WhatsInMySkyDialog(
4332
4364
  parent=self,
4333
4365
  wims_path=wims_path, # window icon
@@ -4339,7 +4371,7 @@ class AstroSuiteProMainWindow(
4339
4371
 
4340
4372
  def _open_wimi(self):
4341
4373
  # Lazy import to avoid loading lightkurve at startup (~12s)
4342
- from wimi import WIMIDialog
4374
+ from setiastro.saspro.wimi import WIMIDialog
4343
4375
  dlg = WIMIDialog(
4344
4376
  parent=self,
4345
4377
  settings=getattr(self, "settings", None),
@@ -7790,6 +7822,90 @@ class AstroSuiteProMainWindow(
7790
7822
  flags |= Qt.WindowType.WindowMaximizeButtonHint
7791
7823
  sw.setWindowFlags(flags)
7792
7824
 
7825
+ # -------------------------------------------------------------------------
7826
+ # Explicitly size the window to valid dimensions so it doesn't default
7827
+ # to "maximized" or "full MDI area" if the previous window was large.
7828
+ # We target ~60% of the viewport height, clamped to sane bounds.
7829
+ # -------------------------------------------------------------------------
7830
+ vp = self.mdi.viewport()
7831
+ area = vp.rect() if vp else self.mdi.rect()
7832
+
7833
+ # Determine aspect ratio
7834
+ img_w = img_h = None
7835
+ try:
7836
+ img_w, img_h = self._infer_image_size(view)
7837
+ except Exception:
7838
+ pass
7839
+
7840
+ if not img_w or not img_h:
7841
+ aspect = 1.0
7842
+ else:
7843
+ aspect = float(img_w) / float(img_h)
7844
+
7845
+ # Clamp aspect
7846
+ aspect = max(0.3, min(aspect, 4.0))
7847
+
7848
+ target_h = int(area.height() * 0.6)
7849
+ target_w = int(target_h * aspect)
7850
+
7851
+ # Ensure it fits within the area (with some margin)
7852
+ max_w = int(area.width() * 0.9)
7853
+ max_h = int(area.height() * 0.9)
7854
+
7855
+ if target_w > max_w:
7856
+ target_w = max_w
7857
+ # Recalculate height to preserve aspect, if possible
7858
+ target_h = int(target_w / aspect)
7859
+
7860
+ if target_h > max_h:
7861
+ target_h = max_h
7862
+
7863
+ # Enforce minimums
7864
+ target_w = max(200, target_w)
7865
+ target_h = max(200, target_h)
7866
+
7867
+ sw.resize(target_w, target_h)
7868
+ sw.showNormal() # CRITICAL: clears any "maximized" flag from previous active window
7869
+
7870
+ # -------------------------------------------------------------------------
7871
+ # Smart Cascade: Position relative to the *currently active* window
7872
+ # (before we make the new one active).
7873
+ # -------------------------------------------------------------------------
7874
+ new_x, new_y = 0, 0
7875
+
7876
+ # Get dominant/active window *before* we activate the new one
7877
+ active = self.mdi.activeSubWindow()
7878
+ if active and active.isVisible() and not (active.windowState() & Qt.WindowState.WindowMinimized):
7879
+ # Cascade from the active window
7880
+ geo = active.geometry()
7881
+ new_x = geo.x() + 30
7882
+ new_y = geo.y() + 30
7883
+ else:
7884
+ # Fallback: try to find the "last added" visible window to cascade from
7885
+ # (useful if active is None but windows exist)
7886
+ try:
7887
+ subs = [s for s in self.mdi.subWindowList() if s.isVisible() and s is not sw]
7888
+ if subs:
7889
+ # simplistic "last created" might be at end of list
7890
+ last = subs[-1]
7891
+ geo = last.geometry()
7892
+ new_x = geo.x() + 30
7893
+ new_y = geo.y() + 30
7894
+ except Exception:
7895
+ pass
7896
+
7897
+ # Bounds check: don't let it drift completely off-screen
7898
+ # (allow valid title bar to be visible at least)
7899
+ if (new_x + target_w > area.width() + 50) or (new_y + 50 > area.height()):
7900
+ new_x = 0
7901
+ new_y = 0
7902
+
7903
+ # Clamp to 0 if negative for some reason
7904
+ new_x = max(0, new_x)
7905
+ new_y = max(0, new_y)
7906
+
7907
+ sw.move(new_x, new_y)
7908
+
7793
7909
  # ❌ removed the "fill MDI viewport" block - we *don't* want full-monitor first window
7794
7910
 
7795
7911
  # Show / activate
@@ -8449,6 +8565,40 @@ class AstroSuiteProMainWindow(
8449
8565
  if self._suspend_dock_sync:
8450
8566
  QTimer.singleShot(0, lambda: self.changeEvent(QEvent(QEvent.Type.WindowStateChange)))
8451
8567
 
8568
+ def resizeEvent(self, event):
8569
+ super().resizeEvent(event)
8570
+ # Update floating resource monitor position if it exists (from DockMixin)
8571
+ if hasattr(self, "_update_monitor_position"):
8572
+ self._update_monitor_position()
8573
+
8574
+ def moveEvent(self, event):
8575
+ super().moveEvent(event)
8576
+ # Update floating resource monitor position if it exists (from DockMixin)
8577
+ if hasattr(self, "_update_monitor_position"):
8578
+ self._update_monitor_position()
8579
+
8580
+ def changeEvent(self, event):
8581
+ super().changeEvent(event)
8582
+ # 1. Existing logic for dock sync (re-instated from showEvent logic if needed, but usually changeEvent is enough)
8583
+ # (The snippet viewed previously showed showEvent firing a oneshot to call changeEvent)
8584
+
8585
+ # 2. Resource Monitor Sync
8586
+ if event.type() == QEvent.Type.WindowStateChange:
8587
+ if self.windowState() & Qt.WindowState.WindowMinimized:
8588
+ # App minimized -> hide overlay
8589
+ if hasattr(self, "resource_monitor") and self.resource_monitor:
8590
+ self.resource_monitor.hide()
8591
+ elif not (self.windowState() & Qt.WindowState.WindowMinimized):
8592
+ # Only auto-show if the initial fade-in is done
8593
+ if getattr(self, "_fade_in_complete", False):
8594
+ # App restored -> show overlay if enabled in settings
8595
+ if hasattr(self, "resource_monitor") and self.resource_monitor:
8596
+ if self.settings.value("ui/resource_monitor_visible", True, type=bool):
8597
+ self.resource_monitor.show()
8598
+ # Ensure position is correct upon restore
8599
+ if hasattr(self, "_update_monitor_position"):
8600
+ self._update_monitor_position()
8601
+
8452
8602
  def save_ui_state(self):
8453
8603
  """Save window geometry, state, and shortcuts to settings."""
8454
8604
  self._ensure_persistent_names()
@@ -8478,12 +8628,69 @@ class AstroSuiteProMainWindow(
8478
8628
 
8479
8629
  self.settings.sync()
8480
8630
 
8631
+ def on_fade_in_complete(self):
8632
+ """Called when main window fade-in is finished."""
8633
+ self._fade_in_complete = True
8634
+ # Sync Monitor Visibility
8635
+ if hasattr(self, "resource_monitor") and self.resource_monitor:
8636
+ if not self.isMinimized() and self.settings.value("ui/resource_monitor_visible", True, type=bool):
8637
+ # Delay show to ensure visually pleasing sequence (monitor appears AFTER app)
8638
+ QTimer.singleShot(500, self.resource_monitor.show)
8639
+ # Ensure position
8640
+ QTimer.singleShot(600, self._update_monitor_position)
8641
+
8642
+ def keyPressEvent(self, event):
8643
+ """Handle key press events for secret shortcuts."""
8644
+ if (event.modifiers() == (Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.AltModifier) and
8645
+ event.key() == Qt.Key.Key_M):
8646
+
8647
+ # Secret minigame launcher
8648
+ # __file__ is in .../saspro/gui/main_window.py
8649
+ # We want to go up to .../saspro/
8650
+ base_pkg = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
8651
+ minigame_path = os.path.join(base_pkg, "widgets", "minigame", "index.html")
8652
+
8653
+ if os.path.exists(minigame_path):
8654
+ QDesktopServices.openUrl(QUrl.fromLocalFile(minigame_path))
8655
+ event.accept()
8656
+ return
8657
+
8658
+ super().keyPressEvent(event)
8659
+
8660
+ def _update_usage_stats(self):
8661
+ try:
8662
+ now = time.time()
8663
+ elapsed = now - self._session_start_time
8664
+ self._session_start_time = now # Reset session start to avoid double counting
8665
+
8666
+ total = self.settings.value("stats/total_time_seconds", 0.0, type=float)
8667
+ self.settings.setValue("stats/total_time_seconds", total + elapsed)
8668
+ except Exception:
8669
+ pass
8670
+
8671
+ def _on_tool_triggered(self):
8672
+ """Slot to track tool usage count."""
8673
+ try:
8674
+ count = self.settings.value("stats/opened_tools_count", 0, type=int)
8675
+ self.settings.setValue("stats/opened_tools_count", count + 1)
8676
+ except Exception:
8677
+ pass
8678
+
8481
8679
  def closeEvent(self, e):
8680
+ self._update_usage_stats()
8681
+
8682
+
8482
8683
  # Optimization: If restarting (e.g. language change), bypass confirmation and close immediately
8483
8684
  if getattr(self, "_is_restarting", False):
8484
8685
  e.accept()
8485
8686
  return
8486
-
8687
+
8688
+ # Check if we have already faded out
8689
+ if getattr(self, "_fade_out_complete", False):
8690
+ # Proceed with shutdown
8691
+ self._do_shutdown_steps(e)
8692
+ return
8693
+
8487
8694
  try:
8488
8695
  if hasattr(self, "_orig_stdout") and self._orig_stdout is not None:
8489
8696
  sys.stdout = self._orig_stdout
@@ -8491,6 +8698,8 @@ class AstroSuiteProMainWindow(
8491
8698
  sys.stderr = self._orig_stderr
8492
8699
  except Exception:
8493
8700
  pass
8701
+
8702
+ # --- Confirmation Logic ---
8494
8703
  self._shutting_down = True
8495
8704
  # Gather open docs
8496
8705
  docs = []
@@ -8501,35 +8710,68 @@ class AstroSuiteProMainWindow(
8501
8710
  docs.append(d)
8502
8711
 
8503
8712
  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()
8713
+ # If user has disabled exit confirmation (optional setting, but default is confirm)
8714
+ confirm = True
8715
+
8716
+ if confirm:
8717
+ msg = self.tr("Exit Seti Astro Suite Pro?")
8718
+ detail = []
8719
+ if docs:
8720
+ detail.append(self.tr("Open images:") + f" {len(docs)}")
8721
+ if edited:
8722
+ detail.append(self.tr("Edited since open:") + f" {len(edited)}")
8723
+ if detail:
8724
+ msg += "\n\n" + "\n".join(detail)
8725
+
8726
+ # --- stay-on-top message box ---
8727
+ mbox = QMessageBox(self)
8728
+ mbox.setIcon(QMessageBox.Icon.Question)
8729
+ mbox.setWindowTitle(self.tr("Confirm Exit"))
8730
+ mbox.setText(msg)
8731
+ mbox.setStandardButtons(
8732
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
8733
+ )
8734
+ mbox.setDefaultButton(QMessageBox.StandardButton.No)
8735
+ mbox.setWindowFlag(Qt.WindowType.WindowStaysOnTopHint, True)
8736
+ mbox.raise_()
8737
+ mbox.activateWindow()
8738
+ btn = mbox.exec()
8739
+
8740
+ if btn != QMessageBox.StandardButton.Yes:
8741
+ e.ignore()
8742
+ self._shutting_down = False
8743
+ return
8527
8744
 
8528
- if btn != QMessageBox.StandardButton.Yes:
8529
- e.ignore()
8530
- return
8745
+ # --- User confirmed (or no confirm needed) ---
8746
+ # Start Fade Out Animation
8747
+ e.ignore() # Defer close until animation completes
8748
+ self.setEnabled(False) # Prevent further interaction
8749
+
8750
+ # Hide monitor immediately when fade starts (user preference)
8751
+ if hasattr(self, "resource_monitor") and self.resource_monitor:
8752
+ try:
8753
+ if hasattr(self.resource_monitor, "backend"):
8754
+ self.resource_monitor.backend.stop()
8755
+ except Exception:
8756
+ pass
8757
+ self.resource_monitor.hide()
8758
+ self.resource_monitor.close()
8759
+
8760
+ self._anim_close = QPropertyAnimation(self, b"windowOpacity")
8761
+ self._anim_close.setDuration(800)
8762
+ self._anim_close.setStartValue(1.0)
8763
+ self._anim_close.setEndValue(0.0)
8764
+ self._anim_close.setEasingCurve(QEasingCurve.Type.OutQuad)
8765
+ self._anim_close.finished.connect(self._on_fade_out_finished)
8766
+ self._anim_close.start()
8767
+
8768
+ def _on_fade_out_finished(self):
8769
+ """Called when close animation completes."""
8770
+ self._fade_out_complete = True
8771
+ self.close()
8531
8772
 
8532
- # User confirmed: prevent per-subwindow prompts and proceed
8773
+ def _do_shutdown_steps(self, e):
8774
+ """Actual shutdown logic after verification and animation."""
8533
8775
  self._force_close_all = True
8534
8776
  self._shutting_down = True
8535
8777
 
@@ -8553,6 +8795,7 @@ class AstroSuiteProMainWindow(
8553
8795
  # CheatSheet dialog and helper functions imported from setiastro.saspro.cheat_sheet
8554
8796
  from setiastro.saspro.cheat_sheet import (
8555
8797
  CheatSheetDialog as _CheatSheetDialog,
8798
+ add_extra_shortcuts,
8556
8799
  _qs_to_str,
8557
8800
  _clean_text,
8558
8801
  _uniq_keep_order,
@@ -14,7 +14,7 @@ from PyQt6.QtWidgets import (
14
14
  QVBoxLayout, QWidget, QTextEdit, QListWidget, QListWidgetItem,
15
15
  QAbstractItemView, QApplication
16
16
  )
17
- from PyQt6.QtGui import QTextCursor
17
+ from PyQt6.QtGui import QTextCursor, QAction
18
18
 
19
19
  if TYPE_CHECKING:
20
20
  from PyQt6.QtWidgets import QAction
@@ -194,6 +194,86 @@ class DockMixin:
194
194
  except Exception:
195
195
  pass
196
196
 
197
+ def _init_resource_monitor_overlay(self):
198
+ """Initialize the QML System Resource Monitor as a floating overlay."""
199
+ try:
200
+ from setiastro.saspro.widgets.resource_monitor import SystemMonitorWidget
201
+
202
+ # Create as a child of the central widget or self to sit on top
203
+ # Using self (QMainWindow) allows it to float over everything including status bar if we want,
204
+ # but usually we want it over MDI area. Let's try self first for "floating" feel.
205
+ self.resource_monitor = SystemMonitorWidget(self)
206
+ self.resource_monitor.setObjectName("ResourceMonitorOverlay")
207
+
208
+ # Make it a proper independent window to allow true transparency (translucent background)
209
+ # without black artifacts from parent composition.
210
+ # Fixed: Removed WindowStaysOnTopHint to allow it to be obscured by other apps (Alt-Tab support)
211
+ self.resource_monitor.setWindowFlags(
212
+ Qt.WindowType.Window |
213
+ Qt.WindowType.FramelessWindowHint |
214
+ Qt.WindowType.Tool
215
+ )
216
+
217
+ # Sizing and Transparency
218
+ self.resource_monitor.setFixedSize(200, 60)
219
+ # self.resource_monitor.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True) # Optional: if we want click-through
220
+
221
+
222
+ # Initial placement (will be updated by resizeEvent)
223
+ self._update_monitor_position()
224
+
225
+ # Defer visibility to MainWindow.showEvent to prevent appearing before main window
226
+ # visible = self.settings.value("ui/resource_monitor_visible", True, type=bool)
227
+ # if visible:
228
+ # self.resource_monitor.show()
229
+ # else:
230
+ # self.resource_monitor.hide()
231
+ except Exception as e:
232
+ print(f"WARNING: Could not initialize System Monitor overlay: {e}")
233
+ self.resource_monitor = None
234
+
235
+ def _toggle_resource_monitor(self, checked: bool):
236
+ """Toggle floating monitor visibility."""
237
+ if hasattr(self, 'resource_monitor') and self.resource_monitor:
238
+ if checked:
239
+ self.resource_monitor.show()
240
+ self._update_monitor_position()
241
+ else:
242
+ self.resource_monitor.hide()
243
+ self.settings.setValue("ui/resource_monitor_visible", checked)
244
+
245
+ def _update_monitor_position(self):
246
+ """Snap monitor to bottom-right corner."""
247
+ if hasattr(self, 'resource_monitor') and self.resource_monitor:
248
+ from PyQt6.QtCore import QPoint
249
+ m = 5 # margin
250
+ # Position relative to the main window geometry
251
+ w = self.resource_monitor.width()
252
+ h = self.resource_monitor.height()
253
+
254
+ # Anchor to bottom-right of the window
255
+ x = self.width() - w - m
256
+ y = self.height() - h - m
257
+
258
+ # Map local MainWindow coordinates to Global Screen coordinates
259
+ # This is required because resource_monitor is a Top-Level Window (for transparency)
260
+ global_pos = self.mapToGlobal(QPoint(x, y))
261
+ self.resource_monitor.move(global_pos)
262
+ self.resource_monitor.raise_()
263
+
264
+ # We need to hook resizeEvent to call _update_monitor_position.
265
+ # Since this is a mixin, we can't easily override resizeEvent of the MainWindow without being careful.
266
+ # Best way: install an event filter on self, or since we are a mixin mixed into MainWindow,
267
+ # we can rely on MainWindow calling a specific method or we can patch it...
268
+ # Actually, MainWindow likely has resizeEvent.
269
+ # simpler: QTimer check? No.
270
+ # Correct way for Mixin: The MainWindow class should call something.
271
+ # BUT, I can just installEventFilter(self) ? No, infinite loop risk.
272
+ #
273
+ # Let's use the 'GeometryMixin' or just add a standard method `_on_resize_for_monitor`
274
+ # and assume I can hook it in MainWindow.py.
275
+
276
+
197
277
  # ❌ Remove this old line; it let random mouse-over updates hijack the dock:
198
278
  # self.currentDocumentChanged.disconnect(self.header_viewer.set_document) # if previously connected
199
279
  # (If you prefer to keep the signal for explicit tab switches, it's fine to leave
@@ -217,6 +297,21 @@ class DockMixin:
217
297
  "Window Shelf": 50,
218
298
  "Command Search": 60,
219
299
  }
300
+
301
+ # Add special action for overlay monitor
302
+ mon_act = QAction(self.tr("System Monitor"), self)
303
+ mon_act.setCheckable(True)
304
+ mon_act.setChecked(self.settings.value("ui/resource_monitor_visible", True, type=bool))
305
+ mon_act.triggered.connect(self._toggle_resource_monitor)
306
+
307
+ # We need to insert it into the logic that populates the menu.
308
+ # But 'dock_mixin' automates menu from self.findChildren(QDockWidget).
309
+ # So we have to manually inject this action into the "Panels" menu if possible
310
+ # or expose it such that main_window can add it.
311
+ #
312
+ # Easier: allow main_window to add it, or ...
313
+ # If I can't easily see where menu is built, I'll bind it to self.act_toggle_monitor = mon_act
314
+ self.act_toggle_monitor = mon_act
220
315
 
221
316
  def key_fn(d: QDockWidget):
222
317
  t = d.windowTitle()
@@ -224,6 +319,10 @@ class DockMixin:
224
319
 
225
320
  for dock in sorted(docks, key=key_fn):
226
321
  self._register_dock_in_view_menu(dock)
322
+
323
+ if hasattr(self, "act_toggle_monitor"):
324
+ menu.addSeparator()
325
+ menu.addAction(self.act_toggle_monitor)
227
326
 
228
327
  def _add_doc_to_explorer(self, doc):
229
328
  base = self._normalize_base_doc(doc)
@@ -120,6 +120,13 @@ class FileMixin:
120
120
  doc = self.docman.open_path(p) # this emits documentAdded
121
121
  self._log(f"Opened: {p}")
122
122
  self._add_recent_image(p) # âœ... track in MRU
123
+
124
+ # Increment statistics
125
+ try:
126
+ count = self.settings.value("stats/opened_images_count", 0, type=int)
127
+ self.settings.setValue("stats/opened_images_count", count + 1)
128
+ except Exception:
129
+ pass
123
130
  except Exception as e:
124
131
  QMessageBox.warning(self, self.tr("Open failed"), f"{p}\n\n{e}")
125
132
 
@@ -25,6 +25,28 @@ class MenuMixin:
25
25
  # This method will be implemented as part of the main window
26
26
  # For now, this is a placeholder showing the mixin pattern
27
27
  pass
28
+
29
+ def _show_statistics(self):
30
+ from setiastro.saspro.gui.statistics_dialog import StatisticsDialog
31
+ dlg = StatisticsDialog(self)
32
+ dlg.exec()
33
+
34
+ def _hook_tool_stats(self, menus):
35
+ if not hasattr(self, "_on_tool_triggered"):
36
+ return
37
+
38
+ seen = set()
39
+ for menu in menus:
40
+ for action in self._iter_menu_actions(menu):
41
+ if action in seen: continue
42
+ seen.add(action)
43
+ if action.isSeparator(): continue
44
+
45
+ try:
46
+ action.triggered.connect(self._on_tool_triggered)
47
+ except Exception:
48
+ pass
49
+
28
50
 
29
51
  def _rebuild_recent_menus(self):
30
52
  """Rebuild the recent files and projects menus."""
@@ -298,6 +320,12 @@ class MenuMixin:
298
320
  m_about.addAction(self.act_check_updates)
299
321
 
300
322
 
323
+ m_about.addSeparator()
324
+ m_about.addAction(self.tr("Statistics..."), self._show_statistics)
325
+
326
+ # Connect tool stats
327
+ self._hook_tool_stats([m_fn, m_tools, mCosmic, m_geom, m_star, m_masks, m_header, m_scripts])
328
+
301
329
  # initialize enabled state + names
302
330
  self.update_undo_redo_action_labels()
303
331
 
@@ -0,0 +1,47 @@
1
+ from PyQt6.QtWidgets import QDialog, QVBoxLayout, QLabel, QFormLayout, QPushButton
2
+ from PyQt6.QtCore import Qt, QSettings
3
+ from PyQt6.QtGui import QIcon
4
+
5
+ class StatisticsDialog(QDialog):
6
+ def __init__(self, parent=None):
7
+ super().__init__(parent)
8
+ self.setWindowTitle(self.tr("App Statistics"))
9
+ self.setWindowFlags(self.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint)
10
+ self.resize(300, 200)
11
+
12
+ # Settings to read stats
13
+ self.settings = QSettings("SetiAstro", "SetiAstroSuitePro")
14
+
15
+ layout = QVBoxLayout(self)
16
+
17
+ form_layout = QFormLayout()
18
+
19
+ # Time Spent
20
+ total_seconds = self.settings.value("stats/total_time_seconds", 0, type=float)
21
+ days = int(total_seconds // 86400)
22
+ hours = int((total_seconds % 86400) // 3600)
23
+ minutes = int((total_seconds % 3600) // 60)
24
+
25
+ time_str = f"{days} {self.tr('Days')}, {hours} {self.tr('Hours')}, {minutes} {self.tr('Minutes')}"
26
+ if days == 0:
27
+ time_str = f"{hours} {self.tr('Hours')}, {minutes} {self.tr('Minutes')}"
28
+
29
+ self.lbl_time = QLabel(time_str)
30
+ form_layout.addRow(self.tr("Time Spent:"), self.lbl_time)
31
+
32
+ # Images Opened
33
+ images_count = self.settings.value("stats/opened_images_count", 0, type=int)
34
+ self.lbl_images = QLabel(str(images_count))
35
+ form_layout.addRow(self.tr("Images Opened:"), self.lbl_images)
36
+
37
+ # Tools Opened
38
+ tools_count = self.settings.value("stats/opened_tools_count", 0, type=int)
39
+ self.lbl_tools = QLabel(str(tools_count))
40
+ form_layout.addRow(self.tr("Tools Opened:"), self.lbl_tools)
41
+
42
+ layout.addLayout(form_layout)
43
+
44
+ # Close button
45
+ btn_close = QPushButton(self.tr("Close"))
46
+ btn_close.clicked.connect(self.accept)
47
+ layout.addWidget(btn_close, alignment=Qt.AlignmentFlag.AlignRight)