Source code for pycsamt.app.desktop.__main__
# Author: LKouadio <etanoyau@gmail.com>
# License: LGPL-3.0
"""Entry point: python -m pycsamt.app.desktop"""
import os
# Several desktop features (AI Inversion, AI-processing agents --
# pycsamt.ai.* / pycsamt.agents.*) import torch/tensorflow/sklearn, and
# other desktop code paths (Occam2D forward modeling, curve fitting) use
# real scipy.optimize -- both in the same long-lived process. On
# Windows/conda that combination can abort the whole interpreter ("OMP:
# Error #15: Initializing libiomp5md.dll, but found ... already
# initialized") -- duplicate OpenMP runtime registration, not a bug in
# either library. Same root cause already documented and guarded against
# in every test conftest that imports both (see MEMORY.md); the desktop
# app itself never had this guard. Must be set before either is imported.
os.environ.setdefault("KMP_DUPLICATE_LIB_OK", "TRUE")
import sys
import warnings
[docs]
def main() -> None:
# PySide6 import deferred so the module is importable without Qt installed.
try:
from PySide6.QtCore import Qt, qInstallMessageHandler
from PySide6.QtWidgets import QApplication
except ImportError as exc:
print(
"PySide6 is required for the desktop app.\n"
"Install it with: pip install 'pycsamt[app]'",
file=sys.stderr,
)
raise SystemExit(1) from exc
from PySide6.QtGui import QIcon, QPixmap
from PySide6.QtWidgets import QSplashScreen
from pycsamt.app.desktop import branding
# Qt can emit one harmless invalid-point-size message while discovering
# the Windows system font, before QApplication lets us correct that font.
previous_qt_handler = None
def _qt_message_handler(msg_type, context, message):
if "QFont::setPointSize: Point size <= 0" in message:
return
if previous_qt_handler is not None:
previous_qt_handler(msg_type, context, message)
else:
print(message, file=sys.stderr)
previous_qt_handler = qInstallMessageHandler(_qt_message_handler)
app = QApplication(sys.argv)
# Some Windows themes expose only a pixel-sized default font. Ensure
# widgets never try to derive an invalid point size (-1) from it.
app_font = app.font()
if app_font.pointSize() <= 0:
app_font.setPointSize(10)
app.setFont(app_font)
# Matplotlib may report cosmetic tight-layout limitations asynchronously
# from the Qt event loop. Embedded canvases use explicit margins instead.
warnings.filterwarnings(
"ignore",
message=r"Tight layout not applied\..*",
category=UserWarning,
)
warnings.filterwarnings(
"ignore",
message=r"This figure includes Axes that are not compatible with tight_layout.*",
category=UserWarning,
)
# Same for constrained layout: a canvas drawn while its panel is still
# a few pixels wide (hidden window, collapsed splitter, first paint)
# cannot fit its axes; it lays out again at its real size.
warnings.filterwarnings(
"ignore",
message=r"constrained_layout not applied because axes sizes "
r"collapsed to zero.*",
category=UserWarning,
)
app.setApplicationName(branding.APP_NAME)
app.setApplicationVersion(branding.get_version())
app.setOrganizationName(branding.ORG_NAME)
app.setOrganizationDomain(branding.ORG_DOMAIN)
# Application icon — multi-resolution ICO
if branding.LOGO_ICO.exists():
app.setWindowIcon(QIcon(str(branding.LOGO_ICO)))
# ── Splash screen (Phase 12) ────────────────────────────────────────
# Shown before any heavy import (MainWindow pulls in matplotlib/pandas/
# every panel controller) so something appears on screen immediately
# instead of a multi-second blank delay after launch.
splash = None
if branding.SPLASH_IMAGE.exists():
pixmap = QPixmap(str(branding.SPLASH_IMAGE))
if not pixmap.isNull():
# The source PNG is a 1672x941 documentation banner image, not
# a splash-sized asset -- QSplashScreen renders a pixmap at its
# native size with no auto-scaling, so left as-is this covers
# most or all of the screen instead of looking like a normal
# small splash. Cap it to a sane splash width, scaled down
# (never up) and never wider than half the available screen.
max_width = 480
screen = app.primaryScreen()
if screen is not None:
max_width = min(max_width, int(screen.availableGeometry().width() * 0.5))
if pixmap.width() > max_width:
pixmap = pixmap.scaledToWidth(
max_width, Qt.TransformationMode.SmoothTransformation
)
splash = QSplashScreen(pixmap)
splash.show()
app.processEvents()
# ── License / trial gate (Phase 11) ─────────────────────────────────
# Hard block, per the product decision in the modernization plan's
# Section 4: MainWindow is never constructed without an active trial
# or a valid license.
from pycsamt.app.desktop.licensing import LicenseStatus, get_default_manager
manager = get_default_manager()
if manager.status() not in (LicenseStatus.TRIAL_ACTIVE, LicenseStatus.LICENSED):
if splash is not None:
splash.hide()
from pycsamt.app.desktop.dialogs.license_expired_dialog import (
LicenseExpiredDialog,
)
gate = LicenseExpiredDialog(manager)
if gate.exec() != gate.DialogCode.Accepted:
sys.exit(0)
if splash is not None:
splash.show()
app.processEvents()
# ── Preparing workspace (cosmetic step, not a dependency installer —
# a PyInstaller-frozen build ships every dependency already bundled;
# see the modernization plan's Guiding Principle 3) ─────────────────
if splash is not None:
splash.showMessage(
"Preparing workspace…",
alignment=Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter,
color=Qt.GlobalColor.white,
)
app.processEvents()
# Heavy import, deferred until after the splash/license gate above so
# the splash is what the user sees first, not a blank window.
from pycsamt.app.desktop.main_window import MainWindow
window = MainWindow()
window.show()
if splash is not None:
splash.finish(window)
sys.exit(app.exec())
if __name__ == "__main__":
main()