diff --git a/electrum/gui/common_qt/util.py b/electrum/gui/common_qt/util.py index 13f8e2e15..7eed35a4c 100644 --- a/electrum/gui/common_qt/util.py +++ b/electrum/gui/common_qt/util.py @@ -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() diff --git a/electrum/gui/qml/qebip39recovery.py b/electrum/gui/qml/qebip39recovery.py index 2e4862732..fd2346f57 100644 --- a/electrum/gui/qml/qebip39recovery.py +++ b/electrum/gui/qml/qebip39recovery.py @@ -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): diff --git a/electrum/gui/qml/util.py b/electrum/gui/qml/util.py index 9a2825e01..3580f39ae 100644 --- a/electrum/gui/qml/util.py +++ b/electrum/gui/qml/util.py @@ -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() diff --git a/electrum/gui/qt/bip39_recovery_dialog.py b/electrum/gui/qt/bip39_recovery_dialog.py index 7a25f2ebc..002dee4b3 100644 --- a/electrum/gui/qt/bip39_recovery_dialog.py +++ b/electrum/gui/qt/bip39_recovery_dialog.py @@ -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__) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index ef77990e4..1bf970a14 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -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 diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 09f6201e1..1649df47b 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -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) diff --git a/electrum/hw_wallet/qt.py b/electrum/hw_wallet/qt.py index 99a58ec9b..9398e6c89 100644 --- a/electrum/hw_wallet/qt.py +++ b/electrum/hw_wallet/qt.py @@ -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 diff --git a/electrum/keystore.py b/electrum/keystore.py index f066a9185..ac9499759 100644 --- a/electrum/keystore.py +++ b/electrum/keystore.py @@ -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 diff --git a/electrum/plugins/labels/qt.py b/electrum/plugins/labels/qt.py index 17c1de879..2eec4b874 100644 --- a/electrum/plugins/labels/qt.py +++ b/electrum/plugins/labels/qt.py @@ -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