setiastrosuitepro 1.6.4__py3-none-any.whl → 1.6.12__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/colorwheel.svg +97 -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 +20 -1
- 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 +108 -40
- 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 +13 -7
- 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 +245 -15
- 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 +429 -228
- setiastro/saspro/gui/mixins/dock_mixin.py +245 -24
- setiastro/saspro/gui/mixins/menu_mixin.py +27 -1
- setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
- setiastro/saspro/gui/mixins/toolbar_mixin.py +384 -18
- 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/starbasedwhitebalance.py +23 -52
- setiastro/saspro/imageops/stretch.py +582 -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 +67 -47
- 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 -57
- setiastro/saspro/ops/commands.py +18 -18
- setiastro/saspro/ops/script_editor.py +10 -2
- setiastro/saspro/ops/scripts.py +122 -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 +364 -152
- setiastro/saspro/shortcuts.py +160 -29
- setiastro/saspro/signature_insert.py +692 -33
- setiastro/saspro/stacking_suite.py +1331 -484
- setiastro/saspro/star_alignment.py +247 -123
- setiastro/saspro/star_spikes.py +4 -0
- setiastro/saspro/star_stretch.py +38 -3
- setiastro/saspro/stat_stretch.py +743 -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 +84 -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.12.dist-info}/METADATA +2 -1
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/RECORD +115 -82
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.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]."""
|
|
@@ -184,6 +184,30 @@ def apply_white_balance_to_doc(doc, preset: Optional[Dict] = None):
|
|
|
184
184
|
step_name="White Balance",
|
|
185
185
|
)
|
|
186
186
|
|
|
187
|
+
def apply_pivot_gain(img: np.ndarray, med: np.ndarray, gains: np.ndarray) -> np.ndarray:
|
|
188
|
+
# img: HxWx3 float32 in [0,1]
|
|
189
|
+
med3 = med.reshape(1, 1, 3).astype(np.float32)
|
|
190
|
+
g3 = gains.reshape(1, 1, 3).astype(np.float32)
|
|
191
|
+
|
|
192
|
+
# pivot around median; do not scale negative deltas
|
|
193
|
+
d = img - med3
|
|
194
|
+
d = np.maximum(d, 0.0)
|
|
195
|
+
out = d * g3 + med3
|
|
196
|
+
return np.clip(out, 0.0, 1.0).astype(np.float32, copy=False)
|
|
197
|
+
|
|
198
|
+
def smoothstep(edge0, edge1, x):
|
|
199
|
+
t = np.clip((x - edge0) / (edge1 - edge0 + 1e-12), 0.0, 1.0)
|
|
200
|
+
return t * t * (3.0 - 2.0 * t)
|
|
201
|
+
|
|
202
|
+
def apply_soft_protect(img: np.ndarray, out_pivot: np.ndarray, k: float = 0.02) -> np.ndarray:
|
|
203
|
+
# luminance-based fade-in above median luminance
|
|
204
|
+
L = 0.2126*img[...,0] + 0.7152*img[...,1] + 0.0722*img[...,2]
|
|
205
|
+
Lm = float(np.median(L))
|
|
206
|
+
w = smoothstep(Lm, Lm + k, L).astype(np.float32)
|
|
207
|
+
w3 = w[..., None]
|
|
208
|
+
out = img * (1.0 - w3) + out_pivot * w3
|
|
209
|
+
return np.clip(out, 0.0, 1.0).astype(np.float32, copy=False)
|
|
210
|
+
|
|
187
211
|
|
|
188
212
|
# -------------------------
|
|
189
213
|
# Interactive dialog (UI)
|
|
@@ -193,16 +217,23 @@ class WhiteBalanceDialog(QDialog):
|
|
|
193
217
|
super().__init__(parent)
|
|
194
218
|
self._main = parent
|
|
195
219
|
self.doc = doc
|
|
196
|
-
|
|
197
|
-
# Connect to active document change signal
|
|
220
|
+
self._active_doc_conn = False
|
|
198
221
|
if hasattr(self._main, "currentDocumentChanged"):
|
|
199
|
-
|
|
222
|
+
try:
|
|
223
|
+
self._main.currentDocumentChanged.connect(self._on_active_doc_changed)
|
|
224
|
+
self._active_doc_conn = True
|
|
225
|
+
except Exception:
|
|
226
|
+
self._active_doc_conn = False
|
|
200
227
|
if icon:
|
|
201
228
|
self.setWindowIcon(icon)
|
|
202
229
|
self.setWindowTitle(self.tr("White Balance"))
|
|
203
230
|
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
204
231
|
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
205
232
|
self.setModal(False)
|
|
233
|
+
try:
|
|
234
|
+
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
235
|
+
except Exception:
|
|
236
|
+
pass # older PyQt6 versions
|
|
206
237
|
self.resize(900, 600)
|
|
207
238
|
|
|
208
239
|
self._build_ui()
|
|
@@ -213,6 +244,8 @@ class WhiteBalanceDialog(QDialog):
|
|
|
213
244
|
self._update_mode_widgets()
|
|
214
245
|
# kick off a first detection preview
|
|
215
246
|
QTimer.singleShot(200, self._update_star_preview)
|
|
247
|
+
self.finished.connect(lambda *_: self._cleanup())
|
|
248
|
+
|
|
216
249
|
|
|
217
250
|
# ---- UI construction ------------------------------------------------
|
|
218
251
|
def _build_ui(self):
|
|
@@ -327,11 +360,16 @@ class WhiteBalanceDialog(QDialog):
|
|
|
327
360
|
)
|
|
328
361
|
self.star_count.setText(self.tr("Detected {0} stars.").format(count))
|
|
329
362
|
# to pixmap
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
363
|
+
overlay8 = np.ascontiguousarray(np.clip(overlay * 255.0, 0, 255).astype(np.uint8))
|
|
364
|
+
h, w, _ = overlay8.shape
|
|
365
|
+
qimg = QImage(overlay8.data, w, h, 3 * w, QImage.Format.Format_RGB888)
|
|
366
|
+
|
|
367
|
+
# Make sure the numpy buffer stays alive until QPixmap is created
|
|
368
|
+
pm = QPixmap.fromImage(qimg.copy()).scaled(
|
|
369
|
+
self.preview.width(), self.preview.height(),
|
|
370
|
+
Qt.AspectRatioMode.KeepAspectRatio,
|
|
371
|
+
Qt.TransformationMode.SmoothTransformation
|
|
372
|
+
)
|
|
335
373
|
self.preview.setPixmap(pm)
|
|
336
374
|
except Exception as e:
|
|
337
375
|
self.star_count.setText(self.tr("Detection failed."))
|
|
@@ -394,7 +432,8 @@ class WhiteBalanceDialog(QDialog):
|
|
|
394
432
|
# Use the headless helper so doc metadata is consistent
|
|
395
433
|
apply_white_balance_to_doc(self.doc, preset)
|
|
396
434
|
# Dialog stays open - refresh document for next operation
|
|
397
|
-
self.
|
|
435
|
+
self._finish_and_close()
|
|
436
|
+
return
|
|
398
437
|
|
|
399
438
|
elif mode == "Auto":
|
|
400
439
|
preset = {"mode": "auto"}
|
|
@@ -404,7 +443,8 @@ class WhiteBalanceDialog(QDialog):
|
|
|
404
443
|
|
|
405
444
|
apply_white_balance_to_doc(self.doc, preset)
|
|
406
445
|
# Dialog stays open - refresh document for next operation
|
|
407
|
-
self.
|
|
446
|
+
self._finish_and_close()
|
|
447
|
+
return
|
|
408
448
|
|
|
409
449
|
else: # --- Star-Based: compute here so we can plot like SASv2 ---
|
|
410
450
|
thr = float(self.thr_slider.value())
|
|
@@ -472,7 +512,8 @@ class WhiteBalanceDialog(QDialog):
|
|
|
472
512
|
self.tr("Star-Based WB applied.\nDetected {0} stars.").format(int(star_count)),
|
|
473
513
|
)
|
|
474
514
|
# Dialog stays open - refresh document for next operation
|
|
475
|
-
self.
|
|
515
|
+
self._finish_and_close()
|
|
516
|
+
return
|
|
476
517
|
|
|
477
518
|
except Exception as e:
|
|
478
519
|
QMessageBox.critical(self, self.tr("White Balance"), self.tr("Failed to apply White Balance:\n{0}").format(e))
|
|
@@ -490,3 +531,34 @@ class WhiteBalanceDialog(QDialog):
|
|
|
490
531
|
self.doc = new_doc
|
|
491
532
|
except Exception:
|
|
492
533
|
pass
|
|
534
|
+
|
|
535
|
+
def _finish_and_close(self):
|
|
536
|
+
"""
|
|
537
|
+
Close this dialog after a successful apply.
|
|
538
|
+
Use accept() so it behaves like a successful completion.
|
|
539
|
+
"""
|
|
540
|
+
try:
|
|
541
|
+
self.accept()
|
|
542
|
+
except Exception:
|
|
543
|
+
self.close()
|
|
544
|
+
|
|
545
|
+
def _cleanup(self):
|
|
546
|
+
# Disconnect active-doc signal so the main window doesn't keep us alive
|
|
547
|
+
try:
|
|
548
|
+
if self._active_doc_conn and hasattr(self._main, "currentDocumentChanged"):
|
|
549
|
+
self._main.currentDocumentChanged.disconnect(self._on_active_doc_changed)
|
|
550
|
+
except Exception:
|
|
551
|
+
pass
|
|
552
|
+
self._active_doc_conn = False
|
|
553
|
+
|
|
554
|
+
# Stop debounce timer
|
|
555
|
+
try:
|
|
556
|
+
if getattr(self, "_debounce", None) is not None:
|
|
557
|
+
self._debounce.stop()
|
|
558
|
+
except Exception:
|
|
559
|
+
pass
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def closeEvent(self, ev):
|
|
563
|
+
self._cleanup()
|
|
564
|
+
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}")
|