diff --git a/electrum/gui/icons/link.png b/electrum/gui/icons/link.png new file mode 100644 index 000000000..af462bc20 Binary files /dev/null and b/electrum/gui/icons/link.png differ diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index af725106c..c99bd830c 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -778,6 +778,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): tools_menu.addSeparator() paytomany_menu = tools_menu.addAction(_("&Pay to many"), self.paytomany) + tools_menu.addAction(_("&Show QR code in separate window"), self.toggle_receive_qr_window) raw_transaction_menu = tools_menu.addMenu(_("&Load transaction")) raw_transaction_menu.addAction(_("&From file"), self.do_process_from_file) @@ -1092,6 +1093,20 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): d = LightningTxDialog(self, tx_item) d.show() + def toggle_receive_qr(self, toggle=False): + b = not self.config.get('receive_qr_visible', False) + self.config.set_key('receive_qr_visible', b) + self.update_receive_widgets() + + def update_receive_widgets(self): + b = self.config.get('receive_qr_visible', False) + self.receive_address_e.setVisible(b) + self.receive_address_qr.setVisible(not b) + self.receive_URI_e.setVisible(b) + self.receive_URI_qr.setVisible(not b) + self.receive_lightning_e.setVisible(b) + self.receive_lightning_qr.setVisible(not b) + def create_receive_tab(self): # A 4-column grid layout. All the stretch is in the last column. # The exchange rate plugin adds a fiat widget in column 2 @@ -1099,15 +1114,13 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): grid.setSpacing(8) grid.setColumnStretch(3, 1) - self.receive_message_e = SizedFreezableLineEdit(width=700) + self.receive_message_e = SizedFreezableLineEdit(width=400) grid.addWidget(QLabel(_('Description')), 0, 0) grid.addWidget(self.receive_message_e, 0, 1, 1, 4) - self.receive_message_e.textChanged.connect(self.update_receive_qr) self.receive_amount_e = BTCAmountEdit(self.get_decimal_point) grid.addWidget(QLabel(_('Requested amount')), 1, 0) grid.addWidget(self.receive_amount_e, 1, 1) - self.receive_amount_e.textChanged.connect(self.update_receive_qr) self.fiat_receive_e = AmountEdit(self.fx.get_currency if self.fx else '') if not self.fx or not self.fx.is_enabled(): @@ -1159,48 +1172,67 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): buttons.addWidget(self.create_invoice_button) grid.addLayout(buttons, 4, 0, 1, -1) - self.receive_payreq_e = ButtonsTextEdit() - self.receive_payreq_e.setFont(QFont(MONOSPACE_FONT)) - self.receive_payreq_e.addCopyButton(self.app) - self.receive_payreq_e.setReadOnly(True) - self.receive_payreq_e.textChanged.connect(self.update_receive_qr) - self.receive_payreq_e.setFocusPolicy(Qt.ClickFocus) - - self.receive_qr = QRCodeWidget(fixedSize=220) - self.receive_qr.mouseReleaseEvent = lambda x: self.toggle_qr_window() - self.receive_qr.enterEvent = lambda x: self.app.setOverrideCursor(QCursor(Qt.PointingHandCursor)) - self.receive_qr.leaveEvent = lambda x: self.app.setOverrideCursor(QCursor(Qt.ArrowCursor)) - self.receive_address_e = ButtonsTextEdit() - self.receive_address_e.setFont(QFont(MONOSPACE_FONT)) - self.receive_address_e.addCopyButton(self.app) - self.receive_address_e.setReadOnly(True) - self.receive_address_e.textChanged.connect(self.update_receive_address_styling) + self.receive_URI_e = ButtonsTextEdit() + self.receive_lightning_e = ButtonsTextEdit() + #self.receive_URI_e.setFocusPolicy(Qt.ClickFocus) - qr_show = lambda: self.show_qrcode(str(self.receive_address_e.text()), _('Receiving address'), parent=self) + fixedSize = 200 qr_icon = "qrcode_white.png" if ColorScheme.dark_scheme else "qrcode.png" - self.receive_address_e.addButton(qr_icon, qr_show, _("Show as QR code")) + for e in [self.receive_address_e, self.receive_URI_e, self.receive_lightning_e]: + e.setFont(QFont(MONOSPACE_FONT)) + e.addCopyButton(self.app) + e.setReadOnly(True) + e.setFixedSize(fixedSize, fixedSize) + e.addButton(qr_icon, self.toggle_receive_qr, _("Show as QR code")) + + self.receive_address_qr = QRCodeWidget(fixedSize=fixedSize) + self.receive_URI_qr = QRCodeWidget(fixedSize=fixedSize) + self.receive_lightning_qr = QRCodeWidget(fixedSize=fixedSize) + + self.receive_lightning_e.textChanged.connect(self.update_receive_widgets) + + receive_address_layout = QHBoxLayout() + receive_address_layout.addWidget(self.receive_address_e) + receive_address_layout.addWidget(self.receive_address_qr) + receive_URI_layout = QHBoxLayout() + receive_URI_layout.addWidget(self.receive_URI_e) + receive_URI_layout.addWidget(self.receive_URI_qr) + receive_lightning_layout = QHBoxLayout() + receive_lightning_layout.addWidget(self.receive_lightning_e) + receive_lightning_layout.addWidget(self.receive_lightning_qr) + + from .util import VTabWidget + self.receive_tabs = VTabWidget() + receive_address_widget = QWidget() + receive_address_widget.setLayout(receive_address_layout) + receive_URI_widget = QWidget() + receive_URI_widget.setLayout(receive_URI_layout) + receive_lightning_widget = QWidget() + receive_lightning_widget.setLayout(receive_lightning_layout) + + self.receive_tabs.addTab(receive_URI_widget, read_QIcon("link.png"), _('URI')) + self.receive_tabs.addTab(receive_address_widget, read_QIcon("bitcoin.png"), _('Address')) + self.receive_tabs.addTab(receive_lightning_widget, read_QIcon("lightning.png"), _('Lightning')) + def on_current_changed(index): + self.update_receive_qr_window() + def on_tab_bar_clicked(index): + w = self.receive_tabs.widget(index) + if w == self.receive_tabs.currentWidget() and self.receive_tabs.isTabEnabled(index): + self.toggle_receive_qr() + self.receive_tabs.tabBarClicked.connect(on_tab_bar_clicked) + self.receive_tabs.currentChanged.connect(on_current_changed) + self.receive_tabs.setCurrentIndex(self.config.get('receive_tabs_index', 0)) + self.receive_tabs.currentChanged.connect(lambda i: self.config.set_key('receive_tabs_index', i)) + receive_tabs_sp = self.receive_tabs.sizePolicy() + receive_tabs_sp.setRetainSizeWhenHidden(True) + self.receive_tabs.setSizePolicy(receive_tabs_sp) + self.receive_tabs.setVisible(False) self.receive_requests_label = QLabel(_('Receive queue')) - from .request_list import RequestList self.request_list = RequestList(self) - receive_tabs = QTabWidget() - receive_tabs.addTab(self.receive_address_e, _('Address')) - receive_tabs.addTab(self.receive_payreq_e, _('Request')) - receive_tabs.addTab(self.receive_qr, _('QR Code')) - receive_tabs.setCurrentIndex(self.config.get('receive_tabs_index', 0)) - receive_tabs.currentChanged.connect(lambda i: self.config.set_key('receive_tabs_index', i)) - receive_tabs_sp = receive_tabs.sizePolicy() - receive_tabs_sp.setRetainSizeWhenHidden(True) - receive_tabs.setSizePolicy(receive_tabs_sp) - - def maybe_hide_receive_tabs(): - receive_tabs.setVisible(bool(self.receive_payreq_e.text())) - self.receive_payreq_e.textChanged.connect(maybe_hide_receive_tabs) - maybe_hide_receive_tabs() - # layout vbox_g = QVBoxLayout() vbox_g.addLayout(grid) @@ -1208,7 +1240,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): hbox = QHBoxLayout() hbox.addLayout(vbox_g) hbox.addStretch() - hbox.addWidget(receive_tabs) + hbox.addWidget(self.receive_tabs) w = QWidget() w.searchable_list = self.request_list @@ -1222,6 +1254,43 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): return w + def show_receive_request(self, req): + addr = req.get_address() or '' + URI = req.get_bip21_URI() if addr else '' + lnaddr = req.lightning_invoice or '' + can_receive_lightning = self.wallet.lnworker and req.get_amount_sat() <= self.wallet.lnworker.num_sats_can_receive() + if not can_receive_lightning: + lnaddr = '' + icon_name = "lightning.png" if can_receive_lightning else "lightning_disconnected.png" + self.receive_tabs.setTabIcon(2, read_QIcon(icon_name)) + # encode lightning invoices as uppercase so QR encoding can use + # alphanumeric mode; resulting in smaller QR codes + lnaddr_qr = lnaddr.upper() + self.receive_address_e.setText(addr) + self.receive_address_qr.setData(addr) + self.receive_URI_e.setText(URI) + self.receive_URI_qr.setData(URI) + self.receive_lightning_e.setText(lnaddr) # TODO maybe prepend "lightning:" ?? + self.receive_lightning_qr.setData(lnaddr_qr) + # macOS hack (similar to #4777) + self.receive_lightning_e.repaint() + self.receive_URI_e.repaint() + self.receive_address_e.repaint() + # always show + self.receive_tabs.setVisible(True) + self.update_receive_qr_window() + + def update_receive_qr_window(self): + if self.qr_window and self.qr_window.isVisible(): + i = self.receive_tabs.currentIndex() + if i == 0: + data = self.receive_address_qr.data + elif i == 1: + data = self.receive_URI_qr.data + else: + data = self.receive_lightning_qr.data + self.qr_window.qrw.setData(data) + def delete_requests(self, keys): for key in keys: self.wallet.delete_request(key) @@ -1276,7 +1345,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): except InvoiceError as e: self.show_error(_('Error creating payment request') + ':\n' + str(e)) return - + except Exception as e: + self.logger.exception('Error adding payment request') + self.show_error(_('Error adding payment request') + ':\n' + repr(e)) + return + self.sign_payment_request(address) assert key is not None self.address_list.refresh_all() self.request_list.update() @@ -1290,7 +1363,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): title = _('Invoice') if r.is_lightning() else _('Address') self.do_copy(content, title=title) - def get_bitcoin_address_for_request(self, amount: int) -> Optional[str]: + def get_bitcoin_address_for_request(self, amount) -> Optional[str]: addr = self.wallet.get_unused_address() if addr is None: if not self.wallet.is_deterministic(): # imported wallet @@ -1318,15 +1391,17 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): QToolTip.showText(QCursor.pos(), tooltip_text, self) def clear_receive_tab(self): - self.receive_payreq_e.setText('') self.receive_address_e.setText('') + self.receive_URI_e.setText('') + self.receive_lightning_e.setText('') + self.receive_tabs.setVisible(False) self.receive_message_e.setText('') self.receive_amount_e.setAmount(None) self.expires_label.hide() self.expires_combo.show() self.request_list.clearSelection() - def toggle_qr_window(self): + def toggle_receive_qr_window(self): from . import qrwindow if not self.qr_window: self.qr_window = qrwindow.QR_Window(self) @@ -1339,7 +1414,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): else: self.qr_window_geometry = self.qr_window.geometry() self.qr_window.setVisible(False) - self.update_receive_qr() def show_send_tab(self): self.tabs.setCurrentIndex(self.tabs.indexOf(self.send_tab)) @@ -1347,16 +1421,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): def show_receive_tab(self): self.tabs.setCurrentIndex(self.tabs.indexOf(self.receive_tab)) - def update_receive_qr(self): - uri = str(self.receive_payreq_e.text()) - if maybe_extract_bolt11_invoice(uri): - # encode lightning invoices as uppercase so QR encoding can use - # alphanumeric mode; resulting in smaller QR codes - uri = uri.upper() - self.receive_qr.setData(uri) - if self.qr_window and self.qr_window.isVisible(): - self.qr_window.qrw.setData(uri) - def update_receive_address_styling(self): addr = str(self.receive_address_e.text()) if is_address(addr) and self.wallet.is_used(addr): @@ -1624,6 +1688,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): self.need_update.set() def on_payment_failed(self, wallet, key, reason): + invoice = self.wallet.get_invoice(key) self.show_error(_('Payment failed') + '\n\n' + reason) def read_invoice(self): diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py index 082995445..ad0fbc6c5 100644 --- a/electrum/gui/qt/request_list.py +++ b/electrum/gui/qt/request_list.py @@ -86,7 +86,8 @@ class RequestList(MyTreeView): def item_changed(self, idx: Optional[QModelIndex]): if idx is None: - self.parent.receive_payreq_e.setText('') + self.parent.receive_URI_e.setText('') + self.parent.receive_lightning_e.setText('') self.parent.receive_address_e.setText('') return if not idx.isValid(): @@ -98,14 +99,7 @@ class RequestList(MyTreeView): if req is None: self.update() return - if req.is_lightning(): - self.parent.receive_payreq_e.setText(req.lightning_invoice) # TODO maybe prepend "lightning:" ?? - self.parent.receive_address_e.setText(req.lightning_invoice) - else: - self.parent.receive_payreq_e.setText(self.parent.wallet.get_request_URI(req)) - self.parent.receive_address_e.setText(req.get_address()) - self.parent.receive_payreq_e.repaint() # macOS hack (similar to #4777) - self.parent.receive_address_e.repaint() # macOS hack (similar to #4777) + self.parent.show_receive_request(req) def clearSelection(self): super().clearSelection() @@ -138,20 +132,12 @@ class RequestList(MyTreeView): date = format_time(timestamp) amount_str = self.parent.format_amount(amount) if amount else "" labels = [date, message, amount_str, status_str] - if req.is_lightning(): - icon = read_QIcon("lightning.png") - tooltip = 'lightning request' - else: - icon = read_QIcon("bitcoin.png") - tooltip = 'onchain request' items = [QStandardItem(e) for e in labels] self.set_editability(items) #items[self.Columns.DATE].setData(request_type, ROLE_REQUEST_TYPE) items[self.Columns.DATE].setData(key, ROLE_KEY) items[self.Columns.DATE].setData(timestamp, ROLE_SORT_ORDER) - items[self.Columns.DATE].setIcon(icon) items[self.Columns.STATUS].setIcon(read_QIcon(pr_icons.get(status))) - items[self.Columns.DATE].setToolTip(tooltip) self.std_model.insertRow(self.std_model.rowCount(), items) self.filter() self.proxy.setDynamicSortFilter(True) @@ -186,13 +172,13 @@ class RequestList(MyTreeView): self.update() return menu = QMenu(self) - self.add_copy_menu(menu, idx) - if req.is_lightning(): - menu.addAction(_("Copy Request"), lambda: self.parent.do_copy(req.lightning_invoice, title='Lightning Request')) - else: - URI = self.wallet.get_request_URI(req) - menu.addAction(_("Copy Request"), lambda: self.parent.do_copy(URI, title='Bitcoin URI')) + if req.get_address(): menu.addAction(_("Copy Address"), lambda: self.parent.do_copy(req.get_address(), title='Bitcoin Address')) + URI = self.wallet.get_request_URI(req) + menu.addAction(_("Copy URI"), lambda: self.parent.do_copy(URI, title='Bitcoin URI')) + if req.is_lightning(): + menu.addAction(_("Copy Lightning Request"), lambda: self.parent.do_copy(req.lightning_invoice, title='Lightning Request')) + self.add_copy_menu(menu, idx) #if 'view_url' in req: # menu.addAction(_("View in web browser"), lambda: webopen(req['view_url'])) menu.addAction(_("Delete"), lambda: self.parent.delete_requests([key])) diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index d9158ecc3..9376eb421 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -1301,6 +1301,47 @@ class ImageGraphicsEffect(QObject): return result +# vertical tabs +# from https://stackoverflow.com/questions/51230544/pyqt5-how-to-set-tabwidget-west-but-keep-the-text-horizontal +from PyQt5 import QtWidgets, QtCore + +class VTabBar(QtWidgets.QTabBar): + + def tabSizeHint(self, index): + s = QtWidgets.QTabBar.tabSizeHint(self, index) + s.transpose() + return s + + def paintEvent(self, event): + painter = QtWidgets.QStylePainter(self) + opt = QtWidgets.QStyleOptionTab() + + for i in range(self.count()): + self.initStyleOption(opt, i) + painter.drawControl(QtWidgets.QStyle.CE_TabBarTabShape, opt) + painter.save() + + s = opt.rect.size() + s.transpose() + r = QtCore.QRect(QtCore.QPoint(), s) + r.moveCenter(opt.rect.center()) + opt.rect = r + + c = self.tabRect(i).center() + painter.translate(c) + painter.rotate(90) + painter.translate(-c) + painter.drawControl(QtWidgets.QStyle.CE_TabBarTabLabel, opt); + painter.restore() + + +class VTabWidget(QtWidgets.QTabWidget): + def __init__(self, *args, **kwargs): + QtWidgets.QTabWidget.__init__(self, *args, **kwargs) + self.setTabBar(VTabBar(self)) + self.setTabPosition(QtWidgets.QTabWidget.West) + + if __name__ == "__main__": app = QApplication([]) t = WaitingDialog(None, 'testing ...', lambda: [time.sleep(1)], lambda x: QMessageBox.information(None, 'done', "done"))