Files
2026-08-14 14:37:30 +08:00

831 lines
32 KiB
Python

"""Application composition root for the doctor workstation."""
from __future__ import annotations
import logging
import os
import sys
import time
from contextlib import suppress
from typing import Any
from PySide6.QtCore import QLibraryInfo, QLocale, QObject, Qt, QTimer, QTranslator
from PySide6.QtGui import QGuiApplication, QIcon
from PySide6.QtWidgets import (
QApplication,
QDialog,
QFrame,
QHBoxLayout,
QLabel,
QPushButton,
QVBoxLayout,
QWidget,
)
from doctor_workstation.config import AppConfig
from doctor_workstation.core import Session
from doctor_workstation.core.errors import AuthenticationExpiredError
from doctor_workstation.logging_setup import configure_logging
from doctor_workstation.resources import resource_path, video_dist_path
from doctor_workstation.services import (
DemoDoctorRepository,
RemoteDoctorRepository,
TokenStore,
build_repository,
)
from doctor_workstation.ui import LoginWindow, ShellWindow, apply_theme
from doctor_workstation.ui.widgets import (
friendly_error,
run_async,
set_authentication_expired_handler,
show_toast,
)
from doctor_workstation.video import BackendMode, launch_video_call
from doctor_workstation.video.window import WEBENGINE_AVAILABLE
LOGGER = logging.getLogger(__name__)
class _ChineseQtTranslator(QTranslator):
"""Guarantee Chinese labels for common Qt standard buttons.
Qt's packaged ``qtbase_zh_CN`` catalog remains the primary source for
framework text. This small fallback also keeps release builds localized
when a packager omits the optional ``.qm`` files.
"""
_BUTTON_TEXT = {
"OK": "确定",
"Open": "打开",
"Save": "保存",
"Save All": "全部保存",
"Cancel": "取消",
"Close": "关闭",
"Yes": "是",
"Yes to All": "全部确认",
"No": "否",
"No to All": "全部否定",
"Abort": "中止",
"Retry": "重试",
"Ignore": "忽略",
"Discard": "放弃",
"Help": "帮助",
"Apply": "应用",
"Reset": "重置",
"Restore Defaults": "恢复默认设置",
"Don't Save": "不保存",
}
def translate(
self,
context: str,
source_text: str,
disambiguation: str | None = None,
n: int = -1,
) -> str | None:
del context, disambiguation, n
# Returning an empty string tells Qt that an unknown source string has
# a valid, deliberately empty translation. That also erased internal
# values such as QPageSize's "A4", producing zero-sized PDF pages.
# ``None`` delegates unknown text to Qt's installed catalog/source.
return self._BUTTON_TEXT.get(source_text.replace("&", ""))
def _install_chinese_translations(application: QApplication) -> None:
"""Install Simplified Chinese Qt catalogs once for the whole process."""
if getattr(application, "_doctor_workstation_chinese_translators", None):
return
QLocale.setDefault(QLocale("zh_CN"))
translators: list[QTranslator] = []
translations_path = QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath)
for catalog in ("qt_zh_CN", "qtbase_zh_CN"):
translator = QTranslator(application)
if translator.load(catalog, translations_path):
application.installTranslator(translator)
translators.append(translator)
fallback = _ChineseQtTranslator(application)
application.installTranslator(fallback)
translators.append(fallback)
application._doctor_workstation_chinese_translators = translators # type: ignore[attr-defined]
class _UnconfiguredRepository:
"""Login boundary used until an administrator supplies a backend URL."""
def login(
self,
account: str,
password: str,
*,
remember_account: bool = False,
) -> Session:
del account, password, remember_account
raise ValueError("请先展开“服务器设置”,填写管理员提供的 HTTPS 接口地址。")
def get_current_user(self) -> None:
return None
class DemoVideoDialog(QDialog):
"""Non-network video-room preview used only by the explicit demo mode."""
def __init__(self, patient_name: str, parent: QWidget | None = None) -> None:
super().__init__(parent)
self._seconds = 0
self.setWindowTitle("视频面诊 · 演示模式")
self.setMinimumSize(760, 520)
self.resize(980, 660)
self.setModal(False)
self.setStyleSheet(
"QDialog{background:#F7F9FE;color:#111F46;}"
"QLabel{color:#111F46;}"
"QFrame#RemoteStage{background:#0E1421;border:1px solid #29334F;border-radius:16px;}"
"QFrame#RemoteStage QLabel{color:#F7F9FE;}"
"QFrame#LocalStage{background:#151D31;border:1px solid #3F4E75;border-radius:12px;}"
"QPushButton{min-width:96px;min-height:40px;padding:0 16px;border-radius:9px;"
"background:#FFFFFF;color:#3F4E75;border:1px solid #E6EAF5;font-weight:600;}"
"QPushButton:hover{color:#4451E2;background:#F0F2FF;border-color:#5761F4;}"
"QPushButton:checked{color:#FFFFFF;background:#5761F4;border-color:#5761F4;}"
"QPushButton#Hangup{color:#FFFFFF;background:#F15B67;border-color:#F15B67;}"
"QPushButton#Hangup:hover{background:#D94857;border-color:#D94857;}"
)
root = QVBoxLayout(self)
root.setContentsMargins(22, 18, 22, 22)
root.setSpacing(14)
header = QHBoxLayout()
title = QLabel(f"与 {patient_name or '患者'} 的视频面诊")
title.setStyleSheet("font-size:18px;font-weight:700;")
header.addWidget(title)
header.addStretch(1)
demo = QLabel("● 演示模式 · 未连接腾讯云")
demo.setStyleSheet("color:#7886AA;font-size:12px;")
header.addWidget(demo)
self.duration_label = QLabel("00:00")
self.duration_label.setStyleSheet("font-weight:700;")
header.addWidget(self.duration_label)
root.addLayout(header)
stage = QFrame()
stage.setObjectName("RemoteStage")
stage_layout = QVBoxLayout(stage)
stage_layout.setContentsMargins(22, 22, 22, 22)
stage_layout.addStretch(1)
avatar = QLabel((patient_name or "患")[:1])
avatar.setAlignment(Qt.AlignmentFlag.AlignCenter)
avatar.setFixedSize(104, 104)
avatar.setStyleSheet(
"background:#F0F2FF;color:#5761F4;border-radius:52px;font-size:42px;font-weight:700;"
)
stage_layout.addWidget(avatar, 0, Qt.AlignmentFlag.AlignHCenter)
waiting = QLabel("等待患者接听…")
waiting.setAlignment(Qt.AlignmentFlag.AlignCenter)
waiting.setStyleSheet("font-size:17px;font-weight:600;")
stage_layout.addWidget(waiting)
hint = QLabel("生产模式将通过后端短时 UserSig 初始化腾讯 TUICallKit")
hint.setAlignment(Qt.AlignmentFlag.AlignCenter)
hint.setStyleSheet("color:#A4ADC3;font-size:12px;")
stage_layout.addWidget(hint)
stage_layout.addStretch(1)
local = QFrame(stage)
local.setObjectName("LocalStage")
local.setGeometry(24, 24, 178, 112)
local_layout = QVBoxLayout(local)
local_label = QLabel("医生画面")
local_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
local_label.setStyleSheet("color:#D9E0F2;font-weight:600;")
local_layout.addWidget(local_label)
root.addWidget(stage, 1)
controls = QHBoxLayout()
controls.addStretch(1)
self.mic_button = QPushButton("麦克风 开")
self.mic_button.setCheckable(True)
self.mic_button.toggled.connect(
lambda muted: self.mic_button.setText("麦克风 关" if muted else "麦克风 开")
)
controls.addWidget(self.mic_button)
self.camera_button = QPushButton("摄像头 开")
self.camera_button.setCheckable(True)
self.camera_button.toggled.connect(
lambda off: self.camera_button.setText("摄像头 关" if off else "摄像头 开")
)
controls.addWidget(self.camera_button)
hangup = QPushButton("结束面诊")
hangup.setObjectName("Hangup")
hangup.clicked.connect(self.close)
controls.addWidget(hangup)
controls.addStretch(1)
root.addLayout(controls)
self._timer = QTimer(self)
self._timer.timeout.connect(self._tick)
self._timer.start(1000)
def _tick(self) -> None:
self._seconds += 1
minutes, seconds = divmod(self._seconds, 60)
self.duration_label.setText(f"{minutes:02d}:{seconds:02d}")
class ApplicationController(QObject):
"""Own windows, repositories and the authenticated application lifecycle."""
def __init__(self, application: QApplication, config: AppConfig) -> None:
super().__init__()
self.application = application
self.config = config
self.token_store = TokenStore(config.config_dir / "credentials.json")
self.demo_repository = DemoDoctorRepository()
self.remote_repository: RemoteDoctorRepository | None = None
self.login_window: LoginWindow | None = None
self.shell_window: ShellWindow | None = None
self.current_repository: Any = None
self.current_demo_mode = config.demo_mode
self.video_calls: dict[str, Any] = {}
self.video_pending: dict[str, object] = {}
self.demo_video_dialogs: dict[str, DemoVideoDialog] = {}
self._restore_generation = 0
self._restore_in_progress = False
self._restore_worker: Any = None
self._shutting_down = False
self._authentication_expiry_in_progress = False
self._rebuild_remote_repository()
set_authentication_expired_handler(self._on_authentication_expired)
application.aboutToQuit.connect(self.shutdown)
def start(self) -> None:
"""Show login immediately, then validate a production token off-thread."""
self._show_login()
self._begin_session_restore()
def _base_repository(self) -> Any:
return self.remote_repository or _UnconfiguredRepository()
def _show_login(self) -> None:
if self.login_window is None:
self.login_window = LoginWindow(
self._base_repository(),
self.config,
self.demo_repository,
credential_store=self.token_store,
)
self.login_window.login_succeeded.connect(self._on_login_succeeded)
self.login_window.config_changed.connect(self._on_config_changed)
self.login_window.demo_mode_changed.connect(self._on_demo_mode_changed)
self._apply_window_icon(self.login_window)
else:
self.login_window.repository = self._base_repository()
self.login_window.config = self.config
if not self.login_window.demo_check.isChecked():
self.login_window.active_repository = self._base_repository()
self.login_window.restore_remembered_credentials()
self.login_window.show()
self.login_window.raise_()
self.login_window.activateWindow()
def _on_config_changed(self, payload: Any) -> None:
previous_connection = (
self.config.api_base_url,
self.config.request_timeout,
self.config.verify_ssl,
)
if isinstance(payload, AppConfig):
updated = payload
elif isinstance(payload, dict):
aliases = {
"base_url": "api_base_url",
"read_timeout": "request_timeout",
}
changes = {aliases.get(key, key): value for key, value in payload.items()}
allowed = {
"api_base_url",
"demo_mode",
"video_mode",
"video_web_url",
"verify_ssl",
"request_timeout",
"log_level",
"remembered_account",
}
updated = self.config.with_updates(
**{key: value for key, value in changes.items() if key in allowed}
)
else:
return
self.config = updated
with suppress(OSError):
self.config.save_preferences()
next_connection = (
self.config.api_base_url,
self.config.request_timeout,
self.config.verify_ssl,
)
if next_connection != previous_connection:
self._cancel_session_restore()
self._rebuild_remote_repository()
if self.login_window is not None:
self.login_window.repository = self._base_repository()
if not self.login_window.demo_check.isChecked():
self.login_window.active_repository = self._base_repository()
if self.login_window is not None:
self.login_window.config = self.config
def _on_demo_mode_changed(self, enabled: bool) -> None:
self.current_demo_mode = enabled
if enabled:
self._cancel_session_restore()
def _rebuild_remote_repository(self) -> None:
old = self.remote_repository
self.remote_repository = None
if self.config.api_base_url:
repository = build_repository(
demo=False,
base_url=self.config.api_base_url,
token_store=self.token_store,
timeout=self.config.request_timeout,
verify=self.config.verify_ssl,
)
if isinstance(repository, RemoteDoctorRepository):
self.remote_repository = repository
if old is not None and old is not self.current_repository:
with suppress(Exception):
old.client.close()
def _begin_session_restore(self) -> None:
"""Validate a scoped persisted token without racing manual login."""
repository = self.remote_repository
if (
self._shutting_down
or self.config.demo_mode
or repository is None
or self.current_repository is not None
):
return
self._restore_generation += 1
generation = self._restore_generation
self._restore_in_progress = True
if self.login_window is not None:
self.login_window.set_session_restore_pending(True)
self._restore_worker = run_async(
repository.restore_session,
on_success=lambda session: self._on_restore_success(
session,
repository,
generation,
),
on_error=lambda error: self._on_restore_error(
error,
repository,
generation,
),
on_finished=lambda: self._on_restore_finished(repository, generation),
)
def _restore_is_current(
self,
repository: RemoteDoctorRepository,
generation: int,
) -> bool:
"""Return whether a restore callback still owns the login surface."""
return (
not self._shutting_down
and self._restore_in_progress
and generation == self._restore_generation
and repository is self.remote_repository
)
def _finish_session_restore(
self,
repository: RemoteDoctorRepository,
generation: int,
) -> bool:
"""Release login controls only for the currently active restore."""
if not self._restore_is_current(repository, generation):
return False
self._restore_in_progress = False
self._restore_worker = None
if self.login_window is not None:
self.login_window.set_session_restore_pending(False)
return True
def _on_restore_success(
self,
session: object,
repository: RemoteDoctorRepository,
generation: int,
) -> None:
"""Enter the shell only for a current, fully validated session."""
if not self._finish_session_restore(repository, generation):
return
if session is None:
return
if not isinstance(session, Session) or not session.authenticated:
with suppress(Exception):
repository.logout()
self._login_guard_error("已保存的登录状态无效,请重新登录。")
return
self._on_login_succeeded(
{
"session": session,
"user": session.user,
"repository": repository,
"demo_mode": False,
"restored_session": True,
}
)
def _on_restore_error(
self,
error: Exception,
repository: RemoteDoctorRepository,
generation: int,
) -> None:
"""Return control to login after a current restore attempt fails."""
if not self._finish_session_restore(repository, generation):
return
if isinstance(error, AuthenticationExpiredError):
message = "登录状态已失效,请重新登录。"
else:
message = f"自动恢复登录失败:{friendly_error(error)}"
self._login_guard_error(message)
def _on_restore_finished(
self,
repository: RemoteDoctorRepository,
generation: int,
) -> None:
"""Release controls when a worker finishes without a result callback."""
self._finish_session_restore(repository, generation)
def _cancel_session_restore(self) -> None:
"""Invalidate late callbacks and re-enable manual login controls."""
self._restore_generation += 1
if not self._restore_in_progress:
return
self._restore_in_progress = False
self._restore_worker = None
if self.login_window is not None:
self.login_window.set_session_restore_pending(False)
def _on_login_succeeded(self, payload: dict[str, Any]) -> None:
self._cancel_session_restore()
session = payload.get("session")
repository = payload.get("repository")
if not isinstance(session, Session) or repository is None:
self._login_guard_error("登录响应不完整,请重试。")
return
if session.password_change_required:
with suppress(Exception):
repository.logout()
self._login_guard_error("该账号需要先修改初始密码,请在管理后台完成后重新登录。")
return
if session.work_wechat_binding_required:
with suppress(Exception):
repository.logout()
self._login_guard_error("该账号需要先绑定企业微信,请在管理后台完成绑定后重新登录。")
return
demo_mode = bool(payload.get("demo_mode"))
if not demo_mode and not session.menu:
with suppress(Exception):
repository.logout()
self._login_guard_error("当前账号没有可用医生端菜单,请联系管理员授权。")
return
self.current_repository = repository
self._authentication_expiry_in_progress = False
self.current_demo_mode = demo_mode
self.shell_window = ShellWindow(repository, payload, session.permissions)
self.shell_window.logout_requested.connect(self._logout)
self.shell_window.video_requested.connect(self._request_video)
self._apply_window_icon(self.shell_window)
if self.login_window is not None:
self.login_window.hide()
self.shell_window.show()
self.shell_window.raise_()
self.shell_window.activateWindow()
def _login_guard_error(self, message: str) -> None:
if self.login_window is not None:
self.login_window.error_banner.show_message(message, "warning")
self.login_window.show()
def _on_authentication_expired(self, error: AuthenticationExpiredError) -> bool:
"""Consume an active-shell expiry and atomically return to login."""
if self.shell_window is None or self.current_repository is None:
return False
if self._authentication_expiry_in_progress:
return True
self._authentication_expiry_in_progress = True
LOGGER.info(
"authenticated session expired",
extra={"api_code": error.code, "request_id": error.request_id},
)
self._logout(message="登录状态已失效,请重新登录。")
return True
def _logout(self, *, message: str = "") -> None:
"""Clear authenticated resources and return to the login window."""
calls = tuple(self.video_calls.values())
for call in calls:
with suppress(Exception):
call.close()
self._wait_for_video_lifecycle(calls, timeout=1.25)
self.video_calls.clear()
self.video_pending.clear()
for dialog in self.demo_video_dialogs.values():
dialog.close()
self.demo_video_dialogs.clear()
if self.current_repository is not None:
with suppress(Exception):
self.current_repository.logout()
self.current_repository = None
if self.shell_window is not None:
self.shell_window.close()
self.shell_window.deleteLater()
self.shell_window = None
self._show_login()
if message and self.login_window is not None:
self.login_window.error_banner.show_message(message, "warning")
def _request_video(self, payload: dict[str, Any]) -> None:
parent = self.shell_window
if parent is None or self.current_repository is None:
return
patient_id = payload.get("patient_id")
diagnosis_id = payload.get("diagnosis_id")
patient_name = str(payload.get("patient_name") or "患者")
open_im = str(payload.get("mode") or "video").lower() == "im"
if patient_id in (None, "") or diagnosis_id in (None, ""):
show_toast(parent, "患者或诊单信息不完整,无法发起视频。", "danger", 4200)
return
call_key = str(diagnosis_id)
existing_call = self.video_calls.get(call_key)
if open_im and existing_call is not None and getattr(existing_call, "open_im", False):
qt_window = getattr(existing_call, "qt_window", None)
if qt_window is not None:
qt_window.show()
qt_window.raise_()
qt_window.activateWindow()
show_toast(parent, "该患者的 IM 会话已经打开。", "info", 3200)
return
if (
call_key in self.video_pending
or existing_call is not None
or call_key in self.demo_video_dialogs
):
show_toast(parent, "该问诊的视频正在准备或通话中。", "info", 3600)
return
closed_previous_im = False
if open_im:
for key, call in tuple(self.video_calls.items()):
if key == call_key or not getattr(call, "open_im", False):
continue
closed_previous_im = True
self.video_calls.pop(key, None)
with suppress(Exception):
call.close()
if self.current_demo_mode:
dialog = DemoVideoDialog(patient_name, parent)
dialog.finished.connect(
lambda _result, key=call_key, item=dialog: self._forget_demo_dialog(
key,
item,
)
)
self.demo_video_dialogs[call_key] = dialog
dialog.show()
return
show_toast(
parent,
"正在打开患者 IM 会话…" if open_im else "正在获取安全通话凭证…",
"info",
)
repository = self.current_repository
marker = object()
self.video_pending[call_key] = marker
def get_ticket() -> Any:
return repository.get_call_ticket(
patient_id=int(patient_id),
diagnosis_id=int(diagnosis_id),
)
def request_ticket() -> None:
if self.video_pending.get(call_key) is not marker:
return
run_async(
get_ticket,
on_success=lambda ticket: self._launch_video(
ticket,
diagnosis_id=diagnosis_id,
patient_id=patient_id,
repository=repository,
call_key=call_key,
marker=marker,
open_im=open_im,
patient_name=patient_name,
),
on_error=lambda error: self._video_ticket_error(
call_key,
marker,
parent,
error,
),
)
# Tencent IM may take a brief moment to release the previous browser
# connection. The admin version also has only one ChatDialog instance.
if closed_previous_im:
QTimer.singleShot(400, request_ticket)
else:
request_ticket()
def _video_ticket_error(
self,
call_key: str,
marker: object,
parent: QWidget,
error: Exception,
) -> None:
if self.video_pending.get(call_key) is not marker:
return
self.video_pending.pop(call_key, None)
if self.shell_window is parent:
show_toast(
parent,
f"视频准备失败:{friendly_error(error)}",
"danger",
5200,
)
def _launch_video(
self,
ticket: Any,
*,
diagnosis_id: Any,
patient_id: Any,
repository: Any,
call_key: str,
marker: object,
open_im: bool = False,
patient_name: str = "患者",
) -> None:
if self.video_pending.get(call_key) is not marker:
return
self.video_pending.pop(call_key, None)
if (
self.shell_window is None
or self.current_repository is not repository
or call_key in self.video_calls
):
return
try:
mode = BackendMode.parse(self.config.video_mode)
if mode is BackendMode.BROWSER:
raise ValueError("当前后端未提供一次性通话交接票据,浏览器视频模式已安全停用。")
if not WEBENGINE_AVAILABLE:
raise ValueError("当前安装缺少 QtWebEngine,无法打开受信任的视频窗口。")
call = launch_video_call(
ticket,
repository=repository,
diagnosis_id=diagnosis_id,
patient_id=patient_id,
backend_mode=mode,
local_dist=video_dist_path(),
remote_url=self.config.video_web_url or None,
logger=logging.getLogger("doctor_workstation.video"),
open_im=open_im,
patient_name=patient_name,
)
except Exception as error:
LOGGER.exception("video call could not be launched")
show_toast(
self.shell_window,
f"视频启动失败:{friendly_error(error)}",
"danger",
5600,
)
return
self.video_calls[call_key] = call
qt_window = getattr(call, "qt_window", None)
if qt_window is not None:
qt_window.destroyed.connect(
lambda _obj=None, key=call_key, expected=call: self._release_video_call(
key,
expected,
)
)
def _release_video_call(self, call_key: str, call: Any) -> None:
if self.video_calls.get(call_key) is call:
self.video_calls.pop(call_key, None)
def _forget_demo_dialog(self, call_key: str, dialog: DemoVideoDialog) -> None:
if self.demo_video_dialogs.get(call_key) is dialog:
self.demo_video_dialogs.pop(call_key, None)
dialog.deleteLater()
@staticmethod
def _wait_for_video_lifecycle(calls: tuple[Any, ...], *, timeout: float) -> bool:
"""Give queued endCall writes one short shared deadline after media closes."""
deadline = time.monotonic() + max(0.0, timeout)
complete = True
for call in calls:
remaining = deadline - time.monotonic()
if remaining <= 0:
complete = False
break
wait = getattr(call, "wait_for_lifecycle", None)
if callable(wait) and not wait(remaining):
complete = False
if not complete:
LOGGER.warning("video lifecycle cleanup exceeded its bounded deadline")
return complete
@staticmethod
def _apply_window_icon(window: QWidget) -> None:
icon_file = resource_path("icon.svg")
if icon_file.exists():
window.setWindowIcon(QIcon(str(icon_file)))
def shutdown(self) -> None:
"""Invalidate asynchronous restoration and release owned resources."""
if self._shutting_down:
return
self._shutting_down = True
self._cancel_session_restore()
set_authentication_expired_handler(None)
calls = tuple(self.video_calls.values())
for call in calls:
with suppress(Exception):
call.close()
self._wait_for_video_lifecycle(calls, timeout=1.25)
if self.remote_repository is not None:
with suppress(Exception):
self.remote_repository.client.close()
def _create_application(argv: list[str]) -> QApplication:
with suppress(AttributeError):
QGuiApplication.setHighDpiScaleFactorRoundingPolicy(
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
)
with suppress(AttributeError):
QApplication.setAttribute(Qt.ApplicationAttribute.AA_DontCreateNativeWidgetSiblings, True)
application = QApplication(argv)
_install_chinese_translations(application)
application.setApplicationName("甄养堂医生工作站")
application.setApplicationDisplayName("甄养堂医生工作站")
application.setOrganizationName("ZhenYangTang")
application.setOrganizationDomain("zhenyangtang.com")
application.setQuitOnLastWindowClosed(True)
icon_file = resource_path("icon.svg")
if icon_file.exists():
application.setWindowIcon(QIcon(str(icon_file)))
apply_theme(application)
return application
def main(argv: list[str] | None = None) -> int:
"""Start the GUI application and return its process exit code."""
config = AppConfig.load()
configure_logging(config.log_dir, config.log_level)
LOGGER.info("doctor workstation starting", extra={"demo_mode": config.demo_mode})
raw_argv = list(sys.argv if argv is None else argv)
smoke_test = "--smoke-test" in raw_argv
application = _create_application(
[argument for argument in raw_argv if argument != "--smoke-test"]
)
controller = ApplicationController(application, config)
controller.start()
if smoke_test or os.getenv("DOCTOR_SMOKE_TEST") == "1":
QTimer.singleShot(1200, application.quit)
return application.exec()
__all__ = ["ApplicationController", "DemoVideoDialog", "main"]