qml: add initial bolt-11/bip-21 chooser in requestdialog
implement proper placement of icon over qr code fix urlencoding in qr imageprovider
This commit is contained in:
@@ -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
|
||||
Layout.preferredWidth: qrloader.width
|
||||
Layout.preferredHeight: qrloader.height
|
||||
|
||||
Image {
|
||||
|
||||
source: '../../icons/electrum.png'
|
||||
x: 1
|
||||
y: 1
|
||||
width: parent.width - 2
|
||||
height: parent.height - 2
|
||||
scale: 0.9
|
||||
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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr('Status')
|
||||
}
|
||||
Label {
|
||||
Layout.columnSpan: 4
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
Normal file
25
electrum/gui/qml/components/controls/QRImage.qml
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user