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.
- setiastro/images/Background_startup.jpg +0 -0
- setiastro/qml/ResourceMonitor.qml +126 -0
- setiastro/saspro/__main__.py +159 -23
- setiastro/saspro/_generated/build_info.py +2 -1
- setiastro/saspro/abe.py +62 -11
- setiastro/saspro/aberration_ai.py +3 -3
- setiastro/saspro/add_stars.py +5 -2
- setiastro/saspro/astrobin_exporter.py +3 -0
- setiastro/saspro/astrospike_python.py +3 -1
- setiastro/saspro/autostretch.py +4 -2
- setiastro/saspro/backgroundneutral.py +52 -10
- setiastro/saspro/batch_convert.py +3 -0
- setiastro/saspro/batch_renamer.py +3 -0
- setiastro/saspro/blemish_blaster.py +3 -0
- setiastro/saspro/cheat_sheet.py +50 -15
- setiastro/saspro/clahe.py +27 -1
- setiastro/saspro/comet_stacking.py +103 -38
- setiastro/saspro/convo.py +3 -0
- setiastro/saspro/copyastro.py +3 -0
- setiastro/saspro/cosmicclarity.py +70 -45
- setiastro/saspro/crop_dialog_pro.py +17 -0
- setiastro/saspro/curve_editor_pro.py +18 -0
- setiastro/saspro/debayer.py +3 -0
- setiastro/saspro/doc_manager.py +39 -16
- setiastro/saspro/fitsmodifier.py +3 -0
- setiastro/saspro/frequency_separation.py +8 -2
- setiastro/saspro/function_bundle.py +2 -0
- setiastro/saspro/generate_translations.py +715 -1
- setiastro/saspro/ghs_dialog_pro.py +3 -0
- setiastro/saspro/graxpert.py +3 -0
- setiastro/saspro/gui/main_window.py +275 -32
- setiastro/saspro/gui/mixins/dock_mixin.py +100 -1
- setiastro/saspro/gui/mixins/file_mixin.py +7 -0
- setiastro/saspro/gui/mixins/menu_mixin.py +28 -0
- setiastro/saspro/gui/statistics_dialog.py +47 -0
- setiastro/saspro/halobgon.py +29 -3
- setiastro/saspro/histogram.py +3 -0
- setiastro/saspro/history_explorer.py +2 -0
- setiastro/saspro/i18n.py +22 -10
- setiastro/saspro/image_combine.py +3 -0
- setiastro/saspro/image_peeker_pro.py +3 -0
- setiastro/saspro/imageops/stretch.py +5 -13
- setiastro/saspro/isophote.py +3 -0
- setiastro/saspro/legacy/numba_utils.py +64 -47
- setiastro/saspro/linear_fit.py +3 -0
- setiastro/saspro/live_stacking.py +13 -2
- setiastro/saspro/mask_creation.py +3 -0
- setiastro/saspro/mfdeconv.py +5 -0
- setiastro/saspro/morphology.py +30 -5
- setiastro/saspro/multiscale_decomp.py +3 -0
- setiastro/saspro/nbtorgb_stars.py +12 -2
- setiastro/saspro/numba_utils.py +148 -47
- setiastro/saspro/ops/scripts.py +77 -17
- setiastro/saspro/ops/settings.py +1 -43
- setiastro/saspro/perfect_palette_picker.py +1 -0
- setiastro/saspro/pixelmath.py +6 -2
- setiastro/saspro/plate_solver.py +2 -1
- setiastro/saspro/remove_green.py +18 -1
- setiastro/saspro/remove_stars.py +136 -162
- setiastro/saspro/resources.py +7 -0
- setiastro/saspro/rgb_combination.py +1 -0
- setiastro/saspro/rgbalign.py +4 -4
- setiastro/saspro/save_options.py +1 -0
- setiastro/saspro/sfcc.py +50 -8
- setiastro/saspro/signature_insert.py +3 -0
- setiastro/saspro/stacking_suite.py +630 -341
- setiastro/saspro/star_alignment.py +16 -1
- setiastro/saspro/star_spikes.py +116 -32
- setiastro/saspro/star_stretch.py +38 -1
- setiastro/saspro/stat_stretch.py +35 -3
- setiastro/saspro/subwindow.py +63 -2
- setiastro/saspro/supernovaasteroidhunter.py +3 -0
- setiastro/saspro/translations/all_source_strings.json +3654 -0
- setiastro/saspro/translations/ar_translations.py +3865 -0
- setiastro/saspro/translations/de_translations.py +16 -0
- setiastro/saspro/translations/es_translations.py +16 -0
- setiastro/saspro/translations/fr_translations.py +16 -0
- setiastro/saspro/translations/hi_translations.py +3571 -0
- setiastro/saspro/translations/integrate_translations.py +36 -0
- setiastro/saspro/translations/it_translations.py +16 -0
- setiastro/saspro/translations/ja_translations.py +16 -0
- setiastro/saspro/translations/pt_translations.py +16 -0
- setiastro/saspro/translations/ru_translations.py +2848 -0
- setiastro/saspro/translations/saspro_ar.qm +0 -0
- setiastro/saspro/translations/saspro_ar.ts +255 -0
- setiastro/saspro/translations/saspro_de.qm +0 -0
- setiastro/saspro/translations/saspro_de.ts +3 -3
- setiastro/saspro/translations/saspro_es.qm +0 -0
- setiastro/saspro/translations/saspro_es.ts +3 -3
- setiastro/saspro/translations/saspro_fr.qm +0 -0
- setiastro/saspro/translations/saspro_fr.ts +3 -3
- setiastro/saspro/translations/saspro_hi.qm +0 -0
- setiastro/saspro/translations/saspro_hi.ts +257 -0
- setiastro/saspro/translations/saspro_it.qm +0 -0
- setiastro/saspro/translations/saspro_it.ts +3 -3
- setiastro/saspro/translations/saspro_ja.qm +0 -0
- setiastro/saspro/translations/saspro_ja.ts +4 -4
- setiastro/saspro/translations/saspro_pt.qm +0 -0
- setiastro/saspro/translations/saspro_pt.ts +3 -3
- setiastro/saspro/translations/saspro_ru.qm +0 -0
- setiastro/saspro/translations/saspro_ru.ts +237 -0
- setiastro/saspro/translations/saspro_sw.qm +0 -0
- setiastro/saspro/translations/saspro_sw.ts +257 -0
- setiastro/saspro/translations/saspro_uk.qm +0 -0
- setiastro/saspro/translations/saspro_uk.ts +10771 -0
- setiastro/saspro/translations/saspro_zh.qm +0 -0
- setiastro/saspro/translations/saspro_zh.ts +3 -3
- setiastro/saspro/translations/sw_translations.py +3671 -0
- setiastro/saspro/translations/uk_translations.py +3700 -0
- setiastro/saspro/translations/zh_translations.py +16 -0
- setiastro/saspro/versioning.py +12 -6
- setiastro/saspro/view_bundle.py +3 -0
- setiastro/saspro/wavescale_hdr.py +22 -1
- setiastro/saspro/wavescalede.py +23 -1
- setiastro/saspro/whitebalance.py +39 -3
- setiastro/saspro/widgets/minigame/game.js +986 -0
- setiastro/saspro/widgets/minigame/index.html +53 -0
- setiastro/saspro/widgets/minigame/style.css +241 -0
- setiastro/saspro/widgets/resource_monitor.py +237 -0
- setiastro/saspro/widgets/wavelet_utils.py +52 -20
- setiastro/saspro/wimi.py +7996 -0
- setiastro/saspro/wims.py +578 -0
- {setiastrosuitepro-1.6.1.dist-info → setiastrosuitepro-1.6.2.dist-info}/METADATA +15 -4
- {setiastrosuitepro-1.6.1.dist-info → setiastrosuitepro-1.6.2.dist-info}/RECORD +128 -103
- {setiastrosuitepro-1.6.1.dist-info → setiastrosuitepro-1.6.2.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.1.dist-info → setiastrosuitepro-1.6.2.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.1.dist-info → setiastrosuitepro-1.6.2.dist-info}/licenses/LICENSE +0 -0
- {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
|
setiastro/saspro/graxpert.py
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
detail
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
|
|
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
|
-
|
|
8529
|
-
|
|
8530
|
-
|
|
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
|
-
|
|
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)
|