Initial commit
Dieser Commit ist enthalten in:
312
views/widgets/progress_modal.py
Normale Datei
312
views/widgets/progress_modal.py
Normale Datei
@ -0,0 +1,312 @@
|
||||
"""
|
||||
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)
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren