Initial commit
This commit is contained in:
322
geniusia2/gui/suggestion_overlay.py
Normal file
322
geniusia2/gui/suggestion_overlay.py
Normal file
@@ -0,0 +1,322 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user