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 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 Loader {
y: (parent.height - size) / 2 id: qrloader
width: size Component {
height: size id: qri_bip21uri
QRImage {
Image { qrdata: _bip21uri
}
source: '../../icons/electrum.png' }
x: 1 Component {
y: 1 id: qri_bolt11
width: parent.width - 2 QRImage {
height: parent.height - 2 qrdata: _bolt11
scale: 0.9 }
}
}
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 { 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,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 { GridLayout {
visible: modelItem.amount.satsInt != 0 columns: 2
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
}
Label { Label {
id: fiatValue visible: modelItem.message != ''
visible: modelItem.amount.satsInt != 0 text: qsTr('Description')
Layout.fillWidth: true }
Layout.columnSpan: 2 Label {
text: Daemon.fx.enabled visible: modelItem.message != ''
? '(' + Daemon.fx.fiatValue(modelItem.amount, false) + ' ' + Daemon.fx.fiatCurrency + ')' Layout.fillWidth: true
: '' wrapMode: Text.Wrap
font.pixelSize: constants.fontSizeMedium text: modelItem.message
wrapMode: Text.Wrap font.pixelSize: constants.fontSizeLarge
} }
Label { Label {
text: qsTr('Address') visible: modelItem.amount.satsInt != 0
visible: !modelItem.is_lightning text: qsTr('Amount')
}
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)
} }
} 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 { Label {
text: qsTr('Status') id: fiatValue
} visible: modelItem.amount.satsInt != 0
Label { Layout.fillWidth: true
Layout.columnSpan: 4 text: Daemon.fx.enabled
Layout.fillWidth: true ? '(' + Daemon.fx.fiatValue(modelItem.amount, false) + ' ' + Daemon.fx.fiatCurrency + ')'
font.pixelSize: constants.fontSizeLarge : ''
text: modelItem.status_str 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: { 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