From 98ac4d0609acfa2a6647bef2b335679dd9b14191 Mon Sep 17 00:00:00 2001 From: tanguy_rousseau Date: Mon, 27 Apr 2026 15:40:40 +0200 Subject: [PATCH] 1.0 --- __init__.py | 3 + animation_form.py | 0 db_manager.py | 173 +++++++++++ dialog_add.py | 42 +++ dialog_auth.py | 257 +++++++++++++++++ dialog_saisie.py | 718 ++++++++++++++++++++++++++++++++++++++++++++++ main_plugin.py | 32 +++ map_point_tool.py | 37 +++ metadata.txt | 8 + 9 files changed, 1270 insertions(+) create mode 100644 __init__.py create mode 100644 animation_form.py create mode 100644 db_manager.py create mode 100644 dialog_add.py create mode 100644 dialog_auth.py create mode 100644 dialog_saisie.py create mode 100644 main_plugin.py create mode 100644 map_point_tool.py create mode 100644 metadata.txt diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..1b877d8 --- /dev/null +++ b/__init__.py @@ -0,0 +1,3 @@ +def classFactory(iface): + from .main_plugin import AnimationPlugin + return AnimationPlugin(iface) diff --git a/animation_form.py b/animation_form.py new file mode 100644 index 0000000..e69de29 diff --git a/db_manager.py b/db_manager.py new file mode 100644 index 0000000..903126f --- /dev/null +++ b/db_manager.py @@ -0,0 +1,173 @@ +import psycopg2 +import json + +class DBManager: + def __init__(self, host, port, dbname, user, password): + self.conn = psycopg2.connect( + host=host, port=port, dbname=dbname, user=user, password=password + ) + self.conn.autocommit = False + + def close(self): + if self.conn: + self.conn.close() + + def fetch_all(self, query, params=None): + with self.conn.cursor() as cur: + cur.execute(query, params) + return cur.fetchall() + + def execute(self, query, params=None): + with self.conn.cursor() as cur: + cur.execute(query, params) + self.conn.commit() + + # --- Reference data loaders --- + def get_animateurs(self): + return self.fetch_all("SELECT code_animateur, animateur FROM animation.animateurs ORDER BY animateur") + + def get_lieux(self): + return self.fetch_all("SELECT code_lieux, lieux FROM animation.lieux_anim ORDER BY lieux") + + def get_themes(self): + return self.fetch_all("SELECT code_type_anim, theme_animation FROM animation.theme_animation ORDER BY theme_animation") + + def get_types_groupe(self): + return self.fetch_all("SELECT code_groupe, type_groupe FROM animation.type_groupe ORDER BY type_groupe") + + def get_codes_annulation(self): + return self.fetch_all("SELECT code_annul, type_annulation FROM animation.code_annulation ORDER BY type_annulation") + + def get_type_dossier_ens(self): + return self.fetch_all("SELECT code_dossier_ens, type_dossier_ens FROM animation.type_dossier_ens ORDER BY type_dossier_ens") + + def get_etablissements(self): + return self.fetch_all("SELECT code_etablissement, nom_etablissement, commune FROM animation.liste_etablissements ORDER BY nom_etablissement") + + def get_all_animations(self): + """Retourne toutes les animations pour la liste déroulante (date + lieu + thème).""" + return self.fetch_all(""" + SELECT da.code_animation, + da.date_anim, + da.lieux, + ta.theme_animation + FROM animation.donnees_animation da + LEFT JOIN animation.theme_animation ta ON ta.code_type_anim = da.code_type_anim + ORDER BY da.date_anim DESC, da.code_animation DESC + """) + + def get_animation_by_id(self, code_animation): + rows = self.fetch_all( + "SELECT * FROM animation.donnees_animation WHERE code_animation = %s", (code_animation,) + ) + if rows: + with self.conn.cursor() as cur: + cur.execute("SELECT * FROM animation.donnees_animation WHERE code_animation = %s", (code_animation,)) + cols = [d[0] for d in cur.description] + cur.execute("SELECT * FROM animation.donnees_animation WHERE code_animation = %s", (code_animation,)) + row = cur.fetchone() + return dict(zip(cols, row)) if row else None + return None + + # --- Insertions --- + def add_animateur(self, nom): + rows = self.fetch_all("SELECT MAX(code_animateur) FROM animation.animateurs") + next_code = (rows[0][0] or 0) + 1 + self.execute("INSERT INTO animation.animateurs (code_animateur, animateur) VALUES (%s, %s)", (next_code, nom)) + return next_code, nom + + def add_lieux(self, lieux, type_lieux='', type_lieux_regroupement='', geom_wkt=None, srid=4326): + rows = self.fetch_all("SELECT MAX(code_lieux) FROM animation.lieux_anim") + next_code = (rows[0][0] or 0) + 1 + if geom_wkt: + self.execute( + "INSERT INTO animation.lieux_anim (code_lieux, lieux, type_lieux, type_lieux_regroupement, geom) " + "VALUES (%s,%s,%s,%s,ST_SetSRID(ST_GeomFromText(%s),%s))", + (next_code, lieux, type_lieux, type_lieux_regroupement, geom_wkt, srid) + ) + else: + self.execute( + "INSERT INTO animation.lieux_anim (code_lieux, lieux, type_lieux, type_lieux_regroupement) VALUES (%s,%s,%s,%s)", + (next_code, lieux, type_lieux, type_lieux_regroupement) + ) + return next_code, lieux + + def add_theme(self, theme, famille=''): + rows = self.fetch_all("SELECT MAX(code_type_anim) FROM animation.theme_animation") + next_code = (rows[0][0] or 0) + 1 + self.execute("INSERT INTO animation.theme_animation (code_type_anim, theme_animation, famille_theme_animation) VALUES (%s,%s,%s)", + (next_code, theme, famille)) + return next_code, theme + + def add_annulation(self, type_annulation): + rows = self.fetch_all("SELECT MAX(code_annul) FROM animation.code_annulation") + next_code = (rows[0][0] or 0) + 1 + self.execute("INSERT INTO animation.code_annulation (code_annul, type_annulation) VALUES (%s,%s)", + (next_code, type_annulation)) + return next_code, type_annulation + + def add_type_dossier(self, type_dossier): + rows = self.fetch_all("SELECT MAX(code_dossier_ens) FROM animation.type_dossier_ens") + next_code = (rows[0][0] or 0) + 1 + self.execute("INSERT INTO animation.type_dossier_ens (code_dossier_ens, type_dossier_ens) VALUES (%s,%s)", + (next_code, type_dossier)) + return next_code, type_dossier + + def add_etablissement(self, nom, commune, prive=False, geom_wkt=None, srid=4326): + rows = self.fetch_all("SELECT MAX(code_etablissement) FROM animation.liste_etablissements") + next_code = (rows[0][0] or 0) + 1 + if geom_wkt: + self.execute( + "INSERT INTO animation.liste_etablissements (code_etablissement, nom_etablissement, prive, commune, geom) " + "VALUES (%s,%s,%s,%s,ST_SetSRID(ST_GeomFromText(%s),%s))", + (next_code, nom, prive, commune, geom_wkt, srid) + ) + else: + self.execute( + "INSERT INTO animation.liste_etablissements (code_etablissement, nom_etablissement, prive, commune) VALUES (%s,%s,%s,%s)", + (next_code, nom, prive, commune) + ) + return next_code, nom, commune + + def get_next_code_animation(self): + rows = self.fetch_all("SELECT MAX(code_animation) FROM animation.donnees_animation") + return (rows[0][0] or 0) + 1 + + def insert_donnees_animation(self, data: dict): + sql = """ + INSERT INTO animation.donnees_animation ( + code_animation, date_anim, duree_anim, animateurs, prepa_deplacemt, temps_total, + code_groupe, precisions_type_de_groupe, commune_provenance, nom_etablissement, + nbre_pers_total, nbre_pers_enfants, nbre_pers_adultes, + lieux, precisions_lieux_anim, accueil_pin, theme_detaille, + code_type_anim, payant, type_annul, remarques, + type_dossier_ens, realise_par_tiers, detail_tiers + ) VALUES ( + %(code_animation)s, %(date_anim)s, %(duree_anim)s, %(animateurs)s, %(prepa_deplacemt)s, %(temps_total)s, + %(code_groupe)s, %(precisions_type_de_groupe)s, %(commune_provenance)s, %(nom_etablissement)s, + %(nbre_pers_total)s, %(nbre_pers_enfants)s, %(nbre_pers_adultes)s, + %(lieux)s, %(precisions_lieux_anim)s, %(accueil_pin)s, %(theme_detaille)s, + %(code_type_anim)s, %(payant)s, %(type_annul)s, %(remarques)s, + %(type_dossier_ens)s, %(realise_par_tiers)s, %(detail_tiers)s + ) + """ + self.execute(sql, data) + + def update_donnees_animation(self, code_animation, data: dict): + sql = """ + UPDATE animation.donnees_animation SET + date_anim=%(date_anim)s, duree_anim=%(duree_anim)s, animateurs=%(animateurs)s, + prepa_deplacemt=%(prepa_deplacemt)s, temps_total=%(temps_total)s, + code_groupe=%(code_groupe)s, precisions_type_de_groupe=%(precisions_type_de_groupe)s, + commune_provenance=%(commune_provenance)s, nom_etablissement=%(nom_etablissement)s, + nbre_pers_total=%(nbre_pers_total)s, nbre_pers_enfants=%(nbre_pers_enfants)s, + nbre_pers_adultes=%(nbre_pers_adultes)s, + lieux=%(lieux)s, precisions_lieux_anim=%(precisions_lieux_anim)s, + accueil_pin=%(accueil_pin)s, theme_detaille=%(theme_detaille)s, + code_type_anim=%(code_type_anim)s, payant=%(payant)s, type_annul=%(type_annul)s, + remarques=%(remarques)s, type_dossier_ens=%(type_dossier_ens)s, + realise_par_tiers=%(realise_par_tiers)s, detail_tiers=%(detail_tiers)s + WHERE code_animation=%(code_animation)s + """ + data['code_animation'] = code_animation + self.execute(sql, data) diff --git a/dialog_add.py b/dialog_add.py new file mode 100644 index 0000000..5bd3a21 --- /dev/null +++ b/dialog_add.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +from qgis.PyQt.QtWidgets import ( + QDialog, QVBoxLayout, QFormLayout, QHBoxLayout, + QLineEdit, QCheckBox, QPushButton +) + + +class AddEtablissementDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Ajouter un établissement") + layout = QVBoxLayout() + self.setLayout(layout) + + form = QFormLayout() + self.nom_edit = QLineEdit() + self.nom_edit.setPlaceholderText("Nom de l'établissement") + self.commune_edit = QLineEdit() + self.commune_edit.setPlaceholderText("Commune") + self.prive_check = QCheckBox("Établissement privé") + + form.addRow("Nom* :", self.nom_edit) + form.addRow("Commune :", self.commune_edit) + form.addRow("", self.prive_check) + layout.addLayout(form) + + btns = QHBoxLayout() + ok = QPushButton("Ajouter") + cancel = QPushButton("Annuler") + btns.addWidget(ok) + btns.addWidget(cancel) + layout.addLayout(btns) + + ok.clicked.connect(self.accept) + cancel.clicked.connect(self.reject) + + def values(self): + return ( + self.nom_edit.text().strip(), + self.prive_check.isChecked(), + self.commune_edit.text().strip() or None, + ) diff --git a/dialog_auth.py b/dialog_auth.py new file mode 100644 index 0000000..00753fc --- /dev/null +++ b/dialog_auth.py @@ -0,0 +1,257 @@ +from qgis.PyQt.QtWidgets import ( + QDialog, + QVBoxLayout, + QHBoxLayout, + QFormLayout, + QLabel, + QLineEdit, + QPushButton, + QMessageBox, + QSpinBox, + QComboBox, + QWidget, +) +from qgis.PyQt.QtCore import Qt +from qgis.core import QgsApplication, QgsAuthMethodConfig, QgsSettings +from .db_manager import DBManager + +SETTINGS_SELECTED_CONNECTION = "000" + + +def try_connect_from_keychain(): + """ + Tente de se connecter via une connexion PostGIS déjà enregistrée dans QGIS + (et potentiellement associée à une config d'authentification 'authcfg'). + Retourne un DBManager connecté ou None. + """ + try: + settings = QgsSettings() + connection_name = settings.value(SETTINGS_SELECTED_CONNECTION, "", type=str).strip() + if not connection_name: + return None + + params = read_postgis_connection(connection_name) + if not params: + return None + + return connect_from_params(params) + except Exception as e: + print(f"Keychain connection failed: {e}") + return None + + +def parse_uri(uri_string): + """Parse QGIS URI format: host=value port=value dbname=value...""" + params = {} + # Remplacer les espaces de séparation par du split plus robuste + parts = uri_string.split() + for part in parts: + if '=' in part: + key, value = part.split('=', 1) + params[key.strip()] = value.strip("'\"") + return params + + +def list_postgis_connections(): + """Retourne les noms des connexions PostgreSQL enregistrées dans QGIS.""" + settings = QgsSettings() + settings.beginGroup("PostgreSQL/connections") + names = settings.childGroups() + settings.endGroup() + return sorted(names) + + +def read_postgis_connection(name): + """ + Lit une connexion PostGIS depuis les settings QGIS. + Retourne un dict avec host/port/dbname/user/password/authcfg (password peut être vide). + """ + settings = QgsSettings() + base = f"PostgreSQL/connections/{name}" + host = settings.value(f"{base}/host", "", type=str) + port = settings.value(f"{base}/port", "5432", type=str) + dbname = settings.value(f"{base}/database", "", type=str) + user = settings.value(f"{base}/username", "", type=str) + password = settings.value(f"{base}/password", "", type=str) + authcfg = settings.value(f"{base}/authcfg", "", type=str) + + if not host and not dbname: + return None + + return { + "name": name, + "host": host or "localhost", + "port": int(port) if str(port).strip() else 5432, + "dbname": dbname, + "user": user, + "password": password, + "authcfg": authcfg, + } + + +def load_auth_config(authcfg): + """Charge une config d'auth QGIS et retourne un dict utile.""" + if not authcfg: + return None + auth_mgr = QgsApplication.authManager() + cfg = QgsAuthMethodConfig() + auth_mgr.loadAuthenticationConfig(authcfg, cfg, True) + config_map = cfg.configMap() or {} + uri = cfg.uri() or "" + return { + "id": cfg.id(), + "name": cfg.name(), + "method": cfg.method(), + "config_map": config_map, + "uri": uri, + } + + +def connect_from_params(params): + """Construit un DBManager à partir des paramètres + authcfg si présent.""" + authcfg = (params.get("authcfg") or "").strip() + user = params.get("user") or "" + password = params.get("password") or "" + + if authcfg: + auth = load_auth_config(authcfg) + if auth: + # Pour les configs de type Basic, on trouve souvent username/password dans configMap + user = user or auth["config_map"].get("username") or auth["config_map"].get("user") or "" + password = password or auth["config_map"].get("password") or "" + if not password and auth.get("uri"): + uri_params = parse_uri(auth["uri"]) + user = user or uri_params.get("user", "") + password = password or uri_params.get("password", "") + + if not params.get("dbname"): + raise ValueError("Base de données manquante dans la connexion PostGIS QGIS.") + if not user: + raise ValueError("Utilisateur manquant (connexion QGIS + authcfg n'ont pas fourni de user).") + if not password: + raise ValueError("Mot de passe manquant (connexion QGIS + authcfg n'ont pas fourni de password).") + + return DBManager( + host=params.get("host") or "localhost", + port=int(params.get("port") or 5432), + dbname=params.get("dbname") or "", + user=user, + password=password, + ) + +class AuthDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Connexion à la base de données – Animation") + self.setMinimumWidth(420) + self.db = None + self._build_ui() + + def _build_ui(self): + layout = QVBoxLayout(self) + + title = QLabel("

