setiastrosuitepro 1.6.4__py3-none-any.whl → 1.6.10__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.
- setiastro/images/abeicon.svg +16 -0
- setiastro/images/acv_icon.png +0 -0
- setiastro/images/cosmic.svg +40 -0
- setiastro/images/cosmicsat.svg +24 -0
- setiastro/images/first_quarter.png +0 -0
- setiastro/images/full_moon.png +0 -0
- setiastro/images/graxpert.svg +19 -0
- setiastro/images/last_quarter.png +0 -0
- setiastro/images/linearfit.svg +32 -0
- setiastro/images/new_moon.png +0 -0
- setiastro/images/pixelmath.svg +42 -0
- setiastro/images/waning_crescent_1.png +0 -0
- setiastro/images/waning_crescent_2.png +0 -0
- setiastro/images/waning_crescent_3.png +0 -0
- setiastro/images/waning_crescent_4.png +0 -0
- setiastro/images/waning_crescent_5.png +0 -0
- setiastro/images/waning_gibbous_1.png +0 -0
- setiastro/images/waning_gibbous_2.png +0 -0
- setiastro/images/waning_gibbous_3.png +0 -0
- setiastro/images/waning_gibbous_4.png +0 -0
- setiastro/images/waning_gibbous_5.png +0 -0
- setiastro/images/waxing_crescent_1.png +0 -0
- setiastro/images/waxing_crescent_2.png +0 -0
- setiastro/images/waxing_crescent_3.png +0 -0
- setiastro/images/waxing_crescent_4.png +0 -0
- setiastro/images/waxing_crescent_5.png +0 -0
- setiastro/images/waxing_gibbous_1.png +0 -0
- setiastro/images/waxing_gibbous_2.png +0 -0
- setiastro/images/waxing_gibbous_3.png +0 -0
- setiastro/images/waxing_gibbous_4.png +0 -0
- setiastro/images/waxing_gibbous_5.png +0 -0
- setiastro/qml/ResourceMonitor.qml +84 -82
- setiastro/saspro/__main__.py +19 -0
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/abe.py +37 -4
- setiastro/saspro/aberration_ai.py +237 -21
- setiastro/saspro/acv_exporter.py +379 -0
- setiastro/saspro/add_stars.py +33 -6
- setiastro/saspro/backgroundneutral.py +35 -7
- setiastro/saspro/blemish_blaster.py +4 -1
- setiastro/saspro/blink_comparator_pro.py +74 -24
- setiastro/saspro/clahe.py +4 -1
- setiastro/saspro/continuum_subtract.py +4 -1
- setiastro/saspro/convo.py +4 -1
- setiastro/saspro/cosmicclarity.py +129 -18
- setiastro/saspro/crop_dialog_pro.py +123 -7
- setiastro/saspro/curve_editor_pro.py +109 -42
- setiastro/saspro/doc_manager.py +67 -4
- setiastro/saspro/exoplanet_detector.py +120 -28
- setiastro/saspro/frequency_separation.py +1158 -204
- setiastro/saspro/ghs_dialog_pro.py +81 -16
- setiastro/saspro/graxpert.py +1 -0
- setiastro/saspro/gui/main_window.py +393 -204
- setiastro/saspro/gui/mixins/menu_mixin.py +1 -0
- setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
- setiastro/saspro/gui/mixins/toolbar_mixin.py +356 -12
- setiastro/saspro/gui/mixins/update_mixin.py +138 -36
- setiastro/saspro/gui/mixins/view_mixin.py +42 -0
- setiastro/saspro/halobgon.py +4 -0
- setiastro/saspro/histogram.py +5 -1
- setiastro/saspro/image_combine.py +4 -0
- setiastro/saspro/image_peeker_pro.py +4 -0
- setiastro/saspro/imageops/stretch.py +531 -62
- setiastro/saspro/isophote.py +4 -0
- setiastro/saspro/layers.py +13 -9
- setiastro/saspro/layers_dock.py +183 -3
- setiastro/saspro/legacy/image_manager.py +154 -20
- setiastro/saspro/legacy/numba_utils.py +43 -0
- setiastro/saspro/legacy/xisf.py +240 -98
- setiastro/saspro/live_stacking.py +180 -79
- setiastro/saspro/luminancerecombine.py +228 -27
- setiastro/saspro/mask_creation.py +174 -15
- setiastro/saspro/mfdeconv.py +113 -35
- setiastro/saspro/mfdeconvcudnn.py +119 -70
- setiastro/saspro/mfdeconvsport.py +112 -35
- setiastro/saspro/morphology.py +4 -0
- setiastro/saspro/multiscale_decomp.py +51 -12
- setiastro/saspro/numba_utils.py +72 -2
- setiastro/saspro/ops/commands.py +18 -18
- setiastro/saspro/ops/script_editor.py +5 -2
- setiastro/saspro/ops/scripts.py +3 -0
- setiastro/saspro/perfect_palette_picker.py +37 -3
- setiastro/saspro/plate_solver.py +84 -49
- setiastro/saspro/psf_viewer.py +119 -37
- setiastro/saspro/resources.py +67 -0
- setiastro/saspro/rgbalign.py +4 -0
- setiastro/saspro/selective_color.py +4 -1
- setiastro/saspro/sfcc.py +60 -2
- setiastro/saspro/shortcuts.py +142 -23
- setiastro/saspro/signature_insert.py +692 -33
- setiastro/saspro/stacking_suite.py +1017 -400
- setiastro/saspro/star_alignment.py +4 -1
- setiastro/saspro/star_spikes.py +4 -0
- setiastro/saspro/star_stretch.py +38 -3
- setiastro/saspro/stat_stretch.py +702 -128
- setiastro/saspro/subwindow.py +786 -360
- setiastro/saspro/supernovaasteroidhunter.py +1 -1
- setiastro/saspro/wavescale_hdr.py +4 -1
- setiastro/saspro/wavescalede.py +4 -1
- setiastro/saspro/whitebalance.py +60 -12
- setiastro/saspro/widgets/common_utilities.py +28 -21
- setiastro/saspro/widgets/resource_monitor.py +109 -59
- setiastro/saspro/widgets/spinboxes.py +10 -13
- setiastro/saspro/wimi.py +27 -656
- setiastro/saspro/wims.py +13 -3
- setiastro/saspro/xisf.py +101 -11
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/METADATA +2 -1
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/RECORD +112 -80
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/licenses/license.txt +0 -0
|
@@ -24,7 +24,7 @@ from setiastro.saspro.legacy.image_manager import load_image, save_image
|
|
|
24
24
|
from setiastro.saspro.legacy.numba_utils import bulk_cosmetic_correction_numba
|
|
25
25
|
from setiastro.saspro.imageops.stretch import stretch_mono_image, stretch_color_image
|
|
26
26
|
from setiastro.saspro.star_alignment import PolyGradientRemoval
|
|
27
|
-
from
|
|
27
|
+
from setiastro.saspro import minorbodycatalog as mbc
|
|
28
28
|
from setiastro.saspro.plate_solver import PlateSolverDialog as PlateSolver
|
|
29
29
|
from setiastro.saspro.widgets.themed_buttons import themed_toolbtn
|
|
30
30
|
|
|
@@ -202,7 +202,10 @@ class WaveScaleHDRDialogPro(QDialog):
|
|
|
202
202
|
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
203
203
|
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
204
204
|
self.setModal(False)
|
|
205
|
-
|
|
205
|
+
try:
|
|
206
|
+
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
207
|
+
except Exception:
|
|
208
|
+
pass # older PyQt6 versions
|
|
206
209
|
self._doc = doc
|
|
207
210
|
base = getattr(doc, "image", None)
|
|
208
211
|
if base is None:
|
setiastro/saspro/wavescalede.py
CHANGED
|
@@ -204,7 +204,10 @@ class WaveScaleDarkEnhancerDialogPro(QDialog):
|
|
|
204
204
|
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
205
205
|
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
206
206
|
self.setModal(False)
|
|
207
|
-
|
|
207
|
+
try:
|
|
208
|
+
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
209
|
+
except Exception:
|
|
210
|
+
pass # older PyQt6 versions
|
|
208
211
|
self._doc = doc
|
|
209
212
|
base = getattr(doc, "image", None)
|
|
210
213
|
if base is None:
|
setiastro/saspro/whitebalance.py
CHANGED
|
@@ -88,7 +88,7 @@ def plot_star_color_ratios_comparison(raw_pixels: np.ndarray, after_pixels: np.n
|
|
|
88
88
|
|
|
89
89
|
plt.suptitle("Star Color Ratios with RGB Mapping", fontsize=14)
|
|
90
90
|
plt.tight_layout()
|
|
91
|
-
plt.show()
|
|
91
|
+
plt.show(block=False)
|
|
92
92
|
|
|
93
93
|
def apply_manual_white_balance(img: np.ndarray, r_gain: float, g_gain: float, b_gain: float) -> np.ndarray:
|
|
94
94
|
"""Simple per-channel gain, clipped to [0,1]."""
|
|
@@ -193,16 +193,23 @@ class WhiteBalanceDialog(QDialog):
|
|
|
193
193
|
super().__init__(parent)
|
|
194
194
|
self._main = parent
|
|
195
195
|
self.doc = doc
|
|
196
|
-
|
|
197
|
-
# Connect to active document change signal
|
|
196
|
+
self._active_doc_conn = False
|
|
198
197
|
if hasattr(self._main, "currentDocumentChanged"):
|
|
199
|
-
|
|
198
|
+
try:
|
|
199
|
+
self._main.currentDocumentChanged.connect(self._on_active_doc_changed)
|
|
200
|
+
self._active_doc_conn = True
|
|
201
|
+
except Exception:
|
|
202
|
+
self._active_doc_conn = False
|
|
200
203
|
if icon:
|
|
201
204
|
self.setWindowIcon(icon)
|
|
202
205
|
self.setWindowTitle(self.tr("White Balance"))
|
|
203
206
|
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
204
207
|
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
205
208
|
self.setModal(False)
|
|
209
|
+
try:
|
|
210
|
+
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
211
|
+
except Exception:
|
|
212
|
+
pass # older PyQt6 versions
|
|
206
213
|
self.resize(900, 600)
|
|
207
214
|
|
|
208
215
|
self._build_ui()
|
|
@@ -213,6 +220,8 @@ class WhiteBalanceDialog(QDialog):
|
|
|
213
220
|
self._update_mode_widgets()
|
|
214
221
|
# kick off a first detection preview
|
|
215
222
|
QTimer.singleShot(200, self._update_star_preview)
|
|
223
|
+
self.finished.connect(lambda *_: self._cleanup())
|
|
224
|
+
|
|
216
225
|
|
|
217
226
|
# ---- UI construction ------------------------------------------------
|
|
218
227
|
def _build_ui(self):
|
|
@@ -327,11 +336,16 @@ class WhiteBalanceDialog(QDialog):
|
|
|
327
336
|
)
|
|
328
337
|
self.star_count.setText(self.tr("Detected {0} stars.").format(count))
|
|
329
338
|
# to pixmap
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
339
|
+
overlay8 = np.ascontiguousarray(np.clip(overlay * 255.0, 0, 255).astype(np.uint8))
|
|
340
|
+
h, w, _ = overlay8.shape
|
|
341
|
+
qimg = QImage(overlay8.data, w, h, 3 * w, QImage.Format.Format_RGB888)
|
|
342
|
+
|
|
343
|
+
# Make sure the numpy buffer stays alive until QPixmap is created
|
|
344
|
+
pm = QPixmap.fromImage(qimg.copy()).scaled(
|
|
345
|
+
self.preview.width(), self.preview.height(),
|
|
346
|
+
Qt.AspectRatioMode.KeepAspectRatio,
|
|
347
|
+
Qt.TransformationMode.SmoothTransformation
|
|
348
|
+
)
|
|
335
349
|
self.preview.setPixmap(pm)
|
|
336
350
|
except Exception as e:
|
|
337
351
|
self.star_count.setText(self.tr("Detection failed."))
|
|
@@ -394,7 +408,8 @@ class WhiteBalanceDialog(QDialog):
|
|
|
394
408
|
# Use the headless helper so doc metadata is consistent
|
|
395
409
|
apply_white_balance_to_doc(self.doc, preset)
|
|
396
410
|
# Dialog stays open - refresh document for next operation
|
|
397
|
-
self.
|
|
411
|
+
self._finish_and_close()
|
|
412
|
+
return
|
|
398
413
|
|
|
399
414
|
elif mode == "Auto":
|
|
400
415
|
preset = {"mode": "auto"}
|
|
@@ -404,7 +419,8 @@ class WhiteBalanceDialog(QDialog):
|
|
|
404
419
|
|
|
405
420
|
apply_white_balance_to_doc(self.doc, preset)
|
|
406
421
|
# Dialog stays open - refresh document for next operation
|
|
407
|
-
self.
|
|
422
|
+
self._finish_and_close()
|
|
423
|
+
return
|
|
408
424
|
|
|
409
425
|
else: # --- Star-Based: compute here so we can plot like SASv2 ---
|
|
410
426
|
thr = float(self.thr_slider.value())
|
|
@@ -472,7 +488,8 @@ class WhiteBalanceDialog(QDialog):
|
|
|
472
488
|
self.tr("Star-Based WB applied.\nDetected {0} stars.").format(int(star_count)),
|
|
473
489
|
)
|
|
474
490
|
# Dialog stays open - refresh document for next operation
|
|
475
|
-
self.
|
|
491
|
+
self._finish_and_close()
|
|
492
|
+
return
|
|
476
493
|
|
|
477
494
|
except Exception as e:
|
|
478
495
|
QMessageBox.critical(self, self.tr("White Balance"), self.tr("Failed to apply White Balance:\n{0}").format(e))
|
|
@@ -490,3 +507,34 @@ class WhiteBalanceDialog(QDialog):
|
|
|
490
507
|
self.doc = new_doc
|
|
491
508
|
except Exception:
|
|
492
509
|
pass
|
|
510
|
+
|
|
511
|
+
def _finish_and_close(self):
|
|
512
|
+
"""
|
|
513
|
+
Close this dialog after a successful apply.
|
|
514
|
+
Use accept() so it behaves like a successful completion.
|
|
515
|
+
"""
|
|
516
|
+
try:
|
|
517
|
+
self.accept()
|
|
518
|
+
except Exception:
|
|
519
|
+
self.close()
|
|
520
|
+
|
|
521
|
+
def _cleanup(self):
|
|
522
|
+
# Disconnect active-doc signal so the main window doesn't keep us alive
|
|
523
|
+
try:
|
|
524
|
+
if self._active_doc_conn and hasattr(self._main, "currentDocumentChanged"):
|
|
525
|
+
self._main.currentDocumentChanged.disconnect(self._on_active_doc_changed)
|
|
526
|
+
except Exception:
|
|
527
|
+
pass
|
|
528
|
+
self._active_doc_conn = False
|
|
529
|
+
|
|
530
|
+
# Stop debounce timer
|
|
531
|
+
try:
|
|
532
|
+
if getattr(self, "_debounce", None) is not None:
|
|
533
|
+
self._debounce.stop()
|
|
534
|
+
except Exception:
|
|
535
|
+
pass
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def closeEvent(self, ev):
|
|
539
|
+
self._cleanup()
|
|
540
|
+
super().closeEvent(ev)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#src/setiastro/saspro/widgets/common_utilities.py
|
|
1
2
|
"""
|
|
2
3
|
Common UI utilities and shared components.
|
|
3
4
|
|
|
@@ -220,32 +221,38 @@ _strip_ui_decorations = strip_ui_decorations
|
|
|
220
221
|
# ---------------------------------------------------------------------------
|
|
221
222
|
|
|
222
223
|
def install_crash_handlers(app: 'QApplication') -> None:
|
|
223
|
-
"""
|
|
224
|
-
Install global crash and exception handlers for the application.
|
|
225
|
-
|
|
226
|
-
Sets up:
|
|
227
|
-
1. faulthandler for hard crashes (segfaults) → saspro_crash.log
|
|
228
|
-
2. sys.excepthook for uncaught main thread exceptions
|
|
229
|
-
3. threading.excepthook for uncaught background thread exceptions
|
|
230
|
-
|
|
231
|
-
All exceptions are logged and displayed in a dialog to the user.
|
|
232
|
-
|
|
233
|
-
Args:
|
|
234
|
-
app: The QApplication instance
|
|
235
|
-
|
|
236
|
-
Example:
|
|
237
|
-
app = QApplication(sys.argv)
|
|
238
|
-
install_crash_handlers(app)
|
|
239
|
-
"""
|
|
240
224
|
import faulthandler
|
|
241
|
-
|
|
242
|
-
|
|
225
|
+
import tempfile
|
|
226
|
+
from pathlib import Path
|
|
227
|
+
|
|
228
|
+
def _get_crash_log_path() -> str:
|
|
229
|
+
try:
|
|
230
|
+
if hasattr(sys, "_MEIPASS"):
|
|
231
|
+
if sys.platform.startswith("win"):
|
|
232
|
+
log_dir = Path(os.path.expandvars("%APPDATA%")) / "SetiAstroSuitePro" / "logs"
|
|
233
|
+
elif sys.platform.startswith("darwin"):
|
|
234
|
+
log_dir = Path.home() / "Library" / "Logs" / "SetiAstroSuitePro"
|
|
235
|
+
else:
|
|
236
|
+
log_dir = Path.home() / ".local" / "share" / "SetiAstroSuitePro" / "logs"
|
|
237
|
+
else:
|
|
238
|
+
# dev fallback
|
|
239
|
+
log_dir = Path("logs")
|
|
240
|
+
|
|
241
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
242
|
+
return str(log_dir / "saspro_crash.log")
|
|
243
|
+
except Exception:
|
|
244
|
+
return str(Path(tempfile.gettempdir()) / "saspro_crash.log")
|
|
245
|
+
|
|
246
|
+
# 1) Hard crashes → saspro_crash.log
|
|
243
247
|
try:
|
|
244
|
-
|
|
248
|
+
crash_path = _get_crash_log_path()
|
|
249
|
+
_crash_log = open(crash_path, "a", encoding="utf-8", errors="replace")
|
|
245
250
|
faulthandler.enable(file=_crash_log, all_threads=True)
|
|
246
251
|
atexit.register(_crash_log.close)
|
|
252
|
+
logging.info("Faulthandler crash log: %s", crash_path)
|
|
247
253
|
except Exception:
|
|
248
254
|
logging.exception("Failed to enable faulthandler")
|
|
255
|
+
|
|
249
256
|
|
|
250
257
|
def _show_dialog(title: str, head: str, details: str) -> None:
|
|
251
258
|
"""Show error dialog marshaled to main thread."""
|
|
@@ -261,7 +268,7 @@ def install_crash_handlers(app: 'QApplication') -> None:
|
|
|
261
268
|
m.setStandardButtons(QMessageBox.StandardButton.Ok)
|
|
262
269
|
m.exec()
|
|
263
270
|
QTimer.singleShot(0, _ui)
|
|
264
|
-
|
|
271
|
+
|
|
265
272
|
# 2) Any uncaught exception on the main thread
|
|
266
273
|
def _excepthook(exc_type, exc_value, exc_tb):
|
|
267
274
|
tb = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
|
|
@@ -5,93 +5,122 @@ import psutil
|
|
|
5
5
|
from PyQt6.QtCore import Qt, QUrl, QTimer, QObject, pyqtProperty, pyqtSignal, QThread
|
|
6
6
|
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QFrame
|
|
7
7
|
from PyQt6.QtQuickWidgets import QQuickWidget
|
|
8
|
-
|
|
8
|
+
import time
|
|
9
|
+
import subprocess
|
|
9
10
|
from setiastro.saspro.memory_utils import get_memory_usage_mb
|
|
10
11
|
from setiastro.saspro.resources import _get_base_path
|
|
11
12
|
|
|
13
|
+
|
|
12
14
|
class GPUWorker(QThread):
|
|
13
|
-
"""Background worker to monitor GPU without blocking the UI."""
|
|
14
15
|
resultReady = pyqtSignal(float)
|
|
15
16
|
|
|
16
17
|
def __init__(self, has_nvidia: bool, parent=None):
|
|
17
18
|
super().__init__(parent)
|
|
18
19
|
self._has_nvidia = has_nvidia
|
|
19
|
-
|
|
20
|
+
|
|
21
|
+
# cache + throttle (Windows PowerShell is expensive)
|
|
22
|
+
self._last_win_poll = 0.0
|
|
23
|
+
self._cached_win_val = 0.0
|
|
24
|
+
|
|
25
|
+
self._last_emit = 0.0
|
|
26
|
+
self._last_emitted_val = None
|
|
27
|
+
|
|
28
|
+
def _startupinfo_hidden(self):
|
|
29
|
+
if os.name != "nt":
|
|
30
|
+
return None
|
|
31
|
+
si = subprocess.STARTUPINFO()
|
|
32
|
+
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
|
33
|
+
si.wShowWindow = 0
|
|
34
|
+
return si
|
|
20
35
|
|
|
21
36
|
def _get_windows_gpu_load(self) -> float:
|
|
22
|
-
if os.name !=
|
|
37
|
+
if os.name != "nt":
|
|
23
38
|
return 0.0
|
|
39
|
+
|
|
40
|
+
now = time.monotonic()
|
|
41
|
+
|
|
42
|
+
# THROTTLE: run this at most once every 1.5 seconds
|
|
43
|
+
if (now - self._last_win_poll) < 1.5:
|
|
44
|
+
return self._cached_win_val
|
|
45
|
+
|
|
46
|
+
self._last_win_poll = now
|
|
47
|
+
|
|
24
48
|
try:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
# Use explicit powershell.exe and make it non-interactive + hidden
|
|
50
|
+
cmd = [
|
|
51
|
+
"powershell.exe",
|
|
52
|
+
"-NoProfile",
|
|
53
|
+
"-NonInteractive",
|
|
54
|
+
"-ExecutionPolicy", "Bypass",
|
|
55
|
+
"-Command",
|
|
56
|
+
(
|
|
57
|
+
"$x = Get-CimInstance Win32_PerfFormattedData_GPUPerformanceCounters_GPUEngine "
|
|
58
|
+
"-ErrorAction SilentlyContinue; "
|
|
59
|
+
"if (-not $x) { 0 } else { "
|
|
60
|
+
" $m = ($x | Measure-Object -Property UtilizationPercentage -Maximum).Maximum; "
|
|
61
|
+
" if ($m) { [math]::Round([double]$m, 1) } else { 0 } "
|
|
62
|
+
"}"
|
|
63
|
+
),
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
out = subprocess.check_output(
|
|
67
|
+
cmd,
|
|
68
|
+
startupinfo=self._startupinfo_hidden(),
|
|
69
|
+
timeout=2.0, # IMPORTANT: don’t allow 5s hangs
|
|
70
|
+
stderr=subprocess.DEVNULL, # keep it quiet
|
|
41
71
|
)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
val_str = out.decode("utf-8").strip()
|
|
48
|
-
|
|
49
|
-
if not val_str: return 0.0
|
|
50
|
-
return float(val_str.replace(",", "."))
|
|
72
|
+
val_str = out.decode("utf-8", errors="ignore").strip()
|
|
73
|
+
|
|
74
|
+
val = float(val_str.replace(",", ".")) if val_str else 0.0
|
|
75
|
+
self._cached_win_val = val
|
|
76
|
+
return val
|
|
51
77
|
except Exception:
|
|
52
|
-
|
|
78
|
+
# keep last known value instead of spamming 0.0
|
|
79
|
+
return self._cached_win_val
|
|
53
80
|
|
|
54
81
|
def _get_gpu_load(self) -> float:
|
|
55
82
|
nv_val = 0.0
|
|
56
83
|
win_val = 0.0
|
|
57
|
-
|
|
58
|
-
#
|
|
84
|
+
|
|
85
|
+
# NVIDIA (fast, keep it)
|
|
59
86
|
if self._has_nvidia:
|
|
60
87
|
try:
|
|
61
|
-
import subprocess
|
|
62
|
-
startupinfo = None
|
|
63
|
-
if os.name == 'nt':
|
|
64
|
-
startupinfo = subprocess.STARTUPINFO()
|
|
65
|
-
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
|
66
|
-
startupinfo.wShowWindow = 0
|
|
67
|
-
|
|
68
88
|
out = subprocess.check_output(
|
|
69
89
|
["nvidia-smi", "--query-gpu=utilization.gpu", "--format=csv,noheader,nounits"],
|
|
70
|
-
startupinfo=
|
|
71
|
-
timeout=0.6
|
|
90
|
+
startupinfo=self._startupinfo_hidden(),
|
|
91
|
+
timeout=0.6,
|
|
92
|
+
stderr=subprocess.DEVNULL,
|
|
72
93
|
)
|
|
73
|
-
line = out.decode("utf-8").strip().split(
|
|
94
|
+
line = out.decode("utf-8", errors="ignore").strip().split("\n")[0]
|
|
74
95
|
nv_val = float(line)
|
|
75
96
|
except Exception:
|
|
76
|
-
pass
|
|
77
|
-
|
|
78
|
-
#
|
|
79
|
-
if os.name ==
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
# Windows integrated (slow, throttled)
|
|
100
|
+
if os.name == "nt":
|
|
80
101
|
win_val = self._get_windows_gpu_load()
|
|
81
|
-
|
|
102
|
+
|
|
82
103
|
return max(nv_val, win_val)
|
|
83
104
|
|
|
84
105
|
def run(self):
|
|
85
106
|
while not self.isInterruptionRequested():
|
|
86
107
|
try:
|
|
87
108
|
val = self._get_gpu_load()
|
|
88
|
-
|
|
89
|
-
#
|
|
90
|
-
|
|
91
|
-
|
|
109
|
+
|
|
110
|
+
# Optional: emit only if value changed a bit, or once per 250ms max
|
|
111
|
+
now = time.monotonic()
|
|
112
|
+
if (
|
|
113
|
+
self._last_emitted_val is None
|
|
114
|
+
or abs(val - self._last_emitted_val) >= 1.0
|
|
115
|
+
or (now - self._last_emit) >= 0.5
|
|
116
|
+
):
|
|
117
|
+
self._last_emit = now
|
|
118
|
+
self._last_emitted_val = val
|
|
119
|
+
self.resultReady.emit(val)
|
|
120
|
+
|
|
92
121
|
self.msleep(250)
|
|
93
122
|
except Exception:
|
|
94
|
-
self.msleep(1000)
|
|
123
|
+
self.msleep(1000)
|
|
95
124
|
|
|
96
125
|
class ResourceBackend(QObject):
|
|
97
126
|
"""Backend logic for the QML Resource Monitor."""
|
|
@@ -197,6 +226,7 @@ class SystemMonitorWidget(QQuickWidget):
|
|
|
197
226
|
self.setAttribute(Qt.WidgetAttribute.WA_AlwaysStackOnTop, False)
|
|
198
227
|
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
|
|
199
228
|
self.setClearColor(Qt.GlobalColor.transparent)
|
|
229
|
+
self._qml_push_pending = False
|
|
200
230
|
|
|
201
231
|
# Connect Backend
|
|
202
232
|
self.backend = ResourceBackend(self)
|
|
@@ -218,16 +248,23 @@ class SystemMonitorWidget(QQuickWidget):
|
|
|
218
248
|
# Let's re-write the QML loading part to use a safer 'initialProperties' approach or just signal/slots.
|
|
219
249
|
#
|
|
220
250
|
# EASIEST: QML binds to `root.cpuUsage`. Python sets `root.cpuUsage`.
|
|
221
|
-
|
|
222
|
-
self.backend.cpuChanged.connect(self._push_data_to_qml)
|
|
223
|
-
self.backend.ramChanged.connect(self._push_data_to_qml)
|
|
224
|
-
self.backend.gpuChanged.connect(self._push_data_to_qml)
|
|
225
|
-
self.backend.appRamChanged.connect(self._push_data_to_qml)
|
|
251
|
+
|
|
226
252
|
|
|
227
253
|
# Load QML
|
|
228
254
|
qml_path = os.path.join(_get_base_path(), "qml", "ResourceMonitor.qml")
|
|
229
255
|
self.setSource(QUrl.fromLocalFile(qml_path))
|
|
230
256
|
|
|
257
|
+
def _schedule_qml_push(self):
|
|
258
|
+
if self._qml_push_pending:
|
|
259
|
+
return
|
|
260
|
+
self._qml_push_pending = True
|
|
261
|
+
QTimer.singleShot(0, self._push_data_to_qml_coalesced)
|
|
262
|
+
|
|
263
|
+
def _push_data_to_qml_coalesced(self):
|
|
264
|
+
self._qml_push_pending = False
|
|
265
|
+
self._push_data_to_qml()
|
|
266
|
+
|
|
267
|
+
|
|
231
268
|
def _push_data_to_qml(self):
|
|
232
269
|
root = self.rootObject()
|
|
233
270
|
if root:
|
|
@@ -239,10 +276,23 @@ class SystemMonitorWidget(QQuickWidget):
|
|
|
239
276
|
# --- Drag & Drop Support ---
|
|
240
277
|
def mousePressEvent(self, event):
|
|
241
278
|
if event.button() == Qt.MouseButton.LeftButton:
|
|
279
|
+
# Wayland-friendly: ask compositor to move the window
|
|
280
|
+
wh = self.windowHandle()
|
|
281
|
+
if wh is not None:
|
|
282
|
+
try:
|
|
283
|
+
# Works best for frameless overlays on Wayland
|
|
284
|
+
wh.startSystemMove()
|
|
285
|
+
event.accept()
|
|
286
|
+
return
|
|
287
|
+
except Exception:
|
|
288
|
+
pass
|
|
289
|
+
|
|
290
|
+
# Fallback (Windows/X11): manual move tracking
|
|
242
291
|
self._drag_start_pos = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
|
|
243
292
|
event.accept()
|
|
244
|
-
|
|
245
|
-
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
super().mousePressEvent(event)
|
|
246
296
|
|
|
247
297
|
def mouseMoveEvent(self, event):
|
|
248
298
|
if event.buttons() & Qt.MouseButton.LeftButton:
|
|
@@ -37,7 +37,7 @@ class CustomSpinBox(QWidget):
|
|
|
37
37
|
self.minimum = minimum
|
|
38
38
|
self.maximum = maximum
|
|
39
39
|
self.step = step
|
|
40
|
-
self._value = initial
|
|
40
|
+
self._value = int(initial)
|
|
41
41
|
|
|
42
42
|
# Line edit for value display/entry
|
|
43
43
|
self.lineEdit = QLineEdit(str(initial))
|
|
@@ -77,14 +77,13 @@ class CustomSpinBox(QWidget):
|
|
|
77
77
|
|
|
78
78
|
self._update_button_states()
|
|
79
79
|
|
|
80
|
-
@property
|
|
81
80
|
def value(self) -> int:
|
|
82
|
-
"""
|
|
83
|
-
return self._value
|
|
81
|
+
"""Qt-style getter."""
|
|
82
|
+
return int(self._value)
|
|
84
83
|
|
|
85
84
|
def setValue(self, val: int) -> None:
|
|
86
|
-
|
|
87
|
-
val = max(self.minimum, min(self.maximum, val))
|
|
85
|
+
val = int(val)
|
|
86
|
+
val = max(int(self.minimum), min(int(self.maximum), val))
|
|
88
87
|
if val != self._value:
|
|
89
88
|
self._value = val
|
|
90
89
|
self.lineEdit.setText(str(val))
|
|
@@ -144,7 +143,6 @@ class CustomSpinBox(QWidget):
|
|
|
144
143
|
return self._value
|
|
145
144
|
|
|
146
145
|
def _update_button_states(self) -> None:
|
|
147
|
-
"""Enable/disable buttons at limits."""
|
|
148
146
|
self.upButton.setEnabled(self._value < self.maximum)
|
|
149
147
|
self.downButton.setEnabled(self._value > self.minimum)
|
|
150
148
|
|
|
@@ -175,7 +173,7 @@ class CustomDoubleSpinBox(QWidget):
|
|
|
175
173
|
self.maximum = maximum
|
|
176
174
|
self.step = step
|
|
177
175
|
self.decimals = decimals
|
|
178
|
-
self._value = initial
|
|
176
|
+
self._value = float(initial)
|
|
179
177
|
|
|
180
178
|
# Line edit for value display/entry
|
|
181
179
|
self.lineEdit = QLineEdit(f"{initial:.{decimals}f}")
|
|
@@ -215,14 +213,13 @@ class CustomDoubleSpinBox(QWidget):
|
|
|
215
213
|
|
|
216
214
|
self._update_button_states()
|
|
217
215
|
|
|
218
|
-
@property
|
|
219
216
|
def value(self) -> float:
|
|
220
|
-
"""
|
|
221
|
-
return self._value
|
|
217
|
+
"""Qt-style getter."""
|
|
218
|
+
return float(self._value)
|
|
222
219
|
|
|
223
220
|
def setValue(self, val: float) -> None:
|
|
224
|
-
|
|
225
|
-
val = max(self.minimum, min(self.maximum, val))
|
|
221
|
+
val = float(val)
|
|
222
|
+
val = max(float(self.minimum), min(float(self.maximum), val))
|
|
226
223
|
if abs(val - self._value) > 1e-10:
|
|
227
224
|
self._value = val
|
|
228
225
|
self.lineEdit.setText(f"{val:.{self.decimals}f}")
|