""" LogsPanel - Widget Qt pour affichage des logs avec scroll et auto-scroll conditionnel Affiche les messages de log avec timestamp, emoji et formatage. """ from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QScrollArea, QLabel, QFrame ) from PyQt5.QtCore import Qt, QTimer, pyqtSignal from PyQt5.QtGui import QFont from datetime import datetime from typing import List, Optional from dataclasses import dataclass @dataclass class LogMessage: """Message de log pour affichage.""" timestamp: datetime emoji: str message: str level: str # 'info', 'success', 'warning', 'error' technical_details: Optional[str] = None # Pour logs techniques class LogsPanel(QWidget): """ Panneau d'affichage des logs avec scroll automatique conditionnel. Caractéristiques: - Affiche les 5 dernières actions visibles par défaut - Scrollable jusqu'à 30 messages maximum - Auto-scroll uniquement si déjà en bas - Format: HH:MM emoji Message - Supprime automatiquement les messages les plus anciens au-delà de 30 Attributes: max_logs: Nombre maximum de logs à conserver (30) logs: Liste des messages de log log_labels: Liste des widgets QLabel pour chaque log scroll_area: Zone de scroll pour les logs content_widget: Widget contenant tous les logs """ # Signal émis quand un log est ajouté log_added = pyqtSignal(str) def __init__(self, parent=None): """ Initialise le panneau de logs. Args: parent: Widget parent (optionnel) """ super().__init__(parent) self.max_logs = 30 self.logs: List[LogMessage] = [] self.log_labels: List[QLabel] = [] self._init_ui() def _init_ui(self): """Initialise l'interface utilisateur du panneau.""" # Layout principal main_layout = QVBoxLayout() main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setSpacing(0) self.setLayout(main_layout) # Titre du panneau title_label = QLabel("📝 Journal d'activité") title_label.setFont(QFont("Arial", 11, QFont.Bold)) title_label.setStyleSheet(""" QLabel { padding: 8px; background-color: #f5f5f5; border-bottom: 2px solid #e0e0e0; } """) main_layout.addWidget(title_label) # Zone de scroll pour les logs self.scroll_area = QScrollArea() self.scroll_area.setWidgetResizable(True) self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.scroll_area.setStyleSheet(""" QScrollArea { border: none; background-color: white; } """) # Widget contenant les logs self.content_widget = QWidget() self.content_layout = QVBoxLayout() self.content_layout.setContentsMargins(5, 5, 5, 5) self.content_layout.setSpacing(2) self.content_layout.addStretch() # Push logs to top self.content_widget.setLayout(self.content_layout) self.scroll_area.setWidget(self.content_widget) main_layout.addWidget(self.scroll_area) # Message par défaut si aucun log self._show_empty_message() def _show_empty_message(self): """Affiche un message quand il n'y a pas de logs.""" if len(self.logs) == 0: empty_label = QLabel("💤 En attente d'activité...") empty_label.setFont(QFont("Arial", 10)) empty_label.setStyleSheet(""" QLabel { color: #999; padding: 20px; text-align: center; } """) empty_label.setAlignment(Qt.AlignCenter) self.content_layout.insertWidget(0, empty_label) def _remove_empty_message(self): """Supprime le message vide si présent.""" if self.content_layout.count() > 1: item = self.content_layout.itemAt(0) if item and item.widget(): widget = item.widget() if isinstance(widget, QLabel) and "En attente" in widget.text(): self.content_layout.removeWidget(widget) widget.deleteLater() def _is_scrolled_to_bottom(self) -> bool: """ Vérifie si le scroll est en bas. Returns: True si le scroll est en bas (ou proche), False sinon """ scrollbar = self.scroll_area.verticalScrollBar() # Considérer "en bas" si on est à moins de 10 pixels du bas return scrollbar.value() >= scrollbar.maximum() - 10 def _scroll_to_bottom(self): """Scroll vers le bas de la zone de logs.""" scrollbar = self.scroll_area.verticalScrollBar() scrollbar.setValue(scrollbar.maximum()) def add_log(self, message: str, emoji: str = "ℹ️", level: str = "info"): """ Ajoute un log avec timestamp. Args: message: Message à afficher emoji: Emoji à afficher (défaut: ℹ️) level: Niveau du log ('info', 'success', 'warning', 'error') Examples: >>> panel = LogsPanel() >>> panel.add_log("J'observe vos actions", "👀", "info") >>> panel.add_log("Pattern détecté", "🎯", "success") """ # Vérifier si on doit auto-scroll should_auto_scroll = self._is_scrolled_to_bottom() # Créer le message de log log_msg = LogMessage( timestamp=datetime.now(), emoji=emoji, message=message, level=level ) # Ajouter à la liste self.logs.append(log_msg) # Supprimer le message vide si c'est le premier log if len(self.logs) == 1: self._remove_empty_message() # Supprimer les logs les plus anciens si on dépasse la limite while len(self.logs) > self.max_logs: removed_log = self.logs.pop(0) # Supprimer le widget correspondant if len(self.log_labels) > 0: old_label = self.log_labels.pop(0) self.content_layout.removeWidget(old_label) old_label.deleteLater() # Créer le widget pour ce log log_label = self._create_log_label(log_msg) self.log_labels.append(log_label) # Insérer avant le stretch (qui est toujours en dernier) insert_position = self.content_layout.count() - 1 self.content_layout.insertWidget(insert_position, log_label) # Auto-scroll si on était déjà en bas if should_auto_scroll: # Utiliser un timer pour s'assurer que le layout est mis à jour QTimer.singleShot(10, self._scroll_to_bottom) # Émettre le signal self.log_added.emit(message) def _create_log_label(self, log_msg: LogMessage) -> QLabel: """ Crée un widget QLabel pour un message de log. Args: log_msg: Message de log à afficher Returns: QLabel formaté pour le log """ # Formater le timestamp (HH:MM) time_str = log_msg.timestamp.strftime("%H:%M") # Créer le texte complet text = f"{time_str} {log_msg.emoji} {log_msg.message}" # Créer le label label = QLabel(text) label.setFont(QFont("Arial", 9)) label.setWordWrap(True) label.setTextInteractionFlags(Qt.TextSelectableByMouse) # Couleurs selon le niveau level_colors = { "info": "#333", "success": "#4CAF50", "warning": "#FF9800", "error": "#f44336" } level_bg_colors = { "info": "#f9f9f9", "success": "#E8F5E9", "warning": "#FFF3E0", "error": "#FFEBEE" } color = level_colors.get(log_msg.level, level_colors["info"]) bg_color = level_bg_colors.get(log_msg.level, level_bg_colors["info"]) label.setStyleSheet(f""" QLabel {{ color: {color}; background-color: {bg_color}; padding: 6px 8px; border-radius: 4px; border-left: 3px solid {color}; }} """) return label def clear(self): """Efface tous les logs.""" # Supprimer tous les widgets for label in self.log_labels: self.content_layout.removeWidget(label) label.deleteLater() # Réinitialiser les listes self.logs.clear() self.log_labels.clear() # Afficher le message vide self._show_empty_message() def get_logs(self) -> List[LogMessage]: """ Retourne l'historique des logs. Returns: Liste des messages de log """ return self.logs.copy() def get_log_count(self) -> int: """ Retourne le nombre de logs actuellement affichés. Returns: Nombre de logs """ return len(self.logs) def get_last_log(self) -> Optional[LogMessage]: """ Retourne le dernier log ajouté. Returns: Dernier message de log ou None si aucun log """ if len(self.logs) > 0: return self.logs[-1] return None if __name__ == "__main__": """Test du LogsPanel""" import sys from PyQt5.QtWidgets import QApplication, QMainWindow app = QApplication(sys.argv) # Créer une fenêtre de test window = QMainWindow() window.setWindowTitle("Test LogsPanel") window.setGeometry(100, 100, 400, 500) # Créer le panneau de logs logs_panel = LogsPanel() window.setCentralWidget(logs_panel) # Ajouter quelques logs de test logs_panel.add_log("J'observe vos actions dans Calculator", "👀", "info") logs_panel.add_log("Tiens ! Vous avez fait 3 fois la même chose", "🎯", "success") logs_panel.add_log("J'apprends: Calculer 9/9 (5 observations)", "📚", "info") logs_panel.add_log("Mode Suggestions activé", "✅", "success") logs_panel.add_log("Prêt à suggérer: Calculer 9/9", "💡", "info") # Ajouter un log d'erreur logs_panel.add_log("Impossible de se connecter - Calculator", "⚠️", "error") # Ajouter un log d'avertissement logs_panel.add_log("Application non autorisée", "⚠️", "warning") # Test: Ajouter beaucoup de logs pour tester la limite de 30 print(f"Nombre de logs avant test: {logs_panel.get_log_count()}") for i in range(25): logs_panel.add_log(f"Test log #{i+8}", "📝", "info") print(f"Nombre de logs après ajout de 25: {logs_panel.get_log_count()}") print(f"Limite max: {logs_panel.max_logs}") # Vérifier que le dernier log est bien le dernier ajouté last_log = logs_panel.get_last_log() if last_log: print(f"Dernier log: {last_log.message}") window.show() sys.exit(app.exec_())