Browse Source

qml: add initial bolt-11/bip-21 chooser in requestdialog

implement proper placement of icon over qr code
fix urlencoding in qr imageprovider
patch-4
Sander van Grieken 3 years ago
parent
commit
a970c0f78a
  1. 117
      electrum/gui/qml/components/RequestDialog.qml
  2. 22
      electrum/gui/qml/components/controls/GenericShareDialog.qml
  3. 25
      electrum/gui/qml/components/controls/QRImage.qml
  4. 4
      electrum/gui/qml/qeapp.py
  5. 49
      electrum/gui/qml/qeqr.py

117
electrum/gui/qml/components/RequestDialog.qml

@ -5,6 +5,8 @@ import QtQuick.Controls.Material 2.0
import org.electrum 1.0 import org.electrum 1.0
import "controls"
Dialog { Dialog {
id: dialog id: dialog
title: qsTr('Payment Request') title: qsTr('Payment Request')
@ -12,6 +14,7 @@ Dialog {
property var modelItem property var modelItem
property string _bip21uri property string _bip21uri
property string _bolt11
parent: Overlay.overlay parent: Overlay.overlay
modal: true modal: true
@ -44,55 +47,96 @@ Dialog {
clip:true clip:true
interactive: height < contentHeight interactive: height < contentHeight
GridLayout { ColumnLayout {
id: rootLayout id: rootLayout
width: parent.width width: parent.width
rowSpacing: constants.paddingMedium spacing: constants.paddingMedium
columns: 5
states: [
State {
name: 'bolt11'
PropertyChanges { target: qrloader; sourceComponent: qri_bolt11 }
PropertyChanges { target: bolt11label; font.bold: true }
},
State {
name: 'bip21uri'
PropertyChanges { target: qrloader; sourceComponent: qri_bip21uri }
PropertyChanges { target: bip21label; font.bold: true }
}
]
Rectangle { Rectangle {
height: 1 height: 1
Layout.fillWidth: true Layout.fillWidth: true
Layout.columnSpan: 5
color: Material.accentColor color: Material.accentColor
} }
Image { Item {
id: qr
Layout.columnSpan: 5
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.topMargin: constants.paddingSmall Layout.topMargin: constants.paddingSmall
Layout.bottomMargin: constants.paddingSmall Layout.bottomMargin: constants.paddingSmall
Rectangle { Layout.preferredWidth: qrloader.width
property int size: 57 // should be qr pixel multiple Layout.preferredHeight: qrloader.height
color: 'white'
x: (parent.width - size) / 2
y: (parent.height - size) / 2
width: size
height: size
Image { Loader {
id: qrloader
Component {
id: qri_bip21uri
QRImage {
qrdata: _bip21uri
}
}
Component {
id: qri_bolt11
QRImage {
qrdata: _bolt11
}
}
}
source: '../../icons/electrum.png' MouseArea {
x: 1 anchors.fill: parent
y: 1 onClicked: {
width: parent.width - 2 if (rootLayout.state == 'bolt11') {
height: parent.height - 2 if (_bip21uri != '')
scale: 0.9 rootLayout.state = 'bip21uri'
} else if (rootLayout.state == 'bip21uri') {
if (_bolt11 != '')
rootLayout.state = 'bolt11'
}
}
}
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: constants.paddingLarge
Label {
id: bolt11label
text: qsTr('BOLT11')
color: _bolt11 ? Material.foreground : constants.mutedForeground
} }
Rectangle {
Layout.preferredWidth: constants.paddingXXSmall
Layout.preferredHeight: constants.paddingXXSmall
radius: constants.paddingXXSmall / 2
color: Material.accentColor
}
Label {
id: bip21label
text: qsTr('BIP21 URI')
color: _bip21uri ? Material.foreground : constants.mutedForeground
} }
} }
Rectangle { Rectangle {
height: 1 height: 1
Layout.fillWidth: true Layout.fillWidth: true
Layout.columnSpan: 5
color: Material.accentColor color: Material.accentColor
} }
RowLayout { RowLayout {
Layout.columnSpan: 5
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Button { Button {
icon.source: '../../icons/delete.png' icon.source: '../../icons/delete.png'
@ -127,13 +171,16 @@ Dialog {
} }
} }
} }
GridLayout {
columns: 2
Label { Label {
visible: modelItem.message != '' visible: modelItem.message != ''
text: qsTr('Description') text: qsTr('Description')
} }
Label { Label {
visible: modelItem.message != '' visible: modelItem.message != ''
Layout.columnSpan: 4
Layout.fillWidth: true Layout.fillWidth: true
wrapMode: Text.Wrap wrapMode: Text.Wrap
text: modelItem.message text: modelItem.message
@ -144,6 +191,7 @@ Dialog {
visible: modelItem.amount.satsInt != 0 visible: modelItem.amount.satsInt != 0
text: qsTr('Amount') text: qsTr('Amount')
} }
RowLayout {
Label { Label {
visible: modelItem.amount.satsInt != 0 visible: modelItem.amount.satsInt != 0
text: Config.formatSats(modelItem.amount) text: Config.formatSats(modelItem.amount)
@ -162,22 +210,23 @@ Dialog {
id: fiatValue id: fiatValue
visible: modelItem.amount.satsInt != 0 visible: modelItem.amount.satsInt != 0
Layout.fillWidth: true Layout.fillWidth: true
Layout.columnSpan: 2
text: Daemon.fx.enabled text: Daemon.fx.enabled
? '(' + Daemon.fx.fiatValue(modelItem.amount, false) + ' ' + Daemon.fx.fiatCurrency + ')' ? '(' + Daemon.fx.fiatValue(modelItem.amount, false) + ' ' + Daemon.fx.fiatCurrency + ')'
: '' : ''
font.pixelSize: constants.fontSizeMedium font.pixelSize: constants.fontSizeMedium
wrapMode: Text.Wrap wrapMode: Text.Wrap
} }
}
Label { Label {
text: qsTr('Address') text: qsTr('Address')
visible: !modelItem.is_lightning visible: !modelItem.is_lightning
} }
RowLayout {
visible: !modelItem.is_lightning
Label { Label {
Layout.fillWidth: true Layout.fillWidth: true
Layout.columnSpan: 3
visible: !modelItem.is_lightning
font.family: FixedFont font.family: FixedFont
font.pixelSize: constants.fontSizeLarge font.pixelSize: constants.fontSizeLarge
wrapMode: Text.WrapAnywhere wrapMode: Text.WrapAnywhere
@ -185,22 +234,21 @@ Dialog {
} }
ToolButton { ToolButton {
icon.source: '../../icons/copy_bw.png' icon.source: '../../icons/copy_bw.png'
visible: !modelItem.is_lightning
onClicked: { onClicked: {
AppController.textToClipboard(modelItem.address) AppController.textToClipboard(modelItem.address)
} }
} }
}
Label { Label {
text: qsTr('Status') text: qsTr('Status')
} }
Label { Label {
Layout.columnSpan: 4
Layout.fillWidth: true Layout.fillWidth: true
font.pixelSize: constants.fontSizeLarge font.pixelSize: constants.fontSizeLarge
text: modelItem.status_str text: modelItem.status_str
} }
}
} }
} }
@ -216,9 +264,14 @@ Dialog {
Component.onCompleted: { Component.onCompleted: {
if (!modelItem.is_lightning) { if (!modelItem.is_lightning) {
_bip21uri = bitcoin.create_bip21_uri(modelItem.address, modelItem.amount, modelItem.message, modelItem.timestamp, modelItem.expiration - modelItem.timestamp) _bip21uri = bitcoin.create_bip21_uri(modelItem.address, modelItem.amount, modelItem.message, modelItem.timestamp, modelItem.expiration - modelItem.timestamp)
qr.source = 'image://qrgen/' + _bip21uri rootLayout.state = 'bip21uri'
} else { } else {
qr.source = 'image://qrgen/' + modelItem.lightning_invoice _bolt11 = modelItem.lightning_invoice
rootLayout.state = 'bolt11'
if (modelItem.address != '') {
_bip21uri = bitcoin.create_bip21_uri(modelItem.address, modelItem.amount, modelItem.message, modelItem.timestamp, modelItem.expiration - modelItem.timestamp)
console.log('BIP21:' + _bip21uri)
}
} }
} }

22
electrum/gui/qml/components/controls/GenericShareDialog.qml

@ -51,29 +51,11 @@ Dialog {
color: Material.accentColor color: Material.accentColor
} }
Image { QRImage {
id: qr id: qr
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.topMargin: constants.paddingSmall Layout.topMargin: constants.paddingSmall
Layout.bottomMargin: constants.paddingSmall Layout.bottomMargin: constants.paddingSmall
Rectangle {
property int size: 57 // should be qr pixel multiple
color: 'white'
x: (parent.width - size) / 2
y: (parent.height - size) / 2
width: size
height: size
Image {
source: '../../../icons/electrum.png'
x: 1
y: 1
width: parent.width - 2
height: parent.height - 2
scale: 0.9
}
}
} }
Rectangle { Rectangle {
@ -114,6 +96,6 @@ Dialog {
} }
Component.onCompleted: { Component.onCompleted: {
qr.source = 'image://qrgen/' + dialog.text qr.qrdata = dialog.text
} }
} }

25
electrum/gui/qml/components/controls/QRImage.qml

@ -0,0 +1,25 @@
import QtQuick 2.6
Image {
property string qrdata
source: qrdata ? 'image://qrgen/' + qrdata : ''
Rectangle {
property var qrprops: QRIP.getDimensions(qrdata)
color: 'white'
x: (parent.width - width) / 2
y: (parent.height - height) / 2
width: qrprops.icon_modules * qrprops.box_size
height: qrprops.icon_modules * qrprops.box_size
Image {
source: '../../../icons/electrum.png'
x: 1
y: 1
width: parent.width - 2
height: parent.height - 2
scale: 0.9
}
}
}

4
electrum/gui/qml/qeapp.py

@ -14,7 +14,7 @@ from .qeconfig import QEConfig
from .qedaemon import QEDaemon, QEWalletListModel from .qedaemon import QEDaemon, QEWalletListModel
from .qenetwork import QENetwork from .qenetwork import QENetwork
from .qewallet import QEWallet from .qewallet import QEWallet
from .qeqr import QEQRParser, QEQRImageProvider from .qeqr import QEQRParser, QEQRImageProvider, QEQRImageProviderHelper
from .qewalletdb import QEWalletDB from .qewalletdb import QEWalletDB
from .qebitcoin import QEBitcoin from .qebitcoin import QEBitcoin
from .qefx import QEFX from .qefx import QEFX
@ -166,6 +166,7 @@ class ElectrumQmlApplication(QGuiApplication):
self.qr_ip = QEQRImageProvider((7/8)*min(screensize.width(), screensize.height())) self.qr_ip = QEQRImageProvider((7/8)*min(screensize.width(), screensize.height()))
self.engine.addImageProvider('qrgen', self.qr_ip) self.engine.addImageProvider('qrgen', self.qr_ip)
self.qr_ip_h = QEQRImageProviderHelper((7/8)*min(screensize.width(), screensize.height()))
# add a monospace font as we can't rely on device having one # add a monospace font as we can't rely on device having one
self.fixedFont = 'PT Mono' self.fixedFont = 'PT Mono'
@ -187,6 +188,7 @@ class ElectrumQmlApplication(QGuiApplication):
self.context.setContextProperty('Daemon', self._qedaemon) self.context.setContextProperty('Daemon', self._qedaemon)
self.context.setContextProperty('FixedFont', self.fixedFont) self.context.setContextProperty('FixedFont', self.fixedFont)
self.context.setContextProperty('MAX', self._maxAmount) self.context.setContextProperty('MAX', self._maxAmount)
self.context.setContextProperty('QRIP', self.qr_ip_h)
self.context.setContextProperty('BUILD', { self.context.setContextProperty('BUILD', {
'electrum_version': version.ELECTRUM_VERSION, 'electrum_version': version.ELECTRUM_VERSION,
'apk_version': version.APK_VERSION, 'apk_version': version.APK_VERSION,

49
electrum/gui/qml/qeqr.py

@ -1,13 +1,14 @@
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl, QRect, QPoint
from PyQt5.QtGui import QImage,QColor
from PyQt5.QtQuick import QQuickImageProvider
import asyncio import asyncio
import qrcode import qrcode
import math import math
import urllib
from PIL import Image, ImageQt from PIL import Image, ImageQt
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QRect, QPoint
from PyQt5.QtGui import QImage,QColor
from PyQt5.QtQuick import QQuickImageProvider
from electrum.logging import get_logger from electrum.logging import get_logger
from electrum.qrreader import get_qr_reader from electrum.qrreader import get_qr_reader
from electrum.i18n import _ from electrum.i18n import _
@ -126,17 +127,53 @@ class QEQRImageProvider(QQuickImageProvider):
@profiler @profiler
def requestImage(self, qstr, size): def requestImage(self, qstr, size):
# Qt does a urldecode before passing the string here
# but BIP21 (and likely other uri based specs) requires urlencoding,
# so we re-encode percent-quoted if a 'scheme' is found in the string
uri = urllib.parse.urlparse(qstr)
if uri.scheme:
# urlencode request parameters
query = urllib.parse.parse_qs(uri.query)
query = urllib.parse.urlencode(query, doseq=True, quote_via=urllib.parse.quote)
uri = uri._replace(query=query)
qstr = urllib.parse.urlunparse(uri)
self._logger.debug('QR requested for %s' % qstr) self._logger.debug('QR requested for %s' % qstr)
qr = qrcode.QRCode(version=1, border=2) qr = qrcode.QRCode(version=1, border=2)
qr.add_data(qstr) qr.add_data(qstr)
# calculate best box_size # calculate best box_size
pixelsize = min(self._max_size, 400) pixelsize = min(self._max_size, 400)
modules = 17 + 4 * qr.best_fit() modules = 17 + 4 * qr.best_fit() + qr.border * 2
qr.box_size = math.floor(pixelsize/(modules+2*2)) qr.box_size = math.floor(pixelsize/modules)
qr.make(fit=True) qr.make(fit=True)
pimg = qr.make_image(fill_color='black', back_color='white') pimg = qr.make_image(fill_color='black', back_color='white')
self.qimg = ImageQt.ImageQt(pimg) self.qimg = ImageQt.ImageQt(pimg)
return self.qimg, self.qimg.size() return self.qimg, self.qimg.size()
# helper for placing icon exactly where it should go on the QR code
# pyqt5 is unwilling to accept slots on QEQRImageProvider, so we need to define
# a separate class (sigh)
class QEQRImageProviderHelper(QObject):
def __init__(self, max_size, parent=None):
super().__init__(parent)
self._max_size = max_size
@pyqtSlot(str, result='QVariantMap')
def getDimensions(self, qstr):
qr = qrcode.QRCode(version=1, border=2)
qr.add_data(qstr)
# calculate best box_size
pixelsize = min(self._max_size, 400)
modules = 17 + 4 * qr.best_fit() + qr.border * 2
qr.box_size = math.floor(pixelsize/modules)
# calculate icon width in modules
icon_modules = int(modules / 5)
icon_modules += (icon_modules+1)%2 # force odd
return { 'modules': modules, 'box_size': qr.box_size, 'icon_modules': icon_modules }

Loading…
Cancel
Save