Animation – Connexion BDD

") + title.setAlignment(Qt.AlignCenter) + layout.addWidget(title) + + info = QLabel( + "Sélectionnez une connexion PostgreSQL déjà enregistrée dans QGIS " + "(idéalement avec une configuration d’authentification associée)." + ) + info.setWordWrap(True) + layout.addWidget(info) + + form = QFormLayout() + self.connection_combo = QComboBox() + self.refresh_button = QPushButton("Actualiser") + refresh_layout = QHBoxLayout() + refresh_layout.addWidget(self.connection_combo, 1) + refresh_layout.addWidget(self.refresh_button) + refresh_widget = QWidget() + refresh_widget.setLayout(refresh_layout) + form.addRow("Connexion PostGIS :", refresh_widget) + + self.manual_host_edit = QLineEdit() + self.manual_port_spin = QSpinBox() + self.manual_port_spin.setRange(1, 65535) + self.manual_port_spin.setValue(5432) + self.manual_db_edit = QLineEdit() + self.manual_user_edit = QLineEdit() + self.manual_password_edit = QLineEdit() + self.manual_password_edit.setEchoMode(QLineEdit.Password) + + form.addRow(QLabel("Fallback manuel (si la connexion QGIS ne contient pas le secret)"), QLabel("")) + form.addRow("Hôte :", self.manual_host_edit) + form.addRow("Port :", self.manual_port_spin) + form.addRow("Base de données :", self.manual_db_edit) + form.addRow("Utilisateur :", self.manual_user_edit) + form.addRow("Mot de passe :", self.manual_password_edit) + + layout.addLayout(form) + + btn_layout = QHBoxLayout() + self.btn_cancel = QPushButton("Annuler") + self.btn_cancel.clicked.connect(self.reject) + self.btn_connect = QPushButton("Se connecter") + self.btn_connect.setDefault(True) + self.btn_connect.clicked.connect(self.connect) + btn_layout.addWidget(self.btn_cancel) + btn_layout.addWidget(self.btn_connect) + layout.addLayout(btn_layout) + + self.refresh_button.clicked.connect(self.refresh_connections) + self.connection_combo.currentIndexChanged.connect(self._on_connection_changed) + self.refresh_connections() + + def connect(self): + try: + params = self._params_from_selection() + if params: + self.db = connect_from_params(params) + QgsSettings().setValue(SETTINGS_SELECTED_CONNECTION, params.get("name", "")) + self.accept() + return + + # fallback manuel + self.db = DBManager( + host=self.manual_host_edit.text().strip() or "localhost", + port=self.manual_port_spin.value(), + dbname=self.manual_db_edit.text().strip(), + user=self.manual_user_edit.text().strip(), + password=self.manual_password_edit.text(), + ) + self.accept() + except Exception as e: + QMessageBox.critical(self, "Erreur de connexion", str(e)) + + def refresh_connections(self): + self.connection_combo.clear() + self.connection_combo.addItem("", "") + for name in list_postgis_connections(): + self.connection_combo.addItem(name, name) + + # Pré-sélection du dernier choix + last = QgsSettings().value(SETTINGS_SELECTED_CONNECTION, "", type=str) + if last: + idx = self.connection_combo.findData(last) + if idx >= 0: + self.connection_combo.setCurrentIndex(idx) + else: + self._on_connection_changed() + + def _on_connection_changed(self): + params = self._params_from_selection() + if not params: + return + # Pré-remplir le fallback manuel avec les valeurs non sensibles + self.manual_host_edit.setText(params.get("host", "") or "") + self.manual_port_spin.setValue(int(params.get("port") or 5432)) + self.manual_db_edit.setText(params.get("dbname", "") or "") + self.manual_user_edit.setText(params.get("user", "") or "") + self.manual_password_edit.clear() + + def _params_from_selection(self): + name = (self.connection_combo.currentData() or "").strip() + if not name: + return None + return read_postgis_connection(name) diff --git a/dialog_saisie.py b/dialog_saisie.py new file mode 100644 index 0000000..c5c2d51 --- /dev/null +++ b/dialog_saisie.py @@ -0,0 +1,718 @@ +import json +from datetime import date + +from qgis.PyQt.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, + QLabel, QLineEdit, QPushButton, QMessageBox, QComboBox, + QDateEdit, QSpinBox, QCheckBox, QScrollArea, QWidget, + QListWidget, QListWidgetItem, QInputDialog, QSizePolicy, + QFrame, QGroupBox +) +from qgis.PyQt.QtCore import Qt, QDate +from qgis.PyQt.QtGui import QFont, QColor +from qgis.core import QgsCoordinateReferenceSystem, QgsCoordinateTransform, QgsProject +from qgis.utils import iface + +from .map_point_tool import PointCaptureTool + + +# --------------------------------------------------------------------------- +# Widgets utilitaires +# --------------------------------------------------------------------------- + +class ComboWithAdd(QWidget): + def __init__(self, add_callback, parent=None): + super().__init__(parent) + self._add_callback = add_callback + layout = QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + self.combo = QComboBox() + self.combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.btn_add = QPushButton("+") + self.btn_add.setFixedWidth(30) + self.btn_add.setToolTip("Ajouter un nouvel élément") + self.btn_add.clicked.connect(lambda: self._add_callback(self)) + layout.addWidget(self.combo) + layout.addWidget(self.btn_add) + + def current_data(self): + return self.combo.currentData() + + def current_text(self): + return self.combo.currentText() + + def populate(self, items, placeholder=None): + self.combo.clear() + if placeholder: + self.combo.addItem(placeholder, None) + for code, label in items: + self.combo.addItem(label, code) + + def add_item(self, code, label): + self.combo.addItem(label, code) + self.combo.setCurrentIndex(self.combo.count() - 1) + + def set_by_text(self, text): + idx = self.combo.findText(text) + if idx >= 0: + self.combo.setCurrentIndex(idx) + + def set_by_data(self, data): + idx = self.combo.findData(data) + if idx >= 0: + self.combo.setCurrentIndex(idx) + + +class AnimateursWidget(QWidget): + def __init__(self, db, parent=None): + super().__init__(parent) + self.db = db + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + + self.list_widget = QListWidget() + self.list_widget.setMaximumHeight(90) + layout.addWidget(self.list_widget) + + btn_layout = QHBoxLayout() + self.combo = QComboBox() + self.combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + btn_add = QPushButton("Ajouter") + btn_add.clicked.connect(self._add_existing) + btn_new = QPushButton("+ Nouveau") + btn_new.clicked.connect(self._add_new) + btn_remove = QPushButton("Retirer") + btn_remove.clicked.connect(self._remove_selected) + btn_layout.addWidget(self.combo) + btn_layout.addWidget(btn_add) + btn_layout.addWidget(btn_new) + btn_layout.addWidget(btn_remove) + layout.addLayout(btn_layout) + + self.load_animateurs() + + def load_animateurs(self): + self.combo.clear() + for code, nom in self.db.get_animateurs(): + self.combo.addItem(nom, code) + + def _add_existing(self): + code = self.combo.currentData() + nom = self.combo.currentText() + if code is None: + return + for i in range(self.list_widget.count()): + if self.list_widget.item(i).data(Qt.UserRole) == code: + return + item = QListWidgetItem(nom) + item.setData(Qt.UserRole, code) + self.list_widget.addItem(item) + + def _add_new(self): + nom, ok = QInputDialog.getText(self, "Nouvel animateur", "Nom :") + if ok and nom.strip(): + try: + code, label = self.db.add_animateur(nom.strip()) + self.combo.addItem(label, code) + item = QListWidgetItem(label) + item.setData(Qt.UserRole, code) + self.list_widget.addItem(item) + except Exception as e: + QMessageBox.critical(self, "Erreur", str(e)) + + def _remove_selected(self): + for item in self.list_widget.selectedItems(): + self.list_widget.takeItem(self.list_widget.row(item)) + + def get_value(self): + result = [] + for i in range(self.list_widget.count()): + item = self.list_widget.item(i) + result.append({"code": item.data(Qt.UserRole), "nom": item.text()}) + return json.dumps(result, ensure_ascii=False) if result else None + + def set_value(self, json_str): + self.list_widget.clear() + if not json_str: + return + try: + data = json.loads(json_str) if isinstance(json_str, str) else json_str + for entry in data: + item = QListWidgetItem(entry.get("nom", "")) + item.setData(Qt.UserRole, entry.get("code")) + self.list_widget.addItem(item) + except Exception: + pass + + +# --------------------------------------------------------------------------- +# Dialogue principal +# --------------------------------------------------------------------------- + +class SaisieDialog(QDialog): + def __init__(self, db, parent=None): + super().__init__(parent) + self.db = db + self._etab_map = {} # code -> (nom, commune) + self._edit_mode = False # True = on édite une animation existante + self._edit_code = None # code_animation en cours d'édition + self._pending_geom = None # (x, y, crs) en attente pour ajout lieu/etab + self._pending_geom_type = None # 'lieu' ou 'etab' + self._point_tool = None + + self.setWindowTitle("Animation – Saisie / Édition") + self.setMinimumWidth(720) + self.setMinimumHeight(820) + self._build_ui() + self._load_reference_data() + + # ----------------------------------------------------------------------- + # Construction UI + # ----------------------------------------------------------------------- + + def _build_ui(self): + main_layout = QVBoxLayout(self) + main_layout.setSpacing(6) + + # --- Bandeau titre --- + self.title_label = QLabel("

