diff --git a/gui/main_ui.py b/gui/main_ui.py index d447668..30a4a0f 100755 --- a/gui/main_ui.py +++ b/gui/main_ui.py @@ -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) diff --git a/gui/v2/infrastructure/ThreadSafetyTool.py b/gui/v2/infrastructure/ThreadSafetyTool.py index 71d2082..0ba105a 100755 --- a/gui/v2/infrastructure/ThreadSafetyTool.py +++ b/gui/v2/infrastructure/ThreadSafetyTool.py @@ -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 + diff --git a/gui/v2/infrastructure/navigator.py b/gui/v2/infrastructure/navigator.py index d7b52d7..e5e8e5a 100755 --- a/gui/v2/infrastructure/navigator.py +++ b/gui/v2/infrastructure/navigator.py @@ -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) diff --git a/gui/v2/infrastructure/take_plumbing_action.py b/gui/v2/infrastructure/take_plumbing_action.py new file mode 100644 index 0000000..44d5bf2 --- /dev/null +++ b/gui/v2/infrastructure/take_plumbing_action.py @@ -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() diff --git a/gui/v2/ui/popups/error_popup.py b/gui/v2/ui/popups/error_popup.py new file mode 100644 index 0000000..b3c7992 --- /dev/null +++ b/gui/v2/ui/popups/error_popup.py @@ -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()