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
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
import qrcode
from electrum.i18n import _
from electrum.logging import Logger
_cached_font_ids: dict[str, int] = {}
def get_font_id(filename: str) -> int:
font_id = _cached_font_ids.get(filename)
if font_id is not None:
@@ -22,6 +25,7 @@ def get_font_id(filename: str) -> int:
_cached_font_ids[filename] = font_id
return font_id
def draw_qr(
*,
qr: Optional[qrcode.main.QRCode],
@@ -116,3 +120,73 @@ def paintQR(data) -> Optional[QImage]:
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.util import get_asyncio_loop
from .util import TaskThread
from electrum.gui.common_qt.util import TaskThread
class QEBip39RecoveryListModel(QAbstractListModel):

View File

@@ -1,16 +1,13 @@
import math
import re
import sys
import queue
from functools import wraps
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.logging import Logger
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
password_strength = {0: _('Weak'), 1: _('Medium'), 2: _('Strong'), 3: _('Very Strong')}
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.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__)

View File

@@ -83,7 +83,7 @@ from .transaction_dialog import show_transaction
from .fee_slider import FeeSlider, FeeComboBox
from .util import (read_QIcon, ColorScheme, text_dialog, icon_path, WaitingDialog,
WindowModalDialog, HelpLabel, Buttons,
OkButton, InfoButton, WWLabel, TaskThread, CancelButton,
OkButton, InfoButton, WWLabel, CancelButton,
CloseButton, MessageBoxMixin, EnterButton, import_meta_gui, export_meta_gui,
filename_field, address_field, char_width_in_lineedit, webopen,
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,
COLOR_LIGHTNING, COLOR_FROZEN_LIGHTNING)
from electrum.gui.common_qt.util import TaskThread
if TYPE_CHECKING:
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.QtGui import (QFont, QColor, QCursor, QPixmap, QImage,
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,
QStyle, QDialog, QGroupBox, QButtonGroup, QRadioButton,
QFileDialog, QWidget, QToolButton, QPlainTextEdit, QApplication, QToolTip,
@@ -24,9 +24,10 @@ from electrum.util import (FileImportFailed, FileExportFailed, resource_path, Ev
get_logger, UserCancelled, UserFacingException)
from electrum.invoices import (PR_UNPAID, PR_PAID, PR_EXPIRED, PR_INFLIGHT, PR_UNKNOWN, PR_FAILED, PR_ROUTING,
PR_UNCONFIRMED, PR_BROADCASTING, PR_BROADCAST)
from electrum.logging import Logger
from electrum.qrreader import MissingQrDetectionLib, QrCodeResult
from electrum.gui.common_qt.util import TaskThread
if TYPE_CHECKING:
from .main_window import ElectrumWindow
from .paytoedit import PayToEdit
@@ -1079,78 +1080,6 @@ class PasswordLineEdit(QLineEdit):
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:
def __init__(self, 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.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.util import (read_QIcon, WWLabel, OkButton, WindowModalDialog,
Buttons, CancelButton, TaskThread, char_width_in_lineedit,
Buttons, CancelButton, char_width_in_lineedit,
PasswordLineEdit)
from electrum.gui.qt.main_window import StatusBarButton
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
if TYPE_CHECKING:
from .gui.qt.util import TaskThread
from .plugins.hw_wallet import HW_PluginBase, HardwareClientBase, HardwareHandlerBase
from .gui.common_qt.util import TaskThread
from .hw_wallet import HW_PluginBase, HardwareClientBase, HardwareHandlerBase
from .wallet_db import WalletDB
from .plugin import Device
class CannotDerivePubkey(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.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