forked from MerginMaps/qgis-plugin
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathplugin.py
More file actions
662 lines (581 loc) · 26.1 KB
/
Copy pathplugin.py
File metadata and controls
662 lines (581 loc) · 26.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
# -*- coding: utf-8 -*-
# GPLv3 license
# Copyright Lutra Consulting Limited
try:
import sip # noqa: F401
except ImportError:
pass
import os
from functools import partial
from qgis.PyQt.QtCore import QUrl, QSettings, Qt
from qgis.PyQt.QtGui import QIcon, QDesktopServices
from qgis.core import (
QgsApplication,
QgsExpressionContextUtils,
QgsProject,
QgsMapLayer,
Qgis,
)
from qgis.utils import iface
from qgis.PyQt.QtWidgets import QAction, QMessageBox, QDockWidget
from urllib.error import URLError
from .configuration_dialog import ConfigurationDialog
from .workspace_selection_dialog import WorkspaceSelectionDialog
from .project_selection_dialog import (
ProjectSelectionDialog,
PublicProjectSelectionDialog,
)
from .data_item import DataItemProvider
from .create_project_wizard import NewMerginProjectWizard
from .diff_dialog import DiffViewerDialog
from .project_settings_widget import MerginProjectConfigFactory
from .projects_manager import MerginProjectsManager
from .configure_sync_wizard import DbSyncConfigWizard
from .version_viewer_dialog import VersionViewerDialog
from .utils import (
ServerType,
ClientError,
LoginError,
InvalidProject,
check_mergin_subdirs,
icon_path,
mm_symbol_path,
mergin_project_local_path,
remove_project_variables,
set_qgis_project_mergin_variables,
unsaved_project_check,
UnsavedChangesStrategy,
)
from .utils_auth import (
create_mergin_client,
MissingAuthConfigError,
AuthTokenExpiredError,
set_qgsexpressionscontext,
get_authcfg,
AuthSync,
setup_qgis_ssl_for_mergin_client,
)
from .mergin.merginproject import MerginProject
from .processing.provider import MerginProvider
import processing
MERGIN_CLIENT_LOG = os.path.join(QgsApplication.qgisSettingsDirPath(), "mergin-client-log.txt")
os.environ["MERGIN_CLIENT_LOG"] = MERGIN_CLIENT_LOG
class MerginPlugin:
def __init__(self, iface):
self.iface = iface
self.plugin_dir = os.path.dirname(__file__)
self.data_item_provider = None
self.actions = []
self.actions_always_on = []
self.menu = "Mergin Maps"
self.mergin_proj_dir = None
self.mc = None
self.manager = None
# current_workspace is a dict with "name" and "id" keys, empty dict() if the server does not support workspaces
self.current_workspace = dict()
self.provider = MerginProvider()
if self.iface is not None:
self.toolbar = self.iface.addToolBar("Mergin Maps Toolbar")
self.toolbar.setToolTip("Mergin Maps Toolbar")
self.toolbar.setObjectName("MerginMapsToolbar")
self.iface.projectRead.connect(self.on_qgis_project_changed)
self.on_qgis_project_changed()
self.iface.newProjectCreated.connect(self.on_qgis_project_changed)
settings = QSettings()
QgsExpressionContextUtils.setGlobalVariable("mergin_username", settings.value("Mergin/username", ""))
QgsExpressionContextUtils.setGlobalVariable("mm_username", settings.value("Mergin/username", ""))
QgsExpressionContextUtils.setGlobalVariable("mergin_url", settings.value("Mergin/server", ""))
QgsExpressionContextUtils.setGlobalVariable("mm_url", settings.value("Mergin/server", ""))
QgsExpressionContextUtils.setGlobalVariable("mergin_full_name", settings.value("Mergin/full_name", ""))
QgsExpressionContextUtils.setGlobalVariable("mm_full_name", settings.value("Mergin/full_name", ""))
QgsExpressionContextUtils.setGlobalVariable("mergin_user_email", settings.value("Mergin/user_email", ""))
QgsExpressionContextUtils.setGlobalVariable("mm_user_email", settings.value("Mergin/user_email", ""))
def initProcessing(self):
QgsApplication.processingRegistry().addProvider(self.provider)
def initGui(self):
# This is a quick fix for a bad crasher for users that have set up master password for their
# storage of authentication configurations. What would happen is that in a worker thread,
# QGIS browser model would start populating Mergin data items which would want to query Mergin
# server and thus request auth info - but as this would be done in a background thread,
# things will get horribly wrong when QGIS tries to display GUI and the app would crash.
# Triggering auth request to QGIS auth framework already at this point will make sure that
# the dialog asking for master password is started from the main thread -> no crash.
self.initProcessing()
try:
setup_qgis_ssl_for_mergin_client()
except Exception as e:
QgsApplication.messageLog().logMessage(f"Mergin Maps plugin: failed to set up SSL certificates: {e}")
if self.iface is not None:
self.add_action(
mm_symbol_path(),
text="Mergin Maps",
callback=self.open_configured_url,
add_to_menu=True,
add_to_toolbar=self.toolbar,
)
self.add_action(
"settings.svg",
text="Configure Mergin Maps Plugin",
callback=self.configure,
add_to_menu=True,
add_to_toolbar=self.toolbar,
)
self.add_action(
"square-plus.svg",
text="Create Mergin Maps Project",
callback=self.create_new_project,
add_to_menu=False,
add_to_toolbar=self.toolbar,
enabled=False,
always_on=False,
)
self.add_action(
"refresh.svg",
text="Synchronise Mergin Maps Project",
callback=self.current_project_sync,
add_to_menu=False,
add_to_toolbar=self.toolbar,
enabled=False,
always_on=False,
)
self.action_db_sync_wizard = self.add_action(
"database-cog.svg",
text="Configure DB sync",
callback=self.configure_db_sync,
add_to_menu=True,
add_to_toolbar=None,
)
self.add_action(
"history.svg",
text="Project History",
callback=self.open_project_history_window,
add_to_menu=False,
add_to_toolbar=self.toolbar,
enabled=False,
always_on=False,
)
self.enable_toolbar_actions()
self.data_item_provider = DataItemProvider(self)
QgsApplication.instance().dataItemProviderRegistry().addProvider(self.data_item_provider)
# related to https://github.com/MerginMaps/qgis-mergin-plugin/issues/3
# if self.iface.browserModel().initialized():
# self.iface.browserModel().reload()
# create manager based on status of QGIS
# if main window is visible, we can create the manager immediately - QGIS is already initialized
# if not, we need to wait for initializationCompleted signal so that QGIS is fully initialized
if self.iface.mainWindow().isVisible():
self.create_manager()
else:
self.iface.initializationCompleted.connect(self.create_manager)
if self.iface is not None:
# register custom mergin widget in project properties
self.mergin_project_config_factory = MerginProjectConfigFactory()
self.iface.registerProjectPropertiesWidgetFactory(self.mergin_project_config_factory)
# add layer context menu action for checking local changes
self.action_show_changes = QAction("Show Local Changes", self.iface.mainWindow())
self.action_show_changes.setIcon(QIcon(icon_path("file-diff.svg")))
self.iface.addCustomActionForLayerType(self.action_show_changes, "", QgsMapLayer.VectorLayer, True)
self.action_show_changes.triggered.connect(self.view_local_changes)
# add layer context menu action for downloading vector tiles
self.action_export_mbtiles = QAction("Make available offline…", self.iface.mainWindow())
self.action_export_mbtiles.setIcon(QIcon(icon_path("file-export.svg")))
self.iface.addCustomActionForLayerType(self.action_export_mbtiles, "", QgsMapLayer.VectorTileLayer, False)
self.action_export_mbtiles.triggered.connect(self.export_vector_tiles)
QgsProject.instance().layersAdded.connect(self.add_context_menu_actions)
def add_action(
self,
icon_name,
callback=None,
text="",
enabled=True,
add_to_menu=False,
add_to_toolbar=None,
status_tip=None,
whats_this=None,
checkable=False,
checked=False,
always_on=True,
):
icon = QIcon(icon_path(icon_name))
action = QAction(icon, text, self.iface.mainWindow())
action.triggered.connect(callback)
action.setCheckable(checkable)
action.setChecked(checked)
action.setEnabled(enabled)
if status_tip is not None:
action.setStatusTip(status_tip)
if whats_this is not None:
action.setWhatsThis(whats_this)
if add_to_toolbar is not None:
add_to_toolbar.addAction(action)
if add_to_menu:
self.iface.addPluginToMenu(self.menu, action)
self.actions.append(action)
if always_on:
self.actions_always_on.append(text)
return action
def create_manager(self):
"""Create Mergin Maps projects manager."""
error = ""
try:
if self.mc is None and get_authcfg():
try:
self.mc = create_mergin_client()
# if the client creation fails with AuthTokenExpiredError, we need relogin user - it should only happen for SSO
except AuthTokenExpiredError:
self.auth_token_expired()
return
if self.mc is not None:
self.choose_active_workspace()
self.manager = MerginProjectsManager(self)
else:
error = "Configure the Mergin Maps plugin \nto access your projects"
except (URLError, ClientError, LoginError):
error = "Plugin not configured or QGIS master password not set up"
except Exception as err:
error = "Error: {}".format(str(err))
if error:
self.mc = None
self.manager = None
if self.has_browser_item():
self.data_item_provider.root_item.update_client_and_manager(mc=self.mc, manager=self.manager, err=error)
def has_browser_item(self):
"""Check if the Mergin Maps provider Browser item exists and has the root item defined."""
if self.data_item_provider is not None:
if self.data_item_provider.root_item is not None:
return True
return False
def on_config_changed(self):
"""Called when plugin config (connection settings) were changed."""
self.create_manager()
self.enable_toolbar_actions()
def open_configured_url(self, path=None):
"""Opens configured Mergin Maps server url in default browser
Use optional parameter path to go directly to a specific page, eg. /workspaces
"""
if self.mc is None:
url = QUrl("https://merginmaps.com")
else:
url = QUrl(self.mc.url)
if path:
url_path = url.path()
while url_path.endswith("/"):
url_path = url_path[:-1]
url.setPath(f"{url_path}{path}")
QDesktopServices.openUrl(url)
def enable_toolbar_actions(self, enable=None):
"""Check current project and set Mergin Maps Toolbar icons enabled accordingly."""
if enable is None:
enable = mergin_project_local_path() is not None
if self.manager is None:
enable = False
for action in self.toolbar.actions():
if action.text() in self.actions_always_on:
action.setEnabled(True)
elif action.text() == "Create Mergin Maps Project":
action.setEnabled(self.mc is not None and self.manager is not None)
else:
action.setEnabled(enable)
def show_browser_panel(self):
"""Check if QGIS Browser panel is open. If not, ask and eventually make it visible to users."""
browser = [w for w in self.iface.mainWindow().findChildren(QDockWidget) if w.objectName() == "Browser"][0]
q = "QGIS Browser panel is currently off. The panel is used for Mergin Maps projects management.\n"
q += "Would you like to open it and see your Mergin projects?"
if not browser.isVisible():
res = QMessageBox.question(None, "Mergin Maps - QGIS Browser Panel", q)
if res == QMessageBox.StandardButton.Yes:
self.iface.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, browser)
def configure(self):
"""Open plugin configuration dialog."""
dlg = ConfigurationDialog()
if dlg.exec():
try:
self.mc = create_mergin_client()
set_qgsexpressionscontext(dlg.server_url(), mc=self.mc)
except (
MissingAuthConfigError,
AuthTokenExpiredError,
ClientError,
ValueError,
) as e:
QMessageBox.critical(None, "Login failed", f"Could not login: {str(e)}")
set_qgsexpressionscontext(dlg.server_url(), mc=None)
return
self.on_config_changed()
self.show_browser_panel()
def configure_db_sync(self):
"""Open db-sync setup wizard."""
project_path = QgsProject.instance().homePath()
if not project_path:
iface.messageBar().pushMessage(
"Mergin",
"Project is not saved, please save project first",
Qgis.Warning,
)
return
if not check_mergin_subdirs(project_path):
iface.messageBar().pushMessage(
"Mergin",
"Current project is not a Mergin project. Please open a Mergin project first.",
Qgis.Warning,
)
return
mp = MerginProject(project_path)
try:
project_name = mp.project_full_name()
except InvalidProject:
iface.messageBar().pushMessage(
"Mergin",
"Current project is not a Mergin project. Please open a Mergin project first.",
Qgis.Warning,
)
return
wizard = DbSyncConfigWizard(project_name)
if not wizard.exec():
return
def open_project_history_window(self):
dlg = VersionViewerDialog(self)
dlg.exec()
def show_no_workspaces_dialog(self):
msg = (
"Workspace is a place to store your projects and share them with your colleagues. "
"Click on the button below to create one. \n\n"
"A minimum of one workspace is required to use Mergin Maps."
)
msg_box = QMessageBox(
QMessageBox.Icon.Critical,
"You do not have any workspace",
msg,
QMessageBox.StandardButton.Close,
)
create_button = msg_box.addButton("Create workspace", msg_box.ActionRole)
create_button.clicked.disconnect()
create_button.clicked.connect(partial(self.open_configured_url, "/workspaces"))
msg_box.exec()
def set_current_workspace(self, workspace):
"""
Sets the current workspace
:param workspace: Dict containing workspace's "name" and "id" keys
"""
settings = QSettings()
self.current_workspace = workspace
workspace_id = self.current_workspace.get("id", None)
settings.setValue("Mergin/lastUsedWorkspaceId", workspace_id)
if self.has_browser_item():
self.data_item_provider.root_item.update_client_and_manager(mc=self.mc, manager=self.manager)
if self.mc.server_type() == ServerType.SAAS and workspace_id:
# check action required flag
try:
service_response = self.mc.workspace_service(workspace_id)
except ClientError:
return
except AuthTokenExpiredError:
self.auth_token_expired()
return
requires_action = service_response.get("action_required", False)
if requires_action:
iface.messageBar().pushMessage(
"Mergin Maps",
"Your attention is required. Please visit the "
f"<a href='{self.mc.url}/dashboard?utm_source=plugin&utm_medium=attention-required&workspace={workspace_id}'>"
"Mergin dashboard</a>",
level=Qgis.Critical,
duration=0,
)
def choose_active_workspace(self):
"""
Called after connecting to server.
Chooses and sets the current workspace based on workspace availability and last used workspace.
"""
user_info = self.mc.user_info()
workspaces = user_info.get("workspaces", None)
if not workspaces:
# User has no workspaces
self.show_no_workspaces_dialog()
self.current_workspace = dict()
return
if len(workspaces) == 1:
workspace = workspaces[0]
else:
settings = QSettings()
previous_workspace = settings.value("Mergin/lastUsedWorkspaceId", None, int)
workspace = None
for ws in workspaces:
if previous_workspace == ws["id"]:
workspace = ws
break
if not workspace:
for ws in workspaces:
if user_info["preferred_workspace"] == ws["id"]:
workspace = ws
break
self.set_current_workspace(workspace)
def create_new_project(self):
"""Open new Mergin Maps project creation dialog."""
check_result = unsaved_project_check()
if check_result == UnsavedChangesStrategy.HasUnsavedChanges:
return
if not self.manager:
QMessageBox.warning(None, "Create Mergin Maps Project", "Plugin not configured!")
return
user_info = self.mc.user_info()
workspaces = user_info.get("workspaces", None)
if not workspaces and workspaces is not None:
self.show_no_workspaces_dialog()
self.current_workspace = dict()
return
default_workspace = self.current_workspace.get("name", None)
wizard = NewMerginProjectWizard(self.manager, user_info=user_info, default_workspace=default_workspace)
if not wizard.exec():
return # cancelled
if self.has_browser_item():
# make sure the item has the link between remote and local project we have just added
self.data_item_provider.root_item.depopulate()
self.data_item_provider.root_item.reload()
def current_project_sync(self):
"""Synchronise current Mergin Maps project."""
AuthSync().export_auth(self.mc)
self.manager.project_status(self.mergin_proj_dir)
def find_project(self):
"""Open new Find Mergin Maps project dialog"""
dlg = ProjectSelectionDialog(self.mc, self.current_workspace.get("name", None))
dlg.new_project_clicked.connect(self.create_new_project)
dlg.switch_workspace_clicked.connect(self.switch_workspace)
dlg.open_project_clicked.connect(self.manager.open_project)
dlg.download_project_clicked.connect(self.manager.download_project)
try:
workspaces = self.mc.workspaces_list()
dlg.enable_workspace_switching(len(workspaces) > 1)
except ClientError:
dlg.enable_workspace_switching(False)
dlg.exec()
def switch_workspace(self):
"""Open new Switch workspace dialog"""
try:
workspaces = self.mc.workspaces_list()
except (URLError, ClientError):
return # Server does not support workspaces
except AuthTokenExpiredError:
self.auth_token_expired()
return
if not workspaces:
self.show_no_workspaces_dialog()
self.current_workspace = dict()
return
dlg = WorkspaceSelectionDialog(workspaces)
dlg.manage_workspaces_clicked.connect(self.open_configured_url)
if not dlg.exec():
return
workspace = dlg.get_workspace()
self.set_current_workspace(workspace)
def explore_public_projects(self):
"""Open new Explore public Mergin Maps projects dialog"""
dlg = PublicProjectSelectionDialog(self.mc)
dlg.open_project_clicked.connect(self.manager.open_project)
dlg.download_project_clicked.connect(self.manager.download_project)
dlg.exec()
def on_qgis_project_changed(self):
"""
Called when QGIS project is created or (re)loaded. Sets QGIS project related Mergin Maps variables.
If a loaded project is not a Mergin Maps project, there are no Mergin variables by default.
If a loaded project is invalid - doesnt have metadata, Mergin variables are removed.
"""
self.enable_toolbar_actions(enable=False)
self.mergin_proj_dir = mergin_project_local_path()
if self.mergin_proj_dir is not None:
self.enable_toolbar_actions()
set_qgis_project_mergin_variables(self.mergin_proj_dir)
def add_context_menu_actions(self, layers):
provider_names = "vectortile"
if Qgis.versionInt() >= 33200:
provider_names = (
"xyzvectortiles",
"arcgisvectortileservice",
"vtpkvectortiles",
)
for layer in layers:
if layer.dataProvider().name() in provider_names:
self.iface.addCustomActionForLayer(self.action_export_mbtiles, layer)
def unload(self):
from .utils import pygeodiff
if self.iface is not None:
# Disconnect Mergin related signals
self.iface.projectRead.disconnect(self.on_qgis_project_changed)
self.iface.newProjectCreated.disconnect(self.on_qgis_project_changed)
for action in self.actions:
self.iface.removePluginMenu(self.menu, action)
self.iface.removeToolBarIcon(action)
del self.toolbar
self.iface.removeCustomActionForLayerType(self.action_show_changes)
self.iface.unregisterProjectPropertiesWidgetFactory(self.mergin_project_config_factory)
remove_project_variables()
QgsExpressionContextUtils.removeGlobalVariable("mergin_username")
QgsExpressionContextUtils.removeGlobalVariable("mm_username")
QgsExpressionContextUtils.removeGlobalVariable("mergin_url")
QgsExpressionContextUtils.removeGlobalVariable("mm_url")
QgsExpressionContextUtils.removeGlobalVariable("mergin_full_name")
QgsExpressionContextUtils.removeGlobalVariable("mm_full_name")
QgsExpressionContextUtils.removeGlobalVariable("mergin_user_email")
QgsExpressionContextUtils.removeGlobalVariable("mm_user_email")
QgsApplication.instance().dataItemProviderRegistry().removeProvider(self.data_item_provider)
self.data_item_provider = None
# unload pygeodiff to avoid .pyd to be write-protected and thus impossible to delete on Windows
pygeodiff.shutdown()
# this is crashing qgis on exit
# self.iface.browserModel().reload()
QgsApplication.processingRegistry().removeProvider(self.provider)
def view_local_changes(self):
project_path = QgsProject.instance().homePath()
if not project_path:
iface.messageBar().pushMessage(
"Mergin",
"Project is not saved, can not compute local changes",
Qgis.Warning,
)
return
if not check_mergin_subdirs(project_path):
iface.messageBar().pushMessage("Mergin", "Current project is not a Mergin project.", Qgis.Warning)
return
check_result = unsaved_project_check()
if check_result == UnsavedChangesStrategy.HasUnsavedChanges:
return
mp = MerginProject(QgsProject.instance().homePath())
push_changes = mp.get_push_changes()
push_changes_summary = mp.get_list_of_push_changes(push_changes)
if not push_changes_summary:
iface.messageBar().pushMessage("Mergin", "No changes found in the project layers.", Qgis.Info)
return
selected_layers = self.iface.layerTreeView().selectedLayersRecursive()
layer_name = None
for layer in selected_layers:
if layer.type() != QgsMapLayer.VectorLayer:
continue
if layer.dataProvider().storageType() == "GPKG":
layer_name = layer.name()
break
dlg_diff_viewer = DiffViewerDialog()
if check_result == UnsavedChangesStrategy.HasUnsavedChangesButIgnore:
dlg_diff_viewer.show_unsaved_changes_warning()
if layer_name:
for i, layer in enumerate(dlg_diff_viewer.diff_layers):
if layer_name in layer.name():
dlg_diff_viewer.tab_bar.setCurrentIndex(i)
break
dlg_diff_viewer.show()
dlg_diff_viewer.exec()
def export_vector_tiles(self):
selected_layers = self.iface.layerTreeView().selectedLayersRecursive()
params = {}
for layer in selected_layers:
if layer.type() != QgsMapLayer.VectorTileLayer:
continue
params["INPUT"] = layer
break
processing.execAlgorithmDialog("mergin:downloadvectortiles", params)
def auth_token_expired(self):
QMessageBox.information(
self.iface.mainWindow(),
"SSO login has expired",
"Your SSO login has expired. To access your remote projects and be able to synchronize, you need to log in again.",
)
self.configure()