diff --git a/electrum/daemon.py b/electrum/daemon.py index 56c5f101c1a7..db080aa91769 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -585,6 +585,17 @@ def delete_wallet(self, path: str) -> bool: return True return False + def rename_wallet_file(self, old_path: str, new_path: str): + old_path = standardize_path(old_path) + new_path = standardize_path(new_path) + if os.path.exists(new_path): + raise ValueError("Wallet file already exists") + os.rename(old_path, new_path) + self.logger.debug(f'renamed wallet: {old_path} -> {new_path}') + self.update_recently_opened_wallets(old_path, remove=True) + if self.config.CURRENT_WALLET == old_path: + self.config.CURRENT_WALLET = new_path + def stop_wallet(self, path: str) -> bool: """Returns True iff a wallet was found.""" assert util.get_running_loop() != util.get_asyncio_loop(), 'must not be called from asyncio thread' diff --git a/electrum/gui/qml/components/WalletDetails.qml b/electrum/gui/qml/components/WalletDetails.qml index 8522c42b2a42..2676c58625a7 100644 --- a/electrum/gui/qml/components/WalletDetails.qml +++ b/electrum/gui/qml/components/WalletDetails.qml @@ -130,6 +130,64 @@ Pane { visible: Daemon.currentWallet columns: 2 + Label { + Layout.columnSpan: 2 + Layout.topMargin: constants.paddingSmall + text: qsTr('Name') + color: Material.accentColor + } + + TextHighlightPane { + id: walletNameContent + property bool editmode: false + Layout.columnSpan: 2 + Layout.fillWidth: true + + RowLayout { + width: parent.width + Label { + visible: !walletNameContent.editmode + text: Daemon.currentWallet.name + wrapMode: Text.Wrap + Layout.fillWidth: true + font.pixelSize: constants.fontSizeLarge + } + ToolButton { + visible: !walletNameContent.editmode + icon.source: '../../icons/pen.png' + icon.color: 'transparent' + onClicked: { + walletNameEdit.text = Daemon.currentWallet.name + walletNameContent.editmode = true + walletNameEdit.focus = true + } + } + TextField { + id: walletNameEdit + visible: walletNameContent.editmode + Layout.fillWidth: true + font.pixelSize: constants.fontSizeLarge + } + ToolButton { + visible: walletNameContent.editmode + icon.source: '../../icons/confirmed.png' + icon.color: enabled ? 'transparent' : constants.mutedForeground + enabled: walletNameEdit.text !== Daemon.currentWallet.name + && Daemon.isValidWalletName(walletNameEdit.text) + onClicked: { + walletNameContent.editmode = false + Daemon.renameWallet(walletNameEdit.text) + } + } + ToolButton { + visible: walletNameContent.editmode + icon.source: '../../icons/closebutton.png' + icon.color: 'transparent' + onClicked: walletNameContent.editmode = false + } + } + } + Label { Layout.columnSpan: 2 Layout.topMargin: constants.paddingSmall @@ -526,6 +584,14 @@ Pane { dialog.open() } } + function onWalletRenameError(message) { + var dialog = app.messageDialog.createObject(app, { + title: qsTr('Error'), + iconSource: Qt.resolvedUrl('../../icons/warning.png'), + text: message + }) + dialog.open() + } } Connections { diff --git a/electrum/gui/qml/qedaemon.py b/electrum/gui/qml/qedaemon.py index 2b75eb04995d..d9eadb324f12 100644 --- a/electrum/gui/qml/qedaemon.py +++ b/electrum/gui/qml/qedaemon.py @@ -150,6 +150,7 @@ class QEDaemon(AuthMixin, QObject): walletRequiresPassword = pyqtSignal([str, str], arguments=['name', 'path']) walletOpenError = pyqtSignal([str], arguments=["error"]) walletDeleteError = pyqtSignal([str, str], arguments=['code', 'message']) + walletRenameError = pyqtSignal([str], arguments=['message']) def __init__(self, daemon: 'Daemon', plugins: 'Plugins', parent=None): super().__init__(parent) @@ -310,6 +311,34 @@ def delete_wallet(self, wallet): self.availableWallets.remove_wallet(path) + @pyqtSlot(str, result=bool) + def isValidWalletName(self, wallet_name: str) -> bool: + if not wallet_name: + return False + if self.availableWallets.wallet_name_exists(wallet_name): + return False + # ensure wallet_name is not interpreted as path + if os.path.basename(wallet_name) != wallet_name: # '/foo/bar/' returns 'bar' + return False + return True + + @pyqtSlot(str) + def renameWallet(self, new_name: str): + wallet = self._current_wallet + assert wallet, "name change without wallet?" + old_path = standardize_path(wallet.wallet.storage.path) + wallet_dir = os.path.dirname(old_path) + new_path = standardize_path(os.path.join(wallet_dir, new_name)) + if old_path == new_path: + return + self._current_wallet = None + self.daemon.stop_wallet(old_path) + try: + self.daemon.rename_wallet_file(old_path, new_path) + except Exception as e: + self.walletRenameError.emit(_('Error renaming wallet:\n') + str(e)) + self.walletLoaded.emit(None, None) + @pyqtProperty(bool, notify=loadingChanged) def loading(self): return self._loading