diff --git a/electrum/gui/qml/components/Constants.qml b/electrum/gui/qml/components/Constants.qml index 01c0865e9..bd4d9e0ea 100644 --- a/electrum/gui/qml/components/Constants.qml +++ b/electrum/gui/qml/components/Constants.qml @@ -3,6 +3,7 @@ import QtQuick.Controls.Material 2.0 Item { readonly property int paddingTiny: 4 + readonly property int paddingXSmall: 6 readonly property int paddingSmall: 8 readonly property int paddingMedium: 12 readonly property int paddingLarge: 16 @@ -25,4 +26,5 @@ Item { property color colorCredit: "#ff80ff80" property color colorDebit: "#ffff8080" property color mutedForeground: 'gray' //Qt.lighter(Material.background, 2) + property color colorMine: "yellow" } diff --git a/electrum/gui/qml/components/History.qml b/electrum/gui/qml/components/History.qml index e94388919..84d584982 100644 --- a/electrum/gui/qml/components/History.qml +++ b/electrum/gui/qml/components/History.qml @@ -66,6 +66,14 @@ Pane { Layout.fillWidth: true Layout.preferredHeight: txinfo.height + onClicked: { + var page = app.stack.push(Qt.resolvedUrl('TxDetails.qml'), {'txid': model.txid}) + page.txDetailsChanged.connect(function() { + // update listmodel when details change + visualModel.model.update_tx_label(model.txid, page.label) + }) + } + GridLayout { id: txinfo columns: 3 diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml new file mode 100644 index 000000000..bef89c949 --- /dev/null +++ b/electrum/gui/qml/components/TxDetails.qml @@ -0,0 +1,257 @@ +import QtQuick 2.6 +import QtQuick.Layouts 1.0 +import QtQuick.Controls 2.3 +import QtQuick.Controls.Material 2.0 + +import org.electrum 1.0 + +import "controls" + +Pane { + id: root + width: parent.width + height: parent.height + + property string title: qsTr("Transaction details") + + property string txid + + property alias label: txdetails.label + + signal txDetailsChanged + + property QtObject menu: Menu { + id: menu + MenuItem { + icon.color: 'transparent' + action: Action { + text: qsTr('Bump fee') + enabled: txdetails.canBump + //onTriggered: + } + } + MenuItem { + icon.color: 'transparent' + action: Action { + text: qsTr('Cancel double-spend') + enabled: txdetails.canCancel + } + } + } + + Flickable { + anchors.fill: parent + contentHeight: rootLayout.height + clip: true + interactive: height < contentHeight + + GridLayout { + id: rootLayout + width: parent.width + columns: 2 + + Label { + text: qsTr('Status') + color: Material.accentColor + } + + Label { + text: txdetails.status + } + + Label { + text: qsTr('Mempool depth') + color: Material.accentColor + visible: !txdetails.isMined + } + + Label { + text: txdetails.mempoolDepth + visible: !txdetails.isMined + } + + Label { + text: qsTr('Date') + color: Material.accentColor + } + + Label { + text: txdetails.date + } + + Label { + text: txdetails.amount.satsInt > 0 + ? qsTr('Amount received') + : qsTr('Amount sent') + color: Material.accentColor + } + + RowLayout { + Label { + text: Config.formatSats(txdetails.amount) + } + Label { + text: Config.baseUnit + color: Material.accentColor + } + } + + Label { + text: qsTr('Transaction fee') + color: Material.accentColor + } + + RowLayout { + Label { + text: Config.formatSats(txdetails.fee) + } + Label { + text: Config.baseUnit + color: Material.accentColor + } + } + + Label { + text: qsTr('Transaction ID') + Layout.columnSpan: 2 + color: Material.accentColor + } + + TextHighlightPane { + Layout.columnSpan: 2 + Layout.fillWidth: true + padding: 0 + leftPadding: constants.paddingSmall + + RowLayout { + width: parent.width + Label { + text: root.txid + font.pixelSize: constants.fontSizeLarge + font.family: FixedFont + Layout.fillWidth: true + wrapMode: Text.Wrap + } + ToolButton { + icon.source: '../../icons/share.png' + icon.color: 'transparent' + onClicked: { + var dialog = share.createObject(root, { 'title': qsTr('Transaction ID'), 'text': root.txid }) + dialog.open() + } + } + } + } + + Label { + text: qsTr('Label') + Layout.columnSpan: 2 + color: Material.accentColor + } + + TextHighlightPane { + id: labelContent + + property bool editmode: false + + Layout.columnSpan: 2 + Layout.fillWidth: true + padding: 0 + leftPadding: constants.paddingSmall + + RowLayout { + width: parent.width + Label { + visible: !labelContent.editmode + text: txdetails.label + wrapMode: Text.Wrap + Layout.fillWidth: true + font.pixelSize: constants.fontSizeLarge + } + ToolButton { + visible: !labelContent.editmode + icon.source: '../../icons/pen.png' + icon.color: 'transparent' + onClicked: { + labelEdit.text = txdetails.label + labelContent.editmode = true + } + } + TextField { + id: labelEdit + visible: labelContent.editmode + text: txdetails.label + font.pixelSize: constants.fontSizeLarge + Layout.fillWidth: true + } + ToolButton { + visible: labelContent.editmode + icon.source: '../../icons/confirmed.png' + icon.color: 'transparent' + onClicked: { + labelContent.editmode = false + txdetails.set_label(labelEdit.text) + } + } + ToolButton { + visible: labelContent.editmode + icon.source: '../../icons/delete.png' + icon.color: 'transparent' + onClicked: labelContent.editmode = false + } + } + } + + + Label { + text: qsTr('Outputs') + Layout.columnSpan: 2 + color: Material.accentColor + } + + Repeater { + model: txdetails.outputs + delegate: TextHighlightPane { + Layout.columnSpan: 2 + Layout.fillWidth: true + padding: 0 + leftPadding: constants.paddingSmall + RowLayout { + width: parent.width + Label { + text: modelData.address + Layout.fillWidth: true + wrapMode: Text.Wrap + font.pixelSize: constants.fontSizeLarge + font.family: FixedFont + color: modelData.is_mine ? constants.colorMine : Material.foreground + } + Label { + text: Config.formatSats(modelData.value) + font.pixelSize: constants.fontSizeLarge + } + Label { + text: Config.baseUnit + font.pixelSize: constants.fontSizeMedium + color: Material.accentColor + } + } + } + } + + } + } + + TxDetails { + id: txdetails + wallet: Daemon.currentWallet + txid: root.txid + onLabelChanged: txDetailsChanged() + } + + Component { + id: share + GenericShareDialog {} + } + +} diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index 146ac9307..d41314f4c 100644 --- a/electrum/gui/qml/qeapp.py +++ b/electrum/gui/qml/qeapp.py @@ -22,6 +22,7 @@ from .qetxfinalizer import QETxFinalizer from .qeinvoice import QEInvoice from .qetypes import QEAmount from .qeaddressdetails import QEAddressDetails +from .qetxdetails import QETxDetails notification = None @@ -120,6 +121,7 @@ class ElectrumQmlApplication(QGuiApplication): qmlRegisterType(QETxFinalizer, 'org.electrum', 1, 0, 'TxFinalizer') qmlRegisterType(QEInvoice, 'org.electrum', 1, 0, 'Invoice') qmlRegisterType(QEAddressDetails, 'org.electrum', 1, 0, 'AddressDetails') + qmlRegisterType(QETxDetails, 'org.electrum', 1, 0, 'TxDetails') qmlRegisterUncreatableType(QEAmount, 'org.electrum', 1, 0, 'Amount', 'Amount can only be used as property') diff --git a/electrum/gui/qml/qetransactionlistmodel.py b/electrum/gui/qml/qetransactionlistmodel.py index 3426b0488..71d474afd 100644 --- a/electrum/gui/qml/qetransactionlistmodel.py +++ b/electrum/gui/qml/qetransactionlistmodel.py @@ -117,6 +117,17 @@ class QETransactionListModel(QAbstractListModel): return i = i + 1 + @pyqtSlot(str, str) + def update_tx_label(self, txid, label): + i = 0 + for tx in self.tx_history: + if tx['txid'] == txid: + tx['label'] = label + index = self.index(i,0) + self.dataChanged.emit(index, index, [self._ROLE_RMAP['label']]) + return + i = i + 1 + @pyqtSlot(int) def updateBlockchainHeight(self, height): self._logger.debug('updating height to %d' % height) diff --git a/electrum/gui/qml/qetxdetails.py b/electrum/gui/qml/qetxdetails.py new file mode 100644 index 000000000..e8e427ce6 --- /dev/null +++ b/electrum/gui/qml/qetxdetails.py @@ -0,0 +1,143 @@ +from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject + +#from decimal import Decimal + +from electrum.logging import get_logger +from electrum.util import format_time + +from .qewallet import QEWallet +from .qetypes import QEAmount + +class QETxDetails(QObject): + def __init__(self, parent=None): + super().__init__(parent) + + _logger = get_logger(__name__) + + _wallet = None + _txid = None + + _mempool_depth = None + _date = None + + detailsChanged = pyqtSignal() + + walletChanged = pyqtSignal() + @pyqtProperty(QEWallet, notify=walletChanged) + def wallet(self): + return self._wallet + + @wallet.setter + def wallet(self, wallet: QEWallet): + if self._wallet != wallet: + self._wallet = wallet + self.walletChanged.emit() + + txidChanged = pyqtSignal() + @pyqtProperty(str, notify=txidChanged) + def txid(self): + return self._txid + + @txid.setter + def txid(self, txid: str): + if self._txid != txid: + self._logger.debug('txid set -> %s' % txid) + self._txid = txid + self.txidChanged.emit() + self.update() + + labelChanged = pyqtSignal() + @pyqtProperty(str, notify=labelChanged) + def label(self): + return self._label + + @pyqtSlot(str) + def set_label(self, label: str): + if label != self._label: + self._wallet.wallet.set_label(self._txid, label) + self._label = label + self.labelChanged.emit() + + @pyqtProperty(str, notify=detailsChanged) + def status(self): + return self._status + + @pyqtProperty(str, notify=detailsChanged) + def date(self): + return self._date + + @pyqtProperty(str, notify=detailsChanged) + def mempoolDepth(self): + return self._mempool_depth + + @pyqtProperty(bool, notify=detailsChanged) + def isMined(self): + return self._is_mined + + @pyqtProperty(bool, notify=detailsChanged) + def isLightningFundingTx(self): + return self._is_lightning_funding_tx + + @pyqtProperty(bool, notify=detailsChanged) + def canBump(self): + return self._can_bump + + @pyqtProperty(bool, notify=detailsChanged) + def canCancel(self): + return self._can_dscancel + + @pyqtProperty(QEAmount, notify=detailsChanged) + def amount(self): + return self._amount + + @pyqtProperty(QEAmount, notify=detailsChanged) + def fee(self): + return self._fee + + @pyqtProperty('QVariantList', notify=detailsChanged) + def inputs(self): + return self._inputs + + @pyqtProperty('QVariantList', notify=detailsChanged) + def outputs(self): + return self._outputs + + def update(self): + if self._wallet is None: + self._logger.error('wallet undefined') + return + + # abusing get_input_tx to get tx from txid + tx = self._wallet.wallet.get_input_tx(self._txid) + + self._inputs = list(map(lambda x: x.to_json(), tx.inputs())) + self._outputs = list(map(lambda x: { + 'address': x.get_ui_address_str(), + 'value': QEAmount(amount_sat=x.value), + 'is_mine': self._wallet.wallet.is_mine(x.get_ui_address_str()) + }, tx.outputs())) + + txinfo = self._wallet.wallet.get_tx_info(tx) + self._status = txinfo.status + self._label = txinfo.label + self._amount = QEAmount(amount_sat=txinfo.amount) # can be None? + self._fee = QEAmount(amount_sat=txinfo.fee) + + self._is_mined = txinfo.tx_mined_status != None + if self._is_mined: + self._date = format_time(txinfo.tx_mined_status.timestamp) + else: + #TODO mempool_depth_bytes can be None? + self._mempool_depth = self._wallet.wallet.config.depth_tooltip(txinfo.mempool_depth_bytes) + + self._is_lightning_funding_tx = txinfo.is_lightning_funding_tx + self._can_bump = txinfo.can_bump + self._can_dscancel = txinfo.can_dscancel + + self._logger.debug(repr(txinfo.mempool_depth_bytes)) + self._logger.debug(repr(txinfo.can_broadcast)) + self._logger.debug(repr(txinfo.can_cpfp)) + self._logger.debug(repr(txinfo.can_save_as_local)) + self._logger.debug(repr(txinfo.can_remove)) + + self.detailsChanged.emit()