trequest_list.py - electrum - Electrum Bitcoin wallet
HTML git clone https://git.parazyd.org/electrum
DIR Log
DIR Files
DIR Refs
DIR Submodules
---
trequest_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))