# -*- coding: utf-8 -*-

# qdrawEVT: plugin qui facilite le dessin
# Author: françois Thévand
#         francois.thevand@gmail.com
#
# Licence GNU GPL v3+
# Ce programme est distribué sans AUCUNE GARANTIE.
# Voir <http://www.gnu.org/licenses/>.

from __future__ import print_function, absolute_import, annotations
from builtins import str, object
from typing import Optional, cast, Tuple

# --- Initialisation du logger global QdrawEVT ---
from .logger import log, redirect_print_to_log
redirect_print_to_log()
log.info("=== QdrawEVT plugin initialization ===")

# --- Capture des exceptions globales ---

# def handle_uncaught_exception(exctype, value, tb):
#     """Capture toute erreur Python non interceptée et l’envoie au logger."""
#     import traceback
#     err = ''.join(traceback.format_exception(exctype, value, tb))
#     log.critical(f"Exception non interceptée : {err}")

# --- Imports QGIS / PyQt ---
from qgis.PyQt.QtCore import (
    Qt, QEvent, QUrl, QTranslator, QSettings, QCoreApplication,
    QLocale, qVersion, QLocale, QTimer, QDateTime, QPoint
)
from qgis.PyQt.QtWidgets import (
    QAction, QApplication, QMessageBox, QMenu, QInputDialog, QLineEdit,
    QTextEdit, QPlainTextEdit, QDateEdit, QDateTimeEdit, QTimeEdit, QGroupBox,
    QPushButton, QComboBox, QLabel, QCheckBox, QProgressDialog, QStyledItemDelegate,
    QDialog, QVBoxLayout, QDialogButtonBox, QButtonGroup, QRadioButton,
    QTextBrowser, QHBoxLayout, QToolBar
)
from qgis.PyQt.QtGui import QIcon, QColor, QFont

# --- Gestion des versions PyQt ---
from qgis.PyQt.QtCore import PYQT_VERSION_STR as pyqt_version
if pyqt_version.startswith("5"):
    pyqt_messagebox_question = QMessageBox.Question
    pyqt_messagebox_critical = QMessageBox.Critical
    pyqt_messagebox_warning = QMessageBox.Warning
    pyqt_messagebox_information = QMessageBox.Information
    yes_button = QMessageBox.Yes
    no_button = QMessageBox.No
    pyqt_messagebox_cancel = QMessageBox.Cancel
    qt_windows_modality = Qt.WindowModal
    qt_application_modal = Qt.ApplicationModal
    qt_window_stays_on_top_hint = Qt.WindowStaysOnTopHint
elif pyqt_version.startswith("6"):
    pyqt_messagebox_question = QMessageBox.Icon.Question
    pyqt_messagebox_critical = QMessageBox.Icon.Critical
    pyqt_messagebox_warning = QMessageBox.Icon.Warning
    pyqt_messagebox_information = QMessageBox.Icon.Information
    yes_button = QMessageBox.StandardButton.Yes
    no_button = QMessageBox.StandardButton.No
    pyqt_messagebox_cancel = QMessageBox.StandardButton.Cancel
    qt_windows_modality = Qt.WindowModality.WindowModal
    qt_application_modal = Qt.WindowModality.ApplicationModal
    qt_window_stays_on_top_hint = Qt.WindowType.WindowStaysOnTopHint

from qgis.core import *
from qgis.gui import QgsRubberBand, QgsAttributeEditorContext, QgsMapToolIdentify, QgsLayerTreeMapCanvasBridge

# --- Imports internes du plugin ---
from .AttributesTable import *
from .drawtools import DrawPoint, DrawRect, DrawLine, DrawCircle, DrawPolygon, SelectPoint, XYDialog, DMSDialog
from .qdrawsettings import QdrawSettings
from .qdrawlayerdialogEVT import QDrawLayerDialog
from datetime import datetime, date, time
import sys, os, unicodedata, random
from . import resources
from pathlib import Path

# Test existance du groupe Evènements avec suppression des espaces et des accents dans le nom du groupe testé
# evt = False
# groupevt = False
# for group in root.children():
#     test = ''.join(
#         x for x in unicodedata.normalize('NFKD', group.name()) if unicodedata.category(x)[0] == 'L').upper()
#     if test == 'EVENEMENTS':
#         evt = True
#         groupevt = group

# Inscrire ici le nom des couches evenement et les noms de colonnes attributaires utilisées dans ces couches
evtpnt = 'POINT_EVENEMENT'
evtlgn = 'LIGNE_EVENEMENT'
evtpol = 'POLYGONE_EVENEMENT'
lst_form_wgt = ['libelle','date','h_creation','source','h_constat','remarques','surface','longueur','x_gps','y_gps','utilisatr']
form_wgt_libelle = 'libelle'
form_wgt_date = 'date'
form_wgt_h_creation = 'h_creation'
form_wgt_source = 'source'
form_wgt_h_constat = 'h_constat'
form_wgt_remarques = 'remarques'
form_wgt_surface = 'surface'
form_wgt_longueur = 'longueur'
form_wgt_x_gps = 'x_gps'
form_wgt_y_gps = 'y_gps'
form_wgt_utilisatr = 'utilisatr'

# Prise en compte des variables dans la traduction

def normalize(s):
    return ''.join(x for x in unicodedata.normalize('NFKD', s)
                   if unicodedata.category(x)[0] == 'L').upper()

lst_group_no_select = [
    normalize('FONDSDEPLAN'),
    normalize('ADMINISTRATIF')
]

lst_group_evt = [
    normalize('EVENEMENTS')
]

lst_group_drawing = [
    normalize('DESSINS'),
    normalize('DRAWINGS'),
]

#Fonction de reconstruction du chemin absolu vers la ressource image ou autre fichier
def resolve(name, basepath=None):
  if not basepath:
    basepath = os.path.dirname(os.path.realpath(__file__))
  return os.path.join(basepath, name)


class UrlDelegate(QStyledItemDelegate):
    def editorEvent(self, event, model, option, index):
        """Ouvre un lien dans le navigateur si la cellule contient une URL."""
        if event.type() == QEvent.MouseButtonRelease:
            url = str(index.data() or "")
            if url.lower().startswith(("http://", "https://")):
                QDesktopServices.openUrl(QUrl(url))
        return super().editorEvent(event, model, option, index)


# --------------------------------------------------
#  Constantes internes (NE PAS traduire)
# --------------------------------------------------
DRAWINGS_GROUP_KEY = "qdrawEVT:DRAWINGS"
DRAWINGS_FIELD_NAME = "drawings"  # nom interne stable du champ (non traduit)

TOOLBAR_OBJECT_NAME = "QdrawEVT"
TOOLBAR_TITLE = "QdrawEVT"


