diff --git a/electrum/gui/qml/__init__.py b/electrum/gui/qml/__init__.py index 9d36f369b..30f7d9d1a 100644 --- a/electrum/gui/qml/__init__.py +++ b/electrum/gui/qml/__init__.py @@ -41,6 +41,7 @@ class ElectrumTranslator(QTranslator): def translate(self, context, source_text, disambiguation, n): return _(source_text, context=context) + class ElectrumGui(BaseElectrumGui, Logger): @profiler @@ -91,7 +92,7 @@ class ElectrumGui(BaseElectrumGui, Logger): Exception_Hook.maybe_setup(config=config, slot=self.app.appController.crash) # Initialize any QML plugins - run_hook('init_qml', self) + run_hook('init_qml', self.app) self.app.engine.load('electrum/gui/qml/components/main.qml') def close(self): diff --git a/electrum/gui/qml/components/Preferences.qml b/electrum/gui/qml/components/Preferences.qml index c2cba8dd7..5ec5356be 100644 --- a/electrum/gui/qml/components/Preferences.qml +++ b/electrum/gui/qml/components/Preferences.qml @@ -217,6 +217,25 @@ Pane { } } + RowLayout { + Layout.columnSpan: 2 + Layout.fillWidth: true + Layout.leftMargin: -constants.paddingSmall + spacing: 0 + Switch { + id: syncLabels + onCheckedChanged: { + if (activeFocus) + AppController.setPluginEnabled('labels', checked) + } + } + Label { + Layout.fillWidth: true + text: qsTr('Synchronize labels') + wrapMode: Text.Wrap + } + } + PrefsHeading { Layout.columnSpan: 2 text: qsTr('Wallet behavior') @@ -384,5 +403,6 @@ Pane { useFallbackAddress.checked = Config.useFallbackAddress enableDebugLogs.checked = Config.enableDebugLogs useRecoverableChannels.checked = Config.useRecoverableChannels + syncLabels.checked = AppController.isPluginEnabled('labels') } } diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index bb2245c4f..20de321b0 100644 --- a/electrum/gui/qml/qeapp.py +++ b/electrum/gui/qml/qeapp.py @@ -19,6 +19,7 @@ from electrum.logging import Logger, get_logger from electrum.bip21 import BITCOIN_BIP21_URI_SCHEME, LIGHTNING_URI_SCHEME from electrum.base_crash_reporter import BaseCrashReporter, EarlyExceptionsQueue from electrum.network import Network +from electrum.plugin import run_hook from .qeconfig import QEConfig from .qedaemon import QEDaemon @@ -70,10 +71,11 @@ class QEAppController(BaseCrashReporter, QObject): sendingBugreportFailure = pyqtSignal(str) secureWindowChanged = pyqtSignal() - def __init__(self, qedaemon: 'QEDaemon', plugins: 'Plugins'): + def __init__(self, qeapp: 'ElectrumQmlApplication', qedaemon: 'QEDaemon', plugins: 'Plugins'): BaseCrashReporter.__init__(self, None, None, None) QObject.__init__(self) + self._app = qeapp self._qedaemon = qedaemon self._plugins = plugins self.config = qedaemon.daemon.config @@ -224,12 +226,18 @@ class QEAppController(BaseCrashReporter, QObject): return s @pyqtSlot(str, bool) - def setPluginEnabled(self, plugin, enabled): + def setPluginEnabled(self, plugin: str, enabled: bool): if enabled: self._plugins.enable(plugin) + # note: all enabled plugins will receive this hook: + run_hook('init_qml', self._app) else: self._plugins.disable(plugin) + @pyqtSlot(str, result=bool) + def isPluginEnabled(self, plugin: str): + return bool(self._plugins.get(plugin)) + @pyqtSlot(result=bool) def isAndroid(self): return 'ANDROID_DATA' in os.environ @@ -369,7 +377,7 @@ class ElectrumQmlApplication(QGuiApplication): self._qeconfig = QEConfig(config) self._qenetwork = QENetwork(daemon.network, self._qeconfig) self.daemon = QEDaemon(daemon) - self.appController = QEAppController(self.daemon, self.plugins) + self.appController = QEAppController(self, self.daemon, self.plugins) self._maxAmount = QEAmount(is_max=True) self.context.setContextProperty('AppController', self.appController) self.context.setContextProperty('Config', self._qeconfig) diff --git a/electrum/plugin.py b/electrum/plugin.py index 0194fb1a2..8879e15a7 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -218,6 +218,7 @@ def hook(func): hook_names.add(func.__name__) return func + def run_hook(name, *args): results = [] f_list = hooks.get(name, []) diff --git a/electrum/plugins/labels/labels.py b/electrum/plugins/labels/labels.py index a20cae8f4..9e392f6f9 100644 --- a/electrum/plugins/labels/labels.py +++ b/electrum/plugins/labels/labels.py @@ -134,9 +134,11 @@ class LabelsPlugin(BasePlugin): response = await self.do_get("/labels/since/%d/for/%s" % (nonce, wallet_id)) except Exception as e: raise ErrorConnectingServer(e) from e - if response["labels"] is None: + if response["labels"] is None or len(response["labels"]) == 0: self.logger.info('no new labels') return + self.logger.debug(f"labels received {response!r}") + self.logger.info(f'received {len(response["labels"])} labels') result = {} for label in response["labels"]: try: @@ -157,7 +159,6 @@ class LabelsPlugin(BasePlugin): if force or not wallet._get_label(key): wallet._set_label(key, value) - self.logger.info(f"received {len(response)} labels") self.set_nonce(wallet, response["nonce"] + 1) self.on_pulled(wallet) @@ -173,15 +174,18 @@ class LabelsPlugin(BasePlugin): self.logger.info(repr(e)) def pull(self, wallet: 'Abstract_Wallet', force: bool): - if not wallet.network: raise Exception(_('You are offline.')) + if not wallet.network: + raise Exception(_('You are offline.')) return asyncio.run_coroutine_threadsafe(self.pull_thread(wallet, force), wallet.network.asyncio_loop).result() def push(self, wallet: 'Abstract_Wallet'): - if not wallet.network: raise Exception(_('You are offline.')) + if not wallet.network: + raise Exception(_('You are offline.')) return asyncio.run_coroutine_threadsafe(self.push_thread(wallet), wallet.network.asyncio_loop).result() def start_wallet(self, wallet: 'Abstract_Wallet'): - if not wallet.network: return # 'offline' mode + if not wallet.network: + return # 'offline' mode mpk = wallet.get_fingerprint() if not mpk: return diff --git a/electrum/plugins/labels/qml.py b/electrum/plugins/labels/qml.py index 6f95dca0e..4ad17263f 100644 --- a/electrum/plugins/labels/qml.py +++ b/electrum/plugins/labels/qml.py @@ -1,6 +1,6 @@ import threading -from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot +from PyQt5.QtCore import pyqtSignal, pyqtSlot from electrum.i18n import _ from electrum.plugin import hook @@ -10,6 +10,7 @@ from electrum.gui.qml.plugins import PluginQObject from .labels import LabelsPlugin + class Plugin(LabelsPlugin): class QSignalObject(PluginQObject): @@ -63,6 +64,8 @@ class Plugin(LabelsPlugin): def __init__(self, *args): LabelsPlugin.__init__(self, *args) + self._app = None + self.so = None @hook def load_wallet(self, wallet): @@ -77,9 +80,9 @@ class Plugin(LabelsPlugin): wallet = self._app.daemon.currentWallet.wallet - def push_thread(wallet): + def push_thread(_wallet): try: - self.push(wallet) + self.push(_wallet) self.so.upload_finished(True) self._app.appController.userNotify.emit(_('Labels uploaded')) except Exception as e: @@ -87,7 +90,7 @@ class Plugin(LabelsPlugin): self.so.upload_finished(False) self._app.appController.userNotify.emit(repr(e)) - threading.Thread(target=push_thread,args=[wallet]).start() + threading.Thread(target=push_thread, args=[wallet]).start() def pull_async(self): if not self._app.daemon.currentWallet: @@ -96,9 +99,10 @@ class Plugin(LabelsPlugin): return wallet = self._app.daemon.currentWallet.wallet - def pull_thread(wallet): + + def pull_thread(_wallet): try: - self.pull(wallet, True) + self.pull(_wallet, True) self.so.download_finished(True) self._app.appController.userNotify.emit(_('Labels downloaded')) except Exception as e: @@ -106,8 +110,7 @@ class Plugin(LabelsPlugin): self.so.download_finished(False) self._app.appController.userNotify.emit(repr(e)) - threading.Thread(target=pull_thread,args=[wallet]).start() - + threading.Thread(target=pull_thread, args=[wallet]).start() def on_pulled(self, wallet): self.logger.info('on pulled') @@ -117,9 +120,15 @@ class Plugin(LabelsPlugin): _wallet.labelsUpdated.emit() @hook - def init_qml(self, gui): - self.logger.debug(f'init_qml hook called, gui={str(type(gui))}') - self._app = gui.app + def init_qml(self, app): + self.logger.debug(f'init_qml hook called, gui={str(type(app))}') + self.logger.debug(f'app={self._app!r}, so={self.so!r}') + self._app = app # important: QSignalObject needs to be parented, as keeping a ref # in the plugin is not enough to avoid gc self.so = Plugin.QSignalObject(self, self._app) + + # If the user just enabled the plugin, the 'load_wallet' hook would not + # get called for already loaded wallets, hence we call it manually for those: + for wallet_name, wallet in app.daemon.daemon._wallets.items(): + self.load_wallet(wallet) diff --git a/electrum/plugins/trustedcoin/qml.py b/electrum/plugins/trustedcoin/qml.py index ea51db35c..962676ee0 100644 --- a/electrum/plugins/trustedcoin/qml.py +++ b/electrum/plugins/trustedcoin/qml.py @@ -19,9 +19,10 @@ from .trustedcoin import (TrustedCoinPlugin, server, ErrorConnectingServer, TrustedCoinException, make_xpub) if TYPE_CHECKING: - from electrum.gui.qml import ElectrumGui + from electrum.gui.qml import ElectrumQmlApplication from electrum.wallet import Abstract_Wallet + class Plugin(TrustedCoinPlugin): class QSignalObject(PluginQObject): @@ -287,9 +288,9 @@ class Plugin(TrustedCoinPlugin): self.start_request_thread(wallet) @hook - def init_qml(self, gui: 'ElectrumGui'): - self.logger.debug(f'init_qml hook called, gui={str(type(gui))}') - self._app = gui.app + def init_qml(self, app: 'ElectrumQmlApplication'): + self.logger.debug(f'init_qml hook called, gui={str(type(app))}') + self._app = app # important: QSignalObject needs to be parented, as keeping a ref # in the plugin is not enough to avoid gc self.so = Plugin.QSignalObject(self, self._app)