From 013cf869f1f85467778620ee122062090397a3ae Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 24 Jun 2021 20:10:29 +0200 Subject: [PATCH] qt: qrreader: keep both old and new toolchain; try to abstract it away --- contrib/build-wine/deterministic.spec | 2 +- contrib/make_zbar.sh | 56 +++--- contrib/osx/osx.spec | 2 +- electrum/gui/qt/__init__.py | 22 --- electrum/gui/qt/main_window.py | 67 +++---- electrum/gui/qt/qrreader/__init__.py | 163 +++++++++++++++--- .../gui/qt/qrreader/qtmultimedia/__init__.py | 39 +++++ .../{ => qtmultimedia}/camera_dialog.py | 4 +- .../{ => qtmultimedia}/crop_blur_effect.py | 0 .../qrreader/{ => qtmultimedia}/validator.py | 0 .../{ => qtmultimedia}/video_overlay.py | 0 .../{ => qtmultimedia}/video_surface.py | 0 .../{ => qtmultimedia}/video_widget.py | 0 electrum/gui/qt/qrtextedit.py | 59 ++----- electrum/gui/qt/settings_dialog.py | 16 +- electrum/qrscanner.py | 4 +- setup.py | 1 + 17 files changed, 251 insertions(+), 184 deletions(-) create mode 100644 electrum/gui/qt/qrreader/qtmultimedia/__init__.py rename electrum/gui/qt/qrreader/{ => qtmultimedia}/camera_dialog.py (99%) rename electrum/gui/qt/qrreader/{ => qtmultimedia}/crop_blur_effect.py (100%) rename electrum/gui/qt/qrreader/{ => qtmultimedia}/validator.py (100%) rename electrum/gui/qt/qrreader/{ => qtmultimedia}/video_overlay.py (100%) rename electrum/gui/qt/qrreader/{ => qtmultimedia}/video_surface.py (100%) rename electrum/gui/qt/qrreader/{ => qtmultimedia}/video_widget.py (100%) diff --git a/contrib/build-wine/deterministic.spec b/contrib/build-wine/deterministic.spec index 34917c6fc..b7896f95f 100644 --- a/contrib/build-wine/deterministic.spec +++ b/contrib/build-wine/deterministic.spec @@ -53,7 +53,7 @@ datas += collect_data_files('bitbox02') # We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports a = Analysis([home+'run_electrum', home+'electrum/gui/qt/main_window.py', - home+'electrum/gui/qt/qrreader/camera_dialog.py', + home+'electrum/gui/qt/qrreader/qtmultimedia/camera_dialog.py', home+'electrum/gui/text.py', home+'electrum/util.py', home+'electrum/wallet.py', diff --git a/contrib/make_zbar.sh b/contrib/make_zbar.sh index 92f31a570..de60c68ab 100755 --- a/contrib/make_zbar.sh +++ b/contrib/make_zbar.sh @@ -47,45 +47,39 @@ info "Building $pkgname..." if ! [ -r config.status ] ; then if [ "$BUILD_TYPE" = "wine" ] ; then # windows target - ./configure \ - $AUTOCONF_FLAGS \ - --prefix="$here/$pkgname/dist" \ + AUTOCONF_FLAGS="$AUTOCONF_FLAGS \ --with-x=no \ - --enable-pthread=no \ - --enable-doc=no \ --enable-video=yes \ - --with-directshow=yes \ --with-jpeg=no \ - --with-python=no \ - --with-gtk=no \ - --with-qt=no \ - --with-java=no \ - --with-imagemagick=no \ - --with-dbus=no \ - --enable-codes=qrcode \ - --disable-dependency-tracking \ - --disable-static \ - --enable-shared || fail "Could not configure $pkgname. Please make sure you have a C compiler installed and try again." + --with-directshow=yes \ + --disable-dependency-tracking" + elif [ $(uname) == "Darwin" ]; then + # macos target + AUTOCONF_FLAGS="$AUTOCONF_FLAGS \ + --with-x=no \ + --enable-video=no \ + --with-jpeg=no" else # linux target - ./configure \ - $AUTOCONF_FLAGS \ - --prefix="$here/$pkgname/dist" \ + AUTOCONF_FLAGS="$AUTOCONF_FLAGS \ --with-x=yes \ - --enable-pthread=no \ - --enable-doc=no \ --enable-video=yes \ - --with-jpeg=yes \ - --with-python=no \ - --with-gtk=no \ - --with-qt=no \ - --with-java=no \ - --with-imagemagick=no \ - --with-dbus=no \ - --enable-codes=qrcode \ - --disable-static \ - --enable-shared || fail "Could not configure $pkgname. Please make sure you have a C compiler installed and try again." + --with-jpeg=yes" fi + ./configure \ + $AUTOCONF_FLAGS \ + --prefix="$here/$pkgname/dist" \ + --enable-pthread=no \ + --enable-doc=no \ + --with-python=no \ + --with-gtk=no \ + --with-qt=no \ + --with-java=no \ + --with-imagemagick=no \ + --with-dbus=no \ + --enable-codes=qrcode \ + --disable-static \ + --enable-shared || fail "Could not configure $pkgname. Please make sure you have a C compiler installed and try again." fi make -j4 || fail "Could not build $pkgname" make install || fail "Could not install $pkgname" diff --git a/contrib/osx/osx.spec b/contrib/osx/osx.spec index 65d66182b..e922cc80e 100644 --- a/contrib/osx/osx.spec +++ b/contrib/osx/osx.spec @@ -96,7 +96,7 @@ binaries += [b for b in collect_dynamic_libs('PyQt5') if 'macstyle' in b[0]] # We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports a = Analysis([electrum+ MAIN_SCRIPT, electrum+'electrum/gui/qt/main_window.py', - electrum+'electrum/gui/qt/qrreader/camera_dialog.py', + electrum+'electrum/gui/qt/qrreader/qtmultimedia/camera_dialog.py', electrum+'electrum/gui/text.py', electrum+'electrum/util.py', electrum+'electrum/wallet.py', diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index c28dda984..238a7b0f8 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -275,28 +275,6 @@ class ElectrumGui(Logger): network_updated_signal_obj=self.network_updated_signal_obj) self.network_dialog.show() - @staticmethod - def warn_if_cant_import_qrreader(parent, *, show_warning=True) -> bool: - """Checks it QR reading from camera is possible. It can fail on a - system lacking QtMultimedia. This can be removed in the future when - we are unlikely to encounter Qt5 installations that are missing - QtMultimedia - """ - try: - from .qrreader import QrReaderCameraDialog - except ImportError as e: - if show_warning: - icon = QMessageBox.Warning - title = _("QR Reader Error") - message = _("QR reader failed to load. This may happen if " - "you are using an older version of PyQt5.") + "\n\n" + str(e) - if isinstance(parent, MessageBoxMixin): - parent.msg_box(title=title, text=message, icon=icon, parent=None) - else: - custom_message_box(title=title, text=message, icon=icon, parent=parent) - return True - return False - def _create_window_for_wallet(self, wallet): w = ElectrumWindow(self, wallet) self.windows.append(w) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 4371b1a9c..aae0dc2b5 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -103,6 +103,7 @@ from .channels_list import ChannelsList from .confirm_tx_dialog import ConfirmTxDialog from .transaction_dialog import PreviewTxDialog from .rbf_dialog import BumpFeeDialog, DSCancelDialog +from .qrreader import scan_qrcode if TYPE_CHECKING: from . import ElectrumGui @@ -2820,54 +2821,28 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): self.show_error("failed to import backup" + '\n' + str(e)) return - # Due to the asynchronous nature of the qr reader we need to keep the - # dialog instance as member variable to prevent reentrancy/multiple ones - # from being presented at once. - _qr_dialog = None - def read_tx_from_qrcode(self): - if self._qr_dialog: - self.logger.warning("QR dialog is already presented, ignoring.") - return - if self.gui_object.warn_if_cant_import_qrreader(self): - return - from .qrreader import QrReaderCameraDialog, CameraError, MissingQrDetectionLib - self._qr_dialog = None - try: - self._qr_dialog = QrReaderCameraDialog(parent=self.top_level_window(), config=self.config) - - def _on_qr_reader_finished(success: bool, error: str, data): - if self._qr_dialog: - self._qr_dialog.deleteLater() - self._qr_dialog = None - if not success: - if error: - self.show_error(error) - return - if not data: - return - # if the user scanned a bitcoin URI - if data.lower().startswith(BITCOIN_BIP21_URI_SCHEME + ':'): - self.pay_to_URI(data) - return - if data.lower().startswith('channel_backup:'): - self.import_channel_backup(data) - return - # else if the user scanned an offline signed tx - tx = self.tx_from_text(data) - if not tx: - return - self.show_transaction(tx) + def cb(success: bool, error: str, data): + if not success: + if error: + self.show_error(error) + return + if not data: + return + # if the user scanned a bitcoin URI + if data.lower().startswith(BITCOIN_BIP21_URI_SCHEME + ':'): + self.pay_to_URI(data) + return + if data.lower().startswith('channel_backup:'): + self.import_channel_backup(data) + return + # else if the user scanned an offline signed tx + tx = self.tx_from_text(data) + if not tx: + return + self.show_transaction(tx) - self._qr_dialog.qr_finished.connect(_on_qr_reader_finished) - self._qr_dialog.start_scan(self.config.get_video_device()) - except (MissingQrDetectionLib, CameraError) as e: - self._qr_dialog = None - self.show_error(str(e)) - except Exception as e: - self.logger.exception('camera error') - self._qr_dialog = None - self.show_error(repr(e)) + scan_qrcode(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 2fbdf34f6..cecd27f1f 100644 --- a/electrum/gui/qt/qrreader/__init__.py +++ b/electrum/gui/qt/qrreader/__init__.py @@ -1,30 +1,141 @@ -#!/usr/bin/env python3 +# Copyright (C) 2021 The Electrum developers +# Distributed under the MIT software license, see the accompanying +# file LICENCE or http://www.opensource.org/licenses/mit-license.php # -# Electron Cash - lightweight Bitcoin client -# Copyright (C) 2019 Axel Gembe +# We have two toolchains to scan qr codes: +# 1. access camera via QtMultimedia, take picture, feed picture to zbar +# 2. let zbar handle whole flow (including accessing the camera) # -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation files -# (the "Software"), to deal in the Software without restriction, -# including without limitation the rights to use, copy, modify, merge, -# publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: +# notes: +# - zbar needs to be compiled with platform-dependent extra config options to be able +# to access the camera +# - zbar fails to access the camera on macOS +# - qtmultimedia seems to support more cameras on Windows than zbar +# - qtmultimedia is often not packaged with PyQt5 +# in particular, on debian, you need both "python3-pyqt5" and "python3-pyqt5.qtmultimedia" +# - older versions of qtmultimedia don't seem to work reliably # -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. +# Considering the above, we use QtMultimedia for Windows and macOS, as there +# most users run our binaries where we can make sure the packaged versions work well. +# On Linux where many people run from source, we use zbar. # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -from .camera_dialog import (QrReaderCameraDialog, CameraError, NoCamerasFound, - NoCameraResolutionsFound, MissingQrDetectionLib) -from .validator import (QrReaderValidatorResult, AbstractQrReaderValidator, - QrReaderValidatorCounting, QrReaderValidatorColorizing, - QrReaderValidatorStrong, QrReaderValidatorCounted) +# Note: this module is safe to import on all platforms. + +import sys +from typing import Callable, Optional, TYPE_CHECKING, Mapping + +from PyQt5.QtWidgets import QMessageBox, QWidget + +from electrum.i18n import _ +from electrum.util import UserFacingException +from electrum.logging import get_logger + +from electrum.gui.qt.util import MessageBoxMixin, custom_message_box + + +if TYPE_CHECKING: + from electrum.simple_config import SimpleConfig + + +_logger = get_logger(__name__) + + +def scan_qrcode( + *, + parent: Optional[QWidget], + config: 'SimpleConfig', + callback: Callable[[bool, str, Optional[str]], None], +) -> None: + 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 find_system_cameras() -> Mapping[str, str]: + """Returns a camera_description -> camera_path map.""" + if sys.platform == 'darwin' or sys.platform in ('windows', 'win32'): + try: + from .qtmultimedia import find_system_cameras + except ImportError as e: + return {} + else: + return find_system_cameras() + else: # desktop Linux and similar + from electrum import qrscanner + return qrscanner.find_system_cameras() + + +# --- Internals below (not part of external API) + +def _scan_qrcode_using_zbar( + *, + parent: Optional[QWidget], + config: 'SimpleConfig', + callback: Callable[[bool, str, Optional[str]], None], +) -> None: + from electrum import qrscanner + data = None + try: + data = qrscanner.scan_barcode(config.get_video_device()) + except UserFacingException as e: + success = False + error = str(e) + except BaseException as e: + _logger.exception('camera error') + success = False + error = repr(e) + else: + success = True + error = "" + callback(success, error, data) + + +# Use a global to prevent multiple QR dialogs created simultaneously +_qr_dialog = None + + +def _scan_qrcode_using_qtmultimedia( + *, + parent: Optional[QWidget], + config: 'SimpleConfig', + callback: Callable[[bool, str, Optional[str]], None], +) -> None: + try: + from .qtmultimedia import QrReaderCameraDialog, CameraError, MissingQrDetectionLib + except ImportError as e: + icon = QMessageBox.Warning + title = _("QR Reader Error") + message = _("QR reader failed to load. This may happen if " + "you are using an older version of PyQt5.") + "\n\n" + str(e) + if isinstance(parent, MessageBoxMixin): + parent.msg_box(title=title, text=message, icon=icon, parent=None) + else: + custom_message_box(title=title, text=message, icon=icon, parent=parent) + return + + global _qr_dialog + if _qr_dialog: + _logger.warning("QR dialog is already presented, ignoring.") + return + _qr_dialog = None + try: + _qr_dialog = QrReaderCameraDialog(parent=parent, config=config) + + def _on_qr_reader_finished(success: bool, error: str, data): + global _qr_dialog + if _qr_dialog: + _qr_dialog.deleteLater() + _qr_dialog = None + callback(success, error, data) + + _qr_dialog.qr_finished.connect(_on_qr_reader_finished) + _qr_dialog.start_scan(config.get_video_device()) + except (MissingQrDetectionLib, CameraError) as e: + _qr_dialog = None + callback(False, str(e), None) + except Exception as e: + _logger.exception('camera error') + _qr_dialog = None + callback(False, repr(e), None) + diff --git a/electrum/gui/qt/qrreader/qtmultimedia/__init__.py b/electrum/gui/qt/qrreader/qtmultimedia/__init__.py new file mode 100644 index 000000000..942c44d21 --- /dev/null +++ b/electrum/gui/qt/qrreader/qtmultimedia/__init__.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# +# Electron Cash - lightweight Bitcoin client +# Copyright (C) 2019 Axel Gembe +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from typing import Mapping + +from .camera_dialog import (QrReaderCameraDialog, CameraError, NoCamerasFound, + NoCameraResolutionsFound, MissingQrDetectionLib) +from .validator import (QrReaderValidatorResult, AbstractQrReaderValidator, + QrReaderValidatorCounting, QrReaderValidatorColorizing, + QrReaderValidatorStrong, QrReaderValidatorCounted) + + +def find_system_cameras() -> Mapping[str, str]: + """Returns a camera_description -> camera_path map.""" + from PyQt5.QtMultimedia import QCameraInfo + system_cameras = QCameraInfo.availableCameras() + return {cam.description(): cam.deviceName() for cam in system_cameras} diff --git a/electrum/gui/qt/qrreader/camera_dialog.py b/electrum/gui/qt/qrreader/qtmultimedia/camera_dialog.py similarity index 99% rename from electrum/gui/qt/qrreader/camera_dialog.py rename to electrum/gui/qt/qrreader/qtmultimedia/camera_dialog.py index e3506a8af..d5bee7111 100644 --- a/electrum/gui/qt/qrreader/camera_dialog.py +++ b/electrum/gui/qt/qrreader/qtmultimedia/camera_dialog.py @@ -39,12 +39,14 @@ from electrum.i18n import _ from electrum.qrreader import get_qr_reader, QrCodeResult from electrum.logging import Logger +from electrum.gui.qt.util import MessageBoxMixin, FixedAspectRatioLayout, ImageGraphicsEffect + from .video_widget import QrReaderVideoWidget from .video_overlay import QrReaderVideoOverlay from .video_surface import QrReaderVideoSurface from .crop_blur_effect import QrReaderCropBlurEffect from .validator import AbstractQrReaderValidator, QrReaderValidatorCounted, QrReaderValidatorResult -from ..util import MessageBoxMixin, FixedAspectRatioLayout, ImageGraphicsEffect + class CameraError(RuntimeError): ''' Base class of the camera-related error conditions. ''' diff --git a/electrum/gui/qt/qrreader/crop_blur_effect.py b/electrum/gui/qt/qrreader/qtmultimedia/crop_blur_effect.py similarity index 100% rename from electrum/gui/qt/qrreader/crop_blur_effect.py rename to electrum/gui/qt/qrreader/qtmultimedia/crop_blur_effect.py diff --git a/electrum/gui/qt/qrreader/validator.py b/electrum/gui/qt/qrreader/qtmultimedia/validator.py similarity index 100% rename from electrum/gui/qt/qrreader/validator.py rename to electrum/gui/qt/qrreader/qtmultimedia/validator.py diff --git a/electrum/gui/qt/qrreader/video_overlay.py b/electrum/gui/qt/qrreader/qtmultimedia/video_overlay.py similarity index 100% rename from electrum/gui/qt/qrreader/video_overlay.py rename to electrum/gui/qt/qrreader/qtmultimedia/video_overlay.py diff --git a/electrum/gui/qt/qrreader/video_surface.py b/electrum/gui/qt/qrreader/qtmultimedia/video_surface.py similarity index 100% rename from electrum/gui/qt/qrreader/video_surface.py rename to electrum/gui/qt/qrreader/qtmultimedia/video_surface.py diff --git a/electrum/gui/qt/qrreader/video_widget.py b/electrum/gui/qt/qrreader/qtmultimedia/video_widget.py similarity index 100% rename from electrum/gui/qt/qrreader/video_widget.py rename to electrum/gui/qt/qrreader/qtmultimedia/video_widget.py diff --git a/electrum/gui/qt/qrtextedit.py b/electrum/gui/qt/qrtextedit.py index e7cbb50d5..3542090ac 100644 --- a/electrum/gui/qt/qrtextedit.py +++ b/electrum/gui/qt/qrtextedit.py @@ -7,6 +7,7 @@ from electrum.util import UserFacingException from electrum.logging import Logger from .util import ButtonsTextEdit, MessageBoxMixin, ColorScheme, getOpenFileName +from .qrreader import scan_qrcode class ShowQRTextEdit(ButtonsTextEdit): @@ -72,49 +73,23 @@ class ScanQRTextEdit(ButtonsTextEdit, MessageBoxMixin, Logger): else: self.setText(data) - # Due to the asynchronous nature of the qr reader we need to keep the - # dialog instance as member variable to prevent reentrancy/multiple ones - # from being presented at once. - qr_dialog = None - def qr_input(self, *, callback=None) -> None: - if self.qr_dialog: - self.logger.warning("QR dialog is already presented, ignoring.") - return - from . import ElectrumGui - if ElectrumGui.warn_if_cant_import_qrreader(self): - return - from .qrreader import QrReaderCameraDialog, CameraError, MissingQrDetectionLib - try: - self.qr_dialog = QrReaderCameraDialog(parent=self.top_level_window(), config=self.config) - - def _on_qr_reader_finished(success: bool, error: str, data): - if self.qr_dialog: - self.qr_dialog.deleteLater() - self.qr_dialog = None - if not success: - if error: - self.show_error(error) - return - if not data: - data = '' - if self.allow_multi: - new_text = self.text() + data + '\n' - else: - new_text = data - self.setText(new_text) - if callback and success: - callback(data) - - self.qr_dialog.qr_finished.connect(_on_qr_reader_finished) - self.qr_dialog.start_scan(self.config.get_video_device()) - except (MissingQrDetectionLib, CameraError) as e: - self.qr_dialog = None - self.show_error(str(e)) - except Exception as e: - self.logger.exception('camera error') - self.qr_dialog = None - self.show_error(repr(e)) + def cb(success: bool, error: str, data): + if not success: + if error: + self.show_error(error) + return + if not data: + data = '' + if self.allow_multi: + new_text = self.text() + data + '\n' + else: + new_text = data + self.setText(new_text) + if callback and success: + callback(data) + + scan_qrcode(parent=self.top_level_window(), config=self.config, callback=cb) def contextMenuEvent(self, e): m = self.createStandardContextMenu() diff --git a/electrum/gui/qt/settings_dialog.py b/electrum/gui/qt/settings_dialog.py index 12ca9d3ed..1109ca16b 100644 --- a/electrum/gui/qt/settings_dialog.py +++ b/electrum/gui/qt/settings_dialog.py @@ -220,18 +220,10 @@ class SettingsDialog(WindowModalDialog): msg = (_("For scanning QR codes.") + "\n" + _("Install the zbar package to enable this.")) qr_label = HelpLabel(_('Video Device') + ':', msg) - system_cameras = [] - try: - from PyQt5.QtMultimedia import QCameraInfo - system_cameras = QCameraInfo.availableCameras() - except ImportError as e: - # Older Qt or missing libs -- disable GUI control and inform user why - qr_combo.setEnabled(False) - qr_label.setEnabled(False) - qr_combo.setToolTip(_("Unable to probe for cameras on this system. QtMultimedia is likely missing.")) - qr_label.setToolTip(qr_combo.toolTip()) - for cam in system_cameras: - qr_combo.addItem(cam.description(), cam.deviceName()) + from .qrreader import find_system_cameras + system_cameras = find_system_cameras() + for cam_desc, cam_path in system_cameras.items(): + qr_combo.addItem(cam_desc, cam_path) index = qr_combo.findData(self.config.get("video_device")) qr_combo.setCurrentIndex(index) on_video_device = lambda x: self.config.set_key("video_device", qr_combo.itemData(x), True) diff --git a/electrum/qrscanner.py b/electrum/qrscanner.py index ccfeba997..6a611ccb1 100644 --- a/electrum/qrscanner.py +++ b/electrum/qrscanner.py @@ -26,7 +26,7 @@ import os import sys import ctypes -from typing import Optional +from typing import Optional, Mapping from .util import UserFacingException from .i18n import _ @@ -82,7 +82,7 @@ def scan_barcode(device='', timeout=-1, display=True, threaded=False) -> Optiona return data.decode('utf8') -def _find_system_cameras(): +def find_system_cameras() -> Mapping[str, str]: device_root = "/sys/class/video4linux" devices = {} # Name -> device if os.path.exists(device_root): diff --git a/setup.py b/setup.py index 1ad3103c6..73fa2359f 100755 --- a/setup.py +++ b/setup.py @@ -76,6 +76,7 @@ setup( 'electrum.gui', 'electrum.gui.qt', 'electrum.gui.qt.qrreader', + 'electrum.gui.qt.qrreader.qtmultimedia', 'electrum.plugins', ] + [('electrum.plugins.'+pkg) for pkg in find_packages('electrum/plugins')], package_dir={