From a500d5194d0903456b7d88544c9030868e0725a8 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 11 Apr 2025 08:53:10 +0200 Subject: [PATCH] make plugins dialog available in tray This makes it possible to install a third-party plugin from the wizard, before creating a wallet, e.g. for a hardware wallet. --- electrum/gui/qt/__init__.py | 16 ++++++++++-- electrum/gui/qt/main_window.py | 17 ++----------- electrum/gui/qt/plugins_dialog.py | 41 +++++++++++++++---------------- electrum/gui/qt/util.py | 7 ++++++ 4 files changed, 43 insertions(+), 38 deletions(-) diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index 8dc7d6b73..c66f48271 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -38,8 +38,8 @@ except Exception as e: "Error: Could not import PyQt6. On Linux systems, " "you may try 'sudo apt-get install python3-pyqt6'") from e -from PyQt6.QtGui import QGuiApplication -from PyQt6.QtWidgets import QApplication, QSystemTrayIcon, QWidget, QMenu, QMessageBox, QDialog +from PyQt6.QtGui import QGuiApplication, QCursor +from PyQt6.QtWidgets import QApplication, QSystemTrayIcon, QWidget, QMenu, QMessageBox, QDialog, QToolTip from PyQt6.QtCore import QObject, pyqtSignal, QTimer, Qt import PyQt6.QtCore as QtCore @@ -208,6 +208,7 @@ class ElectrumGui(BaseElectrumGui, Logger): m = self.tray.contextMenu() m.clear() network = self.daemon.network + m.addAction(_("Plugins"), self.show_plugins_dialog) if network: m.addAction(_("Network"), self.show_network_dialog) if network and network.lngossip: @@ -293,6 +294,11 @@ class ElectrumGui(BaseElectrumGui, Logger): self.lightning_dialog = LightningDialog(self) self.lightning_dialog.bring_to_top() + def show_plugins_dialog(self): + from .plugins_dialog import PluginsDialog + d = PluginsDialog(self) + d.exec() + def show_network_dialog(self, proxy_tab=False): if self.network_dialog: self.network_dialog.show(proxy_tab=proxy_tab) @@ -546,3 +552,9 @@ class ElectrumGui(BaseElectrumGui, Logger): if hasattr(PyQt6, "__path__"): ret["pyqt.path"] = ", ".join(PyQt6.__path__ or []) return ret + + def do_copy(self, text: str, *, title: str = None) -> None: + self.app.clipboard().setText(text) + message = _("Text copied to Clipboard") if title is None else _("{} copied to Clipboard").format(title) + # tooltip cannot be displayed immediately when called from a menu; wait 200ms + self.timer.singleShot(200, lambda: QToolTip.showText(QCursor.pos(), message, None)) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 833896c56..919fea096 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -759,7 +759,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): tools_menu.addAction(_("Electrum preferences"), self.settings_dialog) tools_menu.addAction(_("&Network"), self.gui_object.show_network_dialog).setEnabled(bool(self.network)) - tools_menu.addAction(_("&Plugins"), self.plugins_dialog) + tools_menu.addAction(_("&Plugins"), self.gui_object.show_plugins_dialog) tools_menu.addSeparator() tools_menu.addAction(_("&Sign/verify message"), self.sign_verify_message) tools_menu.addAction(_("&Encrypt/decrypt message"), self.encrypt_message) @@ -1131,9 +1131,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): return ReceiveTab(self) def do_copy(self, text: str, *, title: str = None) -> None: - self.app.clipboard().setText(text) - message = _("Text copied to Clipboard") if title is None else _("{} copied to Clipboard").format(title) - self.show_tooltip_after_delay(message) + self.gui_object.do_copy(text, title=title) def show_tooltip_after_delay(self, message): # tooltip cannot be displayed immediately when called from a menu; wait 200ms @@ -2209,12 +2207,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): layout.addLayout(hbox, 4, 1) d.exec() - def password_dialog(self, msg=None, parent=None): - from .password_dialog import PasswordDialog - parent = parent or self - d = PasswordDialog(parent, msg) - return d.run() - def tx_from_text(self, data: Union[str, bytes]) -> Union[None, 'PartialTransaction', 'Transaction']: from electrum.transaction import tx_from_any try: @@ -2654,11 +2646,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): self.gui_object.timer.timeout.disconnect(self.timer_actions) self.gui_object.close_window(self) - def plugins_dialog(self): - from .plugins_dialog import PluginsDialog - d = PluginsDialog(self) - d.exec() - def cpfp_dialog(self, parent_tx: Transaction) -> None: new_tx = self.wallet.cpfp(parent_tx, 0) total_size = parent_tx.estimated_size() + new_tx.estimated_size() diff --git a/electrum/gui/qt/plugins_dialog.py b/electrum/gui/qt/plugins_dialog.py index 7b9e7081f..f74e1d827 100644 --- a/electrum/gui/qt/plugins_dialog.py +++ b/electrum/gui/qt/plugins_dialog.py @@ -6,17 +6,17 @@ from PyQt6.QtWidgets import QLabel, QVBoxLayout, QGridLayout, QPushButton, QWidg from electrum.i18n import _ from electrum.plugin import run_hook -from .util import WindowModalDialog, Buttons, CloseButton, WWLabel, insert_spaces +from .util import WindowModalDialog, Buttons, CloseButton, WWLabel, insert_spaces, MessageBoxMixin if TYPE_CHECKING: - from .main_window import ElectrumWindow + from . import ElectrumGui from electrum_cc import ECPrivkey class PluginDialog(WindowModalDialog): - def __init__(self, name, metadata, status_button: Optional['PluginStatusButton'], window: 'ElectrumWindow'): + def __init__(self, name, metadata, status_button: Optional['PluginStatusButton'], window: 'PluginsDialog'): display_name = metadata.get('fullname', '') author = metadata.get('author', '') description = metadata.get('description', '') @@ -80,12 +80,12 @@ class PluginDialog(WindowModalDialog): self.status_button.update() self.close() # note: all enabled plugins will receive this hook: - run_hook('init_qt', self.window.window.gui_object) + run_hook('init_qt', self.window.gui_object) class PluginStatusButton(QPushButton): - def __init__(self, window, name): + def __init__(self, window: 'PluginsDialog', name: str): QPushButton.__init__(self, '') self.window = window self.plugins = window.plugins @@ -103,7 +103,7 @@ class PluginStatusButton(QPushButton): p = self.plugins.get(self.name) plugin_is_loaded = p is not None enabled = ( - not plugin_is_loaded and self.plugins.is_available(self.name, self.window.wallet) + not plugin_is_loaded or plugin_is_loaded and p.can_user_disable() ) self.setEnabled(enabled) @@ -113,14 +113,13 @@ class PluginStatusButton(QPushButton): self.setText(text) -class PluginsDialog(WindowModalDialog): +class PluginsDialog(WindowModalDialog, MessageBoxMixin): - def __init__(self, window: 'ElectrumWindow'): - WindowModalDialog.__init__(self, window, _('Electrum Plugins')) - self.window = window - self.wallet = self.window.wallet - self.config = window.config - self.plugins = self.window.gui_object.plugins + def __init__(self, gui_object: 'ElectrumGui'): + WindowModalDialog.__init__(self, None, _('Electrum Plugins')) + self.gui_object = gui_object + self.config = gui_object.config + self.plugins = gui_object.plugins vbox = QVBoxLayout(self) scroll = QScrollArea() scroll.setEnabled(True) @@ -144,7 +143,7 @@ class PluginsDialog(WindowModalDialog): self.init_plugins_password() return # ask for url and password, same window - pw = self.window.password_dialog( + pw = self.password_dialog( msg=' '.join([ _('Warning: Third-party plugins are not endorsed by Electrum!'), '

