forked from Support/sp-hydra-veil-gui
New UI prompt for stacktrace errors
This commit is contained in:
parent
539215299f
commit
8b1265d0d9
3 changed files with 253 additions and 2 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
249
gui/v2/ui/popups/stack_error_popup.py
Normal file
249
gui/v2/ui/popups/stack_error_popup.py
Normal file
|
|
@ -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
|
||||
|
||||
Loading…
Reference in a new issue