"""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())