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. 243
      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

243
electrum/gui/qml/components/RequestDialog.qml

@ -5,6 +5,8 @@ import QtQuick.Controls.Material 2.0
import org.electrum 1.0
import "controls"
Dialog {
id: dialog
title: qsTr('Payment Request')
@ -12,6 +14,7 @@ Dialog {
property var modelItem
property string _bip21uri
property string _bolt11
parent: Overlay.overlay
modal: true
@ -44,55 +47,96 @@ Dialog {
clip:true
interactive: height < contentHeight
GridLayout {
ColumnLayout {
id: rootLayout
width: parent.width
rowSpacing: constants.paddingMedium
columns: 5
spacing: constants.paddingMedium
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 {
height: 1
Layout.fillWidth: true
Layout.columnSpan: 5
color: Material.accentColor
}
Image {
id: qr
Layout.columnSpan: 5
Item {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 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
Layout.preferredWidth: qrloader.width
Layout.preferredHeight: qrloader.height
Loader {
id: qrloader
Component {
id: qri_bip21uri
QRImage {
qrdata: _bip21uri
}
}
Component {
id: qri_bolt11
QRImage {
qrdata: _bolt11
}
}
}
MouseArea {
anchors.fill: parent
onClicked: {
if (rootLayout.state == 'bolt11') {
if (_bip21uri != '')
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 {
height: 1
Layout.fillWidth: true
Layout.columnSpan: 5
color: Material.accentColor
}
RowLayout {
Layout.columnSpan: 5
Layout.alignment: Qt.AlignHCenter
Button {
icon.source: '../../icons/delete.png'
@ -127,80 +171,84 @@ Dialog {
}
}
}
Label {
visible: modelItem.message != ''
text: qsTr('Description')
}
Label {
visible: modelItem.message != ''
Layout.columnSpan: 4
Layout.fillWidth: true
wrapMode: Text.Wrap
text: modelItem.message
font.pixelSize: constants.fontSizeLarge
}
Label {
visible: modelItem.amount.satsInt != 0
text: qsTr('Amount')
}
Label {
visible: modelItem.amount.satsInt != 0
text: Config.formatSats(modelItem.amount)
font.family: FixedFont
font.pixelSize: constants.fontSizeLarge
font.bold: true
}
Label {
visible: modelItem.amount.satsInt != 0
text: Config.baseUnit
color: Material.accentColor
font.pixelSize: constants.fontSizeLarge
}
GridLayout {
columns: 2
Label {
id: fiatValue
visible: modelItem.amount.satsInt != 0
Layout.fillWidth: true
Layout.columnSpan: 2
text: Daemon.fx.enabled
? '(' + Daemon.fx.fiatValue(modelItem.amount, false) + ' ' + Daemon.fx.fiatCurrency + ')'
: ''
font.pixelSize: constants.fontSizeMedium
wrapMode: Text.Wrap
}
Label {
visible: modelItem.message != ''
text: qsTr('Description')
}
Label {
visible: modelItem.message != ''
Layout.fillWidth: true
wrapMode: Text.Wrap
text: modelItem.message
font.pixelSize: constants.fontSizeLarge
}
Label {
text: qsTr('Address')
visible: !modelItem.is_lightning
}
Label {
Layout.fillWidth: true
Layout.columnSpan: 3
visible: !modelItem.is_lightning
font.family: FixedFont
font.pixelSize: constants.fontSizeLarge
wrapMode: Text.WrapAnywhere
text: modelItem.address
}
ToolButton {
icon.source: '../../icons/copy_bw.png'
visible: !modelItem.is_lightning
onClicked: {
AppController.textToClipboard(modelItem.address)
Label {
visible: modelItem.amount.satsInt != 0
text: qsTr('Amount')
}
}
RowLayout {
Label {
visible: modelItem.amount.satsInt != 0
text: Config.formatSats(modelItem.amount)
font.family: FixedFont
font.pixelSize: constants.fontSizeLarge
font.bold: true
}
Label {
visible: modelItem.amount.satsInt != 0
text: Config.baseUnit
color: Material.accentColor
font.pixelSize: constants.fontSizeLarge
}
Label {
text: qsTr('Status')
}
Label {
Layout.columnSpan: 4
Layout.fillWidth: true
font.pixelSize: constants.fontSizeLarge
text: modelItem.status_str
}
Label {
id: fiatValue
visible: modelItem.amount.satsInt != 0
Layout.fillWidth: true
text: Daemon.fx.enabled
? '(' + Daemon.fx.fiatValue(modelItem.amount, false) + ' ' + Daemon.fx.fiatCurrency + ')'
: ''
font.pixelSize: constants.fontSizeMedium
wrapMode: Text.Wrap
}
}
Label {
text: qsTr('Address')
visible: !modelItem.is_lightning
}
RowLayout {
visible: !modelItem.is_lightning
Label {
Layout.fillWidth: true
font.family: FixedFont
font.pixelSize: constants.fontSizeLarge
wrapMode: Text.WrapAnywhere
text: modelItem.address
}
ToolButton {
icon.source: '../../icons/copy_bw.png'
onClicked: {
AppController.textToClipboard(modelItem.address)
}
}
}
Label {
text: qsTr('Status')
}
Label {
Layout.fillWidth: true
font.pixelSize: constants.fontSizeLarge
text: modelItem.status_str
}
}
}
}
@ -216,9 +264,14 @@ Dialog {
Component.onCompleted: {
if (!modelItem.is_lightning) {
_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 {
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
}
Image {
QRImage {
id: qr
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 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 {
@ -114,6 +96,6 @@ Dialog {
}
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 .qenetwork import QENetwork
from .qewallet import QEWallet
from .qeqr import QEQRParser, QEQRImageProvider
from .qeqr import QEQRParser, QEQRImageProvider, QEQRImageProviderHelper
from .qewalletdb import QEWalletDB
from .qebitcoin import QEBitcoin
from .qefx import QEFX
@ -166,6 +166,7 @@ class ElectrumQmlApplication(QGuiApplication):
self.qr_ip = QEQRImageProvider((7/8)*min(screensize.width(), screensize.height()))
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
self.fixedFont = 'PT Mono'
@ -187,6 +188,7 @@ class ElectrumQmlApplication(QGuiApplication):
self.context.setContextProperty('Daemon', self._qedaemon)
self.context.setContextProperty('FixedFont', self.fixedFont)
self.context.setContextProperty('MAX', self._maxAmount)
self.context.setContextProperty('QRIP', self.qr_ip_h)
self.context.setContextProperty('BUILD', {
'electrum_version': version.ELECTRUM_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 qrcode
import math
import urllib
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.qrreader import get_qr_reader
from electrum.i18n import _
@ -126,17 +127,53 @@ class QEQRImageProvider(QQuickImageProvider):
@profiler
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)
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.box_size = math.floor(pixelsize/(modules+2*2))
modules = 17 + 4 * qr.best_fit() + qr.border * 2
qr.box_size = math.floor(pixelsize/modules)
qr.make(fit=True)
pimg = qr.make_image(fill_color='black', back_color='white')
self.qimg = ImageQt.ImageQt(pimg)
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