# modules/messaging_client.py
from PyQt5.QtCore import Qt, QUrl, pyqtSlot, QTimer, QRect, pyqtSignal
from PyQt5.QtGui import QPixmap, QPainter, QPen
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton, QTextEdit, QTextBrowser, QLabel, QDialog, \
QScrollArea, QFileDialog, QMessageBox, QApplication
from PyQt5.QtWebSockets import QWebSocket
from PyQt5 import QtCore
from datetime import datetime
import base64
import mimetypes
import json
import os
import shutil
class MessagingClientWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.ws_url = "wss://ma2c-messaging.onrender.com/ws"
self.history_dir = r"c:\autolook\messaging"
self.history_file = os.path.join(self.history_dir, "chat_history.json")
self.chat_history = []
self.setWindowTitle("Messagerie MA2C")
self.resize(400, 300)
self.websocket = QWebSocket()
self.websocket.error.connect(self.on_error)
self.websocket.connected.connect(self.on_connected)
self.websocket.disconnected.connect(self.on_disconnected)
self.websocket.textMessageReceived.connect(self.on_message_received)
self.init_ui()
self.load_history()
self.websocket.open(QUrl(self.ws_url))
def init_ui(self):
layout = QVBoxLayout(self)
self.status_label = QLabel("🔌 Connexion...")
layout.addWidget(self.status_label)
self.message_area = QTextBrowser()
self.message_area.setReadOnly(True)
self.message_area.setOpenExternalLinks(False) # On gère nous-mêmes les clics
self.message_area.setOpenLinks(False) # IMPORTANT: Empêche l'ouverture automatique
self.message_area.anchorClicked.connect(self.on_link_clicked)
layout.addWidget(self.message_area)
input_layout = QHBoxLayout()
self.input_field = QLineEdit()
self.input_field.setPlaceholderText("Écrire un message...")
self.send_button = QPushButton("Envoyer")
self.send_button.clicked.connect(self.send_message)
self.history_button = QPushButton("📚 Historique")
self.history_button.clicked.connect(self.show_history_window)
self.files_button = QPushButton("📁 Réception Fichiers")
self.files_button.clicked.connect(self.open_received_files_folder)
self.file_button = QPushButton("📎 Envoi Fichier")
self.file_button.clicked.connect(self.select_file)
self.screenshot_button = QPushButton("📷 Capture")
self.screenshot_button.clicked.connect(self.take_screenshot)
input_layout.addWidget(self.input_field)
input_layout.addWidget(self.send_button)
input_layout.addWidget(self.file_button)
input_layout.addWidget(self.screenshot_button)
input_layout.addWidget(self.history_button)
input_layout.addWidget(self.files_button)
layout.addLayout(input_layout)
# === STYLES UNIFORMISÉS AU THÈME MA2C ===
self.setStyleSheet("""
QWidget {
background-color: #2b2b2b;
color: #ffffff;
font-family: Segoe UI, sans-serif;
font-size: 12px;
}
QLineEdit {
background-color: #3b3b3b;
color: white;
padding: 4px;
border: 1px solid #5c5c5c;
border-radius: 5px;
}
QTextEdit {
background-color: #1f1f1f;
color: #eeeeee;
border: 1px solid #444444;
border-radius: 5px;
padding: 5px;
}
QPushButton {
background-color: #0078D7;
color: white;
border: none;
padding: 6px 10px;
border-radius: 5px;
}
QPushButton:hover {
background-color: #005fa3;
}
QLabel {
font-weight: bold;
padding: 4px;
}
""")
def get_timestamp(self):
return datetime.now().strftime("[%H:%M:%S]")
def ensure_history_directory(self):
"""Crée le répertoire d'historique s'il n'existe pas"""
if not os.path.exists(self.history_dir):
os.makedirs(self.history_dir)
def save_history(self):
"""Sauvegarde l'historique dans le fichier JSON"""
try:
self.ensure_history_directory()
with open(self.history_file, 'w', encoding='utf-8') as f:
json.dump(self.chat_history, f, ensure_ascii=False, indent=2)
except Exception as e:
print(f"Erreur lors de la sauvegarde : {e}")
def load_history(self):
"""Charge l'historique depuis le fichier JSON"""
try:
if os.path.exists(self.history_file):
with open(self.history_file, 'r', encoding='utf-8') as f:
self.chat_history = json.load(f)
# Afficher les derniers messages dans l'interface
for entry in self.chat_history[-50:]: # Affiche les 50 derniers messages
# Éviter d'afficher les caractères HTML corrompus
message = entry['message']
# Si le message contient des caractères bizarres, l'ignorer
if any(ord(char) > 127 and char not in 'àâäéèêëïîôöùûüç' for char in message):
continue
self.message_area.append(f"{entry['timestamp']} {message}")
except Exception as e:
print(f"Erreur lors du chargement : {e}")
self.chat_history = []
def add_to_history(self, message_type, content, file_path=None):
"""Ajoute un message à l'historique"""
timestamp = self.get_timestamp()
if message_type == "sent":
full_message = f"🧑💻 Moi : {content}"
elif message_type == "received":
full_message = f"💬 {content}"
elif message_type == "system":
full_message = content
else:
full_message = content
entry = {
"timestamp": timestamp,
"type": message_type,
"message": full_message,
"raw_content": content,
"file_path": file_path # Ajouter le chemin du fichier
}
self.chat_history.append(entry)
self.save_history()
def show_history_window(self):
"""Ouvre la fenêtre d'historique"""
self.history_window = HistoryWindow(self.chat_history, self)
self.history_window.show()
def select_file(self):
"""Ouvre une boîte de dialogue pour sélectionner un fichier"""
file_path, _ = QFileDialog.getOpenFileName(
self,
"Sélectionner un fichier",
"",
"Tous les fichiers (*);;Images (*.png *.jpg *.jpeg *.gif *.bmp);;Documents (*.pdf *.doc *.docx *.txt)"
)
if file_path:
self.send_file(file_path)
def send_file(self, file_path):
"""Encode et envoie un fichier via websocket"""
try:
# Vérifier la taille du fichier (limite à 10MB)
file_size = os.path.getsize(file_path)
if file_size > 10 * 1024 * 1024: # 10MB
QMessageBox.warning(self, "Fichier trop volumineux",
"Le fichier ne peut pas dépasser 10MB.")
return
# Lire et encoder le fichier
with open(file_path, "rb") as file:
file_data = file.read()
file_base64 = base64.b64encode(file_data).decode('utf-8')
# Obtenir le type MIME
mime_type, _ = mimetypes.guess_type(file_path)
if not mime_type:
mime_type = "application/octet-stream"
# Obtenir le nom du fichier
file_name = os.path.basename(file_path)
# Créer le message JSON pour le fichier
file_message = {
"type": "file",
"filename": file_name,
"mime_type": mime_type,
"data": file_base64,
"size": file_size
}
# Envoyer via websocket
self.websocket.sendTextMessage(json.dumps(file_message))
# Afficher dans l'interface
self.display_sent_file(file_name, file_path, mime_type)
# Ajouter à l'historique
self.add_to_history("sent", f"📎 Fichier envoyé: {file_name}", file_path)
except Exception as e:
QMessageBox.critical(self, "Erreur", f"Impossible d'envoyer le fichier: {str(e)}")
def display_sent_file(self, filename, file_path, mime_type):
"""Affiche un fichier envoyé dans la conversation avec lien cliquable"""
timestamp = self.get_timestamp()
# Créer un lien HTML cliquable avec le chemin correct
escaped_path = file_path.replace("\\", "/")
link_html = f'{filename}'
if mime_type.startswith('image/'):
# Afficher l'aperçu de l'image avec lien
self.message_area.append(f"{timestamp} 🧑💻 Moi : 📷 Image envoyée: {link_html}")
else:
# Afficher le fichier avec lien cliquable
self.message_area.append(f"{timestamp} 🧑💻 Moi : 📎 Fichier envoyé: {link_html}")
def add_image_preview(self, image_path, is_sent=False):
"""Ajoute un aperçu d'image dans la conversation"""
try:
# Créer un QLabel pour l'image
pixmap = QPixmap(image_path)
if not pixmap.isNull():
# Redimensionner l'image pour l'aperçu (max 200x200)
scaled_pixmap = pixmap.scaled(200, 200, Qt.KeepAspectRatio, Qt.SmoothTransformation)
# Pour l'instant, on affiche juste le chemin car QTextEdit ne supporte pas directement les images
# On ajoutera une solution plus avancée si nécessaire
prefix = "🧑💻 Moi" if is_sent else "💬 Reçu"
self.message_area.append(
f" └── 🖼️ Aperçu: {os.path.basename(image_path)} ({pixmap.width()}x{pixmap.height()})")
except Exception as e:
print(f"Erreur lors de l'affichage de l'aperçu: {e}")
def take_screenshot(self):
"""Méthode principale de capture d'écran - VERSION SIMPLE UNIFIÉE"""
try:
# Proposer les options de capture
reply = QMessageBox.question(
self,
'Mode de capture',
'Choisissez le mode de capture:\n\n'
'Oui = Écran complet\n'
'Non = Sélection de zone\n'
'Annuler = Annuler',
QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel,
QMessageBox.Yes
)
if reply == QMessageBox.Yes:
# Capture d'écran complet
self.capture_full_screen()
elif reply == QMessageBox.No:
# Sélection de zone
self.start_area_selection()
# Si Cancel, ne rien faire
except Exception as e:
QMessageBox.critical(self, "Erreur", f"Erreur lors de la capture: {str(e)}")
def capture_full_screen(self):
"""Capture l'écran complet"""
try:
print("📸 Capture écran complet") # Debug
# Cacher la fenêtre temporairement
self.hide()
# Attendre un peu pour que la fenêtre se cache
QTimer.singleShot(500, self._do_full_screen_capture)
except Exception as e:
print(f"❌ Erreur capture_full_screen: {e}")
QMessageBox.critical(self, "Erreur", f"Erreur: {str(e)}")
self.show()
def _do_full_screen_capture(self):
"""Effectue la capture d'écran complète"""
try:
# Capturer tout l'écran
app = QApplication.instance()
screen = app.primaryScreen()
screenshot = screen.grabWindow(0)
# Sauvegarder
self.ensure_history_directory()
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
screenshot_path = os.path.join(self.history_dir, f"screenshot_{timestamp}.png")
if screenshot.save(screenshot_path):
print(f"✅ Screenshot sauvé: {screenshot_path}")
self.send_file(screenshot_path)
QMessageBox.information(self, "Succès", "Capture d'écran envoyée!")
else:
print("❌ Échec sauvegarde screenshot")
QMessageBox.critical(self, "Erreur", "Impossible de sauvegarder")
except Exception as e:
print(f"❌ Erreur _do_full_screen_capture: {e}")
QMessageBox.critical(self, "Erreur", f"Erreur: {str(e)}")
finally:
self.show()
def start_area_selection(self):
"""Démarre la sélection de zone avec PyQt5"""
try:
# Masquer la fenêtre principale
self.hide()
# Attendre un peu pour que la fenêtre se cache
QTimer.singleShot(500, self.create_area_selector)
except Exception as e:
QMessageBox.critical(self, "Erreur", f"Erreur lors de la capture: {str(e)}")
self.show()
def create_area_selector(self):
"""Crée le sélecteur de zone - VERSION AMÉLIORÉE"""
try:
print("🎯 Création du sélecteur de zone") # Debug
# Capturer tout l'écran d'abord
app = QApplication.instance()
screen = app.primaryScreen()
self.full_screenshot = screen.grabWindow(0)
print(f"📸 Screenshot capturé: {self.full_screenshot.size()}") # Debug
# Créer la fenêtre de sélection
self.area_selector = AreaSelector(self.full_screenshot, self)
self.area_selector.area_selected.connect(self.on_area_selected)
self.area_selector.selection_cancelled.connect(self.on_selection_cancelled)
# Affichage avec méthodes multiples pour s'assurer que ça marche
self.area_selector.showFullScreen()
self.area_selector.activateWindow()
self.area_selector.raise_()
# Donner le focus à la fenêtre
QApplication.processEvents()
self.area_selector.setFocus()
print("✅ Sélecteur de zone créé et affiché") # Debug
except Exception as e:
print(f"❌ Erreur create_area_selector: {e}") # Debug
QMessageBox.critical(self, "Erreur", f"Erreur lors de la capture: {str(e)}")
self.show()
def on_area_selected(self, rect):
"""Traite la zone sélectionnée - VERSION AVEC DEBUG"""
try:
print(f"🎯 Zone sélectionnée: {rect}") # Debug
if rect.width() > 0 and rect.height() > 0:
# Découper l'image selon la sélection
cropped = self.full_screenshot.copy(rect)
print(f"✂️ Image découpée: {cropped.size()}") # Debug
# Sauvegarder
self.ensure_history_directory()
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
screenshot_path = os.path.join(self.history_dir, f"screenshot_{timestamp}.png")
if cropped.save(screenshot_path):
print(f"💾 Sauvegardé: {screenshot_path}") # Debug
self.send_file(screenshot_path)
QMessageBox.information(self, "Succès",
f"Capture d'écran envoyée!\nTaille: {rect.width()}x{rect.height()}")
else:
print("❌ Échec sauvegarde") # Debug
QMessageBox.critical(self, "Erreur", "Impossible de sauvegarder la capture")
else:
print("⚠️ Rectangle invalide") # Debug
except Exception as e:
print(f"❌ Erreur on_area_selected: {e}") # Debug
QMessageBox.critical(self, "Erreur", f"Erreur lors de la capture: {str(e)}")
finally:
self.show()
def on_selection_cancelled(self):
"""Traite l'annulation de la sélection - VERSION AVEC DEBUG"""
print("⛔ Sélection annulée") # Debug
self.show()
@pyqtSlot()
def on_connected(self):
self.status_label.setText("🟢 Connecté au serveur MA2C")
message = "✅ Connecté au serveur"
self.message_area.append(f"{self.get_timestamp()} {message}")
self.add_to_history("system", message)
@pyqtSlot()
def on_disconnected(self):
self.status_label.setText("🔴 Déconnecté")
message = "❌ Déconnecté du serveur"
self.message_area.append(f"{self.get_timestamp()} {message}")
self.add_to_history("system", message)
@pyqtSlot(str)
def on_message_received(self, message):
try:
# Essayer de parser le message comme JSON (pour les fichiers)
message_data = json.loads(message)
# Vérifier si c'est un fichier
if isinstance(message_data, dict) and message_data.get("type") == "file":
self.handle_received_file(message_data)
else:
# Message texte normal
# Échapper le HTML dans les messages reçus
escaped_message = message.replace("&", "&").replace("<", "<").replace(">", ">")
self.message_area.append(f"{self.get_timestamp()} 💬 {escaped_message}")
self.add_to_history("received", message)
except json.JSONDecodeError:
# Ce n'est pas du JSON, c'est un message texte normal
# Échapper le HTML dans les messages reçus
escaped_message = message.replace("&", "&").replace("<", "<").replace(">", ">")
self.message_area.append(f"{self.get_timestamp()} 💬 {escaped_message}")
self.add_to_history("received", message)
except Exception as e:
print(f"Erreur lors de la réception du message: {e}")
# Afficher le message brut en cas d'erreur
# Échapper le HTML dans les messages reçus
escaped_message = message.replace("&", "&").replace("<", "<").replace(">", ">")
self.message_area.append(f"{self.get_timestamp()} 💬 {escaped_message}")
self.add_to_history("received", message)
def handle_received_file(self, file_data):
"""Traite un fichier reçu via websocket"""
try:
filename = file_data.get("filename", "fichier_inconnu")
mime_type = file_data.get("mime_type", "application/octet-stream")
file_base64 = file_data.get("data", "")
file_size = file_data.get("size", 0)
if not file_base64:
self.message_area.append(f"{self.get_timestamp()} ❌ Fichier reçu vide: {filename}")
return
# Créer le dossier de réception s'il n'existe pas
received_dir = os.path.join(self.history_dir, "received_files")
if not os.path.exists(received_dir):
os.makedirs(received_dir)
# Décoder et sauvegarder le fichier
file_bytes = base64.b64decode(file_base64)
# Générer un nom unique pour éviter les conflits
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
name, ext = os.path.splitext(filename)
unique_filename = f"{name}_{timestamp}{ext}"
file_path = os.path.join(received_dir, unique_filename)
# Sauvegarder le fichier
with open(file_path, 'wb') as f:
f.write(file_bytes)
# Afficher dans l'interface
self.display_received_file(filename, file_path, mime_type, file_size)
# Ajouter à l'historique
self.add_to_history("received", f"📎 Fichier reçu: {filename} (sauvé comme {unique_filename})")
print(f"✅ Fichier reçu sauvegardé: {file_path}")
except Exception as e:
error_msg = f"Erreur lors de la réception du fichier: {str(e)}"
print(f"❌ {error_msg}")
self.message_area.append(f"{self.get_timestamp()} ❌ {error_msg}")
def display_received_file(self, filename, file_path, mime_type, file_size):
"""Affiche un fichier reçu dans la conversation"""
timestamp = self.get_timestamp()
# Convertir la taille en format lisible
if file_size < 1024:
size_str = f"{file_size} B"
elif file_size < 1024 * 1024:
size_str = f"{file_size / 1024:.1f} KB"
else:
size_str = f"{file_size / (1024 * 1024):.1f} MB"
if mime_type.startswith('image/'):
# Afficher l'aperçu de l'image reçue
self.message_area.append(f"{timestamp} 💬 📷 Image reçue: {filename} ({size_str})")
self.message_area.append(f" └── 💾 Sauvegardée dans: {file_path}")
self.add_image_preview(file_path, is_sent=False)
else:
# Afficher le fichier reçu
self.message_area.append(f"{timestamp} 💬 📎 Fichier reçu: {filename} ({size_str})")
self.message_area.append(f" └── 💾 Sauvegardé dans: {file_path}")
def open_received_files_folder(self):
"""Ouvre le dossier contenant les fichiers reçus"""
try:
received_dir = os.path.join(self.history_dir, "received_files")
if os.path.exists(received_dir):
os.startfile(received_dir) # Windows
else:
QMessageBox.information(self, "Info", "Aucun fichier reçu pour le moment.")
except Exception as e:
QMessageBox.critical(self, "Erreur", f"Impossible d'ouvrir le dossier: {str(e)}")
def on_link_clicked(self, url):
"""Gère les clics sur les liens - VERSION SIMPLE: sauvegarder uniquement"""
try:
# Extraire le chemin du fichier de l'URL
url_string = url.toString()
# Supprimer le préfixe "file:///" et décoder l'URL
if url_string.startswith("file:///"):
file_path = url_string[8:] # Enlever "file:///"
file_path = file_path.replace("/", "\\") # Convertir en chemin Windows
else:
file_path = url_string
print(f"🔍 Fichier à copier: {file_path}")
# Vérifier si le fichier existe
if os.path.exists(file_path):
# Directement ouvrir la boîte de dialogue pour sauvegarder
self.copy_file_to_location(file_path)
else:
QMessageBox.warning(self, "Fichier introuvable",
f"Le fichier n'existe plus à l'emplacement:\n{file_path}")
except Exception as e:
QMessageBox.critical(self, "Erreur", f"Impossible d'accéder au fichier: {str(e)}")
print(f"❌ Erreur on_link_clicked: {e}")
def copy_file_to_location(self, source_file_path):
"""Permet de copier un fichier vers un emplacement choisi par l'utilisateur"""
try:
# Obtenir le nom du fichier original
original_filename = os.path.basename(source_file_path)
# Déterminer le filtre par défaut selon le type de fichier
_, ext = os.path.splitext(original_filename.lower())
if ext in ['.png', '.jpg', '.jpeg', '.gif', '.bmp']:
file_filter = "Images PNG (*.png);;Images JPEG (*.jpg);;Tous les fichiers (*)"
default_filter = "Images PNG (*.png)"
else:
file_filter = "Tous les fichiers (*);;Documents (*.pdf *.txt *.doc)"
default_filter = "Tous les fichiers (*)"
destination_path, _ = QFileDialog.getSaveFileName(
self,
"Sauvegarder le fichier sous...",
original_filename, # Nom par défaut
file_filter,
default_filter
)
if destination_path:
# Copier le fichier
import shutil
shutil.copy2(source_file_path, destination_path)
QMessageBox.information(
self,
"Fichier copié",
f"Le fichier a été copié avec succès vers :\n{destination_path}"
)
print(f"✅ Fichier copié: {source_file_path} -> {destination_path}")
except Exception as e:
QMessageBox.critical(self, "Erreur", f"Impossible de copier le fichier: {str(e)}")
print(f"❌ Erreur copy_file_to_location: {e}")
@pyqtSlot()
def on_error(self):
self.status_label.setText("❌ Erreur de connexion")
message = "⚠️ Erreur réseau"
self.message_area.append(f"{self.get_timestamp()} {message}")
self.add_to_history("system", message)
def send_message(self):
text = self.input_field.text().strip()
if text:
self.websocket.sendTextMessage(text)
# Échapper le HTML dans les messages texte pour éviter les conflits
escaped_text = text.replace("&", "&").replace("<", "<").replace(">", ">")
self.message_area.append(f"{self.get_timestamp()} 🧑💻 Moi : {escaped_text}")
self.add_to_history("sent", text)
self.input_field.clear()
class HistoryWindow(QDialog):
def __init__(self, chat_history, parent=None):
super().__init__(parent)
self.chat_history = chat_history
self.setWindowTitle("📚 Historique des conversations")
self.setGeometry(100, 100, 600, 500)
self.init_ui()
def init_ui(self):
layout = QVBoxLayout(self)
# Zone d'affichage de l'historique
self.history_area = QTextEdit()
self.history_area.setReadOnly(True)
layout.addWidget(self.history_area)
# Boutons
button_layout = QHBoxLayout()
self.refresh_button = QPushButton("🔄 Actualiser")
self.refresh_button.clicked.connect(self.refresh_history)
self.clear_button = QPushButton("🗑️ Vider l'historique")
self.clear_button.clicked.connect(self.clear_history)
self.close_button = QPushButton("❌ Fermer")
self.close_button.clicked.connect(self.close)
button_layout.addWidget(self.refresh_button)
button_layout.addWidget(self.clear_button)
button_layout.addWidget(self.close_button)
layout.addLayout(button_layout)
# Appliquer le même style que la fenêtre principale
self.setStyleSheet("""
QWidget {
background-color: #2b2b2b;
color: #ffffff;
font-family: Segoe UI, sans-serif;
font-size: 12px;
}
QTextBrowser {
background-color: #1f1f1f;
color: #eeeeee;
border: 1px solid #444444;
border-radius: 5px;
padding: 5px;
}
QPushButton {
background-color: #0078D7;
color: white;
border: none;
padding: 6px 10px;
border-radius: 5px;
margin: 2px;
}
QPushButton:hover {
background-color: #005fa3;
}
""")
self.load_history()
def load_history(self):
"""Charge l'historique depuis le fichier JSON"""
try:
if os.path.exists(self.history_file):
with open(self.history_file, 'r', encoding='utf-8') as f:
self.chat_history = json.load(f)
# Afficher les derniers messages dans l'interface
for entry in self.chat_history[-50:]: # Affiche les 50 derniers messages
self.display_history_entry(entry)
except Exception as e:
print(f"Erreur lors du chargement : {e}")
self.chat_history = []
def display_history_entry(self, entry):
"""Affiche une entrée d'historique avec les liens si nécessaire"""
try:
timestamp = entry['timestamp']
message = entry['message']
file_path = entry.get('file_path')
# Si c'est un fichier envoyé et qu'on a le chemin, recréer le lien
if file_path and "Fichier envoyé:" in message and os.path.exists(file_path):
filename = os.path.basename(file_path)
escaped_path = file_path.replace("\\", "/")
link_html = f'{filename}'
if "📷 Image envoyée:" in message:
display_message = f"🧑💻 Moi : 📷 Image envoyée: {link_html}"
else:
display_message = f"🧑💻 Moi : 📎 Fichier envoyé: {link_html}"
self.message_area.append(f"{timestamp} {display_message}")
else:
# Message normal
self.message_area.append(f"{timestamp} {message}")
except Exception as e:
print(f"Erreur affichage historique: {e}")
# En cas d'erreur, afficher le message brut
self.message_area.append(f"{entry['timestamp']} {entry['message']}")
def refresh_history(self):
"""Actualise l'affichage de l'historique"""
if self.parent():
self.chat_history = self.parent().chat_history
self.load_history()
def clear_history(self):
"""Vide l'historique (avec confirmation)"""
reply = QMessageBox.question(self, 'Confirmation',
'Êtes-vous sûr de vouloir vider tout l\'historique ?',
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No)
if reply == QMessageBox.Yes:
if self.parent():
self.parent().chat_history = []
self.parent().save_history()
self.load_history()
QMessageBox.information(self, 'Succès', 'Historique vidé avec succès!')
class AreaSelector(QWidget):
area_selected = pyqtSignal(QRect)
selection_cancelled = pyqtSignal()
def __init__(self, screenshot, parent=None):
super().__init__(parent)
self.screenshot = screenshot
self.start_point = None
self.end_point = None
self.selecting = False
print("🔍 AreaSelector créé") # Debug
# Configuration critique de la fenêtre
self.setWindowFlags(
Qt.WindowStaysOnTopHint |
Qt.FramelessWindowHint |
Qt.Tool
)
self.setAttribute(Qt.WA_TranslucentBackground, True)
self.setCursor(Qt.CrossCursor)
# IMPORTANT: Obtenir les vraies dimensions de l'écran
screen = QApplication.instance().primaryScreen()
screen_geometry = screen.geometry()
print(f"📐 Écran: {screen_geometry}") # Debug
# Définir la géométrie AVANT showFullScreen
self.setGeometry(screen_geometry)
# Forcer la fenêtre à être active
self.activateWindow()
self.raise_()
self.show()
def showEvent(self, event):
"""Appelé quand la fenêtre s'affiche"""
super().showEvent(event)
print("👁️ AreaSelector affiché") # Debug
# S'assurer que la fenêtre est au premier plan
self.activateWindow()
self.raise_()
def paintEvent(self, event):
"""Dessine l'interface de sélection"""
try:
painter = QPainter(self)
# Dessiner le screenshot avec transparence
painter.setOpacity(0.3)
painter.drawPixmap(self.rect(), self.screenshot)
# Si on a une sélection, la dessiner
if self.start_point and self.end_point:
painter.setOpacity(1.0)
# Rectangle de sélection en coordonnées LOCALES
rect = QRect(self.start_point, self.end_point).normalized()
# Dessiner le contour rouge
painter.setPen(QPen(Qt.red, 3))
painter.setBrush(Qt.NoBrush)
painter.drawRect(rect)
# Dessiner les coins pour faciliter la sélection
corner_size = 8
painter.setBrush(Qt.red)
painter.drawRect(rect.topLeft().x() - corner_size // 2,
rect.topLeft().y() - corner_size // 2,
corner_size, corner_size)
painter.drawRect(rect.bottomRight().x() - corner_size // 2,
rect.bottomRight().y() - corner_size // 2,
corner_size, corner_size)
# Afficher les dimensions
painter.setPen(QPen(Qt.white, 2))
painter.setBrush(Qt.black)
text = f"{rect.width()} x {rect.height()}"
text_rect = painter.fontMetrics().boundingRect(text)
text_pos = rect.topLeft() + QtCore.QPoint(5, -5)
# Fond noir pour le texte
painter.fillRect(text_rect.translated(text_pos), Qt.black)
painter.setPen(Qt.white)
painter.drawText(text_pos, text)
except Exception as e:
print(f"❌ Erreur paintEvent: {e}")
def mousePressEvent(self, event):
"""Début de sélection"""
try:
if event.button() == Qt.LeftButton:
print(f"🖱️ Clic début: {event.pos()}") # Debug
# Utiliser les coordonnées LOCALES directement
self.start_point = event.pos()
self.end_point = self.start_point
self.selecting = True
self.update()
except Exception as e:
print(f"❌ Erreur mousePressEvent: {e}")
def mouseMoveEvent(self, event):
"""Déplacement pendant sélection"""
try:
if self.selecting and self.start_point:
# Utiliser les coordonnées LOCALES directement
self.end_point = event.pos()
self.update()
except Exception as e:
print(f"❌ Erreur mouseMoveEvent: {e}")
def mouseReleaseEvent(self, event):
"""Fin de sélection"""
try:
if event.button() == Qt.LeftButton and self.selecting:
self.selecting = False
print(f"🖱️ Clic fin: {event.pos()}") # Debug
if self.start_point and self.end_point:
# Créer le rectangle final en coordonnées locales
rect = QRect(self.start_point, self.end_point).normalized()
print(f"📏 Rectangle sélectionné: {rect}") # Debug
if rect.width() >= 10 and rect.height() >= 10:
print("✅ Sélection valide, émission du signal") # Debug
self.close()
# Émettre le rectangle en coordonnées d'écran
screen_rect = QRect(
rect.x() + self.geometry().x(),
rect.y() + self.geometry().y(),
rect.width(),
rect.height()
)
self.area_selected.emit(screen_rect)
return
else:
print("⚠️ Sélection trop petite") # Debug
# Sélection trop petite, recommencer
self.start_point = None
self.end_point = None
self.update()
except Exception as e:
print(f"❌ Erreur mouseReleaseEvent: {e}")
def keyPressEvent(self, event):
"""Gestion des touches"""
if event.key() == Qt.Key_Escape:
print("⛔ Annulation par Échap") # Debug
self.close()
self.selection_cancelled.emit()
elif event.key() == Qt.Key_Return or event.key() == Qt.Key_Enter:
# Permettre de valider avec Entrée si on a une sélection
if self.start_point and self.end_point:
self.mouseReleaseEvent(type('MockEvent', (), {
'button': lambda: Qt.LeftButton,
'pos': lambda: self.end_point
})())
def closeEvent(self, event):
"""Fermeture de la fenêtre"""
print("🚪 AreaSelector fermé") # Debug
event.accept()
# MODIFIEZ AUSSI la méthode create_area_selector dans MessagingClientWidget :
# Test du module si exécuté directement
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
widget = MessagingClientWidget()
widget.show()
sys.exit(app.exec_())