1
0

Merge pull request #9759 from accumulator/common_taskthread

qt,qml: move TaskThread to common_qt
This commit is contained in:
accumulator
2025-04-25 09:37:56 +02:00
committed by GitHub
9 changed files with 95 additions and 163 deletions

View File

@@ -1,16 +1,19 @@
from typing import Optional import queue
import sys
from typing import Optional, NamedTuple, Callable
import os.path import os.path
from PyQt6 import QtGui from PyQt6 import QtGui
from PyQt6.QtCore import Qt from PyQt6.QtCore import Qt, QThread, pyqtSignal
from PyQt6.QtGui import QColor, QPen, QPaintDevice, QFontDatabase, QImage from PyQt6.QtGui import QColor, QPen, QPaintDevice, QFontDatabase, QImage
import qrcode import qrcode
from electrum.i18n import _ from electrum.i18n import _
from electrum.logging import Logger
_cached_font_ids: dict[str, int] = {} _cached_font_ids: dict[str, int] = {}
def get_font_id(filename: str) -> int: def get_font_id(filename: str) -> int:
font_id = _cached_font_ids.get(filename) font_id = _cached_font_ids.get(filename)
if font_id is not None: if font_id is not None:
@@ -22,6 +25,7 @@ def get_font_id(filename: str) -> int:
_cached_font_ids[filename] = font_id _cached_font_ids[filename] = font_id
return font_id return font_id
def draw_qr( def draw_qr(
*, *,
qr: Optional[qrcode.main.QRCode], qr: Optional[qrcode.main.QRCode],
@@ -116,3 +120,73 @@ def paintQR(data) -> Optional[QImage]:
return base_img return base_img
class TaskThread(QThread, Logger):
"""Thread that runs background tasks. Callbacks are guaranteed
to happen in the context of its parent."""
class Task(NamedTuple):
task: Callable
cb_success: Optional[Callable]
cb_done: Optional[Callable]
cb_error: Optional[Callable]
cancel: Optional[Callable] = None
doneSig = pyqtSignal(object, object, object)
def __init__(self, parent, on_error=None):
QThread.__init__(self, parent)
Logger.__init__(self)
self.on_error = on_error
self.tasks = queue.Queue()
self._cur_task = None # type: Optional[TaskThread.Task]
self._stopping = False
self.doneSig.connect(self.on_done)
self.start()
def add(self, task, on_success=None, on_done=None, on_error=None, *, cancel=None):
if self._stopping:
self.logger.warning(f"stopping or already stopped but tried to add new task.")
return
on_error = on_error or self.on_error
task_ = TaskThread.Task(task, on_success, on_done, on_error, cancel=cancel)
self.tasks.put(task_)
def run(self):
while True:
if self._stopping:
break
task = self.tasks.get() # type: TaskThread.Task
self._cur_task = task
if not task or self._stopping:
break
try:
result = task.task()
self.doneSig.emit(result, task.cb_done, task.cb_success)
except BaseException:
self.doneSig.emit(sys.exc_info(), task.cb_done, task.cb_error)
def on_done(self, result, cb_done, cb_result):
# This runs in the parent's thread.
if cb_done:
cb_done()
if cb_result:
cb_result(result)
def stop(self):
self._stopping = True
# try to cancel currently running task now.
# if the task does not implement "cancel", we will have to wait until it finishes.
task = self._cur_task
if task and task.cancel:
task.cancel()
# cancel the remaining tasks in the queue
while True:
try:
task = self.tasks.get_nowait()
except queue.Empty:
break
if task and task.cancel:
task.cancel()
self.tasks.put(None) # in case the thread is still waiting on the queue
self.exit()
self.wait()

View File

@@ -11,7 +11,7 @@ from electrum.bip39_recovery import account_discovery
from electrum.logging import get_logger from electrum.logging import get_logger
from electrum.util import get_asyncio_loop from electrum.util import get_asyncio_loop
from .util import TaskThread from electrum.gui.common_qt.util import TaskThread
class QEBip39RecoveryListModel(QAbstractListModel): class QEBip39RecoveryListModel(QAbstractListModel):

View File

@@ -1,16 +1,13 @@
import math import math
import re import re
import sys
import queue
from functools import wraps from functools import wraps
from time import time from time import time
from typing import Callable, Optional, NamedTuple, Tuple from typing import Tuple
from PyQt6.QtCore import pyqtSignal, QThread from PyQt6.QtCore import pyqtSignal
from electrum.i18n import _ from electrum.i18n import _
from electrum.logging import Logger
from electrum.util import EventListener, event_listener from electrum.util import EventListener, event_listener
@@ -73,76 +70,3 @@ def check_password_strength(password: str) -> Tuple[int, str]:
score = len(password)*(n + caps + num + extra)/20 score = len(password)*(n + caps + num + extra)/20
password_strength = {0: _('Weak'), 1: _('Medium'), 2: _('Strong'), 3: _('Very Strong')} password_strength = {0: _('Weak'), 1: _('Medium'), 2: _('Strong'), 3: _('Very Strong')}
return min(3, int(score)), password_strength[min(3, int(score))] return min(3, int(score)), password_strength[min(3, int(score))]
# TODO: copied from desktop client, this could be moved to a set of common code.
class TaskThread(QThread, Logger):
"""Thread that runs background tasks. Callbacks are guaranteed
to happen in the context of its parent."""
class Task(NamedTuple):
task: Callable
cb_success: Optional[Callable]
cb_done: Optional[Callable]
cb_error: Optional[Callable]
cancel: Optional[Callable] = None
doneSig = pyqtSignal(object, object, object)
def __init__(self, parent, on_error=None):
QThread.__init__(self, parent)
Logger.__init__(self)
self.on_error = on_error
self.tasks = queue.Queue()
self._cur_task = None # type: Optional[TaskThread.Task]
self._stopping = False
self.doneSig.connect(self.on_done)
self.start()
def add(self, task, on_success=None, on_done=None, on_error=None, *, cancel=None):
if self._stopping:
self.logger.warning(f"stopping or already stopped but tried to add new task.")
return
on_error = on_error or self.on_error
task_ = TaskThread.Task(task, on_success, on_done, on_error, cancel=cancel)
self.tasks.put(task_)
def run(self):
while True:
if self._stopping:
break
task = self.tasks.get() # type: TaskThread.Task
self._cur_task = task
if not task or self._stopping:
break
try:
result = task.task()
self.doneSig.emit(result, task.cb_done, task.cb_success)
except BaseException:
self.doneSig.emit(sys.exc_info(), task.cb_done, task.cb_error)
def on_done(self, result, cb_done, cb_result):
# This runs in the parent's thread.
if cb_done:
cb_done()
if cb_result:
cb_result(result)
def stop(self):
self._stopping = True
# try to cancel currently running task now.
# if the task does not implement "cancel", we will have to wait until it finishes.
task = self._cur_task
if task and task.cancel:
task.cancel()
# cancel the remaining tasks in the queue
while True:
try:
task = self.tasks.get_nowait()
except queue.Empty:
break
if task and task.cancel:
task.cancel()
self.tasks.put(None) # in case the thread is still waiting on the queue
self.exit()
self.wait()

View File

@@ -14,8 +14,9 @@ from electrum.bip39_recovery import account_discovery
from electrum.logging import get_logger from electrum.logging import get_logger
from electrum.util import get_asyncio_loop, UserFacingException from electrum.util import get_asyncio_loop, UserFacingException
from .util import WindowModalDialog, TaskThread, Buttons, CancelButton, OkButton from electrum.gui.common_qt.util import TaskThread
from .util import WindowModalDialog, Buttons, CancelButton, OkButton
_logger = get_logger(__name__) _logger = get_logger(__name__)

View File

@@ -83,7 +83,7 @@ from .transaction_dialog import show_transaction
from .fee_slider import FeeSlider, FeeComboBox from .fee_slider import FeeSlider, FeeComboBox
from .util import (read_QIcon, ColorScheme, text_dialog, icon_path, WaitingDialog, from .util import (read_QIcon, ColorScheme, text_dialog, icon_path, WaitingDialog,
WindowModalDialog, HelpLabel, Buttons, WindowModalDialog, HelpLabel, Buttons,
OkButton, InfoButton, WWLabel, TaskThread, CancelButton, OkButton, InfoButton, WWLabel, CancelButton,
CloseButton, MessageBoxMixin, EnterButton, import_meta_gui, export_meta_gui, CloseButton, MessageBoxMixin, EnterButton, import_meta_gui, export_meta_gui,
filename_field, address_field, char_width_in_lineedit, webopen, filename_field, address_field, char_width_in_lineedit, webopen,
TRANSACTION_FILE_EXTENSION_FILTER_ANY, MONOSPACE_FONT, TRANSACTION_FILE_EXTENSION_FILTER_ANY, MONOSPACE_FONT,
@@ -100,6 +100,8 @@ from .swap_dialog import SwapDialog, InvalidSwapParameters
from .balance_dialog import (BalanceToolButton, COLOR_FROZEN, COLOR_UNMATURED, COLOR_UNCONFIRMED, COLOR_CONFIRMED, from .balance_dialog import (BalanceToolButton, COLOR_FROZEN, COLOR_UNMATURED, COLOR_UNCONFIRMED, COLOR_CONFIRMED,
COLOR_LIGHTNING, COLOR_FROZEN_LIGHTNING) COLOR_LIGHTNING, COLOR_FROZEN_LIGHTNING)
from electrum.gui.common_qt.util import TaskThread
if TYPE_CHECKING: if TYPE_CHECKING:
from . import ElectrumGui from . import ElectrumGui

View File

@@ -12,7 +12,7 @@ from typing import (NamedTuple, Callable, Optional, TYPE_CHECKING, List, Any, Se
from PyQt6 import QtCore from PyQt6 import QtCore
from PyQt6.QtGui import (QFont, QColor, QCursor, QPixmap, QImage, from PyQt6.QtGui import (QFont, QColor, QCursor, QPixmap, QImage,
QPalette, QIcon, QFontMetrics, QPainter, QContextMenuEvent, QMovie) QPalette, QIcon, QFontMetrics, QPainter, QContextMenuEvent, QMovie)
from PyQt6.QtCore import (Qt, pyqtSignal, QCoreApplication, QThread, QSize, QRect, QPoint, QObject) from PyQt6.QtCore import (Qt, pyqtSignal, QCoreApplication, QSize, QRect, QPoint, QObject)
from PyQt6.QtWidgets import (QPushButton, QLabel, QMessageBox, QHBoxLayout, QVBoxLayout, QLineEdit, from PyQt6.QtWidgets import (QPushButton, QLabel, QMessageBox, QHBoxLayout, QVBoxLayout, QLineEdit,
QStyle, QDialog, QGroupBox, QButtonGroup, QRadioButton, QStyle, QDialog, QGroupBox, QButtonGroup, QRadioButton,
QFileDialog, QWidget, QToolButton, QPlainTextEdit, QApplication, QToolTip, QFileDialog, QWidget, QToolButton, QPlainTextEdit, QApplication, QToolTip,
@@ -24,9 +24,10 @@ from electrum.util import (FileImportFailed, FileExportFailed, resource_path, Ev
get_logger, UserCancelled, UserFacingException) get_logger, UserCancelled, UserFacingException)
from electrum.invoices import (PR_UNPAID, PR_PAID, PR_EXPIRED, PR_INFLIGHT, PR_UNKNOWN, PR_FAILED, PR_ROUTING, from electrum.invoices import (PR_UNPAID, PR_PAID, PR_EXPIRED, PR_INFLIGHT, PR_UNKNOWN, PR_FAILED, PR_ROUTING,
PR_UNCONFIRMED, PR_BROADCASTING, PR_BROADCAST) PR_UNCONFIRMED, PR_BROADCASTING, PR_BROADCAST)
from electrum.logging import Logger
from electrum.qrreader import MissingQrDetectionLib, QrCodeResult from electrum.qrreader import MissingQrDetectionLib, QrCodeResult
from electrum.gui.common_qt.util import TaskThread
if TYPE_CHECKING: if TYPE_CHECKING:
from .main_window import ElectrumWindow from .main_window import ElectrumWindow
from .paytoedit import PayToEdit from .paytoedit import PayToEdit
@@ -1079,78 +1080,6 @@ class PasswordLineEdit(QLineEdit):
super().clear() super().clear()
class TaskThread(QThread, Logger):
'''Thread that runs background tasks. Callbacks are guaranteed
to happen in the context of its parent.'''
class Task(NamedTuple):
task: Callable
cb_success: Optional[Callable]
cb_done: Optional[Callable]
cb_error: Optional[Callable]
cancel: Optional[Callable] = None
doneSig = pyqtSignal(object, object, object)
def __init__(self, parent, on_error=None):
QThread.__init__(self, parent)
Logger.__init__(self)
self.on_error = on_error
self.tasks = queue.Queue()
self._cur_task = None # type: Optional[TaskThread.Task]
self._stopping = False
self.doneSig.connect(self.on_done)
self.start()
def add(self, task, on_success=None, on_done=None, on_error=None, *, cancel=None):
if self._stopping:
self.logger.warning(f"stopping or already stopped but tried to add new task.")
return
on_error = on_error or self.on_error
task_ = TaskThread.Task(task, on_success, on_done, on_error, cancel=cancel)
self.tasks.put(task_)
def run(self):
while True:
if self._stopping:
break
task = self.tasks.get() # type: TaskThread.Task
self._cur_task = task
if not task or self._stopping:
break
try:
result = task.task()
self.doneSig.emit(result, task.cb_done, task.cb_success)
except BaseException:
self.doneSig.emit(sys.exc_info(), task.cb_done, task.cb_error)
def on_done(self, result, cb_done, cb_result):
# This runs in the parent's thread.
if cb_done:
cb_done()
if cb_result:
cb_result(result)
def stop(self):
self._stopping = True
# try to cancel currently running task now.
# if the task does not implement "cancel", we will have to wait until it finishes.
task = self._cur_task
if task and task.cancel:
task.cancel()
# cancel the remaining tasks in the queue
while True:
try:
task = self.tasks.get_nowait()
except queue.Empty:
break
if task and task.cancel:
task.cancel()
self.tasks.put(None) # in case the thread is still waiting on the queue
self.exit()
self.wait()
class ColorSchemeItem: class ColorSchemeItem:
def __init__(self, fg_color, bg_color): def __init__(self, fg_color, bg_color):
self.colors = (fg_color, bg_color) self.colors = (fg_color, bg_color)

View File

@@ -31,9 +31,10 @@ from typing import TYPE_CHECKING, Union, Optional, Sequence, Tuple
from PyQt6.QtCore import QObject, pyqtSignal, Qt from PyQt6.QtCore import QObject, pyqtSignal, Qt
from PyQt6.QtWidgets import QVBoxLayout, QLineEdit, QHBoxLayout, QLabel from PyQt6.QtWidgets import QVBoxLayout, QLineEdit, QHBoxLayout, QLabel
from electrum.gui.common_qt.util import TaskThread
from electrum.gui.qt.password_dialog import PasswordLayout, PW_PASSPHRASE from electrum.gui.qt.password_dialog import PasswordLayout, PW_PASSPHRASE
from electrum.gui.qt.util import (read_QIcon, WWLabel, OkButton, WindowModalDialog, from electrum.gui.qt.util import (read_QIcon, WWLabel, OkButton, WindowModalDialog,
Buttons, CancelButton, TaskThread, char_width_in_lineedit, Buttons, CancelButton, char_width_in_lineedit,
PasswordLineEdit) PasswordLineEdit)
from electrum.gui.qt.main_window import StatusBarButton from electrum.gui.qt.main_window import StatusBarButton
from electrum.gui.qt.util import read_QIcon_from_bytes from electrum.gui.qt.util import read_QIcon_from_bytes

View File

@@ -54,14 +54,13 @@ from .plugin import run_hook
from .logging import Logger from .logging import Logger
if TYPE_CHECKING: if TYPE_CHECKING:
from .gui.qt.util import TaskThread from .gui.common_qt.util import TaskThread
from .plugins.hw_wallet import HW_PluginBase, HardwareClientBase, HardwareHandlerBase from .hw_wallet import HW_PluginBase, HardwareClientBase, HardwareHandlerBase
from .wallet_db import WalletDB from .wallet_db import WalletDB
from .plugin import Device from .plugin import Device
class CannotDerivePubkey(Exception): pass class CannotDerivePubkey(Exception): pass
class ScriptTypeNotSupported(Exception): pass class ScriptTypeNotSupported(Exception): pass

View File

@@ -5,7 +5,9 @@ from PyQt6.QtCore import QObject, pyqtSignal
from electrum.plugin import hook from electrum.plugin import hook
from electrum.i18n import _ from electrum.i18n import _
from electrum.gui.qt.util import TaskThread, read_QIcon_from_bytes
from electrum.gui.common_qt.util import TaskThread
from electrum.gui.qt.util import read_QIcon_from_bytes
from .labels import LabelsPlugin from .labels import LabelsPlugin