Files
orisys-app/apps/studio/src/orisys_studio/main.py
T
2026-09-27 19:20:43 +08:00

116 lines
4.3 KiB
Python

"""Studio app entrypoint."""
from __future__ import annotations
import logging
import os
import sys
# pyqtgraph picks its toolkit from whichever binding is already imported, and in a
# development environment that also has PyQt5 installed (``orisys[gui]``) it can pick the
# wrong one. The application is PySide6-only, so say so before anything imports pyqtgraph.
os.environ.setdefault("PYQTGRAPH_QT_LIB", "PySide6")
from orisys_app_shared import API_VERSION
from orisys_app_shared import __version__ as shared_version
from orisys_app_shared.compat import check_shared_compat
from orisys_app_shared.i18n import set_locale
from orisys_app_shared.logging import init_logging, install_exception_hooks
from orisys_app_shared.paths import prefer_local_sensor_bundles
from orisys_app_shared.prefs import load_preferences
from orisys_app_shared.theme import build_qss
from PySide6.QtWidgets import QApplication # noqa: E402
from orisys_studio import REQUIRES_SHARED_API
from orisys_studio import __version__ as app_version
from orisys_studio.app_window import StudioMainWindow
logger = logging.getLogger(__name__)
#: The GL profiles the 3D view can be started with. One per process: the surface
#: format is a start-up decision, so it is read from the preferences here rather
#: than switched at runtime (design document §11.1).
GL_PROFILES = ("desktop33", "gles30")
DEFAULT_GL_PROFILE = "desktop33"
def _configure_gl(prefs: dict) -> str:
"""Select the default GL surface format. Must run before ``QApplication``.
Optional and failure-tolerant on purpose: a machine without PyOpenGL, or with a
preference that names a profile this build does not know, still gets the whole
two-dimensional application. The 3D pane reports its own state; a start-up that
refused to run because a picture might not appear would be the worse bug.
"""
profile = str(prefs.get("gl_profile") or DEFAULT_GL_PROFILE)
if profile not in GL_PROFILES:
logger.warning("Unknown gl_profile %r; using %s", profile, DEFAULT_GL_PROFILE)
profile = DEFAULT_GL_PROFILE
try:
from orisys_studio.scene3d.gl_backend import configure_surface_format
configure_surface_format(profile) # type: ignore[arg-type]
except Exception: # noqa: BLE001 - 3D is optional, the window is not
logger.exception("Could not configure the OpenGL surface format")
return profile
logger.info("OpenGL surface format requested: profile=%s", profile)
return profile
def main() -> int:
init_logging("orisys-studio")
install_exception_hooks()
preferred = prefer_local_sensor_bundles()
if preferred is not None:
logger.info("Preferring local sensor bundles at %s", preferred)
check_shared_compat(REQUIRES_SHARED_API)
logger.info(
"Versions: app=%s shared=%s api=%s (requires %s)",
app_version,
shared_version,
API_VERSION,
REQUIRES_SHARED_API,
)
prefs = load_preferences()
language = str(prefs.get("language") or "zh_CN")
theme = str(prefs.get("theme") or "dark")
set_locale(language)
gl_profile = _configure_gl(prefs)
app = QApplication(sys.argv)
app.setApplicationName("Orisys Studio")
app.setApplicationVersion(app_version)
app.setStyleSheet(build_qss(theme))
window = StudioMainWindow(prefs=prefs)
window.show()
logger.info(
"Studio shell started (theme=%s, language=%s, gl_profile=%s)",
theme,
language,
gl_profile,
)
code = int(app.exec())
# Leave the process immediately.
#
# The device SDK and OpenCV keep *native* threads and handles of their own;
# a normal return would run interpreter shutdown on top of them, and the
# process then sat there after the window closed ("the console never came
# back"). Everything that must be durable was already flushed by
# ``StudioMainWindow.closeEvent`` and by the log handlers below, so the only
# thing skipped here is teardown of libraries that no longer have anything
# to write.
logging.shutdown()
for stream in (sys.stdout, sys.stderr):
try:
stream.flush()
except Exception: # noqa: BLE001 - a closed stream must not block exit
pass
os._exit(code)
if __name__ == "__main__":
raise SystemExit(main())