""" Notifications post-action pour afficher le succès/échec des actions avec possibilité de retour correctif """ from PyQt5.QtWidgets import QWidget, QLabel, QVBoxLayout, QHBoxLayout, QPushButton from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QPropertyAnimation, QEasingCurve, QPoint from PyQt5.QtGui import QFont, QPainter, QColor from typing import Optional, Dict, Any, Callable import logging class PostActionNotification(QWidget): """ Notification post-action affichant le résultat d'une action automatisée avec indicateur de succès (✔️) ou d'échec (❌) et possibilité de correction """ # Signaux correction_requested = pyqtSignal(dict) # Émis quand l'utilisateur demande une correction feedback_provided = pyqtSignal(str) # Émis quand un retour est fourni ("success", "failed", "corrected") def __init__(self, action_result: Dict[str, Any], allow_correction: bool = True, timeout_ms: int = 5000, parent=None): """ Initialiser la notification post-action Args: action_result: Résultat de l'action { "action_type": str, "target_element": str, "result": str ("success" ou "failed"), "confidence": float, "error_message": str (optionnel, si échec) } allow_correction: Permettre le retour correctif (défaut: True) timeout_ms: Timeout en millisecondes avant fermeture auto (défaut: 5000) parent: Widget parent (optionnel) """ super().__init__(parent) self.action_result = action_result self.allow_correction = allow_correction self.timeout_ms = timeout_ms self.logger = logging.getLogger(__name__) self.correction_callback = None self.is_closed = False self.init_ui() self.setup_animation() self.setup_timer() self.logger.info( f"PostActionNotification créée: {action_result.get('action_type')} - " f"{action_result.get('result')}" ) def init_ui(self): """Initialiser l'interface de la notification""" # Fenêtre sans bordure, toujours au-dessus self.setWindowFlags( Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint | Qt.Tool ) self.setAttribute(Qt.WA_TranslucentBackground) # Taille fixe self.setFixedSize(350, 120) # Positionner en haut à droite de l'écran from PyQt5.QtWidgets import QApplication screen = QApplication.primaryScreen().geometry() x = screen.width() - self.width() - 20 y = 20 self.move(x, y) # Widget conteneur avec fond self.container = QWidget(self) self.container.setGeometry(0, 0, self.width(), self.height()) # Déterminer le style selon le résultat result = self.action_result.get("result", "failed") if result == "success": bg_color = "#4CAF50" icon = "✔️" title = "Action Réussie" else: bg_color = "#f44336" icon = "❌" title = "Action Échouée" self.container.setStyleSheet(f""" QWidget {{ background-color: {bg_color}; border-radius: 10px; }} """) # Layout principal main_layout = QVBoxLayout() self.container.setLayout(main_layout) main_layout.setContentsMargins(15, 10, 15, 10) main_layout.setSpacing(8) # En-tête avec icône et titre header_layout = QHBoxLayout() icon_label = QLabel(icon) icon_label.setFont(QFont("Arial", 20)) header_layout.addWidget(icon_label) title_label = QLabel(title) title_label.setFont(QFont("Arial", 12, QFont.Bold)) title_label.setStyleSheet("color: white;") header_layout.addWidget(title_label) header_layout.addStretch() # Bouton de fermeture close_button = QPushButton("×") close_button.setFont(QFont("Arial", 16, QFont.Bold)) close_button.setFixedSize(25, 25) close_button.setStyleSheet(""" QPushButton { background-color: transparent; color: white; border: none; } QPushButton:hover { background-color: rgba(255, 255, 255, 50); border-radius: 12px; } """) close_button.clicked.connect(self.close_notification) header_layout.addWidget(close_button) main_layout.addLayout(header_layout) # Détails de l'action action_type = self.action_result.get("action_type", "action") target = self.action_result.get("target_element", "élément") details_text = f"{action_type.upper()} sur '{target}'" details_label = QLabel(details_text) details_label.setFont(QFont("Arial", 10)) details_label.setStyleSheet("color: white;") details_label.setWordWrap(True) main_layout.addWidget(details_label) # Message d'erreur si échec if result == "failed": error_msg = self.action_result.get("error_message", "Erreur inconnue") error_label = QLabel(f"Erreur: {error_msg}") error_label.setFont(QFont("Arial", 9)) error_label.setStyleSheet("color: rgba(255, 255, 255, 200);") error_label.setWordWrap(True) main_layout.addWidget(error_label) # Bouton de correction (si autorisé et échec) if self.allow_correction and result == "failed": correction_button = QPushButton("✎ Corriger") correction_button.setFont(QFont("Arial", 9)) correction_button.setStyleSheet(""" QPushButton { background-color: rgba(255, 255, 255, 200); color: #f44336; border: none; padding: 5px 10px; border-radius: 5px; } QPushButton:hover { background-color: white; } """) correction_button.clicked.connect(self.on_correction_requested) main_layout.addWidget(correction_button) # Barre de progression du timeout self.progress_bar = QWidget(self.container) self.progress_bar.setGeometry(0, self.height() - 3, self.width(), 3) self.progress_bar.setStyleSheet(f""" QWidget {{ background-color: rgba(255, 255, 255, 150); }} """) def setup_animation(self): """Configurer l'animation d'entrée""" # Animation de glissement depuis la droite from PyQt5.QtWidgets import QApplication screen = QApplication.primaryScreen().geometry() start_x = screen.width() end_x = screen.width() - self.width() - 20 self.animation = QPropertyAnimation(self, b"pos") self.animation.setDuration(300) self.animation.setStartValue(QPoint(start_x, 20)) self.animation.setEndValue(QPoint(end_x, 20)) self.animation.setEasingCurve(QEasingCurve.OutCubic) def setup_timer(self): """Configurer le timer de fermeture automatique""" self.close_timer = QTimer(self) self.close_timer.timeout.connect(self.close_notification) self.close_timer.setSingleShot(True) # Timer pour l'animation de la barre de progression self.progress_timer = QTimer(self) self.progress_timer.timeout.connect(self.update_progress) self.progress_value = 0 self.progress_step = 100 / (self.timeout_ms / 50) # Mise à jour toutes les 50ms def show_notification(self): """Afficher la notification avec animation""" self.show() self.animation.start() # Démarrer les timers self.close_timer.start(self.timeout_ms) self.progress_timer.start(50) self.logger.info("Notification affichée") def update_progress(self): """Mettre à jour la barre de progression""" self.progress_value += self.progress_step if self.progress_value >= 100: self.progress_value = 100 self.progress_timer.stop() # Réduire la largeur de la barre remaining_width = int(self.width() * (1 - self.progress_value / 100)) self.progress_bar.setGeometry(0, self.height() - 3, remaining_width, 3) def close_notification(self): """Fermer la notification""" if self.is_closed: return self.is_closed = True self.close_timer.stop() self.progress_timer.stop() # Émettre le signal de retour result = self.action_result.get("result", "failed") self.feedback_provided.emit(result) self.logger.info("Notification fermée") self.close() def on_correction_requested(self): """Gestionnaire de demande de correction""" self.close_timer.stop() self.progress_timer.stop() self.correction_requested.emit(self.action_result) self.feedback_provided.emit("corrected") self.logger.info("Correction demandée") self.close() def allow_corrective_feedback(self, callback: Optional[Callable] = None): """ Permettre le retour correctif avec callback optionnel Args: callback: Fonction à appeler quand une correction est demandée """ self.correction_callback = callback if callback: self.correction_requested.connect(lambda data: callback(data)) def mousePressEvent(self, event): """Permettre de fermer en cliquant sur la notification""" if event.button() == Qt.LeftButton: self.close_notification() @staticmethod def show_success(action_type: str, target_element: str, confidence: float = 1.0, timeout_ms: int = 5000, parent=None) -> 'PostActionNotification': """ Afficher une notification de succès Args: action_type: Type d'action target_element: Élément cible confidence: Score de confiance timeout_ms: Timeout en millisecondes parent: Widget parent Returns: Instance de PostActionNotification """ action_result = { "action_type": action_type, "target_element": target_element, "result": "success", "confidence": confidence } notification = PostActionNotification( action_result, allow_correction=False, timeout_ms=timeout_ms, parent=parent ) notification.show_notification() return notification @staticmethod def show_failure(action_type: str, target_element: str, error_message: str = "Erreur inconnue", confidence: float = 0.0, allow_correction: bool = True, timeout_ms: int = 5000, parent=None) -> 'PostActionNotification': """ Afficher une notification d'échec Args: action_type: Type d'action target_element: Élément cible error_message: Message d'erreur confidence: Score de confiance allow_correction: Permettre la correction timeout_ms: Timeout en millisecondes parent: Widget parent Returns: Instance de PostActionNotification """ action_result = { "action_type": action_type, "target_element": target_element, "result": "failed", "confidence": confidence, "error_message": error_message } notification = PostActionNotification( action_result, allow_correction=allow_correction, timeout_ms=timeout_ms, parent=parent ) notification.show_notification() return notification if __name__ == "__main__": """Test des notifications post-action""" from PyQt5.QtWidgets import QApplication import sys app = QApplication(sys.argv) def test_success(): """Test notification de succès""" notification = PostActionNotification.show_success( action_type="click", target_element="valider_button", confidence=0.95 ) notification.feedback_provided.connect( lambda result: print(f"Retour: {result}") ) def test_failure(): """Test notification d'échec""" def on_correction(data): print(f"Correction demandée pour: {data}") notification = PostActionNotification.show_failure( action_type="click", target_element="annuler_button", error_message="Élément non trouvé à l'écran", confidence=0.65, allow_correction=True ) notification.allow_corrective_feedback(on_correction) notification.feedback_provided.connect( lambda result: print(f"Retour: {result}") ) # Tester les deux types test_success() # Tester l'échec après 2 secondes QTimer.singleShot(2000, test_failure) sys.exit(app.exec_())