class QdrawEVT(object):
    def __init__(self, iface):
        self._buffer_warned_no_intersection = None
        self.action_aide = None
        self.groupe_selectionne = None
        self._groupe_msg_nul = None
        self._groupe_msg_affiche = None
        self.iface = iface
        log.info("QdrawEVT plugin: base initialization OK")

        self.table = None
        self.feature = None
        self.features = None
        self.layer = None
        self.index = None
        self.layers = None
        self.featureid = None
        self.groupes_coches = None
        self.request = None
        self.XYcrs = None
        self.layerevtname = None
        self.drawShape = None
        self._buffer_mode: Optional[str] = None

        overrideLocale = QSettings().value("locale/overrideFlag", False, type=bool)
        if not overrideLocale: locale = QLocale.system().name()
        else:
            locale = QSettings().value("locale/userLocale", "")
            if locale.__class__.__name__=='QVariant': locale= 'en'
        locale = locale[0:2]
        locale_path = os.path.join(
            os.path.dirname(__file__),
            'i18n',
            'qdrawEVT_{}.qm'.format(locale))

        # *** ATTENTION *** : dans le fichier de traduction la ligne <name>QdrawEVT</name> porte sur la classe principale (ici QdrawEVT dans le fichier qdrawEVT.py)

        self.translator = None
        if os.path.exists(locale_path):
            self.translator = QTranslator()
            self.translator.load(locale_path)

            if qVersion() > '4.3.3':
                QCoreApplication.installTranslator(self.translator)

        # Obtenir l'instance du projet en cours
        self.project = QgsProject.instance()
        # Root du projet en cours
        self.root = self.project.layerTreeRoot()

        # --------------------------------------------------
        # Nettoyage automatique des groupes Dessins / Drawings
        # - déclenché à chaque chargement / ouverture de projet
        # --------------------------------------------------
        try:
            self.project.readProject.connect(self._auto_cleanup_drawings_groups)
            QTimer.singleShot(0, self._auto_cleanup_drawings_groups)
        except Exception as e:
            QgsMessageLog.logMessage(
                f"QdrawEVT: readProject hook failed: {e}",
                "QdrawEVT",
                Qgis.Warning
            )

        # ✅ Nettoyage auto : fusionne Drawings/Dessins legacy en un seul groupe stable
        self._auto_cleanup_drawings_groups()

        self.evt = None
        self.group_no_select = []
        self.group_evt = []
        self.group_drawing = []

        self.sb = self.iface.statusBarIface()
        self.tool = None
        self.toolname = None

        self.results = []

        self.bGeom = None

        self.actions = []
        self.menu = 'QdrawEVT'

        self._remove_existing_toolbar()
        self.toolbar = None
        self.gc_anchor = None

        self.settings = QdrawSettings(form_wgt_libelle, self.iface)
        # self.bridge = QgsLayerTreeMapCanvasBridge(QgsProject.instance().layerTreeRoot(), self.iface.mapCanvas())

        # Ajout de la fenêtre de sélection
        self.loadingWindow = QProgressDialog(self.tr('Selecting...'),self.tr('Pass'),0,100)
        self.loadingWindow.setAutoClose(False)
        self.loadingWindow.close()

        # --- Anti double-déclenchement (clic rapide / double signal) ---
        self._buffer_draw_lock: bool = False
        self._buffer_draw_last_ms: int = 0
        self._buffer_draw_debounce_ms: int = 250  # ajuste si besoin (200-400ms)

    def tr(self, message):
        return QCoreApplication.translate('QdrawEVT', message)

    def _remove_existing_toolbar(self):
        mw = self.iface.mainWindow()
        for tb in mw.findChildren(QToolBar):
            if tb.objectName() == TOOLBAR_OBJECT_NAME:
                mw.removeToolBar(tb)
                tb.deleteLater()

    def _canvas_pos_from_tool(self) -> Optional[QPoint]:
        """
        Récupère la position pixel-canvas du dernier clic depuis le MapTool.
        """
        if not getattr(self, "tool", None):
            return None

        for attr in (
                "last_canvas_pos",  # ✅ ce qu’on va mettre dans SelectPoint
                "lastPoint",  # (souvent utilisé ailleurs)
                "last_pos", "lastPos",
                "pos", "mouse_pos", "mousePos",
        ):
            p = getattr(self.tool, attr, None)
            if isinstance(p, QPoint):
                return p
        return None

    def identify_topmost_vector_feature(self, canvas_pos: QPoint) -> Optional[Tuple[QgsVectorLayer, QgsFeature]]:
        canvas = self.iface.mapCanvas()
        ident = QgsMapToolIdentify(canvas)
        root = QgsProject.instance().layerTreeRoot()

        layers = []
        for lyr in canvas.layers():
            if isinstance(lyr, QgsVectorLayer):
                node = root.findLayer(lyr.id())
                if node is not None and node.isVisible():
                    layers.append(lyr)

        if not layers:
            return None

        results = ident.identify(
            canvas_pos.x(),
            canvas_pos.y(),
            layers,
            QgsMapToolIdentify.TopDownStopAtFirst,
        )

        for r in results:
            lyr = r.mLayer
            feat = r.mFeature
            if isinstance(lyr, QgsVectorLayer) and feat and feat.isValid():
                return lyr, feat

        return None

    def _identify_feature_under_click(self, canvas_pos: QPoint) -> Optional[tuple]:
        """
        Retourne (layer, feature) pour la première entité trouvée sous le clic,
        en scannant les couches vectorielles visibles.
        """
        try:
            from qgis.gui import QgsMapToolIdentify
        except Exception:
            return None

        try:
            ident = QgsMapToolIdentify(self.iface.mapCanvas())
            results = ident.identify(
                canvas_pos.x(),
                canvas_pos.y(),
                [QgsMapLayer.VectorLayer],
                QgsMapToolIdentify.TopDownAll
            )
        except Exception as e:
            self._buffer_log("ERROR", f"[Buffer] identify() a échoué: {e}")
            return None

        # on prend le 1er résultat valide visible
        root = QgsProject.instance().layerTreeRoot()
        for r in results:
            try:
                layer = r.mLayer
                feat = r.mFeature
                if layer is None or feat is None:
                    continue
                node = root.findLayer(layer.id())
                if node is None or not node.isVisible():
                    continue
                return (layer, feat)
            except Exception:
                continue

        return None

    def _find_group_by_key(self, key: str):
        """Trouve un groupe via customProperty, pas via le nom (qui peut être traduit)."""
        root = self.root
        stack = [root]
        while stack:
            node = stack.pop()
            if isinstance(node, QgsLayerTreeGroup):
                if node.customProperty("qdrawEVT/group_key", "") == key:
                    return node
            for ch in node.children():
                stack.append(ch)
        return None

    def _get_or_create_drawings_group(self):
        """Retourne le groupe Drawings (clé interne stable) et applique le nom UI traduit."""
        g = self._find_group_by_key(DRAWINGS_GROUP_KEY)
        if g is None:
            g = QgsLayerTreeGroup("DRAWINGS")  # nom provisoire interne
            g.setCustomProperty("qdrawEVT/group_key", DRAWINGS_GROUP_KEY)
            self.root.insertChildNode(0, g)
        # Nom visible (traduit)
        g.setName(self.tr("Drawings"))
        return g

    def _apply_drawings_field_alias(self, layer):
        """Applique l'alias traduit 'Drawings' sur le champ interne drawings."""
        try:
            idx = layer.fields().indexOf(DRAWINGS_FIELD_NAME)
            if idx != -1:
                layer.setFieldAlias(idx, self.tr("Drawings"))
        except Exception:
            pass

    # --------------------------------------------------
    #  Nettoyage automatique Drawings / Dessins (legacy)
    # --------------------------------------------------

    def _iter_groups_recursive(self, group: QgsLayerTreeGroup):
        """Itère récursivement sur tous les sous-groupes (incluant le group lui-même)."""
        stack = [group]
        while stack:
            g = stack.pop()
            yield g
            for ch in g.children():
                if isinstance(ch, QgsLayerTreeGroup):
                    stack.append(ch)

    def _find_all_legacy_drawings_groups(self):
        """
        Retourne tous les groupes dont le nom normalisé est dans lst_group_drawing
        et qui ne sont PAS tagués avec la clé interne.
        Recherche récursive dans tout l'arbre.
        """
        legacy = []
        for g in self._iter_groups_recursive(self.root):
            try:
                if g is self.root:
                    continue
                # ignore le groupe tagué
                if g.customProperty("qdrawEVT/group_key", "") == DRAWINGS_GROUP_KEY:
                    continue
            except Exception:
                pass

            try:
                if normalize(g.name()) in lst_group_drawing:
                    legacy.append(g)
            except Exception:
                pass
        return legacy

    def _ensure_main_drawings_group(self) -> QgsLayerTreeGroup:
        """Crée ou récupère le groupe Drawings tagué (stable)."""
        g = self._find_group_by_key(DRAWINGS_GROUP_KEY)
        if g is None:
            g = QgsLayerTreeGroup("DRAWINGS")
            g.setCustomProperty("qdrawEVT/group_key", DRAWINGS_GROUP_KEY)
            # au choix : en haut de l’arbre
            self.root.insertChildNode(0, g)
        g.setName(self.tr("Drawings"))
        return g

    def _move_children(self, src: QgsLayerTreeGroup, dst: QgsLayerTreeGroup):
        """
        Déplace les enfants de src vers dst en conservant leur ordre visuel.
        IMPORTANT : on déplace les nodes eux-mêmes (pas clone),
        car clone + remove est souvent source de surprises.
        """
        if src is None or dst is None or src == dst:
            return

        # On prend une copie de la liste car on va la modifier
        children = list(src.children())
        if not children:
            return

        # Déplacement : on retire puis on insère dans dst
        # On conserve l'ordre (les ajouter à la fin)
        for node in children:
            try:
                src.removeChildNode(node)
                dst.addChildNode(node)
            except Exception:
                # fallback "clone" si QGIS refuse de déplacer un node existant
                try:
                    cloned = node.clone()
                    dst.addChildNode(cloned)
                    src.removeChildNode(node)
                except Exception:
                    pass

    def _remove_group_if_empty(self, g: QgsLayerTreeGroup):
        """Supprime un groupe s’il est vide."""
        try:
            if g is None or g is self.root:
                return
            if len(g.children()) > 0:
                return
            parent = g.parent()
            if parent:
                parent.removeChildNode(g)
        except Exception:
            pass

    def _auto_cleanup_drawings_groups(self):
        """
        Fusionne tous les groupes legacy Dessins/Drawings (même imbriqués) vers le groupe tagué.
        Puis supprime les legacy vides.
        """
        try:
            main = self._ensure_main_drawings_group()

            legacy_groups = self._find_all_legacy_drawings_groups()
            if not legacy_groups:
                return

            # Si par malheur le main est aussi dans legacy (cas bizarre), on l’enlève
            legacy_groups = [g for g in legacy_groups if g != main]

            # Fusion
            for g in legacy_groups:
                self._move_children(g, main)

            # Suppression legacy vides
            for g in legacy_groups:
                self._remove_group_if_empty(g)

            # Garantit le nom UI final (traduit)
            main.setName(self.tr("Drawings"))

            # Rafraîchit la vue
            try:
                self.iface.layerTreeView().refreshLayerSymbology("")
            except Exception:
                pass

        except Exception:
            pass

    def unload(self):
        log.info("QdrawEVT plugin unloaded cleanly.")

        # Retire les actions ajoutées au menu + détruit les QAction
        for action in getattr(self, "actions", []):
            try:
                self.iface.removePluginMenu(self.menu, action)
            except Exception:
                pass
            try:
                action.deleteLater()
            except Exception:
                pass
        self.actions = []

        # Retire l’action "update_gc" (elle n’est pas dans self.actions chez toi)
        a = getattr(self, "action_update_gc", None)
        if a is not None:
            try:
                self.iface.removePluginMenu(self.menu, a)
            except Exception:
                pass
            try:
                a.deleteLater()
            except Exception:
                pass
            self.action_update_gc = None

        # Retire la toolbar
        tb = getattr(self, "toolbar", None)
        if tb is not None:
            try:
                self.iface.mainWindow().removeToolBar(tb)
            except Exception:
                pass
            tb.deleteLater()
            self.toolbar = None

        # Sécurité : supprime toute toolbar fantôme restante
        try:
            self._remove_existing_toolbar()
        except Exception:
            pass

    def read_plugin_version(self, plugin_dir: Path) -> str:
        """
        Renvoie la version du plugin à partir de metadata.txt
        Utilise configparser pour une lecture robuste (sections INI).
        """
        import configparser
        metadata_file = plugin_dir / "metadata.txt"
        if not metadata_file.is_file():
            return "0.0.0"
        config = configparser.ConfigParser()
        try:
            config.read(metadata_file, encoding="utf-8")
            # QGIS metadata.txt possède normalement une section [general]
            if "general" in config and "version" in config["general"]:
                return config["general"]["version"].strip()
            # fallback si fichier non standard
            for section in config.sections():
                if "version" in config[section]:
                    return config[section]["version"].strip()
        except Exception:
            pass
        return "0.0.0"

    def version_is_newer(self, v1: str, v2: str) -> bool:
        """Retourne True si v1 > v2."""
        def parse(v):
            return tuple(int(x) for x in v.split(".") if x.isdigit())
        return parse(v1) > parse(v2)

    def unload_plugin_clean(self, plugin_name: str):
        """Décharge proprement un plugin déjà actif, supprime icônes & actions."""
        try:
            pm = getattr(self.iface, "_QGISPluginManager", None)
        except Exception:
            pm = None
        # 1) Appel direct à unload pour le plugin courant
        try:
            if hasattr(self.iface, "removePluginMenu"):
                # Supprime les éléments de menu/menu contextuel potentiellement doublés
                # (QGIS ne nettoie pas toujours correctement)
                pass
        except Exception:
            pass
        # 2) Si instance chargée via notre loader, appelons unloadGui()
        if hasattr(self, "_loaded_plugins") and plugin_name in self._loaded_plugins:
            instance = self._loaded_plugins[plugin_name]
            if hasattr(instance, "unload"):
                try:
                    instance.unload()
                except Exception:
                    pass
            if hasattr(instance, "unloadGui"):
                try:
                    instance.unloadGui()
                except Exception:
                    pass
            # Enlever du registre interne
            del self._loaded_plugins[plugin_name]
        return True

    def update_gestion_crise(self):
        """
        Mise à jour / installation du plugin gestion_crise depuis le ZIP embarqué.
        - Compare versions (ZIP vs installé)
        - Supprime proprement l’ancienne version
        - Installe la nouvelle
        - Active immédiatement le plugin
        - Recharger sans redémarrer QGIS
        - Rafraîchir le gestionnaire d’extensions
        """
        import zipfile
        import tempfile
        import shutil
        from qgis.PyQt.QtCore import QFile, QIODevice
        from pathlib import Path
        import qgis.utils

        plugin_name = "gestion_crise"
        plugins_dir = Path(QgsApplication.qgisSettingsDirPath()) / "python" / "plugins"
        plugin_dir = plugins_dir / plugin_name

        # ------------------------------------------------------------
        # 1) Lire version locale du plugin (si déjà installé)
        # ------------------------------------------------------------
        installed_version = self.read_plugin_version(plugin_dir) if plugin_dir.is_dir() else "0.0.0"
        # ------------------------------------------------------------
        # 2) Extraire le ZIP embarqué dans les ressources
        # ------------------------------------------------------------
        resource_path = ":/plugins/qdrawEVT/resources/gestion_crise.zip"
        qfile = QFile(resource_path)

        if not qfile.exists():
            QMessageBox.critical(self.iface.mainWindow(), "QdrawEVT",
                                 "Ressource gestion_crise.zip introuvable.")
            return

        if not qfile.open(QIODevice.ReadOnly):
            QMessageBox.critical(self.iface.mainWindow(), "QdrawEVT",
                                 "Impossible de lire gestion_crise.zip.")
            return
        # Sauvegarde temporaire du ZIP
        data = bytes(qfile.readAll())
        qfile.close()

        temp_zip = Path(tempfile.gettempdir()) / "gestion_crise_update.zip"
        with open(temp_zip, "wb") as z:
            z.write(data)
        # Extraction temporaire
        tmp_extract = Path(tempfile.gettempdir()) / "gc_update_extract"
        if tmp_extract.exists():
            shutil.rmtree(tmp_extract)
        with zipfile.ZipFile(temp_zip, "r") as z:
            z.extractall(tmp_extract)

        zip_plugin_dir = tmp_extract / plugin_name
        zip_version = self.read_plugin_version(zip_plugin_dir)
        # ------------------------------------------------------------
        # 3) Comparer les versions
        # ------------------------------------------------------------
        if not self.version_is_newer(zip_version, installed_version):
            QMessageBox.information(
                self.iface.mainWindow(),
                "QdrawEVT",
                f"Aucune mise à jour nécessaire.\n"
                f"Version installée : {installed_version}\n"
                f"Version ZIP : {zip_version}"
            )
            return
        # ------------------------------------------------------------
        # 4) Décharger Gestion_crise proprement côté QGIS
        # ------------------------------------------------------------
        # Supprimer les icônes existantes (via notre loader interne)
        try:
            self.unload_plugin_clean(plugin_name)
        except Exception:
            pass
        # ------------------------------------------------------------
        # 5) Installer la nouvelle version du plugin
        # ------------------------------------------------------------
        try:
            if plugin_dir.exists():
                shutil.rmtree(plugin_dir)

            shutil.copytree(zip_plugin_dir, plugin_dir)

        except Exception as e:
            QMessageBox.critical(
                self.iface.mainWindow(),
                "QdrawEVT",
                f"Erreur lors de l’installation de Gestion_crise :\n{e}"
            )
            return
        # ------------------------------------------------------------
        # 6) Activer le plugin dans qgis.ini
        # ------------------------------------------------------------
        settings = QgsSettings()
        settings.beginGroup("PythonPlugins")
        settings.setValue(plugin_name, True)
        settings.endGroup()
        # ------------------------------------------------------------
        # 7) Recharger le plugin immédiatement
        # ------------------------------------------------------------
        ok = False
        try:
            ok = qgis.utils.reloadPlugin(plugin_name)
        except Exception:
            ok = False

        if not ok:
            QMessageBox.warning(
                self.iface.mainWindow(),
                "QdrawEVT",
                "Plugin mis à jour mais n’a pas pu être rechargé automatiquement."
            )
        else:
            QMessageBox.information(
                self.iface.mainWindow(),
                "QdrawEVT",
                f"Installation réussie de Gestion_crise (v{zip_version})."
            )
        # ------------------------------------------------------------
        # 8) Rafraîchir la fenêtre Extensions (QGIS 3.x)
        # ------------------------------------------------------------
        try:
            pm = self.iface.pluginManagerInterface()
            pm.reloadModel()  # Re-scan complet des plugins installés
        except Exception:
            pass
        # ------------------------------------------------------------
        # 9) Ouvrir la fenêtre Extensions (optionnel)
        # ------------------------------------------------------------
        # self.iface.actionManagePlugins().trigger()

    def add_action(
            self,
            icon_path,
            text,
            callback,
            enabled_flag=True,
            checkable=False,
            add_to_menu=True,
            add_to_toolbar=True,
            status_tip=None,
            whats_this=None,
            menu=None,
            parent=None):
        icon = QIcon(icon_path)
        action = QAction(icon, text, parent)
        action.triggered.connect(callback)
        action.setEnabled(enabled_flag)
        action.setCheckable(checkable)

        if status_tip is not None:
            action.setStatusTip(status_tip)

        if whats_this is not None:
            action.setWhatsThis(whats_this)

        if menu is not None:
            action.setMenu(menu)

        if add_to_toolbar:
            self.toolbar.addAction(action)

        if add_to_menu:
            self.iface.addPluginToMenu(
                self.menu,
                action)

        self.actions.append(action)

        return action

    def initGui(self):
        # Chargement de l’interface et actions
        log.info("Initialisation de l’interface graphique QdrawEVT")
        redirect_print_to_log()  # ✅ redirection sûre ici
        log.info("Redirection print() → logger QdrawEVT activée")

        # --- Toolbar: éviter les doublons après mise à jour/reload ---
        self._remove_existing_toolbar()

        self.toolbar = self.iface.addToolBar(TOOLBAR_TITLE)
        self.toolbar.setObjectName(TOOLBAR_OBJECT_NAME)
        self.toolbar.setToolTip("QdrawEVT")

        # Emplacement réservé pour les icônes Gestion_crise (au début)
        self.gc_anchor = QAction("gc_anchor", self.toolbar)
        self.gc_anchor.setVisible(False)  # invisible mais agit comme un repère
        self.toolbar.addAction(self.gc_anchor)

        pointMenu = QMenu()
        pointMenu.addAction(
            QIcon(':/plugins/qdrawEVT/resources/icon_DrawPtXY_EVT.png'),
            self.tr('XY Point drawing tool'), self.drawXYPoint)
        pointMenu.addAction(
            QIcon(':/plugins/qdrawEVT/resources/icon_DrawPtDMS_EVT.png'),
            self.tr('DMS Point drawing tool'), self.drawDMSPoint)
        icon_path = ':/plugins/qdrawEVT/resources/icon_DrawText_EVT.png'
        self.add_action(
            icon_path,
            text=self.tr('Text drawing tool'),
            checkable=True,
            callback=self.drawText,
            parent=self.iface.mainWindow()
        )
        icon_path = ':/plugins/qdrawEVT/resources/icon_DrawPt_EVT.png'
        self.add_action(
            icon_path,
            text=self.tr('Point drawing tool'),
            checkable=True,
            menu=pointMenu,
            callback=self.drawPoint,
            parent=self.iface.mainWindow()
        )
        icon_path = ':/plugins/qdrawEVT/resources/icon_DrawL_EVT.png'
        self.add_action(
            icon_path,
            text=self.tr('Line drawing tool'),
            checkable=True,
            callback=self.drawLine,
            parent=self.iface.mainWindow()
        )
        icon_path = ':/plugins/qdrawEVT/resources/icon_DrawR_EVT.png'
        self.add_action(
            icon_path,
            text=self.tr('Rectangle drawing tool'),
            checkable=True,
            callback=self.drawRect,
            parent=self.iface.mainWindow()
        )
        icon_path = ':/plugins/qdrawEVT/resources/icon_DrawC_EVT.png'
        self.add_action(
            icon_path,
            text=self.tr('Circle drawing tool'),
            checkable=True,
            callback=self.drawCircle,
            parent=self.iface.mainWindow()
        )
        icon_path = ':/plugins/qdrawEVT/resources/icon_DrawP_EVT.png'
        self.add_action(
            icon_path,
            text=self.tr('Polygon drawing tool'),
            checkable=True,
            callback=self.drawPolygon,
            parent=self.iface.mainWindow()
        )
        bufferMenu = QMenu()
        polygonBufferAction = QAction(
            QIcon(':/plugins/qdrawEVT/resources/icon_DrawTP_EVT.png'),
            self.tr('Buffer drawing with polygonal selection of objects in the selected layer'),
            bufferMenu)
        polygonBufferAction.triggered.connect(self.drawPolygonBuffer)
        bufferMenu.addAction(polygonBufferAction)
        icon_path = ':/plugins/qdrawEVT/resources/icon_DrawT_EVT.png'
        self.add_action(
            icon_path,
            text=self.tr('Buffer drawing on objects in selected layer'),
            checkable=True,
            menu=bufferMenu,
            callback=self.drawBuffer,
            parent=self.iface.mainWindow()
        )
        icon_path = ':/plugins/qdrawEVT/resources/icon_Settings_EVT.png'
        self.add_action(
            icon_path,
            text=self.tr('Settings'),
            callback=self.showSettingsWindow,
            parent=self.iface.mainWindow()
        )

        # --- Mettre à jour plugin Gestion_Crise ---
        self.action_update_gc = QAction(
            QIcon(":/plugins/qdrawEVT/resources/icon_GC.png"),
            self.tr("Install the Gestion_crise plugin"), self.iface.mainWindow()
        )
        self.action_update_gc.triggered.connect(self.update_gestion_crise)
        self.iface.addPluginToMenu(self.menu, self.action_update_gc)

        # Détermination de l'icône native QGIS pour l'aide
        icon_help = QgsApplication.getThemeIcon("mActionHelpContents.svg")
        self.action_aide = QAction(
            icon_help,
            self.tr("Help / User manual"),
            self.iface.mainWindow()
        )
        self.action_aide.setObjectName("gc_action_aide")
        self.action_aide.triggered.connect(self.afficher_aide)
        # On ajoute l’action EN BAS du menu
        self.iface.addPluginToMenu(self.menu, self.action_aide)
        # On garde une trace pour unload()
        self.actions.append(self.action_aide)

    # --- Fonction d'installation du plugin gestion_crise ---
    def install_gestion_crise(self):
        """
        Installe le plugin gestion_crise à partir du ZIP embarqué
        dans les ressources de qdrawEVT, l’active et le charge sans
        redémarrer QGIS.
        """
        try:
            from qgis.PyQt.QtCore import QFile, QIODevice
            import zipfile
            import tempfile

            # 1) ZIP embarqué dans les ressources de qdrawEVT
            resource_path = ":/plugins/qdrawEVT/resources/gestion_crise.zip"
            qfile = QFile(resource_path)

            if not qfile.exists():
                QMessageBox.critical(
                    self.iface.mainWindow(),
                    "QdrawEVT",
                    "Le fichier gestion_crise.zip est introuvable dans les ressources "
                    "(ajoute-le dans resources.qrc et recompile resources.py).",
                )
                return

            if not qfile.open(QIODevice.ReadOnly):
                QMessageBox.critical(
                    self.iface.mainWindow(),
                    "QdrawEVT",
                    "Impossible d’ouvrir gestion_crise.zip dans les ressources.",
                )
                return

            data = qfile.readAll()
            qfile.close()

            # 2) Écriture dans un ZIP temporaire
            temp_zip = Path(tempfile.gettempdir()) / "gestion_crise_install.zip"
            with open(temp_zip, "wb") as zf:
                zf.write(bytes(data))

            # 3) Extraction dans le dossier plugins utilisateur
            plugins_dir = Path(QgsApplication.qgisSettingsDirPath()) / "python" / "plugins"
            plugins_dir.mkdir(parents=True, exist_ok=True)

            with zipfile.ZipFile(temp_zip, "r") as z:
                z.extractall(plugins_dir)

            # 4) Nom du dossier du plugin (dans le ZIP, c’est "gestion_crise/")
            plugin_folder = "gestion_crise"
            plugin_path = plugins_dir / plugin_folder
            if not plugin_path.is_dir():
                QMessageBox.critical(
                    self.iface.mainWindow(),
                    "QdrawEVT",
                    f"Le dossier du plugin n’a pas été trouvé : {plugin_path}",
                )
                return

            # 5) Activer dans [PythonPlugins] de qgis.ini
            settings = QgsSettings()
            settings.beginGroup("PythonPlugins")
            settings.setValue(plugin_folder, True)
            settings.endGroup()

            # 6) Chargement immédiat sans redémarrage
            ok = self.force_load_plugin_standard(plugin_folder)

            if ok:
                QMessageBox.information(
                    self.iface.mainWindow(),
                    "QdrawEVT",
                    "Le plugin Gestion_crise a été installé et chargé immédiatement.",
                )
            else:
                QMessageBox.warning(
                    self.iface.mainWindow(),
                    "QdrawEVT",
                    "Le plugin a été installé mais n’a pas pu être chargé automatiquement.\n"
                    "Il restera néanmoins disponible dans le Gestionnaire d’extensions.",
                )

        except Exception as e:
            QMessageBox.critical(
                self.iface.mainWindow(),
                "QdrawEVT",
                f"Erreur lors de l’installation : {e}",
            )

    def force_load_plugin_standard(self, plugin_name: str) -> bool:
        """Charge un plugin standard (avec classFactory) sans redémarrage."""
        try:
            import importlib, sys
            plugins_dir = Path(QgsApplication.qgisSettingsDirPath()) / "python" / "plugins"
            plugin_dir = plugins_dir / plugin_name

            if not plugin_dir.is_dir():
                return False

            if str(plugins_dir) not in sys.path:
                sys.path.insert(0, str(plugins_dir))

            # Import / reload package
            try:
                if plugin_name in sys.modules:
                    pkg = importlib.reload(sys.modules[plugin_name])
                else:
                    pkg = importlib.import_module(plugin_name)
            except Exception as e:
                QMessageBox.critical(self.iface.mainWindow(), "QdrawEVT",
                                     f"Erreur import plugin {plugin_name} : {e}")
                return False

            if not hasattr(pkg, "classFactory"):
                QMessageBox.warning(self.iface.mainWindow(), "QdrawEVT",
                                    "classFactory non trouvé dans le plugin.")
                return False

            plugin_instance = pkg.classFactory(self.iface)

            if hasattr(plugin_instance, "initGui"):
                plugin_instance.initGui()

            # Empêche destruction par Python
            if not hasattr(self, "_loaded_plugins"):
                self._loaded_plugins = {}
            self._loaded_plugins[plugin_name] = plugin_instance

            return True

        except Exception as e:
            QMessageBox.critical(self.iface.mainWindow(), "QdrawEVT",
                                 f"Erreur chargement automatique plugin : {e}")
            return False

    # --- Fonction principale d'affichage de la table d’attributs ---
    def showAttributesTable(self, name):
        """Affiche la table d’attributs pour toutes les couches visibles contenant une sélection."""
        # Création de la fenêtre d’attributs
        tab = AttributesTable(name, self.iface)
        tab.setWindowFlags(qt_window_stays_on_top_hint)

        layers = self.project.mapLayers().values()
        selected_found = False  # Pour vérifier s'il y a au moins une sélection

        for layer in layers:
            try:
                # Ne traiter que les couches vectorielles visibles
                if (
                        layer.type() == QgsMapLayer.VectorLayer
                        and QgsProject.instance().layerTreeRoot().findLayer(layer.id()).isVisible()
                ):
                    cells = layer.selectedFeatures()
                    if not cells:
                        continue

                    selected_found = True

                    # Récupération des colonnes non masquées et de leurs alias
                    columns = layer.attributeTableConfig().columns()
                    fields_name = []
                    fields_type = []
                    idx_visible = []

                    for col in columns:
                        if col.hidden:
                            continue
                        idx = layer.fields().indexOf(col.name)
                        alias = layer.attributeAlias(idx)
                        fields_name.append(alias if alias else col.name)
                        fields_type.append(col.type)
                        idx_visible.append(idx)

                    # Ajout de la couche dans la table
                    tab.addLayer(layer, fields_name, fields_type, cells, idx_visible)

            except Exception as e:
                QgsMessageLog.logMessage(f"Erreur dans showAttributesTable : {e}", "QdrawEVT", Qgis.Warning)

        # Si aucune sélection trouvée
        if not selected_found:
            QMessageBox.information(
                self.iface.mainWindow(),
                self.tr("No selection"),
                self.tr("No selected items found in visible vector layers."),
            )
            return

        # Fonction utilitaire : détection des colonnes d’URL
        def is_url_item(item):
            if not item:
                return False
            txt = item.text().strip().lower()
            return txt.startswith("http://") or txt.startswith("https://")

        # Application du UrlDelegate sur les colonnes d’URL
        for idx in range(tab.tabWidget.count()):
            widget = tab.tabWidget.widget(idx)
            table_widget = next(
                (child for child in widget.children() if isinstance(child, QTableWidget)), None
            )
            if not table_widget:
                continue

            for col in range(table_widget.columnCount()):
                if all(
                        is_url_item(table_widget.item(row, col))
                        for row in range(table_widget.rowCount())
                        if table_widget.item(row, col)
                ):
                    table_widget.setItemDelegateForColumn(col, UrlDelegate(table_widget))

        # Affichage de la fenêtre d’attributs
        tab.loadingWindow.close()
        tab.show()
        tab.activateWindow()
        tab.showNormal()

        # Connexion du signal destroyed pour nettoyage
        tab.destroyed.connect(lambda: self.closeAttributesTable(tab))

        # Sauvegarde dans la liste des résultats et nettoyage de la sélection
        self.results.append(tab)
        self.unselectAll()

    def unselectAll(self):
        # Récupérer toutes les couches du projet
        layers = QgsProject.instance().mapLayers().values()
        # Parcourir chaque couche
        for layer in layers:
            if layer.type() == QgsMapLayer.VectorLayer:  # Vérifier si c'est une couche vectorielle
                layer.removeSelection()  # Désélectionner tous les éléments
        # Mettre à jour l'affichage de la carte
        self.iface.mapCanvas().refresh()

    def closeAttributesTable(self, tab):
        """
        Supprime la fenêtre d’attributs fermée de la liste des résultats actifs.
        Évite les références fantômes et nettoie proprement la mémoire.
        """
        try:
            if tab in self.results:
                self.results.remove(tab)
                QgsMessageLog.logMessage(
                    f"AttributesTable fermée et retirée de self.results ({len(self.results)} restantes).",
                    "QdrawEVT",
                    Qgis.Info,
                )
        except Exception as e:
            QgsMessageLog.logMessage(
                f"Erreur dans closeAttributesTable : {e}", "QdrawEVT", Qgis.Warning
            )

    def testGroups(self):
        # mise en variable des groupes a gérer pour la sélection
        self.group_no_select = []
        for group in self.root.children():
            test = ''.join(x for x in unicodedata.normalize('NFKD', group.name()) if unicodedata.category(x)[0] == 'L').upper()
            if test in lst_group_no_select:
                self.group_no_select.append(group)
        self.group_evt = []
        for group in self.root.children():
            test = ''.join(x for x in unicodedata.normalize('NFKD', group.name()) if unicodedata.category(x)[0] == 'L').upper()
            if test in lst_group_evt:
                self.group_evt.append(group)
                self.group_no_select.append(group)

        # --------------------------------------------------
        # Groupes Drawings
        # - priorité à la clé interne
        # - fallback legacy par nom (Drawings / Dessins)
        # --------------------------------------------------
        self.group_drawing = []
        # 1) Groupe Drawings tagué (CAS NORMAL)
        g_draw = self._find_group_by_key(DRAWINGS_GROUP_KEY)
        if g_draw is not None:
            self.group_drawing.append(g_draw)
            self.group_no_select.append(g_draw)
        # 2) Fallback legacy (anciens projets)
        else:
            for group in self.root.children():
                if not isinstance(group, QgsLayerTreeGroup):
                    continue

                test = normalize(group.name())

                if test in lst_group_drawing:
                    self.group_drawing.append(group)
                    self.group_no_select.append(group)

        if self.group_evt:
            self.evt = True
        else:
            self.evt = False

    def isEVT(self):
        layers_evt = []
        layers_group_drawing = []
        sortieEVT = False
        for group in self.root.children():
            test = ''.join(x for x in unicodedata.normalize('NFKD', group.name()) if unicodedata.category(x)[0] == 'L').upper()
            if test in lst_group_evt:
                # Réinitialisation de l'analyse thématique des couches évènement
                if group:
                    for lyr in group.children():
                        layers_evt.append(self.project.mapLayersByName(lyr.name())[0])
                        # self.QgisV3LayerRandomColor(layer, form_wgt_libelle)
                        # self.iface.layerTreeView().refreshLayerSymbology(layer.name())
                # return True, layers_evt
                sortieEVT = True
                break

            # Code en prévision utilisation couche dessin
            # elif test in lst_group_drawing:
            #     if group:
            #         for lyr in group.children():
            #             layers_group_drawing.append(self.project.mapLayersByName(lyr.name())[0])
            #             # self.QgisV3LayerRandomColor(layer, form_wgt_libelle)
            #             # self.iface.layerTreeView().refreshLayerSymbology(layer.name())
            #     sortie_group_drawing = True

        if sortieEVT:
            return True, layers_evt
        else:
            return False, layers_evt

    def drawPoint(self):
        # Modification qdrawEVT
        # print('self.isEVT() : ' + str(self.isEVT()))
        if self.isEVT() == None:
            self.msgisEVTNone()
            return
        elif self.isEVT()[0]:
            self.actions[1].setIcon(QIcon(':/plugins/qdrawEVT/resources/icon_DrawPt_EVT'))
            self.actions[1].menu().actions()[0].setIcon(QIcon(':/plugins/qdrawEVT/resources/icon_DrawPtXY_EVT'))
            self.actions[1].menu().actions()[1].setIcon(QIcon(':/plugins/qdrawEVT/resources/icon_DrawPtDMS_EVT'))
        else:
            self.actions[1].setIcon(QIcon(':/plugins/qdrawEVT/resources/icon_DrawPt'))
            self.actions[1].menu().actions()[0].setIcon(QIcon(':/plugins/qdrawEVT/resources/icon_DrawPtXY'))
            self.actions[1].menu().actions()[1].setIcon(QIcon(':/plugins/qdrawEVT/resources/icon_DrawPtDMS'))
        self.layerevtname = evtpnt
        self.actions[1].triggered.disconnect()
        self.actions[1].triggered.connect(self.drawPoint)
        self.actions[1].menu().actions()[0].triggered.disconnect()
        self.actions[1].menu().actions()[0].triggered.connect(self.drawXYPoint)
        self.actions[1].menu().actions()[1].triggered.disconnect()
        self.actions[1].menu().actions()[1].triggered.connect(self.drawDMSPoint)
        if self.tool:
            self.tool.reset()
        self.tool = DrawPoint(self.iface, self.settings.getColor())
        self.tool.setAction(self.actions[1])
        self.tool.selectionDone.connect(self.draw)
        self.iface.mapCanvas().setMapTool(self.tool)
        self.drawShape = 'point'
        self.toolname = 'drawPoint'
        self.resetSB()

    def drawText(self):
        if self.tool:
            self.tool.reset()
        self.tool = DrawPoint(self.iface, self.settings.getColor())
        self.tool.setAction(self.actions[0])
        self.tool.selectionDone.connect(self.draw)
        self.iface.mapCanvas().setMapTool(self.tool)
        self.drawShape = 'text'
        self.toolname = 'drawText'
        # Modification qdrawEVT
        self.layerevtname = None
        self.resetSB()

    def drawXYPoint(self):
        # Modification qdrawEVT
        self.layerevtname = evtpnt
        mc = self.iface.mapCanvas()
        ms = mc.mapSettings()
        # default for drawXYPoint data : WGS84 EPSG:4326
        crs = QgsCoordinateReferenceSystem(4326)
        ms.setDestinationCrs(crs)
        if self.isEVT() == None:
            self.msgisEVTNone()
            return
        elif self.isEVT()[0]:
            self.actions[1].setIcon(
                QIcon(':/plugins/qdrawEVT/resources/icon_DrawPt_EVT.png'))
            self.actions[1].menu().actions()[0].setIcon(
                QIcon(':/plugins/qdrawEVT/resources/icon_DrawPtXY_EVT.png'))
            self.actions[1].menu().actions()[1].setIcon(
                QIcon(':/plugins/qdrawEVT/resources/icon_DrawPtDMS_EVT.png'))
        elif not self.isEVT()[0]:
            self.actions[1].setIcon(
                QIcon(':/plugins/qdrawEVT/resources/icon_DrawPt.png'))
            self.actions[1].menu().actions()[0].setIcon(
                QIcon(':/plugins/qdrawEVT/resources/icon_DrawPtXY.png'))
            self.actions[1].menu().actions()[1].setIcon(
                QIcon(':/plugins/qdrawEVT/resources/icon_DrawPtDMS.png'))
        self.actions[1].menu().actions()[0].setText(
            self.tr('XY point drawing tool'))
        self.actions[1].menu().actions()[1].setText(
            self.tr('DMS point drawing tool'))
        self.actions[1].menu().actions()[0].triggered.disconnect()
        self.actions[1].menu().actions()[0].triggered.connect(self.drawXYPoint)
        self.actions[1].menu().actions()[1].triggered.disconnect()
        self.actions[1].menu().actions()[1].triggered.connect(self.drawDMSPoint)
        try:
            xytuple, ok = XYDialog().getPoint(ms.destinationCrs())
        except:
            pyqt_messagebox_critical(
                self.iface.mainWindow(),
                self.tr('Error'), self.tr('Saisie incorrecte ou clic sur Annuler'))
            return
        # print('xytuple : ' + str(xytuple))
        # print('crs : ' + str(crs))
        point = xytuple[0]
        ptOrigin = point
        if xytuple[1] != crs:
            crsSrc = xytuple[1] # projection choisie par l'utilisateur
        else:
            crsSrc = crs # projection définie par défaut dans drawXYPoint() qdrawEVT.py (4326)
        crsDest = QgsProject.instance().crs()
        crsTransform = QgsCoordinateTransform(crsSrc,crsDest, QgsProject.instance())
        # Transformation crsSrc -> crsDest
        point = crsTransform.transform(ptOrigin)
        # Remplacé pour utilisation de la projection du projet en cours
        # crs = QgsCoordinateReferenceSystem(2154)
        # Ligne de remplacement
        crs = QgsProject.instance().crs()
        ms.setDestinationCrs(crs)
        self.XYcrs = crs
        if ok:
            if point.x() == 0 and point.y() == 0:
                pyqt_messagebox_critical(
                    self.iface.mainWindow(),
                    self.tr('Error'), self.tr('Invalid input !'))
                if self.tool:
                    self.tool.reset()
                self.tool = DrawPoint(self.iface, self.settings.getColor())
                self.tool.setAction(self.actions[1])
                self.tool.selectionDone.connect(self.drawPoint)
                self.iface.mapCanvas().setMapTool(self.tool)
                self.drawShape = 'point'
                self.toolname = 'drawPoint'
                self.resetSB()
                return
            else:
                self.drawPoint()
                self.tool.rb = QgsRubberBand(
                    self.iface.mapCanvas(), QgsWkbTypes.PointGeometry)
                self.tool.rb.addPoint(point)
                self.tool.rb.setColor(self.settings.getColor())
                self.tool.rb.setWidth(4)
                self.drawShape = 'XYpoint'
                # print('self.tool.rb : ' + str(self.tool.rb))
                self.draw()
                mc.setCenter(point)
                mc.refresh()
