diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 05600c3c7..7ae0cf113 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -97,7 +97,7 @@ from .update_checker import UpdateCheck, UpdateCheckThread from .channels_list import ChannelsList from .confirm_tx_dialog import ConfirmTxDialog from .rbf_dialog import BumpFeeDialog, DSCancelDialog -from .qrreader import scan_qrcode +from .qrreader import scan_qrcode_from_camera from .swap_dialog import SwapDialog, InvalidSwapParameters from .balance_dialog import (BalanceToolButton, COLOR_FROZEN, COLOR_UNMATURED, COLOR_UNCONFIRMED, COLOR_CONFIRMED, COLOR_LIGHTNING, COLOR_FROZEN_LIGHTNING) @@ -2322,7 +2322,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): return self.show_transaction(tx) - scan_qrcode(parent=self.top_level_window(), config=self.config, callback=cb) + scan_qrcode_from_camera(parent=self.top_level_window(), config=self.config, callback=cb) def read_tx_from_file(self) -> Optional[Transaction]: fileName = getOpenFileName( diff --git a/electrum/gui/qt/qrreader/__init__.py b/electrum/gui/qt/qrreader/__init__.py index 367417632..914a2a178 100644 --- a/electrum/gui/qt/qrreader/__init__.py +++ b/electrum/gui/qt/qrreader/__init__.py @@ -26,7 +26,8 @@ from typing import Callable, Optional, TYPE_CHECKING, Mapping, Sequence from PyQt6.QtWidgets import QMessageBox, QWidget from PyQt6.QtGui import QImage, QPainter, QColor -from PyQt6.QtCore import QRect +from PyQt6.QtCore import QRect, QCoreApplication +from PyQt6 import QtCore from electrum.i18n import _ from electrum.util import UserFacingException @@ -43,18 +44,24 @@ if TYPE_CHECKING: _logger = get_logger(__name__) -def scan_qrcode( +def scan_qrcode_from_camera( *, parent: Optional[QWidget], config: 'SimpleConfig', callback: Callable[[bool, str, Optional[str]], None], ) -> None: - """Scans QR code using camera.""" + """Scans QR code using camera. It handles requesting camera access permission from the OS if needed.""" assert parent is None or isinstance(parent, QWidget), f"parent should be a QWidget, not {parent!r}" - if sys.platform == 'darwin' or sys.platform in ('windows', 'win32'): - _scan_qrcode_using_qtmultimedia(parent=parent, config=config, callback=callback) - else: # desktop Linux and similar - _scan_qrcode_using_zbar(parent=parent, config=config, callback=callback) + def do_scan(): + _scan_qrcode_from_camera(parent=parent, config=config, callback=callback) + + if _has_camera_permission(): + do_scan() + else: + # Request permission now. This is only a thing on macOS atm. + # Note: this assumes we are running on the main thread. Permissions can only be requested from the main thread. + app = QCoreApplication.instance() + app.requestPermission(QtCore.QCameraPermission(), lambda _x: do_scan()) def scan_qr_from_image(image: QImage) -> Sequence[QrCodeResult]: @@ -181,3 +188,29 @@ def _scan_qrcode_using_qtmultimedia( _qr_dialog = None callback(False, repr(e), None) + +def _scan_qrcode_from_camera( + *, + parent: Optional[QWidget], + config: 'SimpleConfig', + callback: Callable[[bool, str, Optional[str]], None], +) -> None: + """Scans QR code using camera.""" + assert parent is None or isinstance(parent, QWidget), f"parent should be a QWidget, not {parent!r}" + if not _has_camera_permission(): + callback(False, _("Missing camera permission."), None) + return + if sys.platform == 'darwin' or sys.platform in ('windows', 'win32'): + _scan_qrcode_using_qtmultimedia(parent=parent, config=config, callback=callback) + else: # desktop Linux and similar + _scan_qrcode_using_zbar(parent=parent, config=config, callback=callback) + + +def _has_camera_permission() -> bool: + if not hasattr(QtCore, "QCameraPermission"): # requires Qt 6.5+ + _logger.info(f"QtCore does not support QCameraPermission. This requires Qt 6.5+") + return True # hope for the best + app = QCoreApplication.instance() + permission_status = app.checkPermission(QtCore.QCameraPermission()) + return permission_status == QtCore.Qt.PermissionStatus.Granted + diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index f744d6333..b41159c44 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -792,10 +792,10 @@ class GenericInputHandler: except Exception as e: show_error(_('Invalid payment identifier in QR') + ':\n' + repr(e)) - from .qrreader import scan_qrcode + from .qrreader import scan_qrcode_from_camera if parent is None: parent = self if isinstance(self, QWidget) else None - scan_qrcode(parent=parent, config=config, callback=cb) + scan_qrcode_from_camera(parent=parent, config=config, callback=cb) def input_qr_from_screenshot( self,