', @@ -160,7 +159,7 @@ class PluginsDialog(WindowModalDialog): privkey = self.plugins.derive_privkey(pw, salt) if pubkey != privkey.get_public_key_bytes(): keyfile_path, keyfile_help = self.plugins.get_keyfile_path() - self.window.show_error( + self.show_error( ''.join([ _('Incorrect password.'), '\n\n', _('Your plugin authorization password is required to install plugins.'), ' ', @@ -186,8 +185,8 @@ class PluginsDialog(WindowModalDialog): _('Your plugins key is:'), '\n\n', key_hex, '\n\n', _('Please save this key in'), '\n\n' + keyfile_path, '\n\n', keyfile_help ]) - self.window.do_copy(key_hex, title=_('Plugins key')) - self.window.show_message(msg) + self.gui_object.do_copy(key_hex, title=_('Plugins key')) + self.show_message(msg) def download_plugin_dialog(self): import os @@ -206,12 +205,12 @@ class PluginsDialog(WindowModalDialog): except UserCancelled: return except Exception as e: - self.window.show_error(f"{e}") + self.show_error(f"{e}") return try: success = self.confirm_add_plugin(path) except Exception as e: - self.window.show_error(f"{e}") + self.show_error(f"{e}") success = False if not success: os.unlink(path) @@ -232,7 +231,7 @@ class PluginsDialog(WindowModalDialog): try: success = self.confirm_add_plugin(path) except Exception as e: - self.window.show_error(f"{e}") + self.show_error(f"{e}") success = False if not success: os.unlink(path) @@ -249,7 +248,7 @@ class PluginsDialog(WindowModalDialog): return False self.plugins.external_plugin_metadata[name] = manifest self.plugins.authorize_plugin(name, path, privkey) - self.window.show_message(_('Plugin installed successfully.')) + self.show_message(_('Plugin installed successfully.')) self.show_list() return True diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 273f73805..6ee320b1d 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -320,6 +320,13 @@ class MessageBoxMixin(object): return None return choice_widget.selected_key + def password_dialog(self, msg=None, parent=None): + from .password_dialog import PasswordDialog + parent = parent or self + d = PasswordDialog(parent, msg) + return d.run() + + def custom_message_box(*, icon, parent, title, text, buttons=QMessageBox.StandardButton.Ok, defaultButton=QMessageBox.StandardButton.NoButton, rich_text=False,