#                # wait .5 seconds to simulate a flashing effect
#                QTimer.singleShot(500,self.resetSB)

    def drawDMSPoint(self):
        # Modification qdrawEVT
        mc = self.iface.mapCanvas()
        ms = mc.mapSettings()
        # Gestion du changement d'aspact de l'icone d'outil
        self.layerevtname = evtpnt
        if self.isEVT() == None:
            self.msgisEVTNone()
            return
        elif self.isEVT()[0]:
            self.actions[1].setIcon(
                QIcon(':/plugins/qdrawEVT/resources/icon_DrawPt_EVT.png'))
            self.actions[1].menu().actions()[0].setIcon(
                QIcon(':/plugins/qdrawEVT/resources/icon_DrawPtXY_EVT.png'))
            self.actions[1].menu().actions()[1].setIcon(
                QIcon(':/plugins/qdrawEVT/resources/icon_DrawPtDMS_EVT.png'))
        elif not self.isEVT()[0]:
            self.actions[1].setIcon(
                QIcon(':/plugins/qdrawEVT/resources/icon_DrawPt.png'))
            self.actions[1].menu().actions()[0].setIcon(
                QIcon(':/plugins/qdrawEVT/resources/icon_DrawPtXY.png'))
            self.actions[1].menu().actions()[1].setIcon(
                QIcon(':/plugins/qdrawEVT/resources/icon_DrawPtDMS.png'))
        self.actions[1].menu().actions()[0].setText(
            self.tr('XY point drawing tool'))
        self.actions[1].menu().actions()[1].setText(
            self.tr('DMS point drawing tool'))
        self.actions[1].menu().actions()[0].triggered.disconnect()
        self.actions[1].menu().actions()[0].triggered.connect(self.drawXYPoint)
        self.actions[1].menu().actions()[1].triggered.disconnect()
        self.actions[1].menu().actions()[1].triggered.connect(self.drawDMSPoint)

        point, ok = DMSDialog().getPoint()
        # default for drawDMSPoint data : WGS84 EPSG:4326
        crsSrc = QgsCoordinateReferenceSystem(4326)
        crsDest = QgsProject.instance().crs()
        ptOrigin = point
        crsTransform = QgsCoordinateTransform(crsSrc,crsDest, QgsProject.instance())
        # Transformation crsSrc -> crsDest
        point = crsTransform.transform(ptOrigin)
        crs = QgsProject.instance().crs()
        ms.setDestinationCrs(crs)
        self.XYcrs = crs

        if ok:
            if point.x() == 0 and point.y() == 0:
                pyqt_messagebox_critical(
                    self.iface.mainWindow(),
                    self.tr('Error'), self.tr('Invalid input !'))
            else:
                self.drawPoint()
                self.tool.rb = QgsRubberBand(
                    self.iface.mapCanvas(), QgsWkbTypes.PointGeometry)
                self.tool.rb.setColor(self.settings.getColor())
                self.tool.rb.setWidth(3)
                self.tool.rb.addPoint(point)
                self.drawShape = 'XYpoint'
                self.draw()
                # centrer la carte sur le point
                mc.setCenter(point)
                mc.refresh()

    def drawLine(self):
        # Modification qdrawEVT
        if self.isEVT() == None:
            self.msgisEVTNone()
            return
        elif self.isEVT()[0]:
            self.actions[2].setIcon(QIcon(':/plugins/qdrawEVT/resources/icon_DrawL_EVT'))
        else:
            self.actions[2].setIcon(QIcon(':/plugins/qdrawEVT/resources/icon_DrawL'))
        self.layerevtname = evtlgn
        if self.tool:
            self.tool.reset()
        self.request = 'intersects'
        self.tool = DrawLine(self.iface, self.settings.getColor())
        self.tool.setAction(self.actions[2])
        self.tool.selectionDone.connect(self.draw)
        self.tool.move.connect(self.updateSB)
        self.iface.mapCanvas().setMapTool(self.tool)
        self.drawShape = 'line'
        self.toolname = 'drawLine'
        self.resetSB()

    def drawRect(self):
        # Modification qdrawEVT
        if self.isEVT() == None:
            self.msgisEVTNone()
            return
        elif self.isEVT()[0]:
            self.actions[3].setIcon(QIcon(':/plugins/qdrawEVT/resources/icon_DrawR_EVT'))
        else:
            self.actions[3].setIcon(QIcon(':/plugins/qdrawEVT/resources/icon_DrawR'))
        self.layerevtname = evtpol
        if self.tool:
            self.tool.reset()
        self.request = 'intersects'
        self.tool = DrawRect(self.iface, self.settings.getColor())
        self.tool.setAction(self.actions[3])
        self.tool.selectionDone.connect(self.draw)
        self.tool.move.connect(self.updateSB)
        self.iface.mapCanvas().setMapTool(self.tool)
        self.drawShape = 'polygon'
        self.toolname = 'drawRect'
        self.resetSB()

    def drawCircle(self):
        # Modification qdrawEVT
        if self.isEVT() == None:
            self.msgisEVTNone()
            return
        elif self.isEVT()[0]:
            self.actions[4].setIcon(QIcon(':/plugins/qdrawEVT/resources/icon_DrawC_EVT'))
        else:
            self.actions[4].setIcon(QIcon(':/plugins/qdrawEVT/resources/icon_DrawC'))
        self.layerevtname = evtpol

        if self.tool:
            self.tool.reset()
        self.request = 'intersects'
        self.tool = DrawCircle(self.iface, self.settings.getColor(), 40)
        self.tool.setAction(self.actions[4])
        self.tool.selectionDone.connect(self.draw)
        self.tool.move.connect(self.updateSB)
        self.iface.mapCanvas().setMapTool(self.tool)
        self.drawShape = 'polygon'
        self.toolname = 'drawCircle'
        self.resetSB()

    def drawPolygon(self):
        # Modification qdrawEVT
        if self.isEVT() == None:
            self.msgisEVTNone()
            return
        elif self.isEVT()[0]:
            self.actions[5].setIcon(QIcon(':/plugins/qdrawEVT/resources/icon_DrawP_EVT'))
        else:
            self.actions[5].setIcon(QIcon(':/plugins/qdrawEVT/resources/icon_DrawP'))
        self.layerevtname = evtpol
        if self.tool:
            self.tool.reset()
        self.request = 'intersects'
        self.tool = DrawPolygon(self.iface, self.settings.getColor())
        self.tool.setAction(self.actions[5])
        self.tool.selectionDone.connect(self.draw)
        self.tool.move.connect(self.updateSB)
        self.iface.mapCanvas().setMapTool(self.tool)
        self.drawShape = 'polygon'
        self.toolname = 'drawPolygon'
        self.resetSB()

    def drawBuffer(self):
        self.layerevtname = evtpol
        self.bGeom = None
        if self.tool:
            self.tool.reset()
        self.request = 'intersects'
        self.tool = SelectPoint(self.iface, self.settings.getColor())
        if self.isEVT() == None:
            self.msgisEVTNone()
            return
        elif self.isEVT()[0]:
            self.actions[6].setIcon(
                QIcon(':/plugins/qdrawEVT/resources/icon_DrawT_EVT.png'))
            self.actions[6].menu().actions()[0].setIcon(
                QIcon(':/plugins/qdrawEVT/resources/icon_DrawTP_EVT.png'))
            self.actions[6].setText(
                self.tr('Buffer drawing tool on the selected layer'))
            self.actions[6].menu().actions()[0].setText(
                self.tr('Polygon buffer drawing tool on the selected layer'))
        elif not self.isEVT()[0]:
            self.actions[6].setIcon(
                QIcon(':/plugins/qdrawEVT/resources/icon_DrawT.png'))
            self.actions[6].menu().actions()[0].setIcon(
                QIcon(':/plugins/qdrawEVT/resources/icon_DrawTP.png'))
            self.actions[6].setText(
                self.tr('Buffer drawing tool on the selected layer'))
            self.actions[6].menu().actions()[0].setText(
                self.tr('Polygon buffer drawing tool on the selected layer'))
        self.actions[6].triggered.disconnect()
        self.actions[6].triggered.connect(self.drawBuffer)
        self.actions[6].menu().actions()[0].triggered.disconnect()
        self.actions[6].menu().actions()[0].triggered.connect(self.drawPolygonBuffer)
        self.tool.setAction(self.actions[6])

        # self.tool.select.connect(self.selectBuffer)
        # self.tool.selectionDone.connect(self.draw)
        self.tool.select.connect(self._on_buffer_left_click)
        # ❌ plus de selectionDone → draw() (on ne veut plus de clic droit)

        self.iface.mapCanvas().setMapTool(self.tool)
        self.drawShape = 'polygon'
        self.toolname = 'drawBuffer'
        self.resetSB()

    def _on_buffer_left_click(self) -> None:
        """
        Clic gauche en mode 'tampon sur objet du calque sélectionné':
        - calcule bGeom (union des géométries intersectées)
        - déclenche draw() immédiatement (boîte de dialogue du tampon)
        """
        self._buffer_log("INFO", "[Buffer] clic gauche détecté → selectBuffer(auto_draw=True)")
        self.selectBuffer(auto_draw=True)

    def drawPolygonBuffer(self):
        self.layerevtname = evtpol
        self.bGeom = None
        if self.tool:
            self.tool.reset()
        self.request = 'intersects'
        self.tool = DrawPolygon(self.iface, self.settings.getColor())
        if self.isEVT() == None:
            self.msgisEVTNone()
            return
        elif self.isEVT()[0]:
            self.actions[6].setIcon(
                QIcon(':/plugins/qdrawEVT/resources/icon_DrawTP_EVT.png'))
            self.actions[6].menu().actions()[0].setIcon(
                QIcon(':/plugins/qdrawEVT/resources/icon_DrawT_EVT.png'))
            self.actions[6].setText(
                self.tr('Polygon buffer drawing tool on the selected layer'))
            self.actions[6].menu().actions()[0].setText(
                self.tr('Buffer drawing tool on the selected layer'))
        elif not self.isEVT()[0]:
            self.actions[6].setIcon(
                QIcon(':/plugins/qdrawEVT/resources/icon_DrawTP.png'))
            self.actions[6].menu().actions()[0].setIcon(
                QIcon(':/plugins/qdrawEVT/resources/icon_DrawT.png'))
            self.actions[6].setText(
                self.tr('Polygon buffer drawing tool on the selected layer'))
            self.actions[6].menu().actions()[0].setText(
                self.tr('Buffer drawing tool on the selected layer'))
        self.actions[6].triggered.disconnect()
        self.actions[6].triggered.connect(self.drawPolygonBuffer)
        self.actions[6].menu().actions()[0].triggered.disconnect()
        self.actions[6].menu().actions()[0].triggered.connect(self.drawBuffer)
        self.tool.setAction(self.actions[6])
        self.tool.selectionDone.connect(self.selectBuffer)
        self.iface.mapCanvas().setMapTool(self.tool)
        self.drawShape = 'polygon'
        self.toolname = 'drawBuffer'
        self.resetSB()

    def showSettingsWindow(self):
        self.settings.settingsChanged.connect(self.settingsChangedSlot)
        self.settings.show()

    # triggered when a setting is changed
    def settingsChangedSlot(self):
        if self.tool:
            self.tool.rb.setColor(self.settings.getColor())

    def tr_messages(self):
        return {
            'drawText': self.tr('Left click to place a text.'),
            'drawPoint': self.tr('Left click to place a point.'),
            'drawLine': self.tr('Left click to place points. Right click to confirm.'),
            'drawRect': self.tr('Maintain the left click to draw a rectangle.'),
            'drawCircle': self.tr(
                'Maintain the left click to draw a circle. '
                'Simple left click to give a perimeter.'
            ),
            'drawPolygon': self.tr(
                'Left click to place points. Right click to confirm.'
            ),
            'drawBuffer': self.tr(
                'Select a vector layer in the Layer Tree, then select an entity on the map.'
            ),
        }

    def resetSB(self):
        self.sb.showMessage(self.tr_messages().get(self.toolname, ''))

    def updateSB(self):
        g = self.geomTransform(
            self.tool.rb.asGeometry(),
            self.iface.mapCanvas().mapSettings().destinationCrs(),
            QgsProject.instance().crs()
        )

        if self.toolname == "drawLine":
            length = max(0.0, float(g.length()))
            self.sb.showMessage(
                self.tr("Length: {value} m").format(value=f"{length:.2f}")
            )
        else:
            area = max(0.0, float(g.area()))
            self.sb.showMessage(
                self.tr("Area: {value} m²").format(value=f"{area:.2f}")
            )

        # (cette ligne ne fait rien, tu peux la supprimer)
        # self.iface.mapCanvas().mapSettings().destinationCrs().authid()

    def geomTransform(self, geom, crs_orig, crs_dest):
        g = QgsGeometry(geom)
        crsTransform = QgsCoordinateTransform(
            crs_orig, crs_dest, QgsCoordinateTransformContext())  # which context ?
        g.transform(crsTransform)
        return g

    def _find_first_visible_vector_layer_intersecting(self, geom: QgsGeometry) -> Optional[QgsVectorLayer]:
        """
        Cherche dans TOUTES les couches vectorielles visibles la première couche
        ayant au moins une entité qui intersecte `geom`.
        Retourne la couche trouvée, sinon None.
        """
        root = self.root  # QgsProject.instance().layerTreeRoot()
        for lyr in QgsProject.instance().mapLayers().values():
            try:
                if lyr.type() != QgsMapLayer.VectorLayer:
                    continue
                node = root.findLayer(lyr.id())
                if node is None or not node.isVisible():
                    continue

                vlayer = lyr  # QgsVectorLayer
                # reprojection geom -> CRS couche
                g = self.geomTransform(
                    geom,
                    self.iface.mapCanvas().mapSettings().destinationCrs(),
                    vlayer.crs(),
                )

                # test rapide bbox + premier intersect
                for f in vlayer.getFeatures(QgsFeatureRequest(g.boundingBox())):
                    try:
                        if g.intersects(f.geometry()):
                            return vlayer
                    except Exception:
                        return vlayer
            except Exception:
                continue
        return None

    def selectBuffer(self, auto_draw: bool = False) -> None:
        rb = self.tool.rb

        if isinstance(self.tool, DrawPolygon):
            rbSelect = self.tool.rb
        else:
            rbSelect = self.tool.rbSelect

        # ✅ couche vectorielle visible robuste (OK même si clic item de légende)
        layer = self.get_current_visible_vector_layer()

        # ------------------------------------------------------------------
        # ✅ PATCH: si aucune couche active/visible, on détecte sous le clic
        # ------------------------------------------------------------------
        if layer is None:
            canvas_pos = self._canvas_pos_from_tool()

            if canvas_pos is not None:
                hit = self.identify_topmost_vector_feature(canvas_pos)

                if hit is not None:
                    layer, feat = hit

                    # on force la couche active + sélection
                    try:
                        self.iface.setActiveLayer(layer)
                    except Exception:
                        pass
                    try:
                        layer.selectByIds([feat.id()])
                    except Exception:
                        pass

                    self._buffer_log(
                        "INFO",
                        f"[Buffer] auto-layer='{layer.name()}', fid={feat.id()} (aucune couche active)"
                    )
                else:
                    self._buffer_log(
                        "WARN",
                        "[Buffer] Aucun objet vectoriel détecté sous le clic (aucune couche active)."
                    )
                    return
            else:
                self._buffer_log(
                    "WARN",
                    "[Buffer] Impossible de récupérer la position du clic (QPoint) pour identify()."
                )
                return

        self._buffer_log("INFO", f"[Buffer] layer='{layer.name()}' auto_draw={auto_draw}")

        # Reprojection rubberband -> CRS layer
        g = self.geomTransform(
            rbSelect.asGeometry(),
            self.iface.mapCanvas().mapSettings().destinationCrs(),
            layer.crs(),
        )

        try:
            features = layer.getFeatures(QgsFeatureRequest(g.boundingBox()))
        except Exception as e:
            self._buffer_log("ERROR", f"[Buffer] getFeatures() a échoué: {e}")
            return

        rbGeom = []
        for feature in features:
            geom = feature.geometry()
            try:
                if g.intersects(geom):
                    rbGeom.append(feature.geometry())
            except Exception:
                # fallback: on garde si intersects plante (géométries invalides)
                rbGeom.append(feature.geometry())

        if not rbGeom:
            self._buffer_log("WARN", "[Buffer] Aucun objet intersecté → bGeom reste None")

            if not self._buffer_warned_no_intersection:
                QMessageBox.warning(
                    self.iface.mainWindow(),
                    self.tr("No object selected"),
                    self.tr(
                        "No object was found under the click.\n\n"
                        "Please click directly on an object of the active layer."
                    ),
                )
                self._buffer_warned_no_intersection = True

            self.bGeom = None
            return
        else:
            self._buffer_warned_no_intersection = False

        # Union des géométries intersectées (ton algo original)
        self.bGeom = None
        for geometry in rbGeom:
            try:
                if rbGeom[0].combine(geometry) is not None:
                    if self.bGeom is None:
                        self.bGeom = geometry
                    else:
                        self.bGeom = self.bGeom.combine(geometry)
            except Exception:
                # combine peut échouer sur géom invalides → fallback simple
                if self.bGeom is None:
                    self.bGeom = geometry

        if self.bGeom is None or self.bGeom.isEmpty():
            self._buffer_log("WARN", "[Buffer] Union vide après combine()")
            return

        # Affiche la géom sélectionnée dans la rubberband
        try:
            rb.setToGeometry(self.bGeom, layer)
        except Exception as e:
            self._buffer_log("ERROR", f"[Buffer] rb.setToGeometry() a échoué: {e}")
            return

        self._buffer_log("INFO", "[Buffer] bGeom OK → rubberband mise à jour")

        # ✅ Mode SelectPoint: déclenchement immédiat
        if auto_draw and not isinstance(self.tool, DrawPolygon):
            # draw() s'appuie sur self.layer dans certains chemins → on synchronise
            self.layer = layer
            self._buffer_try_trigger_draw(reason="auto_draw_left_click")
            return

        # ✅ Mode polygonal: on garde ton fonctionnement existant
        if isinstance(self.tool, DrawPolygon):
            self._buffer_log("INFO", "[Buffer] Mode DrawPolygon: draw() via fin de polygone")
            self.draw()

    def _apply_text_labeling(self, layer, field_name: str):
        """
        Labeling texte robuste multi-QGIS (3.22 → 3.42)
        Spécialement corrigé pour QGIS 3.40.x (Bratislava).
        """

        layer_settings = QgsPalLayerSettings()

        # --------------------------------------------------
        # Format texte
        # --------------------------------------------------
        text_format = QgsTextFormat()
        text_format.setFont(self.settings.getFont())
        text_format.setSize(self.settings.getFont().pointSize())
        text_format.setColor(self.settings.getColorFont())

        layer_settings.setFormat(text_format)
        layer_settings.fieldName = field_name

        v = Qgis.QGIS_VERSION_INT

        # --------------------------------------------------
        # Placement (ENUM CORRECT SELON VERSION)
        # --------------------------------------------------
        if v >= 34200:
            # QGIS 3.42+
            layer_settings.placement = QgsPalLayerSettings.PointPlacement

        elif v >= 34000:
            # QGIS 3.40.x (Bratislava) → ENUM DANS Qgis
            layer_settings.placement = Qgis.LabelPlacement.OverPoint

        elif v >= 33000:
            # QGIS 3.30 → 3.38
            layer_settings.placement = QgsPalLayerSettings.LabelPlacement.OverPoint

        else:
            # QGIS ≤ 3.28
            layer_settings.placement = QgsPalLayerSettings.OverPoint

        # --------------------------------------------------
        # NE PAS toucher predefinedPosition en 3.40.x
        # --------------------------------------------------
        # --------------------------------------------------
        # Centrage strict sur le point (évite décalages)
        # --------------------------------------------------
        try:
            layer_settings.dist = 0
            layer_settings.xOffset = 0
            layer_settings.yOffset = 0
        except Exception:
            pass
        # Forcer placement "sur le point" (et pas autour)
        try:
            layer_settings.placement = QgsPalLayerSettings.OverPoint
        except Exception:
            pass

        labeling = QgsVectorLayerSimpleLabeling(layer_settings)
        layer.setLabeling(labeling)
        layer.setLabelsEnabled(True)

    def _geometry_from_click_point(self, rb_geom: QgsGeometry) -> QgsGeometry:
        """
        Retourne une géométrie Point EXACTE du clic si possible.
        Fallback : centroid.
        """
        try:
            if rb_geom and not rb_geom.isEmpty():
                if rb_geom.type() == QgsWkbTypes.PointGeometry:
                    # Cas standard : point
                    return QgsGeometry(rb_geom)
                # Si la rubberband a une géom "bizarre", on force le centroid
        except Exception:
            pass
        return rb_geom.centroid() if rb_geom else rb_geom

    def _clear_tool_rubberbands(self):
        """Supprime immédiatement les rubberbands (points virtuels) du tool."""
        try:
            if self.tool and hasattr(self.tool, "rb") and self.tool.rb:
                self.tool.rb.reset(QgsWkbTypes.PointGeometry)
        except Exception:
            pass
        try:
            if self.tool and hasattr(self.tool, "rbSelect") and self.tool.rbSelect:
                self.tool.rbSelect.reset(QgsWkbTypes.PointGeometry)
        except Exception:
            pass
        try:
            self.iface.mapCanvas().refresh()
        except Exception:
            pass

    def draw(self) -> None:
        """
        Draw handler (QdrawEVT) — version robuste + choix tampon / pas de tampon.

        ✅ Points clés :
        - utilise get_current_visible_vector_layer()
        - choix modale "tampon / pas de tampon / annuler"
        - adapte le titre/label de QDrawLayerDialog selon le choix
        - corrige le bug logique (double elif btn_buffer)
        """
        test_draw = None
        rb = self.tool.rb
        g = rb.asGeometry()

        # Modification qdrawEVT : recentre/zoom
        mc = self.iface.mapCanvas()
        mc.setCenter(g.centroid().asPoint())
        if self.drawShape in ["point", "XYpoint", "text"]:
            mc.zoomScale(5000)
        else:
            mc.zoomToFeatureExtent(g.boundingBox())

        ok = True
        warning = False
        errBuffer_noAtt = False
        errBuffer_Vertices = False

        # ✅ couche visible robuste (au lieu de currentLayer)
        self.layer = self.get_current_visible_vector_layer()

        # >>> PATCH START (Buffer: choix tampon/pas tampon) -------------------------
        if self.toolname == "drawBuffer":
            if self.layer is None:
                self._buffer_log("WARN", "[Buffer] Aucune couche courante visible")
                return

            if self.bGeom is None or self.bGeom.isEmpty():
                warning = True
                errBuffer_noAtt = True
                return

            # --------------------------------------------------
            # ✅ QUESTION MODALE : créer un tampon ou non ?
            # --------------------------------------------------
            msg = QMessageBox(self.iface.mainWindow())
            msg.setWindowTitle(self.tr("Buffer creation"))
            msg.setIcon(pyqt_messagebox_question)
            msg.setText(self.tr("Do you want to create a buffer around the selected object?"))
            msg.setInformativeText(
                self.tr("Choose whether the selection must use the object itself or a buffer around it.")
            )

            btn_buffer = msg.addButton(self.tr("Create buffer"), QMessageBox.AcceptRole)
            btn_nobuffer = msg.addButton(self.tr("No buffer"), QMessageBox.DestructiveRole)
            btn_cancel = msg.addButton(self.tr("Cancel"), QMessageBox.RejectRole)

            msg.setWindowModality(qt_application_modal)
            msg.setWindowFlags(msg.windowFlags() | qt_window_stays_on_top_hint)

            msg.exec()
            clicked = msg.clickedButton()

            # ❌ Annulation
            if clicked == btn_cancel:
                self._buffer_log("INFO", "[Buffer] Annulé par l'utilisateur")
                return

            # ✅ PAS DE TAMPON
            if clicked == btn_nobuffer:
                self._buffer_mode = "polygon"
                self._buffer_log("INFO", "[Buffer] Mode POLYGON (sans tampon)")
                g = QgsGeometry(self.bGeom)

            # ✅ AVEC TAMPON
            elif clicked == btn_buffer:
                self._buffer_mode = "buffer"
                self._buffer_log("INFO", "[Buffer] Mode BUFFER → demande distance")

                perim, ok = QInputDialog.getDouble(
                    self.iface.mainWindow(),
                    self.tr("Buffer size"),
                    self.tr(
                        "Enter the buffer size in meters:\n"
                        "(works only with a metric coordinate reference system)"
                    ),
                    min=0,
                )

                if not ok or perim <= 0:
                    warning = True
                    errBuffer_Vertices = True
                    return

                g = self.bGeom.buffer(perim, 40)

                rb.setToGeometry(
                    g,
                    QgsVectorLayer(
                        "Polygon?crs=" + self.layer.crs().authid(),
                        "",
                        "memory",
                    ),
                )
        # <<< PATCH END ------------------------------------------------------------

        if self.toolname == "drawCopies":
            errBuffer_noAtt = True

        name_draw = ""
        add = False

        self.testGroups()

        if ok and not warning:
            self.index = 0
            self.layers = []

            # --------------------------------------------------
            # Adaptation du texte selon l'outil (circle, buffer, etc.)
            # --------------------------------------------------
            dialog_title = None
            dialog_label = None
            dialog_placeholder = None  # optionnel (si tu l'ajoutes au dialog)

            if self.toolname == "drawCircle":
                dialog_title = self.tr("Circle name")
                dialog_label = self.tr("Enter a name for the circle:")
                dialog_placeholder = self.tr("Circle…")

            elif self.toolname == "drawRect":
                dialog_title = self.tr("Rectangle name")
                dialog_label = self.tr("Enter a name for the rectangle:")
                dialog_placeholder = self.tr("Rectangle…")

            elif self.toolname == "drawPolygon":
                dialog_title = self.tr("Polygon name")
                dialog_label = self.tr("Enter a name for the polygon:")
                dialog_placeholder = self.tr("Polygon…")

            # (garde ton bloc drawBuffer ici, comme tu l'as déjà)
            if self.toolname == "drawBuffer":
                if self._buffer_mode == "buffer":
                    dialog_title = self.tr("Buffer name")
                    dialog_label = self.tr("Enter a name for the buffer:")
                    dialog_placeholder = self.tr("Buffer…")
                elif self._buffer_mode == "polygon":
                    dialog_title = self.tr("Polygon name")
                    dialog_label = self.tr("Enter a name for the polygon:")
                    dialog_placeholder = self.tr("Polygon…")

            # ... puis ton while ...
            name_draw, add, self.index, self.layers, ok = QDrawLayerDialog.getName(
                self.iface,
                gtype=self.drawShape,
                layerevt=self.layerevtname,
                evt=self.evt,
                toolname=self.toolname,
                draw_shape=self.drawShape,
                title_override=dialog_title,
                label_override=dialog_label,
                placeholder_override=dialog_placeholder,  # <-- si tu l'ajoutes (voir plus bas)
            )

            # -------------------------------------------------------------------------
            # Le reste de draw() reste IDENTIQUE (ajout entités, attributs, etc.)
            # -------------------------------------------------------------------------

            if ok and not warning and self.evt:
                if self.drawShape == "text":
                    group = self._get_or_create_drawings_group()

                    test_draw = 1
                    self.layer = QgsVectorLayer(
                        "Point?crs="
                        + self.iface.mapCanvas().mapSettings().destinationCrs().authid()
                        + f"&field={DRAWINGS_FIELD_NAME}:string(255)",
                        name_draw,
                        "memory",
                    )

                    self._apply_drawings_field_alias(self.layer)

                    g = self._geometry_from_click_point(g)
                    self.layer.startEditing()
                    symbols = self.layer.renderer().symbols(QgsRenderContext())
                    symbols[0].setColor(self.settings.getColor())

                    self.feature = QgsFeature(self.layer.fields())
                    self.feature.setGeometry(g)
                    self.feature.setAttributes([name_draw])
                    self.layer.dataProvider().addFeatures([self.feature])
                    self.featureid = self.feature.id()

                    self._clear_tool_rubberbands()

                    symbols[0].setSize(0)

                    self._apply_text_labeling(layer=self.layer, field_name=DRAWINGS_FIELD_NAME)

                    if not add:
                        self.project.addMapLayer(self.layer, False)
                        group.insertLayer(0, self.layer)

                    self.iface.layerTreeView().refreshLayerSymbology(self.layer.id())
                    self.iface.mapCanvas().refresh()

                else:
                    self.layer = self.project.mapLayersByName(self.layerevtname)[0]
                    self.iface.setActiveLayer(self.layer)
                    self.layer.setReadOnly(False)
                    self.layer.startEditing()

                    self.features = QgsFeature()
                    self.features.setGeometry(g)
                    self.features.setFields(self.layer.fields())
                    self.features.setAttribute(form_wgt_utilisatr, os.environ.get("USERNAME"))

                    if self.drawShape in ["point", "XYpoint"]:
                        crsSrc = QgsProject.instance().crs()
                        crsDest = QgsCoordinateReferenceSystem(4326)
                        xform1 = QgsCoordinateTransform(crsSrc, crsDest, self.project)
                        geom = g.centroid().asPoint()
                        pt1 = xform1.transform(geom)
                        x1, y1 = pt1.x(), pt1.y()
                        self.features.setAttribute(form_wgt_x_gps, round(x1, 6))
                        self.features.setAttribute(form_wgt_y_gps, round(y1, 6))

                    elif self.drawShape == "line":
                        self.features.setAttribute(form_wgt_longueur, int(round(g.length(), 0)))

                    elif self.drawShape == "polygon":
                        self.features.setAttribute(form_wgt_surface, int(round(g.area(), -2)))

                    form = self.iface.getFeatureForm(self.layer, self.features)
                    form.setMode(QgsAttributeEditorContext.AddFeatureMode)

                    if form_wgt_libelle in lst_form_wgt:
                        libelle = form.findChild(QLineEdit, form_wgt_libelle)
                        libelle.setText(name_draw)

                    if form_wgt_source in lst_form_wgt:
                        source = form.findChild(QLineEdit, form_wgt_source)
                        source.setFocus()

                    test_draw = form.exec()

                    if test_draw == 1:
                        self.QgisV3LayerRandomColor(self.layer, form_wgt_libelle)
                        self.layer.commitChanges()
                        self.layer.setReadOnly(True)
                    else:
                        self.layer.rollBack()
                        self.layer.setReadOnly(True)

            if ok and not warning and not self.evt:
                group = self._get_or_create_drawings_group()

                test_draw = 1
                if add:
                    self.layer = self.layers[self.index]
                    if self.drawShape in ["point", "XYpoint", "text"]:
                        g = self._geometry_from_click_point(g)

                else:
                    if self.drawShape in ["point", "text"]:
                        self.layer = QgsVectorLayer(
                            "Point?crs="
                            + self.iface.mapCanvas().mapSettings().destinationCrs().authid()
                            + f"&field={DRAWINGS_FIELD_NAME}:string(255)",
                            name_draw,
                            "memory",
                        )
                        g = self._geometry_from_click_point(g)

                    elif self.drawShape == "XYpoint":
                        self.layer = QgsVectorLayer(
                            "Point?crs="
                            + self.XYcrs.authid()
                            + f"&field={DRAWINGS_FIELD_NAME}:string(255)",
                            name_draw,
                            "memory",
                        )
                        g = self._geometry_from_click_point(g)

                    elif self.drawShape == "line":
                        self.layer = QgsVectorLayer(
                            "LineString?crs="
                            + self.iface.mapCanvas().mapSettings().destinationCrs().authid()
                            + f"&field={DRAWINGS_FIELD_NAME}:string(255)",
                            name_draw,
                            "memory",
                        )

                    else:
                        self.layer = QgsVectorLayer(
                            "Polygon?crs="
                            + self.iface.mapCanvas().mapSettings().destinationCrs().authid()
                            + f"&field={DRAWINGS_FIELD_NAME}:string(255)",
                            name_draw,
                            "memory",
                        )

                self._apply_drawings_field_alias(self.layer)

                self.layer.startEditing()
                symbols = self.layer.renderer().symbols(QgsRenderContext())
                symbols[0].setColor(self.settings.getColor())

                self.feature = QgsFeature(self.layer.fields())
                self.feature.setGeometry(g)
                self.feature.setAttributes([name_draw])
                self.layer.dataProvider().addFeatures([self.feature])
                self.featureid = self.feature.id()

                self._clear_tool_rubberbands()

                if self.drawShape == "text":
                    symbols[0].setSize(0)
                    self._apply_text_labeling(layer=self.layer, field_name=DRAWINGS_FIELD_NAME)
                    self.layer.renderer().symbol().symbolLayer(0).setShape(
                        QgsSimpleMarkerSymbolLayerBase.Star
                    )
                    self.layer.triggerRepaint()
                    self.iface.layerTreeView().refreshLayerSymbology(self.layer.id())
                    self.iface.mapCanvas().refresh()

                if not add:
                    self.project.addMapLayer(self.layer, False)
                    group.insertLayer(0, self.layer)

                self.iface.layerTreeView().refreshLayerSymbology(self.layer.id())
                self.iface.mapCanvas().refresh()

            # -------------------------------------------------------------------------
            # ... (le reste de ton draw() continue ensuite sans changement)
            # -------------------------------------------------------------------------

            # (IMPORTANT : je n’ai pas recopié la fin complète ici si elle est identique,
            # mais tu peux coller ce draw() en remplaçant ton bloc initial + garder ta fin.)

        # Modification pour ajout de la sélection des couches intersectant
        # une ligne ou un polygone, avec cochage automatique puis décochage du groupe sélectionné
        if self.drawShape not in ['point', 'XYpoint', 'text'] and test_draw == 1:
            # Définition du choix d'une sélection en intersection
            box = QMessageBox()
            box.setWindowTitle(self.tr('Choosing a selection'))
            box.setIcon(pyqt_messagebox_information)
            box.setText(self.tr("{name} drawn. Make a selection within its extent?").format(name=name_draw))
            box.setStandardButtons(yes_button | no_button)
            box.setDefaultButton(yes_button)
            buttonYes = box.button(yes_button)
            buttonNo = box.button(no_button)
            box.setWindowModality(qt_windows_modality)
            box.setWindowFlags(qt_window_stays_on_top_hint)
            box.exec()

            self.groupes_coches = []
            test_coche = self.verifCocheGroupes()

            if box.clickedButton() == buttonYes:

                # ------------------------------------------------------------------
                # PATCH : Aucun groupe existant → création d'un groupe + choix des couches
                # ------------------------------------------------------------------

                root = QgsProject.instance().layerTreeRoot()

                # Construire la liste des groupes exclus
                excluded_groups = self.group_no_select + self.group_evt + self.group_drawing

                # Construire la liste des couches exclues (pour ne pas les proposer dans la boîte)
                excluded_layer_ids = set()
                for grp in excluded_groups:
                    for child in grp.children():
                        if isinstance(child, QgsLayerTreeLayer):
                            excluded_layer_ids.add(child.layerId())

                # Groupes existants (hors groupes exclus)
                groupes_existants = [
                    g for g in root.children()
                    if isinstance(g, QgsLayerTreeGroup) and g not in excluded_groups
                ]

                if len(groupes_existants) == 0:
                    # Aucun groupe exploitable → proposer création d’un nouveau
                    dlg = QDialog()
                    dlg.setWindowTitle(self.tr("No group found"))
                    layout = QVBoxLayout(dlg)

                    label = QLabel(self.tr(
                        "Aucun groupe n’existe dans le projet.\n"
                        "Créez un groupe et choisissez les couches à y mettre :"
                    ))
                    layout.addWidget(label)

                    # Nom du groupe
                    name_edit = QLineEdit()
                    name_edit.setPlaceholderText(self.tr("New group name"))
                    layout.addWidget(name_edit)

                    # Couches disponibles (hors groupes exclus)
                    layers_box = QGroupBox(self.tr("Available layers"))
                    vbox_layers = QVBoxLayout(layers_box)

                    checkboxes = {}

                    for layer in QgsProject.instance().mapLayers().values():
                        # On exclut :
                        # 1) les couches non vectorielles
                        # 2) les couches contenues dans les groupes exclus
                        if (
                            layer.type() == QgsMapLayer.VectorLayer
                            and layer.id() not in excluded_layer_ids
                        ):
                            cb = QCheckBox(layer.name())
                            vbox_layers.addWidget(cb)
                            checkboxes[cb] = layer

                    layout.addWidget(layers_box)

                    # Boutons OK / Annuler
                    buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
                    layout.addWidget(buttons)

                    def accept():
                        if not name_edit.text().strip():
                            QMessageBox.warning(
                                dlg, self.tr("Error"),
                                self.tr("You must name the group.")
                            )
                            return
                        dlg.accept()

                    buttons.accepted.connect(accept)
                    buttons.rejected.connect(dlg.reject)

                    res = dlg.exec()

                    # L’utilisateur annule
                    if res != QDialog.Accepted:
                        if self.isEVT() == None:
                            self.msgisEVTNone()
                            return
                        elif self.isEVT()[0]:
                            self.layer.rollBack()
                        else:
                            self.project.removeMapLayer(self.layer.id())
                        self.msgForgottenTick()
                        return

                    # Création du groupe
                    new_group = QgsLayerTreeGroup(name_edit.text().strip())
                    root.insertChildNode(0, new_group)

                    # ----------------------------------------------------------
                    # Nouveau déplacement des couches cochées
                    # ----------------------------------------------------------
                    layers_to_move = [
                        layer
                        for cb, layer in checkboxes.items()
                        if cb.isChecked()
                    ]

                    # Appel de l’algo robuste
                    self.moveLayersIntoGroup(layers_to_move, new_group)
                    # ----------------------------------------------------------

                    new_group.setItemVisibilityChecked(True)

                    self.iface.messageBar().pushMessage(
                        self.tr("Group created"),
                        self.tr("The {new_group_name} group has been created and the selected layers have been moved.").format(new_group_name=new_group.name())
                        ,
                        Qgis.Success,
                        3
                    )

                    self.groupe_selectionne = new_group
                    self.groupes_coches = [new_group]
                    test_coche = True

                else:
                    # Il existe au moins un groupe exploitable
                    # 1) Si aucun groupe n’est coché, on propose le choix des groupes non cochés
                    if not test_coche:
                        groupe_selectionne = self.askUncheckedGroup()
                        if groupe_selectionne:
                            # L’utilisateur a choisi un groupe : on le mémorise
                            self.groupe_selectionne = groupe_selectionne
                            test_coche = True
                            self.groupes_coches = [groupe_selectionne]
                        else:
                            # L’utilisateur n’a rien choisi → annulation comme avant
                            if self.isEVT() == None:
                                self.msgisEVTNone()
                                return
                            elif self.isEVT()[0]:
                                self.layer.rollBack()
                            else:
                                self.project.removeMapLayer(self.layer.id())
                            self.msgForgottenTick()
                            return
                    # 2) À partir d’ici, on est sûr d’avoir un groupe exploitable coché
                    warning = False
                    ok = True
                    active = False
                    errBuffer_noAtt = False
                    errBuffer_Vertices = False
                    buffer_geom = None
                    buffer_geom_crs = None

                    # Masquer les groupes non sélectionnables
                    for group in self.root.children():
                        if group in self.group_no_select:
                            group.setItemVisibilityChecked(False)
                        if group in self.group_drawing:
                            group.setItemVisibilityCheckedRecursive(False)

                    # Vérifie qu’au moins une couche visible existe
                    for layer in QgsProject().instance().mapLayers().values():
                        try:
                            if self.root.findLayer(layer.id()).isVisible():
                                warning = False
                                active = True
                                break
                        except:
                            pass

                    # Création d’un buffer sur la couche courante (si demandé)
                    if self.request == 'buffer':

                        layer = self.get_current_visible_vector_layer()
                        # layer = self.iface.layerTreeView().currentLayer()

                        if layer is not None and layer.type() == QgsMapLayer.VectorLayer and QgsProject.instance().layerTreeRoot().findLayer(
                                layer.id()).isVisible():

                            g = self.geomTransform(rb.asGeometry(),
                                                   self.iface.mapCanvas().mapSettings().destinationCrs(), layer.crs())
                            features = layer.getFeatures(QgsFeatureRequest(g.boundingBox()))
                            style = layer.renderer()
                            rbGeom = []
                            for feature in features:
                                geom = feature.geometry()
                                if g.intersects(geom):
                                    rbGeom.append(QgsGeometry(feature.geometry()))
                            if len(rbGeom) > 0:
                                union_geoms = rbGeom[0]
                                for geometry in rbGeom:
                                    if union_geoms.combine(geometry) is not None:
                                        union_geoms = union_geoms.combine(geometry)

                                rb.setToGeometry(union_geoms, layer)
                                perim, ok = QInputDialog.getDouble(
                                    self.iface.mainWindow(),
                                    self.tr('Perimeter'),
                                    self.tr('Give a perimeter in m:\n(works only with metric crs)'),
                                    min=0)
                                buffer_geom_crs = layer.crs()
                                buffer_geom = union_geoms.buffer(perim, 40)
                                rb.setToGeometry(buffer_geom,
                                                 QgsVectorLayer("Polygon?crs=" + layer.crs().authid(), "", "memory"))
                                rb.setRenderer(style)
                                if buffer_geom.length == 0:
                                    warning = True
                                    errBuffer_Vertices = True
                            else:
                                warning = True
                                errBuffer_noAtt = True
                        else:
                            warning = True

                    # --- Sélection des entités intersectantes ---
                    if len(QgsProject().instance().mapLayers().values()) > 0 and not warning and ok:
                        self.loadingWindow.show()
                        self.loadingWindow.activateWindow()
                        self.loadingWindow.showNormal()

                        for layer in QgsProject().instance().mapLayers().values():
                            try:
                                if layer.type() == QgsMapLayer.VectorLayer and QgsProject.instance().layerTreeRoot().findLayer(
                                        layer.id()).isVisible():
                                    if self.request == 'buffer' and self.iface.layerTreeView().currentLayer() == layer:
                                        layer.selectByIds([])
                                        continue
                                    self.loadingWindow.reset()
                                    self.loadingWindow.setWindowTitle(self.tr('Selecting...'))
                                    self.loadingWindow.setLabelText(layer.name())

                                    # rubberband reprojection
                                    if self.request == 'buffer':
                                        if buffer_geom_crs.authid() != layer.crs().authid():
                                            g = self.geomTransform(buffer_geom, buffer_geom_crs, layer.crs())
                                        else:
                                            g = self.geomTransform(buffer_geom, buffer_geom_crs, layer.crs())
                                    else:
                                        g = self.geomTransform(
                                            rb.asGeometry(),
                                            self.iface.mapCanvas().mapSettings().destinationCrs(),
                                            layer.crs()
                                        )

                                    feat_id = []
                                    features = layer.getFeatures(QgsFeatureRequest(g.boundingBox()))
                                    count = layer.getFeatures(QgsFeatureRequest(g.boundingBox()))

                                    nbfeatures = 0
                                    for feature in count:
                                        nbfeatures += 1

                                    index = 0
                                    for feature in features:
                                        geom = feature.geometry()
                                        try:
                                            if g.intersects(geom):
                                                feat_id.append(feature.id())
                                        except:
                                            feat_id.append(feature.id())
                                        index += 1
                                        self.loadingWindow.setValue(int((float(index) / nbfeatures) * 100))
                                        if self.loadingWindow.wasCanceled():
                                            self.loadingWindow.reset()
                                            break
                                        QApplication.processEvents()
                                    layer.selectByIds(feat_id)
                            except:
                                pass

                        self.loadingWindow.close()
                        self.showAttributesTable(name_draw)

                        # Une fois la sélection terminée → décochage du groupe
                        if self.groupe_selectionne:
                            if self.groupExists(self.groupe_selectionne):
                                self.groupe_selectionne.setItemVisibilityChecked(False)
                            else:
                                self.groupe_selectionne = None
                            
                            QApplication.processEvents()
                            self.iface.messageBar().pushMessage(
                                self.tr("End of selection"),
                                self.tr("The {sel_group} group has been unchecked.").format(sel_group=self.groupe_selectionne.name()),
                                Qgis.Info,
                                2
                            )
                            self._groupe_msg_affiche = False
                            self._groupe_msg_nul = False
                    else:
                        # Avertissements en cas d’erreur
                        if errBuffer_noAtt:
                            self.iface.messageBar().pushWarning(
                                self.tr('Warning'),
                                self.tr("You didn't click on a layer's attribute !"))
                        elif errBuffer_Vertices:
                            self.iface.messageBar().pushWarning(
                                self.tr('Warning'),
                                self.tr("You must give a non-null value for a point's or line's perimeter!"))
                        else:
                            self.iface.messageBar().pushWarning(
                                self.tr('Warning'),
                                self.tr('There is no selected layer, or it is not vector nor visible!'))
                        for layer in self.isEVT()[1]:
                            layer.rollback()

            elif box.clickedButton() == buttonNo:
                pass

        else:
            pass

        if self.isEVT() == None:
            self.msgisEVTNone()
            return
        elif self.isEVT()[0] and ok:
            if self.layer is None:
                self.msgNoSelect()
                return
            self.QgisV3LayerRandomColor(self.layer, form_wgt_libelle)
            self.layer.setReadOnly(True)
            try:
                for couche in self.groupes_coches:
                    couche.setItemVisibilityChecked(False)
                for group in self.group_no_select:
                    group.setItemVisibilityChecked(True)
                for group in self.group_drawing:
                    group.setItemVisibilityCheckedRecursive(True)
            except:
                pass
        elif not self.isEVT()[0]:
            for layer in self.isEVT()[1]:
                self.QgisV3LayerRandomColor(layer, DRAWINGS_FIELD_NAME)
            for group in self.group_drawing:
                group.setItemVisibilityCheckedRecursive(True)
        if not self.groupes_coches:
            return
        try:
            self.layer.commitChanges()
        except:
            pass
        self.tool.reset()
        self.resetSB()
        self.bGeom = None

    # --------------------------------------------------------------
    # Fonction pour vérifier si un groupe existe
    # --------------------------------------------------------------
    def groupExists(self, group):
        """Vérifie si un QgsLayerTreeGroup existe encore dans l'arbre."""
        if group is None:
            return False
        try:
            # Accès à une propriété simple : si l'objet est deleted → RuntimeError
            _ = group.name()
            return True
        except RuntimeError:
            return False

    def _now_ms(self) -> int:
        """Timestamp ms (Qt) stable multi-plateformes."""
        return int(QDateTime.currentMSecsSinceEpoch())

    def _buffer_log(self, level: str, msg: str) -> None:
        """
        Log un message de debug buffer (via ton logger + fallback QgsMessageLog).
        level: 'INFO' | 'WARN' | 'ERROR'
        """
        try:
            if level == "WARN":
                log.warning(msg)
            elif level == "ERROR":
                log.error(msg)
            else:
                log.info(msg)
        except Exception:
            pass

        # Optionnel: aussi dans le log QGIS (pratique en prod)
        try:
            qlvl = Qgis.Info
            if level == "WARN":
                qlvl = Qgis.Warning
            elif level == "ERROR":
                qlvl = Qgis.Critical
            QgsMessageLog.logMessage(msg, "QdrawEVT", qlvl)
        except Exception:
            pass

    def _buffer_try_trigger_draw(self, reason: str) -> None:
        """
        Déclenche draw() de manière robuste :
        - debounce (anti double signal)
        - lock (anti ré-entrance)
        - exécution différée (QTimer.singleShot) pour laisser QGIS finir l'UI event.
        """
        now = self._now_ms()

        if self._buffer_draw_lock:
            self._buffer_log("INFO", f"[Buffer] draw() ignoré (lock actif) — reason={reason}")
            return

        if (now - self._buffer_draw_last_ms) < self._buffer_draw_debounce_ms:
            self._buffer_log(
                "INFO",
                f"[Buffer] draw() ignoré (debounce {now - self._buffer_draw_last_ms}ms) — reason={reason}",
            )
            return

        if self.bGeom is None or self.bGeom.isEmpty():
            self._buffer_log("WARN", f"[Buffer] draw() non déclenché (bGeom vide) — reason={reason}")
            return

        self._buffer_draw_lock = True
        self._buffer_draw_last_ms = now
        self._buffer_log("INFO", f"[Buffer] draw() planifié — reason={reason}")

        def _do():
            try:
                # Sécurité : si le tool a changé entre-temps, on annule
                if self.toolname != "drawBuffer":
                    self._buffer_log("WARN", "[Buffer] draw() annulé (toolname a changé)")
                    return

                self._buffer_log("INFO", "[Buffer] draw() EXEC")
                self.draw()
            except Exception as e:
                self._buffer_log("ERROR", f"[Buffer] Exception draw(): {e}")
            finally:
                # libère le lock légèrement plus tard pour éviter la rafale de signaux
                QTimer.singleShot(self._buffer_draw_debounce_ms, self._buffer_release_draw_lock)

        QTimer.singleShot(0, _do)

    def _buffer_release_draw_lock(self) -> None:
        self._buffer_draw_lock = False
        self._buffer_log("INFO", "[Buffer] lock relâché")

    # -------------------------------------------------------------------------------------
    # Fonction pour sélectionner la couche en cliquant sur un item de l'analyse thématique
    # -------------------------------------------------------------------------------------

    def get_current_visible_vector_layer(self) -> Optional[QgsVectorLayer]:
        """
        Return the currently selected *visible* vector layer in the Layer Tree.

        Works even when the user clicked a legend/thematic item (categorized rule, etc.),
        where `layerTreeView().currentNode()` can be None or not a layer node.

        Returns:
            QgsVectorLayer if the current layer is a visible vector layer, else None.
        """
        view = self.iface.layerTreeView()
        layer = view.currentLayer()

        if layer is None or layer.type() != QgsMapLayer.VectorLayer:
            return None

        vlayer = cast(QgsVectorLayer, layer)

        node = self._layer_tree_node_for_layer(vlayer)
        if node is None:
            return None

        if self._is_layer_tree_node_visible(node):
            return vlayer

        return None

    def _layer_tree_node_for_layer(self, layer: QgsMapLayer) -> Optional[QgsLayerTreeLayer]:
        """
        Return the real Layer Tree node for a map layer, or None if not found.
        """
        root = QgsProject.instance().layerTreeRoot()
        try:
            node = root.findLayer(layer.id())
        except Exception:
            return None

        if node is None:
            return None

        # In practice, findLayer returns QgsLayerTreeLayer; we keep it safe/typed.
        try:
            return cast(QgsLayerTreeLayer, node)
        except Exception:
            return None

    def _is_layer_tree_node_visible(self, node: QgsLayerTreeLayer) -> bool:
        """
        Robust visibility test across QGIS 3.x.

        Prefer `isVisible()` (effective visibility), fallback to `itemVisibilityChecked()`.
        """
        try:
            return bool(node.isVisible())
        except Exception:
            pass

        try:
            return bool(node.itemVisibilityChecked())
        except Exception:
            return False

    # --------------------------------------------------------------
    # Fonction pour afficher les groupes non cochés (sélection unique)
    # --------------------------------------------------------------
    def askUncheckedGroup(self):
        # Mise à jour des groupes interdits / EVT / Drawings
        self.testGroups()
        unchecked_groups = []
        # --- Récupération des groupes non cochés dans l’arbre ---
        root = QgsProject.instance().layerTreeRoot()
        for child in root.children():
            if isinstance(child, QgsLayerTreeGroup):
                if (
                        not child.isVisible()
                        and normalize(child.name()) not in lst_group_no_select
                        and normalize(child.name()) not in lst_group_evt
                        and normalize(child.name()) not in lst_group_drawing
                ):
                    unchecked_groups.append(child)
        if not unchecked_groups:
            QMessageBox.information(
                self.iface.mainWindow(),
                self.tr("No groups"),
                self.tr("All groups are already checked.")
            )
            return None

        # --- Création de la boîte de dialogue ---
        self.dlg = QDialog(self.iface.mainWindow())
        dlg = self.dlg
        title = self.tr("Choose a group for selection")
        dlg.setWindowTitle(title)

        # Supprime les boutons système (fermer, réduire, agrandir)
        dlg.setWindowFlags(
            Qt.Window |
            Qt.CustomizeWindowHint |
            Qt.WindowTitleHint |
            Qt.WindowStaysOnTopHint
        )
        dlg.setWindowModality(Qt.ApplicationModal)

        dlg_layout = QVBoxLayout(dlg)
        label = QLabel(self.tr("Choose a group from the list:"))
        dlg_layout.addWidget(label)

        # Groupe exclusif de boutons radio
        button_group = QButtonGroup(dlg)
        button_group.setExclusive(True)

        # Fonction appelée dès qu'un groupe est choisi
        def on_selection(group):
            self.groupe_selectionne = group
            group.setItemVisibilityChecked(True)
            dlg.done(QDialog.Accepted)

        # --- Calcul de la largeur maximale parmi les noms de groupes ---
        font_metrics = dlg.fontMetrics()
        max_group_width = 0
        for group in unchecked_groups:
            text_width = font_metrics.boundingRect(group.name()).width()
            if text_width > max_group_width:
                max_group_width = text_width

        # Ajout des boutons radio
        for group in unchecked_groups:
            rb = QRadioButton(group.name())
            dlg_layout.addWidget(rb)
            button_group.addButton(rb)
            rb.toggled.connect(lambda checked, g=group: on_selection(g) if checked else None)

        # --- Ajustement automatique de la largeur ---
        title_width = font_metrics.boundingRect(title).width()
        dlg.adjustSize()
        dlg_width = max(title_width, max_group_width) + 120  # 120 px marge visuelle
        dlg.setFixedWidth(dlg_width)
        dlg.setSizeGripEnabled(False)

        result = dlg.exec_()
        if result == QDialog.Accepted:
            return self.groupe_selectionne
        return None

    def QgisV3LayerRandomColor(self, layer, fieldName):
        """
        QGIS3 : Classification automatique du moteur de rendu pyqgis catégorisé sur le champ 'fieldname' de la table attributaire 'layer'
        copyright           : (C) 2019 by Sylvain T., CNRS FR 3020
        Licence             : Creative Commons CC-BY-SA V4+
        Sources             : https://gis.stackexchange.com/questions/226642/automatic-pyqgis-categorized-renderer-classification
        :param layer: QgsVectorLayer
        :param fieldName: str
        :return:
        """
        # provide file name index and field's unique values
        fni = layer.dataProvider().fields().indexFromName(fieldName)
        unique_values = layer.uniqueValues(fni)

        # fill categories
        categories = []
        # print('layer.geometryType() : ' + str(layer.geometryType()))
        # layer.geometryType() : point = 0, ligne = 1, polygone = 2
        geom_type = layer.geometryType()
        for unique_value in unique_values:
            # initialize the default symbol for this geometry type
            symbol = QgsSymbol.defaultSymbol(layer.geometryType())
            if geom_type == 0:
                # Réglage de la taille des points
                symbol.setSize(4)
            elif geom_type == 1:
                # Réglage de l'épaisseur des lignes
                symbol.setWidth(2)
            symbol.setOpacity(0.4)

            # Couleurs aléatoires, le dernier paramètre règle le niveau de transparence (de 0 à 100%) Impose
            # l'importation de random layer_style = %1 layer_style['color'] = '%d, %d, %d' % (random.randrange(0,
            # 256), random.randrange(0, 256), random.randrange(0, 256)) layer_style['outline'] = '#000000'
            # symbol_layer = QgsSimpleFillSymbolLayer.create(layer_style) # replace default symbol layer with the
            # configured one if symbol_layer is not None: symbol.changeSymbolLayer(0, symbol_layer)

            # Couleur rouge sur tous les objets
            symbol.setColor(QColor(255, 0, 0))

            # create renderer object
            category = QgsRendererCategory(unique_value, symbol, str(unique_value))
            # entry for the list of category items
            categories.append(category)
        # Création du rendu de l'objet (on ne fais rien si c'est un objet texte)
        if self.drawShape != 'text':
            renderer = QgsCategorizedSymbolRenderer(fieldName, categories)
            # assign the created renderer to the layer
            if renderer is not None:
                layer.setRenderer(renderer)
        layer.triggerRepaint()

    def verifCocheGroupes(self):
        # Toujours réinitialiser
        self.groupes_coches = []
        for group in self.root.children():
            # Exclure tous les groupes non sélectionnables
            if (
                    group not in self.group_no_select
                    and group not in self.group_evt
                    and group not in self.group_drawing
            ):
                if isinstance(group, QgsLayerTreeGroup):
                    node = self.root.findGroup(group.name())
                else:
                    lyr = self.project.mapLayersByName(group.name())[0]
                    node = self.root.findLayer(lyr.id())
                if node and node.isVisible():
                    self.groupes_coches.append(group)
        return bool(self.groupes_coches)

    def moveLayersIntoGroup(self, layers_to_move, target_group):
        """
        Déplace proprement une liste de QgsVectorLayer dans un groupe donné.
        On utilise l'API layer tree standard : root.findLayer(), parent.removeChildNode().
        """
        root = self.root

        for lyr in layers_to_move:
            if lyr is None:
                continue

            # Node correspondant à la couche dans l'arbre
            node = root.findLayer(lyr.id())
            if node is None:
                continue

            parent = node.parent()
            if parent is None:
                # cas très rare, mais on évite de crasher
                continue

            # On clone le node, on insère le clone dans le groupe,
            # puis on supprime l'ancien du parent.
            cloned = node.clone()
            target_group.insertChildNode(0, cloned)
            parent.removeChildNode(node)

    def findLayerNodeAnywhere(self, root, layer_id):
        """Recherche un node de couche dans tout l'arbre."""
        stack = [root]
        while stack:
            node = stack.pop()
            if isinstance(node, QgsLayerTreeLayer) and node.layerId() == layer_id:
                return node
            for child in node.children():
                stack.append(child)
        return None

    def msgisEVTNone(self):
        msgBox = QMessageBox()
        msgBox.setIcon(pyqt_messagebox_critical)
        msgBox.setWindowTitle(self.tr("WARNING!"))
        msgBox.setText(self.tr("Command not available: empty project."))
        msgBox.setStandardButtons(yes_button)
        msgBox.setDefaultButton(yes_button)
        buttonYes = msgBox.button(yes_button)
        buttonYes.setText(self.tr('Start again'))
        # Pour que la fenêtre reste au premier plan de toutes les applications en cours
        msgBox.setWindowModality(qt_windows_modality)
        msgBox.setWindowFlags(qt_window_stays_on_top_hint)
        returnValue = msgBox.exec()
        if returnValue == buttonYes:
            # print('OK clicked')
            pass

    def msgForgottenTick(self):
        msgBox = QMessageBox()
        msgBox.setIcon(pyqt_messagebox_critical)
        msgBox.setWindowTitle(self.tr("WARNING!"))
        msgBox.setText(self.tr("No layer or group is selected. Start again."))
        msgBox.setStandardButtons(yes_button)
        msgBox.setDefaultButton(yes_button)
        buttonYes = msgBox.button(yes_button)
        buttonYes.setText(self.tr('Start again'))
        # Pour que la fenêtre reste au premier plan de toutes les applications en cours
        msgBox.setWindowModality(qt_windows_modality)
        msgBox.setWindowFlags(qt_window_stays_on_top_hint)
        returnValue = msgBox.exec()
        if returnValue == buttonYes:
            # print('OK clicked')
            pass

    def msgForgottenPolygon(self):
        msgBox = QMessageBox()
        msgBox.setIcon(pyqt_messagebox_warning)
        msgBox.setWindowTitle(self.tr("WARNING!"))
        # msgBox.setText(self.tr("Selection not possible. the checked layer is not a polygon."))
        msgBox.setText(self.tr("Selection not possible. The checked layer is not a polygon."))
        msgBox.setStandardButtons(yes_button)
        msgBox.setDefaultButton(yes_button)
        buttonYes = msgBox.button(yes_button)
        buttonYes.setText(self.tr('Start again'))
        # Pour que la fenêtre reste au premier plan de toutes les applications en cours
        msgBox.setWindowModality(qt_windows_modality)
        msgBox.setWindowFlags(qt_window_stays_on_top_hint)
        returnValue = msgBox.exec()
        if returnValue == buttonYes:
            # print('OK clicked')
            pass

    def msgNoSelect(self):
        msgBox = QMessageBox()
        msgBox.setIcon(pyqt_messagebox_warning)
        msgBox.setWindowTitle(self.tr("WARNING!"))
        # msgBox.setText(self.tr("Selection not possible. No layers are selected in the Layer Manager."))
        msgBox.setText(self.tr("Selection not possible. No layer is selected in the Layer Manager."))
        msgBox.setStandardButtons(yes_button)
        msgBox.setDefaultButton(yes_button)
        buttonYes = msgBox.button(yes_button)
        buttonYes.setText(self.tr('Start again'))
        # Pour que la fenêtre reste au premier plan de toutes les applications en cours
        msgBox.setWindowModality(qt_windows_modality)
        msgBox.setWindowFlags(qt_window_stays_on_top_hint)
        returnValue = msgBox.exec()
        if returnValue == buttonYes:
            # print('OK clicked')
            pass

    def afficher_aide(self):
        """Affiche le fichier d'aide (HTML / Markdown / TXT) dans une boîte de dialogue."""

        # --- 1) Détermination de la langue ---
        lang_full = QLocale.system().name().lower()  # ex: 'fr_fr', 'en_gb'
        lang = lang_full.split("_")[0]  # 'fr', 'en', 'de', ...

        plugin_dir = Path(__file__).resolve().parent
        help_dir = plugin_dir / "help"

        # --- 2) Ordre de recherche des fichiers d'aide ---
        candidats = [
            help_dir / f"readme_{lang}.html",
            help_dir / f"readme_{lang}.htm",
            help_dir / f"readme_{lang}.md",
            help_dir / f"readme_{lang}.txt",
            help_dir / "readme_fr.html",
            help_dir / "readme_fr.htm",
            help_dir / "readme_fr.md",
            help_dir / "readme_fr.txt",
        ]
        fichier = None
        for cand in candidats:
            if cand.is_file():
                fichier = cand
                break
        # Si aucun fichier trouvé : message simple
        if fichier is None:
            QMessageBox.warning(
                self.iface.mainWindow(),
                self.tr("QdrawEVT Help - Gestion_crise"),
                self.tr("No help file was found in the 'help' folder."),
            )
            return
        # --- 3) Lecture du contenu ---
        try:
            contenu = fichier.read_text(encoding="utf-8")
        except Exception as e:
            QMessageBox.critical(
                self.iface.mainWindow(),
                self.tr("QdrawEVT Help - Gestion_crise"),
                self.tr("Unable to read the help file: {error}").format(error=str(e)),
            )
            return
        suffix = fichier.suffix.lower()
        # --- 4) Préparation HTML / Markdown / Texte brut ---
        html = ""
        if suffix in (".html", ".htm"):
            # On suppose que le contenu est déjà du HTML
            html = contenu
        elif suffix == ".md":
            # Conversion très simple Markdown -> HTML (sans dépendance externe)
            # - # Titre -> <h1>, ## -> <h2>, etc.
            # - lignes normales -> <p>
            lines = contenu.splitlines()
            html_lines = []
            for line in lines:
                stripped = line.strip()
                if stripped.startswith("###### "):
                    html_lines.append("<h6>{}</h6>".format(stripped[7:]))
                elif stripped.startswith("##### "):
                    html_lines.append("<h5>{}</h5>".format(stripped[6:]))
                elif stripped.startswith("#### "):
                    html_lines.append("<h4>{}</h4>".format(stripped[5:]))
                elif stripped.startswith("### "):
                    html_lines.append("<h3>{}</h3>".format(stripped[4:]))
                elif stripped.startswith("## "):
                    html_lines.append("<h2>{}</h2>".format(stripped[3:]))
                elif stripped.startswith("# "):
                    html_lines.append("<h1>{}</h1>".format(stripped[2:]))
                elif stripped == "":
                    html_lines.append("<br>")
                else:
                    # on échappe juste les < et > pour éviter de casser le HTML
                    safe = stripped.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
                    html_lines.append("<p>{}</p>".format(safe))
            html = "<html><body>{}</body></html>".format("\n".join(html_lines))
        else:
            # TXT : conversion propre en HTML avec wrapping automatique
            safe = contenu.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
            safe = safe.replace("\n", "<br>")
            html = f"""
                <html>
                <head>
                    <style>
                        body {{
                            font-family: 'Segoe UI', Arial, sans-serif;
                            font-size: 11pt;
                            line-height: 1.4;
                            white-space: normal;
                            word-wrap: break-word;
                            margin: 8px;
                        }}
                    </style>
                </head>
                <body>{safe}</body>
                </html>
                """
        # --- 5) Création de la boîte de dialogue ---
        dlg = QDialog(self.iface.mainWindow())
        dlg.setWindowTitle(self.tr("QdrawEVT Help - Gestion_crise"))
        dlg.resize(800, 600)

        layout = QVBoxLayout(dlg)

        browser = QTextBrowser()
        browser.setOpenExternalLinks(True)  # liens cliquables vers le web, si présents
        browser.setHtml(html)
        layout.addWidget(browser)
        # --- 6) Boutons en bas (Fermer) ---
        btn_layout = QHBoxLayout()
        btn_layout.addStretch(1)

        btn_close = QPushButton(self.tr("Close"))
        btn_close.clicked.connect(dlg.accept)
        btn_layout.addWidget(btn_close)

        layout.addLayout(btn_layout)

        dlg.exec()

    def get_gc_anchor(self):
        return self.gc_anchor

    def get_toolbar(self):
        return self.toolbar

