402 lines
14 KiB
Python
402 lines
14 KiB
Python
"""
|
||
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_())
|