Merge pull request #9759 from accumulator/common_taskthread
qt,qml: move TaskThread to common_qt
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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()
|
|
||||||
|
|||||||
@@ -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__)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user