URI: 
       tutil.py - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
       tutil.py (38566B)
       ---
            1 import asyncio
            2 import os.path
            3 import time
            4 import sys
            5 import platform
            6 import queue
            7 import traceback
            8 import os
            9 import webbrowser
           10 from decimal import Decimal
           11 from functools import partial, lru_cache
           12 from typing import (NamedTuple, Callable, Optional, TYPE_CHECKING, Union, List, Dict, Any,
           13                     Sequence, Iterable)
           14 
           15 from PyQt5.QtGui import (QFont, QColor, QCursor, QPixmap, QStandardItem,
           16                          QPalette, QIcon, QFontMetrics, QShowEvent)
           17 from PyQt5.QtCore import (Qt, QPersistentModelIndex, QModelIndex, pyqtSignal,
           18                           QCoreApplication, QItemSelectionModel, QThread,
           19                           QSortFilterProxyModel, QSize, QLocale, QAbstractItemModel)
           20 from PyQt5.QtWidgets import (QPushButton, QLabel, QMessageBox, QHBoxLayout,
           21                              QAbstractItemView, QVBoxLayout, QLineEdit,
           22                              QStyle, QDialog, QGroupBox, QButtonGroup, QRadioButton,
           23                              QFileDialog, QWidget, QToolButton, QTreeView, QPlainTextEdit,
           24                              QHeaderView, QApplication, QToolTip, QTreeWidget, QStyledItemDelegate,
           25                              QMenu)
           26 
           27 from electrum.i18n import _, languages
           28 from electrum.util import FileImportFailed, FileExportFailed, make_aiohttp_session, resource_path
           29 from electrum.invoices import PR_UNPAID, PR_PAID, PR_EXPIRED, PR_INFLIGHT, PR_UNKNOWN, PR_FAILED, PR_ROUTING, PR_UNCONFIRMED
           30 
           31 if TYPE_CHECKING:
           32     from .main_window import ElectrumWindow
           33     from .installwizard import InstallWizard
           34     from electrum.simple_config import SimpleConfig
           35 
           36 
           37 if platform.system() == 'Windows':
           38     MONOSPACE_FONT = 'Lucida Console'
           39 elif platform.system() == 'Darwin':
           40     MONOSPACE_FONT = 'Monaco'
           41 else:
           42     MONOSPACE_FONT = 'monospace'
           43 
           44 
           45 dialogs = []
           46 
           47 pr_icons = {
           48     PR_UNKNOWN:"warning.png",
           49     PR_UNPAID:"unpaid.png",
           50     PR_PAID:"confirmed.png",
           51     PR_EXPIRED:"expired.png",
           52     PR_INFLIGHT:"unconfirmed.png",
           53     PR_FAILED:"warning.png",
           54     PR_ROUTING:"unconfirmed.png",
           55     PR_UNCONFIRMED:"unconfirmed.png",
           56 }
           57 
           58 
           59 # filter tx files in QFileDialog:
           60 TRANSACTION_FILE_EXTENSION_FILTER_ANY = "Transaction (*.txn *.psbt);;All files (*)"
           61 TRANSACTION_FILE_EXTENSION_FILTER_ONLY_PARTIAL_TX = "Partial Transaction (*.psbt)"
           62 TRANSACTION_FILE_EXTENSION_FILTER_ONLY_COMPLETE_TX = "Complete Transaction (*.txn)"
           63 TRANSACTION_FILE_EXTENSION_FILTER_SEPARATE = (f"{TRANSACTION_FILE_EXTENSION_FILTER_ONLY_PARTIAL_TX};;"
           64                                               f"{TRANSACTION_FILE_EXTENSION_FILTER_ONLY_COMPLETE_TX};;"
           65                                               f"All files (*)")
           66 
           67 
           68 class EnterButton(QPushButton):
           69     def __init__(self, text, func):
           70         QPushButton.__init__(self, text)
           71         self.func = func
           72         self.clicked.connect(func)
           73 
           74     def keyPressEvent(self, e):
           75         if e.key() in [ Qt.Key_Return, Qt.Key_Enter ]:
           76             self.func()
           77 
           78 
           79 class ThreadedButton(QPushButton):
           80     def __init__(self, text, task, on_success=None, on_error=None):
           81         QPushButton.__init__(self, text)
           82         self.task = task
           83         self.on_success = on_success
           84         self.on_error = on_error
           85         self.clicked.connect(self.run_task)
           86 
           87     def run_task(self):
           88         self.setEnabled(False)
           89         self.thread = TaskThread(self)
           90         self.thread.add(self.task, self.on_success, self.done, self.on_error)
           91 
           92     def done(self):
           93         self.setEnabled(True)
           94         self.thread.stop()
           95 
           96 
           97 class WWLabel(QLabel):
           98     def __init__ (self, text="", parent=None):
           99         QLabel.__init__(self, text, parent)
          100         self.setWordWrap(True)
          101         self.setTextInteractionFlags(Qt.TextSelectableByMouse)
          102 
          103 
          104 class HelpLabel(QLabel):
          105 
          106     def __init__(self, text, help_text):
          107         QLabel.__init__(self, text)
          108         self.help_text = help_text
          109         self.app = QCoreApplication.instance()
          110         self.font = QFont()
          111 
          112     def mouseReleaseEvent(self, x):
          113         custom_message_box(icon=QMessageBox.Information,
          114                            parent=self,
          115                            title=_('Help'),
          116                            text=self.help_text)
          117 
          118     def enterEvent(self, event):
          119         self.font.setUnderline(True)
          120         self.setFont(self.font)
          121         self.app.setOverrideCursor(QCursor(Qt.PointingHandCursor))
          122         return QLabel.enterEvent(self, event)
          123 
          124     def leaveEvent(self, event):
          125         self.font.setUnderline(False)
          126         self.setFont(self.font)
          127         self.app.setOverrideCursor(QCursor(Qt.ArrowCursor))
          128         return QLabel.leaveEvent(self, event)
          129 
          130 
          131 class HelpButton(QToolButton):
          132     def __init__(self, text):
          133         QToolButton.__init__(self)
          134         self.setText('?')
          135         self.help_text = text
          136         self.setFocusPolicy(Qt.NoFocus)
          137         self.setFixedWidth(round(2.2 * char_width_in_lineedit()))
          138         self.clicked.connect(self.onclick)
          139 
          140     def onclick(self):
          141         custom_message_box(icon=QMessageBox.Information,
          142                            parent=self,
          143                            title=_('Help'),
          144                            text=self.help_text,
          145                            rich_text=True)
          146 
          147 
          148 class InfoButton(QPushButton):
          149     def __init__(self, text):
          150         QPushButton.__init__(self, 'Info')
          151         self.help_text = text
          152         self.setFocusPolicy(Qt.NoFocus)
          153         self.setFixedWidth(6 * char_width_in_lineedit())
          154         self.clicked.connect(self.onclick)
          155 
          156     def onclick(self):
          157         custom_message_box(icon=QMessageBox.Information,
          158                            parent=self,
          159                            title=_('Info'),
          160                            text=self.help_text,
          161                            rich_text=True)
          162 
          163 
          164 class Buttons(QHBoxLayout):
          165     def __init__(self, *buttons):
          166         QHBoxLayout.__init__(self)
          167         self.addStretch(1)
          168         for b in buttons:
          169             if b is None:
          170                 continue
          171             self.addWidget(b)
          172 
          173 class CloseButton(QPushButton):
          174     def __init__(self, dialog):
          175         QPushButton.__init__(self, _("Close"))
          176         self.clicked.connect(dialog.close)
          177         self.setDefault(True)
          178 
          179 class CopyButton(QPushButton):
          180     def __init__(self, text_getter, app):
          181         QPushButton.__init__(self, _("Copy"))
          182         self.clicked.connect(lambda: app.clipboard().setText(text_getter()))
          183 
          184 class CopyCloseButton(QPushButton):
          185     def __init__(self, text_getter, app, dialog):
          186         QPushButton.__init__(self, _("Copy and Close"))
          187         self.clicked.connect(lambda: app.clipboard().setText(text_getter()))
          188         self.clicked.connect(dialog.close)
          189         self.setDefault(True)
          190 
          191 class OkButton(QPushButton):
          192     def __init__(self, dialog, label=None):
          193         QPushButton.__init__(self, label or _("OK"))
          194         self.clicked.connect(dialog.accept)
          195         self.setDefault(True)
          196 
          197 class CancelButton(QPushButton):
          198     def __init__(self, dialog, label=None):
          199         QPushButton.__init__(self, label or _("Cancel"))
          200         self.clicked.connect(dialog.reject)
          201 
          202 class MessageBoxMixin(object):
          203     def top_level_window_recurse(self, window=None, test_func=None):
          204         window = window or self
          205         classes = (WindowModalDialog, QMessageBox)
          206         if test_func is None:
          207             test_func = lambda x: True
          208         for n, child in enumerate(window.children()):
          209             # Test for visibility as old closed dialogs may not be GC-ed.
          210             # Only accept children that confirm to test_func.
          211             if isinstance(child, classes) and child.isVisible() \
          212                     and test_func(child):
          213                 return self.top_level_window_recurse(child, test_func=test_func)
          214         return window
          215 
          216     def top_level_window(self, test_func=None):
          217         return self.top_level_window_recurse(test_func)
          218 
          219     def question(self, msg, parent=None, title=None, icon=None, **kwargs) -> bool:
          220         Yes, No = QMessageBox.Yes, QMessageBox.No
          221         return Yes == self.msg_box(icon=icon or QMessageBox.Question,
          222                                    parent=parent,
          223                                    title=title or '',
          224                                    text=msg,
          225                                    buttons=Yes|No,
          226                                    defaultButton=No,
          227                                    **kwargs)
          228 
          229     def show_warning(self, msg, parent=None, title=None, **kwargs):
          230         return self.msg_box(QMessageBox.Warning, parent,
          231                             title or _('Warning'), msg, **kwargs)
          232 
          233     def show_error(self, msg, parent=None, **kwargs):
          234         return self.msg_box(QMessageBox.Warning, parent,
          235                             _('Error'), msg, **kwargs)
          236 
          237     def show_critical(self, msg, parent=None, title=None, **kwargs):
          238         return self.msg_box(QMessageBox.Critical, parent,
          239                             title or _('Critical Error'), msg, **kwargs)
          240 
          241     def show_message(self, msg, parent=None, title=None, **kwargs):
          242         return self.msg_box(QMessageBox.Information, parent,
          243                             title or _('Information'), msg, **kwargs)
          244 
          245     def msg_box(self, icon, parent, title, text, *, buttons=QMessageBox.Ok,
          246                 defaultButton=QMessageBox.NoButton, rich_text=False,
          247                 checkbox=None):
          248         parent = parent or self.top_level_window()
          249         return custom_message_box(icon=icon,
          250                                   parent=parent,
          251                                   title=title,
          252                                   text=text,
          253                                   buttons=buttons,
          254                                   defaultButton=defaultButton,
          255                                   rich_text=rich_text,
          256                                   checkbox=checkbox)
          257 
          258 
          259 def custom_message_box(*, icon, parent, title, text, buttons=QMessageBox.Ok,
          260                        defaultButton=QMessageBox.NoButton, rich_text=False,
          261                        checkbox=None):
          262     if type(icon) is QPixmap:
          263         d = QMessageBox(QMessageBox.Information, title, str(text), buttons, parent)
          264         d.setIconPixmap(icon)
          265     else:
          266         d = QMessageBox(icon, title, str(text), buttons, parent)
          267     d.setWindowModality(Qt.WindowModal)
          268     d.setDefaultButton(defaultButton)
          269     if rich_text:
          270         d.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.LinksAccessibleByMouse)
          271         # set AutoText instead of RichText
          272         # AutoText lets Qt figure out whether to render as rich text.
          273         # e.g. if text is actually plain text and uses "\n" newlines;
          274         #      and we set RichText here, newlines would be swallowed
          275         d.setTextFormat(Qt.AutoText)
          276     else:
          277         d.setTextInteractionFlags(Qt.TextSelectableByMouse)
          278         d.setTextFormat(Qt.PlainText)
          279     if checkbox is not None:
          280         d.setCheckBox(checkbox)
          281     return d.exec_()
          282 
          283 
          284 class WindowModalDialog(QDialog, MessageBoxMixin):
          285     '''Handy wrapper; window modal dialogs are better for our multi-window
          286     daemon model as other wallet windows can still be accessed.'''
          287     def __init__(self, parent, title=None):
          288         QDialog.__init__(self, parent)
          289         self.setWindowModality(Qt.WindowModal)
          290         if title:
          291             self.setWindowTitle(title)
          292 
          293 
          294 class WaitingDialog(WindowModalDialog):
          295     '''Shows a please wait dialog whilst running a task.  It is not
          296     necessary to maintain a reference to this dialog.'''
          297     def __init__(self, parent: QWidget, message: str, task, on_success=None, on_error=None):
          298         assert parent
          299         if isinstance(parent, MessageBoxMixin):
          300             parent = parent.top_level_window()
          301         WindowModalDialog.__init__(self, parent, _("Please wait"))
          302         self.message_label = QLabel(message)
          303         vbox = QVBoxLayout(self)
          304         vbox.addWidget(self.message_label)
          305         self.accepted.connect(self.on_accepted)
          306         self.show()
          307         self.thread = TaskThread(self)
          308         self.thread.finished.connect(self.deleteLater)  # see #3956
          309         self.thread.add(task, on_success, self.accept, on_error)
          310 
          311     def wait(self):
          312         self.thread.wait()
          313 
          314     def on_accepted(self):
          315         self.thread.stop()
          316 
          317     def update(self, msg):
          318         print(msg)
          319         self.message_label.setText(msg)
          320 
          321 
          322 class BlockingWaitingDialog(WindowModalDialog):
          323     """Shows a waiting dialog whilst running a task.
          324     Should be called from the GUI thread. The GUI thread will be blocked while
          325     the task is running; the point of the dialog is to provide feedback
          326     to the user regarding what is going on.
          327     """
          328     def __init__(self, parent: QWidget, message: str, task: Callable[[], Any]):
          329         assert parent
          330         if isinstance(parent, MessageBoxMixin):
          331             parent = parent.top_level_window()
          332         WindowModalDialog.__init__(self, parent, _("Please wait"))
          333         self.message_label = QLabel(message)
          334         vbox = QVBoxLayout(self)
          335         vbox.addWidget(self.message_label)
          336         # show popup
          337         self.show()
          338         # refresh GUI; needed for popup to appear and for message_label to get drawn
          339         QCoreApplication.processEvents()
          340         QCoreApplication.processEvents()
          341         # block and run given task
          342         task()
          343         # close popup
          344         self.accept()
          345 
          346 
          347 def line_dialog(parent, title, label, ok_label, default=None):
          348     dialog = WindowModalDialog(parent, title)
          349     dialog.setMinimumWidth(500)
          350     l = QVBoxLayout()
          351     dialog.setLayout(l)
          352     l.addWidget(QLabel(label))
          353     txt = QLineEdit()
          354     if default:
          355         txt.setText(default)
          356     l.addWidget(txt)
          357     l.addLayout(Buttons(CancelButton(dialog), OkButton(dialog, ok_label)))
          358     if dialog.exec_():
          359         return txt.text()
          360 
          361 def text_dialog(
          362         *,
          363         parent,
          364         title,
          365         header_layout,
          366         ok_label,
          367         default=None,
          368         allow_multi=False,
          369         config: 'SimpleConfig',
          370 ):
          371     from .qrtextedit import ScanQRTextEdit
          372     dialog = WindowModalDialog(parent, title)
          373     dialog.setMinimumWidth(600)
          374     l = QVBoxLayout()
          375     dialog.setLayout(l)
          376     if isinstance(header_layout, str):
          377         l.addWidget(QLabel(header_layout))
          378     else:
          379         l.addLayout(header_layout)
          380     txt = ScanQRTextEdit(allow_multi=allow_multi, config=config)
          381     if default:
          382         txt.setText(default)
          383     l.addWidget(txt)
          384     l.addLayout(Buttons(CancelButton(dialog), OkButton(dialog, ok_label)))
          385     if dialog.exec_():
          386         return txt.toPlainText()
          387 
          388 class ChoicesLayout(object):
          389     def __init__(self, msg, choices, on_clicked=None, checked_index=0):
          390         vbox = QVBoxLayout()
          391         if len(msg) > 50:
          392             vbox.addWidget(WWLabel(msg))
          393             msg = ""
          394         gb2 = QGroupBox(msg)
          395         vbox.addWidget(gb2)
          396 
          397         vbox2 = QVBoxLayout()
          398         gb2.setLayout(vbox2)
          399 
          400         self.group = group = QButtonGroup()
          401         for i,c in enumerate(choices):
          402             button = QRadioButton(gb2)
          403             button.setText(c)
          404             vbox2.addWidget(button)
          405             group.addButton(button)
          406             group.setId(button, i)
          407             if i==checked_index:
          408                 button.setChecked(True)
          409 
          410         if on_clicked:
          411             group.buttonClicked.connect(partial(on_clicked, self))
          412 
          413         self.vbox = vbox
          414 
          415     def layout(self):
          416         return self.vbox
          417 
          418     def selected_index(self):
          419         return self.group.checkedId()
          420 
          421 def address_field(addresses):
          422     hbox = QHBoxLayout()
          423     address_e = QLineEdit()
          424     if addresses and len(addresses) > 0:
          425         address_e.setText(addresses[0])
          426     else:
          427         addresses = []
          428     def func():
          429         try:
          430             i = addresses.index(str(address_e.text())) + 1
          431             i = i % len(addresses)
          432             address_e.setText(addresses[i])
          433         except ValueError:
          434             # the user might have changed address_e to an
          435             # address not in the wallet (or to something that isn't an address)
          436             if addresses and len(addresses) > 0:
          437                 address_e.setText(addresses[0])
          438     button = QPushButton(_('Address'))
          439     button.clicked.connect(func)
          440     hbox.addWidget(button)
          441     hbox.addWidget(address_e)
          442     return hbox, address_e
          443 
          444 
          445 def filename_field(parent, config, defaultname, select_msg):
          446 
          447     vbox = QVBoxLayout()
          448     vbox.addWidget(QLabel(_("Format")))
          449     gb = QGroupBox("format", parent)
          450     b1 = QRadioButton(gb)
          451     b1.setText(_("CSV"))
          452     b1.setChecked(True)
          453     b2 = QRadioButton(gb)
          454     b2.setText(_("json"))
          455     vbox.addWidget(b1)
          456     vbox.addWidget(b2)
          457 
          458     hbox = QHBoxLayout()
          459 
          460     directory = config.get('io_dir', os.path.expanduser('~'))
          461     path = os.path.join( directory, defaultname )
          462     filename_e = QLineEdit()
          463     filename_e.setText(path)
          464 
          465     def func():
          466         text = filename_e.text()
          467         _filter = "*.csv" if defaultname.endswith(".csv") else "*.json" if defaultname.endswith(".json") else None
          468         p = getSaveFileName(
          469             parent=None,
          470             title=select_msg,
          471             filename=text,
          472             filter=_filter,
          473             config=config,
          474         )
          475         if p:
          476             filename_e.setText(p)
          477 
          478     button = QPushButton(_('File'))
          479     button.clicked.connect(func)
          480     hbox.addWidget(button)
          481     hbox.addWidget(filename_e)
          482     vbox.addLayout(hbox)
          483 
          484     def set_csv(v):
          485         text = filename_e.text()
          486         text = text.replace(".json",".csv") if v else text.replace(".csv",".json")
          487         filename_e.setText(text)
          488 
          489     b1.clicked.connect(lambda: set_csv(True))
          490     b2.clicked.connect(lambda: set_csv(False))
          491 
          492     return vbox, filename_e, b1
          493 
          494 
          495 class ElectrumItemDelegate(QStyledItemDelegate):
          496     def __init__(self, tv: 'MyTreeView'):
          497         super().__init__(tv)
          498         self.tv = tv
          499         self.opened = None
          500         def on_closeEditor(editor: QLineEdit, hint):
          501             self.opened = None
          502             self.tv.is_editor_open = False
          503             if self.tv._pending_update:
          504                 self.tv.update()
          505         def on_commitData(editor: QLineEdit):
          506             new_text = editor.text()
          507             idx = QModelIndex(self.opened)
          508             row, col = idx.row(), idx.column()
          509             _prior_text, user_role = self.tv.get_text_and_userrole_from_coordinate(row, col)
          510             # check that we didn't forget to set UserRole on an editable field
          511             assert user_role is not None, (row, col)
          512             self.tv.on_edited(idx, user_role, new_text)
          513         self.closeEditor.connect(on_closeEditor)
          514         self.commitData.connect(on_commitData)
          515 
          516     def createEditor(self, parent, option, idx):
          517         self.opened = QPersistentModelIndex(idx)
          518         self.tv.is_editor_open = True
          519         return super().createEditor(parent, option, idx)
          520 
          521 
          522 class MyTreeView(QTreeView):
          523     ROLE_CLIPBOARD_DATA = Qt.UserRole + 100
          524 
          525     filter_columns: Iterable[int]
          526 
          527     def __init__(self, parent: 'ElectrumWindow', create_menu, *,
          528                  stretch_column=None, editable_columns=None):
          529         super().__init__(parent)
          530         self.parent = parent
          531         self.config = self.parent.config
          532         self.stretch_column = stretch_column
          533         self.setContextMenuPolicy(Qt.CustomContextMenu)
          534         self.customContextMenuRequested.connect(create_menu)
          535         self.setUniformRowHeights(True)
          536 
          537         # Control which columns are editable
          538         if editable_columns is not None:
          539             editable_columns = set(editable_columns)
          540         elif stretch_column is not None:
          541             editable_columns = {stretch_column}
          542         else:
          543             editable_columns = {}
          544         self.editable_columns = editable_columns
          545         self.setItemDelegate(ElectrumItemDelegate(self))
          546         self.current_filter = ""
          547         self.is_editor_open = False
          548 
          549         self.setRootIsDecorated(False)  # remove left margin
          550         self.toolbar_shown = False
          551 
          552         # When figuring out the size of columns, Qt by default looks at
          553         # the first 1000 rows (at least if resize mode is QHeaderView.ResizeToContents).
          554         # This would be REALLY SLOW, and it's not perfect anyway.
          555         # So to speed the UI up considerably, set it to
          556         # only look at as many rows as currently visible.
          557         self.header().setResizeContentsPrecision(0)
          558 
          559         self._pending_update = False
          560         self._forced_update = False
          561 
          562     def set_editability(self, items):
          563         for idx, i in enumerate(items):
          564             i.setEditable(idx in self.editable_columns)
          565 
          566     def selected_in_column(self, column: int):
          567         items = self.selectionModel().selectedIndexes()
          568         return list(x for x in items if x.column() == column)
          569 
          570     def current_item_user_role(self, col) -> Any:
          571         idx = self.selectionModel().currentIndex()
          572         idx = idx.sibling(idx.row(), col)
          573         item = self.item_from_index(idx)
          574         if item:
          575             return item.data(Qt.UserRole)
          576 
          577     def item_from_index(self, idx: QModelIndex) -> Optional[QStandardItem]:
          578         model = self.model()
          579         if isinstance(model, QSortFilterProxyModel):
          580             idx = model.mapToSource(idx)
          581             return model.sourceModel().itemFromIndex(idx)
          582         else:
          583             return model.itemFromIndex(idx)
          584 
          585     def original_model(self) -> QAbstractItemModel:
          586         model = self.model()
          587         if isinstance(model, QSortFilterProxyModel):
          588             return model.sourceModel()
          589         else:
          590             return model
          591 
          592     def set_current_idx(self, set_current: QPersistentModelIndex):
          593         if set_current:
          594             assert isinstance(set_current, QPersistentModelIndex)
          595             assert set_current.isValid()
          596             self.selectionModel().select(QModelIndex(set_current), QItemSelectionModel.SelectCurrent)
          597 
          598     def update_headers(self, headers: Union[List[str], Dict[int, str]]):
          599         # headers is either a list of column names, or a dict: (col_idx->col_name)
          600         if not isinstance(headers, dict):  # convert to dict
          601             headers = dict(enumerate(headers))
          602         col_names = [headers[col_idx] for col_idx in sorted(headers.keys())]
          603         self.original_model().setHorizontalHeaderLabels(col_names)
          604         self.header().setStretchLastSection(False)
          605         for col_idx in headers:
          606             sm = QHeaderView.Stretch if col_idx == self.stretch_column else QHeaderView.ResizeToContents
          607             self.header().setSectionResizeMode(col_idx, sm)
          608 
          609     def keyPressEvent(self, event):
          610         if self.itemDelegate().opened:
          611             return
          612         if event.key() in [ Qt.Key_F2, Qt.Key_Return, Qt.Key_Enter ]:
          613             self.on_activated(self.selectionModel().currentIndex())
          614             return
          615         super().keyPressEvent(event)
          616 
          617     def on_activated(self, idx):
          618         # on 'enter' we show the menu
          619         pt = self.visualRect(idx).bottomLeft()
          620         pt.setX(50)
          621         self.customContextMenuRequested.emit(pt)
          622 
          623     def edit(self, idx, trigger=QAbstractItemView.AllEditTriggers, event=None):
          624         """
          625         this is to prevent:
          626            edit: editing failed
          627         from inside qt
          628         """
          629         return super().edit(idx, trigger, event)
          630 
          631     def on_edited(self, idx: QModelIndex, user_role, text):
          632         self.parent.wallet.set_label(user_role, text)
          633         self.parent.history_model.refresh('on_edited in MyTreeView')
          634         self.parent.utxo_list.update()
          635         self.parent.update_completions()
          636 
          637     def should_hide(self, row):
          638         """
          639         row_num is for self.model(). So if there is a proxy, it is the row number
          640         in that!
          641         """
          642         return False
          643 
          644     def get_text_and_userrole_from_coordinate(self, row_num, column):
          645         idx = self.model().index(row_num, column)
          646         item = self.item_from_index(idx)
          647         user_role = item.data(Qt.UserRole)
          648         return item.text(), user_role
          649 
          650     def hide_row(self, row_num):
          651         """
          652         row_num is for self.model(). So if there is a proxy, it is the row number
          653         in that!
          654         """
          655         should_hide = self.should_hide(row_num)
          656         if not self.current_filter and should_hide is None:
          657             # no filters at all, neither date nor search
          658             self.setRowHidden(row_num, QModelIndex(), False)
          659             return
          660         for column in self.filter_columns:
          661             txt, _ = self.get_text_and_userrole_from_coordinate(row_num, column)
          662             txt = txt.lower()
          663             if self.current_filter in txt:
          664                 # the filter matched, but the date filter might apply
          665                 self.setRowHidden(row_num, QModelIndex(), bool(should_hide))
          666                 break
          667         else:
          668             # we did not find the filter in any columns, hide the item
          669             self.setRowHidden(row_num, QModelIndex(), True)
          670 
          671     def filter(self, p=None):
          672         if p is not None:
          673             p = p.lower()
          674             self.current_filter = p
          675         self.hide_rows()
          676 
          677     def hide_rows(self):
          678         for row in range(self.model().rowCount()):
          679             self.hide_row(row)
          680 
          681     def create_toolbar(self, config=None):
          682         hbox = QHBoxLayout()
          683         buttons = self.get_toolbar_buttons()
          684         for b in buttons:
          685             b.setVisible(False)
          686             hbox.addWidget(b)
          687         hide_button = QPushButton('x')
          688         hide_button.setVisible(False)
          689         hide_button.pressed.connect(lambda: self.show_toolbar(False, config))
          690         self.toolbar_buttons = buttons + (hide_button,)
          691         hbox.addStretch()
          692         hbox.addWidget(hide_button)
          693         return hbox
          694 
          695     def save_toolbar_state(self, state, config):
          696         pass  # implemented in subclasses
          697 
          698     def show_toolbar(self, state, config=None):
          699         if state == self.toolbar_shown:
          700             return
          701         self.toolbar_shown = state
          702         if config:
          703             self.save_toolbar_state(state, config)
          704         for b in self.toolbar_buttons:
          705             b.setVisible(state)
          706         if not state:
          707             self.on_hide_toolbar()
          708 
          709     def toggle_toolbar(self, config=None):
          710         self.show_toolbar(not self.toolbar_shown, config)
          711 
          712     def add_copy_menu(self, menu: QMenu, idx) -> QMenu:
          713         cc = menu.addMenu(_("Copy"))
          714         for column in self.Columns:
          715             column_title = self.original_model().horizontalHeaderItem(column).text()
          716             item_col = self.item_from_index(idx.sibling(idx.row(), column))
          717             clipboard_data = item_col.data(self.ROLE_CLIPBOARD_DATA)
          718             if clipboard_data is None:
          719                 clipboard_data = item_col.text().strip()
          720             cc.addAction(column_title,
          721                          lambda text=clipboard_data, title=column_title:
          722                          self.place_text_on_clipboard(text, title=title))
          723         return cc
          724 
          725     def place_text_on_clipboard(self, text: str, *, title: str = None) -> None:
          726         self.parent.do_copy(text, title=title)
          727 
          728     def showEvent(self, e: 'QShowEvent'):
          729         super().showEvent(e)
          730         if e.isAccepted() and self._pending_update:
          731             self._forced_update = True
          732             self.update()
          733             self._forced_update = False
          734 
          735     def maybe_defer_update(self) -> bool:
          736         """Returns whether we should defer an update/refresh."""
          737         defer = (not self._forced_update
          738                  and (not self.isVisible() or self.is_editor_open))
          739         # side-effect: if we decide to defer update, the state will become stale:
          740         self._pending_update = defer
          741         return defer
          742 
          743 
          744 class MySortModel(QSortFilterProxyModel):
          745     def __init__(self, parent, *, sort_role):
          746         super().__init__(parent)
          747         self._sort_role = sort_role
          748 
          749     def lessThan(self, source_left: QModelIndex, source_right: QModelIndex):
          750         item1 = self.sourceModel().itemFromIndex(source_left)
          751         item2 = self.sourceModel().itemFromIndex(source_right)
          752         data1 = item1.data(self._sort_role)
          753         data2 = item2.data(self._sort_role)
          754         if data1 is not None and data2 is not None:
          755             return data1 < data2
          756         v1 = item1.text()
          757         v2 = item2.text()
          758         try:
          759             return Decimal(v1) < Decimal(v2)
          760         except:
          761             return v1 < v2
          762 
          763 
          764 class ButtonsWidget(QWidget):
          765 
          766     def __init__(self):
          767         super(QWidget, self).__init__()
          768         self.buttons = []  # type: List[QToolButton]
          769 
          770     def resizeButtons(self):
          771         frameWidth = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth)
          772         x = self.rect().right() - frameWidth - 10
          773         y = self.rect().bottom() - frameWidth
          774         for button in self.buttons:
          775             sz = button.sizeHint()
          776             x -= sz.width()
          777             button.move(x, y - sz.height())
          778 
          779     def addButton(self, icon_name, on_click, tooltip):
          780         button = QToolButton(self)
          781         button.setIcon(read_QIcon(icon_name))
          782         button.setIconSize(QSize(25,25))
          783         button.setCursor(QCursor(Qt.PointingHandCursor))
          784         button.setStyleSheet("QToolButton { border: none; hover {border: 1px} pressed {border: 1px} padding: 0px; }")
          785         button.setVisible(True)
          786         button.setToolTip(tooltip)
          787         button.clicked.connect(on_click)
          788         self.buttons.append(button)
          789         return button
          790 
          791     def addCopyButton(self, app):
          792         self.app = app
          793         self.addButton("copy.png", self.on_copy, _("Copy to clipboard"))
          794 
          795     def on_copy(self):
          796         self.app.clipboard().setText(self.text())
          797         QToolTip.showText(QCursor.pos(), _("Text copied to clipboard"), self)
          798 
          799     def addPasteButton(self, app):
          800         self.app = app
          801         self.addButton("copy.png", self.on_paste, _("Paste from clipboard"))
          802 
          803     def on_paste(self):
          804         self.setText(self.app.clipboard().text())
          805 
          806 
          807 class ButtonsLineEdit(QLineEdit, ButtonsWidget):
          808     def __init__(self, text=None):
          809         QLineEdit.__init__(self, text)
          810         self.buttons = []
          811 
          812     def resizeEvent(self, e):
          813         o = QLineEdit.resizeEvent(self, e)
          814         self.resizeButtons()
          815         return o
          816 
          817 class ButtonsTextEdit(QPlainTextEdit, ButtonsWidget):
          818     def __init__(self, text=None):
          819         QPlainTextEdit.__init__(self, text)
          820         self.setText = self.setPlainText
          821         self.text = self.toPlainText
          822         self.buttons = []
          823 
          824     def resizeEvent(self, e):
          825         o = QPlainTextEdit.resizeEvent(self, e)
          826         self.resizeButtons()
          827         return o
          828 
          829 
          830 class PasswordLineEdit(QLineEdit):
          831     def __init__(self, *args, **kwargs):
          832         QLineEdit.__init__(self, *args, **kwargs)
          833         self.setEchoMode(QLineEdit.Password)
          834 
          835     def clear(self):
          836         # Try to actually overwrite the memory.
          837         # This is really just a best-effort thing...
          838         self.setText(len(self.text()) * " ")
          839         super().clear()
          840 
          841 
          842 class TaskThread(QThread):
          843     '''Thread that runs background tasks.  Callbacks are guaranteed
          844     to happen in the context of its parent.'''
          845 
          846     class Task(NamedTuple):
          847         task: Callable
          848         cb_success: Optional[Callable]
          849         cb_done: Optional[Callable]
          850         cb_error: Optional[Callable]
          851 
          852     doneSig = pyqtSignal(object, object, object)
          853 
          854     def __init__(self, parent, on_error=None):
          855         super(TaskThread, self).__init__(parent)
          856         self.on_error = on_error
          857         self.tasks = queue.Queue()
          858         self.doneSig.connect(self.on_done)
          859         self.start()
          860 
          861     def add(self, task, on_success=None, on_done=None, on_error=None):
          862         on_error = on_error or self.on_error
          863         self.tasks.put(TaskThread.Task(task, on_success, on_done, on_error))
          864 
          865     def run(self):
          866         while True:
          867             task = self.tasks.get()  # type: TaskThread.Task
          868             if not task:
          869                 break
          870             try:
          871                 result = task.task()
          872                 self.doneSig.emit(result, task.cb_done, task.cb_success)
          873             except BaseException:
          874                 self.doneSig.emit(sys.exc_info(), task.cb_done, task.cb_error)
          875 
          876     def on_done(self, result, cb_done, cb_result):
          877         # This runs in the parent's thread.
          878         if cb_done:
          879             cb_done()
          880         if cb_result:
          881             cb_result(result)
          882 
          883     def stop(self):
          884         self.tasks.put(None)
          885 
          886 
          887 class ColorSchemeItem:
          888     def __init__(self, fg_color, bg_color):
          889         self.colors = (fg_color, bg_color)
          890 
          891     def _get_color(self, background):
          892         return self.colors[(int(background) + int(ColorScheme.dark_scheme)) % 2]
          893 
          894     def as_stylesheet(self, background=False):
          895         css_prefix = "background-" if background else ""
          896         color = self._get_color(background)
          897         return "QWidget {{ {}color:{}; }}".format(css_prefix, color)
          898 
          899     def as_color(self, background=False):
          900         color = self._get_color(background)
          901         return QColor(color)
          902 
          903 
          904 class ColorScheme:
          905     dark_scheme = False
          906 
          907     GREEN = ColorSchemeItem("#117c11", "#8af296")
          908     YELLOW = ColorSchemeItem("#897b2a", "#ffff00")
          909     RED = ColorSchemeItem("#7c1111", "#f18c8c")
          910     BLUE = ColorSchemeItem("#123b7c", "#8cb3f2")
          911     DEFAULT = ColorSchemeItem("black", "white")
          912     GRAY = ColorSchemeItem("gray", "gray")
          913 
          914     @staticmethod
          915     def has_dark_background(widget):
          916         brightness = sum(widget.palette().color(QPalette.Background).getRgb()[0:3])
          917         return brightness < (255*3/2)
          918 
          919     @staticmethod
          920     def update_from_widget(widget, force_dark=False):
          921         if force_dark or ColorScheme.has_dark_background(widget):
          922             ColorScheme.dark_scheme = True
          923 
          924 
          925 class AcceptFileDragDrop:
          926     def __init__(self, file_type=""):
          927         assert isinstance(self, QWidget)
          928         self.setAcceptDrops(True)
          929         self.file_type = file_type
          930 
          931     def validateEvent(self, event):
          932         if not event.mimeData().hasUrls():
          933             event.ignore()
          934             return False
          935         for url in event.mimeData().urls():
          936             if not url.toLocalFile().endswith(self.file_type):
          937                 event.ignore()
          938                 return False
          939         event.accept()
          940         return True
          941 
          942     def dragEnterEvent(self, event):
          943         self.validateEvent(event)
          944 
          945     def dragMoveEvent(self, event):
          946         if self.validateEvent(event):
          947             event.setDropAction(Qt.CopyAction)
          948 
          949     def dropEvent(self, event):
          950         if self.validateEvent(event):
          951             for url in event.mimeData().urls():
          952                 self.onFileAdded(url.toLocalFile())
          953 
          954     def onFileAdded(self, fn):
          955         raise NotImplementedError()
          956 
          957 
          958 def import_meta_gui(electrum_window: 'ElectrumWindow', title, importer, on_success):
          959     filter_ = "JSON (*.json);;All files (*)"
          960     filename = getOpenFileName(
          961         parent=electrum_window,
          962         title=_("Open {} file").format(title),
          963         filter=filter_,
          964         config=electrum_window.config,
          965     )
          966     if not filename:
          967         return
          968     try:
          969         importer(filename)
          970     except FileImportFailed as e:
          971         electrum_window.show_critical(str(e))
          972     else:
          973         electrum_window.show_message(_("Your {} were successfully imported").format(title))
          974         on_success()
          975 
          976 
          977 def export_meta_gui(electrum_window: 'ElectrumWindow', title, exporter):
          978     filter_ = "JSON (*.json);;All files (*)"
          979     filename = getSaveFileName(
          980         parent=electrum_window,
          981         title=_("Select file to save your {}").format(title),
          982         filename='electrum_{}.json'.format(title),
          983         filter=filter_,
          984         config=electrum_window.config,
          985     )
          986     if not filename:
          987         return
          988     try:
          989         exporter(filename)
          990     except FileExportFailed as e:
          991         electrum_window.show_critical(str(e))
          992     else:
          993         electrum_window.show_message(_("Your {0} were exported to '{1}'")
          994                                      .format(title, str(filename)))
          995 
          996 
          997 def getOpenFileName(*, parent, title, filter="", config: 'SimpleConfig') -> Optional[str]:
          998     """Custom wrapper for getOpenFileName that remembers the path selected by the user."""
          999     directory = config.get('io_dir', os.path.expanduser('~'))
         1000     fileName, __ = QFileDialog.getOpenFileName(parent, title, directory, filter)
         1001     if fileName and directory != os.path.dirname(fileName):
         1002         config.set_key('io_dir', os.path.dirname(fileName), True)
         1003     return fileName
         1004 
         1005 
         1006 def getSaveFileName(
         1007         *,
         1008         parent,
         1009         title,
         1010         filename,
         1011         filter="",
         1012         default_extension: str = None,
         1013         default_filter: str = None,
         1014         config: 'SimpleConfig',
         1015 ) -> Optional[str]:
         1016     """Custom wrapper for getSaveFileName that remembers the path selected by the user."""
         1017     directory = config.get('io_dir', os.path.expanduser('~'))
         1018     path = os.path.join(directory, filename)
         1019 
         1020     file_dialog = QFileDialog(parent, title, path, filter)
         1021     file_dialog.setAcceptMode(QFileDialog.AcceptSave)
         1022     if default_extension:
         1023         # note: on MacOS, the selected filter's first extension seems to have priority over this...
         1024         file_dialog.setDefaultSuffix(default_extension)
         1025     if default_filter:
         1026         assert default_filter in filter, f"default_filter={default_filter!r} does not appear in filter={filter!r}"
         1027         file_dialog.selectNameFilter(default_filter)
         1028     if file_dialog.exec() != QDialog.Accepted:
         1029         return None
         1030 
         1031     selected_path = file_dialog.selectedFiles()[0]
         1032     if selected_path and directory != os.path.dirname(selected_path):
         1033         config.set_key('io_dir', os.path.dirname(selected_path), True)
         1034     return selected_path
         1035 
         1036 
         1037 def icon_path(icon_basename):
         1038     return resource_path('gui', 'icons', icon_basename)
         1039 
         1040 
         1041 @lru_cache(maxsize=1000)
         1042 def read_QIcon(icon_basename):
         1043     return QIcon(icon_path(icon_basename))
         1044 
         1045 class IconLabel(QWidget):
         1046     IconSize = QSize(16, 16)
         1047     HorizontalSpacing = 2
         1048     def __init__(self, *, text='', final_stretch=True):
         1049         super(QWidget, self).__init__()
         1050         layout = QHBoxLayout()
         1051         layout.setContentsMargins(0, 0, 0, 0)
         1052         self.setLayout(layout)
         1053         self.icon = QLabel()
         1054         self.label = QLabel(text)
         1055         layout.addWidget(self.label)
         1056         layout.addSpacing(self.HorizontalSpacing)
         1057         layout.addWidget(self.icon)
         1058         if final_stretch:
         1059             layout.addStretch()
         1060     def setText(self, text):
         1061         self.label.setText(text)
         1062     def setIcon(self, icon):
         1063         self.icon.setPixmap(icon.pixmap(self.IconSize))
         1064         self.icon.repaint()  # macOS hack for #6269
         1065 
         1066 def get_default_language():
         1067     name = QLocale.system().name()
         1068     return name if name in languages else 'en_UK'
         1069 
         1070 
         1071 def char_width_in_lineedit() -> int:
         1072     char_width = QFontMetrics(QLineEdit().font()).averageCharWidth()
         1073     # 'averageCharWidth' seems to underestimate on Windows, hence 'max()'
         1074     return max(9, char_width)
         1075 
         1076 
         1077 def webopen(url: str):
         1078     if sys.platform == 'linux' and os.environ.get('APPIMAGE'):
         1079         # When on Linux webbrowser.open can fail in AppImage because it can't find the correct libdbus.
         1080         # We just fork the process and unset LD_LIBRARY_PATH before opening the URL.
         1081         # See #5425
         1082         if os.fork() == 0:
         1083             del os.environ['LD_LIBRARY_PATH']
         1084             webbrowser.open(url)
         1085             os._exit(0)
         1086     else:
         1087         webbrowser.open(url)
         1088 
         1089 
         1090 if __name__ == "__main__":
         1091     app = QApplication([])
         1092     t = WaitingDialog(None, 'testing ...', lambda: [time.sleep(1)], lambda x: QMessageBox.information(None, 'done', "done"))
         1093     t.start()
         1094     app.exec_()