diff --git a/electrum/gui/qt/address_dialog.py b/electrum/gui/qt/address_dialog.py index c78a18ae8..2e2b58237 100644 --- a/electrum/gui/qt/address_dialog.py +++ b/electrum/gui/qt/address_dialog.py @@ -87,14 +87,14 @@ class AddressDialog(WindowModalDialog): redeem_script = self.wallet.get_redeem_script(address) if redeem_script: vbox.addWidget(QLabel(_("Redeem Script") + ':')) - redeem_e = ShowQRTextEdit(text=redeem_script) + redeem_e = ShowQRTextEdit(text=redeem_script, config=self.config) redeem_e.addCopyButton(self.app) vbox.addWidget(redeem_e) witness_script = self.wallet.get_witness_script(address) if witness_script: vbox.addWidget(QLabel(_("Witness Script") + ':')) - witness_e = ShowQRTextEdit(text=witness_script) + witness_e = ShowQRTextEdit(text=witness_script, config=self.config) witness_e.addCopyButton(self.app) vbox.addWidget(witness_e) diff --git a/electrum/gui/qt/completion_text_edit.py b/electrum/gui/qt/completion_text_edit.py index 05830b9a1..b34d976c6 100644 --- a/electrum/gui/qt/completion_text_edit.py +++ b/electrum/gui/qt/completion_text_edit.py @@ -32,8 +32,8 @@ from .util import ButtonsTextEdit class CompletionTextEdit(ButtonsTextEdit): - def __init__(self, parent=None): - super(CompletionTextEdit, self).__init__(parent) + def __init__(self): + ButtonsTextEdit.__init__(self) self.completer = None self.moveCursor(QTextCursor.End) self.disable_suggestions() diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index c3ed880e6..7066dfd1d 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -89,7 +89,8 @@ from .util import (read_QIcon, ColorScheme, text_dialog, icon_path, WaitingDialo CloseButton, HelpButton, MessageBoxMixin, EnterButton, import_meta_gui, export_meta_gui, filename_field, address_field, char_width_in_lineedit, webopen, - TRANSACTION_FILE_EXTENSION_FILTER_ANY, MONOSPACE_FONT) + TRANSACTION_FILE_EXTENSION_FILTER_ANY, MONOSPACE_FONT, + getOpenFileName, getSaveFileName) from .util import ButtonsTextEdit, ButtonsLineEdit from .installwizard import WIF_HELP_TEXT from .history_list import HistoryList, HistoryModel @@ -841,38 +842,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): except TypeError: self.tray.showMessage("Electrum", message, QSystemTrayIcon.Information, 20000) - - - # custom wrappers for getOpenFileName and getSaveFileName, that remember the path selected by the user - def getOpenFileName(self, title, filter = ""): - directory = self.config.get('io_dir', os.path.expanduser('~')) - fileName, __ = QFileDialog.getOpenFileName(self, title, directory, filter) - if fileName and directory != os.path.dirname(fileName): - self.config.set_key('io_dir', os.path.dirname(fileName), True) - return fileName - - def getSaveFileName(self, title, filename, filter="", - *, default_extension: str = None, - default_filter: str = None) -> Optional[str]: - directory = self.config.get('io_dir', os.path.expanduser('~')) - path = os.path.join(directory, filename) - - file_dialog = QFileDialog(self, title, path, filter) - file_dialog.setAcceptMode(QFileDialog.AcceptSave) - if default_extension: - # note: on MacOS, the selected filter's first extension seems to have priority over this... - file_dialog.setDefaultSuffix(default_extension) - if default_filter: - assert default_filter in filter, f"default_filter={default_filter!r} does not appear in filter={filter!r}" - file_dialog.selectNameFilter(default_filter) - if file_dialog.exec() != QDialog.Accepted: - return None - - selected_path = file_dialog.selectedFiles()[0] - if selected_path and directory != os.path.dirname(selected_path): - self.config.set_key('io_dir', os.path.dirname(selected_path), True) - return selected_path - def timer_actions(self): self.request_list.refresh_status() # Note this runs in the GUI thread @@ -2077,12 +2046,18 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): def do_export(): key = pr.get_id() name = str(key) + '.bip70' - fn = self.getSaveFileName(_("Save invoice to file"), name, filter="*.bip70") + fn = getSaveFileName( + parent=self, + title=_("Save invoice to file"), + filename=name, + filter="*.bip70", + config=self.config, + ) if not fn: return with open(fn, 'wb') as f: data = f.write(pr.raw) - self.show_message(_('BIP70 invoice saved as' + ' ' + fn)) + self.show_message(_('BIP70 invoice saved as {}').format(fn)) exportButton = EnterButton(_('Export'), do_export) buttons = Buttons(exportButton, CloseButton(d)) else: @@ -2113,7 +2088,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): grid.addWidget(QLabel(_("Expires") + ':'), 4, 0) grid.addWidget(QLabel(format_time(invoice.time + invoice.exp)), 4, 1) vbox.addLayout(grid) - invoice_e = ShowQRTextEdit() + invoice_e = ShowQRTextEdit(config=self.config) invoice_e.addCopyButton(self.app) invoice_e.setText(invoice.invoice) vbox.addWidget(invoice_e) @@ -2392,7 +2367,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): ks_vbox.setContentsMargins(0, 0, 0, 0) ks_w.setLayout(ks_vbox) - mpk_text = ShowQRTextEdit(ks.get_master_public_key()) + mpk_text = ShowQRTextEdit(ks.get_master_public_key(), config=self.config) mpk_text.setMaximumHeight(150) mpk_text.addCopyButton(self.app) run_hook('show_xpub_button', mpk_text, ks) @@ -2461,8 +2436,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): help_text=None, show_copy_text_btn=False): if not data: return - d = QRDialog(data, parent or self, title, help_text=help_text, - show_copy_text_btn=show_copy_text_btn) + d = QRDialog( + data=data, + parent=parent or self, + title=title, + help_text=help_text, + show_copy_text_btn=show_copy_text_btn, + config=self.config, + ) d.exec_() @protected @@ -2482,14 +2463,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): vbox.addWidget(QLabel(_("Address") + ': ' + address)) vbox.addWidget(QLabel(_("Script type") + ': ' + xtype)) vbox.addWidget(QLabel(_("Private key") + ':')) - keys_e = ShowQRTextEdit(text=pk) + keys_e = ShowQRTextEdit(text=pk, config=self.config) keys_e.addCopyButton(self.app) vbox.addWidget(keys_e) - # if redeem_script: - # vbox.addWidget(QLabel(_("Redeem Script") + ':')) - # rds_e = ShowQRTextEdit(text=redeem_script) - # rds_e.addCopyButton(self.app) - # vbox.addWidget(rds_e) vbox.addLayout(Buttons(CloseButton(d))) d.setLayout(vbox) d.exec_() @@ -2701,8 +2677,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): self.show_transaction(tx) def read_tx_from_file(self) -> Optional[Transaction]: - fileName = self.getOpenFileName(_("Select your transaction file"), - TRANSACTION_FILE_EXTENSION_FILTER_ANY) + fileName = getOpenFileName( + parent=self, + title=_("Select your transaction file"), + filter=TRANSACTION_FILE_EXTENSION_FILTER_ANY, + config=self.config, + ) if not fileName: return try: diff --git a/electrum/gui/qt/qrcodewidget.py b/electrum/gui/qt/qrcodewidget.py index 0c9a49e57..567f6ce87 100644 --- a/electrum/gui/qt/qrcodewidget.py +++ b/electrum/gui/qt/qrcodewidget.py @@ -9,8 +9,9 @@ from PyQt5.QtWidgets import ( ) from electrum.i18n import _ +from electrum.simple_config import SimpleConfig -from .util import WindowModalDialog, get_parent_main_window, WWLabel +from .util import WindowModalDialog, WWLabel, getSaveFileName class QRCodeWidget(QWidget): @@ -95,15 +96,17 @@ class QRDialog(WindowModalDialog): def __init__( self, + *, data, parent=None, title="", show_text=False, - *, help_text=None, show_copy_text_btn=False, + config: SimpleConfig, ): WindowModalDialog.__init__(self, parent, title) + self.config = config vbox = QVBoxLayout() @@ -122,11 +125,12 @@ class QRDialog(WindowModalDialog): hbox.addStretch(1) def print_qr(): - main_window = get_parent_main_window(self) - if main_window: - filename = main_window.getSaveFileName(_("Select where to save file"), "qrcode.png") - else: - filename, __ = QFileDialog.getSaveFileName(self, _("Select where to save file"), "qrcode.png") + filename = getSaveFileName( + parent=self, + title=_("Select where to save file"), + filename="qrcode.png", + config=self.config, + ) if not filename: return p = qrw.grab() diff --git a/electrum/gui/qt/qrtextedit.py b/electrum/gui/qt/qrtextedit.py index b596ffad8..453b3a445 100644 --- a/electrum/gui/qt/qrtextedit.py +++ b/electrum/gui/qt/qrtextedit.py @@ -4,14 +4,15 @@ from electrum.i18n import _ from electrum.plugin import run_hook from electrum.simple_config import SimpleConfig -from .util import ButtonsTextEdit, MessageBoxMixin, ColorScheme +from .util import ButtonsTextEdit, MessageBoxMixin, ColorScheme, getOpenFileName class ShowQRTextEdit(ButtonsTextEdit): - def __init__(self, text=None): + def __init__(self, text=None, *, config: SimpleConfig): ButtonsTextEdit.__init__(self, text) - self.setReadOnly(1) + self.config = config + self.setReadOnly(True) icon = "qrcode_white.png" if ColorScheme.dark_scheme else "qrcode.png" self.addButton(icon, self.qr_show, _("Show as QR code")) @@ -23,7 +24,11 @@ class ShowQRTextEdit(ButtonsTextEdit): s = str(self.toPlainText()) except: s = self.toPlainText() - QRDialog(s, parent=self).exec_() + QRDialog( + data=s, + parent=self, + config=self.config, + ).exec_() def contextMenuEvent(self, e): m = self.createStandardContextMenu() @@ -44,7 +49,11 @@ class ScanQRTextEdit(ButtonsTextEdit, MessageBoxMixin): run_hook('scan_text_edit', self) def file_input(self): - fileName, __ = QFileDialog.getOpenFileName(self, 'select file') + fileName = getOpenFileName( + parent=self, + title='select file', + config=self.config, + ) if not fileName: return try: diff --git a/electrum/gui/qt/seed_dialog.py b/electrum/gui/qt/seed_dialog.py index 0c2683a9f..280ed73c5 100644 --- a/electrum/gui/qt/seed_dialog.py +++ b/electrum/gui/qt/seed_dialog.py @@ -105,7 +105,7 @@ class SeedLayout(QVBoxLayout): if for_seed_words: self.seed_e = ButtonsTextEdit() else: # e.g. xpub - self.seed_e = ShowQRTextEdit() + self.seed_e = ShowQRTextEdit(config=self.config) self.seed_e.setReadOnly(True) self.seed_e.setText(seed) else: # we expect user to enter text diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 590ba6f59..5aec59af5 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -53,7 +53,7 @@ from .util import (MessageBoxMixin, read_QIcon, Buttons, icon_path, char_width_in_lineedit, TRANSACTION_FILE_EXTENSION_FILTER_SEPARATE, TRANSACTION_FILE_EXTENSION_FILTER_ONLY_COMPLETE_TX, TRANSACTION_FILE_EXTENSION_FILTER_ONLY_PARTIAL_TX, - BlockingWaitingDialog) + BlockingWaitingDialog, getSaveFileName) from .fee_slider import FeeSlider, FeeComboBox from .confirm_tx_dialog import TxEditor @@ -331,11 +331,15 @@ class BaseTxDialog(QDialog, MessageBoxMixin): extension = 'psbt' default_filter = TRANSACTION_FILE_EXTENSION_FILTER_ONLY_PARTIAL_TX name = f'{name}.{extension}' - fileName = self.main_window.getSaveFileName(_("Select where to save your transaction"), - name, - TRANSACTION_FILE_EXTENSION_FILTER_SEPARATE, - default_extension=extension, - default_filter=default_filter) + fileName = getSaveFileName( + parent=self, + title=_("Select where to save your transaction"), + filename=name, + filter=TRANSACTION_FILE_EXTENSION_FILTER_SEPARATE, + default_extension=extension, + default_filter=default_filter, + config=self.config, + ) if not fileName: return if tx.is_complete(): # network tx hex diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 5c45b1599..e0adb72e7 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -458,8 +458,14 @@ def filename_field(parent, config, defaultname, select_msg): def func(): text = filename_e.text() - _filter = "*.csv" if text.endswith(".csv") else "*.json" if text.endswith(".json") else None - p, __ = QFileDialog.getSaveFileName(None, select_msg, text, _filter) + _filter = "*.csv" if defaultname.endswith(".csv") else "*.json" if defaultname.endswith(".json") else None + p = getSaveFileName( + parent=None, + title=select_msg, + filename=text, + filter=_filter, + config=config, + ) if p: filename_e.setText(p) @@ -935,9 +941,14 @@ class AcceptFileDragDrop: raise NotImplementedError() -def import_meta_gui(electrum_window, title, importer, on_success): +def import_meta_gui(electrum_window: 'ElectrumWindow', title, importer, on_success): filter_ = "JSON (*.json);;All files (*)" - filename = electrum_window.getOpenFileName(_("Open {} file").format(title), filter_) + filename = getOpenFileName( + parent=electrum_window, + title=_("Open {} file").format(title), + filter=filter_, + config=electrum_window.config, + ) if not filename: return try: @@ -949,10 +960,15 @@ def import_meta_gui(electrum_window, title, importer, on_success): on_success() -def export_meta_gui(electrum_window, title, exporter): +def export_meta_gui(electrum_window: 'ElectrumWindow', title, exporter): filter_ = "JSON (*.json);;All files (*)" - filename = electrum_window.getSaveFileName(_("Select file to save your {}").format(title), - 'electrum_{}.json'.format(title), filter_) + filename = getSaveFileName( + parent=electrum_window, + title=_("Select file to save your {}").format(title), + filename='electrum_{}.json'.format(title), + filter=filter_, + config=electrum_window.config, + ) if not filename: return try: @@ -964,24 +980,44 @@ def export_meta_gui(electrum_window, title, exporter): .format(title, str(filename))) -def get_parent_main_window( - widget, *, allow_wizard: bool = False, -) -> Union[None, 'ElectrumWindow', 'InstallWizard']: - """Returns a reference to the ElectrumWindow this widget belongs to.""" - from .main_window import ElectrumWindow - from .transaction_dialog import TxDialog - from .installwizard import InstallWizard - for _ in range(100): - if widget is None: - return None - if isinstance(widget, ElectrumWindow): - return widget - if isinstance(widget, TxDialog): - return widget.main_window - if isinstance(widget, InstallWizard) and allow_wizard: - return widget - widget = widget.parentWidget() - return None +def getOpenFileName(*, parent, title, filter="", config: 'SimpleConfig') -> Optional[str]: + """Custom wrapper for getOpenFileName that remembers the path selected by the user.""" + directory = config.get('io_dir', os.path.expanduser('~')) + fileName, __ = QFileDialog.getOpenFileName(parent, title, directory, filter) + if fileName and directory != os.path.dirname(fileName): + config.set_key('io_dir', os.path.dirname(fileName), True) + return fileName + + +def getSaveFileName( + *, + parent, + title, + filename, + filter="", + default_extension: str = None, + default_filter: str = None, + config: 'SimpleConfig', +) -> Optional[str]: + """Custom wrapper for getSaveFileName that remembers the path selected by the user.""" + directory = config.get('io_dir', os.path.expanduser('~')) + path = os.path.join(directory, filename) + + file_dialog = QFileDialog(parent, title, path, filter) + file_dialog.setAcceptMode(QFileDialog.AcceptSave) + if default_extension: + # note: on MacOS, the selected filter's first extension seems to have priority over this... + file_dialog.setDefaultSuffix(default_extension) + if default_filter: + assert default_filter in filter, f"default_filter={default_filter!r} does not appear in filter={filter!r}" + file_dialog.selectNameFilter(default_filter) + if file_dialog.exec() != QDialog.Accepted: + return None + + selected_path = file_dialog.selectedFiles()[0] + if selected_path and directory != os.path.dirname(selected_path): + config.set_key('io_dir', os.path.dirname(selected_path), True) + return selected_path def icon_path(icon_basename): diff --git a/electrum/plugins/coldcard/qt.py b/electrum/plugins/coldcard/qt.py index 0ce585036..7a6f4058a 100644 --- a/electrum/plugins/coldcard/qt.py +++ b/electrum/plugins/coldcard/qt.py @@ -5,7 +5,8 @@ import copy from PyQt5.QtCore import Qt, pyqtSignal from PyQt5.QtWidgets import QPushButton, QLabel, QVBoxLayout, QWidget, QGridLayout -from electrum.gui.qt.util import WindowModalDialog, CloseButton, Buttons +from electrum.gui.qt.util import (WindowModalDialog, CloseButton, Buttons, getOpenFileName, + getSaveFileName) from electrum.gui.qt.transaction_dialog import TxDialog from electrum.gui.qt.main_window import ElectrumWindow @@ -64,8 +65,13 @@ class Plugin(ColdcardPlugin, QtPluginBase): basename = wallet.basename().rsplit('.', 1)[0] # trim .json name = f'{basename}-cc-export.txt'.replace(' ', '-') - fileName = main_window.getSaveFileName(_("Select where to save the setup file"), - name, "*.txt") + fileName = getSaveFileName( + parent=main_window, + title=_("Select where to save the setup file"), + filename=name, + filter="*.txt", + config=self.config, + ) if fileName: with open(fileName, "wt") as f: ColdcardPlugin.export_ms_wallet(wallet, f, basename) @@ -191,10 +197,14 @@ class CKCCSettingsDialog(WindowModalDialog): def start_upgrade(self, client): # ask for a filename (must have already downloaded it) - mw = self.window dev = client.dev - fileName = mw.getOpenFileName("Select upgraded firmware file", "*.dfu") + fileName = getOpenFileName( + parent=self, + title="Select upgraded firmware file", + filter="*.dfu", + config=self.window.config, + ) if not fileName: return @@ -220,7 +230,7 @@ class CKCCSettingsDialog(WindowModalDialog): if magic != FW_HEADER_MAGIC: raise ValueError("Bad magic") except Exception as exc: - mw.show_error("Does not appear to be a Coldcard firmware file.\n\n%s" % exc) + self.window.show_error("Does not appear to be a Coldcard firmware file.\n\n%s" % exc) return # TODO: @@ -228,7 +238,7 @@ class CKCCSettingsDialog(WindowModalDialog): # - warn them about the reboot? # - length checks # - add progress local bar - mw.show_message("Ready to Upgrade.\n\nBe patient. Unit will reboot itself when complete.") + self.window.show_message("Ready to Upgrade.\n\nBe patient. Unit will reboot itself when complete.") def doit(): dlen, _ = dev.upload_file(firmware, verify=True) diff --git a/electrum/plugins/safe_t/qt.py b/electrum/plugins/safe_t/qt.py index 6d3e9399e..92e7b3ac8 100644 --- a/electrum/plugins/safe_t/qt.py +++ b/electrum/plugins/safe_t/qt.py @@ -9,7 +9,7 @@ from PyQt5.QtWidgets import (QVBoxLayout, QLabel, QGridLayout, QPushButton, QMessageBox, QFileDialog, QSlider, QTabWidget) from electrum.gui.qt.util import (WindowModalDialog, WWLabel, Buttons, CancelButton, - OkButton, CloseButton) + OkButton, CloseButton, getOpenFileName) from electrum.i18n import _ from electrum.plugin import hook from electrum.util import bh2u @@ -276,8 +276,11 @@ class SettingsDialog(WindowModalDialog): invoke_client('toggle_passphrase', unpair_after=currently_enabled) def change_homescreen(): - dialog = QFileDialog(self, _("Choose Homescreen")) - filename, __ = dialog.getOpenFileName() + filename = getOpenFileName( + parent=self, + title=_("Choose Homescreen"), + config=config, + ) if not filename: return # user cancelled diff --git a/electrum/plugins/trezor/qt.py b/electrum/plugins/trezor/qt.py index bf8911c05..72674267e 100644 --- a/electrum/plugins/trezor/qt.py +++ b/electrum/plugins/trezor/qt.py @@ -8,7 +8,7 @@ from PyQt5.QtWidgets import (QVBoxLayout, QLabel, QGridLayout, QPushButton, QMessageBox, QFileDialog, QSlider, QTabWidget) from electrum.gui.qt.util import (WindowModalDialog, WWLabel, Buttons, CancelButton, - OkButton, CloseButton, PasswordLineEdit) + OkButton, CloseButton, PasswordLineEdit, getOpenFileName) from electrum.i18n import _ from electrum.plugin import hook from electrum.util import bh2u @@ -542,8 +542,11 @@ class SettingsDialog(WindowModalDialog): invoke_client('toggle_passphrase', unpair_after=currently_enabled) def change_homescreen(): - dialog = QFileDialog(self, _("Choose Homescreen")) - filename, __ = dialog.getOpenFileName() + filename = getOpenFileName( + parent=self, + title=_("Choose Homescreen"), + config=config, + ) if not filename: return # user cancelled