Files
AccountForger-neuerUpload/views/widgets/progress_modal.py
Claude Project Manager 04585e95b6 Initial commit
2025-08-01 23:50:28 +02:00

312 Zeilen
12 KiB
Python

"""
Progress Modal - Basis-Klasse für Prozess-Modals
"""
import logging
from typing import Optional, Dict, Any
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QFrame, QGraphicsBlurEffect
)
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QPropertyAnimation, QEasingCurve
from PyQt5.QtGui import QFont, QMovie, QPainter, QBrush, QColor
from views.widgets.forge_animation_widget import ForgeAnimationWidget
from styles.modal_styles import ModalStyles
logger = logging.getLogger("progress_modal")
class ProgressModal(QDialog):
"""
Basis-Klasse für Progress-Modals während Automatisierungsprozessen.
Zeigt den Benutzer eine nicht-unterbrechbare Fortschrittsanzeige.
"""
# Signale
force_closed = pyqtSignal() # Wird ausgelöst wenn Modal zwangsweise geschlossen wird
def __init__(self, parent=None, modal_type: str = "generic", language_manager=None, style_manager=None):
super().__init__(parent)
self.modal_type = modal_type
self.language_manager = language_manager
self.style_manager = style_manager or ModalStyles()
self.is_process_running = False
self.auto_close_timer = None
self.fade_animation = None
# Modal-Texte laden
self.modal_texts = self.style_manager.get_modal_texts()
self.init_ui()
self.setup_animations()
if self.language_manager:
self.language_manager.language_changed.connect(self.update_texts)
self.update_texts()
def init_ui(self):
"""Initialisiert die UI nach AccountForger Styleguide"""
# Modal-Eigenschaften
self.setModal(True)
self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint)
# Transparenz entfernt - solider Hintergrund
# self.setAttribute(Qt.WA_TranslucentBackground)
self.setFixedSize(
self.style_manager.SIZES['modal_width'],
self.style_manager.SIZES['modal_height']
)
# Zentriere auf Parent oder Bildschirm
if self.parent():
parent_rect = self.parent().geometry()
x = parent_rect.x() + (parent_rect.width() - self.width()) // 2
y = parent_rect.y() + (parent_rect.height() - self.height()) // 2
self.move(x, y)
# Hauptlayout
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
# Modal-Container mit solidem Hintergrund
self.modal_container = QFrame()
self.modal_container.setObjectName("modal_container")
self.modal_container.setStyleSheet(self.style_manager.get_modal_container_style())
# Container-Layout
container_layout = QVBoxLayout(self.modal_container)
padding = self.style_manager.SIZES['padding_large']
container_layout.setContentsMargins(padding, padding, padding, padding)
container_layout.setSpacing(self.style_manager.SIZES['spacing_default'])
# Titel
self.title_label = QLabel()
self.title_label.setAlignment(Qt.AlignCenter)
self.title_label.setObjectName("modal_title")
self.title_label.setFont(self.style_manager.create_font('title'))
self.title_label.setStyleSheet(self.style_manager.get_title_label_style())
container_layout.addWidget(self.title_label)
# Animation Widget (Spinner/Forge Animation)
self.animation_widget = ForgeAnimationWidget()
animation_size = self.style_manager.SIZES['animation_size']
self.animation_widget.setFixedSize(animation_size, animation_size)
self.animation_widget.setAlignment(Qt.AlignCenter)
animation_layout = QHBoxLayout()
animation_layout.addStretch()
animation_layout.addWidget(self.animation_widget)
animation_layout.addStretch()
container_layout.addLayout(animation_layout)
# Subtitle/Status
self.status_label = QLabel()
self.status_label.setAlignment(Qt.AlignCenter)
self.status_label.setObjectName("modal_status")
self.status_label.setFont(self.style_manager.create_font('status'))
self.status_label.setStyleSheet(self.style_manager.get_status_label_style())
container_layout.addWidget(self.status_label)
# Detail-Status (optional)
self.detail_label = QLabel()
self.detail_label.setAlignment(Qt.AlignCenter)
self.detail_label.setObjectName("modal_detail")
self.detail_label.setVisible(False)
self.detail_label.setFont(self.style_manager.create_font('detail'))
self.detail_label.setStyleSheet(self.style_manager.get_detail_label_style())
container_layout.addWidget(self.detail_label)
layout.addWidget(self.modal_container)
# Failsafe Timer
self.failsafe_timer = QTimer()
self.failsafe_timer.setSingleShot(True)
self.failsafe_timer.timeout.connect(self._force_close)
def setup_animations(self):
"""Richtet Fade-Animationen ein"""
self.fade_animation = QPropertyAnimation(self, b"windowOpacity")
self.fade_animation.setDuration(self.style_manager.ANIMATIONS['fade_duration'])
self.fade_animation.setEasingCurve(QEasingCurve.OutCubic)
def _get_modal_texts(self) -> Dict[str, Dict[str, str]]:
"""Gibt die Modal-Texte zurück"""
# Diese Methode ist jetzt nur für Rückwärtskompatibilität
# Die echten Texte kommen vom style_manager
return self.style_manager.get_modal_texts()
def show_process(self, process_type: Optional[str] = None):
"""
Zeigt das Modal für einen bestimmten Prozess-Typ an.
Args:
process_type: Optional - überschreibt den Modal-Typ
"""
if process_type:
self.modal_type = process_type
self.is_process_running = True
# Texte aktualisieren
self.update_texts()
# Animation starten
self.animation_widget.start_animation()
# Failsafe Timer starten
self.failsafe_timer.start(self.style_manager.ANIMATIONS['failsafe_timeout'])
# Modal anzeigen ohne Fade-In (immer voll sichtbar)
self.setWindowOpacity(1.0)
self.show()
# Fade-Animation deaktiviert für volle Sichtbarkeit
# if self.fade_animation:
# self.fade_animation.setStartValue(0.0)
# self.fade_animation.setEndValue(1.0)
# self.fade_animation.start()
logger.info(f"Progress Modal angezeigt für: {self.modal_type}")
def hide_process(self):
"""Versteckt das Modal mit Fade-Out Animation"""
if not self.is_process_running:
return
self.is_process_running = False
# Timer stoppen
self.failsafe_timer.stop()
# Animation stoppen
self.animation_widget.stop_animation()
# Fade-Out Animation deaktiviert - sofort verstecken
self._finish_hide()
# if self.fade_animation:
# self.fade_animation.setStartValue(1.0)
# self.fade_animation.setEndValue(0.0)
# self.fade_animation.finished.connect(self._finish_hide)
# self.fade_animation.start()
# else:
# self._finish_hide()
logger.info(f"Progress Modal versteckt für: {self.modal_type}")
def _finish_hide(self):
"""Beendet das Verstecken des Modals"""
self.hide()
if self.fade_animation:
self.fade_animation.finished.disconnect()
def update_status(self, status: str, detail: str = None):
"""
Aktualisiert den Status-Text des Modals.
Args:
status: Haupt-Status-Text
detail: Optional - Detail-Text
"""
self.status_label.setText(status)
if detail:
self.detail_label.setText(detail)
self.detail_label.setVisible(True)
else:
self.detail_label.setVisible(False)
def show_error(self, error_message: str, auto_close_seconds: int = 3):
"""
Zeigt eine Fehlermeldung im Modal an.
Args:
error_message: Fehlermeldung
auto_close_seconds: Sekunden bis automatisches Schließen
"""
self.title_label.setText("❌ Fehler aufgetreten")
self.status_label.setText(error_message)
self.detail_label.setVisible(False)
# Animation stoppen
self.animation_widget.stop_animation()
# Auto-Close Timer
if auto_close_seconds > 0:
self.auto_close_timer = QTimer()
self.auto_close_timer.setSingleShot(True)
self.auto_close_timer.timeout.connect(self.hide_process)
self.auto_close_timer.start(auto_close_seconds * 1000)
def _force_close(self):
"""Zwangsschließung nach Timeout"""
logger.warning(f"Progress Modal Timeout erreicht für: {self.modal_type}")
self.force_closed.emit()
self.hide_process()
def update_texts(self):
"""Aktualisiert die Texte gemäß der aktuellen Sprache"""
if not self.language_manager:
# Fallback zu Standardtexten
texts = self.modal_texts.get(self.modal_type, self.modal_texts['generic'])
self.title_label.setText(texts['title'])
self.status_label.setText(texts['status'])
if texts['detail']:
self.detail_label.setText(texts['detail'])
self.detail_label.setVisible(True)
return
# Multilingual texts (für zukünftige Erweiterung)
title_key = f"modal.{self.modal_type}.title"
status_key = f"modal.{self.modal_type}.status"
detail_key = f"modal.{self.modal_type}.detail"
# Fallback zu Standardtexten wenn Übersetzung nicht vorhanden
texts = self.modal_texts.get(self.modal_type, self.modal_texts['generic'])
self.title_label.setText(
self.language_manager.get_text(title_key, texts['title'])
)
self.status_label.setText(
self.language_manager.get_text(status_key, texts['status'])
)
detail_text = self.language_manager.get_text(detail_key, texts['detail'])
if detail_text:
self.detail_label.setText(detail_text)
self.detail_label.setVisible(True)
else:
self.detail_label.setVisible(False)
def paintEvent(self, event):
"""Custom Paint Event für soliden Hintergrund"""
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# Solider Hintergrund (komplett undurchsichtig)
overlay_color = QColor(self.style_manager.COLORS['overlay'])
brush = QBrush(overlay_color)
painter.fillRect(self.rect(), brush)
super().paintEvent(event)
def keyPressEvent(self, event):
"""Verhindert das Schließen mit Escape während Prozess läuft"""
if self.is_process_running and event.key() == Qt.Key_Escape:
event.ignore()
return
super().keyPressEvent(event)
def closeEvent(self, event):
"""Verhindert das Schließen während Prozess läuft"""
if self.is_process_running:
event.ignore()
return
super().closeEvent(event)