request_list.py (9422B)
1 #!/usr/bin/env python 2 # 3 # Electrum - lightweight Bitcoin client 4 # Copyright (C) 2015 Thomas Voegtlin 5 # 6 # Permission is hereby granted, free of charge, to any person 7 # obtaining a copy of this software and associated documentation files 8 # (the "Software"), to deal in the Software without restriction, 9 # including without limitation the rights to use, copy, modify, merge, 10 # publish, distribute, sublicense, and/or sell copies of the Software, 11 # and to permit persons to whom the Software is furnished to do so, 12 # subject to the following conditions: 13 # 14 # The above copyright notice and this permission notice shall be 15 # included in all copies or substantial portions of the Software. 16 # 17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 21 # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 22 # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 23 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 # SOFTWARE. 25 26 from enum import IntEnum 27 from typing import Optional, TYPE_CHECKING 28 29 from PyQt5.QtGui import QStandardItemModel, QStandardItem 30 from PyQt5.QtWidgets import QMenu, QAbstractItemView 31 from PyQt5.QtCore import Qt, QItemSelectionModel, QModelIndex 32 33 from electrum.i18n import _ 34 from electrum.util import format_time 35 from electrum.invoices import PR_TYPE_ONCHAIN, PR_TYPE_LN, LNInvoice, OnchainInvoice 36 from electrum.plugin import run_hook 37 from electrum.invoices import Invoice 38 39 from .util import MyTreeView, pr_icons, read_QIcon, webopen, MySortModel 40 41 if TYPE_CHECKING: 42 from .main_window import ElectrumWindow 43 44 45 ROLE_REQUEST_TYPE = Qt.UserRole 46 ROLE_KEY = Qt.UserRole + 1 47 ROLE_SORT_ORDER = Qt.UserRole + 2 48 49 50 class RequestList(MyTreeView): 51 52 class Columns(IntEnum): 53 DATE = 0 54 DESCRIPTION = 1 55 AMOUNT = 2 56 STATUS = 3 57 58 headers = { 59 Columns.DATE: _('Date'), 60 Columns.DESCRIPTION: _('Description'), 61 Columns.AMOUNT: _('Amount'), 62 Columns.STATUS: _('Status'), 63 } 64 filter_columns = [Columns.DATE, Columns.DESCRIPTION, Columns.AMOUNT] 65 66 def __init__(self, parent: 'ElectrumWindow'): 67 super().__init__(parent, self.create_menu, 68 stretch_column=self.Columns.DESCRIPTION, 69 editable_columns=[]) 70 self.wallet = self.parent.wallet 71 self.std_model = QStandardItemModel(self) 72 self.proxy = MySortModel(self, sort_role=ROLE_SORT_ORDER) 73 self.proxy.setSourceModel(self.std_model) 74 self.setModel(self.proxy) 75 self.setSortingEnabled(True) 76 self.selectionModel().currentRowChanged.connect(self.item_changed) 77 self.setSelectionMode(QAbstractItemView.ExtendedSelection) 78 self.update() 79 80 def select_key(self, key): 81 for i in range(self.model().rowCount()): 82 item = self.model().index(i, self.Columns.DATE) 83 row_key = item.data(ROLE_KEY) 84 if key == row_key: 85 self.selectionModel().setCurrentIndex(item, QItemSelectionModel.SelectCurrent | QItemSelectionModel.Rows) 86 break 87 88 def item_changed(self, idx: Optional[QModelIndex]): 89 if idx is None: 90 self.parent.receive_payreq_e.setText('') 91 self.parent.receive_address_e.setText('') 92 return 93 if not idx.isValid(): 94 return 95 # TODO use siblingAtColumn when min Qt version is >=5.11 96 item = self.item_from_index(idx.sibling(idx.row(), self.Columns.DATE)) 97 key = item.data(ROLE_KEY) 98 req = self.wallet.get_request(key) 99 if req is None: 100 self.update() 101 return 102 if req.is_lightning(): 103 self.parent.receive_payreq_e.setText(req.invoice) # TODO maybe prepend "lightning:" ?? 104 self.parent.receive_address_e.setText(req.invoice) 105 else: 106 self.parent.receive_payreq_e.setText(self.parent.wallet.get_request_URI(req)) 107 self.parent.receive_address_e.setText(req.get_address()) 108 self.parent.receive_payreq_e.repaint() # macOS hack (similar to #4777) 109 self.parent.receive_address_e.repaint() # macOS hack (similar to #4777) 110 111 def clearSelection(self): 112 super().clearSelection() 113 self.selectionModel().clearCurrentIndex() 114 115 def refresh_status(self): 116 m = self.std_model 117 for r in range(m.rowCount()): 118 idx = m.index(r, self.Columns.STATUS) 119 date_idx = idx.sibling(idx.row(), self.Columns.DATE) 120 date_item = m.itemFromIndex(date_idx) 121 status_item = m.itemFromIndex(idx) 122 key = date_item.data(ROLE_KEY) 123 req = self.wallet.get_request(key) 124 if req: 125 status = self.parent.wallet.get_request_status(key) 126 status_str = req.get_status_str(status) 127 status_item.setText(status_str) 128 status_item.setIcon(read_QIcon(pr_icons.get(status))) 129 130 def update_item(self, key, invoice: Invoice): 131 model = self.std_model 132 for row in range(0, model.rowCount()): 133 item = model.item(row, 0) 134 if item.data(ROLE_KEY) == key: 135 break 136 else: 137 return 138 status_item = model.item(row, self.Columns.STATUS) 139 status = self.parent.wallet.get_request_status(key) 140 status_str = invoice.get_status_str(status) 141 status_item.setText(status_str) 142 status_item.setIcon(read_QIcon(pr_icons.get(status))) 143 144 def update(self): 145 # not calling maybe_defer_update() as it interferes with conditional-visibility 146 self.parent.update_receive_address_styling() 147 self.proxy.setDynamicSortFilter(False) # temp. disable re-sorting after every change 148 self.std_model.clear() 149 self.update_headers(self.__class__.headers) 150 for req in self.wallet.get_unpaid_requests(): 151 key = self.wallet.get_key_for_receive_request(req) 152 status = self.parent.wallet.get_request_status(key) 153 status_str = req.get_status_str(status) 154 request_type = req.type 155 timestamp = req.time 156 amount = req.get_amount_sat() 157 message = req.message 158 date = format_time(timestamp) 159 amount_str = self.parent.format_amount(amount) if amount else "" 160 labels = [date, message, amount_str, status_str] 161 if req.is_lightning(): 162 icon = read_QIcon("lightning.png") 163 tooltip = 'lightning request' 164 else: 165 icon = read_QIcon("bitcoin.png") 166 tooltip = 'onchain request' 167 items = [QStandardItem(e) for e in labels] 168 self.set_editability(items) 169 items[self.Columns.DATE].setData(request_type, ROLE_REQUEST_TYPE) 170 items[self.Columns.DATE].setData(key, ROLE_KEY) 171 items[self.Columns.DATE].setData(timestamp, ROLE_SORT_ORDER) 172 items[self.Columns.DATE].setIcon(icon) 173 items[self.Columns.STATUS].setIcon(read_QIcon(pr_icons.get(status))) 174 items[self.Columns.DATE].setToolTip(tooltip) 175 self.std_model.insertRow(self.std_model.rowCount(), items) 176 self.filter() 177 self.proxy.setDynamicSortFilter(True) 178 # sort requests by date 179 self.sortByColumn(self.Columns.DATE, Qt.DescendingOrder) 180 # hide list if empty 181 if self.parent.isVisible(): 182 b = self.std_model.rowCount() > 0 183 self.setVisible(b) 184 self.parent.receive_requests_label.setVisible(b) 185 if not b: 186 # list got hidden, so selected item should also be cleared: 187 self.item_changed(None) 188 189 def create_menu(self, position): 190 items = self.selected_in_column(0) 191 if len(items)>1: 192 keys = [ item.data(ROLE_KEY) for item in items] 193 menu = QMenu(self) 194 menu.addAction(_("Delete requests"), lambda: self.parent.delete_requests(keys)) 195 menu.exec_(self.viewport().mapToGlobal(position)) 196 return 197 idx = self.indexAt(position) 198 # TODO use siblingAtColumn when min Qt version is >=5.11 199 item = self.item_from_index(idx.sibling(idx.row(), self.Columns.DATE)) 200 if not item: 201 return 202 key = item.data(ROLE_KEY) 203 req = self.wallet.get_request(key) 204 if req is None: 205 self.update() 206 return 207 menu = QMenu(self) 208 self.add_copy_menu(menu, idx) 209 if req.is_lightning(): 210 menu.addAction(_("Copy Request"), lambda: self.parent.do_copy(req.invoice, title='Lightning Request')) 211 else: 212 URI = self.wallet.get_request_URI(req) 213 menu.addAction(_("Copy Request"), lambda: self.parent.do_copy(URI, title='Bitcoin URI')) 214 menu.addAction(_("Copy Address"), lambda: self.parent.do_copy(req.get_address(), title='Bitcoin Address')) 215 #if 'view_url' in req: 216 # menu.addAction(_("View in web browser"), lambda: webopen(req['view_url'])) 217 menu.addAction(_("Delete"), lambda: self.parent.delete_requests([key])) 218 run_hook('receive_list_menu', self.parent, menu, key) 219 menu.exec_(self.viewport().mapToGlobal(position))