116 lines
4.3 KiB
Python
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())
|