""" 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_())