diff --git a/electrum/gui/qml/components/Addresses.qml b/electrum/gui/qml/components/Addresses.qml new file mode 100644 index 000000000..c2b57a79e --- /dev/null +++ b/electrum/gui/qml/components/Addresses.qml @@ -0,0 +1,88 @@ +import QtQuick 2.6 +import QtQuick.Layouts 1.0 +import QtQuick.Controls 2.0 +import QtQuick.Controls.Material 2.0 + +import org.electrum 1.0 + +Pane { + id: rootItem + anchors.fill: parent + + property string title: Daemon.walletName + ' - ' + qsTr('Addresses') + + ColumnLayout { + id: layout + width: parent.width + height: parent.height + + Item { + width: parent.width + Layout.fillHeight: true + + ListView { + id: listview + width: parent.width + height: parent.height + clip: true + model: Daemon.currentWallet.addressModel + + delegate: AbstractButton { + id: delegate + width: ListView.view.width + height: 30 + + background: Rectangle { + color: model.held ? Qt.rgba(1,0,0,0.5) : + model.numtx > 0 && model.balance == 0 ? Qt.rgba(1,1,1,0.25) : + model.type == 'receive' ? Qt.rgba(0,1,0,0.25) : + Qt.rgba(1,0.93,0,0.25) + Rectangle { + height: 1 + width: parent.width + anchors.top: parent.top + border.color: Material.accentColor + visible: model.index > 0 + } + } + RowLayout { + x: 10 + spacing: 5 + width: parent.width - 20 + anchors.verticalCenter: parent.verticalCenter + + Label { + font.pixelSize: 12 + text: model.type + } + Label { + font.pixelSize: 12 + font.family: "Courier" // TODO: use system monospace font + text: model.address + elide: Text.ElideMiddle + Layout.maximumWidth: delegate.width / 4 + } + Label { + font.pixelSize: 12 + text: model.label + elide: Text.ElideRight + Layout.minimumWidth: delegate.width / 4 + Layout.fillWidth: true + } + Label { + font.pixelSize: 12 + text: model.balance + } + Label { + font.pixelSize: 12 + text: model.numtx + } + } + } + } + + } + } + + Component.onCompleted: Daemon.currentWallet.addressModel.init_model() +} diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index a5388a183..06291f438 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -9,12 +9,35 @@ Item { property string title: Daemon.walletName property QtObject menu: Menu { - MenuItem { text: 'Wallets'; onTriggered: stack.push(Qt.resolvedUrl('Wallets.qml')) } - MenuItem { text: 'Network'; onTriggered: stack.push(Qt.resolvedUrl('NetworkStats.qml')) } + MenuItem { text: qsTr('Addresses'); onTriggered: stack.push(Qt.resolvedUrl('Addresses.qml')); visible: Daemon.currentWallet != null } + MenuItem { text: qsTr('Wallets'); onTriggered: stack.push(Qt.resolvedUrl('Wallets.qml')) } + MenuItem { text: qsTr('Network'); onTriggered: stack.push(Qt.resolvedUrl('NetworkStats.qml')) } + } + + ColumnLayout { + anchors.centerIn: parent + width: parent.width + spacing: 40 + visible: Daemon.currentWallet == null + + Label { + text: qsTr('No wallet loaded') + font.pixelSize: 24 + Layout.alignment: Qt.AlignHCenter + } + + Button { + text: qsTr('Open/Create Wallet') + Layout.alignment: Qt.AlignHCenter + onClicked: { + stack.push(Qt.resolvedUrl('Wallets.qml')) + } + } } ColumnLayout { anchors.fill: parent + visible: Daemon.currentWallet != null TabBar { id: tabbar diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index 7fe2e5156..2f9bdaa64 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -31,6 +31,7 @@ ApplicationWindow onClicked: stack.pop() } Item { + visible: Network.isTestNet width: column.width height: column.height MouseArea { @@ -46,7 +47,6 @@ ApplicationWindow Column { id: column - visible: Network.isTestNet Image { anchors.horizontalCenter: parent.horizontalCenter width: 16 @@ -63,15 +63,22 @@ ApplicationWindow } } + Image { + Layout.preferredWidth: 16 + Layout.preferredHeight: 16 + source: Daemon.currentWallet.isUptodate ? "../../icons/status_connected.png" : "../../icons/status_lagging.png" + } + Label { text: stack.currentItem.title elide: Label.ElideRight horizontalAlignment: Qt.AlignHCenter verticalAlignment: Qt.AlignVCenter Layout.fillWidth: true - font.pointSize: 10 + font.pixelSize: 14 font.bold: true } + ToolButton { text: qsTr("⋮") onClicked: { @@ -203,5 +210,10 @@ ApplicationWindow // var dialog = _openWallet.createObject(app) //dialog.open() } + function onWalletOpenError(error) { + console.log('wallet open error') + var dialog = app.messageDialog.createObject(app, {'text': error}) + dialog.open() + } } } diff --git a/electrum/gui/qml/qedaemon.py b/electrum/gui/qml/qedaemon.py index 8feeab858..ce6030e55 100644 --- a/electrum/gui/qml/qedaemon.py +++ b/electrum/gui/qml/qedaemon.py @@ -3,7 +3,7 @@ import os from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex -from electrum.util import register_callback, get_new_wallet_name +from electrum.util import register_callback, get_new_wallet_name, WalletFileException from electrum.logging import get_logger from electrum.wallet import Wallet, Abstract_Wallet from electrum.storage import WalletStorage, StorageReadWriteError @@ -90,7 +90,7 @@ class QEDaemon(QObject): walletRequiresPassword = pyqtSignal() activeWalletsChanged = pyqtSignal() availableWalletsChanged = pyqtSignal() - couldNotOpenFile = pyqtSignal() + walletOpenError = pyqtSignal([str], arguments=["error"]) @pyqtSlot() @pyqtSlot(str) @@ -107,21 +107,25 @@ class QEDaemon(QObject): try: storage = WalletStorage(self._path) if not storage.file_exists(): - self.couldNotOpenFile.emit() + self.walletOpenError.emit(qsTr('File not found')) return except StorageReadWriteError as e: - self.couldNotOpenFile.emit() + self.walletOpenError.emit('Storage read/write error') return - wallet = self.daemon.load_wallet(self._path, password) - if wallet != None: - self._loaded_wallets.add_wallet(wallet=wallet) - self._current_wallet = QEWallet(wallet) - self.walletLoaded.emit() - self.daemon.config.save_last_wallet(wallet) - else: - self._logger.info('password required but unset or incorrect') - self.walletRequiresPassword.emit() + try: + wallet = self.daemon.load_wallet(self._path, password) + if wallet != None: + self._loaded_wallets.add_wallet(wallet=wallet) + self._current_wallet = QEWallet(wallet) + self.walletLoaded.emit() + self.daemon.config.save_last_wallet(wallet) + else: + self._logger.info('password required but unset or incorrect') + self.walletRequiresPassword.emit() + except WalletFileException as e: + self._logger.error(str(e)) + self.walletOpenError.emit(str(e)) @pyqtProperty('QString') def path(self): diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 1ded4dc7b..060556c9a 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -8,9 +8,10 @@ from electrum import bitcoin from electrum.transaction import Transaction, tx_from_any, PartialTransaction, PartialTxOutput from electrum.invoices import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, PR_UNCONFIRMED, PR_TYPE_ONCHAIN, PR_TYPE_LN -class QETransactionsListModel(QAbstractListModel): - def __init__(self, parent=None): +class QETransactionListModel(QAbstractListModel): + def __init__(self, wallet, parent=None): super().__init__(parent) + self.wallet = wallet self.tx_history = [] _logger = get_logger(__name__) @@ -43,20 +44,95 @@ class QETransactionsListModel(QAbstractListModel): self.endResetModel() # initial model data - def set_history(self, history): + def init_model(self): + history = self.wallet.get_detailed_history(show_addresses = True) + txs = history['transactions'] + # use primitives + for tx in txs: + for output in tx['outputs']: + output['value'] = output['value'].value + self.clear() - self.beginInsertRows(QModelIndex(), 0, len(history) - 1) - self.tx_history = history + self.beginInsertRows(QModelIndex(), 0, len(txs) - 1) + self.tx_history = txs self.tx_history.reverse() self.endInsertRows() +class QEAddressListModel(QAbstractListModel): + def __init__(self, wallet, parent=None): + super().__init__(parent) + self.wallet = wallet + self.receive_addresses = [] + self.change_addresses = [] + + + _logger = get_logger(__name__) + + # define listmodel rolemap + _ROLE_NAMES=('type','address','label','balance','numtx', 'held') + _ROLE_KEYS = range(Qt.UserRole + 1, Qt.UserRole + 1 + len(_ROLE_NAMES)) + _ROLE_MAP = dict(zip(_ROLE_KEYS, [bytearray(x.encode()) for x in _ROLE_NAMES])) + + def rowCount(self, index): + return len(self.receive_addresses) + len(self.change_addresses) + + def roleNames(self): + return self._ROLE_MAP + + def data(self, index, role): + if index.row() > len(self.receive_addresses) - 1: + address = self.change_addresses[index.row() - len(self.receive_addresses)] + else: + address = self.receive_addresses[index.row()] + role_index = role - (Qt.UserRole + 1) + value = address[self._ROLE_NAMES[role_index]] + if isinstance(value, bool) or isinstance(value, list) or isinstance(value, int) or value is None: + return value + if isinstance(value, Satoshis): + return value.value + return str(value) + + def clear(self): + self.beginResetModel() + self.receive_addresses = [] + self.change_addresses = [] + self.endResetModel() + + # initial model data + @pyqtSlot() + def init_model(self): + r_addresses = self.wallet.get_receiving_addresses() + c_addresses = self.wallet.get_change_addresses() + n_addresses = len(r_addresses) + len(c_addresses) + + def insert_row(atype, alist, address): + item = {} + item['type'] = atype + item['address'] = address + item['numtx'] = self.wallet.get_address_history_len(address) + item['label'] = self.wallet.get_label(address) + c, u, x = self.wallet.get_addr_balance(address) + item['balance'] = c + u + x + item['held'] = self.wallet.is_frozen_address(address) + alist.append(item) + + self.clear() + self.beginInsertRows(QModelIndex(), 0, n_addresses - 1) + for address in r_addresses: + insert_row('receive', self.receive_addresses, address) + for address in c_addresses: + insert_row('change', self.change_addresses, address) + self.endInsertRows() + class QEWallet(QObject): def __init__(self, wallet, parent=None): super().__init__(parent) self.wallet = wallet - self._historyModel = QETransactionsListModel() - self.get_history() + self._historyModel = QETransactionListModel(wallet) + self._addressModel = QEAddressListModel(wallet) + self._historyModel.init_model() register_callback(self.on_request_status, ['request_status']) + register_callback(self.on_status, ['status']) _logger = get_logger(__name__) @@ -65,15 +141,18 @@ class QEWallet(QObject): requestStatus = pyqtSignal() def on_request_status(self, event, *args): self._logger.debug(str(event)) -# (wallet, addr, status) = args -# self._historyModel.add_tx() self.requestStatus.emit() historyModelChanged = pyqtSignal() - @pyqtProperty(QETransactionsListModel, notify=historyModelChanged) + @pyqtProperty(QETransactionListModel, notify=historyModelChanged) def historyModel(self): return self._historyModel + addressModelChanged = pyqtSignal() + @pyqtProperty(QEAddressListModel, notify=addressModelChanged) + def addressModel(self): + return self._addressModel + @pyqtProperty('QString', notify=dataChanged) def txinType(self): return self.wallet.get_txin_type(self.wallet.dummy_address()) @@ -115,22 +194,16 @@ class QEWallet(QObject): return c+x + def on_status(self, status): + self._logger.info('wallet: status update: ' + str(status)) + self.isUptodateChanged.emit() + # lightning feature? isUptodateChanged = pyqtSignal() @pyqtProperty(bool, notify=isUptodateChanged) def isUptodate(self): return self.wallet.is_up_to_date() - def get_history(self): - history = self.wallet.get_detailed_history(show_addresses = True) - txs = history['transactions'] - # use primitives - for tx in txs: - for output in tx['outputs']: - output['value'] = output['value'].value - self._historyModel.set_history(txs) - self.historyModelChanged.emit() - @pyqtSlot('QString', int, int, bool) def send_onchain(self, address, amount, fee=None, rbf=False): self._logger.info('send_onchain: ' + address + ' ' + str(amount))