From 5a951edec702a25235c3aee8453896244b8e86cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Sp=C3=A1=C4=8Dil?= Date: Fri, 13 Mar 2026 10:48:12 +0100 Subject: [PATCH] Fixed compatibility issues with QGIS 4 (#24) * used PyQt6 compatible objects * metadata update --- .gitignore | 1 + amcr_viewer/amcr_dialog.py | 122 ++++++++++++++++++------------------- amcr_viewer/amcr_tools.py | 85 +++++++++++++------------- amcr_viewer/amcr_viewer.py | 6 +- amcr_viewer/metadata.txt | 5 +- amcr_viewer/resources.py | 2 +- 6 files changed, 111 insertions(+), 110 deletions(-) diff --git a/.gitignore b/.gitignore index ed51dcb..8c9db34 100644 --- a/.gitignore +++ b/.gitignore @@ -210,3 +210,4 @@ __marimo__/ README_files/ README.html +amcr_viewer.zip diff --git a/amcr_viewer/amcr_dialog.py b/amcr_viewer/amcr_dialog.py index 87336da..3d5542c 100644 --- a/amcr_viewer/amcr_dialog.py +++ b/amcr_viewer/amcr_dialog.py @@ -13,6 +13,7 @@ from .amcr_codelists import (OBDOBI, TYP_AKCE, KRAJE, AREAL, ORGANIZACE, class FilterableSelectionDialog(QDialog): """ A custom dialog for selecting multiple items from a list with a search filter. + Updated for PyQt6/Qt6 compatibility. """ def __init__(self, title, data_dict, preselected_codes, parent=None): super().__init__(parent) @@ -37,7 +38,9 @@ class FilterableSelectionDialog(QDialog): layout.addWidget(self.list_widget) # Standard OK/Cancel dialog buttons - buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + buttons = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel + ) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) layout.addWidget(buttons) @@ -52,16 +55,16 @@ class FilterableSelectionDialog(QDialog): item = QListWidgetItem(name) # Store the actual code (ID) hidden in the UserRole - item.setData(Qt.UserRole, code) + item.setData(Qt.ItemDataRole.UserRole, code) # Make the item checkable (adds a checkbox) - item.setFlags(item.flags() | Qt.ItemIsUserCheckable) + item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable) # Restore previous selection state if code in self.preselected: - item.setCheckState(Qt.Checked) + item.setCheckState(Qt.CheckState.Checked) else: - item.setCheckState(Qt.Unchecked) + item.setCheckState(Qt.CheckState.Unchecked) self.list_widget.addItem(item) @@ -70,10 +73,7 @@ class FilterableSelectionDialog(QDialog): search_text = text.lower() for i in range(self.list_widget.count()): item = self.list_widget.item(i) - if search_text not in item.text().lower(): - item.setHidden(True) - else: - item.setHidden(False) + item.setHidden(search_text not in item.text().lower()) def get_selected_codes(self): """Returns the hidden codes and display labels of all checked items.""" @@ -81,8 +81,8 @@ class FilterableSelectionDialog(QDialog): labels = [] for i in range(self.list_widget.count()): item = self.list_widget.item(i) - if item.checkState() == Qt.Checked: - codes.append(item.data(Qt.UserRole)) + if item.checkState() == Qt.CheckState.Checked: + codes.append(item.data(Qt.ItemDataRole.UserRole)) labels.append(item.text()) return codes, labels @@ -93,7 +93,7 @@ class AmcrFilterDialog(QDialog): The main filtering UI where users set criteria before downloading data. """ def __init__(self, typ_dat, parent=None): - super(AmcrFilterDialog, self).__init__(parent) + super().__init__(parent) self.setWindowTitle("Filtr AMČR") self.resize(500, 750) @@ -189,7 +189,9 @@ class AmcrFilterDialog(QDialog): layout.addStretch(1) # Main dialog OK/Cancel buttons - buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + buttons = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel + ) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) layout.addWidget(buttons) @@ -197,60 +199,56 @@ class AmcrFilterDialog(QDialog): self.setLayout(layout) def setup_picker(self, label_text, cache_key, data_source, extra_btn=None): - """ - Creates a reusable UI component consisting of a label, a read-only - text field showing selected items, and a button to open the selection dialog. - """ - row_widget = QGroupBox(label_text) - # row_widget.setFlat(True) - - row_layout = QHBoxLayout() - row_layout.setContentsMargins(5, 5, 5, 5) - - # Read-only field displaying the names of selected items - display_field = QLineEdit() - display_field.setReadOnly(True) - display_field.setPlaceholderText("Nic nevybráno (vše)") - display_field.setStyleSheet("background-color: #f0f0f0; color: #333;") - - btn = QPushButton("Vybrat...") - btn.setFixedWidth(80) - - # Nested function that handles opening the dialog and saving results - def open_dialog(): - dlg = FilterableSelectionDialog(label_text, data_source, self.selection_cache[cache_key], self) - if dlg.exec() == QDialog.Accepted: - codes, labels = dlg.get_selected_codes() - - # Update local cache with selected IDs - self.selection_cache[cache_key] = codes - - # Update the UI text field with selected names - if labels: - display_field.setText(", ".join(labels)) - else: - display_field.clear() - - # Special case: Pre-fill specific accuracy levels by default - if cache_key == 'pian_presnost': - display_field.setText("odchylka jednotky metrů, odchylka desítky metrů, odchylka stovky metrů") - self.selection_cache[cache_key] = ['HES-000861', 'HES-000862', 'HES-000863'] + """ + Creates a reusable UI component consisting of a label, a read-only + text field showing selected items, and a button to open the selection dialog. + """ + row_widget = QGroupBox(label_text) + row_layout = QHBoxLayout() + row_layout.setContentsMargins(5, 5, 5, 5) + + # Read-only field displaying the names of selected items + display_field = QLineEdit() + display_field.setReadOnly(True) + display_field.setPlaceholderText("Nic nevybráno (vše)") + display_field.setStyleSheet("background-color: #f0f0f0; color: #333;") + + btn = QPushButton("Vybrat...") + btn.setFixedWidth(80) + + # Nested function that handles opening the dialog and saving results + def open_dialog(): + dlg = FilterableSelectionDialog(label_text, data_source, self.selection_cache[cache_key], self) + if dlg.exec() == QDialog.DialogCode.Accepted: # PyQt6: DialogCode + codes, labels = dlg.get_selected_codes() + # Update local cache with selected IDs + self.selection_cache[cache_key] = codes + # Update the UI text field with selected names + if labels: + display_field.setText(", ".join(labels)) + else: + display_field.clear() + + # Special case: Pre-fill specific accuracy levels by default + if cache_key == 'pian_presnost': + display_field.setText("odchylka jednotky metrů, odchylka desítky metrů, odchylka stovky metrů") + self.selection_cache[cache_key] = ['HES-000861', 'HES-000862', 'HES-000863'] - btn.clicked.connect(open_dialog) + btn.clicked.connect(open_dialog) + + row_layout.addWidget(display_field) + row_layout.addWidget(btn) + + # Add an optional extra button (e.g., the refresh button for leaders) + if extra_btn: + row_layout.addWidget(extra_btn) - row_layout.addWidget(display_field) - row_layout.addWidget(btn) - - # Add an optional extra button (e.g., the refresh button for leaders) - if extra_btn: - row_layout.addWidget(extra_btn) - - row_widget.setLayout(row_layout) - return row_widget + row_widget.setLayout(row_layout) + return row_widget def action_update_vedouci(self): # Change cursor to loading state to indicate background task - QApplication.setOverrideCursor(Qt.WaitCursor) + QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor) try: success, msg = download_vedouci() if success: diff --git a/amcr_viewer/amcr_tools.py b/amcr_viewer/amcr_tools.py index 7bd237c..1b1e26c 100644 --- a/amcr_viewer/amcr_tools.py +++ b/amcr_viewer/amcr_tools.py @@ -2,10 +2,11 @@ from qgis.gui import QgsMapToolIdentifyFeature from qgis.core import (QgsProject, QgsVectorLayer, QgsFeature, QgsGeometry, QgsField, QgsCoordinateReferenceSystem, QgsCoordinateTransform, - QgsWkbTypes, QgsRelation, QgsEditorWidgetSetup) + QgsWkbTypes, QgsRelation, QgsEditorWidgetSetup, Qgis) from qgis.utils import iface -from qgis.PyQt.QtCore import QVariant, Qt +from qgis.PyQt.QtCore import Qt, QMetaType from qgis.PyQt.QtWidgets import QMessageBox, QApplication +from qgis.PyQt.QtGui import QCursor import requests import json import xml.etree.ElementTree as ET @@ -56,8 +57,8 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false") url = "https://digiarchiv.aiscr.cz/api/search/query" - iface.messageBar().pushMessage("AMCR", "Hledám záznamy...", level=1) - QApplication.setOverrideCursor(Qt.WaitCursor) + iface.messageBar().pushMessage("AMCR", "Hledám záznamy...", level=Qgis.MessageLevel.Info) + QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor)) try: # ========================================== @@ -128,7 +129,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false") if len(docs) >= num_found: break if len(docs) >= MAX_LIMIT: - iface.messageBar().pushMessage("AMCR", f"Limit {MAX_LIMIT} záznamů dosažen.", level=1) + iface.messageBar().pushMessage("AMCR", f"Limit {MAX_LIMIT} záznamů dosažen.", level=Qgis.MessageLevel.Warning) break current_page += 1 @@ -139,7 +140,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false") break if not docs: - iface.messageBar().pushMessage("AMCR", "Žádné záznamy nenalezeny.", level=1) + iface.messageBar().pushMessage("AMCR", "Žádné záznamy nenalezeny.", level=Qgis.MessageLevel.Warning) return # ========================================== @@ -268,7 +269,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false") feats_k.append(feat) if not target_pian_ids: - iface.messageBar().pushMessage("AMCR", f"Nalezeno {len(docs)} záznamů, ale žádný nemá geometrii.", level=1) + iface.messageBar().pushMessage("AMCR", f"Nalezeno {len(docs)} záznamů, ale žádný nemá geometrii.", level=Qgis.MessageLevel.Warning) return @@ -280,7 +281,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false") docs_pian = [] BATCH_PIAN = 200 # Geometry requests are batch-processed to stay under URL length limits - iface.messageBar().pushMessage("AMCR", f"Záznamů: {len(docs)} (z toho {actions_with_geom} s mapou). Stahuji {total_pians} unikátních geometrií, vykresluji {target_pian_ids_count} geometrií...", level=1) + iface.messageBar().pushMessage("AMCR", f"Záznamů: {len(docs)} (z toho {actions_with_geom} s mapou). Stahuji {total_pians} unikátních geometrií, vykresluji {target_pian_ids_count} geometrií...", level=Qgis.MessageLevel.Info) fl_pian = ["ident_cely", "pian_typ", "pian_chranene_udaje", "pian_presnost"] @@ -318,43 +319,43 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false") # Define attribute table structure cols = [ - QgsField("PIAN", QVariant.String), - QgsField("Přesnost", QVariant.String), - QgsField("PIAN – typ", QVariant.String), - QgsField("Dokumentační jednotka", QVariant.String), - QgsField("Typ dokumentační jednotky", QVariant.String), - QgsField("Definiční bod(y) (WGS-84)", QVariant.String), - QgsField(archeologicky_zaznam, QVariant.String), - QgsField("Odkaz do Digitálního archivu AMČR", QVariant.String), - QgsField("Okres", QVariant.String), - QgsField("Katastr", QVariant.String), - QgsField("Další katastry", QVariant.String) + QgsField("PIAN", QMetaType.Type.QString), + QgsField("Přesnost", QMetaType.Type.QString), + QgsField("PIAN – typ", QMetaType.Type.QString), + QgsField("Dokumentační jednotka", QMetaType.Type.QString), + QgsField("Typ dokumentační jednotky", QMetaType.Type.QString), + QgsField("Definiční bod(y) (WGS-84)", QMetaType.Type.QString), + QgsField(archeologicky_zaznam, QMetaType.Type.QString), + QgsField("Odkaz do Digitálního archivu AMČR", QMetaType.Type.QString), + QgsField("Okres", QMetaType.Type.QString), + QgsField("Katastr", QMetaType.Type.QString), + QgsField("Další katastry", QMetaType.Type.QString) ] # Extend table based on data type if typ_dat == "akce": cols += [ - QgsField("Akce – lokalizace", QVariant.String), - QgsField("Vedoucí akce", QVariant.String), - QgsField("Organizace", QVariant.String), - QgsField("Specifikace data", QVariant.String), - QgsField("Datum zahájeni", QVariant.String), - QgsField("Datum ukončení", QVariant.String), - QgsField("Hlavní typ", QVariant.String), - QgsField("Vedlejší typ", QVariant.String), - QgsField("Zjištění", QVariant.String), - QgsField("Akce – nahrazuje NZ", QVariant.String), + QgsField("Akce – lokalizace", QMetaType.Type.QString), + QgsField("Vedoucí akce", QMetaType.Type.QString), + QgsField("Organizace", QMetaType.Type.QString), + QgsField("Specifikace data", QMetaType.Type.QString), + QgsField("Datum zahájeni", QMetaType.Type.QString), + QgsField("Datum ukončení", QMetaType.Type.QString), + QgsField("Hlavní typ", QMetaType.Type.QString), + QgsField("Vedlejší typ", QMetaType.Type.QString), + QgsField("Zjištění", QMetaType.Type.QString), + QgsField("Akce – nahrazuje NZ", QMetaType.Type.QString), ] elif typ_dat == "lokalita": cols += [ - QgsField("nazev_lokality", QVariant.String), - QgsField("popis_lokality", QVariant.String), - QgsField("typ_lokality", QVariant.String), - QgsField("druh_lokality", QVariant.String), - QgsField("zachovalost", QVariant.String) + QgsField("nazev_lokality", QMetaType.Type.QString), + QgsField("popis_lokality", QMetaType.Type.QString), + QgsField("typ_lokality", QMetaType.Type.QString), + QgsField("druh_lokality", QMetaType.Type.QString), + QgsField("zachovalost", QMetaType.Type.QString) ] - cols.append(QgsField("Přístupnost", QVariant.String)) + cols.append(QgsField("Přístupnost", QMetaType.Type.QString)) # Use aliases for technical field names alias_map = { @@ -370,10 +371,10 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false") vl_komponenty = QgsVectorLayer("None", "AMCR Komponenty", "memory") pr = vl_komponenty.dataProvider() komponenty_cols = [ - QgsField("komponenta", QVariant.String), - QgsField("dj_id", QVariant.String), - QgsField("komponenta_areal", QVariant.String), - QgsField("komponenta_obdobi", QVariant.String) + QgsField("komponenta", QMetaType.Type.QString), + QgsField("dj_id", QMetaType.Type.QString), + QgsField("komponenta_areal", QMetaType.Type.QString), + QgsField("komponenta_obdobi", QMetaType.Type.QString) ] pr.addAttributes(komponenty_cols) vl_komponenty.updateFields() @@ -493,7 +494,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false") added += len(f) if added > 0: - iface.messageBar().pushMessage("AMCR", f"Hotovo. Záznamů: {len(docs)} (s geom: {actions_with_geom}). Vykresleno: {added} prvků.", level=0) + iface.messageBar().pushMessage("AMCR", f"Hotovo. Záznamů: {len(docs)} (s geom: {actions_with_geom}). Vykresleno: {added} prvků.", level=Qgis.MessageLevel.Success) # --- RELATIONSHIP MANAGEMENT --- # Set up automatic links between spatial layers and the component table @@ -518,10 +519,10 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false") print(f"Relace pro {label} není validní!") else: - iface.messageBar().pushMessage("AMCR", "Žádná data k zobrazení.", level=1) + iface.messageBar().pushMessage("AMCR", "Žádná data k zobrazení.", level=Qgis.MessageLevel.Info) except Exception as e: - iface.messageBar().pushMessage("Chyba", str(e), level=2) + iface.messageBar().pushMessage("Chyba", str(e), level=Qgis.MessageLevel.Critical) finally: # Always restore cursor, even after failure QApplication.restoreOverrideCursor() \ No newline at end of file diff --git a/amcr_viewer/amcr_viewer.py b/amcr_viewer/amcr_viewer.py index fdcefe3..fe91bc0 100644 --- a/amcr_viewer/amcr_viewer.py +++ b/amcr_viewer/amcr_viewer.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication from qgis.PyQt.QtGui import QIcon -from qgis.PyQt.QtWidgets import QMenu, QAction, QToolButton +from qgis.PyQt.QtWidgets import QMenu, QAction, QToolButton, QDialog from .amcr_tools import load_amcr_data from .amcr_dialog import AmcrFilterDialog @@ -120,7 +120,7 @@ class AmcrViewer: self.tool_button = QToolButton() self.tool_button.setMenu(self.plugin_menu) self.tool_button.setDefaultAction(self.action_download_akce) - self.tool_button.setPopupMode(QToolButton.MenuButtonPopup) + self.tool_button.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup) # Add the widget directly to the toolbar and store the reference for cleanup self.toolbar_action = self.iface.addToolBarWidget(self.tool_button) @@ -161,7 +161,7 @@ class AmcrViewer: result = dlg.exec() # If user confirmed the dialog (OK button), gather filters and load data - if result == 1: + if result == QDialog.DialogCode.Accepted: filters = dlg.get_filters() bbox = dlg.get_bbox() komponenty = dlg.get_komponenty() diff --git a/amcr_viewer/metadata.txt b/amcr_viewer/metadata.txt index 5e45734..bd262de 100644 --- a/amcr_viewer/metadata.txt +++ b/amcr_viewer/metadata.txt @@ -5,9 +5,10 @@ [general] name=AMČR Viewer -qgisMinimumVersion=3.4 +qgisMinimumVersion=3.4.0 +qgisMaximumVersion=4.9.9 description=Viewing and downloading the AMČR data. -version=1.2.0 +version=1.3.0 author=David Spáčil email=spacil@arub.cz diff --git a/amcr_viewer/resources.py b/amcr_viewer/resources.py index 214c24f..2d64ca9 100644 --- a/amcr_viewer/resources.py +++ b/amcr_viewer/resources.py @@ -6,7 +6,7 @@ # # WARNING! All changes made in this file will be lost! -from PyQt5 import QtCore +from qgis.PyQt import QtCore qt_resource_data = b"\ \x00\x00\x04\x0a\