electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

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