add gui/qt changes from electrum 3.1.3
- Add electrum 3.1.3 changes to gui/qt. - Get new electrum 3.1.3 gui/qt/completion_text_edit.py. - Get new electrum 3.1.3 gui/qt/exception_window.py.
This commit is contained in:
parent
604ec07fa5
commit
4df22e559c
|
@ -25,6 +25,7 @@
|
|||
|
||||
import signal
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
|
||||
try:
|
||||
|
@ -43,7 +44,8 @@ from electrum_zcash import WalletStorage
|
|||
# from electrum_zcash.synchronizer import Synchronizer
|
||||
# from electrum_zcash.verifier import SPV
|
||||
# from electrum_zcash.util import DebugMem
|
||||
from electrum_zcash.util import UserCancelled, print_error
|
||||
from electrum_zcash.util import (UserCancelled, print_error,
|
||||
WalletFileException, BitcoinException)
|
||||
# from electrum_zcash.wallet import Abstract_Wallet
|
||||
|
||||
from .installwizard import InstallWizard, GoBack
|
||||
|
@ -92,6 +94,10 @@ class ElectrumGui:
|
|||
#network.add_jobs([DebugMem([Abstract_Wallet, SPV, Synchronizer,
|
||||
# ElectrumWindow], interval=5)])
|
||||
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads)
|
||||
if hasattr(QtCore.Qt, "AA_ShareOpenGLContexts"):
|
||||
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts)
|
||||
if hasattr(QGuiApplication, 'setDesktopFileName'):
|
||||
QGuiApplication.setDesktopFileName('electrum-zcash.desktop')
|
||||
self.config = config
|
||||
self.daemon = daemon
|
||||
self.plugins = plugins
|
||||
|
@ -180,33 +186,51 @@ class ElectrumGui:
|
|||
|
||||
def start_new_window(self, path, uri):
|
||||
'''Raises the window for the wallet if it is open. Otherwise
|
||||
opens the wallet and creates a new window for it.'''
|
||||
for w in self.windows:
|
||||
if w.wallet.storage.path == path:
|
||||
w.bring_to_top()
|
||||
break
|
||||
else:
|
||||
opens the wallet and creates a new window for it'''
|
||||
try:
|
||||
wallet = self.daemon.load_wallet(path, None)
|
||||
except BaseException as e:
|
||||
traceback.print_exc(file=sys.stdout)
|
||||
d = QMessageBox(QMessageBox.Warning, _('Error'),
|
||||
_('Cannot load wallet') + ' (1):\n' + str(e))
|
||||
d.exec_()
|
||||
return
|
||||
if not wallet:
|
||||
storage = WalletStorage(path, manual_upgrades=True)
|
||||
wizard = InstallWizard(self.config, self.app, self.plugins, storage)
|
||||
try:
|
||||
wallet = self.daemon.load_wallet(path, None)
|
||||
except BaseException as e:
|
||||
d = QMessageBox(QMessageBox.Warning, _('Error'), 'Cannot load wallet:\n' + str(e))
|
||||
wallet = wizard.run_and_get_wallet(self.daemon.get_wallet)
|
||||
except UserCancelled:
|
||||
pass
|
||||
except GoBack as e:
|
||||
print_error('[start_new_window] Exception caught (GoBack)', e)
|
||||
except (WalletFileException, BitcoinException) as e:
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
d = QMessageBox(QMessageBox.Warning, _('Error'),
|
||||
_('Cannot load wallet') + ' (2):\n' + str(e))
|
||||
d.exec_()
|
||||
return
|
||||
if not wallet:
|
||||
storage = WalletStorage(path, manual_upgrades=True)
|
||||
wizard = InstallWizard(self.config, self.app, self.plugins, storage)
|
||||
try:
|
||||
wallet = wizard.run_and_get_wallet()
|
||||
except UserCancelled:
|
||||
pass
|
||||
except GoBack as e:
|
||||
print_error('[start_new_window] Exception caught (GoBack)', e)
|
||||
finally:
|
||||
wizard.terminate()
|
||||
if not wallet:
|
||||
return
|
||||
if not wallet:
|
||||
return
|
||||
|
||||
if not self.daemon.get_wallet(wallet.storage.path):
|
||||
# wallet was not in memory
|
||||
wallet.start_threads(self.daemon.network)
|
||||
self.daemon.add_wallet(wallet)
|
||||
try:
|
||||
for w in self.windows:
|
||||
if w.wallet.storage.path == wallet.storage.path:
|
||||
w.bring_to_top()
|
||||
return
|
||||
w = self.create_window_for_wallet(wallet)
|
||||
except BaseException as e:
|
||||
traceback.print_exc(file=sys.stdout)
|
||||
d = QMessageBox(QMessageBox.Warning, _('Error'),
|
||||
_('Cannot create window for wallet') + ':\n' + str(e))
|
||||
d.exec_()
|
||||
return
|
||||
if uri:
|
||||
w.pay_to_URI(uri)
|
||||
w.bring_to_top()
|
||||
|
@ -239,8 +263,7 @@ class ElectrumGui:
|
|||
return
|
||||
except GoBack:
|
||||
return
|
||||
except:
|
||||
import traceback
|
||||
except BaseException as e:
|
||||
traceback.print_exc(file=sys.stdout)
|
||||
return
|
||||
self.timer.start()
|
||||
|
|
|
@ -24,47 +24,56 @@
|
|||
# SOFTWARE.
|
||||
import webbrowser
|
||||
|
||||
from .util import *
|
||||
from electrum_zcash.i18n import _
|
||||
from electrum_zcash.util import block_explorer_URL
|
||||
from electrum_zcash.plugins import run_hook
|
||||
from electrum_zcash.bitcoin import is_address
|
||||
|
||||
from .util import *
|
||||
|
||||
|
||||
class AddressList(MyTreeWidget):
|
||||
filter_columns = [0, 1, 2] # Address, Label, Balance
|
||||
filter_columns = [0, 1, 2, 3] # Type, Address, Label, Balance
|
||||
|
||||
def __init__(self, parent=None):
|
||||
MyTreeWidget.__init__(self, parent, self.create_menu, [], 1)
|
||||
MyTreeWidget.__init__(self, parent, self.create_menu, [], 2)
|
||||
self.refresh_headers()
|
||||
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
||||
self.show_change = False
|
||||
self.setSortingEnabled(True)
|
||||
self.show_change = 0
|
||||
self.show_used = 0
|
||||
self.change_button = QComboBox(self)
|
||||
self.change_button.currentIndexChanged.connect(self.toggle_change)
|
||||
for t in [_('Receiving'), _('Change')]:
|
||||
for t in [_('All'), _('Receiving'), _('Change')]:
|
||||
self.change_button.addItem(t)
|
||||
self.used_button = QComboBox(self)
|
||||
self.used_button.currentIndexChanged.connect(self.toggle_used)
|
||||
for t in [_('All'), _('Unused'), _('Funded'), _('Used')]:
|
||||
self.used_button.addItem(t)
|
||||
|
||||
def get_list_header(self):
|
||||
def get_toolbar_buttons(self):
|
||||
return QLabel(_("Filter:")), self.change_button, self.used_button
|
||||
|
||||
def on_hide_toolbar(self):
|
||||
self.show_change = 0
|
||||
self.show_used = 0
|
||||
self.update()
|
||||
|
||||
def save_toolbar_state(self, state, config):
|
||||
config.set_key('show_toolbar_addresses', state)
|
||||
|
||||
def refresh_headers(self):
|
||||
headers = [ _('Address'), _('Label'), _('Balance')]
|
||||
headers = [_('Type'), _('Address'), _('Label'), _('Balance')]
|
||||
fx = self.parent.fx
|
||||
if fx and fx.get_fiat_address_config():
|
||||
headers.extend([_(fx.get_currency()+' Balance')])
|
||||
headers.extend([_('Tx')])
|
||||
self.update_headers(headers)
|
||||
|
||||
def toggle_change(self, show):
|
||||
show = bool(show)
|
||||
if show == self.show_change:
|
||||
def toggle_change(self, state):
|
||||
if state == self.show_change:
|
||||
return
|
||||
self.show_change = show
|
||||
self.show_change = state
|
||||
self.update()
|
||||
|
||||
def toggle_used(self, state):
|
||||
|
@ -77,10 +86,15 @@ class AddressList(MyTreeWidget):
|
|||
self.wallet = self.parent.wallet
|
||||
item = self.currentItem()
|
||||
current_address = item.data(0, Qt.UserRole) if item else None
|
||||
addr_list = self.wallet.get_change_addresses() if self.show_change else self.wallet.get_receiving_addresses()
|
||||
if self.show_change == 1:
|
||||
addr_list = self.wallet.get_receiving_addresses()
|
||||
elif self.show_change == 2:
|
||||
addr_list = self.wallet.get_change_addresses()
|
||||
else:
|
||||
addr_list = self.wallet.get_addresses()
|
||||
self.clear()
|
||||
for address in addr_list:
|
||||
num = len(self.wallet.history.get(address,[]))
|
||||
num = len(self.wallet.get_address_history(address))
|
||||
is_used = self.wallet.is_used(address)
|
||||
label = self.wallet.labels.get(address, '')
|
||||
c, u, x = self.wallet.get_addr_balance(address)
|
||||
|
@ -91,23 +105,29 @@ class AddressList(MyTreeWidget):
|
|||
continue
|
||||
if self.show_used == 3 and not is_used:
|
||||
continue
|
||||
balance_text = self.parent.format_amount(balance)
|
||||
balance_text = self.parent.format_amount(balance, whitespaces=True)
|
||||
fx = self.parent.fx
|
||||
if fx and fx.get_fiat_address_config():
|
||||
rate = fx.exchange_rate()
|
||||
fiat_balance = fx.value_str(balance, rate)
|
||||
address_item = QTreeWidgetItem([address, label, balance_text, fiat_balance, "%d"%num])
|
||||
address_item.setTextAlignment(3, Qt.AlignRight)
|
||||
address_item = SortableTreeWidgetItem(['', address, label, balance_text, fiat_balance, "%d"%num])
|
||||
address_item.setTextAlignment(4, Qt.AlignRight)
|
||||
address_item.setFont(4, QFont(MONOSPACE_FONT))
|
||||
else:
|
||||
address_item = QTreeWidgetItem([address, label, balance_text, "%d"%num])
|
||||
address_item.setTextAlignment(2, Qt.AlignRight)
|
||||
address_item.setFont(0, QFont(MONOSPACE_FONT))
|
||||
address_item.setData(0, Qt.UserRole, address)
|
||||
address_item.setData(0, Qt.UserRole+1, True) # label can be edited
|
||||
address_item = SortableTreeWidgetItem(['', address, label, balance_text, "%d"%num])
|
||||
address_item.setFont(3, QFont(MONOSPACE_FONT))
|
||||
if self.wallet.is_change(address):
|
||||
address_item.setText(0, _('change'))
|
||||
address_item.setBackground(0, ColorScheme.YELLOW.as_color(True))
|
||||
else:
|
||||
address_item.setText(0, _('receiving'))
|
||||
address_item.setBackground(0, ColorScheme.GREEN.as_color(True))
|
||||
address_item.setFont(1, QFont(MONOSPACE_FONT))
|
||||
address_item.setData(0, Qt.UserRole, address) # column 0; independent from address column
|
||||
if self.wallet.is_frozen(address):
|
||||
address_item.setBackground(0, ColorScheme.BLUE.as_color(True))
|
||||
if self.wallet.is_beyond_limit(address, self.show_change):
|
||||
address_item.setBackground(0, ColorScheme.RED.as_color(True))
|
||||
address_item.setBackground(1, ColorScheme.BLUE.as_color(True))
|
||||
if self.wallet.is_beyond_limit(address):
|
||||
address_item.setBackground(1, ColorScheme.RED.as_color(True))
|
||||
self.addChild(address_item)
|
||||
if address == current_address:
|
||||
self.setCurrentItem(address_item)
|
||||
|
@ -118,7 +138,7 @@ class AddressList(MyTreeWidget):
|
|||
can_delete = self.wallet.can_delete_address()
|
||||
selected = self.selectedItems()
|
||||
multi_select = len(selected) > 1
|
||||
addrs = [item.text(0) for item in selected]
|
||||
addrs = [item.text(1) for item in selected]
|
||||
if not addrs:
|
||||
return
|
||||
if not multi_select:
|
||||
|
@ -135,10 +155,10 @@ class AddressList(MyTreeWidget):
|
|||
if not multi_select:
|
||||
column_title = self.headerItem().text(col)
|
||||
copy_text = item.text(col)
|
||||
menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(copy_text))
|
||||
menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(copy_text))
|
||||
menu.addAction(_('Details'), lambda: self.parent.show_address(addr))
|
||||
if col in self.editable_columns:
|
||||
menu.addAction(_("Edit %s")%column_title, lambda: self.editItem(item, col))
|
||||
menu.addAction(_("Edit {}").format(column_title), lambda: self.editItem(item, col))
|
||||
menu.addAction(_("Request payment"), lambda: self.parent.receive_at(addr))
|
||||
if self.wallet.can_export():
|
||||
menu.addAction(_("Private key"), lambda: self.parent.show_private_key(addr))
|
||||
|
|
|
@ -69,6 +69,9 @@ class AmountEdit(MyLineEdit):
|
|||
except:
|
||||
return None
|
||||
|
||||
def setAmount(self, x):
|
||||
self.setText("%d"%x)
|
||||
|
||||
|
||||
class BTCAmountEdit(AmountEdit):
|
||||
|
||||
|
@ -78,7 +81,6 @@ class BTCAmountEdit(AmountEdit):
|
|||
|
||||
def _base_unit(self):
|
||||
p = self.decimal_point()
|
||||
assert p in [2, 5, 8]
|
||||
if p == 8:
|
||||
return 'ZEC'
|
||||
if p == 5:
|
||||
|
@ -101,6 +103,13 @@ class BTCAmountEdit(AmountEdit):
|
|||
else:
|
||||
self.setText(format_satoshis_plain(amount, self.decimal_point()))
|
||||
|
||||
class BTCkBEdit(BTCAmountEdit):
|
||||
|
||||
class FeerateEdit(BTCAmountEdit):
|
||||
def _base_unit(self):
|
||||
return BTCAmountEdit._base_unit(self) + '/kB'
|
||||
return 'sat/byte'
|
||||
|
||||
def get_amount(self):
|
||||
sat_per_byte_amount = BTCAmountEdit.get_amount(self)
|
||||
if sat_per_byte_amount is None:
|
||||
return None
|
||||
return 1000 * sat_per_byte_amount
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
# Electrum - lightweight Bitcoin client
|
||||
# Copyright (C) 2018 The Electrum developers
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
# obtaining a copy of this software and associated documentation files
|
||||
# (the "Software"), to deal in the Software without restriction,
|
||||
# including without limitation the rights to use, copy, modify, merge,
|
||||
# publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
# and to permit persons to whom the Software is furnished to do so,
|
||||
# subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from PyQt5.QtGui import *
|
||||
from PyQt5.QtCore import *
|
||||
from PyQt5.QtWidgets import *
|
||||
from .util import ButtonsTextEdit
|
||||
|
||||
class CompletionTextEdit(ButtonsTextEdit):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(CompletionTextEdit, self).__init__(parent)
|
||||
self.completer = None
|
||||
self.moveCursor(QTextCursor.End)
|
||||
self.disable_suggestions()
|
||||
|
||||
def set_completer(self, completer):
|
||||
self.completer = completer
|
||||
self.initialize_completer()
|
||||
|
||||
def initialize_completer(self):
|
||||
self.completer.setWidget(self)
|
||||
self.completer.setCompletionMode(QCompleter.PopupCompletion)
|
||||
self.completer.activated.connect(self.insert_completion)
|
||||
self.enable_suggestions()
|
||||
|
||||
def insert_completion(self, completion):
|
||||
if self.completer.widget() != self:
|
||||
return
|
||||
text_cursor = self.textCursor()
|
||||
extra = len(completion) - len(self.completer.completionPrefix())
|
||||
text_cursor.movePosition(QTextCursor.Left)
|
||||
text_cursor.movePosition(QTextCursor.EndOfWord)
|
||||
if extra == 0:
|
||||
text_cursor.insertText(" ")
|
||||
else:
|
||||
text_cursor.insertText(completion[-extra:] + " ")
|
||||
self.setTextCursor(text_cursor)
|
||||
|
||||
def text_under_cursor(self):
|
||||
tc = self.textCursor()
|
||||
tc.select(QTextCursor.WordUnderCursor)
|
||||
return tc.selectedText()
|
||||
|
||||
def enable_suggestions(self):
|
||||
self.suggestions_enabled = True
|
||||
|
||||
def disable_suggestions(self):
|
||||
self.suggestions_enabled = False
|
||||
|
||||
def keyPressEvent(self, e):
|
||||
if self.isReadOnly():
|
||||
return
|
||||
|
||||
if self.is_special_key(e):
|
||||
e.ignore()
|
||||
return
|
||||
|
||||
QPlainTextEdit.keyPressEvent(self, e)
|
||||
|
||||
ctrlOrShift = e.modifiers() and (Qt.ControlModifier or Qt.ShiftModifier)
|
||||
if self.completer is None or (ctrlOrShift and not e.text()):
|
||||
return
|
||||
|
||||
if not self.suggestions_enabled:
|
||||
return
|
||||
|
||||
eow = "~!@#$%^&*()_+{}|:\"<>?,./;'[]\\-="
|
||||
hasModifier = (e.modifiers() != Qt.NoModifier) and not ctrlOrShift
|
||||
completionPrefix = self.text_under_cursor()
|
||||
|
||||
if hasModifier or not e.text() or len(completionPrefix) < 1 or eow.find(e.text()[-1]) >= 0:
|
||||
self.completer.popup().hide()
|
||||
return
|
||||
|
||||
if completionPrefix != self.completer.completionPrefix():
|
||||
self.completer.setCompletionPrefix(completionPrefix)
|
||||
self.completer.popup().setCurrentIndex(self.completer.completionModel().index(0, 0))
|
||||
|
||||
cr = self.cursorRect()
|
||||
cr.setWidth(self.completer.popup().sizeHintForColumn(0) + self.completer.popup().verticalScrollBar().sizeHint().width())
|
||||
self.completer.complete(cr)
|
||||
|
||||
def is_special_key(self, e):
|
||||
if self.completer != None and self.completer.popup().isVisible():
|
||||
if e.key() in [Qt.Key_Enter, Qt.Key_Return]:
|
||||
return True
|
||||
if e.key() in [Qt.Key_Tab, Qt.Key_Down, Qt.Key_Up]:
|
||||
return True
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication([])
|
||||
completer = QCompleter(["alabama", "arkansas", "avocado", "breakfast", "sausage"])
|
||||
te = CompletionTextEdit()
|
||||
te.set_completer(completer)
|
||||
te.show()
|
||||
app.exec_()
|
|
@ -203,7 +203,8 @@ class Console(QtWidgets.QPlainTextEdit):
|
|||
self.skip = not self.skip
|
||||
|
||||
if type(self.namespace.get(command)) == type(lambda:None):
|
||||
self.appendPlainText("'%s' is a function. Type '%s()' to use it in the Python console."%(command, command))
|
||||
self.appendPlainText("'{}' is a function. Type '{}()' to use it in the Python console."
|
||||
.format(command, command))
|
||||
self.newPrompt()
|
||||
return
|
||||
|
||||
|
@ -222,7 +223,7 @@ class Console(QtWidgets.QPlainTextEdit):
|
|||
exec(command, self.namespace, self.namespace)
|
||||
except SystemExit:
|
||||
self.close()
|
||||
except Exception:
|
||||
except BaseException:
|
||||
traceback_lines = traceback.format_exc().split('\n')
|
||||
# Remove traceback mentioning this file, and a linebreak
|
||||
for i in (3,2,1,-1):
|
||||
|
|
|
@ -32,7 +32,7 @@ from PyQt5.QtGui import *
|
|||
from PyQt5.QtCore import *
|
||||
from PyQt5.QtWidgets import (
|
||||
QAbstractItemView, QFileDialog, QMenu, QTreeWidgetItem)
|
||||
from .util import MyTreeWidget
|
||||
from .util import MyTreeWidget, import_meta_gui, export_meta_gui
|
||||
|
||||
|
||||
class ContactList(MyTreeWidget):
|
||||
|
@ -53,12 +53,10 @@ class ContactList(MyTreeWidget):
|
|||
self.parent.set_contact(item.text(0), item.text(1))
|
||||
|
||||
def import_contacts(self):
|
||||
wallet_folder = self.parent.get_wallet_folder()
|
||||
filename, __ = QFileDialog.getOpenFileName(self.parent, "Select your wallet file", wallet_folder)
|
||||
if not filename:
|
||||
return
|
||||
self.parent.contacts.import_file(filename)
|
||||
self.on_update()
|
||||
import_meta_gui(self.parent, _('contacts'), self.parent.contacts.import_file, self.on_update)
|
||||
|
||||
def export_contacts(self):
|
||||
export_meta_gui(self.parent, _('contacts'), self.parent.contacts.export_file)
|
||||
|
||||
def create_menu(self, position):
|
||||
menu = QMenu()
|
||||
|
@ -66,16 +64,17 @@ class ContactList(MyTreeWidget):
|
|||
if not selected:
|
||||
menu.addAction(_("New contact"), lambda: self.parent.new_contact_dialog())
|
||||
menu.addAction(_("Import file"), lambda: self.import_contacts())
|
||||
menu.addAction(_("Export file"), lambda: self.export_contacts())
|
||||
else:
|
||||
names = [item.text(0) for item in selected]
|
||||
keys = [item.text(1) for item in selected]
|
||||
column = self.currentColumn()
|
||||
column_title = self.headerItem().text(column)
|
||||
column_data = '\n'.join([item.text(column) for item in selected])
|
||||
menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(column_data))
|
||||
menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data))
|
||||
if column in self.editable_columns:
|
||||
item = self.currentItem()
|
||||
menu.addAction(_("Edit %s")%column_title, lambda: self.editItem(item, column))
|
||||
menu.addAction(_("Edit {}").format(column_title), lambda: self.editItem(item, column))
|
||||
menu.addAction(_("Pay to"), lambda: self.parent.payto_contacts(keys))
|
||||
menu.addAction(_("Delete"), lambda: self.parent.delete_contacts(keys))
|
||||
URLs = [block_explorer_URL(self.config, 'addr', key) for key in filter(is_address, keys)]
|
||||
|
|
|
@ -0,0 +1,211 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
# Electrum - lightweight Bitcoin client
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
# obtaining a copy of this software and associated documentation files
|
||||
# (the "Software"), to deal in the Software without restriction,
|
||||
# including without limitation the rights to use, copy, modify, merge,
|
||||
# publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
# and to permit persons to whom the Software is furnished to do so,
|
||||
# subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import json
|
||||
import locale
|
||||
import platform
|
||||
import traceback
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
import requests
|
||||
from PyQt5.QtCore import QObject
|
||||
import PyQt5.QtCore as QtCore
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import *
|
||||
|
||||
from electrum_zcash.i18n import _
|
||||
from electrum_zcash import ELECTRUM_VERSION, bitcoin, constants
|
||||
|
||||
from .util import MessageBoxMixin
|
||||
|
||||
issue_template = """<h2>Traceback</h2>
|
||||
<pre>
|
||||
{traceback}
|
||||
</pre>
|
||||
|
||||
<h2>Additional information</h2>
|
||||
<ul>
|
||||
<li>Electrum-Zcash version: {app_version}</li>
|
||||
<li>Operating system: {os}</li>
|
||||
<li>Wallet type: {wallet_type}</li>
|
||||
<li>Locale: {locale}</li>
|
||||
</ul>
|
||||
"""
|
||||
report_server = "https://crashhub.electrum.org/crash"
|
||||
|
||||
|
||||
class Exception_Window(QWidget, MessageBoxMixin):
|
||||
_active_window = None
|
||||
|
||||
def __init__(self, main_window, exctype, value, tb):
|
||||
self.exc_args = (exctype, value, tb)
|
||||
self.main_window = main_window
|
||||
QWidget.__init__(self)
|
||||
self.setWindowTitle('Electrum-Zcash - ' + _('An Error Occurred'))
|
||||
self.setMinimumSize(600, 300)
|
||||
|
||||
main_box = QVBoxLayout()
|
||||
|
||||
heading = QLabel('<h2>' + _('Sorry!') + '</h2>')
|
||||
main_box.addWidget(heading)
|
||||
main_box.addWidget(QLabel(_('Something went wrong while executing Electrum-Zcash.')))
|
||||
|
||||
main_box.addWidget(QLabel(
|
||||
_('To help us diagnose and fix the problem, you can send us a bug report that contains useful debug '
|
||||
'information:')))
|
||||
|
||||
collapse_info = QPushButton(_("Show report contents"))
|
||||
collapse_info.clicked.connect(
|
||||
lambda: self.msg_box(QMessageBox.NoIcon,
|
||||
self, "Report contents", self.get_report_string()))
|
||||
main_box.addWidget(collapse_info)
|
||||
|
||||
main_box.addWidget(QLabel(_("Please briefly describe what led to the error (optional):")))
|
||||
|
||||
self.description_textfield = QTextEdit()
|
||||
self.description_textfield.setFixedHeight(50)
|
||||
main_box.addWidget(self.description_textfield)
|
||||
|
||||
main_box.addWidget(QLabel(_("Do you want to send this report?")))
|
||||
|
||||
buttons = QHBoxLayout()
|
||||
|
||||
report_button = QPushButton(_('Send Bug Report'))
|
||||
report_button.clicked.connect(self.send_report)
|
||||
report_button.setIcon(QIcon(":icons/tab_send.png"))
|
||||
buttons.addWidget(report_button)
|
||||
|
||||
never_button = QPushButton(_('Never'))
|
||||
never_button.clicked.connect(self.show_never)
|
||||
buttons.addWidget(never_button)
|
||||
|
||||
close_button = QPushButton(_('Not Now'))
|
||||
close_button.clicked.connect(self.close)
|
||||
buttons.addWidget(close_button)
|
||||
|
||||
main_box.addLayout(buttons)
|
||||
|
||||
self.setLayout(main_box)
|
||||
self.show()
|
||||
|
||||
def send_report(self):
|
||||
if constants.net.GENESIS[-4:] not in ["4943", "e26f"] and ".electrum.org" in report_server:
|
||||
# Gah! Some kind of altcoin wants to send us crash reports.
|
||||
self.main_window.show_critical(_("Please report this issue manually."))
|
||||
return
|
||||
report = self.get_traceback_info()
|
||||
report.update(self.get_additional_info())
|
||||
report = json.dumps(report)
|
||||
try:
|
||||
response = requests.post(report_server, data=report, timeout=20)
|
||||
except BaseException as e:
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
self.main_window.show_critical(_('There was a problem with the automatic reporting:') + '\n' +
|
||||
str(e) + '\n' +
|
||||
_("Please report this issue manually."))
|
||||
return
|
||||
else:
|
||||
QMessageBox.about(self, "Crash report", response.text)
|
||||
self.close()
|
||||
|
||||
def on_close(self):
|
||||
Exception_Window._active_window = None
|
||||
sys.__excepthook__(*self.exc_args)
|
||||
self.close()
|
||||
|
||||
def show_never(self):
|
||||
self.main_window.config.set_key("show_crash_reporter", False)
|
||||
self.close()
|
||||
|
||||
def closeEvent(self, event):
|
||||
self.on_close()
|
||||
event.accept()
|
||||
|
||||
def get_traceback_info(self):
|
||||
exc_string = str(self.exc_args[1])
|
||||
stack = traceback.extract_tb(self.exc_args[2])
|
||||
readable_trace = "".join(traceback.format_list(stack))
|
||||
id = {
|
||||
"file": stack[-1].filename,
|
||||
"name": stack[-1].name,
|
||||
"type": self.exc_args[0].__name__
|
||||
}
|
||||
return {
|
||||
"exc_string": exc_string,
|
||||
"stack": readable_trace,
|
||||
"id": id
|
||||
}
|
||||
|
||||
def get_additional_info(self):
|
||||
args = {
|
||||
"app_version": ELECTRUM_VERSION,
|
||||
"os": platform.platform(),
|
||||
"wallet_type": "unknown",
|
||||
"locale": locale.getdefaultlocale()[0],
|
||||
"description": self.description_textfield.toPlainText()
|
||||
}
|
||||
try:
|
||||
args["wallet_type"] = self.main_window.wallet.wallet_type
|
||||
except:
|
||||
# Maybe the wallet isn't loaded yet
|
||||
pass
|
||||
try:
|
||||
args["app_version"] = self.get_git_version()
|
||||
except:
|
||||
# This is probably not running from source
|
||||
pass
|
||||
return args
|
||||
|
||||
def get_report_string(self):
|
||||
info = self.get_additional_info()
|
||||
info["traceback"] = "".join(traceback.format_exception(*self.exc_args))
|
||||
return issue_template.format(**info)
|
||||
|
||||
@staticmethod
|
||||
def get_git_version():
|
||||
dir = os.path.dirname(os.path.realpath(sys.argv[0]))
|
||||
version = subprocess.check_output(
|
||||
['git', 'describe', '--always', '--dirty'], cwd=dir)
|
||||
return str(version, "utf8").strip()
|
||||
|
||||
|
||||
def _show_window(*args):
|
||||
if not Exception_Window._active_window:
|
||||
Exception_Window._active_window = Exception_Window(*args)
|
||||
|
||||
|
||||
class Exception_Hook(QObject):
|
||||
_report_exception = QtCore.pyqtSignal(object, object, object, object)
|
||||
|
||||
def __init__(self, main_window, *args, **kwargs):
|
||||
super(Exception_Hook, self).__init__(*args, **kwargs)
|
||||
if not main_window.config.get("show_crash_reporter", default=True):
|
||||
return
|
||||
self.main_window = main_window
|
||||
sys.excepthook = self.handler
|
||||
self._report_exception.connect(_show_window)
|
||||
|
||||
def handler(self, *args):
|
||||
self._report_exception.emit(self.main_window, *args)
|
|
@ -1,6 +1,4 @@
|
|||
|
||||
from electrum_zcash.i18n import _
|
||||
|
||||
from PyQt5.QtGui import *
|
||||
from PyQt5.QtCore import *
|
||||
from PyQt5.QtWidgets import QSlider, QToolTip
|
||||
|
@ -18,39 +16,63 @@ class FeeSlider(QSlider):
|
|||
self.lock = threading.RLock()
|
||||
self.update()
|
||||
self.valueChanged.connect(self.moved)
|
||||
self._active = True
|
||||
|
||||
def moved(self, pos):
|
||||
with self.lock:
|
||||
fee_rate = self.config.dynfee(pos) if self.dyn else self.config.static_fee(pos)
|
||||
if self.dyn:
|
||||
fee_rate = self.config.depth_to_fee(pos) if self.config.use_mempool_fees() else self.config.eta_to_fee(pos)
|
||||
else:
|
||||
fee_rate = self.config.static_fee(pos)
|
||||
tooltip = self.get_tooltip(pos, fee_rate)
|
||||
QToolTip.showText(QCursor.pos(), tooltip, self)
|
||||
self.setToolTip(tooltip)
|
||||
self.callback(self.dyn, pos, fee_rate)
|
||||
|
||||
def get_tooltip(self, pos, fee_rate):
|
||||
from electrum_zcash.util import fee_levels
|
||||
rate_str = self.window.format_fee_rate(fee_rate) if fee_rate else _('unknown')
|
||||
mempool = self.config.use_mempool_fees()
|
||||
target, estimate = self.config.get_fee_text(pos, self.dyn, mempool, fee_rate)
|
||||
if self.dyn:
|
||||
tooltip = fee_levels[pos] + '\n' + rate_str
|
||||
return _('Target') + ': ' + target + '\n' + _('Current rate') + ': ' + estimate
|
||||
else:
|
||||
tooltip = 'Fixed rate: ' + rate_str
|
||||
if self.config.has_fee_estimates():
|
||||
i = self.config.reverse_dynfee(fee_rate)
|
||||
tooltip += '\n' + (_('Low fee') if i < 0 else 'Within %d blocks'%i)
|
||||
return tooltip
|
||||
return _('Fixed rate') + ': ' + target + '\n' + _('Estimate') + ': ' + estimate
|
||||
|
||||
def update(self):
|
||||
with self.lock:
|
||||
self.dyn = self.config.is_dynfee()
|
||||
if self.dyn:
|
||||
pos = self.config.get('fee_level', 2)
|
||||
fee_rate = self.config.dynfee(pos)
|
||||
self.setRange(0, 4)
|
||||
self.setValue(pos)
|
||||
else:
|
||||
fee_rate = self.config.fee_per_kb()
|
||||
pos = self.config.static_fee_index(fee_rate)
|
||||
self.setRange(0, 9)
|
||||
self.setValue(pos)
|
||||
mempool = self.config.use_mempool_fees()
|
||||
maxp, pos, fee_rate = self.config.get_fee_slider(self.dyn, mempool)
|
||||
self.setRange(0, maxp)
|
||||
self.setValue(pos)
|
||||
tooltip = self.get_tooltip(pos, fee_rate)
|
||||
self.setToolTip(tooltip)
|
||||
|
||||
def activate(self):
|
||||
self._active = True
|
||||
self.setStyleSheet('')
|
||||
|
||||
def deactivate(self):
|
||||
self._active = False
|
||||
# TODO it would be nice to find a platform-independent solution
|
||||
# that makes the slider look as if it was disabled
|
||||
self.setStyleSheet(
|
||||
"""
|
||||
QSlider::groove:horizontal {
|
||||
border: 1px solid #999999;
|
||||
height: 8px;
|
||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #B1B1B1, stop:1 #B1B1B1);
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
QSlider::handle:horizontal {
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #b4b4b4, stop:1 #8f8f8f);
|
||||
border: 1px solid #5c5c5c;
|
||||
width: 12px;
|
||||
margin: -2px 0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
def is_active(self):
|
||||
return self._active
|
||||
|
|
|
@ -24,19 +24,24 @@
|
|||
# SOFTWARE.
|
||||
|
||||
import webbrowser
|
||||
import datetime
|
||||
|
||||
from electrum_zcash.wallet import AddTransactionException, TX_HEIGHT_LOCAL
|
||||
from .util import *
|
||||
from electrum_zcash.i18n import _
|
||||
from electrum_zcash.util import block_explorer_URL
|
||||
from electrum_zcash.util import timestamp_to_datetime, profiler
|
||||
from electrum_zcash.util import block_explorer_URL, profiler
|
||||
|
||||
try:
|
||||
from electrum_zcash.plot import plot_history, NothingToPlotException
|
||||
except:
|
||||
plot_history = None
|
||||
|
||||
# note: this list needs to be kept in sync with another in kivy
|
||||
TX_ICONS = [
|
||||
"warning.png",
|
||||
"warning.png",
|
||||
"unconfirmed.png",
|
||||
"warning.png",
|
||||
"unconfirmed.png",
|
||||
"unconfirmed.png",
|
||||
"offline_tx.png",
|
||||
"clock1.png",
|
||||
"clock2.png",
|
||||
"clock3.png",
|
||||
|
@ -46,67 +51,238 @@ TX_ICONS = [
|
|||
]
|
||||
|
||||
|
||||
class HistoryList(MyTreeWidget):
|
||||
class HistoryList(MyTreeWidget, AcceptFileDragDrop):
|
||||
filter_columns = [2, 3, 4] # Date, Description, Amount
|
||||
|
||||
def __init__(self, parent=None):
|
||||
MyTreeWidget.__init__(self, parent, self.create_menu, [], 3)
|
||||
AcceptFileDragDrop.__init__(self, ".txn")
|
||||
self.refresh_headers()
|
||||
self.setColumnHidden(1, True)
|
||||
self.setSortingEnabled(True)
|
||||
self.sortByColumn(0, Qt.AscendingOrder)
|
||||
self.start_timestamp = None
|
||||
self.end_timestamp = None
|
||||
self.years = []
|
||||
self.create_toolbar_buttons()
|
||||
self.wallet = None
|
||||
|
||||
def format_date(self, d):
|
||||
return str(datetime.date(d.year, d.month, d.day)) if d else _('None')
|
||||
|
||||
def refresh_headers(self):
|
||||
headers = ['', '', _('Date'), _('Description') , _('Amount'), _('Balance')]
|
||||
headers = ['', '', _('Date'), _('Description'), _('Amount'), _('Balance')]
|
||||
fx = self.parent.fx
|
||||
if fx and fx.show_history():
|
||||
headers.extend(['%s '%fx.ccy + _('Amount'), '%s '%fx.ccy + _('Balance')])
|
||||
headers.extend(['%s '%fx.ccy + _('Value')])
|
||||
self.editable_columns |= {6}
|
||||
if fx.get_history_capital_gains_config():
|
||||
headers.extend(['%s '%fx.ccy + _('Acquisition price')])
|
||||
headers.extend(['%s '%fx.ccy + _('Capital Gains')])
|
||||
else:
|
||||
self.editable_columns -= {6}
|
||||
self.update_headers(headers)
|
||||
|
||||
def get_domain(self):
|
||||
'''Replaced in address_dialog.py'''
|
||||
return self.wallet.get_addresses()
|
||||
|
||||
def on_combo(self, x):
|
||||
s = self.period_combo.itemText(x)
|
||||
x = s == _('Custom')
|
||||
self.start_button.setEnabled(x)
|
||||
self.end_button.setEnabled(x)
|
||||
if s == _('All'):
|
||||
self.start_timestamp = None
|
||||
self.end_timestamp = None
|
||||
self.start_button.setText("-")
|
||||
self.end_button.setText("-")
|
||||
else:
|
||||
try:
|
||||
year = int(s)
|
||||
except:
|
||||
return
|
||||
start_date = datetime.datetime(year, 1, 1)
|
||||
end_date = datetime.datetime(year+1, 1, 1)
|
||||
self.start_timestamp = time.mktime(start_date.timetuple())
|
||||
self.end_timestamp = time.mktime(end_date.timetuple())
|
||||
self.start_button.setText(_('From') + ' ' + self.format_date(start_date))
|
||||
self.end_button.setText(_('To') + ' ' + self.format_date(end_date))
|
||||
self.update()
|
||||
|
||||
def create_toolbar_buttons(self):
|
||||
self.period_combo = QComboBox()
|
||||
self.start_button = QPushButton('-')
|
||||
self.start_button.pressed.connect(self.select_start_date)
|
||||
self.start_button.setEnabled(False)
|
||||
self.end_button = QPushButton('-')
|
||||
self.end_button.pressed.connect(self.select_end_date)
|
||||
self.end_button.setEnabled(False)
|
||||
self.period_combo.addItems([_('All'), _('Custom')])
|
||||
self.period_combo.activated.connect(self.on_combo)
|
||||
|
||||
def get_toolbar_buttons(self):
|
||||
return self.period_combo, self.start_button, self.end_button
|
||||
|
||||
def on_hide_toolbar(self):
|
||||
self.start_timestamp = None
|
||||
self.end_timestamp = None
|
||||
self.update()
|
||||
|
||||
def save_toolbar_state(self, state, config):
|
||||
config.set_key('show_toolbar_history', state)
|
||||
|
||||
def select_start_date(self):
|
||||
self.start_timestamp = self.select_date(self.start_button)
|
||||
self.update()
|
||||
|
||||
def select_end_date(self):
|
||||
self.end_timestamp = self.select_date(self.end_button)
|
||||
self.update()
|
||||
|
||||
def select_date(self, button):
|
||||
d = WindowModalDialog(self, _("Select date"))
|
||||
d.setMinimumSize(600, 150)
|
||||
d.date = None
|
||||
vbox = QVBoxLayout()
|
||||
def on_date(date):
|
||||
d.date = date
|
||||
cal = QCalendarWidget()
|
||||
cal.setGridVisible(True)
|
||||
cal.clicked[QDate].connect(on_date)
|
||||
vbox.addWidget(cal)
|
||||
vbox.addLayout(Buttons(OkButton(d), CancelButton(d)))
|
||||
d.setLayout(vbox)
|
||||
if d.exec_():
|
||||
if d.date is None:
|
||||
return None
|
||||
date = d.date.toPyDate()
|
||||
button.setText(self.format_date(date))
|
||||
return time.mktime(date.timetuple())
|
||||
|
||||
def show_summary(self):
|
||||
h = self.summary
|
||||
if not h:
|
||||
self.parent.show_message(_("Nothing to summarize."))
|
||||
return
|
||||
start_date = h.get('start_date')
|
||||
end_date = h.get('end_date')
|
||||
format_amount = lambda x: self.parent.format_amount(x.value) + ' ' + self.parent.base_unit()
|
||||
d = WindowModalDialog(self, _("Summary"))
|
||||
d.setMinimumSize(600, 150)
|
||||
vbox = QVBoxLayout()
|
||||
grid = QGridLayout()
|
||||
grid.addWidget(QLabel(_("Start")), 0, 0)
|
||||
grid.addWidget(QLabel(self.format_date(start_date)), 0, 1)
|
||||
grid.addWidget(QLabel(_("End")), 1, 0)
|
||||
grid.addWidget(QLabel(self.format_date(end_date)), 1, 1)
|
||||
grid.addWidget(QLabel(_("Initial balance")), 2, 0)
|
||||
grid.addWidget(QLabel(format_amount(h['start_balance'])), 2, 1)
|
||||
grid.addWidget(QLabel(str(h.get('start_fiat_balance'))), 2, 2)
|
||||
grid.addWidget(QLabel(_("Final balance")), 4, 0)
|
||||
grid.addWidget(QLabel(format_amount(h['end_balance'])), 4, 1)
|
||||
grid.addWidget(QLabel(str(h.get('end_fiat_balance'))), 4, 2)
|
||||
grid.addWidget(QLabel(_("Income")), 5, 0)
|
||||
grid.addWidget(QLabel(format_amount(h.get('income'))), 5, 1)
|
||||
grid.addWidget(QLabel(str(h.get('fiat_income'))), 5, 2)
|
||||
grid.addWidget(QLabel(_("Expenditures")), 6, 0)
|
||||
grid.addWidget(QLabel(format_amount(h.get('expenditures'))), 6, 1)
|
||||
grid.addWidget(QLabel(str(h.get('fiat_expenditures'))), 6, 2)
|
||||
grid.addWidget(QLabel(_("Capital gains")), 7, 0)
|
||||
grid.addWidget(QLabel(str(h.get('capital_gains'))), 7, 2)
|
||||
grid.addWidget(QLabel(_("Unrealized gains")), 8, 0)
|
||||
grid.addWidget(QLabel(str(h.get('unrealized_gains', ''))), 8, 2)
|
||||
vbox.addLayout(grid)
|
||||
vbox.addLayout(Buttons(CloseButton(d)))
|
||||
d.setLayout(vbox)
|
||||
d.exec_()
|
||||
|
||||
def plot_history_dialog(self):
|
||||
if plot_history is None:
|
||||
self.parent.show_message(
|
||||
_("Can't plot history.") + '\n' +
|
||||
_("Perhaps some dependencies are missing...") + " (matplotlib?)")
|
||||
return
|
||||
try:
|
||||
plt = plot_history(self.transactions)
|
||||
plt.show()
|
||||
except NothingToPlotException as e:
|
||||
self.parent.show_message(str(e))
|
||||
|
||||
@profiler
|
||||
def on_update(self):
|
||||
self.wallet = self.parent.wallet
|
||||
h = self.wallet.get_history(self.get_domain())
|
||||
fx = self.parent.fx
|
||||
r = self.wallet.get_full_history(domain=self.get_domain(), from_timestamp=self.start_timestamp, to_timestamp=self.end_timestamp, fx=fx)
|
||||
self.transactions = r['transactions']
|
||||
self.summary = r['summary']
|
||||
if not self.years and self.transactions:
|
||||
from datetime import date
|
||||
start_date = self.transactions[0].get('date') or date.today()
|
||||
end_date = self.transactions[-1].get('date') or date.today()
|
||||
self.years = [str(i) for i in range(start_date.year, end_date.year + 1)]
|
||||
self.period_combo.insertItems(1, self.years)
|
||||
item = self.currentItem()
|
||||
current_tx = item.data(0, Qt.UserRole) if item else None
|
||||
self.clear()
|
||||
fx = self.parent.fx
|
||||
if fx: fx.history_used_spot = False
|
||||
for h_item in h:
|
||||
tx_hash, height, conf, timestamp, value, balance = h_item
|
||||
for tx_item in self.transactions:
|
||||
tx_hash = tx_item['txid']
|
||||
height = tx_item['height']
|
||||
conf = tx_item['confirmations']
|
||||
timestamp = tx_item['timestamp']
|
||||
value = tx_item['value'].value
|
||||
balance = tx_item['balance'].value
|
||||
label = tx_item['label']
|
||||
status, status_str = self.wallet.get_tx_status(tx_hash, height, conf, timestamp)
|
||||
has_invoice = self.wallet.invoices.paid.get(tx_hash)
|
||||
icon = QIcon(":icons/" + TX_ICONS[status])
|
||||
icon = self.icon_cache.get(":icons/" + TX_ICONS[status])
|
||||
v_str = self.parent.format_amount(value, True, whitespaces=True)
|
||||
balance_str = self.parent.format_amount(balance, whitespaces=True)
|
||||
label = self.wallet.get_label(tx_hash)
|
||||
entry = ['', tx_hash, status_str, label, v_str, balance_str]
|
||||
if fx and fx.show_history():
|
||||
date = timestamp_to_datetime(time.time() if conf <= 0 else timestamp)
|
||||
for amount in [value, balance]:
|
||||
text = fx.historical_value_str(amount, date)
|
||||
entry.append(text)
|
||||
item = QTreeWidgetItem(entry)
|
||||
fiat_value = None
|
||||
if value is not None and fx and fx.show_history():
|
||||
fiat_value = tx_item['fiat_value'].value
|
||||
value_str = fx.format_fiat(fiat_value)
|
||||
entry.append(value_str)
|
||||
# fixme: should use is_mine
|
||||
if value < 0:
|
||||
entry.append(fx.format_fiat(tx_item['acquisition_price'].value))
|
||||
entry.append(fx.format_fiat(tx_item['capital_gain'].value))
|
||||
item = SortableTreeWidgetItem(entry)
|
||||
item.setIcon(0, icon)
|
||||
item.setToolTip(0, str(conf) + " confirmation" + ("s" if conf != 1 else ""))
|
||||
item.setData(0, SortableTreeWidgetItem.DataRole, (status, conf))
|
||||
if has_invoice:
|
||||
item.setIcon(3, QIcon(":icons/seal"))
|
||||
item.setIcon(3, self.icon_cache.get(":icons/seal"))
|
||||
for i in range(len(entry)):
|
||||
if i>3:
|
||||
item.setTextAlignment(i, Qt.AlignRight)
|
||||
if i!=2:
|
||||
item.setFont(i, QFont(MONOSPACE_FONT))
|
||||
item.setTextAlignment(i, Qt.AlignRight | Qt.AlignVCenter)
|
||||
item.setFont(i, QFont(MONOSPACE_FONT))
|
||||
if value and value < 0:
|
||||
item.setForeground(3, QBrush(QColor("#BC1E1E")))
|
||||
item.setForeground(4, QBrush(QColor("#BC1E1E")))
|
||||
if fiat_value and not tx_item['fiat_default']:
|
||||
item.setForeground(6, QBrush(QColor("#1E1EFF")))
|
||||
if tx_hash:
|
||||
item.setData(0, Qt.UserRole, tx_hash)
|
||||
self.insertTopLevelItem(0, item)
|
||||
if current_tx == tx_hash:
|
||||
self.setCurrentItem(item)
|
||||
|
||||
def on_edited(self, item, column, prior):
|
||||
'''Called only when the text actually changes'''
|
||||
key = item.data(0, Qt.UserRole)
|
||||
text = item.text(column)
|
||||
# fixme
|
||||
if column == 3:
|
||||
self.parent.wallet.set_label(key, text)
|
||||
self.update_labels()
|
||||
self.parent.update_completions()
|
||||
elif column == 6:
|
||||
self.parent.wallet.set_fiat_value(key, self.parent.fx.ccy, text)
|
||||
self.on_update()
|
||||
|
||||
def on_doubleclick(self, item, column):
|
||||
if self.permit_edit(item, column):
|
||||
super(HistoryList, self).on_doubleclick(item, column)
|
||||
|
@ -125,12 +301,15 @@ class HistoryList(MyTreeWidget):
|
|||
item.setText(3, label)
|
||||
|
||||
def update_item(self, tx_hash, height, conf, timestamp):
|
||||
if self.wallet is None:
|
||||
return
|
||||
status, status_str = self.wallet.get_tx_status(tx_hash, height, conf, timestamp)
|
||||
icon = QIcon(":icons/" + TX_ICONS[status])
|
||||
icon = self.icon_cache.get(":icons/" + TX_ICONS[status])
|
||||
items = self.findItems(tx_hash, Qt.UserRole|Qt.MatchContains|Qt.MatchRecursive, column=1)
|
||||
if items:
|
||||
item = items[0]
|
||||
item.setIcon(0, icon)
|
||||
item.setData(0, SortableTreeWidgetItem.DataRole, (status, conf))
|
||||
item.setText(2, status_str)
|
||||
|
||||
def create_menu(self, position):
|
||||
|
@ -148,23 +327,92 @@ class HistoryList(MyTreeWidget):
|
|||
else:
|
||||
column_title = self.headerItem().text(column)
|
||||
column_data = item.text(column)
|
||||
|
||||
tx_URL = block_explorer_URL(self.config, 'tx', tx_hash)
|
||||
height, conf, timestamp = self.wallet.get_tx_height(tx_hash)
|
||||
tx = self.wallet.transactions.get(tx_hash)
|
||||
is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx)
|
||||
is_unconfirmed = height <= 0
|
||||
pr_key = self.wallet.invoices.paid.get(tx_hash)
|
||||
|
||||
menu = QMenu()
|
||||
|
||||
menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(column_data))
|
||||
if column in self.editable_columns:
|
||||
menu.addAction(_("Edit %s")%column_title, lambda: self.editItem(item, column))
|
||||
|
||||
if height == TX_HEIGHT_LOCAL:
|
||||
menu.addAction(_("Remove"), lambda: self.remove_local_tx(tx_hash))
|
||||
menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data))
|
||||
for c in self.editable_columns:
|
||||
menu.addAction(_("Edit {}").format(self.headerItem().text(c)),
|
||||
lambda bound_c=c: self.editItem(item, bound_c))
|
||||
menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx))
|
||||
if pr_key:
|
||||
menu.addAction(QIcon(":icons/seal"), _("View invoice"), lambda: self.parent.show_invoice(pr_key))
|
||||
menu.addAction(self.icon_cache.get(":icons/seal"), _("View invoice"), lambda: self.parent.show_invoice(pr_key))
|
||||
if tx_URL:
|
||||
menu.addAction(_("View on block explorer"), lambda: webbrowser.open(tx_URL))
|
||||
menu.exec_(self.viewport().mapToGlobal(position))
|
||||
|
||||
def remove_local_tx(self, delete_tx):
|
||||
to_delete = {delete_tx}
|
||||
to_delete |= self.wallet.get_depending_transactions(delete_tx)
|
||||
question = _("Are you sure you want to remove this transaction?")
|
||||
if len(to_delete) > 1:
|
||||
question = _(
|
||||
"Are you sure you want to remove this transaction and {} child transactions?".format(len(to_delete) - 1)
|
||||
)
|
||||
answer = QMessageBox.question(self.parent, _("Please confirm"), question, QMessageBox.Yes, QMessageBox.No)
|
||||
if answer == QMessageBox.No:
|
||||
return
|
||||
for tx in to_delete:
|
||||
self.wallet.remove_transaction(tx)
|
||||
self.wallet.save_transactions(write=True)
|
||||
# need to update at least: history_list, utxo_list, address_list
|
||||
self.parent.need_update.set()
|
||||
|
||||
def onFileAdded(self, fn):
|
||||
try:
|
||||
with open(fn) as f:
|
||||
tx = self.parent.tx_from_text(f.read())
|
||||
self.parent.save_transaction_into_wallet(tx)
|
||||
except IOError as e:
|
||||
self.parent.show_error(e)
|
||||
|
||||
def export_history_dialog(self):
|
||||
d = WindowModalDialog(self, _('Export History'))
|
||||
d.setMinimumSize(400, 200)
|
||||
vbox = QVBoxLayout(d)
|
||||
defaultname = os.path.expanduser('~/electrum-zcash-history.csv')
|
||||
select_msg = _('Select file to export your wallet transactions to')
|
||||
hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg)
|
||||
vbox.addLayout(hbox)
|
||||
vbox.addStretch(1)
|
||||
hbox = Buttons(CancelButton(d), OkButton(d, _('Export')))
|
||||
vbox.addLayout(hbox)
|
||||
#run_hook('export_history_dialog', self, hbox)
|
||||
self.update()
|
||||
if not d.exec_():
|
||||
return
|
||||
filename = filename_e.text()
|
||||
if not filename:
|
||||
return
|
||||
try:
|
||||
self.do_export_history(self.wallet, filename, csv_button.isChecked())
|
||||
except (IOError, os.error) as reason:
|
||||
export_error_label = _("Electrum-Zcash was unable to produce a transaction export.")
|
||||
self.parent.show_critical(export_error_label + "\n" + str(reason), title=_("Unable to export history"))
|
||||
return
|
||||
self.parent.show_message(_("Your wallet history has been successfully exported."))
|
||||
|
||||
def do_export_history(self, wallet, fileName, is_csv):
|
||||
history = self.transactions
|
||||
lines = []
|
||||
for item in history:
|
||||
if is_csv:
|
||||
lines.append([item['txid'], item.get('label', ''), item['confirmations'], item['value'], item['date']])
|
||||
else:
|
||||
lines.append(item)
|
||||
with open(fileName, "w+", encoding='utf-8') as f:
|
||||
if is_csv:
|
||||
import csv
|
||||
transaction = csv.writer(f, lineterminator='\n')
|
||||
transaction.writerow(["transaction_hash","label", "confirmations", "value", "timestamp"])
|
||||
for line in lines:
|
||||
transaction.writerow(line)
|
||||
else:
|
||||
from electrum_zcash.util import json_encode
|
||||
f.write(json_encode(history))
|
||||
|
|
|
@ -10,29 +10,24 @@ from PyQt5.QtWidgets import *
|
|||
|
||||
from electrum_zcash import Wallet, WalletStorage
|
||||
from electrum_zcash.util import UserCancelled, InvalidPassword
|
||||
from electrum_zcash.base_wizard import BaseWizard
|
||||
from electrum_zcash.base_wizard import BaseWizard, HWD_SETUP_DECRYPT_WALLET
|
||||
from electrum_zcash.i18n import _
|
||||
|
||||
from .seed_dialog import SeedLayout, KeysLayout
|
||||
from .network_dialog import NetworkChoiceLayout
|
||||
from .util import *
|
||||
from .password_dialog import PasswordLayout, PW_NEW
|
||||
from .password_dialog import PasswordLayout, PasswordLayoutForHW, PW_NEW
|
||||
|
||||
|
||||
class GoBack(Exception):
|
||||
pass
|
||||
|
||||
MSG_GENERATING_WAIT = _("Electrum-Zcash is generating your addresses, please wait...")
|
||||
MSG_ENTER_ANYTHING = _("Please enter a seed phrase, a master key, a list of "
|
||||
"Zcash addresses, or a list of private keys")
|
||||
MSG_ENTER_SEED_OR_MPK = _("Please enter a seed phrase or a master key (xpub or xprv):")
|
||||
MSG_COSIGNER = _("Please enter the master public key of cosigner #%d:")
|
||||
MSG_ENTER_PASSWORD = _("Choose a password to encrypt your wallet keys.") + '\n'\
|
||||
+ _("Leave this field empty if you want to disable encryption.")
|
||||
MSG_RESTORE_PASSPHRASE = \
|
||||
_("Please enter your seed derivation passphrase. "
|
||||
"Note: this is NOT your encryption password. "
|
||||
"Leave this field empty if you did not use one or are unsure.")
|
||||
MSG_HW_STORAGE_ENCRYPTION = _("Set wallet file encryption.") + '\n'\
|
||||
+ _("Your wallet file does not contain secrets, mostly just metadata. ") \
|
||||
+ _("It also contains your master public key that allows watching your addresses.") + '\n\n'\
|
||||
+ _("Note: If you enable this setting, you will need your hardware device to open your wallet.")
|
||||
|
||||
|
||||
class CosignWidget(QWidget):
|
||||
|
@ -152,7 +147,7 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
|
|||
self.raise_()
|
||||
self.refresh_gui() # Need for QT on MacOSX. Lame.
|
||||
|
||||
def run_and_get_wallet(self):
|
||||
def run_and_get_wallet(self, get_wallet_from_daemon):
|
||||
|
||||
vbox = QVBoxLayout()
|
||||
hbox = QHBoxLayout()
|
||||
|
@ -185,10 +180,15 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
|
|||
|
||||
def on_filename(filename):
|
||||
path = os.path.join(wallet_folder, filename)
|
||||
wallet_from_memory = get_wallet_from_daemon(path)
|
||||
try:
|
||||
self.storage = WalletStorage(path, manual_upgrades=True)
|
||||
if wallet_from_memory:
|
||||
self.storage = wallet_from_memory.storage
|
||||
else:
|
||||
self.storage = WalletStorage(path, manual_upgrades=True)
|
||||
self.next_button.setEnabled(True)
|
||||
except IOError:
|
||||
except BaseException:
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
self.storage = None
|
||||
self.next_button.setEnabled(False)
|
||||
if self.storage:
|
||||
|
@ -196,11 +196,21 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
|
|||
msg =_("This file does not exist.") + '\n' \
|
||||
+ _("Press 'Next' to create this wallet, or choose another file.")
|
||||
pw = False
|
||||
elif self.storage.file_exists() and self.storage.is_encrypted():
|
||||
msg = _("This file is encrypted.") + '\n' + _('Enter your password or choose another file.')
|
||||
pw = True
|
||||
elif not wallet_from_memory:
|
||||
if self.storage.is_encrypted_with_user_pw():
|
||||
msg = _("This file is encrypted with a password.") + '\n' \
|
||||
+ _('Enter your password or choose another file.')
|
||||
pw = True
|
||||
elif self.storage.is_encrypted_with_hw_device():
|
||||
msg = _("This file is encrypted using a hardware device.") + '\n' \
|
||||
+ _("Press 'Next' to choose device to decrypt.")
|
||||
pw = False
|
||||
else:
|
||||
msg = _("Press 'Next' to open this wallet.")
|
||||
pw = False
|
||||
else:
|
||||
msg = _("Press 'Next' to open this wallet.")
|
||||
msg = _("This file is already open in memory.") + "\n" \
|
||||
+ _("Press 'Next' to create/focus window.")
|
||||
pw = False
|
||||
else:
|
||||
msg = _('Cannot read file')
|
||||
|
@ -226,24 +236,48 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
|
|||
return
|
||||
if not self.storage.file_exists():
|
||||
break
|
||||
wallet_from_memory = get_wallet_from_daemon(self.storage.path)
|
||||
if wallet_from_memory:
|
||||
return wallet_from_memory
|
||||
if self.storage.file_exists() and self.storage.is_encrypted():
|
||||
password = self.pw_e.text()
|
||||
try:
|
||||
self.storage.decrypt(password)
|
||||
break
|
||||
except InvalidPassword as e:
|
||||
QMessageBox.information(None, _('Error'), str(e))
|
||||
continue
|
||||
except BaseException as e:
|
||||
traceback.print_exc(file=sys.stdout)
|
||||
QMessageBox.information(None, _('Error'), str(e))
|
||||
return
|
||||
if self.storage.is_encrypted_with_user_pw():
|
||||
password = self.pw_e.text()
|
||||
try:
|
||||
self.storage.decrypt(password)
|
||||
break
|
||||
except InvalidPassword as e:
|
||||
QMessageBox.information(None, _('Error'), str(e))
|
||||
continue
|
||||
except BaseException as e:
|
||||
traceback.print_exc(file=sys.stdout)
|
||||
QMessageBox.information(None, _('Error'), str(e))
|
||||
return
|
||||
elif self.storage.is_encrypted_with_hw_device():
|
||||
try:
|
||||
self.run('choose_hw_device', HWD_SETUP_DECRYPT_WALLET)
|
||||
except InvalidPassword as e:
|
||||
QMessageBox.information(
|
||||
None, _('Error'),
|
||||
_('Failed to decrypt using this hardware device.') + '\n' +
|
||||
_('If you use a passphrase, make sure it is correct.'))
|
||||
self.stack = []
|
||||
return self.run_and_get_wallet(get_wallet_from_daemon)
|
||||
except BaseException as e:
|
||||
traceback.print_exc(file=sys.stdout)
|
||||
QMessageBox.information(None, _('Error'), str(e))
|
||||
return
|
||||
if self.storage.is_past_initial_decryption():
|
||||
break
|
||||
else:
|
||||
return
|
||||
else:
|
||||
raise Exception('Unexpected encryption version')
|
||||
|
||||
path = self.storage.path
|
||||
if self.storage.requires_split():
|
||||
self.hide()
|
||||
msg = _("The wallet '%s' contains multiple accounts, which are no longer supported since Electrum-Zcash 2.7.\n\n"
|
||||
"Do you want to split your wallet into multiple files?"%path)
|
||||
msg = _("The wallet '{}' contains multiple accounts, which are no longer supported since Electrum-Zcash 2.7.\n\n"
|
||||
"Do you want to split your wallet into multiple files?").format(path)
|
||||
if not self.question(msg):
|
||||
return
|
||||
file_list = '\n'.join(self.storage.split_accounts())
|
||||
|
@ -261,10 +295,10 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
|
|||
action = self.storage.get_action()
|
||||
if action and action != 'new':
|
||||
self.hide()
|
||||
msg = _("The file '%s' contains an incompletely created wallet.\n"
|
||||
"Do you want to complete its creation now?") % path
|
||||
msg = _("The file '{}' contains an incompletely created wallet.\n"
|
||||
"Do you want to complete its creation now?").format(path)
|
||||
if not self.question(msg):
|
||||
if self.question(_("Do you want to delete '%s'?") % path):
|
||||
if self.question(_("Do you want to delete '{}'?").format(path)):
|
||||
os.remove(path)
|
||||
self.show_warning(_('The file was removed'))
|
||||
return
|
||||
|
@ -277,8 +311,6 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
|
|||
self.wallet = Wallet(self.storage)
|
||||
return self.wallet
|
||||
|
||||
|
||||
|
||||
def finished(self):
|
||||
"""Called in hardware client wrapper, in order to close popups."""
|
||||
return
|
||||
|
@ -316,7 +348,7 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
|
|||
if not result and raise_on_cancel:
|
||||
raise UserCancelled
|
||||
if result == 1:
|
||||
raise GoBack
|
||||
raise GoBack from None
|
||||
self.title.setVisible(False)
|
||||
self.back_button.setEnabled(False)
|
||||
self.next_button.setEnabled(False)
|
||||
|
@ -333,8 +365,9 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
|
|||
def remove_from_recently_open(self, filename):
|
||||
self.config.remove_from_recently_open(filename)
|
||||
|
||||
def text_input(self, title, message, is_valid):
|
||||
slayout = KeysLayout(parent=self, title=message, is_valid=is_valid)
|
||||
def text_input(self, title, message, is_valid, allow_multi=False):
|
||||
slayout = KeysLayout(parent=self, title=message, is_valid=is_valid,
|
||||
allow_multi=allow_multi)
|
||||
self.exec_layout(slayout, title, next_enabled=False)
|
||||
return slayout.get_text()
|
||||
|
||||
|
@ -344,8 +377,8 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
|
|||
return slayout.get_seed(), slayout.is_bip39, slayout.is_ext
|
||||
|
||||
@wizard_dialog
|
||||
def add_xpub_dialog(self, title, message, is_valid, run_next):
|
||||
return self.text_input(title, message, is_valid)
|
||||
def add_xpub_dialog(self, title, message, is_valid, run_next, allow_multi=False):
|
||||
return self.text_input(title, message, is_valid, allow_multi)
|
||||
|
||||
@wizard_dialog
|
||||
def add_cosigner_dialog(self, run_next, index, is_valid):
|
||||
|
@ -386,21 +419,29 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
|
|||
self.exec_layout(slayout)
|
||||
return slayout.is_ext
|
||||
|
||||
def pw_layout(self, msg, kind):
|
||||
playout = PasswordLayout(None, msg, kind, self.next_button)
|
||||
def pw_layout(self, msg, kind, force_disable_encrypt_cb):
|
||||
playout = PasswordLayout(None, msg, kind, self.next_button,
|
||||
force_disable_encrypt_cb=force_disable_encrypt_cb)
|
||||
playout.encrypt_cb.setChecked(True)
|
||||
self.exec_layout(playout.layout())
|
||||
return playout.new_password(), playout.encrypt_cb.isChecked()
|
||||
|
||||
@wizard_dialog
|
||||
def request_password(self, run_next):
|
||||
def request_password(self, run_next, force_disable_encrypt_cb=False):
|
||||
"""Request the user enter a new password and confirm it. Return
|
||||
the password or None for no password."""
|
||||
return self.pw_layout(MSG_ENTER_PASSWORD, PW_NEW)
|
||||
return self.pw_layout(MSG_ENTER_PASSWORD, PW_NEW, force_disable_encrypt_cb)
|
||||
|
||||
@wizard_dialog
|
||||
def request_storage_encryption(self, run_next):
|
||||
playout = PasswordLayoutForHW(None, MSG_HW_STORAGE_ENCRYPTION, PW_NEW, self.next_button)
|
||||
playout.encrypt_cb.setChecked(True)
|
||||
self.exec_layout(playout.layout())
|
||||
return playout.encrypt_cb.isChecked()
|
||||
|
||||
def show_restore(self, wallet, network):
|
||||
# FIXME: these messages are shown after the install wizard is
|
||||
# finished and the window closed. On MacOSX they appear parented
|
||||
# finished and the window closed. On macOS they appear parented
|
||||
# with a re-appeared ghost install wizard window...
|
||||
if network:
|
||||
def task():
|
||||
|
@ -437,7 +478,7 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
|
|||
self.accept_signal.emit()
|
||||
|
||||
def waiting_dialog(self, task, msg):
|
||||
self.please_wait.setText(MSG_GENERATING_WAIT)
|
||||
self.please_wait.setText(msg)
|
||||
self.refresh_gui()
|
||||
t = threading.Thread(target = task)
|
||||
t.start()
|
||||
|
@ -463,7 +504,8 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
|
|||
return clayout.selected_index()
|
||||
|
||||
@wizard_dialog
|
||||
def line_dialog(self, run_next, title, message, default, test, warning=''):
|
||||
def line_dialog(self, run_next, title, message, default, test, warning='',
|
||||
presets=()):
|
||||
vbox = QVBoxLayout()
|
||||
vbox.addWidget(WWLabel(message))
|
||||
line = QLineEdit()
|
||||
|
@ -473,6 +515,15 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
|
|||
line.textEdited.connect(f)
|
||||
vbox.addWidget(line)
|
||||
vbox.addWidget(WWLabel(warning))
|
||||
|
||||
for preset in presets:
|
||||
button = QPushButton(preset[0])
|
||||
button.clicked.connect(lambda __, text=preset[1]: line.setText(text))
|
||||
button.setMaximumWidth(150)
|
||||
hbox = QHBoxLayout()
|
||||
hbox.addWidget(button, Qt.AlignCenter)
|
||||
vbox.addLayout(hbox)
|
||||
|
||||
self.exec_layout(vbox, title, next_enabled=test(default))
|
||||
return ' '.join(line.text().split())
|
||||
|
||||
|
|
|
@ -23,10 +23,11 @@
|
|||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from .util import *
|
||||
from electrum_zcash.i18n import _
|
||||
from electrum_zcash.util import format_time
|
||||
|
||||
from .util import *
|
||||
|
||||
|
||||
class InvoiceList(MyTreeWidget):
|
||||
filter_columns = [0, 1, 2, 3] # Date, Requestor, Description, Amount
|
||||
|
@ -47,7 +48,7 @@ class InvoiceList(MyTreeWidget):
|
|||
exp = pr.get_expiration_date()
|
||||
date_str = format_time(exp) if exp else _('Never')
|
||||
item = QTreeWidgetItem([date_str, requestor, pr.memo, self.parent.format_amount(pr.get_amount(), whitespaces=True), pr_tooltips.get(status,'')])
|
||||
item.setIcon(4, QIcon(pr_icons.get(status)))
|
||||
item.setIcon(4, self.icon_cache.get(pr_icons.get(status)))
|
||||
item.setData(0, Qt.UserRole, key)
|
||||
item.setFont(1, QFont(MONOSPACE_FONT))
|
||||
item.setFont(3, QFont(MONOSPACE_FONT))
|
||||
|
@ -57,12 +58,10 @@ class InvoiceList(MyTreeWidget):
|
|||
self.parent.invoices_label.setVisible(len(inv_list))
|
||||
|
||||
def import_invoices(self):
|
||||
wallet_folder = self.parent.get_wallet_folder()
|
||||
filename, __ = QFileDialog.getOpenFileName(self.parent, "Select your wallet file", wallet_folder)
|
||||
if not filename:
|
||||
return
|
||||
self.parent.invoices.import_file(filename)
|
||||
self.on_update()
|
||||
import_meta_gui(self.parent, _('invoices'), self.parent.invoices.import_file, self.on_update)
|
||||
|
||||
def export_invoices(self):
|
||||
export_meta_gui(self.parent, _('invoices'), self.parent.invoices.export_file)
|
||||
|
||||
def create_menu(self, position):
|
||||
menu = QMenu()
|
||||
|
@ -76,7 +75,7 @@ class InvoiceList(MyTreeWidget):
|
|||
pr = self.parent.invoices.get(key)
|
||||
status = self.parent.invoices.get_status(key)
|
||||
if column_data:
|
||||
menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(column_data))
|
||||
menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data))
|
||||
menu.addAction(_("Details"), lambda: self.parent.show_invoice(key))
|
||||
if status == PR_UNPAID:
|
||||
menu.addAction(_("Pay Now"), lambda: self.parent.do_pay_invoice(key))
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -31,8 +31,9 @@ from PyQt5.QtWidgets import *
|
|||
import PyQt5.QtCore as QtCore
|
||||
|
||||
from electrum_zcash.i18n import _
|
||||
from electrum_zcash.bitcoin import NetworkConstants
|
||||
from electrum_zcash import constants
|
||||
from electrum_zcash.util import print_error
|
||||
from electrum_zcash.network import serialize_server, deserialize_server
|
||||
|
||||
from .util import *
|
||||
|
||||
|
@ -145,7 +146,7 @@ class ServerListWidget(QTreeWidget):
|
|||
menu.exec_(self.viewport().mapToGlobal(position))
|
||||
|
||||
def set_server(self, s):
|
||||
host, port, protocol = s.split(':')
|
||||
host, port, protocol = deserialize_server(s)
|
||||
self.parent.server_host.setText(host)
|
||||
self.parent.server_port.setText(port)
|
||||
self.parent.set_server()
|
||||
|
@ -170,7 +171,7 @@ class ServerListWidget(QTreeWidget):
|
|||
port = d.get(protocol)
|
||||
if port:
|
||||
x = QTreeWidgetItem([_host, port])
|
||||
server = _host+':'+port+':'+protocol
|
||||
server = serialize_server(_host, port, protocol)
|
||||
x.setData(1, Qt.UserRole, server)
|
||||
self.addTopLevelItem(x)
|
||||
|
||||
|
@ -193,8 +194,8 @@ class NetworkChoiceLayout(object):
|
|||
proxy_tab = QWidget()
|
||||
blockchain_tab = QWidget()
|
||||
tabs.addTab(blockchain_tab, _('Overview'))
|
||||
tabs.addTab(proxy_tab, _('Connection'))
|
||||
tabs.addTab(server_tab, _('Server'))
|
||||
tabs.addTab(proxy_tab, _('Proxy'))
|
||||
|
||||
# server tab
|
||||
grid = QGridLayout(server_tab)
|
||||
|
@ -204,13 +205,11 @@ class NetworkChoiceLayout(object):
|
|||
self.server_host.setFixedWidth(200)
|
||||
self.server_port = QLineEdit()
|
||||
self.server_port.setFixedWidth(60)
|
||||
self.ssl_cb = QCheckBox(_('Use SSL'))
|
||||
self.autoconnect_cb = QCheckBox(_('Select server automatically'))
|
||||
self.autoconnect_cb.setEnabled(self.config.is_modifiable('auto_connect'))
|
||||
|
||||
self.server_host.editingFinished.connect(self.set_server)
|
||||
self.server_port.editingFinished.connect(self.set_server)
|
||||
self.ssl_cb.clicked.connect(self.change_protocol)
|
||||
self.autoconnect_cb.clicked.connect(self.set_server)
|
||||
self.autoconnect_cb.clicked.connect(self.update)
|
||||
|
||||
|
@ -269,8 +268,6 @@ class NetworkChoiceLayout(object):
|
|||
self.tor_cb.hide()
|
||||
self.tor_cb.clicked.connect(self.use_tor_proxy)
|
||||
|
||||
grid.addWidget(self.ssl_cb, 0, 0, 1, 3)
|
||||
grid.addWidget(HelpButton(_('SSL is used to authenticate and encrypt your connections with Electrum-Zcash servers.')), 0, 4)
|
||||
grid.addWidget(self.tor_cb, 1, 0, 1, 3)
|
||||
grid.addWidget(self.proxy_cb, 2, 0, 1, 3)
|
||||
grid.addWidget(HelpButton(_('Proxy settings apply to all connections: with Electrum-Zcash servers, but also with third-party services.')), 2, 4)
|
||||
|
@ -334,14 +331,13 @@ class NetworkChoiceLayout(object):
|
|||
self.server_port.setEnabled(enabled)
|
||||
self.servers_list.setEnabled(enabled)
|
||||
else:
|
||||
for w in [self.autoconnect_cb, self.server_host, self.server_port, self.ssl_cb, self.servers_list]:
|
||||
for w in [self.autoconnect_cb, self.server_host, self.server_port, self.servers_list]:
|
||||
w.setEnabled(False)
|
||||
|
||||
def update(self):
|
||||
host, port, protocol, proxy_config, auto_connect = self.network.get_parameters()
|
||||
self.server_host.setText(host)
|
||||
self.server_port.setText(port)
|
||||
self.ssl_cb.setChecked(protocol=='s')
|
||||
self.autoconnect_cb.setChecked(auto_connect)
|
||||
|
||||
host = self.network.interface.host if self.network.interface else _('None')
|
||||
|
@ -397,7 +393,7 @@ class NetworkChoiceLayout(object):
|
|||
def change_protocol(self, use_ssl):
|
||||
p = 's' if use_ssl else 't'
|
||||
host = self.server_host.text()
|
||||
pp = self.servers.get(host, NetworkConstants.DEFAULT_PORTS)
|
||||
pp = self.servers.get(host, constants.net.DEFAULT_PORTS)
|
||||
if p not in pp.keys():
|
||||
p = list(pp.keys())[0]
|
||||
port = pp[p]
|
||||
|
@ -413,7 +409,7 @@ class NetworkChoiceLayout(object):
|
|||
def follow_server(self, server):
|
||||
self.network.switch_to_interface(server)
|
||||
host, port, protocol, proxy, auto_connect = self.network.get_parameters()
|
||||
host, port, protocol = server.split(':')
|
||||
host, port, protocol = deserialize_server(server)
|
||||
self.network.set_parameters(host, port, protocol, proxy, auto_connect)
|
||||
self.update()
|
||||
|
||||
|
@ -422,7 +418,7 @@ class NetworkChoiceLayout(object):
|
|||
self.change_server(str(x.text(0)), self.protocol)
|
||||
|
||||
def change_server(self, host, protocol):
|
||||
pp = self.servers.get(host, NetworkConstants.DEFAULT_PORTS)
|
||||
pp = self.servers.get(host, constants.net.DEFAULT_PORTS)
|
||||
if protocol and protocol not in protocol_letters:
|
||||
protocol = None
|
||||
if protocol:
|
||||
|
@ -438,7 +434,6 @@ class NetworkChoiceLayout(object):
|
|||
port = pp.get(protocol)
|
||||
self.server_host.setText(host)
|
||||
self.server_port.setText(port)
|
||||
self.ssl_cb.setChecked(protocol=='s')
|
||||
|
||||
def accept(self):
|
||||
pass
|
||||
|
@ -447,7 +442,6 @@ class NetworkChoiceLayout(object):
|
|||
host, port, protocol, proxy, auto_connect = self.network.get_parameters()
|
||||
host = str(self.server_host.text())
|
||||
port = str(self.server_port.text())
|
||||
protocol = 's' if self.ssl_cb.isChecked() else 't'
|
||||
auto_connect = self.autoconnect_cb.isChecked()
|
||||
self.network.set_parameters(host, port, protocol, proxy, auto_connect)
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ class PasswordLayout(object):
|
|||
|
||||
titles = [_("Enter Password"), _("Change Password"), _("Enter Passphrase")]
|
||||
|
||||
def __init__(self, wallet, msg, kind, OK_button):
|
||||
def __init__(self, wallet, msg, kind, OK_button, force_disable_encrypt_cb=False):
|
||||
self.wallet = wallet
|
||||
|
||||
self.pw = QLineEdit()
|
||||
|
@ -127,7 +127,8 @@ class PasswordLayout(object):
|
|||
def enable_OK():
|
||||
ok = self.new_pw.text() == self.conf_pw.text()
|
||||
OK_button.setEnabled(ok)
|
||||
self.encrypt_cb.setEnabled(ok and bool(self.new_pw.text()))
|
||||
self.encrypt_cb.setEnabled(ok and bool(self.new_pw.text())
|
||||
and not force_disable_encrypt_cb)
|
||||
self.new_pw.textChanged.connect(enable_OK)
|
||||
self.conf_pw.textChanged.connect(enable_OK)
|
||||
|
||||
|
@ -164,11 +165,84 @@ class PasswordLayout(object):
|
|||
return pw
|
||||
|
||||
|
||||
class ChangePasswordDialog(WindowModalDialog):
|
||||
class PasswordLayoutForHW(object):
|
||||
|
||||
def __init__(self, wallet, msg, kind, OK_button):
|
||||
self.wallet = wallet
|
||||
|
||||
self.kind = kind
|
||||
self.OK_button = OK_button
|
||||
|
||||
vbox = QVBoxLayout()
|
||||
label = QLabel(msg + "\n")
|
||||
label.setWordWrap(True)
|
||||
|
||||
grid = QGridLayout()
|
||||
grid.setSpacing(8)
|
||||
grid.setColumnMinimumWidth(0, 150)
|
||||
grid.setColumnMinimumWidth(1, 100)
|
||||
grid.setColumnStretch(1,1)
|
||||
|
||||
logo_grid = QGridLayout()
|
||||
logo_grid.setSpacing(8)
|
||||
logo_grid.setColumnMinimumWidth(0, 70)
|
||||
logo_grid.setColumnStretch(1,1)
|
||||
|
||||
logo = QLabel()
|
||||
logo.setAlignment(Qt.AlignCenter)
|
||||
|
||||
logo_grid.addWidget(logo, 0, 0)
|
||||
logo_grid.addWidget(label, 0, 1, 1, 2)
|
||||
vbox.addLayout(logo_grid)
|
||||
|
||||
if wallet and wallet.has_storage_encryption():
|
||||
lockfile = ":icons/lock.png"
|
||||
else:
|
||||
lockfile = ":icons/unlock.png"
|
||||
logo.setPixmap(QPixmap(lockfile).scaledToWidth(36))
|
||||
|
||||
vbox.addLayout(grid)
|
||||
|
||||
self.encrypt_cb = QCheckBox(_('Encrypt wallet file'))
|
||||
grid.addWidget(self.encrypt_cb, 1, 0, 1, 2)
|
||||
|
||||
self.vbox = vbox
|
||||
|
||||
def title(self):
|
||||
return _("Toggle Encryption")
|
||||
|
||||
def layout(self):
|
||||
return self.vbox
|
||||
|
||||
|
||||
class ChangePasswordDialogBase(WindowModalDialog):
|
||||
|
||||
def __init__(self, parent, wallet):
|
||||
WindowModalDialog.__init__(self, parent)
|
||||
is_encrypted = wallet.storage.is_encrypted()
|
||||
is_encrypted = wallet.has_storage_encryption()
|
||||
OK_button = OkButton(self)
|
||||
|
||||
self.create_password_layout(wallet, is_encrypted, OK_button)
|
||||
|
||||
self.setWindowTitle(self.playout.title())
|
||||
vbox = QVBoxLayout(self)
|
||||
vbox.addLayout(self.playout.layout())
|
||||
vbox.addStretch(1)
|
||||
vbox.addLayout(Buttons(CancelButton(self), OK_button))
|
||||
self.playout.encrypt_cb.setChecked(is_encrypted)
|
||||
|
||||
def create_password_layout(self, wallet, is_encrypted, OK_button):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class ChangePasswordDialogForSW(ChangePasswordDialogBase):
|
||||
|
||||
def __init__(self, parent, wallet):
|
||||
ChangePasswordDialogBase.__init__(self, parent, wallet)
|
||||
if not wallet.has_password():
|
||||
self.playout.encrypt_cb.setChecked(True)
|
||||
|
||||
def create_password_layout(self, wallet, is_encrypted, OK_button):
|
||||
if not wallet.has_password():
|
||||
msg = _('Your wallet is not protected.')
|
||||
msg += ' ' + _('Use this dialog to add a password to your wallet.')
|
||||
|
@ -178,14 +252,9 @@ class ChangePasswordDialog(WindowModalDialog):
|
|||
else:
|
||||
msg = _('Your wallet is password protected and encrypted.')
|
||||
msg += ' ' + _('Use this dialog to change your password.')
|
||||
OK_button = OkButton(self)
|
||||
self.playout = PasswordLayout(wallet, msg, PW_CHANGE, OK_button)
|
||||
self.setWindowTitle(self.playout.title())
|
||||
vbox = QVBoxLayout(self)
|
||||
vbox.addLayout(self.playout.layout())
|
||||
vbox.addStretch(1)
|
||||
vbox.addLayout(Buttons(CancelButton(self), OK_button))
|
||||
self.playout.encrypt_cb.setChecked(is_encrypted or not wallet.has_password())
|
||||
self.playout = PasswordLayout(
|
||||
wallet, msg, PW_CHANGE, OK_button,
|
||||
force_disable_encrypt_cb=not wallet.can_have_keystore_encryption())
|
||||
|
||||
def run(self):
|
||||
if not self.exec_():
|
||||
|
@ -193,6 +262,26 @@ class ChangePasswordDialog(WindowModalDialog):
|
|||
return True, self.playout.old_password(), self.playout.new_password(), self.playout.encrypt_cb.isChecked()
|
||||
|
||||
|
||||
class ChangePasswordDialogForHW(ChangePasswordDialogBase):
|
||||
|
||||
def __init__(self, parent, wallet):
|
||||
ChangePasswordDialogBase.__init__(self, parent, wallet)
|
||||
|
||||
def create_password_layout(self, wallet, is_encrypted, OK_button):
|
||||
if not is_encrypted:
|
||||
msg = _('Your wallet file is NOT encrypted.')
|
||||
else:
|
||||
msg = _('Your wallet file is encrypted.')
|
||||
msg += '\n' + _('Note: If you enable this setting, you will need your hardware device to open your wallet.')
|
||||
msg += '\n' + _('Use this dialog to toggle encryption.')
|
||||
self.playout = PasswordLayoutForHW(wallet, msg, PW_CHANGE, OK_button)
|
||||
|
||||
def run(self):
|
||||
if not self.exec_():
|
||||
return False, None
|
||||
return True, self.playout.encrypt_cb.isChecked()
|
||||
|
||||
|
||||
class PasswordDialog(WindowModalDialog):
|
||||
|
||||
def __init__(self, parent=None, msg=None):
|
||||
|
|
|
@ -23,15 +23,16 @@
|
|||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from PyQt5.QtCore import *
|
||||
from PyQt5.QtGui import *
|
||||
from PyQt5.QtWidgets import QCompleter, QPlainTextEdit
|
||||
from .qrtextedit import ScanQRTextEdit
|
||||
|
||||
from PyQt5.QtWidgets import QLineEdit
|
||||
import re
|
||||
from decimal import Decimal
|
||||
from electrum_zcash import bitcoin
|
||||
|
||||
from electrum_zcash import bitcoin
|
||||
from electrum_zcash.util import bfh
|
||||
|
||||
from .qrtextedit import ScanQRTextEdit
|
||||
from .completion_text_edit import CompletionTextEdit
|
||||
from . import util
|
||||
|
||||
RE_ADDRESS = '[1-9A-HJ-NP-Za-km-z]{26,}'
|
||||
|
@ -40,9 +41,10 @@ RE_ALIAS = '(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>'
|
|||
frozen_style = "QWidget { background-color:none; border:none;}"
|
||||
normal_style = "QPlainTextEdit { }"
|
||||
|
||||
class PayToEdit(ScanQRTextEdit):
|
||||
class PayToEdit(CompletionTextEdit, ScanQRTextEdit):
|
||||
|
||||
def __init__(self, win):
|
||||
CompletionTextEdit.__init__(self)
|
||||
ScanQRTextEdit.__init__(self)
|
||||
self.win = win
|
||||
self.amount_edit = win.amount_e
|
||||
|
@ -93,9 +95,12 @@ class PayToEdit(ScanQRTextEdit):
|
|||
for word in x.split():
|
||||
if word[0:3] == 'OP_':
|
||||
assert word in opcodes.lookup
|
||||
script += chr(opcodes.lookup[word])
|
||||
opcode_int = opcodes.lookup[word]
|
||||
assert opcode_int < 256 # opcode is single-byte
|
||||
script += bitcoin.int_to_hex(opcode_int)
|
||||
else:
|
||||
script += push_script(word).decode('hex')
|
||||
bfh(word) # to test it is hex data
|
||||
script += push_script(word)
|
||||
return script
|
||||
|
||||
def parse_amount(self, x):
|
||||
|
@ -186,78 +191,15 @@ class PayToEdit(ScanQRTextEdit):
|
|||
self.update_size()
|
||||
|
||||
def update_size(self):
|
||||
lineHeight = QFontMetrics(self.document().defaultFont()).height()
|
||||
docHeight = self.document().size().height()
|
||||
h = docHeight * lineHeight + 11
|
||||
lineEditHeight = QLineEdit().sizeHint().height()
|
||||
lineHeight = self.fontMetrics().height()
|
||||
h = lineEditHeight + lineHeight * (docHeight - 1)
|
||||
if self.heightMin <= h <= self.heightMax:
|
||||
self.setMinimumHeight(h)
|
||||
self.setMaximumHeight(h)
|
||||
self.verticalScrollBar().hide()
|
||||
|
||||
|
||||
def setCompleter(self, completer):
|
||||
self.c = completer
|
||||
self.c.setWidget(self)
|
||||
self.c.setCompletionMode(QCompleter.PopupCompletion)
|
||||
self.c.activated.connect(self.insertCompletion)
|
||||
|
||||
|
||||
def insertCompletion(self, completion):
|
||||
if self.c.widget() != self:
|
||||
return
|
||||
tc = self.textCursor()
|
||||
extra = len(completion) - len(self.c.completionPrefix())
|
||||
tc.movePosition(QTextCursor.Left)
|
||||
tc.movePosition(QTextCursor.EndOfWord)
|
||||
tc.insertText(completion[-extra:])
|
||||
self.setTextCursor(tc)
|
||||
|
||||
|
||||
def textUnderCursor(self):
|
||||
tc = self.textCursor()
|
||||
tc.select(QTextCursor.WordUnderCursor)
|
||||
return tc.selectedText()
|
||||
|
||||
|
||||
def keyPressEvent(self, e):
|
||||
if self.isReadOnly():
|
||||
return
|
||||
|
||||
if self.c.popup().isVisible():
|
||||
if e.key() in [Qt.Key_Enter, Qt.Key_Return]:
|
||||
e.ignore()
|
||||
return
|
||||
|
||||
if e.key() in [Qt.Key_Tab]:
|
||||
e.ignore()
|
||||
return
|
||||
|
||||
if e.key() in [Qt.Key_Down, Qt.Key_Up] and not self.is_multiline():
|
||||
e.ignore()
|
||||
return
|
||||
|
||||
QPlainTextEdit.keyPressEvent(self, e)
|
||||
|
||||
ctrlOrShift = e.modifiers() and (Qt.ControlModifier or Qt.ShiftModifier)
|
||||
if self.c is None or (ctrlOrShift and not e.text()):
|
||||
return
|
||||
|
||||
eow = "~!@#$%^&*()_+{}|:\"<>?,./;'[]\\-="
|
||||
hasModifier = (e.modifiers() != Qt.NoModifier) and not ctrlOrShift
|
||||
completionPrefix = self.textUnderCursor()
|
||||
|
||||
if hasModifier or not e.text() or len(completionPrefix) < 1 or eow.find(e.text()[-1]) >= 0:
|
||||
self.c.popup().hide()
|
||||
return
|
||||
|
||||
if completionPrefix != self.c.completionPrefix():
|
||||
self.c.setCompletionPrefix(completionPrefix)
|
||||
self.c.popup().setCurrentIndex(self.c.completionModel().index(0, 0))
|
||||
|
||||
cr = self.cursorRect()
|
||||
cr.setWidth(self.c.popup().sizeHintForColumn(0) + self.c.popup().verticalScrollBar().sizeHint().width())
|
||||
self.c.complete(cr)
|
||||
|
||||
def qr_input(self):
|
||||
data = super(PayToEdit,self).qr_input()
|
||||
if data.startswith("zcash:"):
|
||||
|
|
|
@ -33,8 +33,9 @@ class ShowQRTextEdit(ButtonsTextEdit):
|
|||
|
||||
class ScanQRTextEdit(ButtonsTextEdit, MessageBoxMixin):
|
||||
|
||||
def __init__(self, text=""):
|
||||
def __init__(self, text="", allow_multi=False):
|
||||
ButtonsTextEdit.__init__(self, text)
|
||||
self.allow_multi = allow_multi
|
||||
self.setReadOnly(0)
|
||||
self.addButton(":icons/file.png", self.file_input, _("Read file"))
|
||||
icon = ":icons/qrcode.png"
|
||||
|
@ -45,9 +46,13 @@ class ScanQRTextEdit(ButtonsTextEdit, MessageBoxMixin):
|
|||
fileName, __ = QFileDialog.getOpenFileName(self, 'select file')
|
||||
if not fileName:
|
||||
return
|
||||
with open(fileName, "r") as f:
|
||||
data = f.read()
|
||||
self.setText(data)
|
||||
try:
|
||||
with open(fileName, "r") as f:
|
||||
data = f.read()
|
||||
except BaseException as e:
|
||||
self.show_error(_('Error opening file') + ':\n' + str(e))
|
||||
else:
|
||||
self.setText(data)
|
||||
|
||||
def qr_input(self):
|
||||
from electrum_zcash import qrscanner, get_config
|
||||
|
@ -58,7 +63,11 @@ class ScanQRTextEdit(ButtonsTextEdit, MessageBoxMixin):
|
|||
data = ''
|
||||
if not data:
|
||||
data = ''
|
||||
self.setText(data)
|
||||
if self.allow_multi:
|
||||
new_text = self.text() + data + '\n'
|
||||
else:
|
||||
new_text = data
|
||||
self.setText(new_text)
|
||||
return data
|
||||
|
||||
def contextMenuEvent(self, e):
|
||||
|
|
|
@ -98,10 +98,10 @@ class RequestList(MyTreeWidget):
|
|||
amount_str = self.parent.format_amount(amount) if amount else ""
|
||||
item = QTreeWidgetItem([date, address, '', message, amount_str, pr_tooltips.get(status,'')])
|
||||
if signature is not None:
|
||||
item.setIcon(2, QIcon(":icons/seal.png"))
|
||||
item.setIcon(2, self.icon_cache.get(":icons/seal.png"))
|
||||
item.setToolTip(2, 'signed by '+ requestor)
|
||||
if status is not PR_UNKNOWN:
|
||||
item.setIcon(6, QIcon(pr_icons.get(status)))
|
||||
item.setIcon(6, self.icon_cache.get(pr_icons.get(status)))
|
||||
self.addTopLevelItem(item)
|
||||
|
||||
|
||||
|
@ -115,7 +115,7 @@ class RequestList(MyTreeWidget):
|
|||
column_title = self.headerItem().text(column)
|
||||
column_data = item.text(column)
|
||||
menu = QMenu(self)
|
||||
menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(column_data))
|
||||
menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data))
|
||||
menu.addAction(_("Copy URI"), lambda: self.parent.view_and_paste('URI', '', self.parent.get_request_URI(addr)))
|
||||
menu.addAction(_("Save as BIP70 file"), lambda: self.parent.export_payment_request(addr))
|
||||
menu.addAction(_("Delete"), lambda: self.parent.delete_payment_request(addr))
|
||||
|
|
|
@ -23,13 +23,13 @@
|
|||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from PyQt5.QtGui import *
|
||||
from PyQt5.QtCore import *
|
||||
from PyQt5.QtWidgets import *
|
||||
from electrum_zcash.i18n import _
|
||||
from electrum_zcash.mnemonic import Mnemonic
|
||||
import electrum_zcash.old_mnemonic
|
||||
|
||||
from .util import *
|
||||
from .qrtextedit import ShowQRTextEdit, ScanQRTextEdit
|
||||
from .completion_text_edit import CompletionTextEdit
|
||||
|
||||
|
||||
def seed_warning_msg(seed):
|
||||
|
@ -92,15 +92,16 @@ class SeedLayout(QVBoxLayout):
|
|||
self.options = options
|
||||
if title:
|
||||
self.addWidget(WWLabel(title))
|
||||
self.seed_e = CompletionTextEdit()
|
||||
if seed:
|
||||
self.seed_e = ShowQRTextEdit()
|
||||
self.seed_e.setText(seed)
|
||||
else:
|
||||
self.seed_e = ScanQRTextEdit()
|
||||
self.seed_e.setTabChangesFocus(True)
|
||||
self.is_seed = is_seed
|
||||
self.saved_is_seed = self.is_seed
|
||||
self.seed_e.textChanged.connect(self.on_edit)
|
||||
self.initialize_completer()
|
||||
|
||||
self.seed_e.setMaximumHeight(75)
|
||||
hbox = QHBoxLayout()
|
||||
if icon:
|
||||
|
@ -133,6 +134,14 @@ class SeedLayout(QVBoxLayout):
|
|||
self.seed_warning.setText(seed_warning_msg(seed))
|
||||
self.addWidget(self.seed_warning)
|
||||
|
||||
def initialize_completer(self):
|
||||
english_list = Mnemonic('en').wordlist
|
||||
old_list = electrum_zcash.old_mnemonic.words
|
||||
self.wordlist = english_list + list(set(old_list) - set(english_list)) #concat both lists
|
||||
self.wordlist.sort()
|
||||
self.completer = QCompleter(self.wordlist)
|
||||
self.seed_e.set_completer(self.completer)
|
||||
|
||||
def get_seed(self):
|
||||
text = self.seed_e.text()
|
||||
return ' '.join(text.split())
|
||||
|
@ -152,13 +161,19 @@ class SeedLayout(QVBoxLayout):
|
|||
self.seed_type_label.setText(label)
|
||||
self.parent.next_button.setEnabled(b)
|
||||
|
||||
# to account for bip39 seeds
|
||||
for word in self.get_seed().split(" ")[:-1]:
|
||||
if word not in self.wordlist:
|
||||
self.seed_e.disable_suggestions()
|
||||
return
|
||||
self.seed_e.enable_suggestions()
|
||||
|
||||
class KeysLayout(QVBoxLayout):
|
||||
def __init__(self, parent=None, title=None, is_valid=None):
|
||||
def __init__(self, parent=None, title=None, is_valid=None, allow_multi=False):
|
||||
QVBoxLayout.__init__(self)
|
||||
self.parent = parent
|
||||
self.is_valid = is_valid
|
||||
self.text_e = ScanQRTextEdit()
|
||||
self.text_e = ScanQRTextEdit(allow_multi=allow_multi)
|
||||
self.text_e.textChanged.connect(self.on_edit)
|
||||
self.addWidget(WWLabel(title))
|
||||
self.addWidget(self.text_e)
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
import copy
|
||||
import datetime
|
||||
import json
|
||||
import traceback
|
||||
|
||||
from PyQt5.QtCore import *
|
||||
from PyQt5.QtGui import *
|
||||
|
@ -33,16 +34,27 @@ from PyQt5.QtWidgets import *
|
|||
from electrum_zcash.bitcoin import base_encode
|
||||
from electrum_zcash.i18n import _
|
||||
from electrum_zcash.plugins import run_hook
|
||||
from electrum_zcash import simple_config
|
||||
|
||||
from electrum_zcash.util import bfh
|
||||
from electrum_zcash.wallet import AddTransactionException
|
||||
from electrum_zcash.transaction import SerializationError
|
||||
|
||||
from .util import *
|
||||
|
||||
dialogs = [] # Otherwise python randomly garbage collects the dialogs...
|
||||
|
||||
|
||||
def show_transaction(tx, parent, desc=None, prompt_if_unsaved=False):
|
||||
d = TxDialog(tx, parent, desc, prompt_if_unsaved)
|
||||
dialogs.append(d)
|
||||
d.show()
|
||||
try:
|
||||
d = TxDialog(tx, parent, desc, prompt_if_unsaved)
|
||||
except SerializationError as e:
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
parent.show_critical(_("Electrum-Zcash was unable to deserialize the transaction:") + "\n" + str(e))
|
||||
else:
|
||||
dialogs.append(d)
|
||||
d.show()
|
||||
|
||||
|
||||
class TxDialog(QDialog, MessageBoxMixin):
|
||||
|
||||
|
@ -56,7 +68,10 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||
# e.g. the FX plugin. If this happens during or after a long
|
||||
# sign operation the signatures are lost.
|
||||
self.tx = copy.deepcopy(tx)
|
||||
self.tx.deserialize()
|
||||
try:
|
||||
self.tx.deserialize()
|
||||
except BaseException as e:
|
||||
raise SerializationError(e)
|
||||
self.main_window = parent
|
||||
self.wallet = parent.wallet
|
||||
self.prompt_if_unsaved = prompt_if_unsaved
|
||||
|
@ -98,8 +113,17 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||
self.broadcast_button = b = QPushButton(_("Broadcast"))
|
||||
b.clicked.connect(self.do_broadcast)
|
||||
|
||||
self.save_button = b = QPushButton(_("Save"))
|
||||
b.clicked.connect(self.save)
|
||||
self.save_button = QPushButton(_("Save"))
|
||||
save_button_disabled = not tx.is_complete()
|
||||
self.save_button.setDisabled(save_button_disabled)
|
||||
if save_button_disabled:
|
||||
self.save_button.setToolTip(_("Please sign this transaction in order to save it"))
|
||||
else:
|
||||
self.save_button.setToolTip("")
|
||||
self.save_button.clicked.connect(self.save)
|
||||
|
||||
self.export_button = b = QPushButton(_("Export"))
|
||||
b.clicked.connect(self.export)
|
||||
|
||||
self.cancel_button = b = QPushButton(_("Close"))
|
||||
b.clicked.connect(self.close)
|
||||
|
@ -112,9 +136,9 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||
self.copy_button = CopyButton(lambda: str(self.tx), parent.app)
|
||||
|
||||
# Action buttons
|
||||
self.buttons = [self.sign_button, self.broadcast_button, self.cancel_button]
|
||||
self.buttons = [self.sign_button, self.broadcast_button, self.save_button, self.cancel_button]
|
||||
# Transaction sharing buttons
|
||||
self.sharing_buttons = [self.copy_button, self.qr_button, self.save_button]
|
||||
self.sharing_buttons = [self.copy_button, self.qr_button, self.export_button]
|
||||
|
||||
run_hook('transaction_dialog', self)
|
||||
|
||||
|
@ -136,11 +160,14 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||
|
||||
def closeEvent(self, event):
|
||||
if (self.prompt_if_unsaved and not self.saved
|
||||
and not self.question(_('This transaction is not saved. Close anyway?'), title=_("Warning"))):
|
||||
and not self.question(_('This transaction is not saved. Close anyway?'), title=_("Warning"))):
|
||||
event.ignore()
|
||||
else:
|
||||
event.accept()
|
||||
dialogs.remove(self)
|
||||
try:
|
||||
dialogs.remove(self)
|
||||
except ValueError:
|
||||
pass # was not in list already
|
||||
|
||||
def show_qr(self):
|
||||
text = bfh(str(self.tx))
|
||||
|
@ -152,9 +179,12 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||
|
||||
def sign(self):
|
||||
def sign_done(success):
|
||||
if success:
|
||||
# note: with segwit we could save partially signed tx, because they have a txid
|
||||
if self.tx.is_complete():
|
||||
self.prompt_if_unsaved = True
|
||||
self.saved = False
|
||||
self.save_button.setDisabled(False)
|
||||
self.save_button.setToolTip("")
|
||||
self.update()
|
||||
self.main_window.pop_top_level_window(self)
|
||||
|
||||
|
@ -163,12 +193,18 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||
self.main_window.sign_tx(self.tx, sign_done)
|
||||
|
||||
def save(self):
|
||||
name = 'signed_%s.txn' % (self.tx.hash()[0:8]) if self.tx.is_complete() else 'unsigned.txn'
|
||||
if self.main_window.save_transaction_into_wallet(self.tx):
|
||||
self.save_button.setDisabled(True)
|
||||
self.saved = True
|
||||
|
||||
|
||||
def export(self):
|
||||
name = 'signed_%s.txn' % (self.tx.txid()[0:8]) if self.tx.is_complete() else 'unsigned.txn'
|
||||
fileName = self.main_window.getSaveFileName(_("Select where to save your signed transaction"), name, "*.txn")
|
||||
if fileName:
|
||||
with open(fileName, "w+") as f:
|
||||
f.write(json.dumps(self.tx.as_dict(), indent=4) + '\n')
|
||||
self.show_message(_("Transaction saved successfully"))
|
||||
self.show_message(_("Transaction exported successfully"))
|
||||
self.saved = True
|
||||
|
||||
def update(self):
|
||||
|
@ -191,11 +227,11 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||
|
||||
if timestamp:
|
||||
time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
|
||||
self.date_label.setText(_("Date: %s")%time_str)
|
||||
self.date_label.setText(_("Date: {}").format(time_str))
|
||||
self.date_label.show()
|
||||
elif exp_n:
|
||||
text = '%d blocks'%(exp_n) if exp_n > 0 else _('unknown (low fee)')
|
||||
self.date_label.setText(_('Expected confirmation time') + ': ' + text)
|
||||
text = '%.2f MB'%(exp_n/1000000)
|
||||
self.date_label.setText(_('Position in mempool') + ': ' + text + ' ' + _('from tip'))
|
||||
self.date_label.show()
|
||||
else:
|
||||
self.date_label.hide()
|
||||
|
@ -206,9 +242,13 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||
else:
|
||||
amount_str = _("Amount sent:") + ' %s'% format_amount(-amount) + ' ' + base_unit
|
||||
size_str = _("Size:") + ' %d bytes'% size
|
||||
fee_str = _("Fee") + ': %s'% (format_amount(fee) + ' ' + base_unit if fee is not None else _('unknown'))
|
||||
fee_str = _("Fee") + ': %s' % (format_amount(fee) + ' ' + base_unit if fee is not None else _('unknown'))
|
||||
if fee is not None:
|
||||
fee_str += ' ( %s ) '% self.main_window.format_fee_rate(fee/size*1000)
|
||||
fee_rate = fee/size*1000
|
||||
fee_str += ' ( %s ) ' % self.main_window.format_fee_rate(fee_rate)
|
||||
confirm_rate = simple_config.FEERATE_WARNING_HIGH_FEE
|
||||
if fee_rate > confirm_rate:
|
||||
fee_str += ' - ' + _('Warning') + ': ' + _("high fee") + '!'
|
||||
self.amount_label.setText(amount_str)
|
||||
self.fee_label.setText(fee_str)
|
||||
self.size_label.setText(size_str)
|
||||
|
@ -224,7 +264,7 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||
rec.setBackground(QBrush(ColorScheme.GREEN.as_color(background=True)))
|
||||
rec.setToolTip(_("Wallet receive address"))
|
||||
chg = QTextCharFormat()
|
||||
chg.setBackground(QBrush(QColor("yellow")))
|
||||
chg.setBackground(QBrush(ColorScheme.YELLOW.as_color(background=True)))
|
||||
chg.setToolTip(_("Wallet change address"))
|
||||
|
||||
def text_format(addr):
|
||||
|
@ -250,7 +290,7 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||
cursor.insertText(prevout_hash[-8:] + ":%-4d " % prevout_n, ext)
|
||||
addr = x.get('address')
|
||||
if addr == "(pubkey)":
|
||||
_addr = self.wallet.find_pay_to_pubkey_address(prevout_hash, prevout_n)
|
||||
_addr = self.wallet.get_txin_address(x)
|
||||
if _addr:
|
||||
addr = _addr
|
||||
if addr is None:
|
||||
|
|
207
gui/qt/util.py
207
gui/qt/util.py
|
@ -6,11 +6,15 @@ import queue
|
|||
from collections import namedtuple
|
||||
from functools import partial
|
||||
|
||||
from electrum_zcash.i18n import _
|
||||
from PyQt5.QtGui import *
|
||||
from PyQt5.QtCore import *
|
||||
from PyQt5.QtWidgets import *
|
||||
|
||||
from electrum_zcash.i18n import _
|
||||
from electrum_zcash.util import FileImportFailed, FileExportFailed
|
||||
from electrum_zcash.paymentrequest import PR_UNPAID, PR_PAID, PR_EXPIRED
|
||||
|
||||
|
||||
if platform.system() == 'Windows':
|
||||
MONOSPACE_FONT = 'Lucida Console'
|
||||
elif platform.system() == 'Darwin':
|
||||
|
@ -21,8 +25,6 @@ else:
|
|||
|
||||
dialogs = []
|
||||
|
||||
from electrum_zcash.paymentrequest import PR_UNPAID, PR_PAID, PR_EXPIRED
|
||||
|
||||
pr_icons = {
|
||||
PR_UNPAID:":icons/unpaid.png",
|
||||
PR_PAID:":icons/confirmed.png",
|
||||
|
@ -163,17 +165,21 @@ class CancelButton(QPushButton):
|
|||
self.clicked.connect(dialog.reject)
|
||||
|
||||
class MessageBoxMixin(object):
|
||||
def top_level_window_recurse(self, window=None):
|
||||
def top_level_window_recurse(self, window=None, test_func=None):
|
||||
window = window or self
|
||||
classes = (WindowModalDialog, QMessageBox)
|
||||
if test_func is None:
|
||||
test_func = lambda x: True
|
||||
for n, child in enumerate(window.children()):
|
||||
# Test for visibility as old closed dialogs may not be GC-ed
|
||||
if isinstance(child, classes) and child.isVisible():
|
||||
return self.top_level_window_recurse(child)
|
||||
# Test for visibility as old closed dialogs may not be GC-ed.
|
||||
# Only accept children that confirm to test_func.
|
||||
if isinstance(child, classes) and child.isVisible() \
|
||||
and test_func(child):
|
||||
return self.top_level_window_recurse(child, test_func=test_func)
|
||||
return window
|
||||
|
||||
def top_level_window(self):
|
||||
return self.top_level_window_recurse()
|
||||
def top_level_window(self, test_func=None):
|
||||
return self.top_level_window_recurse(test_func)
|
||||
|
||||
def question(self, msg, parent=None, title=None, icon=None):
|
||||
Yes, No = QMessageBox.Yes, QMessageBox.No
|
||||
|
@ -200,9 +206,14 @@ class MessageBoxMixin(object):
|
|||
def msg_box(self, icon, parent, title, text, buttons=QMessageBox.Ok,
|
||||
defaultButton=QMessageBox.NoButton):
|
||||
parent = parent or self.top_level_window()
|
||||
d = QMessageBox(icon, title, str(text), buttons, parent)
|
||||
if type(icon) is QPixmap:
|
||||
d = QMessageBox(QMessageBox.Information, title, str(text), buttons, parent)
|
||||
d.setIconPixmap(icon)
|
||||
else:
|
||||
d = QMessageBox(icon, title, str(text), buttons, parent)
|
||||
d.setWindowModality(Qt.WindowModal)
|
||||
d.setDefaultButton(defaultButton)
|
||||
d.setTextInteractionFlags(Qt.TextSelectableByMouse)
|
||||
return d.exec_()
|
||||
|
||||
class WindowModalDialog(QDialog, MessageBoxMixin):
|
||||
|
@ -216,7 +227,7 @@ class WindowModalDialog(QDialog, MessageBoxMixin):
|
|||
|
||||
|
||||
class WaitingDialog(WindowModalDialog):
|
||||
'''Shows a please wait dialog whilst runnning a task. It is not
|
||||
'''Shows a please wait dialog whilst running a task. It is not
|
||||
necessary to maintain a reference to this dialog.'''
|
||||
def __init__(self, parent, message, task, on_success=None, on_error=None):
|
||||
assert parent
|
||||
|
@ -228,6 +239,7 @@ class WaitingDialog(WindowModalDialog):
|
|||
self.accepted.connect(self.on_accepted)
|
||||
self.show()
|
||||
self.thread = TaskThread(self)
|
||||
self.thread.finished.connect(self.deleteLater) # see #3956
|
||||
self.thread.add(task, on_success, self.accept, on_error)
|
||||
|
||||
def wait(self):
|
||||
|
@ -251,14 +263,14 @@ def line_dialog(parent, title, label, ok_label, default=None):
|
|||
if dialog.exec_():
|
||||
return txt.text()
|
||||
|
||||
def text_dialog(parent, title, label, ok_label, default=None):
|
||||
def text_dialog(parent, title, label, ok_label, default=None, allow_multi=False):
|
||||
from .qrtextedit import ScanQRTextEdit
|
||||
dialog = WindowModalDialog(parent, title)
|
||||
dialog.setMinimumWidth(500)
|
||||
dialog.setMinimumWidth(600)
|
||||
l = QVBoxLayout()
|
||||
dialog.setLayout(l)
|
||||
l.addWidget(QLabel(label))
|
||||
txt = ScanQRTextEdit()
|
||||
txt = ScanQRTextEdit(allow_multi=allow_multi)
|
||||
if default:
|
||||
txt.setText(default)
|
||||
l.addWidget(txt)
|
||||
|
@ -385,17 +397,24 @@ class MyTreeWidget(QTreeWidget):
|
|||
self.addChild = self.addTopLevelItem
|
||||
self.insertChild = self.insertTopLevelItem
|
||||
|
||||
self.icon_cache = IconCache()
|
||||
|
||||
# Control which columns are editable
|
||||
self.editor = None
|
||||
self.pending_update = False
|
||||
if editable_columns is None:
|
||||
editable_columns = [stretch_column]
|
||||
editable_columns = {stretch_column}
|
||||
else:
|
||||
editable_columns = set(editable_columns)
|
||||
self.editable_columns = editable_columns
|
||||
self.setItemDelegate(ElectrumItemDelegate(self))
|
||||
self.itemDoubleClicked.connect(self.on_doubleclick)
|
||||
self.update_headers(headers)
|
||||
self.current_filter = ""
|
||||
|
||||
self.setRootIsDecorated(False) # remove left margin
|
||||
self.toolbar_shown = False
|
||||
|
||||
def update_headers(self, headers):
|
||||
self.setColumnCount(len(headers))
|
||||
self.setHeaderLabels(headers)
|
||||
|
@ -406,11 +425,15 @@ class MyTreeWidget(QTreeWidget):
|
|||
|
||||
def editItem(self, item, column):
|
||||
if column in self.editable_columns:
|
||||
self.editing_itemcol = (item, column, item.text(column))
|
||||
# Calling setFlags causes on_changed events for some reason
|
||||
item.setFlags(item.flags() | Qt.ItemIsEditable)
|
||||
QTreeWidget.editItem(self, item, column)
|
||||
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
|
||||
try:
|
||||
self.editing_itemcol = (item, column, item.text(column))
|
||||
# Calling setFlags causes on_changed events for some reason
|
||||
item.setFlags(item.flags() | Qt.ItemIsEditable)
|
||||
QTreeWidget.editItem(self, item, column)
|
||||
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
|
||||
except RuntimeError:
|
||||
# (item) wrapped C/C++ object has been deleted
|
||||
pass
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
if event.key() in [ Qt.Key_F2, Qt.Key_Return ] and self.editor is None:
|
||||
|
@ -478,8 +501,12 @@ class MyTreeWidget(QTreeWidget):
|
|||
self.pending_update = True
|
||||
else:
|
||||
self.setUpdatesEnabled(False)
|
||||
scroll_pos = self.verticalScrollBar().value()
|
||||
self.on_update()
|
||||
self.setUpdatesEnabled(True)
|
||||
# To paint the list before resetting the scroll position
|
||||
self.parent.app.processEvents()
|
||||
self.verticalScrollBar().setValue(scroll_pos)
|
||||
if self.current_filter:
|
||||
self.filter(self.current_filter)
|
||||
|
||||
|
@ -503,6 +530,37 @@ class MyTreeWidget(QTreeWidget):
|
|||
item.setHidden(all([item.text(column).lower().find(p) == -1
|
||||
for column in columns]))
|
||||
|
||||
def create_toolbar(self, config=None):
|
||||
hbox = QHBoxLayout()
|
||||
buttons = self.get_toolbar_buttons()
|
||||
for b in buttons:
|
||||
b.setVisible(False)
|
||||
hbox.addWidget(b)
|
||||
hide_button = QPushButton('x')
|
||||
hide_button.setVisible(False)
|
||||
hide_button.pressed.connect(lambda: self.show_toolbar(False, config))
|
||||
self.toolbar_buttons = buttons + (hide_button,)
|
||||
hbox.addStretch()
|
||||
hbox.addWidget(hide_button)
|
||||
return hbox
|
||||
|
||||
def save_toolbar_state(self, state, config):
|
||||
pass # implemented in subclasses
|
||||
|
||||
def show_toolbar(self, state, config=None):
|
||||
if state == self.toolbar_shown:
|
||||
return
|
||||
self.toolbar_shown = state
|
||||
if config:
|
||||
self.save_toolbar_state(state, config)
|
||||
for b in self.toolbar_buttons:
|
||||
b.setVisible(state)
|
||||
if not state:
|
||||
self.on_hide_toolbar()
|
||||
|
||||
def toggle_toolbar(self, config=None):
|
||||
self.show_toolbar(not self.toolbar_shown, config)
|
||||
|
||||
|
||||
class ButtonsWidget(QWidget):
|
||||
|
||||
|
@ -589,12 +647,12 @@ class TaskThread(QThread):
|
|||
except BaseException:
|
||||
self.doneSig.emit(sys.exc_info(), task.cb_done, task.cb_error)
|
||||
|
||||
def on_done(self, result, cb_done, cb):
|
||||
def on_done(self, result, cb_done, cb_result):
|
||||
# This runs in the parent's thread.
|
||||
if cb_done:
|
||||
cb_done()
|
||||
if cb:
|
||||
cb(result)
|
||||
if cb_result:
|
||||
cb_result(result)
|
||||
|
||||
def stop(self):
|
||||
self.tasks.put(None)
|
||||
|
@ -621,6 +679,7 @@ class ColorScheme:
|
|||
dark_scheme = False
|
||||
|
||||
GREEN = ColorSchemeItem("#117c11", "#8af296")
|
||||
YELLOW = ColorSchemeItem("#897b2a", "#ffff00")
|
||||
RED = ColorSchemeItem("#7c1111", "#f18c8c")
|
||||
BLUE = ColorSchemeItem("#123b7c", "#8cb3f2")
|
||||
DEFAULT = ColorSchemeItem("black", "white")
|
||||
|
@ -635,6 +694,108 @@ class ColorScheme:
|
|||
if ColorScheme.has_dark_background(widget):
|
||||
ColorScheme.dark_scheme = True
|
||||
|
||||
|
||||
class AcceptFileDragDrop:
|
||||
def __init__(self, file_type=""):
|
||||
assert isinstance(self, QWidget)
|
||||
self.setAcceptDrops(True)
|
||||
self.file_type = file_type
|
||||
|
||||
def validateEvent(self, event):
|
||||
if not event.mimeData().hasUrls():
|
||||
event.ignore()
|
||||
return False
|
||||
for url in event.mimeData().urls():
|
||||
if not url.toLocalFile().endswith(self.file_type):
|
||||
event.ignore()
|
||||
return False
|
||||
event.accept()
|
||||
return True
|
||||
|
||||
def dragEnterEvent(self, event):
|
||||
self.validateEvent(event)
|
||||
|
||||
def dragMoveEvent(self, event):
|
||||
if self.validateEvent(event):
|
||||
event.setDropAction(Qt.CopyAction)
|
||||
|
||||
def dropEvent(self, event):
|
||||
if self.validateEvent(event):
|
||||
for url in event.mimeData().urls():
|
||||
self.onFileAdded(url.toLocalFile())
|
||||
|
||||
def onFileAdded(self, fn):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def import_meta_gui(electrum_window, title, importer, on_success):
|
||||
filter_ = "JSON (*.json);;All files (*)"
|
||||
filename = electrum_window.getOpenFileName(_("Open {} file").format(title), filter_)
|
||||
if not filename:
|
||||
return
|
||||
try:
|
||||
importer(filename)
|
||||
except FileImportFailed as e:
|
||||
electrum_window.show_critical(str(e))
|
||||
else:
|
||||
electrum_window.show_message(_("Your {} were successfully imported").format(title))
|
||||
on_success()
|
||||
|
||||
|
||||
def export_meta_gui(electrum_window, title, exporter):
|
||||
filter_ = "JSON (*.json);;All files (*)"
|
||||
filename = electrum_window.getSaveFileName(_("Select file to save your {}").format(title),
|
||||
'electrum_{}.json'.format(title), filter_)
|
||||
if not filename:
|
||||
return
|
||||
try:
|
||||
exporter(filename)
|
||||
except FileExportFailed as e:
|
||||
electrum_window.show_critical(str(e))
|
||||
else:
|
||||
electrum_window.show_message(_("Your {0} were exported to '{1}'")
|
||||
.format(title, str(filename)))
|
||||
|
||||
|
||||
def get_parent_main_window(widget):
|
||||
"""Returns a reference to the ElectrumWindow this widget belongs to."""
|
||||
from .main_window import ElectrumWindow
|
||||
for _ in range(100):
|
||||
if widget is None:
|
||||
return None
|
||||
if not isinstance(widget, ElectrumWindow):
|
||||
widget = widget.parentWidget()
|
||||
else:
|
||||
return widget
|
||||
return None
|
||||
|
||||
class SortableTreeWidgetItem(QTreeWidgetItem):
|
||||
DataRole = Qt.UserRole + 1
|
||||
|
||||
def __lt__(self, other):
|
||||
column = self.treeWidget().sortColumn()
|
||||
if None not in [x.data(column, self.DataRole) for x in [self, other]]:
|
||||
# We have set custom data to sort by
|
||||
return self.data(column, self.DataRole) < other.data(column, self.DataRole)
|
||||
try:
|
||||
# Is the value something numeric?
|
||||
return float(self.text(column)) < float(other.text(column))
|
||||
except ValueError:
|
||||
# If not, we will just do string comparison
|
||||
return self.text(column) < other.text(column)
|
||||
|
||||
|
||||
class IconCache:
|
||||
|
||||
def __init__(self):
|
||||
self.__cache = {}
|
||||
|
||||
def get(self, file_name):
|
||||
if file_name not in self.__cache:
|
||||
self.__cache[file_name] = QIcon(file_name)
|
||||
return self.__cache[file_name]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication([])
|
||||
t = WaitingDialog(None, 'testing ...', lambda: [time.sleep(1)], lambda x: QMessageBox.information(None, 'done', "done"))
|
||||
|
|
|
@ -32,6 +32,7 @@ class UTXOList(MyTreeWidget):
|
|||
def __init__(self, parent=None):
|
||||
MyTreeWidget.__init__(self, parent, self.create_menu, [ _('Address'), _('Label'), _('Amount'), _('Height'), _('Output point')], 1)
|
||||
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
||||
self.setSortingEnabled(True)
|
||||
|
||||
def get_name(self, x):
|
||||
return x.get('prevout_hash') + ":%d"%x.get('prevout_n')
|
||||
|
@ -46,10 +47,12 @@ class UTXOList(MyTreeWidget):
|
|||
height = x.get('height')
|
||||
name = self.get_name(x)
|
||||
label = self.wallet.get_label(x.get('prevout_hash'))
|
||||
amount = self.parent.format_amount(x['value'])
|
||||
utxo_item = QTreeWidgetItem([address, label, amount, '%d'%height, name[0:10] + '...' + name[-2:]])
|
||||
utxo_item.setFont(0, QFont(MONOSPACE_FONT))
|
||||
utxo_item.setFont(4, QFont(MONOSPACE_FONT))
|
||||
amount = self.parent.format_amount(x['value'], whitespaces=True)
|
||||
utxo_item = SortableTreeWidgetItem([address, label, amount, '%d'%height, name[0:10] + '...' + name[-2:]])
|
||||
for i in range(5):
|
||||
utxo_item.setFont(i, QFont(MONOSPACE_FONT))
|
||||
utxo_item.setTextAlignment(2, Qt.AlignRight)
|
||||
utxo_item.setTextAlignment(3, Qt.AlignRight)
|
||||
utxo_item.setData(0, Qt.UserRole, name)
|
||||
if self.wallet.is_frozen(address):
|
||||
utxo_item.setBackground(0, ColorScheme.BLUE.as_color(True))
|
||||
|
|
Loading…
Reference in New Issue