Source code for qtframework.core.application

"""Application core module."""

from __future__ import annotations

import sys
from collections.abc import Callable
from typing import TYPE_CHECKING, Any, cast

from PySide6.QtCore import QMessageLogContext, QSettings, QtMsgType, Signal, qInstallMessageHandler
from PySide6.QtGui import QGuiApplication, QPalette
from PySide6.QtWidgets import QApplication

from qtframework.core.context import Context
from qtframework.themes import ThemeManager
from qtframework.utils.logger import get_logger
from qtframework.utils.resources import ResourceManager


if TYPE_CHECKING:
    from qtframework.core.window import BaseWindow

logger = get_logger(__name__)
_MessageHandler = Callable[[QtMsgType, QMessageLogContext, str], None]


_original_qt_message_handler: _MessageHandler | None = None


def _qt_message_filter(mode: QtMsgType, context: QMessageLogContext, message: str) -> None:
    """Filter Qt messages to suppress stylesheet parsing warnings.

    Args:
        mode: Message type (debug, warning, critical, etc.)
        context: Context information about the message
        message: The log message
    """
    if message != "Could not parse application stylesheet" and _original_qt_message_handler:
        _original_qt_message_handler(mode, context, message)


[docs] class Application(QApplication): """Enhanced QApplication with built-in theme and context management.""" theme_changed = Signal(str) context_changed = Signal() def __init__( self, argv: list[str] | None = None, *, app_name: str = "QtFrameworkApp", org_name: str = "QtFramework", org_domain: str = "qtframework.local", resource_manager: ResourceManager | None = None, excluded_builtin_themes: list[str] | None = None, excluded_themes: list[str] | None = None, included_themes: list[str] | None = None, include_auto_theme: bool = True, ) -> None: """Initialize the application. Args: argv: Command line arguments app_name: Application name org_name: Organization name org_domain: Organization domain resource_manager: Optional custom resource manager for themes/icons/translations excluded_builtin_themes: List of built-in theme names to exclude from loading excluded_themes: List of custom theme names to exclude from loading included_themes: If specified, ONLY load these built-in framework themes (custom themes always load) include_auto_theme: Whether to include 'auto' theme in theme list (default: True) """ super().__init__(argv or sys.argv) self.setApplicationName(app_name) self.setOrganizationName(org_name) self.setOrganizationDomain(org_domain) self._context = Context() self._resource_manager = resource_manager or ResourceManager() self._theme_manager = ThemeManager( resource_manager=self._resource_manager, excluded_builtin_themes=excluded_builtin_themes, excluded_themes=excluded_themes, included_themes=included_themes, include_auto_theme=include_auto_theme, ) self._current_stylesheet: str = "" self._settings = QSettings() self._windows: list[BaseWindow] = [] self._initialize() def _install_stylesheet_warning_filter(self) -> None: """Install Qt message handler to filter stylesheet warnings.""" global _original_qt_message_handler if _original_qt_message_handler is None: handler = qInstallMessageHandler(_qt_message_filter) _original_qt_message_handler = cast("_MessageHandler", handler) def _initialize(self) -> None: """Initialize application components.""" logger.info(f"Initializing {self.applicationName()}") self._install_stylesheet_warning_filter() self._load_settings() self._setup_theme() self._connect_signals() logger.info("Application initialized successfully") def _load_settings(self) -> None: """Load application settings.""" saved_theme = self._settings.value("theme", None) if saved_theme: theme = str(saved_theme) # Verify theme exists, otherwise use first available available_themes = self._theme_manager.list_themes() if theme not in available_themes and theme not in self._theme_manager._themes: logger.warning(f"Saved theme '{theme}' not available, using default") theme = self._theme_manager._current_theme_name else: # Try to detect OS theme, fallback to default if not available detected = self._detect_os_theme() if detected in self._theme_manager._themes or detected == "auto": theme = detected else: theme = self._theme_manager._current_theme_name self._context.set("theme", theme) def _setup_theme(self) -> None: """Setup initial theme.""" theme_name = self._context.get("theme", self._theme_manager._current_theme_name) if not self._theme_manager.set_theme(theme_name): # Fallback to current theme if requested theme not available logger.warning(f"Could not set theme '{theme_name}', using fallback") theme_name = self._theme_manager._current_theme_name self._theme_manager.set_theme(theme_name) self._apply_stylesheet(self._theme_manager.get_stylesheet()) def _apply_stylesheet(self, stylesheet: str | None) -> None: """Apply stylesheet if it changed to avoid redundant parsing.""" if stylesheet is None: stylesheet = "" if stylesheet == self._current_stylesheet: return self.setStyleSheet(stylesheet) self._current_stylesheet = self.styleSheet() def _connect_signals(self) -> None: """Connect internal signals.""" self._theme_manager.theme_changed.connect(self._on_theme_changed) self.aboutToQuit.connect(self._on_about_to_quit) def _on_theme_changed(self, theme_name: str) -> None: """Handle theme change. Args: theme_name: New theme name """ logger.info("Changing theme to: %s", theme_name) self._context.set("theme", theme_name) self._apply_stylesheet(self._theme_manager.get_stylesheet()) self.theme_changed.emit(theme_name) def _on_about_to_quit(self) -> None: """Handle application quit.""" logger.info("Application shutting down") self._save_settings() def _save_settings(self) -> None: """Save application settings.""" self._settings.setValue("theme", self._context.get("theme", "light")) self._settings.sync() def _detect_os_theme(self) -> str: """Detect OS dark mode preference. Returns: "dark" if OS is in dark mode, "light" otherwise """ # Qt 6.5+ supports direct color scheme detection try: style_hints = QGuiApplication.styleHints() if hasattr(style_hints, "colorScheme"): from PySide6.QtCore import Qt if style_hints.colorScheme() == Qt.ColorScheme.Dark: logger.info("OS dark mode detected via styleHints") return "dark" except (AttributeError, ImportError): pass # Fallback: Infer from palette luminance palette = QGuiApplication.palette() window_color = palette.color(QPalette.ColorRole.Window) luminance = ( 0.299 * window_color.red() + 0.587 * window_color.green() + 0.114 * window_color.blue() ) / 255 if luminance < 0.5: logger.info("Dark theme detected via palette luminance") return "dark" logger.info("Light theme detected") return "light" @property def context(self) -> Context: """Get application context.""" return self._context @property def theme_manager(self) -> ThemeManager: """Get theme manager.""" return self._theme_manager @property def resource_manager(self) -> ResourceManager: """Get resource manager.""" return self._resource_manager @property def settings(self) -> QSettings: """Get settings object.""" return self._settings
[docs] def register_window(self, window: BaseWindow) -> None: """Register a window with the application. Args: window: Window to register """ if window not in self._windows: self._windows.append(window) logger.debug(f"Registered window: {window.windowTitle()}")
[docs] def unregister_window(self, window: BaseWindow) -> None: """Unregister a window from the application. Args: window: Window to unregister """ if window in self._windows: self._windows.remove(window) logger.debug(f"Unregistered window: {window.windowTitle()}")
[docs] @staticmethod def exec() -> int: """Execute the application.""" app = QApplication.instance() if isinstance(app, Application): logger.info(f"Starting {app.applicationName()}") return int(QApplication.exec())
[docs] @classmethod def create_and_run( cls, window_class: type[BaseWindow], *, argv: list[str] | None = None, app_name: str = "QtFrameworkApp", org_name: str = "QtFramework", org_domain: str = "qtframework.local", excluded_builtin_themes: list[str] | None = None, excluded_themes: list[str] | None = None, included_themes: list[str] | None = None, include_auto_theme: bool = True, **window_kwargs: Any, ) -> int: """Create and run application with main window. Args: window_class: Main window class argv: Command line arguments app_name: Application name org_name: Organization name org_domain: Organization domain excluded_builtin_themes: List of built-in theme names to exclude from loading excluded_themes: List of custom theme names to exclude from loading included_themes: If specified, ONLY load these built-in framework themes (custom themes always load) include_auto_theme: Whether to include 'auto' theme in theme list (default: True) **window_kwargs: Keyword arguments for window Returns: Application exit code """ app = cls( argv=argv, app_name=app_name, org_name=org_name, org_domain=org_domain, excluded_builtin_themes=excluded_builtin_themes, excluded_themes=excluded_themes, included_themes=included_themes, include_auto_theme=include_auto_theme, ) window = window_class(application=app, **window_kwargs) window.show() return app.exec()