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