Oprava pádů: chybějící locale při startu a životní cyklus úlohy aktualizace heslářů

- amcr_viewer.py: QSettings().value('locale/userLocale') může být None na
  čisté instalaci QGIS – ošetřen fallback na 'en', jinak plugin spadne
  na TypeError hned při načtení.
- amcr_viewer.py: odstraněn nepoužívaný hvězdičkový import resources.
- amcr_dialog.py: úloha aktualizace heslářů se drží v modulové referenci,
  aby GC neuklidil Python wrapper QgsTask před dokončením (známá příčina
  pádů QGIS).
- amcr_dialog.py: tlačítko aktualizace se po spuštění zakáže – nelze už
  spustit několik stahování paralelně přepisujících heslar.csv.
- amcr_dialog.py: závěrečné QMessageBoxy se parentují na hlavní okno QGIS
  místo dialogu, který může být v době dokončení úlohy už zavřený
  (smazaný C++ objekt → pád).
This commit is contained in:
Claude
2026-06-12 08:26:44 +00:00
committed by David Spáčil
parent d417a78b85
commit d9f5d2ae6e
2 changed files with 38 additions and 13 deletions
+35 -10
View File
@@ -7,6 +7,7 @@ from qgis.PyQt.QtWidgets import (QDialog, QVBoxLayout,
from qgis.PyQt.QtCore import Qt, QSettings from qgis.PyQt.QtCore import Qt, QSettings
from qgis.core import (QgsTask, QgsApplication, from qgis.core import (QgsTask, QgsApplication,
QgsMessageLog, Qgis, QgsAuthMethodConfig) QgsMessageLog, Qgis, QgsAuthMethodConfig)
from qgis.utils import iface
from .amcr_codelists import (OBDOBI, TYP_AKCE, KRAJE, AREAL, ORGANIZACE, from .amcr_codelists import (OBDOBI, TYP_AKCE, KRAJE, AREAL, ORGANIZACE,
OKRESY, KATASTRY, VEDOUCI, PIAN_PRESNOST, OKRESY, KATASTRY, VEDOUCI, PIAN_PRESNOST,
TYP_LOKALITY, DRUH_LOKALITY, JISTOTA, TYP_LOKALITY, DRUH_LOKALITY, JISTOTA,
@@ -14,6 +15,12 @@ from .amcr_codelists import (OBDOBI, TYP_AKCE, KRAJE, AREAL, ORGANIZACE,
download_heslare, refresh_globals) download_heslare, refresh_globals)
# Keep Python references to running tasks. QgsTaskManager only holds the
# C++ object; without a Python-side reference the wrapper can be garbage
# collected before the task finishes, which crashes QGIS.
_ACTIVE_TASKS = []
class UpdateCodelistsTask(QgsTask): class UpdateCodelistsTask(QgsTask):
def __init__(self, description): def __init__(self, description):
super().__init__(description, QgsTask.CanCancel) super().__init__(description, QgsTask.CanCancel)
@@ -370,21 +377,38 @@ class AmcrFilterDialog(QDialog):
return row_widget return row_widget
def action_update_heslare(self): def action_update_heslare(self):
# Create the task instance # Create the task instance and keep a reference so the Python
# wrapper survives until the task finishes
task = UpdateCodelistsTask("Aktualizace heslářů AMČR") task = UpdateCodelistsTask("Aktualizace heslářů AMČR")
_ACTIVE_TASKS.append(task)
# Re-enable the button regardless of the outcome # Prevent parallel downloads overwriting heslar.csv
task.taskCompleted.connect(lambda: self.btn_update.setEnabled(True)) self.btn_update.setEnabled(False)
task.taskTerminated.connect(lambda: self.btn_update.setEnabled(True))
task.taskCompleted.connect(lambda: QMessageBox.information( # Message boxes are parented to the main window, not to this dialog
self, # the dialog may already be closed (and its C++ object deleted)
"Hotovo", # by the time the minute-long task finishes.
"Hesláře byly úspěšně aktualizovány." parent_win = iface.mainWindow() if iface else None
))
def _cleanup():
if task in _ACTIVE_TASKS:
_ACTIVE_TASKS.remove(task)
try:
self.btn_update.setEnabled(True)
except RuntimeError:
pass # dialog already closed
def on_completed():
_cleanup()
QMessageBox.information(
parent_win,
"Hotovo",
"Hesláře byly úspěšně aktualizovány."
)
# Show the exact error if the task fails # Show the exact error if the task fails
def on_error(): def on_error():
_cleanup()
if task.exception: if task.exception:
# This will show exactly what went wrong (e.g. PermissionError) # This will show exactly what went wrong (e.g. PermissionError)
msg = ( msg = (
@@ -393,8 +417,9 @@ class AmcrFilterDialog(QDialog):
) )
else: else:
msg = "Aktualizace byla zrušena uživatelem." msg = "Aktualizace byla zrušena uživatelem."
QMessageBox.warning(self, "Chyba / Zrušeno", msg) QMessageBox.warning(parent_win, "Chyba / Zrušeno", msg)
task.taskCompleted.connect(on_completed)
task.taskTerminated.connect(on_error) task.taskTerminated.connect(on_error)
QgsApplication.taskManager().addTask(task) QgsApplication.taskManager().addTask(task)
+3 -3
View File
@@ -6,7 +6,6 @@ from qgis.core import Qgis
from .amcr_tools import load_amcr_data, login_to_api from .amcr_tools import load_amcr_data, login_to_api
from .amcr_dialog import AmcrFilterDialog, LoginDialog from .amcr_dialog import AmcrFilterDialog, LoginDialog
from .resources import *
import os.path import os.path
@@ -24,8 +23,9 @@ class AmcrViewer:
self.iface = iface self.iface = iface
self.plugin_dir = os.path.dirname(__file__) self.plugin_dir = os.path.dirname(__file__)
# Determine the user's locale to load appropriate translation files # Determine the user's locale to load appropriate translation files.
locale = QSettings().value('locale/userLocale')[0:2] # The setting may be missing (None) on a fresh QGIS install.
locale = str(QSettings().value('locale/userLocale') or 'en')[0:2]
locale_path = os.path.join( locale_path = os.path.join(
self.plugin_dir, self.plugin_dir,
'i18n', 'i18n',