diff --git a/gui/v2/infrastructure/take_plumbing_action.py b/gui/v2/infrastructure/take_plumbing_action.py index 42c7dcc..5e2746b 100644 --- a/gui/v2/infrastructure/take_plumbing_action.py +++ b/gui/v2/infrastructure/take_plumbing_action.py @@ -4,6 +4,7 @@ from core.models.Result import ResultError from gui.v2.infrastructure.action_registry import ENUM_MAP from gui.v2.infrastructure.ThreadSafetyTool import ThreadSafetyTool from gui.v2.ui.popups.error_popup import ErrorPopup +from gui.v2.ui.popups.stack_error_popup import ErrorStackPrompt from typing import Callable @@ -19,6 +20,7 @@ def take_action(action: Callable, ui_caller: Callable, *args, **kwargs): action=action, ui_dispatcher=ThreadSafetyTool(), # PyQt6 as mainthread error_implementation=ErrorPopup, # PyQt6 ErrorPopup + stack_error_implementation=ErrorStackPrompt, # PyQt6 ENUM_MAP=ENUM_MAP, auto_navigate_dict=AUTO_NAVIGATE_REGISTRY, ui_caller=ui_caller, diff --git a/gui/v2/ui/popups/error_popup.py b/gui/v2/ui/popups/error_popup.py index b3c7992..b77f60c 100644 --- a/gui/v2/ui/popups/error_popup.py +++ b/gui/v2/ui/popups/error_popup.py @@ -26,7 +26,7 @@ class ErrorPopup(QDialog): self.setFixedSize(400, 300) self.setModal(True) - # Dark Theme Styling (Replacing Kivy Canvas/RoundedRectangle) + # Dark Theme Styling self.setStyleSheet(""" QDialog { background-color: #1a1a1a; @@ -89,7 +89,7 @@ class ErrorPopup(QDialog): self.accept() def _terminate_app(self): - """Desktop equivalent of App.get_running_app().stop()""" + """Quit the Graphical App""" QApplication.quit() def show(self): diff --git a/gui/v2/ui/popups/stack_error_popup.py b/gui/v2/ui/popups/stack_error_popup.py new file mode 100644 index 0000000..d1c49b2 --- /dev/null +++ b/gui/v2/ui/popups/stack_error_popup.py @@ -0,0 +1,249 @@ +from core.errors.ErrorStack import ErrorStack + + +import sys +from PyQt6.QtWidgets import ( + QApplication, QDialog, QVBoxLayout, QHBoxLayout, QLabel, + QFrame, QScrollArea, QPushButton, QPlainTextEdit +) +from PyQt6.QtCore import Qt, QTimer +from PyQt6.QtGui import QFont + +class ErrorStackPrompt(QDialog): + IGNORE = 1 + EXIT = 2 + + def __init__(self, error_data: ErrorStack, parent=None): + super().__init__(parent) + self.error_data = error_data + self.setWindowTitle("System Error") + self.setModal(True) + self.user_choice = self.EXIT + self.setMinimumWidth(600) + self.setMinimumHeight(500) + self.setMaximumWidth(800) + self.setMaximumHeight(800) + + self.setStyleSheet(""" + ErrorStackPrompt { + background: qlineargradient(x1:0, y1:0, x2:1, y2:1, + stop:0 #0f172a, stop:1 #1e293b); + } + """) + + main_layout = QVBoxLayout() + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(0) + + # --- Header --- + header = QFrame() + header.setStyleSheet(""" + QFrame { + background: qlineargradient(x1:0, y1:0, x2:1, y2:0, + stop:0 #3b82f6, stop:1 #1e40af); + border: none; + } + """) + + header.setFixedHeight(100) + header_layout = QHBoxLayout() + header_layout.setContentsMargins(30, 20, 30, 20) + + icon_label = QLabel("⚠️") + icon_label.setFont(QFont("Segoe UI", 48)) + header_layout.addWidget(icon_label, 0, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) + + title_layout = QVBoxLayout() + title_layout.setSpacing(5) + + title = QLabel("App Error") + title.setFont(QFont("Segoe UI", 18, QFont.Weight.Bold)) + title.setStyleSheet("color: white;") + title_layout.addWidget(title) + + subtitle = QLabel(f"Error in {self.error_data.func_name or 'Unknown'}") + subtitle.setFont(QFont("Segoe UI", 11)) + subtitle.setStyleSheet("color: rgba(255, 255, 255, 0.8);") + title_layout.addWidget(subtitle) + + header_layout.addLayout(title_layout, 1) + header.setLayout(header_layout) + main_layout.addWidget(header) + + # --- Content Area --- + content = QFrame() + content.setStyleSheet("QFrame { background: #1e293b; border: none; }") + content_layout = QVBoxLayout() + + scroll_area = QScrollArea() + scroll_area.setWidgetResizable(True) + scroll_area.setStyleSheet(""" + QScrollArea { background: #1e293b; border: none; } + QScrollBar:vertical { background: #1e293b; width: 12px; } + QScrollBar::handle:vertical { background: #475569; border-radius: 6px; } + """) + + scroll_widget = QFrame() + scroll_widget.setStyleSheet("background: #1e293b; border: none;") + scroll_layout = QVBoxLayout() + scroll_layout.setContentsMargins(40, 30, 40, 30) + scroll_layout.setSpacing(20) + + # User Message Section + msg_container = QVBoxLayout() + msg_label = QLabel("Message for User:") + msg_label.setFont(QFont("Segoe UI", 10, QFont.Weight.Bold)) + msg_label.setStyleSheet("color: #94a3b8;") + msg_container.addWidget(msg_label) + + user_msg = QLabel(str(self.error_data.message_for_user)) + user_msg.setFont(QFont("Segoe UI", 12)) + user_msg.setStyleSheet("color: #e2e8f0;") + user_msg.setWordWrap(True) + msg_container.addWidget(user_msg) + + btn_copy_msg = self._create_small_button("Copy Message", "📋") + btn_copy_msg.clicked.connect(lambda: self._handle_copy(btn_copy_msg, str(self.error_data.message_for_user))) + msg_container.addWidget(btn_copy_msg, 0, Qt.AlignmentFlag.AlignRight) + + scroll_layout.addLayout(msg_container) + + # Stacktrace Section (Small scroll box) + trace_container = QVBoxLayout() + trace_label = QLabel("Technical Details (Stacktrace):") + trace_label.setFont(QFont("Segoe UI", 10, QFont.Weight.Bold)) + trace_label.setStyleSheet("color: #94a3b8;") + trace_container.addWidget(trace_label) + + self.trace_box = QPlainTextEdit() + self.trace_box.setPlainText(str(self.error_data.full_stacktrace)) + self.trace_box.setReadOnly(True) + self.trace_box.setMaximumHeight(200) # contained in box + self.trace_box.setStyleSheet(""" + QPlainTextEdit { + background: #0f172a; + color: #cbd5e1; + border: 1px solid #334155; + border-radius: 4px; + font-family: 'Consolas', 'Monospace'; + font-size: 10pt; + } + """) + trace_container.addWidget(self.trace_box) + + btn_copy_trace = self._create_small_button("Copy Stacktrace", "📋") + btn_copy_trace.clicked.connect(lambda: self._handle_copy(btn_copy_trace, str(self.error_data.full_stacktrace))) + trace_container.addWidget(btn_copy_trace, 0, Qt.AlignmentFlag.AlignRight) + + scroll_layout.addLayout(trace_container) + scroll_layout.addStretch() + + scroll_widget.setLayout(scroll_layout) + scroll_area.setWidget(scroll_widget) + content_layout.addWidget(scroll_area, 1) + content.setLayout(content_layout) + main_layout.addWidget(content, 1) + + # --- Button Area --- + button_area = QFrame() + button_area.setStyleSheet("QFrame { background: #0f172a; border-top: 1px solid #334155; }") + button_layout = QVBoxLayout() + button_layout.setContentsMargins(40, 20, 40, 20) + button_layout.setSpacing(12) + + btn_ignore = self._create_button("Ignore and Continue", "secondary", "⏩") + btn_ignore.clicked.connect(self.on_ignore) + button_layout.addWidget(btn_ignore) + + btn_exit = self._create_button("Exit Application", "danger", "❌") + btn_exit.clicked.connect(self.on_exit) + button_layout.addWidget(btn_exit) + + button_area.setLayout(button_layout) + main_layout.addWidget(button_area) + + self.setLayout(main_layout) + + def _create_button(self, text, style_type, icon): + btn = QPushButton(f" {icon} {text}") + btn.setFont(QFont("Segoe UI", 11, QFont.Weight.Medium)) + btn.setFixedHeight(45) + btn.setCursor(Qt.CursorShape.PointingHandCursor) + + if style_type == "secondary": + btn.setStyleSheet(""" + QPushButton { + background: #334155; color: #e2e8f0; border: 1px solid #475569; + border-radius: 6px; font-weight: 600; + } + QPushButton:hover { background: #475569; } + """) + elif style_type == "danger": + btn.setStyleSheet(""" + QPushButton { + background: #64748b; color: #e2e8f0; border: 1px solid #475569; + border-radius: 6px; font-weight: 600; + } + QPushButton:hover { background: #ef4444; border: 1px solid #dc2626; } + """) + return btn + + def _create_small_button(self, text, icon): + btn = QPushButton(f"{icon} {text}") + btn.setFixedHeight(30) + btn.setCursor(Qt.CursorShape.PointingHandCursor) + btn.setStyleSheet(""" + QPushButton { + background: #334155; + color: #cbd5e1; + border-radius: 4px; + font-size: 10pt; + border: 1px solid #475569; + padding: 0 10px; /* Adds a little breathing room on the sides */ + } + QPushButton:hover { background: #475569; } + """) + return btn + + def _handle_copy(self, button, text): + # 1. Perform the copy + QApplication.clipboard().setText(text) + + # 2. Save the original style so we can restore it + original_style = button.styleSheet() + + # 3. Change to a "Success" green color + button.setStyleSheet(""" + QPushButton { + background: #22c55e; + color: white; + border-radius: 4px; + font-size: 10pt; + border: 1px solid #16a34a; + padding: 0 10px; + } + """) + + # 4. Use a single-shot timer to reset the style after 500ms + QTimer.singleShot(500, lambda: button.setStyleSheet(original_style)) + + def on_ignore(self): + self.user_choice = self.IGNORE + self.accept() + + def on_exit(self): + self.user_choice = self.EXIT + self.reject() + + def show_window(self): + """Executes the dialog and returns the user's choice.""" + self.exec() + return self.user_choice + + +def stand_alone_show(error_stack: ErrorStack): + app = QApplication.instance() or QApplication(sys.argv) + dialog = ErrorStackPrompt(error_stack) + dialog.exec() + return dialog.user_choice +