qyro-engine 1.0.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qyro/__init__.py +71 -0
- qyro/adapters/__init__.py +22 -0
- qyro/adapters/environment/__init__.py +3 -0
- qyro/adapters/environment/system_environment.py +57 -0
- qyro/adapters/frameworks/__init__.py +21 -0
- qyro/adapters/frameworks/base.py +20 -0
- qyro/adapters/frameworks/factory.py +62 -0
- qyro/adapters/frameworks/headless_adapter.py +57 -0
- qyro/adapters/frameworks/kivy_adapter.py +79 -0
- qyro/adapters/frameworks/pyqt5_adapter.py +23 -0
- qyro/adapters/frameworks/pyqt6_adapter.py +35 -0
- qyro/adapters/frameworks/pyside2_adapter.py +28 -0
- qyro/adapters/frameworks/pyside6_adapter.py +35 -0
- qyro/adapters/frameworks/qt_base.py +107 -0
- qyro/adapters/frameworks/tkinter_adapter.py +80 -0
- qyro/adapters/platform/__init__.py +3 -0
- qyro/adapters/platform/detector.py +73 -0
- qyro/adapters/resources/__init__.py +3 -0
- qyro/adapters/resources/filesystem_resources.py +120 -0
- qyro/adapters/settings/__init__.py +3 -0
- qyro/adapters/settings/json_settings.py +86 -0
- qyro/adapters/telemetry/__init__.py +4 -0
- qyro/adapters/telemetry/console_hook.py +38 -0
- qyro/adapters/telemetry/sentry_hook.py +51 -0
- qyro/application/__init__.py +28 -0
- qyro/application/ports/__init__.py +18 -0
- qyro/application/ports/environment.py +37 -0
- qyro/application/ports/resources.py +23 -0
- qyro/application/ports/settings.py +22 -0
- qyro/application/ports/telemetry.py +27 -0
- qyro/application/ports/ui_framework.py +49 -0
- qyro/application/use_cases/__init__.py +13 -0
- qyro/application/use_cases/initialize_engine.py +52 -0
- qyro/application/use_cases/load_settings.py +25 -0
- qyro/application/use_cases/resolve_resource.py +30 -0
- qyro/client/__init__.py +3 -0
- qyro/client/component.py +529 -0
- qyro/client/context.py +510 -0
- qyro/container.py +79 -0
- qyro/domain/__init__.py +33 -0
- qyro/domain/entities.py +83 -0
- qyro/domain/errors.py +49 -0
- qyro/ui/__init__.py +8 -0
- qyro/ui/component.py +8 -0
- qyro_engine-1.0.0a1.dist-info/LICENSE +21 -0
- qyro_engine-1.0.0a1.dist-info/METADATA +477 -0
- qyro_engine-1.0.0a1.dist-info/NOTICE +159 -0
- qyro_engine-1.0.0a1.dist-info/RECORD +49 -0
- qyro_engine-1.0.0a1.dist-info/WHEEL +4 -0
qyro/__init__.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Qyro.
|
|
3
|
+
Clean Architecture runtime engine and lifecycle framework for Python desktop GUI apps.
|
|
4
|
+
Decoupled from CLI tools, independent and multi-toolkit ready.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from qyro.client.context import ApplicationContext
|
|
8
|
+
from qyro.client.component import Component
|
|
9
|
+
from qyro.container import EngineContainer
|
|
10
|
+
from qyro.adapters.platform.detector import PlatformDetector
|
|
11
|
+
from qyro.domain.entities import PlatformType, ExecutionMode, AppMetadata
|
|
12
|
+
from qyro.domain.errors import (
|
|
13
|
+
QyroEngineError,
|
|
14
|
+
ResourceNotFoundError,
|
|
15
|
+
SettingsNotFoundError,
|
|
16
|
+
FrameworkNotAvailableError,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__version__ = "0.1.0"
|
|
20
|
+
|
|
21
|
+
# Module-level singleton container for quick procedural usage
|
|
22
|
+
_default_container: EngineContainer | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _get_default_container() -> EngineContainer:
|
|
26
|
+
global _default_container
|
|
27
|
+
app_container = ApplicationContext._global_container
|
|
28
|
+
if app_container is not None:
|
|
29
|
+
_default_container = app_container
|
|
30
|
+
return app_container
|
|
31
|
+
|
|
32
|
+
if _default_container is None:
|
|
33
|
+
_default_container = EngineContainer()
|
|
34
|
+
return _default_container
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_resource(*segments: str, required: bool = True) -> str:
|
|
38
|
+
"""Convenience helper to resolve resource path without manually creating ApplicationContext."""
|
|
39
|
+
container = _get_default_container()
|
|
40
|
+
return str(container.resolve_resource_use_case.execute(*segments, required=required))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def load_build_settings() -> dict:
|
|
44
|
+
"""Convenience helper to load build settings dictionary."""
|
|
45
|
+
container = _get_default_container()
|
|
46
|
+
return container.load_settings_use_case.execute().raw_settings
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def is_frozen() -> bool:
|
|
50
|
+
"""Convenience helper to check if running in a frozen executable."""
|
|
51
|
+
container = _get_default_container()
|
|
52
|
+
return container.env_adapter.is_frozen()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
"ApplicationContext",
|
|
58
|
+
"Component",
|
|
59
|
+
"EngineContainer",
|
|
60
|
+
"PlatformDetector",
|
|
61
|
+
"PlatformType",
|
|
62
|
+
"ExecutionMode",
|
|
63
|
+
"AppMetadata",
|
|
64
|
+
"QyroEngineError",
|
|
65
|
+
"ResourceNotFoundError",
|
|
66
|
+
"SettingsNotFoundError",
|
|
67
|
+
"FrameworkNotAvailableError",
|
|
68
|
+
"get_resource",
|
|
69
|
+
"load_build_settings",
|
|
70
|
+
"is_frozen",
|
|
71
|
+
]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Qyro - Adapters Layer.
|
|
3
|
+
Concrete implementations of application ports.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .environment.system_environment import SystemEnvironmentAdapter
|
|
7
|
+
from .settings.json_settings import JsonSettingsAdapter
|
|
8
|
+
from .resources.filesystem_resources import FileSystemResourceAdapter
|
|
9
|
+
from .telemetry.console_hook import ConsoleExceptionHookAdapter
|
|
10
|
+
from .telemetry.sentry_hook import SentryExceptionHookAdapter
|
|
11
|
+
from .platform.detector import PlatformDetector
|
|
12
|
+
from .frameworks.factory import FrameworkFactory
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"SystemEnvironmentAdapter",
|
|
16
|
+
"JsonSettingsAdapter",
|
|
17
|
+
"FileSystemResourceAdapter",
|
|
18
|
+
"ConsoleExceptionHookAdapter",
|
|
19
|
+
"SentryExceptionHookAdapter",
|
|
20
|
+
"PlatformDetector",
|
|
21
|
+
"FrameworkFactory",
|
|
22
|
+
]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
System Environment Adapter.
|
|
3
|
+
Implements IEnvironmentPort for PyInstaller, cx_Freeze, and Source environments.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from qyro.application.ports.environment import IEnvironmentPort
|
|
9
|
+
from qyro.domain.entities import ExecutionMode, PlatformType
|
|
10
|
+
from qyro.adapters.platform.detector import PlatformDetector
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SystemEnvironmentAdapter(IEnvironmentPort):
|
|
14
|
+
"""Detects runtime execution specifics across operating systems."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, custom_root: Path | None = None) -> None:
|
|
17
|
+
self._custom_root = custom_root
|
|
18
|
+
|
|
19
|
+
def is_frozen(self) -> bool:
|
|
20
|
+
return getattr(sys, "frozen", False)
|
|
21
|
+
|
|
22
|
+
def get_execution_mode(self) -> ExecutionMode:
|
|
23
|
+
if self.is_frozen():
|
|
24
|
+
if hasattr(sys, "_MEIPASS"):
|
|
25
|
+
return ExecutionMode.FROZEN_PYINSTALLER
|
|
26
|
+
return ExecutionMode.FROZEN_GENERIC
|
|
27
|
+
return ExecutionMode.SOURCE
|
|
28
|
+
|
|
29
|
+
def get_platform(self) -> PlatformType:
|
|
30
|
+
return PlatformDetector.get_platform()
|
|
31
|
+
|
|
32
|
+
def get_root_dir(self) -> Path:
|
|
33
|
+
if self._custom_root:
|
|
34
|
+
return self._custom_root
|
|
35
|
+
|
|
36
|
+
if self.is_frozen():
|
|
37
|
+
# Executable directory
|
|
38
|
+
return Path(sys.executable).resolve().parent
|
|
39
|
+
|
|
40
|
+
# Source mode: find project root containing pyproject.toml or src/
|
|
41
|
+
cwd = Path.cwd()
|
|
42
|
+
for parent in [cwd] + list(cwd.parents):
|
|
43
|
+
if (parent / "pyproject.toml").exists() or (parent / "src").exists():
|
|
44
|
+
return parent
|
|
45
|
+
return cwd
|
|
46
|
+
|
|
47
|
+
def get_bundle_dir(self) -> Path:
|
|
48
|
+
if self.is_frozen() and hasattr(sys, "_MEIPASS"):
|
|
49
|
+
return Path(getattr(sys, "_MEIPASS")).resolve()
|
|
50
|
+
|
|
51
|
+
# macOS bundle check
|
|
52
|
+
if self.is_frozen() and sys.platform == "darwin":
|
|
53
|
+
mac_resources = Path(sys.executable).resolve().parent.parent / "Resources"
|
|
54
|
+
if mac_resources.exists():
|
|
55
|
+
return mac_resources
|
|
56
|
+
|
|
57
|
+
return self.get_root_dir()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from .base import BaseUIFrameworkAdapter
|
|
2
|
+
from .qt_base import BaseQtAdapter
|
|
3
|
+
from .pyside6_adapter import PySide6Adapter
|
|
4
|
+
from .pyqt6_adapter import PyQt6Adapter
|
|
5
|
+
from .pyside2_adapter import PySide2Adapter
|
|
6
|
+
from .pyqt5_adapter import PyQt5Adapter
|
|
7
|
+
from .kivy_adapter import KivyAdapter
|
|
8
|
+
from .tkinter_adapter import TkinterAdapter
|
|
9
|
+
from .factory import FrameworkFactory
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"BaseUIFrameworkAdapter",
|
|
13
|
+
"BaseQtAdapter",
|
|
14
|
+
"PySide6Adapter",
|
|
15
|
+
"PyQt6Adapter",
|
|
16
|
+
"PySide2Adapter",
|
|
17
|
+
"PyQt5Adapter",
|
|
18
|
+
"KivyAdapter",
|
|
19
|
+
"TkinterAdapter",
|
|
20
|
+
"FrameworkFactory",
|
|
21
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base UI Framework Adapter.
|
|
3
|
+
Common abstract implementation for GUI toolkits.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from abc import abstractmethod
|
|
7
|
+
from typing import Any, List, Optional
|
|
8
|
+
from qyro.application.ports.ui_framework import IUIFrameworkPort
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BaseUIFrameworkAdapter(IUIFrameworkPort):
|
|
12
|
+
"""Base framework adapter storing app reference and lifecycle state."""
|
|
13
|
+
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
self._app: Any = None
|
|
16
|
+
self._is_running: bool = False
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def app_instance(self) -> Any:
|
|
20
|
+
return self._app
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Framework Factory.
|
|
3
|
+
Detects available UI toolkits or instantiates the user's requested engine backend.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from qyro.application.ports.ui_framework import IUIFrameworkPort
|
|
8
|
+
from qyro.domain.errors import FrameworkNotAvailableError
|
|
9
|
+
from .pyside6_adapter import PySide6Adapter
|
|
10
|
+
from .pyqt6_adapter import PyQt6Adapter
|
|
11
|
+
from .pyside2_adapter import PySide2Adapter
|
|
12
|
+
from .pyqt5_adapter import PyQt5Adapter
|
|
13
|
+
from .kivy_adapter import KivyAdapter
|
|
14
|
+
from .tkinter_adapter import TkinterAdapter
|
|
15
|
+
from .headless_adapter import HeadlessAdapter
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FrameworkFactory:
|
|
19
|
+
"""Instantiates framework adapters based on preference or automatic detection."""
|
|
20
|
+
|
|
21
|
+
_FRAMEWORK_REGISTRY = {
|
|
22
|
+
"pyside6": PySide6Adapter,
|
|
23
|
+
"pyqt6": PyQt6Adapter,
|
|
24
|
+
"pyside2": PySide2Adapter,
|
|
25
|
+
"pyqt5": PyQt5Adapter,
|
|
26
|
+
"kivy": KivyAdapter,
|
|
27
|
+
"tkinter": TkinterAdapter,
|
|
28
|
+
"headless": HeadlessAdapter,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def create(cls, name: Optional[str] = None) -> IUIFrameworkPort:
|
|
33
|
+
if name:
|
|
34
|
+
normalized = name.strip().lower()
|
|
35
|
+
adapter_cls = cls._FRAMEWORK_REGISTRY.get(normalized)
|
|
36
|
+
if not adapter_cls:
|
|
37
|
+
raise FrameworkNotAvailableError(
|
|
38
|
+
name,
|
|
39
|
+
f"Supported frameworks: {', '.join(cls._FRAMEWORK_REGISTRY.keys())}"
|
|
40
|
+
)
|
|
41
|
+
adapter = adapter_cls()
|
|
42
|
+
if not adapter.is_available():
|
|
43
|
+
raise FrameworkNotAvailableError(adapter.framework_name)
|
|
44
|
+
return adapter
|
|
45
|
+
|
|
46
|
+
# Auto-detect in order of preference
|
|
47
|
+
preference = [
|
|
48
|
+
PySide6Adapter,
|
|
49
|
+
PyQt6Adapter,
|
|
50
|
+
PySide2Adapter,
|
|
51
|
+
PyQt5Adapter,
|
|
52
|
+
KivyAdapter,
|
|
53
|
+
TkinterAdapter,
|
|
54
|
+
HeadlessAdapter,
|
|
55
|
+
]
|
|
56
|
+
for candidate_cls in preference:
|
|
57
|
+
candidate = candidate_cls()
|
|
58
|
+
if candidate.is_available():
|
|
59
|
+
return candidate
|
|
60
|
+
|
|
61
|
+
return HeadlessAdapter()
|
|
62
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Headless Framework Adapter for CI, tests, and headless environments.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, List, Optional
|
|
6
|
+
from qyro.application.ports.ui_framework import IUIFrameworkPort
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class HeadlessApplication:
|
|
10
|
+
"""Mock GUI application instance for headless environments."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, argv: Optional[List[str]] = None) -> None:
|
|
13
|
+
self.argv = argv or []
|
|
14
|
+
|
|
15
|
+
def exec(self) -> int:
|
|
16
|
+
return 0
|
|
17
|
+
|
|
18
|
+
def exec_(self) -> int:
|
|
19
|
+
return 0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class HeadlessAdapter(IUIFrameworkPort):
|
|
23
|
+
"""Fallback adapter when running in headless mode or without GUI libraries installed."""
|
|
24
|
+
|
|
25
|
+
def __init__(self) -> None:
|
|
26
|
+
super().__init__()
|
|
27
|
+
self.applied_app_icon: Optional[str] = None
|
|
28
|
+
self.applied_window_icons: dict = {}
|
|
29
|
+
self.applied_window_titles: dict = {}
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def framework_name(self) -> str:
|
|
33
|
+
return "Headless"
|
|
34
|
+
|
|
35
|
+
def is_available(self) -> bool:
|
|
36
|
+
return True
|
|
37
|
+
|
|
38
|
+
def create_application(self, argv: Optional[List[str]] = None) -> Any:
|
|
39
|
+
return HeadlessApplication(argv)
|
|
40
|
+
|
|
41
|
+
def run(self) -> int:
|
|
42
|
+
return 0
|
|
43
|
+
|
|
44
|
+
def exit(self, code: int = 0) -> None:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
def set_application_icon(self, icon_path: str) -> bool:
|
|
48
|
+
self.applied_app_icon = icon_path
|
|
49
|
+
return True
|
|
50
|
+
|
|
51
|
+
def set_window_icon(self, window: Any, icon_path: str) -> bool:
|
|
52
|
+
self.applied_window_icons[window] = icon_path
|
|
53
|
+
return True
|
|
54
|
+
|
|
55
|
+
def set_window_title(self, window: Any, title: str) -> bool:
|
|
56
|
+
self.applied_window_titles[window] = title
|
|
57
|
+
return True
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Kivy and Tkinter Framework Adapters for Qyro.
|
|
3
|
+
Supports cross-platform desktop/mobile (Kivy) and lightweight builtin (Tkinter) runtimes.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Any, List, Optional
|
|
8
|
+
from .base import BaseUIFrameworkAdapter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class KivyAdapter(BaseUIFrameworkAdapter):
|
|
12
|
+
"""Kivy toolkit adapter for multi-touch and mobile/desktop apps."""
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def framework_name(self) -> str:
|
|
16
|
+
return "Kivy"
|
|
17
|
+
|
|
18
|
+
def is_available(self) -> bool:
|
|
19
|
+
try:
|
|
20
|
+
import kivy # noqa: F401
|
|
21
|
+
return True
|
|
22
|
+
except ImportError:
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
def create_application(self, argv: Optional[List[str]] = None) -> Any:
|
|
26
|
+
try:
|
|
27
|
+
from kivy.app import App
|
|
28
|
+
running = App.get_running_app()
|
|
29
|
+
if running:
|
|
30
|
+
self._app = running
|
|
31
|
+
return self._app
|
|
32
|
+
except Exception:
|
|
33
|
+
pass
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
def run(self) -> int:
|
|
37
|
+
if not self._app:
|
|
38
|
+
try:
|
|
39
|
+
from kivy.app import App
|
|
40
|
+
self._app = App.get_running_app()
|
|
41
|
+
except Exception:
|
|
42
|
+
pass
|
|
43
|
+
if not self._app:
|
|
44
|
+
from kivy.app import App
|
|
45
|
+
|
|
46
|
+
class GenericKivyApp(App):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
self._app = GenericKivyApp()
|
|
50
|
+
|
|
51
|
+
self._is_running = True
|
|
52
|
+
self._app.run()
|
|
53
|
+
return 0
|
|
54
|
+
|
|
55
|
+
def exit(self, code: int = 0) -> None:
|
|
56
|
+
if self._app and hasattr(self._app, "stop"):
|
|
57
|
+
self._app.stop()
|
|
58
|
+
self._is_running = False
|
|
59
|
+
|
|
60
|
+
def set_application_icon(self, icon_path: str) -> bool:
|
|
61
|
+
if self._app and hasattr(self._app, "icon"):
|
|
62
|
+
self._app.icon = icon_path
|
|
63
|
+
return True
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
def set_window_icon(self, window: Any, icon_path: str) -> bool:
|
|
67
|
+
if window and hasattr(window, "icon"):
|
|
68
|
+
window.icon = icon_path
|
|
69
|
+
return True
|
|
70
|
+
return self.set_application_icon(icon_path)
|
|
71
|
+
|
|
72
|
+
def set_window_title(self, window: Any, title: str) -> bool:
|
|
73
|
+
if window and hasattr(window, "title"):
|
|
74
|
+
window.title = title
|
|
75
|
+
return True
|
|
76
|
+
if self._app and hasattr(self._app, "title"):
|
|
77
|
+
self._app.title = title
|
|
78
|
+
return True
|
|
79
|
+
return False
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from .qt_base import BaseQtAdapter
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class PyQt5Adapter(BaseQtAdapter):
|
|
5
|
+
"""PyQt5 toolkit adapter for Qt 5."""
|
|
6
|
+
|
|
7
|
+
def __init__(self) -> None:
|
|
8
|
+
qapp = None
|
|
9
|
+
if self.is_available():
|
|
10
|
+
from PyQt5.QtWidgets import QApplication
|
|
11
|
+
qapp = QApplication
|
|
12
|
+
super().__init__(qapp_class=qapp, is_qt6=False)
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def framework_name(self) -> str:
|
|
16
|
+
return "PyQt5"
|
|
17
|
+
|
|
18
|
+
def is_available(self) -> bool:
|
|
19
|
+
try:
|
|
20
|
+
import PyQt5.QtWidgets # noqa: F401
|
|
21
|
+
return True
|
|
22
|
+
except ImportError:
|
|
23
|
+
return False
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PyQt6 Adapter.
|
|
3
|
+
Riverbank PyQt6 binding implementation for Qyro.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any, List, Optional
|
|
7
|
+
from .qt_base import BaseQtAdapter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PyQt6Adapter(BaseQtAdapter):
|
|
11
|
+
"""PyQt6 toolkit adapter."""
|
|
12
|
+
|
|
13
|
+
def __init__(self) -> None:
|
|
14
|
+
qapp = None
|
|
15
|
+
if self.is_available():
|
|
16
|
+
from PyQt6.QtWidgets import QApplication
|
|
17
|
+
qapp = QApplication
|
|
18
|
+
super().__init__(qapp_class=qapp, is_qt6=True)
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def framework_name(self) -> str:
|
|
22
|
+
return "PyQt6"
|
|
23
|
+
|
|
24
|
+
def is_available(self) -> bool:
|
|
25
|
+
try:
|
|
26
|
+
import PyQt6.QtWidgets # noqa: F401
|
|
27
|
+
return True
|
|
28
|
+
except ImportError:
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
def create_application(self, argv: Optional[List[str]] = None) -> Any:
|
|
32
|
+
if not self._qapp_class and self.is_available():
|
|
33
|
+
from PyQt6.QtWidgets import QApplication
|
|
34
|
+
self._qapp_class = QApplication
|
|
35
|
+
return super().create_application(argv)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PySide2 and PyQt5 Legacy Adapters for Qt 5.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .qt_base import BaseQtAdapter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PySide2Adapter(BaseQtAdapter):
|
|
9
|
+
"""PySide2 toolkit adapter for Qt 5."""
|
|
10
|
+
|
|
11
|
+
def __init__(self) -> None:
|
|
12
|
+
qapp = None
|
|
13
|
+
if self.is_available():
|
|
14
|
+
from PySide2.QtWidgets import QApplication
|
|
15
|
+
qapp = QApplication
|
|
16
|
+
super().__init__(qapp_class=qapp, is_qt6=False)
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def framework_name(self) -> str:
|
|
20
|
+
return "PySide2"
|
|
21
|
+
|
|
22
|
+
def is_available(self) -> bool:
|
|
23
|
+
try:
|
|
24
|
+
import PySide2.QtWidgets # noqa: F401
|
|
25
|
+
return True
|
|
26
|
+
except ImportError:
|
|
27
|
+
return False
|
|
28
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PySide6 Adapter.
|
|
3
|
+
Official Qt 6 binding implementation for Qyro.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any, List, Optional
|
|
7
|
+
from .qt_base import BaseQtAdapter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PySide6Adapter(BaseQtAdapter):
|
|
11
|
+
"""PySide6 toolkit adapter."""
|
|
12
|
+
|
|
13
|
+
def __init__(self) -> None:
|
|
14
|
+
qapp = None
|
|
15
|
+
if self.is_available():
|
|
16
|
+
from PySide6.QtWidgets import QApplication
|
|
17
|
+
qapp = QApplication
|
|
18
|
+
super().__init__(qapp_class=qapp, is_qt6=True)
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def framework_name(self) -> str:
|
|
22
|
+
return "PySide6"
|
|
23
|
+
|
|
24
|
+
def is_available(self) -> bool:
|
|
25
|
+
try:
|
|
26
|
+
import PySide6.QtWidgets # noqa: F401
|
|
27
|
+
return True
|
|
28
|
+
except ImportError:
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
def create_application(self, argv: Optional[List[str]] = None) -> Any:
|
|
32
|
+
if not self._qapp_class and self.is_available():
|
|
33
|
+
from PySide6.QtWidgets import QApplication
|
|
34
|
+
self._qapp_class = QApplication
|
|
35
|
+
return super().create_application(argv)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base Qt Adapter for PySide and PyQt bindings.
|
|
3
|
+
Encapsulates common QApplication creation, high-DPI attributes, and event loop execution.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Any, List, Optional
|
|
8
|
+
from .base import BaseUIFrameworkAdapter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BaseQtAdapter(BaseUIFrameworkAdapter):
|
|
12
|
+
"""Shared implementation details across PyQt and PySide versions."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, qapp_class: Any, is_qt6: bool = True) -> None:
|
|
15
|
+
super().__init__()
|
|
16
|
+
self._qapp_class = qapp_class
|
|
17
|
+
self._is_qt6 = is_qt6
|
|
18
|
+
|
|
19
|
+
def create_application(self, argv: Optional[List[str]] = None) -> Any:
|
|
20
|
+
if self._qapp_class.instance():
|
|
21
|
+
self._app = self._qapp_class.instance()
|
|
22
|
+
return self._app
|
|
23
|
+
|
|
24
|
+
args = argv if argv is not None else sys.argv
|
|
25
|
+
self._app = self._qapp_class(args)
|
|
26
|
+
return self._app
|
|
27
|
+
|
|
28
|
+
def run(self) -> int:
|
|
29
|
+
if not self._app:
|
|
30
|
+
self.create_application()
|
|
31
|
+
|
|
32
|
+
self._is_running = True
|
|
33
|
+
# Qt 6 uses app.exec(), Qt 5 used app.exec_()
|
|
34
|
+
if hasattr(self._app, "exec"):
|
|
35
|
+
return self._app.exec()
|
|
36
|
+
return self._app.exec_()
|
|
37
|
+
|
|
38
|
+
def exit(self, code: int = 0) -> None:
|
|
39
|
+
if self._app:
|
|
40
|
+
self._app.exit(code)
|
|
41
|
+
self._is_running = False
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def binding_package(self) -> str:
|
|
45
|
+
"""Determines the exact Qt package name associated with this adapter instance."""
|
|
46
|
+
if self._qapp_class and hasattr(self._qapp_class, "__module__"):
|
|
47
|
+
return self._qapp_class.__module__.split(".")[0]
|
|
48
|
+
return self.framework_name
|
|
49
|
+
|
|
50
|
+
def _create_qicon(self, icon_path: str, target: Optional[Any] = None) -> Optional[Any]:
|
|
51
|
+
"""
|
|
52
|
+
Dynamically imports QIcon strictly from the adapter's matching Qt binding.
|
|
53
|
+
Avoids cross-importing other installed Qt bindings (e.g. PySide6 when running PyQt5),
|
|
54
|
+
which causes macOS duplicate symbol warnings, QPixmap crashes, and SIGABRT.
|
|
55
|
+
"""
|
|
56
|
+
pkg = None
|
|
57
|
+
if target is not None:
|
|
58
|
+
for cls in getattr(type(target), "__mro__", []):
|
|
59
|
+
mod = getattr(cls, "__module__", "")
|
|
60
|
+
if mod:
|
|
61
|
+
root = mod.split(".")[0]
|
|
62
|
+
if root in ("PyQt5", "PySide6", "PyQt6", "PySide2"):
|
|
63
|
+
pkg = root
|
|
64
|
+
break
|
|
65
|
+
|
|
66
|
+
if not pkg:
|
|
67
|
+
pkg = self.binding_package
|
|
68
|
+
|
|
69
|
+
if pkg:
|
|
70
|
+
try:
|
|
71
|
+
mod = __import__(f"{pkg}.QtGui", fromlist=["QIcon"])
|
|
72
|
+
QIcon = getattr(mod, "QIcon", None)
|
|
73
|
+
if QIcon:
|
|
74
|
+
return QIcon(icon_path)
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
def set_application_icon(self, icon_path: str) -> bool:
|
|
80
|
+
if not self._app:
|
|
81
|
+
return False
|
|
82
|
+
qicon = self._create_qicon(icon_path, target=self._app)
|
|
83
|
+
if qicon and hasattr(self._app, "setWindowIcon"):
|
|
84
|
+
self._app.setWindowIcon(qicon)
|
|
85
|
+
return True
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
def set_window_icon(self, window: Any, icon_path: str) -> bool:
|
|
89
|
+
if not window:
|
|
90
|
+
return False
|
|
91
|
+
qicon = self._create_qicon(icon_path, target=window)
|
|
92
|
+
if qicon and hasattr(window, "setWindowIcon"):
|
|
93
|
+
try:
|
|
94
|
+
window.setWindowIcon(qicon)
|
|
95
|
+
return True
|
|
96
|
+
except (RuntimeError, Exception):
|
|
97
|
+
return False
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
def set_window_title(self, window: Any, title: str) -> bool:
|
|
101
|
+
if window and hasattr(window, "setWindowTitle"):
|
|
102
|
+
try:
|
|
103
|
+
window.setWindowTitle(title)
|
|
104
|
+
return True
|
|
105
|
+
except (RuntimeError, Exception):
|
|
106
|
+
return False
|
|
107
|
+
return False
|