|
8 | 8 |
|
9 | 9 | # standard library |
10 | 10 | import logging |
| 11 | +import threading |
11 | 12 | from functools import partial |
12 | 13 | from typing import Callable |
13 | 14 |
|
14 | 15 | # PyQGIS |
15 | 16 | from qgis.core import QgsMessageLog, QgsMessageOutput |
16 | 17 | from qgis.gui import QgsMessageBar |
| 18 | +from qgis.PyQt.QtCore import QCoreApplication, QObject, Qt, QThread, pyqtSignal, pyqtSlot |
17 | 19 | from qgis.PyQt.QtWidgets import QPushButton, QWidget |
18 | 20 | from qgis.utils import iface |
19 | 21 |
|
|
27 | 29 | # ################################## |
28 | 30 |
|
29 | 31 |
|
| 32 | +class _MainThreadInvoker(QObject): |
| 33 | + """Runs functions on the Qt main thread. |
| 34 | +
|
| 35 | + A signal emitted from a worker thread is delivered to a slot of an object |
| 36 | + that lives on the main thread through the main thread's event queue. |
| 37 | + """ |
| 38 | + |
| 39 | + invoke = pyqtSignal(object) |
| 40 | + |
| 41 | + def __init__(self): |
| 42 | + super().__init__() |
| 43 | + self.invoke.connect(self._run, Qt.ConnectionType.QueuedConnection) |
| 44 | + |
| 45 | + @pyqtSlot(object) |
| 46 | + def _run(self, func): |
| 47 | + func() |
| 48 | + |
| 49 | + |
| 50 | +_invoker = None |
| 51 | +_invoker_lock = threading.Lock() |
| 52 | + |
| 53 | + |
| 54 | +def _run_on_main_thread(func): |
| 55 | + """Run `func` on the Qt main thread: now if this is the main thread, else queued.""" |
| 56 | + global _invoker |
| 57 | + app = QCoreApplication.instance() |
| 58 | + if app is None or QThread.currentThread() == app.thread(): |
| 59 | + func() |
| 60 | + return |
| 61 | + with _invoker_lock: |
| 62 | + if _invoker is None: |
| 63 | + invoker = _MainThreadInvoker() |
| 64 | + # An object can only be moved from the thread it lives on, which |
| 65 | + # is this thread, because it was made here. |
| 66 | + invoker.moveToThread(app.thread()) |
| 67 | + _invoker = invoker |
| 68 | + _invoker.invoke.emit(func) |
| 69 | + |
| 70 | + |
30 | 71 | class PlgLogger(logging.Handler): |
31 | 72 | """Python logging handler supercharged with QGIS useful methods.""" |
32 | 73 |
|
@@ -146,30 +187,15 @@ def _do_push(): |
146 | 187 | except Exception: |
147 | 188 | logging.exception("Failed to push message to QGIS message bar") |
148 | 189 |
|
149 | | - # Try to schedule the UI interaction on the Qt main thread using QTimer |
| 190 | + # The message bar is a widget, so only the Qt main thread can change it. |
| 191 | + # QTimer.singleShot(0, func) does not do this: it runs func on the |
| 192 | + # calling thread, so a call from a background task changed the message |
| 193 | + # bar from the worker thread and locked the GUI. |
150 | 194 | try: |
151 | | - try: |
152 | | - from qgis.PyQt.QtCore import QTimer as _QTimer |
153 | | - except Exception: |
154 | | - # fall back to qgis.PyQt/PySide2 if qgis.PyQt namespace isn't present |
155 | | - try: |
156 | | - from qgis.PyQt.QtCore import QTimer as _QTimer # type: ignore |
157 | | - except Exception: |
158 | | - try: |
159 | | - from PySide2.QtCore import QTimer as _QTimer # type: ignore |
160 | | - except Exception: |
161 | | - _QTimer = None |
162 | | - if _QTimer is not None: |
163 | | - _QTimer.singleShot(0, _do_push) |
164 | | - else: |
165 | | - # last resort: call directly (may block if called from background thread) |
166 | | - _do_push() |
| 195 | + _run_on_main_thread(_do_push) |
167 | 196 | except Exception: |
168 | 197 | # ensure we do not raise from logging |
169 | | - try: |
170 | | - _do_push() |
171 | | - except Exception: |
172 | | - pass |
| 198 | + pass |
173 | 199 |
|
174 | 200 |
|
175 | 201 | class PlgLoggerHandler(logging.Handler): |
|
0 commit comments