forked from Support/sp-hydra-veil-gui
PyQt6 implementation of the cross-platform cross-framework threading plumbing error handler
This commit is contained in:
parent
80407f636d
commit
e7e7df5bfa
5 changed files with 138 additions and 2 deletions
|
|
@ -17,6 +17,7 @@ from PyQt6 import QtGui
|
||||||
from PyQt6.QtCore import Qt, QTimer, QEvent
|
from PyQt6.QtCore import Qt, QTimer, QEvent
|
||||||
|
|
||||||
from core.Constants import Constants
|
from core.Constants import Constants
|
||||||
|
from core.infrastructure.service_registry import ServiceLocator
|
||||||
from core.controllers.ConfigurationController import ConfigurationController
|
from core.controllers.ConfigurationController import ConfigurationController
|
||||||
from core.controllers.ProfileController import ProfileController
|
from core.controllers.ProfileController import ProfileController
|
||||||
from core.Errors import UnknownConnectionTypeError
|
from core.Errors import UnknownConnectionTypeError
|
||||||
|
|
@ -175,6 +176,7 @@ class CustomWindow(QMainWindow):
|
||||||
self.check_logging()
|
self.check_logging()
|
||||||
|
|
||||||
self.navigator = Navigator(self)
|
self.navigator = Navigator(self)
|
||||||
|
ServiceLocator.register(name="navigator", service=self.navigator)
|
||||||
|
|
||||||
self.page_stack.currentChanged.connect(self.page_changed)
|
self.page_stack.currentChanged.connect(self.page_changed)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
# from core.infrastructure.thread_dispatch import MainThreadDispatcher
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from PyQt6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QPushButton, QLabel
|
from PyQt6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QPushButton, QLabel
|
||||||
|
|
@ -13,9 +15,15 @@ class ThreadSafetyTool(QObject):
|
||||||
|
|
||||||
def _execute(self, func, args, kwargs):
|
def _execute(self, func, args, kwargs):
|
||||||
func(*args, **kwargs)
|
func(*args, **kwargs)
|
||||||
|
|
||||||
|
# --- PROTOCOL REQUIRES THIS ---
|
||||||
|
def run_on_main_thread(self, func, *args, **kwargs):
|
||||||
|
self._signal.emit(func, args, kwargs)
|
||||||
|
# -----------------------------------------
|
||||||
|
|
||||||
def __call__(self, func):
|
def __call__(self, func):
|
||||||
"""Decorator that forces a function to run on the main thread."""
|
"""Decorator that forces a function to run on the main thread."""
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
self._signal.emit(func, args, kwargs)
|
self._signal.emit(func, args, kwargs)
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import importlib
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from PyQt6.QtCore import QObject, QTimer
|
from PyQt6.QtCore import QObject, QTimer, pyqtSignal
|
||||||
|
|
||||||
from gui.v2.infrastructure.page_registry import (
|
from gui.v2.infrastructure.page_registry import (
|
||||||
PAGE_REGISTRY,
|
PAGE_REGISTRY,
|
||||||
|
|
@ -17,6 +17,8 @@ from core.errors.logger import logger
|
||||||
|
|
||||||
|
|
||||||
class Navigator(QObject):
|
class Navigator(QObject):
|
||||||
|
external_navigation = pyqtSignal(str)
|
||||||
|
|
||||||
def __init__(self, custom_window):
|
def __init__(self, custom_window):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.custom_window = custom_window
|
self.custom_window = custom_window
|
||||||
|
|
@ -37,6 +39,9 @@ class Navigator(QObject):
|
||||||
self._preload_timer.setSingleShot(True)
|
self._preload_timer.setSingleShot(True)
|
||||||
self._preload_timer.timeout.connect(self._preload_next)
|
self._preload_timer.timeout.connect(self._preload_next)
|
||||||
|
|
||||||
|
# external navigation:
|
||||||
|
self.external_navigation.connect(self.navigate)
|
||||||
|
|
||||||
def get_class(self, name):
|
def get_class(self, name):
|
||||||
module_path, class_name = PAGE_REGISTRY[name]
|
module_path, class_name = PAGE_REGISTRY[name]
|
||||||
module = importlib.import_module(module_path)
|
module = importlib.import_module(module_path)
|
||||||
|
|
|
||||||
25
gui/v2/infrastructure/take_plumbing_action.py
Normal file
25
gui/v2/infrastructure/take_plumbing_action.py
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
from core.infrastructure.plumbing import ThreadPlumbing
|
||||||
|
from core.models.Result import ResultError
|
||||||
|
from gui.v2.infrastructure.ThreadSafetyTool import ThreadSafetyTool
|
||||||
|
from gui.v2.ui.popups.error_popup import ErrorPopup
|
||||||
|
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
AUTO_NAVIGATE_REGISTRY = {
|
||||||
|
ResultError.WRONG_LOCATION: "settings"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def take_action(action: Callable, ui_caller: Callable, *args, **kwargs):
|
||||||
|
"""PyQt6 GUI implementation of plumbing"""
|
||||||
|
|
||||||
|
controller = ThreadPlumbing(
|
||||||
|
action=action,
|
||||||
|
ui_dispatcher=ThreadSafetyTool(), # PyQt6 as mainthread
|
||||||
|
error_implementation=ErrorPopup, # PyQt6 ErrorPopup
|
||||||
|
auto_navigate_dict=AUTO_NAVIGATE_REGISTRY,
|
||||||
|
ui_caller=ui_caller,
|
||||||
|
*args,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
controller.execute()
|
||||||
96
gui/v2/ui/popups/error_popup.py
Normal file
96
gui/v2/ui/popups/error_popup.py
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
from typing import Optional, Callable, Any
|
||||||
|
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QApplication
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
|
|
||||||
|
class ErrorPopup(QDialog):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
choice: bool,
|
||||||
|
title: str,
|
||||||
|
message: str,
|
||||||
|
end_app: Optional[bool] = False,
|
||||||
|
on_answer: Optional[Callable] = None,
|
||||||
|
passed_result: Optional[Any] = None,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
# Protocol state
|
||||||
|
self.title_text = title
|
||||||
|
self.on_answer = on_answer
|
||||||
|
self.passed_result = passed_result
|
||||||
|
self.end_app = end_app
|
||||||
|
|
||||||
|
# Window Configuration
|
||||||
|
self.setWindowTitle(title)
|
||||||
|
self.setFixedSize(400, 300)
|
||||||
|
self.setModal(True)
|
||||||
|
|
||||||
|
# Dark Theme Styling (Replacing Kivy Canvas/RoundedRectangle)
|
||||||
|
self.setStyleSheet("""
|
||||||
|
QDialog {
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
QLabel {
|
||||||
|
color: #33ff33;
|
||||||
|
font-size: 24px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
QPushButton {
|
||||||
|
color: white;
|
||||||
|
font-size: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
min-height: 50px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Layout setup
|
||||||
|
main_layout = QVBoxLayout()
|
||||||
|
main_layout.setContentsMargins(20, 20, 20, 20)
|
||||||
|
main_layout.setSpacing(20)
|
||||||
|
|
||||||
|
# MESSAGE LABEL
|
||||||
|
self.message_label = QLabel(message)
|
||||||
|
self.message_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.message_label.setWordWrap(True)
|
||||||
|
main_layout.addWidget(self.message_label)
|
||||||
|
|
||||||
|
if choice:
|
||||||
|
# BUTTON LAYOUT for Yes/No
|
||||||
|
button_layout = QHBoxLayout()
|
||||||
|
button_layout.setSpacing(20)
|
||||||
|
|
||||||
|
yes_btn = QPushButton("Yes")
|
||||||
|
yes_btn.setStyleSheet("background-color: #1b4d1b;")
|
||||||
|
yes_btn.clicked.connect(lambda: self._choice_response(True))
|
||||||
|
|
||||||
|
no_btn = QPushButton("No")
|
||||||
|
no_btn.setStyleSheet("background-color: #801a1a;")
|
||||||
|
no_btn.clicked.connect(lambda: self._choice_response(False))
|
||||||
|
|
||||||
|
button_layout.addWidget(yes_btn)
|
||||||
|
button_layout.addWidget(no_btn)
|
||||||
|
main_layout.addLayout(button_layout)
|
||||||
|
else:
|
||||||
|
# OK button
|
||||||
|
ok_btn = QPushButton("OK")
|
||||||
|
ok_btn.setStyleSheet("background-color: #1b4d1b;")
|
||||||
|
ok_btn.clicked.connect(self.accept) # Closes the dialog
|
||||||
|
main_layout.addWidget(ok_btn)
|
||||||
|
|
||||||
|
self.setLayout(main_layout)
|
||||||
|
|
||||||
|
def _choice_response(self, user_choice: bool):
|
||||||
|
if self.on_answer:
|
||||||
|
self.on_answer(user_choice, self.passed_result)
|
||||||
|
self.accept()
|
||||||
|
|
||||||
|
def _terminate_app(self):
|
||||||
|
"""Desktop equivalent of App.get_running_app().stop()"""
|
||||||
|
QApplication.quit()
|
||||||
|
|
||||||
|
def show(self):
|
||||||
|
self.exec()
|
||||||
Loading…
Reference in a new issue