Browse Source

qt: qrreader: keep both old and new toolchain; try to abstract it away

patch-4
SomberNight 3 years ago
parent
commit
013cf869f1
No known key found for this signature in database GPG Key ID: B33B5F232C6271E9
  1. 2
      contrib/build-wine/deterministic.spec
  2. 56
      contrib/make_zbar.sh
  3. 2
      contrib/osx/osx.spec
  4. 22
      electrum/gui/qt/__init__.py
  5. 67
      electrum/gui/qt/main_window.py
  6. 163
      electrum/gui/qt/qrreader/__init__.py
  7. 39
      electrum/gui/qt/qrreader/qtmultimedia/__init__.py
  8. 4
      electrum/gui/qt/qrreader/qtmultimedia/camera_dialog.py
  9. 0
      electrum/gui/qt/qrreader/qtmultimedia/crop_blur_effect.py
  10. 0
      electrum/gui/qt/qrreader/qtmultimedia/validator.py
  11. 0
      electrum/gui/qt/qrreader/qtmultimedia/video_overlay.py
  12. 0
      electrum/gui/qt/qrreader/qtmultimedia/video_surface.py
  13. 0
      electrum/gui/qt/qrreader/qtmultimedia/video_widget.py
  14. 59
      electrum/gui/qt/qrtextedit.py
  15. 16
      electrum/gui/qt/settings_dialog.py
  16. 4
      electrum/qrscanner.py
  17. 1
      setup.py

2
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',

56
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"

2
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',

22
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)

67
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(

163
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 <derago@gmail.com>
# 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)

39
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 <derago@gmail.com>
#
# 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}

4
electrum/gui/qt/qrreader/camera_dialog.py → 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. '''

0
electrum/gui/qt/qrreader/crop_blur_effect.py → electrum/gui/qt/qrreader/qtmultimedia/crop_blur_effect.py

0
electrum/gui/qt/qrreader/validator.py → electrum/gui/qt/qrreader/qtmultimedia/validator.py

0
electrum/gui/qt/qrreader/video_overlay.py → electrum/gui/qt/qrreader/qtmultimedia/video_overlay.py

0
electrum/gui/qt/qrreader/video_surface.py → electrum/gui/qt/qrreader/qtmultimedia/video_surface.py

0
electrum/gui/qt/qrreader/video_widget.py → electrum/gui/qt/qrreader/qtmultimedia/video_widget.py

59
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()

16
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)

4
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):

1
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={

Loading…
Cancel
Save