Nouvelle animation

") + self.title_label.setAlignment(Qt.AlignCenter) + main_layout.addWidget(self.title_label) + + # --- Sélecteur d'animation existante --- + grp_sel = QGroupBox("Charger / modifier une animation existante") + sel_layout = QHBoxLayout(grp_sel) + self.anim_combo = QComboBox() + self.anim_combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.anim_combo.setPlaceholderText("── Nouvelle saisie ──") + self.anim_combo.addItem("── Nouvelle saisie ──", None) + self.anim_combo.currentIndexChanged.connect(self._on_anim_selected) + sel_layout.addWidget(QLabel("Animation :")) + sel_layout.addWidget(self.anim_combo) + btn_refresh = QPushButton("🔄") + btn_refresh.setFixedWidth(32) + btn_refresh.setToolTip("Rafraîchir la liste") + btn_refresh.clicked.connect(self._reload_animations_list) + sel_layout.addWidget(btn_refresh) + main_layout.addWidget(grp_sel) + + # --- Zone scrollable du formulaire --- + scroll = QScrollArea() + scroll.setWidgetResizable(True) + container = QWidget() + self.fl = QFormLayout(container) + self.fl.setLabelAlignment(Qt.AlignRight) + self.fl.setSpacing(8) + scroll.setWidget(container) + main_layout.addWidget(scroll) + + fl = self.fl + + # ---- Date & Durée ---- + self._section(fl, "Date et durée") + + self.date_edit = QDateEdit(QDate.currentDate()) + self.date_edit.setCalendarPopup(True) + self.date_edit.setDisplayFormat("dd/MM/yyyy") + fl.addRow("Date *:", self.date_edit) + + dur_w = QWidget() + dur_l = QHBoxLayout(dur_w) + dur_l.setContentsMargins(0, 0, 0, 0) + self.duree_h = QSpinBox() + self.duree_h.setRange(0, 24) + self.duree_h.setSuffix(" h") + self.duree_min = QSpinBox() + self.duree_min.setRange(0, 59) + self.duree_min.setSuffix(" min") + dur_l.addWidget(self.duree_h) + dur_l.addWidget(self.duree_min) + dur_l.addStretch() + fl.addRow("Durée :", dur_w) + + self.prepa_spin = QSpinBox() + self.prepa_spin.setRange(0, 9999) + self.prepa_spin.setSuffix(" min") + for w in (self.duree_h, self.duree_min, self.prepa_spin): + w.valueChanged.connect(self._update_temps_total) + fl.addRow("Prépa / déplacement :", self.prepa_spin) + + self.temps_total_label = QLabel("0 min") + fl.addRow("Temps total (calculé) :", self.temps_total_label) + + # ---- Animateurs ---- + self._sep(fl) + self._section(fl, "Animateurs") + self.animateurs_widget = AnimateursWidget(self.db) + fl.addRow("Animateurs :", self.animateurs_widget) + + # ---- Groupe ---- + self._sep(fl) + self._section(fl, "Groupe") + self.type_groupe_combo = ComboWithAdd(self._add_type_groupe) + fl.addRow("Type de groupe :", self.type_groupe_combo) + self.precisions_groupe_edit = QLineEdit() + fl.addRow("Précisions groupe :", self.precisions_groupe_edit) + + # ---- Établissement ---- + self._sep(fl) + self._section(fl, "Établissement / Provenance") + self.etab_combo = ComboWithAdd(self._add_etablissement) + self.etab_combo.combo.currentIndexChanged.connect(self._on_etab_changed) + fl.addRow("Établissement :", self.etab_combo) + self.commune_label = QLabel("") + self.commune_label.setStyleSheet("color:#555; font-style:italic;") + fl.addRow("Commune (auto) :", self.commune_label) + + # ---- Participants ---- + self._sep(fl) + self._section(fl, "Participants") + self.enfants_spin = QSpinBox() + self.enfants_spin.setRange(0, 9999) + self.enfants_spin.valueChanged.connect(self._update_total_pers) + fl.addRow("Enfants :", self.enfants_spin) + self.adultes_spin = QSpinBox() + self.adultes_spin.setRange(0, 9999) + self.adultes_spin.valueChanged.connect(self._update_total_pers) + fl.addRow("Adultes :", self.adultes_spin) + self.total_pers_label = QLabel("0") + fl.addRow("Total (calculé) :", self.total_pers_label) + + # ---- Lieu ---- + self._sep(fl) + self._section(fl, "Lieu") + self.lieux_combo = ComboWithAdd(self._add_lieu) + fl.addRow("Lieu :", self.lieux_combo) + self.precisions_lieux_edit = QLineEdit() + fl.addRow("Précisions lieu :", self.precisions_lieux_edit) + self.accueil_pin_check = QCheckBox("Oui") + fl.addRow("Accueil PIN :", self.accueil_pin_check) + + # ---- Thème ---- + self._sep(fl) + self._section(fl, "Thème") + self.theme_combo = ComboWithAdd(self._add_theme) + fl.addRow("Thème :", self.theme_combo) + self.theme_detaille_edit = QLineEdit() + fl.addRow("Thème détaillé :", self.theme_detaille_edit) + + # ---- Statuts ---- + self._sep(fl) + self._section(fl, "Statuts") + self.payant_check = QCheckBox("Oui") + fl.addRow("Payant :", self.payant_check) + self.annul_combo = ComboWithAdd(self._add_annulation) + fl.addRow("Annulation :", self.annul_combo) + self.dossier_combo = ComboWithAdd(self._add_dossier) + fl.addRow("Dossier enseignant :", self.dossier_combo) + self.tiers_check = QCheckBox("Oui") + self.tiers_check.stateChanged.connect(self._on_tiers_changed) + fl.addRow("Réalisé par tiers :", self.tiers_check) + self.detail_tiers_edit = QLineEdit() + self.detail_tiers_edit.setEnabled(False) + fl.addRow("Détail tiers :", self.detail_tiers_edit) + + # ---- Remarques ---- + self._sep(fl) + self.remarques_edit = QLineEdit() + fl.addRow("Remarques :", self.remarques_edit) + + # --- Boutons d'action --- + btn_layout = QHBoxLayout() + btn_cancel = QPushButton("Fermer") + btn_cancel.clicked.connect(self.reject) + + self.btn_new_form = QPushButton("✚ Nouveau") + self.btn_new_form.setToolTip("Réinitialiser pour une nouvelle saisie") + self.btn_new_form.clicked.connect(self._new_form) + + self.btn_save_new = QPushButton("Enregistrer et nouveau") + self.btn_save_new.clicked.connect(self._save_and_new) + + self.btn_save = QPushButton("Enregistrer") + self.btn_save.setDefault(True) + self.btn_save.clicked.connect(self._save) + + btn_layout.addWidget(btn_cancel) + btn_layout.addWidget(self.btn_new_form) + btn_layout.addStretch() + btn_layout.addWidget(self.btn_save_new) + btn_layout.addWidget(self.btn_save) + main_layout.addLayout(btn_layout) + + def _section(self, fl, title): + lbl = QLabel(f"{title}") + fl.addRow(lbl) + + def _sep(self, fl): + line = QFrame() + line.setFrameShape(QFrame.HLine) + line.setStyleSheet("color:#ccc;") + fl.addRow(line) + + # ----------------------------------------------------------------------- + # Chargement données de référence + # ----------------------------------------------------------------------- + + def _load_reference_data(self): + try: + self.type_groupe_combo.populate(self.db.get_types_groupe(), "-- Sélectionner --") + etabs = self.db.get_etablissements() + self._etab_map = {c: (n, com) for c, n, com in etabs} + self.etab_combo.populate( + [(c, f"{n} ({com})" if com else n) for c, n, com in etabs], + "-- Sélectionner --" + ) + self.lieux_combo.populate(self.db.get_lieux(), "-- Sélectionner --") + self.theme_combo.populate(self.db.get_themes(), "-- Sélectionner --") + self.annul_combo.populate(self.db.get_codes_annulation(), "Non") + self._set_default(self.annul_combo, "Non") + self.dossier_combo.populate(self.db.get_type_dossier_ens(), "Non") + self._set_default(self.dossier_combo, "Non") + self._reload_animations_list() + except Exception as e: + QMessageBox.critical(self, "Erreur chargement", str(e)) + + def _set_default(self, combo_widget, text): + idx = combo_widget.combo.findText(text) + if idx >= 0: + combo_widget.combo.setCurrentIndex(idx) + + def _reload_animations_list(self): + self.anim_combo.blockSignals(True) + self.anim_combo.clear() + self.anim_combo.addItem("── Nouvelle saisie ──", None) + try: + rows = self.db.get_all_animations() + for code, date_anim, lieux, theme in rows: + date_str = date_anim.strftime("%d/%m/%Y") if date_anim else "?" + lieux_str = lieux or "Lieu ?" + theme_str = theme or "Thème ?" + label = f"{date_str} | {lieux_str} | {theme_str} [#{code}]" + self.anim_combo.addItem(label, code) + except Exception as e: + QMessageBox.warning(self, "Erreur", f"Impossible de charger les animations : {e}") + self.anim_combo.blockSignals(False) + + # ----------------------------------------------------------------------- + # Événements + # ----------------------------------------------------------------------- + + def _on_anim_selected(self, idx): + code = self.anim_combo.itemData(idx) + if code is None: + self._new_form() + return + try: + data = self.db.get_animation_by_id(code) + if data: + self._edit_mode = True + self._edit_code = code + self.title_label.setText(f"

