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