Initial commit
This commit is contained in:
344
geniusia2/gui/interactive_dialog.py
Normal file
344
geniusia2/gui/interactive_dialog.py
Normal file
@@ -0,0 +1,344 @@
|
||||
"""
|
||||
InteractiveDialog - Dialogue interactif avec timeout automatique
|
||||
Permet de demander confirmation à l'utilisateur de manière non-bloquante
|
||||
"""
|
||||
|
||||
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton
|
||||
from PyQt5.QtCore import Qt, QTimer, pyqtSignal
|
||||
from PyQt5.QtGui import QFont
|
||||
from typing import Callable, Optional
|
||||
import logging
|
||||
|
||||
|
||||
class InteractiveDialog(QDialog):
|
||||
"""
|
||||
Dialogue interactif avec timeout automatique.
|
||||
|
||||
Caractéristiques:
|
||||
- Non-bloquant (l'application continue)
|
||||
- Timeout de 10 secondes par défaut
|
||||
- Callbacks pour accept/reject
|
||||
- Fermeture automatique après timeout
|
||||
- Compte à rebours visible
|
||||
|
||||
Signals:
|
||||
accepted: Émis quand l'utilisateur accepte
|
||||
rejected: Émis quand l'utilisateur refuse
|
||||
timeout: Émis quand le timeout est atteint
|
||||
"""
|
||||
|
||||
accepted = pyqtSignal()
|
||||
rejected = pyqtSignal()
|
||||
timeout = pyqtSignal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
title: str,
|
||||
message: str,
|
||||
on_accept: Optional[Callable] = None,
|
||||
on_reject: Optional[Callable] = None,
|
||||
timeout_seconds: int = 10,
|
||||
parent=None
|
||||
):
|
||||
"""
|
||||
Initialise le dialogue interactif.
|
||||
|
||||
Args:
|
||||
title: Titre du dialogue
|
||||
message: Message à afficher
|
||||
on_accept: Callback appelé si l'utilisateur accepte
|
||||
on_reject: Callback appelé si l'utilisateur refuse
|
||||
timeout_seconds: Durée du timeout en secondes (défaut: 10)
|
||||
parent: Widget parent (optionnel)
|
||||
"""
|
||||
super().__init__(parent)
|
||||
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.on_accept_callback = on_accept
|
||||
self.on_reject_callback = on_reject
|
||||
self.timeout_seconds = timeout_seconds
|
||||
self.remaining_seconds = timeout_seconds
|
||||
self.result_value = None
|
||||
|
||||
# Configuration du dialogue
|
||||
self.setWindowTitle(title)
|
||||
self.setModal(False) # Non-bloquant
|
||||
self.setWindowFlags(
|
||||
Qt.Dialog |
|
||||
Qt.WindowStaysOnTopHint |
|
||||
Qt.WindowCloseButtonHint
|
||||
)
|
||||
|
||||
# Initialiser l'interface
|
||||
self._init_ui(title, message)
|
||||
|
||||
# Démarrer le timer de timeout
|
||||
self._start_timeout_timer()
|
||||
|
||||
self.logger.info(f"InteractiveDialog créé: {title}")
|
||||
|
||||
def _init_ui(self, title: str, message: str):
|
||||
"""Initialise l'interface utilisateur."""
|
||||
# Layout principal
|
||||
layout = QVBoxLayout()
|
||||
layout.setContentsMargins(20, 20, 20, 20)
|
||||
layout.setSpacing(15)
|
||||
self.setLayout(layout)
|
||||
|
||||
# Icône et titre
|
||||
title_layout = QHBoxLayout()
|
||||
title_icon = QLabel("💡")
|
||||
title_icon.setFont(QFont("Arial", 24))
|
||||
title_layout.addWidget(title_icon)
|
||||
|
||||
title_label = QLabel(title)
|
||||
title_label.setFont(QFont("Arial", 14, QFont.Bold))
|
||||
title_label.setWordWrap(True)
|
||||
title_layout.addWidget(title_label, 1)
|
||||
|
||||
layout.addLayout(title_layout)
|
||||
|
||||
# Message
|
||||
message_label = QLabel(message)
|
||||
message_label.setFont(QFont("Arial", 11))
|
||||
message_label.setWordWrap(True)
|
||||
message_label.setStyleSheet("color: #333; padding: 10px 0;")
|
||||
layout.addWidget(message_label)
|
||||
|
||||
# Compte à rebours
|
||||
self.countdown_label = QLabel(f"⏱️ Fermeture auto dans {self.remaining_seconds}s")
|
||||
self.countdown_label.setFont(QFont("Arial", 9))
|
||||
self.countdown_label.setStyleSheet("color: #999;")
|
||||
self.countdown_label.setAlignment(Qt.AlignCenter)
|
||||
layout.addWidget(self.countdown_label)
|
||||
|
||||
# Boutons
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.setSpacing(10)
|
||||
|
||||
# Bouton Refuser
|
||||
self.reject_button = QPushButton("Non, continue")
|
||||
self.reject_button.setFont(QFont("Arial", 10))
|
||||
self.reject_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #da190b;
|
||||
}
|
||||
""")
|
||||
self.reject_button.clicked.connect(self._on_reject)
|
||||
button_layout.addWidget(self.reject_button)
|
||||
|
||||
# Bouton Accepter
|
||||
self.accept_button = QPushButton("Oui, essaie !")
|
||||
self.accept_button.setFont(QFont("Arial", 10))
|
||||
self.accept_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
""")
|
||||
self.accept_button.clicked.connect(self._on_accept)
|
||||
button_layout.addWidget(self.accept_button)
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
# Ajuster la taille
|
||||
self.setMinimumWidth(400)
|
||||
self.adjustSize()
|
||||
|
||||
def _start_timeout_timer(self):
|
||||
"""Démarre le timer de timeout."""
|
||||
self.timeout_timer = QTimer(self)
|
||||
self.timeout_timer.timeout.connect(self._on_timeout_tick)
|
||||
self.timeout_timer.start(1000) # 1 seconde
|
||||
|
||||
def _on_timeout_tick(self):
|
||||
"""Gestionnaire du tick du timer."""
|
||||
self.remaining_seconds -= 1
|
||||
|
||||
if self.remaining_seconds > 0:
|
||||
# Mettre à jour le compte à rebours
|
||||
self.countdown_label.setText(
|
||||
f"⏱️ Fermeture auto dans {self.remaining_seconds}s"
|
||||
)
|
||||
else:
|
||||
# Timeout atteint
|
||||
self._on_timeout()
|
||||
|
||||
def _on_accept(self):
|
||||
"""Gestionnaire du bouton Accepter."""
|
||||
self.timeout_timer.stop()
|
||||
self.result_value = "accept"
|
||||
|
||||
# Émettre le signal
|
||||
self.accepted.emit()
|
||||
|
||||
# Appeler le callback
|
||||
if self.on_accept_callback:
|
||||
try:
|
||||
self.on_accept_callback()
|
||||
except Exception as e:
|
||||
self.logger.error(f"Erreur dans on_accept callback: {e}")
|
||||
|
||||
self.logger.info("Dialogue accepté")
|
||||
self.accept()
|
||||
|
||||
def _on_reject(self):
|
||||
"""Gestionnaire du bouton Refuser."""
|
||||
self.timeout_timer.stop()
|
||||
self.result_value = "reject"
|
||||
|
||||
# Émettre le signal
|
||||
self.rejected.emit()
|
||||
|
||||
# Appeler le callback
|
||||
if self.on_reject_callback:
|
||||
try:
|
||||
self.on_reject_callback()
|
||||
except Exception as e:
|
||||
self.logger.error(f"Erreur dans on_reject callback: {e}")
|
||||
|
||||
self.logger.info("Dialogue refusé")
|
||||
self.reject()
|
||||
|
||||
def _on_timeout(self):
|
||||
"""Gestionnaire du timeout."""
|
||||
self.timeout_timer.stop()
|
||||
self.result_value = "timeout"
|
||||
|
||||
# Émettre le signal
|
||||
self.timeout.emit()
|
||||
|
||||
# Par défaut, timeout = reject
|
||||
if self.on_reject_callback:
|
||||
try:
|
||||
self.on_reject_callback()
|
||||
except Exception as e:
|
||||
self.logger.error(f"Erreur dans on_reject callback (timeout): {e}")
|
||||
|
||||
self.logger.info("Dialogue timeout")
|
||||
self.reject()
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Gestionnaire de fermeture du dialogue."""
|
||||
# Arrêter le timer
|
||||
if hasattr(self, 'timeout_timer'):
|
||||
self.timeout_timer.stop()
|
||||
|
||||
# Si pas de résultat, considérer comme reject
|
||||
if self.result_value is None:
|
||||
self.result_value = "reject"
|
||||
if self.on_reject_callback:
|
||||
try:
|
||||
self.on_reject_callback()
|
||||
except Exception as e:
|
||||
self.logger.error(f"Erreur dans on_reject callback (close): {e}")
|
||||
|
||||
event.accept()
|
||||
|
||||
@staticmethod
|
||||
def show_dialog(
|
||||
title: str,
|
||||
message: str,
|
||||
on_accept: Optional[Callable] = None,
|
||||
on_reject: Optional[Callable] = None,
|
||||
timeout_seconds: int = 10,
|
||||
parent=None
|
||||
) -> 'InteractiveDialog':
|
||||
"""
|
||||
Méthode statique pour afficher un dialogue.
|
||||
|
||||
Args:
|
||||
title: Titre du dialogue
|
||||
message: Message à afficher
|
||||
on_accept: Callback appelé si l'utilisateur accepte
|
||||
on_reject: Callback appelé si l'utilisateur refuse
|
||||
timeout_seconds: Durée du timeout en secondes
|
||||
parent: Widget parent
|
||||
|
||||
Returns:
|
||||
Instance du dialogue créé
|
||||
|
||||
Examples:
|
||||
>>> def on_yes():
|
||||
... print("Utilisateur a accepté")
|
||||
>>> def on_no():
|
||||
... print("Utilisateur a refusé")
|
||||
>>> dialog = InteractiveDialog.show_dialog(
|
||||
... "Confirmation",
|
||||
... "Voulez-vous continuer ?",
|
||||
... on_yes,
|
||||
... on_no
|
||||
... )
|
||||
"""
|
||||
dialog = InteractiveDialog(
|
||||
title, message, on_accept, on_reject, timeout_seconds, parent
|
||||
)
|
||||
dialog.show()
|
||||
return dialog
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
"""Test de l'InteractiveDialog"""
|
||||
import sys
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
print("=" * 60)
|
||||
print("Test de l'InteractiveDialog")
|
||||
print("=" * 60)
|
||||
|
||||
# Callbacks de test
|
||||
def on_accept():
|
||||
print("\n✅ Utilisateur a ACCEPTÉ")
|
||||
|
||||
def on_reject():
|
||||
print("\n❌ Utilisateur a REFUSÉ")
|
||||
|
||||
# Test 1: Dialogue avec timeout de 10 secondes
|
||||
print("\nTest 1: Dialogue avec timeout de 10 secondes")
|
||||
print("Attendez 10 secondes ou cliquez sur un bouton")
|
||||
|
||||
dialog = InteractiveDialog.show_dialog(
|
||||
"J'ai une idée !",
|
||||
"J'ai remarqué que vous faites souvent:\n"
|
||||
"\"Calculer 9/9 dans la calculatrice\"\n\n"
|
||||
"Est-ce que je peux essayer de vous suggérer\n"
|
||||
"cette action la prochaine fois ?",
|
||||
on_accept,
|
||||
on_reject,
|
||||
timeout_seconds=10
|
||||
)
|
||||
|
||||
# Connecter les signaux pour les tests
|
||||
dialog.accepted.connect(lambda: print("Signal 'accepted' émis"))
|
||||
dialog.rejected.connect(lambda: print("Signal 'rejected' émis"))
|
||||
dialog.timeout.connect(lambda: print("Signal 'timeout' émis"))
|
||||
|
||||
# Test 2: Créer un deuxième dialogue après 3 secondes
|
||||
def create_second_dialog():
|
||||
print("\nTest 2: Deuxième dialogue avec timeout de 5 secondes")
|
||||
dialog2 = InteractiveDialog.show_dialog(
|
||||
"Changement de mode",
|
||||
"Voulez-vous passer en mode Suggestions ?",
|
||||
lambda: print("\n✅ Mode Suggestions activé"),
|
||||
lambda: print("\n❌ Reste en mode Shadow"),
|
||||
timeout_seconds=5
|
||||
)
|
||||
|
||||
QTimer.singleShot(3000, create_second_dialog)
|
||||
|
||||
sys.exit(app.exec_())
|
||||
Reference in New Issue
Block a user