add QEAddressListModel and initial Addresses.qml page.
show sane main view when no wallet loaded. show error dialog when wallet could not be loaded. show wallet up_to_date indicator in title bar. refactor QETransactionListModel to be more self-contained.
This commit is contained in:
88
electrum/gui/qml/components/Addresses.qml
Normal file
88
electrum/gui/qml/components/Addresses.qml
Normal file
@@ -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()
|
||||||
|
}
|
||||||
@@ -9,12 +9,35 @@ Item {
|
|||||||
property string title: Daemon.walletName
|
property string title: Daemon.walletName
|
||||||
|
|
||||||
property QtObject menu: Menu {
|
property QtObject menu: Menu {
|
||||||
MenuItem { text: 'Wallets'; onTriggered: stack.push(Qt.resolvedUrl('Wallets.qml')) }
|
MenuItem { text: qsTr('Addresses'); onTriggered: stack.push(Qt.resolvedUrl('Addresses.qml')); visible: Daemon.currentWallet != null }
|
||||||
MenuItem { text: 'Network'; onTriggered: stack.push(Qt.resolvedUrl('NetworkStats.qml')) }
|
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 {
|
ColumnLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
visible: Daemon.currentWallet != null
|
||||||
|
|
||||||
TabBar {
|
TabBar {
|
||||||
id: tabbar
|
id: tabbar
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ ApplicationWindow
|
|||||||
onClicked: stack.pop()
|
onClicked: stack.pop()
|
||||||
}
|
}
|
||||||
Item {
|
Item {
|
||||||
|
visible: Network.isTestNet
|
||||||
width: column.width
|
width: column.width
|
||||||
height: column.height
|
height: column.height
|
||||||
MouseArea {
|
MouseArea {
|
||||||
@@ -46,7 +47,6 @@ ApplicationWindow
|
|||||||
|
|
||||||
Column {
|
Column {
|
||||||
id: column
|
id: column
|
||||||
visible: Network.isTestNet
|
|
||||||
Image {
|
Image {
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
width: 16
|
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 {
|
Label {
|
||||||
text: stack.currentItem.title
|
text: stack.currentItem.title
|
||||||
elide: Label.ElideRight
|
elide: Label.ElideRight
|
||||||
horizontalAlignment: Qt.AlignHCenter
|
horizontalAlignment: Qt.AlignHCenter
|
||||||
verticalAlignment: Qt.AlignVCenter
|
verticalAlignment: Qt.AlignVCenter
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
font.pointSize: 10
|
font.pixelSize: 14
|
||||||
font.bold: true
|
font.bold: true
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolButton {
|
ToolButton {
|
||||||
text: qsTr("⋮")
|
text: qsTr("⋮")
|
||||||
onClicked: {
|
onClicked: {
|
||||||
@@ -203,5 +210,10 @@ ApplicationWindow
|
|||||||
// var dialog = _openWallet.createObject(app)
|
// var dialog = _openWallet.createObject(app)
|
||||||
//dialog.open()
|
//dialog.open()
|
||||||
}
|
}
|
||||||
|
function onWalletOpenError(error) {
|
||||||
|
console.log('wallet open error')
|
||||||
|
var dialog = app.messageDialog.createObject(app, {'text': error})
|
||||||
|
dialog.open()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import os
|
|||||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl
|
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl
|
||||||
from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex
|
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.logging import get_logger
|
||||||
from electrum.wallet import Wallet, Abstract_Wallet
|
from electrum.wallet import Wallet, Abstract_Wallet
|
||||||
from electrum.storage import WalletStorage, StorageReadWriteError
|
from electrum.storage import WalletStorage, StorageReadWriteError
|
||||||
@@ -90,7 +90,7 @@ class QEDaemon(QObject):
|
|||||||
walletRequiresPassword = pyqtSignal()
|
walletRequiresPassword = pyqtSignal()
|
||||||
activeWalletsChanged = pyqtSignal()
|
activeWalletsChanged = pyqtSignal()
|
||||||
availableWalletsChanged = pyqtSignal()
|
availableWalletsChanged = pyqtSignal()
|
||||||
couldNotOpenFile = pyqtSignal()
|
walletOpenError = pyqtSignal([str], arguments=["error"])
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
@@ -107,21 +107,25 @@ class QEDaemon(QObject):
|
|||||||
try:
|
try:
|
||||||
storage = WalletStorage(self._path)
|
storage = WalletStorage(self._path)
|
||||||
if not storage.file_exists():
|
if not storage.file_exists():
|
||||||
self.couldNotOpenFile.emit()
|
self.walletOpenError.emit(qsTr('File not found'))
|
||||||
return
|
return
|
||||||
except StorageReadWriteError as e:
|
except StorageReadWriteError as e:
|
||||||
self.couldNotOpenFile.emit()
|
self.walletOpenError.emit('Storage read/write error')
|
||||||
return
|
return
|
||||||
|
|
||||||
wallet = self.daemon.load_wallet(self._path, password)
|
try:
|
||||||
if wallet != None:
|
wallet = self.daemon.load_wallet(self._path, password)
|
||||||
self._loaded_wallets.add_wallet(wallet=wallet)
|
if wallet != None:
|
||||||
self._current_wallet = QEWallet(wallet)
|
self._loaded_wallets.add_wallet(wallet=wallet)
|
||||||
self.walletLoaded.emit()
|
self._current_wallet = QEWallet(wallet)
|
||||||
self.daemon.config.save_last_wallet(wallet)
|
self.walletLoaded.emit()
|
||||||
else:
|
self.daemon.config.save_last_wallet(wallet)
|
||||||
self._logger.info('password required but unset or incorrect')
|
else:
|
||||||
self.walletRequiresPassword.emit()
|
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')
|
@pyqtProperty('QString')
|
||||||
def path(self):
|
def path(self):
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ from electrum import bitcoin
|
|||||||
from electrum.transaction import Transaction, tx_from_any, PartialTransaction, PartialTxOutput
|
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
|
from electrum.invoices import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, PR_UNCONFIRMED, PR_TYPE_ONCHAIN, PR_TYPE_LN
|
||||||
|
|
||||||
class QETransactionsListModel(QAbstractListModel):
|
class QETransactionListModel(QAbstractListModel):
|
||||||
def __init__(self, parent=None):
|
def __init__(self, wallet, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
self.wallet = wallet
|
||||||
self.tx_history = []
|
self.tx_history = []
|
||||||
|
|
||||||
_logger = get_logger(__name__)
|
_logger = get_logger(__name__)
|
||||||
@@ -43,20 +44,95 @@ class QETransactionsListModel(QAbstractListModel):
|
|||||||
self.endResetModel()
|
self.endResetModel()
|
||||||
|
|
||||||
# initial model data
|
# 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.clear()
|
||||||
self.beginInsertRows(QModelIndex(), 0, len(history) - 1)
|
self.beginInsertRows(QModelIndex(), 0, len(txs) - 1)
|
||||||
self.tx_history = history
|
self.tx_history = txs
|
||||||
self.tx_history.reverse()
|
self.tx_history.reverse()
|
||||||
self.endInsertRows()
|
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):
|
class QEWallet(QObject):
|
||||||
def __init__(self, wallet, parent=None):
|
def __init__(self, wallet, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.wallet = wallet
|
self.wallet = wallet
|
||||||
self._historyModel = QETransactionsListModel()
|
self._historyModel = QETransactionListModel(wallet)
|
||||||
self.get_history()
|
self._addressModel = QEAddressListModel(wallet)
|
||||||
|
self._historyModel.init_model()
|
||||||
register_callback(self.on_request_status, ['request_status'])
|
register_callback(self.on_request_status, ['request_status'])
|
||||||
|
register_callback(self.on_status, ['status'])
|
||||||
|
|
||||||
_logger = get_logger(__name__)
|
_logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -65,15 +141,18 @@ class QEWallet(QObject):
|
|||||||
requestStatus = pyqtSignal()
|
requestStatus = pyqtSignal()
|
||||||
def on_request_status(self, event, *args):
|
def on_request_status(self, event, *args):
|
||||||
self._logger.debug(str(event))
|
self._logger.debug(str(event))
|
||||||
# (wallet, addr, status) = args
|
|
||||||
# self._historyModel.add_tx()
|
|
||||||
self.requestStatus.emit()
|
self.requestStatus.emit()
|
||||||
|
|
||||||
historyModelChanged = pyqtSignal()
|
historyModelChanged = pyqtSignal()
|
||||||
@pyqtProperty(QETransactionsListModel, notify=historyModelChanged)
|
@pyqtProperty(QETransactionListModel, notify=historyModelChanged)
|
||||||
def historyModel(self):
|
def historyModel(self):
|
||||||
return self._historyModel
|
return self._historyModel
|
||||||
|
|
||||||
|
addressModelChanged = pyqtSignal()
|
||||||
|
@pyqtProperty(QEAddressListModel, notify=addressModelChanged)
|
||||||
|
def addressModel(self):
|
||||||
|
return self._addressModel
|
||||||
|
|
||||||
@pyqtProperty('QString', notify=dataChanged)
|
@pyqtProperty('QString', notify=dataChanged)
|
||||||
def txinType(self):
|
def txinType(self):
|
||||||
return self.wallet.get_txin_type(self.wallet.dummy_address())
|
return self.wallet.get_txin_type(self.wallet.dummy_address())
|
||||||
@@ -115,22 +194,16 @@ class QEWallet(QObject):
|
|||||||
|
|
||||||
return c+x
|
return c+x
|
||||||
|
|
||||||
|
def on_status(self, status):
|
||||||
|
self._logger.info('wallet: status update: ' + str(status))
|
||||||
|
self.isUptodateChanged.emit()
|
||||||
|
|
||||||
# lightning feature?
|
# lightning feature?
|
||||||
isUptodateChanged = pyqtSignal()
|
isUptodateChanged = pyqtSignal()
|
||||||
@pyqtProperty(bool, notify=isUptodateChanged)
|
@pyqtProperty(bool, notify=isUptodateChanged)
|
||||||
def isUptodate(self):
|
def isUptodate(self):
|
||||||
return self.wallet.is_up_to_date()
|
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)
|
@pyqtSlot('QString', int, int, bool)
|
||||||
def send_onchain(self, address, amount, fee=None, rbf=False):
|
def send_onchain(self, address, amount, fee=None, rbf=False):
|
||||||
self._logger.info('send_onchain: ' + address + ' ' + str(amount))
|
self._logger.info('send_onchain: ' + address + ' ' + str(amount))
|
||||||
|
|||||||
Reference in New Issue
Block a user