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