""" Superposition de suggestion pour afficher les actions suggérées avec surlignage visuel des éléments UI """ from PyQt5.QtWidgets import QWidget, QLabel, QVBoxLayout, QHBoxLayout, QPushButton from PyQt5.QtCore import Qt, QRect, QTimer, pyqtSignal from PyQt5.QtGui import QPainter, QColor, QPen, QFont, QBrush from typing import Optional, Dict, Any, Tuple import logging class SuggestionOverlay(QWidget): """ Superposition transparente pour afficher les suggestions d'actions avec surlignage visuel des éléments UI suggérés """ # Signal émis quand l'utilisateur donne un retour feedback_received = pyqtSignal(str) # "accept", "reject", "correct" def __init__(self, decision: Dict[str, Any], parent=None): """ Initialiser la superposition de suggestion Args: decision: Dictionnaire contenant les détails de la décision { "action_type": str, "target_element": str, "bbox": (x, y, w, h), "confidence": float, "description": str (optionnel) } parent: Widget parent (optionnel) """ super().__init__(parent) self.decision = decision self.logger = logging.getLogger(__name__) self.feedback_result = None self.init_ui() self.setup_shortcuts() self.logger.info(f"SuggestionOverlay créée pour action: {decision.get('action_type')}") def init_ui(self): """Initialiser l'interface de la superposition""" # Fenêtre sans bordure, toujours au-dessus, transparente self.setWindowFlags( Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint | Qt.Tool ) self.setAttribute(Qt.WA_TranslucentBackground) # Plein écran from PyQt5.QtWidgets import QApplication screen = QApplication.primaryScreen().geometry() self.setGeometry(screen) # Extraire les informations de la décision action_type = self.decision.get("action_type", "action") target = self.decision.get("target_element", "élément") confidence = self.decision.get("confidence", 0.0) description = self.decision.get("description", "") # Créer le panneau d'information self.info_panel = QWidget(self) self.info_panel.setStyleSheet(""" QWidget { background-color: rgba(33, 150, 243, 230); border-radius: 10px; padding: 15px; } """) panel_layout = QVBoxLayout() self.info_panel.setLayout(panel_layout) # Titre title_label = QLabel("🤝 Suggestion d'Action") title_label.setFont(QFont("Arial", 14, QFont.Bold)) title_label.setStyleSheet("color: white;") panel_layout.addWidget(title_label) # Détails de l'action action_text = f"Action: {action_type.upper()}" action_label = QLabel(action_text) action_label.setFont(QFont("Arial", 11)) action_label.setStyleSheet("color: white;") panel_layout.addWidget(action_label) target_text = f"Élément: {target}" target_label = QLabel(target_text) target_label.setFont(QFont("Arial", 11)) target_label.setStyleSheet("color: white;") panel_layout.addWidget(target_label) confidence_text = f"Confiance: {confidence * 100:.1f}%" confidence_label = QLabel(confidence_text) confidence_label.setFont(QFont("Arial", 11)) confidence_label.setStyleSheet("color: white;") panel_layout.addWidget(confidence_label) if description: desc_label = QLabel(description) desc_label.setFont(QFont("Arial", 10)) desc_label.setStyleSheet("color: rgba(255, 255, 255, 200);") desc_label.setWordWrap(True) panel_layout.addWidget(desc_label) # Boutons de contrôle button_layout = QHBoxLayout() accept_button = QPushButton("✓ Accepter (Entrée)") accept_button.setFont(QFont("Arial", 10)) accept_button.setStyleSheet(""" QPushButton { background-color: #4CAF50; color: white; border: none; padding: 8px 15px; border-radius: 5px; } QPushButton:hover { background-color: #45a049; } """) accept_button.clicked.connect(lambda: self.on_feedback("accept")) button_layout.addWidget(accept_button) reject_button = QPushButton("✗ Refuser (Échap)") reject_button.setFont(QFont("Arial", 10)) reject_button.setStyleSheet(""" QPushButton { background-color: #f44336; color: white; border: none; padding: 8px 15px; border-radius: 5px; } QPushButton:hover { background-color: #da190b; } """) reject_button.clicked.connect(lambda: self.on_feedback("reject")) button_layout.addWidget(reject_button) correct_button = QPushButton("✎ Corriger (Alt+C)") correct_button.setFont(QFont("Arial", 10)) correct_button.setStyleSheet(""" QPushButton { background-color: #FF9800; color: white; border: none; padding: 8px 15px; border-radius: 5px; } QPushButton:hover { background-color: #e68900; } """) correct_button.clicked.connect(lambda: self.on_feedback("correct")) button_layout.addWidget(correct_button) panel_layout.addLayout(button_layout) # Positionner le panneau en haut au centre panel_width = 400 panel_height = 250 screen_width = self.width() panel_x = (screen_width - panel_width) // 2 panel_y = 50 self.info_panel.setGeometry(panel_x, panel_y, panel_width, panel_height) def setup_shortcuts(self): """Configurer les raccourcis clavier""" from PyQt5.QtWidgets import QShortcut from PyQt5.QtGui import QKeySequence # Entrée pour accepter accept_shortcut = QShortcut(QKeySequence(Qt.Key_Return), self) accept_shortcut.activated.connect(lambda: self.on_feedback("accept")) enter_shortcut = QShortcut(QKeySequence(Qt.Key_Enter), self) enter_shortcut.activated.connect(lambda: self.on_feedback("accept")) # Échap pour refuser reject_shortcut = QShortcut(QKeySequence(Qt.Key_Escape), self) reject_shortcut.activated.connect(lambda: self.on_feedback("reject")) # Alt+C pour corriger correct_shortcut = QShortcut(QKeySequence("Alt+C"), self) correct_shortcut.activated.connect(lambda: self.on_feedback("correct")) def paintEvent(self, event): """Dessiner la superposition avec surlignage de l'élément""" painter = QPainter(self) painter.setRenderHint(QPainter.Antialiasing) # Fond semi-transparent painter.fillRect(self.rect(), QColor(0, 0, 0, 100)) # Surligner l'élément UI suggéré bbox = self.decision.get("bbox") if bbox: x, y, w, h = bbox # Rectangle de surlignage avec bordure épaisse pen = QPen(QColor(33, 150, 243), 4, Qt.SolidLine) painter.setPen(pen) # Fond légèrement transparent pour l'élément brush = QBrush(QColor(33, 150, 243, 50)) painter.setBrush(brush) # Dessiner le rectangle de surlignage highlight_rect = QRect(x - 5, y - 5, w + 10, h + 10) painter.drawRect(highlight_rect) # Ajouter une animation de pulsation (optionnel) # Pour l'instant, on garde un surlignage statique def on_feedback(self, feedback: str): """ Gestionnaire de retour utilisateur Args: feedback: Type de retour ("accept", "reject", "correct") """ self.feedback_result = feedback self.feedback_received.emit(feedback) self.logger.info(f"Retour reçu: {feedback}") self.close() def wait_for_feedback(self) -> str: """ Attendre le retour utilisateur (bloquant) Returns: str: Type de retour ("accept", "reject", "correct") """ # Afficher la superposition self.show() # Boucle d'événements locale pour attendre le retour from PyQt5.QtCore import QEventLoop loop = QEventLoop() self.feedback_received.connect(loop.quit) loop.exec_() return self.feedback_result or "reject" def show_suggestion(self, decision: Dict[str, Any]) -> str: """ Méthode statique pour afficher une suggestion et attendre le retour Args: decision: Dictionnaire contenant les détails de la décision Returns: str: Type de retour ("accept", "reject", "correct") """ overlay = SuggestionOverlay(decision) return overlay.wait_for_feedback() class AnimatedSuggestionOverlay(SuggestionOverlay): """ Version animée de la superposition avec effet de pulsation """ def __init__(self, decision: Dict[str, Any], parent=None): super().__init__(decision, parent) # Timer pour l'animation self.animation_timer = QTimer(self) self.animation_timer.timeout.connect(self.animate) self.animation_timer.start(500) # Pulse toutes les 500ms self.pulse_state = 0 def animate(self): """Animer le surlignage avec effet de pulsation""" self.pulse_state = (self.pulse_state + 1) % 2 self.update() # Redessiner def paintEvent(self, event): """Dessiner avec animation de pulsation""" painter = QPainter(self) painter.setRenderHint(QPainter.Antialiasing) # Fond semi-transparent painter.fillRect(self.rect(), QColor(0, 0, 0, 100)) # Surligner l'élément UI suggéré avec pulsation bbox = self.decision.get("bbox") if bbox: x, y, w, h = bbox # Varier l'épaisseur et l'opacité selon l'état de pulsation pen_width = 4 if self.pulse_state == 0 else 6 alpha = 200 if self.pulse_state == 0 else 255 pen = QPen(QColor(33, 150, 243, alpha), pen_width, Qt.SolidLine) painter.setPen(pen) brush_alpha = 50 if self.pulse_state == 0 else 80 brush = QBrush(QColor(33, 150, 243, brush_alpha)) painter.setBrush(brush) # Dessiner le rectangle de surlignage padding = 5 if self.pulse_state == 0 else 8 highlight_rect = QRect(x - padding, y - padding, w + 2*padding, h + 2*padding) painter.drawRect(highlight_rect) def close(self): """Arrêter l'animation avant de fermer""" self.animation_timer.stop() super().close()