PyQt6 implementation of the cross-platform cross-framework threading plumbing error handler

This commit is contained in:
SimplifiedPrivacy 2026-09-28 17:40:26 -04:00
parent 80407f636d
commit e7e7df5bfa
5 changed files with 138 additions and 2 deletions

View file

@ -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)

View file

@ -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

View file

@ -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)

View 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()

View 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()