Initial commit
This commit is contained in:
347
geniusia2/gui/logs_panel.py
Normal file
347
geniusia2/gui/logs_panel.py
Normal file
@@ -0,0 +1,347 @@
|
||||
"""
|
||||
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_())
|
||||
Reference in New Issue
Block a user