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 core.Constants import Constants
|
||||
from core.infrastructure.service_registry import ServiceLocator
|
||||
from core.controllers.ConfigurationController import ConfigurationController
|
||||
from core.controllers.ProfileController import ProfileController
|
||||
from core.Errors import UnknownConnectionTypeError
|
||||
|
|
@ -175,6 +176,7 @@ class CustomWindow(QMainWindow):
|
|||
self.check_logging()
|
||||
|
||||
self.navigator = Navigator(self)
|
||||
ServiceLocator.register(name="navigator", service=self.navigator)
|
||||
|
||||
self.page_stack.currentChanged.connect(self.page_changed)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# from core.infrastructure.thread_dispatch import MainThreadDispatcher
|
||||
|
||||
import sys
|
||||
import time
|
||||
from PyQt6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QPushButton, QLabel
|
||||
|
|
@ -13,9 +15,15 @@ class ThreadSafetyTool(QObject):
|
|||
|
||||
def _execute(self, 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):
|
||||
"""Decorator that forces a function to run on the main thread."""
|
||||
def wrapper(*args, **kwargs):
|
||||
self._signal.emit(func, args, kwargs)
|
||||
return wrapper
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import importlib
|
|||
import time
|
||||
import traceback
|
||||
|
||||
from PyQt6.QtCore import QObject, QTimer
|
||||
from PyQt6.QtCore import QObject, QTimer, pyqtSignal
|
||||
|
||||
from gui.v2.infrastructure.page_registry import (
|
||||
PAGE_REGISTRY,
|
||||
|
|
@ -17,6 +17,8 @@ from core.errors.logger import logger
|
|||
|
||||
|
||||
class Navigator(QObject):
|
||||
external_navigation = pyqtSignal(str)
|
||||
|
||||
def __init__(self, custom_window):
|
||||
super().__init__()
|
||||
self.custom_window = custom_window
|
||||
|
|
@ -37,6 +39,9 @@ class Navigator(QObject):
|
|||
self._preload_timer.setSingleShot(True)
|
||||
self._preload_timer.timeout.connect(self._preload_next)
|
||||
|
||||
# external navigation:
|
||||
self.external_navigation.connect(self.navigate)
|
||||
|
||||
def get_class(self, name):
|
||||
module_path, class_name = PAGE_REGISTRY[name]
|
||||
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