Édition animation #{code}

") + self.btn_save.setText("Mettre à jour") + self.btn_save_new.setVisible(False) + self._populate_form(data) + except Exception as e: + QMessageBox.critical(self, "Erreur", str(e)) + + def _populate_form(self, data): + """Remplit le formulaire avec les données d'une animation existante.""" + if data.get('date_anim'): + d = data['date_anim'] + self.date_edit.setDate(QDate(d.year, d.month, d.day)) + + duree = data.get('duree_anim', '0h00') or '0h00' + try: + if 'h' in duree: + h, m = duree.split('h') + self.duree_h.setValue(int(h)) + self.duree_min.setValue(int(m) if m else 0) + except Exception: + pass + + self.prepa_spin.setValue(data.get('prepa_deplacemt') or 0) + self.animateurs_widget.set_value(data.get('animateurs')) + self.type_groupe_combo.set_by_data(data.get('code_groupe')) + self.precisions_groupe_edit.setText(data.get('precisions_type_de_groupe') or '') + + # Établissement : chercher par nom + nom_etab = data.get('nom_etablissement') or '' + for code, (nom, com) in self._etab_map.items(): + if nom == nom_etab: + self.etab_combo.set_by_data(code) + break + + self.enfants_spin.setValue(data.get('nbre_pers_enfants') or 0) + self.adultes_spin.setValue(data.get('nbre_pers_adultes') or 0) + self.lieux_combo.set_by_text(data.get('lieux') or '') + self.precisions_lieux_edit.setText(data.get('precisions_lieux_anim') or '') + self.accueil_pin_check.setChecked(bool(data.get('accueil_pin'))) + self.theme_detaille_edit.setText(data.get('theme_detaille') or '') + self.theme_combo.set_by_data(data.get('code_type_anim')) + self.payant_check.setChecked(bool(data.get('payant'))) + self.annul_combo.set_by_text(data.get('type_annul') or 'Non') + self.remarques_edit.setText(data.get('remarques') or '') + self.dossier_combo.set_by_text(data.get('type_dossier_ens') or 'Non') + realise = bool(data.get('realise_par_tiers')) + self.tiers_check.setChecked(realise) + self.detail_tiers_edit.setText(data.get('detail_tiers') or '') + self.detail_tiers_edit.setEnabled(realise) + + def _new_form(self): + self._edit_mode = False + self._edit_code = None + self.title_label.setText("

