From 256a467dd71d95efaf454bb00a5355f83159ed5d Mon Sep 17 00:00:00 2001 From: Tafelpoot Date: Fri, 24 Oct 2014 15:45:10 +0200 Subject: [PATCH] QR code fixes New classes ScanQRTextEdit and ShowQRTextEdit. Reason: dependencies on zbar availability and issues with the QRTextEdit constructor. - ScanQRTextEdit needs access to the config (fetch camera). It needs to load the zbar processor properly before trying to scan. Keeping a reference to the processor in qrscaner fixes the crashes on windows. - ShowQRTextEdit should not have access to scan_qr(). - no need to setReadOnly anymore. It is clear from the class name. Show master pub keys now has a Combobox if multiple accounts are available. --- gui/qt/installwizard.py | 2 +- gui/qt/main_window.py | 64 ++++++++++++++++++++++++++++------------- gui/qt/paytoedit.py | 15 ++++++---- gui/qt/qrcodewidget.py | 2 +- gui/qt/qrtextedit.py | 48 ++++++++++++++++++++++++------- gui/qt/seed_dialog.py | 11 +++---- gui/qt/util.py | 4 +-- lib/qrscanner.py | 15 ++++++++-- 8 files changed, 111 insertions(+), 50 deletions(-) diff --git a/gui/qt/installwizard.py b/gui/qt/installwizard.py index 34eeb367d..b5af998e3 100644 --- a/gui/qt/installwizard.py +++ b/gui/qt/installwizard.py @@ -128,7 +128,7 @@ class InstallWizard(QDialog): def enter_seed_dialog(self, msg, sid, func=None): if func is None: func = self.is_any - vbox, seed_e = seed_dialog.enter_seed_box(msg, sid) + vbox, seed_e = seed_dialog.enter_seed_box(msg, self, sid) vbox.addStretch(1) hbox, button = ok_cancel_buttons2(self, _('Next')) vbox.addLayout(hbox) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index 296361497..939b44f1c 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -45,7 +45,7 @@ from electrum import Imported_Wallet from amountedit import AmountEdit, BTCAmountEdit, MyLineEdit from network_dialog import NetworkDialog from qrcodewidget import QRCodeWidget, QRDialog -from qrtextedit import QRTextEdit +from qrtextedit import ScanQRTextEdit, ShowQRTextEdit from decimal import Decimal @@ -1828,15 +1828,36 @@ class ElectrumWindow(QMainWindow): main_layout = QGridLayout() mpk_dict = self.wallet.get_master_public_keys() - i = 0 - for key, value in mpk_dict.items(): - main_layout.addWidget(QLabel(key), i, 0) - mpk_text = QRTextEdit() - mpk_text.setReadOnly(True) + # filter out the empty keys (PendingAccount) + mpk_dict = {acc:mpk for acc,mpk in mpk_dict.items() if mpk} + + # only show the combobox in case multiple accounts are available + if len(mpk_dict) > 1: + main_layout.addWidget(QLabel(_("Select Account")), 0, 0) + + combobox = QComboBox() + for name in mpk_dict: + combobox.addItem(name) + combobox.setCurrentIndex(0) + main_layout.addWidget(combobox, 1, 0) + + account = unicode(combobox.currentText()) + mpk_text = ShowQRTextEdit(text=mpk_dict[account]) + mpk_text.setMaximumHeight(170) + mpk_text.selectAll() # for easy copying + main_layout.addWidget(mpk_text, 2, 0) + + def show_mpk(account): + mpk = mpk_dict.get(unicode(account), "") + mpk_text.setText(mpk) + + combobox.currentIndexChanged[str].connect(lambda acc: show_mpk(acc)) + elif len(mpk_dict) == 1: + mpk = mpk_dict.values()[0] + mpk_text = ShowQRTextEdit(text=mpk) mpk_text.setMaximumHeight(170) - mpk_text.setText(value) - main_layout.addWidget(mpk_text, i + 1, 0) - i += 2 + mpk_text.selectAll() # for easy copying + main_layout.addWidget(mpk_text, 2, 0) vbox = QVBoxLayout() vbox.addLayout(main_layout) @@ -1845,7 +1866,6 @@ class ElectrumWindow(QMainWindow): dialog.setLayout(vbox) dialog.exec_() - @protected def show_seed_dialog(self, password): if not self.wallet.has_seed(): @@ -1900,9 +1920,7 @@ class ElectrumWindow(QMainWindow): vbox = QVBoxLayout() vbox.addWidget( QLabel(_("Address") + ': ' + address)) vbox.addWidget( QLabel(_("Public key") + ':')) - keys = QRTextEdit() - keys.setReadOnly(True) - keys.setText('\n'.join(pubkey_list)) + keys = ShowQRTextEdit(text='\n'.join(pubkey_list)) vbox.addWidget(keys) vbox.addLayout(close_button(d)) d.setLayout(vbox) @@ -1924,9 +1942,7 @@ class ElectrumWindow(QMainWindow): vbox = QVBoxLayout() vbox.addWidget( QLabel(_("Address") + ': ' + address)) vbox.addWidget( QLabel(_("Private key") + ':')) - keys = QRTextEdit() - keys.setReadOnly(True) - keys.setText('\n'.join(pk_list)) + keys = ShowQRTextEdit(text='\n'.join(pk_list)) vbox.addWidget(keys) vbox.addLayout(close_button(d)) d.setLayout(vbox) @@ -2128,6 +2144,12 @@ class ElectrumWindow(QMainWindow): def read_tx_from_qrcode(self): from electrum import qrscanner + if qrscanner.proc is None: + try: + qrscanner.init(self.config) + except Exception, e: + QMessageBox.warning(self, _('Error'), _(e), _('OK')) + return try: data = qrscanner.scan_qr(self.config) except BaseException, e: @@ -2135,12 +2157,14 @@ class ElectrumWindow(QMainWindow): return if not data: return + # if the user scanned a bitcoin URI + if data.startswith("bitcoin:"): + self.pay_from_URI(data) + return + # else if the user scanned an offline signed tx # transactions are binary, but qrcode seems to return utf8... z = data.decode('utf8') - s = '' - for b in z: - s += chr(ord(b)) - data = s.encode('hex') + data = ''.join(chr(ord(b)) for b in z).encode('hex') tx = self.tx_from_text(data) if not tx: return diff --git a/gui/qt/paytoedit.py b/gui/qt/paytoedit.py index 472bad175..47aaba517 100644 --- a/gui/qt/paytoedit.py +++ b/gui/qt/paytoedit.py @@ -18,7 +18,7 @@ from PyQt4.QtCore import * from PyQt4.QtGui import * -from qrtextedit import QRTextEdit +from qrtextedit import ScanQRTextEdit import re from decimal import Decimal @@ -30,11 +30,9 @@ RE_ALIAS = '(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>' frozen_style = "QWidget { background-color:none; border:none;}" normal_style = "QPlainTextEdit { }" -class PayToEdit(QRTextEdit): - +class PayToEdit(ScanQRTextEdit): def __init__(self, win): - QRTextEdit.__init__(self) - self.win = win + super(PayToEdit,self).__init__(win=win) self.amount_edit = win.amount_e self.document().contentsChanged.connect(self.update_size) self.heightMin = 0 @@ -235,3 +233,10 @@ class PayToEdit(QRTextEdit): cr = self.cursorRect() cr.setWidth(self.c.popup().sizeHintForColumn(0) + self.c.popup().verticalScrollBar().sizeHint().width()) self.c.complete(cr) + + + def qr_input(self): + data = super(PayToEdit,self).qr_input() + if data.startswith("bitcoin:"): + self.scan_f(data) + # TODO: update fee diff --git a/gui/qt/qrcodewidget.py b/gui/qt/qrcodewidget.py index cc47b0535..0095baa63 100644 --- a/gui/qt/qrcodewidget.py +++ b/gui/qt/qrcodewidget.py @@ -114,7 +114,7 @@ class QRDialog(QDialog): def copy_to_clipboard(): bmp.save_qrcode(qrw.qr, filename) - self.parent().app.clipboard().setImage(QImage(filename)) + QApplication.clipboard().setImage(QImage(filename)) QMessageBox.information(None, _('Message'), _("QR code saved to clipboard"), _('OK')) b = QPushButton(_("Copy")) diff --git a/gui/qt/qrtextedit.py b/gui/qt/qrtextedit.py index 76b1aae1a..a381da799 100644 --- a/gui/qt/qrtextedit.py +++ b/gui/qt/qrtextedit.py @@ -3,14 +3,13 @@ from PyQt4.QtGui import * from PyQt4.QtCore import * class QRTextEdit(QPlainTextEdit): - + """Abstract class for QR-code related TextEdits. Do not use directly.""" def __init__(self, text=None): - QPlainTextEdit.__init__(self, text) + super(QRTextEdit, self).__init__(text) self.button = QToolButton(self) self.button.setIcon(QIcon(":icons/qrcode.png")) self.button.setStyleSheet("QToolButton { border: none; padding: 0px; }") self.button.setVisible(True) - self.button.clicked.connect(lambda: self.qr_show() if self.isReadOnly() else self.qr_input()) self.setText = self.setPlainText def resizeEvent(self, e): @@ -21,13 +20,11 @@ class QRTextEdit(QPlainTextEdit): (self.rect().bottom() - frameWidth - sz.height())) return o - def contextMenuEvent(self, e): - m = self.createStandardContextMenu() - if self.isReadOnly(): - m.addAction(_("Show as QR code"), self.qr_show) - else: - m.addAction(_("Read QR code"), self.qr_input) - m.exec_(e.globalPos()) +class ShowQRTextEdit(QRTextEdit): + def __init__(self, text=None): + super(ShowQRTextEdit, self).__init__(text) + self.setReadOnly(1) + self.button.clicked.connect(self.qr_show) def qr_show(self): from qrcodewidget import QRDialog @@ -37,13 +34,42 @@ class QRTextEdit(QPlainTextEdit): s = unicode(self.toPlainText()) QRDialog(s).exec_() + def contextMenuEvent(self, e): + m = self.createStandardContextMenu() + m.addAction(_("Show as QR code"), self.qr_show) + m.exec_(e.globalPos()) + + +class ScanQRTextEdit(QRTextEdit): + def __init__(self, win, text=""): + super(ScanQRTextEdit,self).__init__(text) + self.setReadOnly(0) + self.win = win + assert win, "You must pass a window with access to the config to ScanQRTextEdit constructor." + if win: + assert hasattr(win,"config"), "You must pass a window with access to the config to ScanQRTextEdit constructor." + self.button.clicked.connect(self.qr_input) + + def qr_input(self): from electrum import qrscanner + if qrscanner.proc is None: + try: + qrscanner.init(self.win.config) + except Exception, e: + QMessageBox.warning(self, _('Error'), _(e), _('OK')) + return try: data = qrscanner.scan_qr(self.win.config) except BaseException, e: - QMessageBox.warning(self.win, _('Error'), _(e), _('OK')) + QMessageBox.warning(self, _('Error'), _(e), _('OK')) return if type(data) != str: return self.setText(data) + return data + + def contextMenuEvent(self, e): + m = self.createStandardContextMenu() + m.addAction(_("Read QR code"), self.qr_input) + m.exec_(e.globalPos()) diff --git a/gui/qt/seed_dialog.py b/gui/qt/seed_dialog.py index 4483d1b61..c27f77050 100644 --- a/gui/qt/seed_dialog.py +++ b/gui/qt/seed_dialog.py @@ -23,7 +23,7 @@ from electrum.i18n import _ from electrum import mnemonic from qrcodewidget import QRCodeWidget, QRDialog from util import close_button -from qrtextedit import QRTextEdit +from qrtextedit import ShowQRTextEdit, ScanQRTextEdit class SeedDialog(QDialog): def __init__(self, parent, seed, imported_keys): @@ -72,15 +72,13 @@ def show_seed_box(seed, sid=None): + _("If you ever need to recover your wallet from seed, you will need both this seed and your cold seed.") + " " \ label1 = QLabel(msg+ ":") - seed_text = QRTextEdit(seed) - seed_text.setReadOnly(True) + seed_text = ShowQRTextEdit(text=seed) seed_text.setMaximumHeight(130) label2 = QLabel(msg2) label2.setWordWrap(True) logo = QLabel() - logo.setPixmap(QPixmap(icon_filename(sid)).scaledToWidth(56)) logo.setMaximumWidth(60) @@ -96,8 +94,7 @@ def show_seed_box(seed, sid=None): return vbox -def enter_seed_box(msg, sid=None): - +def enter_seed_box(msg, window, sid=None): vbox = QVBoxLayout() logo = QLabel() logo.setPixmap(QPixmap(icon_filename(sid)).scaledToWidth(56)) @@ -106,7 +103,7 @@ def enter_seed_box(msg, sid=None): label = QLabel(msg) label.setWordWrap(True) - seed_e = QRTextEdit() + seed_e = ScanQRTextEdit(win=window) seed_e.setMaximumHeight(100) seed_e.setTabChangesFocus(True) diff --git a/gui/qt/util.py b/gui/qt/util.py index ba6624ad3..ac041a549 100644 --- a/gui/qt/util.py +++ b/gui/qt/util.py @@ -137,7 +137,7 @@ def line_dialog(parent, title, label, ok_label, default=None): return unicode(txt.text()) def text_dialog(parent, title, label, ok_label, default=None): - from qrtextedit import QRTextEdit + from qrtextedit import ScanQRTextEdit dialog = QDialog(parent) dialog.setMinimumWidth(500) dialog.setWindowTitle(title) @@ -145,7 +145,7 @@ def text_dialog(parent, title, label, ok_label, default=None): l = QVBoxLayout() dialog.setLayout(l) l.addWidget(QLabel(label)) - txt = QRTextEdit() + txt = ScanQRTextEdit(parent) if default: txt.setText(default) l.addWidget(txt) diff --git a/lib/qrscanner.py b/lib/qrscanner.py index 736947df1..a8caae03b 100644 --- a/lib/qrscanner.py +++ b/lib/qrscanner.py @@ -6,28 +6,37 @@ try: except ImportError: zbar = None +proc = None -def scan_qr(config): +def init(config): if not zbar: raise BaseException("\n".join([_("Cannot start QR scanner."),_("The zbar package is not available."),_("On Linux, try 'sudo apt-get install python-zbar'")])) device = config.get("video_device", "default") if device == 'default': device = '' + global proc proc = zbar.Processor() proc.init(video_device=device) + +def scan_qr(self): + if not zbar: + raise BaseException("\n".join([_("Cannot start QR scanner."),_("The zbar package is not available."),_("On Linux, try 'sudo apt-get install python-zbar'")])) + if proc is None: + raise BaseException("Start proc first") proc.visible = True while True: try: proc.process_one() except Exception: # User closed the preview window - return {} + return "" for r in proc.results: if str(r.type) != 'QRCODE': continue + # hiding the preview window stops the camera + proc.visible = False return r.data - def _find_system_cameras(): device_root = "/sys/class/video4linux" devices = {} # Name -> device