1
0

Merge pull request #10078 from accumulator/android_qml_qrscan_signals

simplify QR scanner QML interface, remove properties, add results to signals
This commit is contained in:
accumulator
2025-08-01 13:16:26 +02:00
committed by GitHub
14 changed files with 86 additions and 69 deletions

View File

@@ -91,11 +91,11 @@ ElDialog {
? qsTr('Scan another address') ? qsTr('Scan another address')
: qsTr('Scan another private key') : qsTr('Scan another private key')
}) })
dialog.onFound.connect(function() { dialog.onFoundText.connect(function(data) {
if (verify(dialog.scanData)) { if (verify(data)) {
if (import_ta.text != '') if (import_ta.text != '')
import_ta.text = import_ta.text + ',\n' import_ta.text = import_ta.text + ',\n'
import_ta.text = import_ta.text + dialog.scanData import_ta.text = import_ta.text + data
} }
dialog.close() dialog.close()
}) })

View File

@@ -62,8 +62,8 @@ ElDialog {
var dialog = app.scanDialog.createObject(app, { var dialog = app.scanDialog.createObject(app, {
hint: qsTr('Scan a channel backup') hint: qsTr('Scan a channel backup')
}) })
dialog.onFound.connect(function() { dialog.onFoundText.connect(function(data) {
channelbackup_ta.text = dialog.scanData channelbackup_ta.text = data
dialog.close() dialog.close()
}) })
dialog.open() dialog.open()

View File

@@ -124,9 +124,9 @@ ElDialog {
var dialog = app.scanDialog.createObject(app, { var dialog = app.scanDialog.createObject(app, {
hint: qsTr('Scan a node-id or a connect string') hint: qsTr('Scan a node-id or a connect string')
}) })
dialog.onFound.connect(function() { dialog.onFoundText.connect(function(data) {
if (channelopener.validateConnectString(dialog.scanData)) { if (channelopener.validateConnectString(data)) {
channelopener.connectStr = dialog.scanData channelopener.connectStr = data
node.text = channelopener.connectStr node.text = channelopener.connectStr
} else { } else {
var errdialog = app.messageDialog.createObject(app, { var errdialog = app.messageDialog.createObject(app, {

View File

@@ -2,17 +2,19 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import org.electrum
import "controls" import "controls"
// currently not used on android, kept for future use when qt6 camera stops crashing // currently not used on android, kept for future use when qt6 camera stops crashing
ElDialog { ElDialog {
id: scanDialog id: scanDialog
property string scanData
property string error property string error
property string hint property string hint
signal found signal foundText(data: string)
signal foundBinary(data: Bytes)
width: parent.width width: parent.width
height: parent.height height: parent.height
@@ -35,9 +37,8 @@ ElDialog {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
hint: scanDialog.hint hint: scanDialog.hint
onFound: { onFoundText: (data) => {
scanDialog.scanData = scanData scanDialog.foundText(data)
scanDialog.found()
} }
} }

View File

@@ -59,7 +59,10 @@ ElDialog {
hint: Daemon.currentWallet.isLightning hint: Daemon.currentWallet.isLightning
? qsTr('Scan an Invoice, an Address, an LNURL-pay, a PSBT or a Channel Backup') ? qsTr('Scan an Invoice, an Address, an LNURL-pay, a PSBT or a Channel Backup')
: qsTr('Scan an Invoice, an Address, an LNURL-pay or a PSBT') : qsTr('Scan an Invoice, an Address, an LNURL-pay or a PSBT')
onFound: dialog.dispatch(scanData)
onFoundText: (data) => {
dialog.dispatch(data)
}
} }
ButtonContainer { ButtonContainer {

View File

@@ -117,9 +117,9 @@ ElDialog {
var dialog = app.scanDialog.createObject(app, { var dialog = app.scanDialog.createObject(app, {
hint: qsTr('Scan a private key') hint: qsTr('Scan a private key')
}) })
dialog.onFound.connect(function() { dialog.onFoundText.connect(function(data) {
if (verifyPrivateKey(dialog.scanData)) if (verifyPrivateKey(data))
addPrivateKey(dialog.scanData) addPrivateKey(data)
dialog.close() dialog.close()
}) })
dialog.open() dialog.open()

View File

@@ -47,8 +47,7 @@ Item {
? qsTr('Scan an Invoice, an Address, an LNURL-pay, a PSBT or a Channel Backup') ? qsTr('Scan an Invoice, an Address, an LNURL-pay, a PSBT or a Channel Backup')
: qsTr('Scan an Invoice, an Address, an LNURL-pay or a PSBT') : qsTr('Scan an Invoice, an Address, an LNURL-pay or a PSBT')
}) })
scanner.onFound.connect(function() { scanner.onFoundText.connect(function(data) {
var data = scanner.scanData
data = data.trim() data = data.trim()
if (bitcoin.isRawTx(data)) { if (bitcoin.isRawTx(data)) {
app.stack.push(Qt.resolvedUrl('TxDetails.qml'), { rawtx: data }) app.stack.push(Qt.resolvedUrl('TxDetails.qml'), { rawtx: data })

View File

@@ -10,14 +10,12 @@ Item {
property bool active: false property bool active: false
property string url property string url
property string scanData
property string hint property string hint
signal found signal foundText(data: string)
function restart() { function restart() {
console.log('qrscan.restart') console.log('qrscan.restart')
scanData = ''
qr.reset() qr.reset()
start() start()
} }
@@ -153,8 +151,7 @@ Item {
function onDataChanged() { function onDataChanged() {
console.log('QR DATA: ' + qr.data) console.log('QR DATA: ' + qr.data)
scanner.active = false scanner.active = false
scanner.scanData = qr.data scanner.foundText(qr.data)
scanner.found()
} }
} }

View File

@@ -166,9 +166,9 @@ WizardComponent {
? qsTr('Scan a cosigner master public key') ? qsTr('Scan a cosigner master public key')
: qsTr('Scan a master key') : qsTr('Scan a master key')
}) })
dialog.onFound.connect(function() { dialog.onFoundText.connect(function(data) {
if (verifyMasterKey(dialog.scanData)) if (verifyMasterKey(data))
masterkey_ta.text = dialog.scanData masterkey_ta.text = data
else else
masterkey_ta.text = '' masterkey_ta.text = ''
dialog.close() dialog.close()

View File

@@ -76,11 +76,11 @@ WizardComponent {
? qsTr('Scan another private key') ? qsTr('Scan another private key')
: qsTr('Scan a private key or an address') : qsTr('Scan a private key or an address')
}) })
dialog.onFound.connect(function() { dialog.onFoundText.connect(function(data) {
if (verify(dialog.scanData)) { if (verify(data)) {
if (import_ta.text != '') if (import_ta.text != '')
import_ta.text = import_ta.text + '\n' import_ta.text = import_ta.text + '\n'
import_ta.text = import_ta.text + dialog.scanData import_ta.text = import_ta.text + data
} }
dialog.close() dialog.close()
}) })

View File

@@ -21,6 +21,8 @@ import androidx.core.app.ActivityCompat;
import java.util.Arrays; import java.util.Arrays;
import de.markusfisch.android.barcodescannerview.widget.BarcodeScannerView; import de.markusfisch.android.barcodescannerview.widget.BarcodeScannerView;
import de.markusfisch.android.zxingcpp.ZxingCpp.Result;
import de.markusfisch.android.zxingcpp.ZxingCpp.ContentType;
import org.electrum.electrum.res.R; // package set in build.gradle import org.electrum.electrum.res.R; // package set in build.gradle
@@ -29,7 +31,7 @@ public class SimpleScannerActivity extends Activity {
private static final int MY_PERMISSIONS_CAMERA = 1002; private static final int MY_PERMISSIONS_CAMERA = 1002;
private BarcodeScannerView mScannerView = null; private BarcodeScannerView mScannerView = null;
final String TAG = "org.electrum.SimpleScannerActivity"; final String TAG = "org.electrum.qr.SimpleScannerActivity";
private boolean mAlreadyRequestedPermissions = false; private boolean mAlreadyRequestedPermissions = false;
@@ -60,7 +62,7 @@ public class SimpleScannerActivity extends Activity {
Toast.makeText(SimpleScannerActivity.this, "Clipboard contents too large.", Toast.LENGTH_SHORT).show(); Toast.makeText(SimpleScannerActivity.this, "Clipboard contents too large.", Toast.LENGTH_SHORT).show();
return; return;
} }
SimpleScannerActivity.this.setResultAndClose(clipboardText); SimpleScannerActivity.this.setResultAndClose(null, clipboardText);
} else { } else {
Toast.makeText(SimpleScannerActivity.this, "Clipboard is empty.", Toast.LENGTH_SHORT).show(); Toast.makeText(SimpleScannerActivity.this, "Clipboard is empty.", Toast.LENGTH_SHORT).show();
} }
@@ -96,7 +98,7 @@ public class SimpleScannerActivity extends Activity {
contentFrame.addView(mScannerView); contentFrame.addView(mScannerView);
mScannerView.setOnBarcodeListener(result -> { mScannerView.setOnBarcodeListener(result -> {
// Handle the scan result // Handle the scan result
this.setResultAndClose(result.getText()); this.setResultAndClose(result, null);
// Return false to stop scanning after first result // Return false to stop scanning after first result
return false; return false;
}); });
@@ -104,9 +106,22 @@ public class SimpleScannerActivity extends Activity {
mScannerView.openAsync(); // Start camera on resume mScannerView.openAsync(); // Start camera on resume
} }
private void setResultAndClose(String resultText) { private void setResultAndClose(Result scanResult, String textOnly) {
Intent resultIntent = new Intent(); Intent resultIntent = new Intent();
resultIntent.putExtra("text", resultText); if (textOnly != null) {
Log.v(TAG, "clipboard contentType TEXT");
resultIntent.putExtra("text", textOnly);
} else if (scanResult != null) {
if (scanResult.getContentType() == ContentType.TEXT) {
Log.v(TAG, "scanResult contentType TEXT");
resultIntent.putExtra("text", scanResult.getText());
} else if (scanResult.getContentType() == ContentType.BINARY) {
Log.v(TAG, "scanResult contentType BINARY");
resultIntent.putExtra("binary", scanResult.getRawBytes());
} else {
Log.v(TAG, "scanresult contenttype unknown");
}
}
setResult(Activity.RESULT_OK, resultIntent); setResult(Activity.RESULT_OK, resultIntent);
this.finish(); this.finish();
} }

View File

@@ -34,7 +34,7 @@ from .qefx import QEFX
from .qetxfinalizer import QETxFinalizer, QETxRbfFeeBumper, QETxCpfpFeeBumper, QETxCanceller, QETxSweepFinalizer, FeeSlider from .qetxfinalizer import QETxFinalizer, QETxRbfFeeBumper, QETxCpfpFeeBumper, QETxCanceller, QETxSweepFinalizer, FeeSlider
from .qeinvoice import QEInvoice, QEInvoiceParser from .qeinvoice import QEInvoice, QEInvoiceParser
from .qerequestdetails import QERequestDetails from .qerequestdetails import QERequestDetails
from .qetypes import QEAmount from .qetypes import QEAmount, QEBytes
from .qeaddressdetails import QEAddressDetails from .qeaddressdetails import QEAddressDetails
from .qetxdetails import QETxDetails from .qetxdetails import QETxDetails
from .qechannelopener import QEChannelOpener from .qechannelopener import QEChannelOpener
@@ -426,6 +426,7 @@ class ElectrumQmlApplication(QGuiApplication):
# TODO QT6 order of declaration is important now? # TODO QT6 order of declaration is important now?
qmlRegisterType(QEAmount, 'org.electrum', 1, 0, 'Amount') qmlRegisterType(QEAmount, 'org.electrum', 1, 0, 'Amount')
qmlRegisterType(QEBytes, 'org.electrum', 1, 0, 'Bytes')
qmlRegisterType(QENewWalletWizard, 'org.electrum', 1, 0, 'QNewWalletWizard') qmlRegisterType(QENewWalletWizard, 'org.electrum', 1, 0, 'QNewWalletWizard')
qmlRegisterType(QETermsOfUseWizard, 'org.electrum', 1, 0, 'QTermsOfUseWizard') qmlRegisterType(QETermsOfUseWizard, 'org.electrum', 1, 0, 'QTermsOfUseWizard')
qmlRegisterType(QEServerConnectWizard, 'org.electrum', 1, 0, 'QServerConnectWizard') qmlRegisterType(QEServerConnectWizard, 'org.electrum', 1, 0, 'QServerConnectWizard')

View File

@@ -3,8 +3,8 @@ import os
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, Qt from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, Qt
from PyQt6.QtGui import QGuiApplication from PyQt6.QtGui import QGuiApplication
from electrum.util import send_exception_to_crash_reporter, UserFacingException from electrum.gui.qml.qetypes import QEBytes
from electrum.simple_config import SimpleConfig from electrum.util import send_exception_to_crash_reporter
from electrum.logging import get_logger from electrum.logging import get_logger
from electrum.i18n import _ from electrum.i18n import _
@@ -21,14 +21,14 @@ if 'ANDROID_DATA' in os.environ:
class QEQRScanner(QObject): class QEQRScanner(QObject):
_logger = get_logger(__name__) _logger = get_logger(__name__)
found = pyqtSignal() foundText = pyqtSignal(str)
foundBinary = pyqtSignal(QEBytes)
finished = pyqtSignal() finished = pyqtSignal()
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self._hint = _("Scan a QR code.") self._hint = _("Scan a QR code.")
self._scan_data = "" # decoded qr code result
self.finished.connect(self._unbind, Qt.ConnectionType.QueuedConnection) self.finished.connect(self._unbind, Qt.ConnectionType.QueuedConnection)
self.destroyed.connect(lambda: self.on_destroy()) self.destroyed.connect(lambda: self.on_destroy())
@@ -44,14 +44,6 @@ class QEQRScanner(QObject):
def hint(self, v: str): def hint(self, v: str):
self._hint = v self._hint = v
@pyqtProperty(str)
def scanData(self):
return self._scan_data
@scanData.setter
def scanData(self, v: str):
self._scan_data = v
@pyqtSlot() @pyqtSlot()
def open(self): def open(self):
if 'ANDROID_DATA' not in os.environ: if 'ANDROID_DATA' not in os.environ:
@@ -67,9 +59,11 @@ class QEQRScanner(QObject):
def on_qr_activity_result(self, requestCode, resultCode, intent): def on_qr_activity_result(self, requestCode, resultCode, intent):
try: try:
if resultCode == -1: # RESULT_OK: if resultCode == -1: # RESULT_OK:
contents = intent.getStringExtra(jString("text")) if (contents := intent.getStringExtra(jString("text"))) is not None:
self.scanData = contents self.foundText.emit(contents)
self.found.emit() if (contents := intent.getByteArrayExtra(jString("binary"))) is not None:
self._binary_content = QEBytes(bytes(contents.tolist()))
self.foundBinary.emit(self._binary_content)
except Exception as e: # exc would otherwise get lost except Exception as e: # exc would otherwise get lost
send_exception_to_crash_reporter(e) send_exception_to_crash_reporter(e)
finally: finally:
@@ -82,23 +76,6 @@ class QEQRScanner(QObject):
def _scan_qr_non_android(self): def _scan_qr_non_android(self):
data = QGuiApplication.clipboard().text() data = QGuiApplication.clipboard().text()
self.scanData = data self.foundText.emit(data)
self.found.emit()
self.finished.emit() self.finished.emit()
return return
# from electrum import qrscanner
# from .qeapp import ElectrumQmlApplication
# daemon = ElectrumQmlApplication._daemon
# config = daemon.config # type: SimpleConfig
# try:
# video_dev = config.get_video_device()
# data = qrscanner.scan_barcode(video_dev)
# if data is not None:
# self.scanData = data
# self.found.emit()
# except UserFacingException as e:
# self._logger.warning(f'camera error: {e!r}')
# #self.show_error(e)
# except Exception as e:
# self._logger.exception('camera error')
# #self.show_error(repr(e))

View File

@@ -116,3 +116,27 @@ class QEAmount(QObject):
def __repr__(self): def __repr__(self):
return f"<QEAmount max={self._is_max} sats={self._amount_sat} msats={self._amount_msat} empty={self.isEmpty}>" return f"<QEAmount max={self._is_max} sats={self._amount_sat} msats={self._amount_msat} empty={self.isEmpty}>"
class QEBytes(QObject):
def __init__(self, data: bytes = None, *, parent=None):
super().__init__(parent)
self.data = data
@property
def data(self):
return self._data
@data.setter
def data(self, _data):
self._data = _data
@pyqtProperty(bool)
def isEmpty(self):
return self._data is None or self._data == bytes()
def __str__(self):
return f'{self._data}'
def __repr__(self):
return f"<QEBytes data={'None' if self._data is None else self._data.hex()}>"