Nouvelle animation

") + self.btn_save.setText("Enregistrer") + self.btn_save_new.setVisible(True) + self.anim_combo.blockSignals(True) + self.anim_combo.setCurrentIndex(0) + self.anim_combo.blockSignals(False) + self._reset_form() + + def _on_etab_changed(self, idx): + code = self.etab_combo.combo.itemData(idx) + if code and code in self._etab_map: + self.commune_label.setText(self._etab_map[code][1] or "") + else: + self.commune_label.setText("") + + def _on_tiers_changed(self, state): + self.detail_tiers_edit.setEnabled(state == Qt.Checked) + + def _update_temps_total(self): + total = self.duree_h.value() * 60 + self.duree_min.value() + self.prepa_spin.value() + self.temps_total_label.setText(f"{total} min") + + def _update_total_pers(self): + self.total_pers_label.setText(str(self.enfants_spin.value() + self.adultes_spin.value())) + + # ----------------------------------------------------------------------- + # Capture géométrie sur carte + # ----------------------------------------------------------------------- + + def _start_point_capture(self, geom_type, on_captured_callback): + """ + Lance l'outil de clic sur la carte. geom_type = 'lieu' ou 'etab'. + Après le clic, appelle on_captured_callback(wkt, srid). + """ + canvas = iface.mapCanvas() + if not canvas: + QMessageBox.warning(self, "Erreur", "Impossible d'accéder à la carte QGIS.") + return + + prev_tool = canvas.mapTool() + tool = PointCaptureTool(canvas, prev_tool) + + def on_point(x, y, crs): + # Convertir en WGS84 (4326) si nécessaire + wgs84 = QgsCoordinateReferenceSystem("EPSG:4326") + if crs != wgs84: + transform = QgsCoordinateTransform(crs, wgs84, QgsProject.instance()) + from qgis.core import QgsPointXY + pt = transform.transform(x, y) + x, y = pt.x(), pt.y() + wkt = f"POINT({x} {y})" + on_captured_callback(wkt, 4326) + self.setWindowState(self.windowState() & ~Qt.WindowMinimized | Qt.WindowActive) + self.activateWindow() + + tool.pointCaptured.connect(on_point) + self._point_tool = tool + + # Minimiser le dialogue et activer l'outil + self.showMinimized() + canvas.setMapTool(tool) + + # ----------------------------------------------------------------------- + # Callbacks ajout nouveaux éléments + # ----------------------------------------------------------------------- + + def _add_type_groupe(self, widget): + QMessageBox.information(self, "Info", + "L'ajout d'un type de groupe nécessite un code entier.\n" + "Veuillez contacter l'administrateur de la base.") + + def _add_lieu(self, widget): + val, ok = QInputDialog.getText(self, "Nouveau lieu", "Nom du lieu :") + if not ok or not val.strip(): + return + nom = val.strip() + + reply = QMessageBox.question(self, "Géométrie", + "Souhaitez-vous localiser ce lieu sur la carte ?", + QMessageBox.Yes | QMessageBox.No) + + if reply == QMessageBox.Yes: + def on_geom(wkt, srid): + try: + code, label = self.db.add_lieux(nom, geom_wkt=wkt, srid=srid) + widget.add_item(code, label) + QMessageBox.information(self, "Succès", f"Lieu « {label} » ajouté avec géométrie.") + except Exception as e: + QMessageBox.critical(self, "Erreur", str(e)) + self._start_point_capture('lieu', on_geom) + else: + try: + code, label = self.db.add_lieux(nom) + widget.add_item(code, label) + except Exception as e: + QMessageBox.critical(self, "Erreur", str(e)) + + def _add_theme(self, widget): + val, ok = QInputDialog.getText(self, "Nouveau thème", "Libellé :") + if ok and val.strip(): + try: + code, label = self.db.add_theme(val.strip()) + widget.add_item(code, label) + except Exception as e: + QMessageBox.critical(self, "Erreur", str(e)) + + def _add_annulation(self, widget): + val, ok = QInputDialog.getText(self, "Nouveau type d'annulation", "Libellé :") + if ok and val.strip(): + try: + code, label = self.db.add_annulation(val.strip()) + widget.add_item(code, label) + except Exception as e: + QMessageBox.critical(self, "Erreur", str(e)) + + def _add_dossier(self, widget): + val, ok = QInputDialog.getText(self, "Nouveau type de dossier", "Libellé :") + if ok and val.strip(): + try: + code, label = self.db.add_type_dossier(val.strip()) + widget.add_item(code, label) + except Exception as e: + QMessageBox.critical(self, "Erreur", str(e)) + + def _add_etablissement(self, widget): + nom, ok = QInputDialog.getText(self, "Nouvel établissement", "Nom :") + if not ok or not nom.strip(): + return + commune, ok2 = QInputDialog.getText(self, "Nouvel établissement", "Commune :") + if not ok2: + return + nom = nom.strip() + commune = commune.strip() + + reply = QMessageBox.question(self, "Géométrie", + "Souhaitez-vous localiser cet établissement sur la carte ?", + QMessageBox.Yes | QMessageBox.No) + + if reply == QMessageBox.Yes: + def on_geom(wkt, srid): + try: + code, label, com = self.db.add_etablissement(nom, commune, geom_wkt=wkt, srid=srid) + self._etab_map[code] = (label, com) + display = f"{label} ({com})" if com else label + widget.add_item(code, display) + QMessageBox.information(self, "Succès", f"Établissement « {label} » ajouté avec géométrie.") + except Exception as e: + QMessageBox.critical(self, "Erreur", str(e)) + self._start_point_capture('etab', on_geom) + else: + try: + code, label, com = self.db.add_etablissement(nom, commune) + self._etab_map[code] = (label, com) + display = f"{label} ({com})" if com else label + widget.add_item(code, display) + except Exception as e: + QMessageBox.critical(self, "Erreur", str(e)) + + # ----------------------------------------------------------------------- + # Collecte, validation, enregistrement + # ----------------------------------------------------------------------- + + def _collect_data(self): + duree_str = f"{self.duree_h.value()}h{self.duree_min.value():02d}" + duree_min_total = self.duree_h.value() * 60 + self.duree_min.value() + temps_total = duree_min_total + self.prepa_spin.value() or None + + etab_code = self.etab_combo.current_data() + nom_etab, commune_prov = (None, None) + if etab_code and etab_code in self._etab_map: + nom_etab, commune_prov = self._etab_map[etab_code] + + lieux_txt = self.lieux_combo.current_text() if self.lieux_combo.current_data() else None + type_annul = self.annul_combo.current_text() if self.annul_combo.current_data() else None + type_dossier = self.dossier_combo.current_text() if self.dossier_combo.current_data() else None + realise_tiers = self.tiers_check.isChecked() + + return { + "date_anim": self.date_edit.date().toPyDate(), + "duree_anim": duree_str, + "animateurs": self.animateurs_widget.get_value(), + "prepa_deplacemt": self.prepa_spin.value() or None, + "temps_total": temps_total, + "code_groupe": self.type_groupe_combo.current_data(), + "precisions_type_de_groupe": self.precisions_groupe_edit.text().strip() or None, + "commune_provenance": commune_prov, + "nom_etablissement": nom_etab, + "nbre_pers_enfants": self.enfants_spin.value() or None, + "nbre_pers_adultes": self.adultes_spin.value() or None, + "nbre_pers_total": (self.enfants_spin.value() + self.adultes_spin.value()) or None, + "lieux": lieux_txt, + "precisions_lieux_anim": self.precisions_lieux_edit.text().strip() or None, + "accueil_pin": self.accueil_pin_check.isChecked(), + "theme_detaille": self.theme_detaille_edit.text().strip() or None, + "code_type_anim": self.theme_combo.current_data(), + "payant": self.payant_check.isChecked(), + "type_annul": type_annul, + "remarques": self.remarques_edit.text().strip() or None, + "type_dossier_ens": type_dossier, + "realise_par_tiers": realise_tiers, + "detail_tiers": self.detail_tiers_edit.text().strip() if realise_tiers else None, + } + + def _reset_form(self): + self.date_edit.setDate(QDate.currentDate()) + self.duree_h.setValue(0) + self.duree_min.setValue(0) + self.prepa_spin.setValue(0) + self.animateurs_widget.list_widget.clear() + self.type_groupe_combo.combo.setCurrentIndex(0) + self.precisions_groupe_edit.clear() + self.etab_combo.combo.setCurrentIndex(0) + self.commune_label.setText("") + self.enfants_spin.setValue(0) + self.adultes_spin.setValue(0) + self.lieux_combo.combo.setCurrentIndex(0) + self.precisions_lieux_edit.clear() + self.accueil_pin_check.setChecked(False) + self.theme_combo.combo.setCurrentIndex(0) + self.theme_detaille_edit.clear() + self.payant_check.setChecked(False) + self.annul_combo.combo.setCurrentIndex(0) + self.remarques_edit.clear() + self.dossier_combo.combo.setCurrentIndex(0) + self.tiers_check.setChecked(False) + self.detail_tiers_edit.clear() + + def _save(self): + try: + data = self._collect_data() + if self._edit_mode: + self.db.update_donnees_animation(self._edit_code, data) + QMessageBox.information(self, "Succès", f"Animation #{self._edit_code} mise à jour.") + self._reload_animations_list() + else: + code = self.db.get_next_code_animation() + data['code_animation'] = code + self.db.insert_donnees_animation(data) + QMessageBox.information(self, "Succès", f"Animation #{code} enregistrée.") + self._reload_animations_list() + self.accept() + except Exception as e: + QMessageBox.critical(self, "Erreur", str(e)) + + def _save_and_new(self): + try: + data = self._collect_data() + code = self.db.get_next_code_animation() + data['code_animation'] = code + self.db.insert_donnees_animation(data) + QMessageBox.information(self, "Succès", f"Animation #{code} enregistrée.") + self._reload_animations_list() + self._new_form() + except Exception as e: + QMessageBox.critical(self, "Erreur", str(e)) diff --git a/main_plugin.py b/main_plugin.py new file mode 100644 index 0000000..082680d --- /dev/null +++ b/main_plugin.py @@ -0,0 +1,32 @@ +from qgis.PyQt.QtWidgets import QAction, QMessageBox, QDialog +from .dialog_auth import AuthDialog, try_connect_from_keychain +from .dialog_saisie import SaisieDialog + + +class AnimationPlugin: + def __init__(self, iface): + self.iface = iface + self.action = None + + def initGui(self): + self.action = QAction("Saisie Animation", self.iface.mainWindow()) + self.action.triggered.connect(self.run) + self.iface.addToolBarIcon(self.action) + self.iface.addPluginToMenu("Animation", self.action) + + def unload(self): + self.iface.removePluginMenu("Animation", self.action) + self.iface.removeToolBarIcon(self.action) + + def run(self): + # Tenter la connexion automatique via le coffre-fort QGIS + db = try_connect_from_keychain() + if db: + dlg = SaisieDialog(db, self.iface.mainWindow()) + dlg.exec_() + else: + # Fallback : sélection d'une connexion QGIS (ou saisie manuelle) + dlg = AuthDialog(self.iface.mainWindow()) + if dlg.exec_() == QDialog.Accepted and dlg.db: + saisie = SaisieDialog(dlg.db, self.iface.mainWindow()) + saisie.exec_() diff --git a/map_point_tool.py b/map_point_tool.py new file mode 100644 index 0000000..ca0efb0 --- /dev/null +++ b/map_point_tool.py @@ -0,0 +1,37 @@ +from qgis.gui import QgsMapTool +from qgis.PyQt.QtCore import pyqtSignal +from qgis.PyQt.QtGui import QCursor +from qgis.PyQt.QtCore import Qt + + +class PointCaptureTool(QgsMapTool): + """ + Outil de capture d'un point sur la carte QGIS. + Émet pointCaptured(x, y, crs) au clic gauche. + Se désactive automatiquement après un clic. + """ + pointCaptured = pyqtSignal(float, float, object) + + def __init__(self, canvas, previous_tool=None): + super().__init__(canvas) + self.canvas = canvas + self.previous_tool = previous_tool + self.setCursor(QCursor(Qt.CrossCursor)) + + def canvasReleaseEvent(self, event): + if event.button() == Qt.LeftButton: + point = self.toMapCoordinates(event.pos()) + crs = self.canvas.mapSettings().destinationCrs() + self.pointCaptured.emit(point.x(), point.y(), crs) + # Restaurer l'outil précédent + if self.previous_tool: + self.canvas.setMapTool(self.previous_tool) + else: + self.canvas.unsetMapTool(self) + + def keyPressEvent(self, event): + if event.key() == Qt.Key_Escape: + if self.previous_tool: + self.canvas.setMapTool(self.previous_tool) + else: + self.canvas.unsetMapTool(self) diff --git a/metadata.txt b/metadata.txt new file mode 100644 index 0000000..1c6b7ac --- /dev/null +++ b/metadata.txt @@ -0,0 +1,8 @@ +[general] +name=Animation Saisie +qgisMinimumVersion=3.0 +description=Plugin de saisie des données d'animation +version=1.0 +author=Animation +email=contact@animation.fr +about=Plugin permettant la saisie des données dans la table animation.donnees_animation