323 lines
11 KiB
Python
323 lines
11 KiB
Python
"""
|
|
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()
|