Browse Source

qml: also show coins in Addresses page, and add a few filter options. Additionally, long press

now activates multi-select mode, and add action to (un)freeze selection.
master
Sander van Grieken 2 years ago
parent
commit
cf91d2e5cc
  1. 252
      electrum/gui/qml/components/Addresses.qml
  2. 6
      electrum/gui/qml/components/controls/AddressDelegate.qml
  3. 97
      electrum/gui/qml/components/controls/CoinDelegate.qml
  4. 195
      electrum/gui/qml/qeaddresslistmodel.py
  5. 5
      electrum/gui/qml/qemodelfilter.py
  6. 22
      electrum/gui/qml/qewallet.py

252
electrum/gui/qml/components/Addresses.qml

@ -2,6 +2,7 @@ import QtQuick 2.6
import QtQuick.Layouts 1.0
import QtQuick.Controls 2.0
import QtQuick.Controls.Material 2.0
import QtQml.Models 2.2
import org.electrum 1.0
@ -14,34 +15,246 @@ Pane {
padding: 0
ColumnLayout {
id: layout
anchors.fill: parent
spacing: 0
ElListView {
id: listview
ColumnLayout {
id: layout
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
model: Daemon.currentWallet.addressModel
currentIndex: -1
Pane {
id: filtersPane
Layout.fillWidth: true
GridLayout {
columns: 3
width: parent.width
section.property: 'type'
section.criteria: ViewSection.FullString
section.delegate: sectionDelegate
CheckBox {
id: showUsed
text: qsTr('Show Used')
enabled: listview.filterModel.showAddressesCoins != 2
onCheckedChanged: listview.filterModel.showUsed = checked
Component.onCompleted: checked = listview.filterModel.showUsed
}
delegate: AddressDelegate {
onClicked: {
var page = app.stack.push(Qt.resolvedUrl('AddressDetails.qml'), {'address': model.address})
page.addressDetailsChanged.connect(function() {
// update listmodel when details change
listview.model.updateAddress(model.address)
})
RowLayout {
Layout.columnSpan: 2
Layout.fillWidth: true
Layout.alignment: Qt.AlignRight
Label {
text: qsTr('Show')
}
ElComboBox {
id: showCoinsAddresses
textRole: 'text'
valueRole: 'value'
model: ListModel {
id: showCoinsAddressesModel
Component.onCompleted: {
// we need to fill the model like this, as ListElement can't evaluate script
showCoinsAddressesModel.append({'text': qsTr('Addresses'), 'value': 1})
showCoinsAddressesModel.append({'text': qsTr('Coins'), 'value': 2})
showCoinsAddressesModel.append({'text': qsTr('Both'), 'value': 3})
showCoinsAddresses.currentIndex = 0
for (let i=0; i < showCoinsAddressesModel.count; i++) {
if (showCoinsAddressesModel.get(i).value == listview.filterModel.showAddressesCoins) {
showCoinsAddresses.currentIndex = i
break
}
}
}
}
onCurrentValueChanged: {
if (activeFocus && currentValue) {
listview.filterModel.showAddressesCoins = currentValue
}
}
}
}
RowLayout {
Layout.columnSpan: 3
Layout.fillWidth: true
TextField {
id: searchEdit
Layout.fillWidth: true
placeholderText: qsTr('text search')
onTextChanged: listview.filterModel.filterText = text
}
Image {
source: Qt.resolvedUrl('../../icons/zoom.png')
sourceSize.width: constants.iconSizeMedium
sourceSize.height: constants.iconSizeMedium
}
}
}
}
ScrollIndicator.vertical: ScrollIndicator { }
Frame {
id: channelsFrame
Layout.fillWidth: true
Layout.fillHeight: true
verticalPadding: 0
horizontalPadding: 0
background: PaneInsetBackground {}
ElListView {
id: listview
anchors.fill: parent
clip: true
property QtObject backingModel: Daemon.currentWallet.addressCoinModel
property QtObject filterModel: Daemon.currentWallet.addressCoinModel.filterModel
property bool selectMode: false
property bool freeze: true
model: visualModel
currentIndex: -1
section.property: 'type'
section.criteria: ViewSection.FullString
section.delegate: sectionDelegate
function getSelectedItems() {
var items = []
for (let i = 0; i < selectedGroup.count; i++) {
let modelitem = selectedGroup.get(i).model
if (modelitem.outpoint)
items.push(modelitem.outpoint)
else
items.push(modelitem.address)
}
return items
}
DelegateModel {
id: visualModel
model: listview.filterModel
groups: [
DelegateModelGroup {
id: selectedGroup;
name: 'selected'
onCountChanged: {
if (count == 0)
listview.selectMode = false
}
}
]
delegate: Loader {
id: loader
width: parent.width
sourceComponent: model.outpoint ? _coinDelegate : _addressDelegate
function toggle() {
loader.DelegateModel.inSelected = !loader.DelegateModel.inSelected
}
Component {
id: _addressDelegate
AddressDelegate {
id: addressDelegate
width: parent.width
property bool selected: loader.DelegateModel.inSelected
highlighted: selected
onClicked: {
if (!listview.selectMode) {
var page = app.stack.push(Qt.resolvedUrl('AddressDetails.qml'), {
address: model.address
})
page.addressDetailsChanged.connect(function() {
// update listmodel when details change
listview.backingModel.updateAddress(model.address)
})
} else {
loader.toggle()
}
}
onPressAndHold: {
loader.toggle()
if (!listview.selectMode && selectedGroup.count > 0)
listview.selectMode = true
}
}
}
Component {
id: _coinDelegate
Pane {
height: coinDelegate.height
padding: 0
background: Rectangle {
color: Qt.darker(constants.darkerBackground, 1.10)
}
CoinDelegate {
id: coinDelegate
width: parent.width
property bool selected: loader.DelegateModel.inSelected
highlighted: selected
onClicked: {
if (!listview.selectMode) {
var page = app.stack.push(Qt.resolvedUrl('TxDetails.qml'), {
txid: model.txid
})
} else {
loader.toggle()
}
}
onPressAndHold: {
loader.toggle()
if (!listview.selectMode && selectedGroup.count > 0)
listview.selectMode = true
}
}
}
}
}
}
add: Transition {
NumberAnimation { properties: "opacity"; from: 0.0; to: 1.0; duration: 300
easing.type: Easing.OutQuad
}
}
onSelectModeChanged: {
if (selectMode) {
listview.freeze = !selectedGroup.get(0).model.held
}
}
ScrollIndicator.vertical: ScrollIndicator { }
}
}
}
ButtonContainer {
Layout.fillWidth: true
FlatButton {
Layout.fillWidth: true
Layout.preferredWidth: 1
text: listview.freeze ? qsTr('Freeze') : qsTr('Unfreeze')
icon.source: '../../icons/seal.png'
visible: listview.selectMode
onClicked: {
var items = listview.getSelectedItems()
listview.backingModel.setFrozenForItems(listview.freeze, items)
selectedGroup.remove(0, selectedGroup.count)
}
}
FlatButton {
Layout.fillWidth: true
Layout.preferredWidth: 1
text: qsTr('Pay from...')
icon.source: '../../icons/tab_send.png'
visible: listview.selectMode
enabled: false // TODO
onClicked: {
//
}
}
}
}
@ -52,7 +265,6 @@ Pane {
id: root
width: ListView.view.width
height: childrenRect.height
required property string section
property string section_label: section == 'receive'
? qsTr('receive addresses')
@ -74,6 +286,6 @@ Pane {
}
Component.onCompleted: {
Daemon.currentWallet.addressModel.initModel()
Daemon.currentWallet.addressCoinModel.initModel()
}
}

6
electrum/gui/qml/components/controls/AddressDelegate.qml

@ -27,9 +27,9 @@ ItemDelegate {
Label {
id: indexLabel
font.bold: true
text: model.iaddr < 10
? '#' + ('0'+model.iaddr).slice(-2)
: '#' + model.iaddr
text: model.addridx < 10
? '#' + ('0'+model.addridx).slice(-2)
: '#' + model.addridx
Layout.fillWidth: true
}
Label {

97
electrum/gui/qml/components/controls/CoinDelegate.qml

@ -0,0 +1,97 @@
import QtQuick 2.6
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.0
import QtQuick.Controls.Material 2.0
import org.electrum 1.0
ItemDelegate {
id: delegate
width: ListView.view.width
height: delegateLayout.height
highlighted: ListView.isCurrentItem
font.pixelSize: constants.fontSizeMedium // set default font size for child controls
ColumnLayout {
id: delegateLayout
width: parent.width
spacing: 0
GridLayout {
columns: 3
Layout.topMargin: constants.paddingSmall
Layout.leftMargin: constants.paddingLarge + 2*constants.paddingLarge
Layout.rightMargin: constants.paddingLarge
Rectangle {
id: useIndicator
Layout.rowSpan: 2
Layout.preferredWidth: constants.iconSizeSmall
Layout.preferredHeight: constants.iconSizeSmall
Layout.alignment: Qt.AlignTop
color: model.held
? constants.colorAddressFrozen
: constants.colorAddressUsedWithBalance
}
RowLayout {
Layout.fillWidth: true
Label {
font.family: FixedFont
text: model.outpoint
elide: Text.ElideMiddle
Layout.preferredWidth: implicitWidth + constants.paddingMedium
}
Label {
Layout.fillWidth: true
visible: model.short_id
font.family: FixedFont
font.pixelSize: constants.fontSizeSmall
text: '[' + model.short_id + ']'
}
Item {
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
visible: !model.short_id
Image {
source: Qt.resolvedUrl('../../../icons/unconfirmed.png')
sourceSize.width: constants.iconSizeSmall
sourceSize.height: constants.iconSizeSmall
}
}
}
RowLayout {
Label {
font.family: FixedFont
text: Config.formatSats(model.amount, false)
visible: model.amount.satsInt != 0
}
Label {
color: Material.accentColor
text: Config.baseUnit
visible: model.amount.satsInt != 0
}
}
Label {
id: labelLabel
Layout.fillWidth: true
Layout.columnSpan: 2
visible: model.label
font.pixelSize: constants.fontSizeMedium
text: model.label
elide: Text.ElideRight
maximumLineCount: 2
wrapMode: Text.WordWrap
}
}
Item {
Layout.preferredWidth: 1
Layout.preferredHeight: constants.paddingSmall
}
}
}

195
electrum/gui/qml/qeaddresslistmodel.py

@ -1,7 +1,6 @@
import itertools
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, List
from PyQt5.QtCore import pyqtSlot
from PyQt5.QtCore import pyqtSlot, QSortFilterProxyModel, pyqtSignal, pyqtProperty
from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex
from electrum.logging import get_logger
@ -11,38 +10,126 @@ from .qetypes import QEAmount
if TYPE_CHECKING:
from electrum.wallet import Abstract_Wallet
from electrum.transaction import PartialTxInput
class QEAddressListModel(QAbstractListModel):
class QEAddressCoinFilterProxyModel(QSortFilterProxyModel):
_logger = get_logger(__name__)
def __init__(self, parent_model, parent=None):
super().__init__(parent)
self._filter_text = None
self._show_coins = True
self._show_addresses = True
self._show_used = False
self._parent_model = parent_model
self.setSourceModel(parent_model)
countChanged = pyqtSignal()
@pyqtProperty(int, notify=countChanged)
def count(self):
return self.rowCount(QModelIndex())
def filterAcceptsRow(self, s_row, s_parent):
parent_model = self.sourceModel()
addridx = parent_model.data(parent_model.index(s_row, 0, s_parent), parent_model._ROLE_RMAP['addridx'])
if addridx is None: # coin
if not self._show_coins:
return False
else:
if not self._show_addresses:
return False
balance = parent_model.data(parent_model.index(s_row, 0, s_parent), parent_model._ROLE_RMAP['balance'])
numtx = parent_model.data(parent_model.index(s_row, 0, s_parent), parent_model._ROLE_RMAP['numtx'])
if balance.isEmpty and numtx and not self._show_used:
return False
if self._filter_text:
label = parent_model.data(parent_model.index(s_row, 0, s_parent), parent_model._ROLE_RMAP['label'])
address = parent_model.data(parent_model.index(s_row, 0, s_parent), parent_model._ROLE_RMAP['address'])
for item in [label, address]:
if self._filter_text in str(item):
return True
return False
return True
showAddressesCoinsChanged = pyqtSignal()
@pyqtProperty(int, notify=showAddressesCoinsChanged)
def showAddressesCoins(self) -> int:
result = 0
if self._show_addresses:
result += 1
if self._show_coins:
result += 2
return result
@showAddressesCoins.setter
def showAddressesCoins(self, show_addresses_coins: int):
show_addresses = show_addresses_coins in [1, 3]
show_coins = show_addresses_coins in [2, 3]
if self._show_addresses != show_addresses or self._show_coins != show_coins:
self._show_addresses = show_addresses
self._show_coins = show_coins
self.invalidateFilter()
self.showAddressesCoinsChanged.emit()
showUsedChanged = pyqtSignal()
@pyqtProperty(bool, notify=showUsedChanged)
def showUsed(self) -> bool:
return self._show_used
@showUsed.setter
def showUsed(self, show_used: bool):
if self._show_used != show_used:
self._show_used = show_used
self.invalidateFilter()
self.showUsedChanged.emit()
filterTextChanged = pyqtSignal()
@pyqtProperty(str, notify=filterTextChanged)
def filterText(self) -> str:
return self._filter_text
@filterText.setter
def filterText(self, filter_text: str):
if self._filter_text != filter_text:
self._filter_text = filter_text
self.invalidateFilter()
self.filterTextChanged.emit()
class QEAddressCoinListModel(QAbstractListModel):
_logger = get_logger(__name__)
# define listmodel rolemap
_ROLE_NAMES=('type', 'iaddr', 'address', 'label', 'balance', 'numtx', 'held')
_ROLE_NAMES=('type', 'addridx', 'address', 'label', 'balance', 'numtx', 'held', 'height', 'amount', 'outpoint',
'short_outpoint', 'short_id', 'txid')
_ROLE_KEYS = range(Qt.UserRole, Qt.UserRole + len(_ROLE_NAMES))
_ROLE_MAP = dict(zip(_ROLE_KEYS, [bytearray(x.encode()) for x in _ROLE_NAMES]))
_ROLE_RMAP = dict(zip(_ROLE_NAMES, _ROLE_KEYS))
def __init__(self, wallet: 'Abstract_Wallet', parent=None):
super().__init__(parent)
self.wallet = wallet
self._receive_addresses = []
self._change_addresses = []
self._items = []
self._filterModel = None
self._dirty = True
self.initModel()
def rowCount(self, index):
return len(self._receive_addresses) + len(self._change_addresses)
return len(self._items)
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()]
address = self._items[index.row()]
role_index = role - Qt.UserRole
value = address[self._ROLE_NAMES[role_index]]
try:
value = address[self._ROLE_NAMES[role_index]]
except KeyError:
return None
if isinstance(value, (bool, list, int, str, QEAmount)) or value is None:
return value
if isinstance(value, Satoshis):
@ -51,13 +138,14 @@ class QEAddressListModel(QAbstractListModel):
def clear(self):
self.beginResetModel()
self._receive_addresses = []
self._change_addresses = []
self._items = []
self.endResetModel()
def addr_to_model(self, address):
def addr_to_model(self, addrtype: str, addridx: int, address: str):
c, u, x = self.wallet.get_addr_balance(address)
item = {
'type': addrtype,
'addridx': addridx,
'address': address,
'numtx': self.wallet.adb.get_address_history_len(address),
'label': self.wallet.get_label_for_address(address),
@ -66,6 +154,27 @@ class QEAddressListModel(QAbstractListModel):
}
return item
def coin_to_model(self, addrtype: str, coin: 'PartialTxInput'):
txid = coin.prevout.txid.hex()
short_id = ''
# check below duplicated from TxInput as we cannot get short_id unambiguously
if coin.block_txpos is not None and coin.block_txpos >= 0:
short_id = str(coin.short_id)
item = {
'type': addrtype,
'amount': QEAmount(amount_sat=coin.value_sats()),
'address': coin.address,
'height': coin.block_height,
'outpoint': coin.prevout.to_str(),
'short_outpoint': coin.prevout.short_name(),
'short_id': short_id,
'txid': txid,
'label': self.wallet.get_label_for_txid(txid) or '',
'held': self.wallet.is_frozen_coin(coin),
'coin': coin
}
return item
@pyqtSlot()
def setDirty(self):
self._dirty = True
@ -80,36 +189,70 @@ class QEAddressListModel(QAbstractListModel):
c_addresses = self.wallet.get_change_addresses() if self.wallet.wallet_type != 'imported' else []
n_addresses = len(r_addresses) + len(c_addresses)
def insert_row(atype, alist, address, iaddr):
item = self.addr_to_model(address)
item['type'] = atype
item['iaddr'] = iaddr
alist.append(item)
def insert_address(atype, address, addridx):
item = self.addr_to_model(atype, addridx, address)
self._items.append(item)
utxos = self.wallet.get_utxos([address])
utxos.sort(key=lambda x: x.block_height)
for i, coin in enumerate(utxos):
self._items.append(self.coin_to_model(atype, coin))
self.clear()
self.beginInsertRows(QModelIndex(), 0, n_addresses - 1)
if self.wallet.wallet_type != 'imported':
for i, address in enumerate(r_addresses):
insert_row('receive', self._receive_addresses, address, i)
insert_address('receive', address, i)
for i, address in enumerate(c_addresses):
insert_row('change', self._change_addresses, address, i)
insert_address('change', address, i)
else:
for i, address in enumerate(r_addresses):
insert_row('imported', self._receive_addresses, address, i)
insert_address('imported', address, i)
self.endInsertRows()
self._dirty = False
@pyqtSlot(str)
def updateAddress(self, address):
for i, a in enumerate(itertools.chain(self._receive_addresses, self._change_addresses)):
for i, a in enumerate(self._items):
if a['address'] == address:
self.do_update(i, a)
return
def updateCoin(self, outpoint):
for i, a in enumerate(self._items):
if a.get('outpoint') == outpoint:
self.do_update(i, a)
return
def do_update(self, modelindex, modelitem):
mi = self.createIndex(modelindex, 0)
self._logger.debug(repr(modelitem))
modelitem.update(self.addr_to_model(modelitem['address']))
if modelitem.get('outpoint'):
modelitem.update(self.coin_to_model(modelitem['type'], modelitem['coin']))
else:
modelitem.update(self.addr_to_model(modelitem['type'], modelitem['addridx'], modelitem['address']))
self._logger.debug(repr(modelitem))
self.dataChanged.emit(mi, mi, self._ROLE_KEYS)
filterModelChanged = pyqtSignal()
@pyqtProperty(QEAddressCoinFilterProxyModel, notify=filterModelChanged)
def filterModel(self):
if self._filterModel is None:
self._filterModel = QEAddressCoinFilterProxyModel(self)
return self._filterModel
@pyqtSlot(bool, list)
def setFrozenForItems(self, freeze: bool, items: List[str]):
self._logger.debug(f'set frozen to {freeze} for {items!r}')
coins = list(filter(lambda x: ':' in x, items))
if len(coins):
self.wallet.set_frozen_state_of_coins(coins, freeze)
for coin in coins:
self.updateCoin(coin)
addresses = list(filter(lambda x: ':' not in x, items))
if len(addresses):
self.wallet.set_frozen_state_of_addresses(addresses, freeze)
for address in addresses:
self.updateAddress(address)

5
electrum/gui/qml/qemodelfilter.py

@ -1,7 +1,8 @@
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QSortFilterProxyModel, QModelIndex
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QSortFilterProxyModel, QModelIndex, pyqtSlot
from electrum.logging import get_logger
class QEFilterProxyModel(QSortFilterProxyModel):
_logger = get_logger(__name__)
@ -18,8 +19,10 @@ class QEFilterProxyModel(QSortFilterProxyModel):
def isCustomFilter(self):
return self._filter_value is not None
@pyqtSlot(str)
def setFilterValue(self, filter_value):
self._filter_value = filter_value
self.invalidate()
def filterAcceptsRow(self, s_row, s_parent):
if not self.isCustomFilter:

22
electrum/gui/qml/qewallet.py

@ -19,7 +19,7 @@ from electrum.wallet import Multisig_Wallet
from electrum.crypto import pw_decode_with_version_and_mac
from .auth import AuthMixin, auth_protect
from .qeaddresslistmodel import QEAddressListModel
from .qeaddresslistmodel import QEAddressCoinListModel
from .qechannellistmodel import QEChannelListModel
from .qeinvoicelistmodel import QEInvoiceListModel, QERequestListModel
from .qetransactionlistmodel import QETransactionListModel
@ -91,7 +91,7 @@ class QEWallet(AuthMixin, QObject, QtEventListener):
self._synchronizing_progress = ''
self._historyModel = None
self._addressModel = None
self._addressCoinModel = None
self._requestModel = None
self._invoiceModel = None
self._channelModel = None
@ -184,7 +184,7 @@ class QEWallet(AuthMixin, QObject, QtEventListener):
if wallet == self.wallet:
self._logger.info(f'new transaction {tx.txid()}')
self.add_tx_notification(tx)
self.addressModel.setDirty()
self.addressCoinModel.setDirty()
self.historyModel.setDirty() # assuming wallet.is_up_to_date triggers after
self.balanceChanged.emit()
@ -198,7 +198,7 @@ class QEWallet(AuthMixin, QObject, QtEventListener):
def on_event_removed_transaction(self, wallet, tx):
if wallet == self.wallet:
self._logger.info(f'removed transaction {tx.txid()}')
self.addressModel.setDirty()
self.addressCoinModel.setDirty()
self.historyModel.initModel(True) # setDirty()?
self.balanceChanged.emit()
@ -295,12 +295,12 @@ class QEWallet(AuthMixin, QObject, QtEventListener):
self._historyModel = QETransactionListModel(self.wallet)
return self._historyModel
addressModelChanged = pyqtSignal()
@pyqtProperty(QEAddressListModel, notify=addressModelChanged)
def addressModel(self):
if self._addressModel is None:
self._addressModel = QEAddressListModel(self.wallet)
return self._addressModel
addressCoinModelChanged = pyqtSignal()
@pyqtProperty(QEAddressCoinListModel, notify=addressCoinModelChanged)
def addressCoinModel(self):
if self._addressCoinModel is None:
self._addressCoinModel = QEAddressCoinListModel(self.wallet)
return self._addressCoinModel
requestModelChanged = pyqtSignal()
@pyqtProperty(QERequestListModel, notify=requestModelChanged)
@ -658,7 +658,7 @@ class QEWallet(AuthMixin, QObject, QtEventListener):
assert key is not None
self._logger.debug(f'created request with key {key} addr {addr}')
self.addressModel.setDirty()
self.addressCoinModel.setDirty()
self.requestModel.add_invoice(self.wallet.get_request(key))
self.requestCreateSuccess.emit(key)

Loading…
Cancel
Save