Files
Geniusia_v2/geniusia2/gui/dialogs/post_action_notification.py
2026-03-05 00:20:25 +01:00

402 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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_())