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:
zebra-lucky 2018-06-04 19:32:41 +03:00
parent 604ec07fa5
commit 4df22e559c
21 changed files with 1783 additions and 629 deletions

View File

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

View File

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

View File

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

View File

@ -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_()

View File

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

View File

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

211
gui/qt/exception_window.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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:"):

View File

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

View File

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

View File

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

View File

@ -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:

View File

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

View File

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