You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

438 lines
20 KiB

#!/usr/bin/env python
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 2012 thomasv@gitorious
#
# 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 ast
from typing import Optional, TYPE_CHECKING
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (QComboBox, QTabWidget, QDialog,
QSpinBox, QFileDialog, QCheckBox, QLabel,
QVBoxLayout, QGridLayout, QLineEdit,
QPushButton, QWidget, QHBoxLayout)
from electrum.i18n import _, languages
from electrum import util, coinchooser, paymentrequest
from electrum.util import base_units_list, event_listener
from electrum.gui import messages
from .util import (ColorScheme, WindowModalDialog, HelpLabel, Buttons,
CloseButton, QtEventListener)
if TYPE_CHECKING:
from electrum.simple_config import SimpleConfig
from .main_window import ElectrumWindow
class SettingsDialog(QDialog, QtEventListener):
def __init__(self, window: 'ElectrumWindow', config: 'SimpleConfig'):
QDialog.__init__(self)
self.setWindowTitle(_('Preferences'))
self.setMinimumWidth(500)
self.config = config
self.network = window.network
self.app = window.app
self.need_restart = False
self.fx = window.fx
self.wallet = window.wallet
self.register_callbacks()
self.app.alias_received_signal.connect(self.set_alias_color)
vbox = QVBoxLayout()
tabs = QTabWidget()
# language
lang_help = _('Select which language is used in the GUI (after restart).')
lang_label = HelpLabel(_('Language') + ':', lang_help)
lang_combo = QComboBox()
lang_combo.addItems(list(languages.values()))
lang_keys = list(languages.keys())
lang_cur_setting = self.config.get("language", '')
try:
index = lang_keys.index(lang_cur_setting)
except ValueError: # not in list
index = 0
lang_combo.setCurrentIndex(index)
if not self.config.is_modifiable('language'):
for w in [lang_combo, lang_label]: w.setEnabled(False)
def on_lang(x):
lang_request = list(languages.keys())[lang_combo.currentIndex()]
if lang_request != self.config.get('language'):
self.config.set_key("language", lang_request, True)
self.need_restart = True
lang_combo.currentIndexChanged.connect(on_lang)
nz_help = _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"')
nz_label = HelpLabel(_('Zeros after decimal point') + ':', nz_help)
nz = QSpinBox()
nz.setMinimum(0)
nz.setMaximum(self.config.decimal_point)
nz.setValue(self.config.num_zeros)
if not self.config.is_modifiable('num_zeros'):
for w in [nz, nz_label]: w.setEnabled(False)
def on_nz():
value = nz.value()
if self.config.num_zeros != value:
self.config.num_zeros = value
self.config.set_key('num_zeros', value, True)
self.app.refresh_tabs_signal.emit()
self.app.update_status_signal.emit()
nz.valueChanged.connect(on_nz)
# lightning
help_trampoline = _(messages.MSG_HELP_TRAMPOLINE)
trampoline_cb = QCheckBox(_("Use trampoline routing"))
trampoline_cb.setToolTip(messages.to_rtf(help_trampoline))
trampoline_cb.setChecked(not bool(self.config.get('use_gossip', False)))
def on_trampoline_checked(use_trampoline):
use_trampoline = bool(use_trampoline)
if not use_trampoline:
if not window.question('\n'.join([
_("Are you sure you want to disable trampoline?"),
_("Without this option, Electrum will need to sync with the Lightning network on every start."),
_("This may impact the reliability of your payments."),
])):
trampoline_cb.setCheckState(Qt.Checked)
return
self.config.set_key('use_gossip', not use_trampoline)
if not use_trampoline:
self.network.start_gossip()
else:
self.network.run_from_another_thread(
self.network.stop_gossip())
util.trigger_callback('ln_gossip_sync_progress')
# FIXME: update all wallet windows
util.trigger_callback('channels_updated', self.wallet)
trampoline_cb.stateChanged.connect(on_trampoline_checked)
help_remote_wt = ' '.join([
_("A watchtower is a daemon that watches your channels and prevents the other party from stealing funds by broadcasting an old state."),
_("If you have private a watchtower, enter its URL here."),
_("Check our online documentation if you want to configure Electrum as a watchtower."),
])
remote_wt_cb = QCheckBox(_("Use a remote watchtower"))
remote_wt_cb.setToolTip('<p>'+help_remote_wt+'</p>')
remote_wt_cb.setChecked(bool(self.config.get('use_watchtower', False)))
def on_remote_wt_checked(x):
self.config.set_key('use_watchtower', bool(x))
self.watchtower_url_e.setEnabled(bool(x))
remote_wt_cb.stateChanged.connect(on_remote_wt_checked)
watchtower_url = self.config.get('watchtower_url')
self.watchtower_url_e = QLineEdit(watchtower_url)
self.watchtower_url_e.setEnabled(self.config.get('use_watchtower', False))
def on_wt_url():
url = self.watchtower_url_e.text() or None
watchtower_url = self.config.set_key('watchtower_url', url)
self.watchtower_url_e.editingFinished.connect(on_wt_url)
msg = _('OpenAlias record, used to receive coins and to sign payment requests.') + '\n\n'\
+ _('The following alias providers are available:') + '\n'\
+ '\n'.join(['https://cryptoname.co/', 'http://xmr.link']) + '\n\n'\
+ 'For more information, see https://openalias.org'
alias_label = HelpLabel(_('OpenAlias') + ':', msg)
alias = self.config.get('alias','')
self.alias_e = QLineEdit(alias)
self.set_alias_color()
self.alias_e.editingFinished.connect(self.on_alias_edit)
msat_cb = QCheckBox(_("Show Lightning amounts with msat precision"))
msat_cb.setChecked(bool(self.config.get('amt_precision_post_satoshi', False)))
def on_msat_checked(v):
prec = 3 if v == Qt.Checked else 0
if self.config.amt_precision_post_satoshi != prec:
self.config.amt_precision_post_satoshi = prec
self.config.set_key('amt_precision_post_satoshi', prec)
self.app.refresh_tabs_signal.emit()
msat_cb.stateChanged.connect(on_msat_checked)
# units
units = base_units_list
msg = (_('Base unit of your wallet.')
+ '\n1 BTC = 1000 mBTC. 1 mBTC = 1000 bits. 1 bit = 100 sat.\n'
+ _('This setting affects the Send tab, and all balance related fields.'))
unit_label = HelpLabel(_('Base unit') + ':', msg)
unit_combo = QComboBox()
unit_combo.addItems(units)
unit_combo.setCurrentIndex(units.index(self.config.get_base_unit()))
def on_unit(x, nz):
unit_result = units[unit_combo.currentIndex()]
if self.config.get_base_unit() == unit_result:
return
self.config.set_base_unit(unit_result)
nz.setMaximum(self.config.decimal_point)
self.app.refresh_tabs_signal.emit()
self.app.update_status_signal.emit()
self.app.refresh_amount_edits_signal.emit()
unit_combo.currentIndexChanged.connect(lambda x: on_unit(x, nz))
thousandsep_cb = QCheckBox(_("Add thousand separators to bitcoin amounts"))
thousandsep_cb.setChecked(bool(self.config.get('amt_add_thousands_sep', False)))
def on_set_thousandsep(v):
checked = v == Qt.Checked
if self.config.amt_add_thousands_sep != checked:
self.config.amt_add_thousands_sep = checked
self.config.set_key('amt_add_thousands_sep', checked)
self.app.refresh_tabs_signal.emit()
thousandsep_cb.stateChanged.connect(on_set_thousandsep)
qr_combo = QComboBox()
qr_combo.addItem("Default", "default")
msg = (_("For scanning QR codes.") + "\n"
+ _("Install the zbar package to enable this."))
qr_label = HelpLabel(_('Video Device') + ':', msg)
from .qrreader import find_system_cameras
system_cameras = find_system_cameras()
for cam_desc, cam_path in system_cameras.items():
qr_combo.addItem(cam_desc, cam_path)
index = qr_combo.findData(self.config.get("video_device"))
qr_combo.setCurrentIndex(index)
on_video_device = lambda x: self.config.set_key("video_device", qr_combo.itemData(x), True)
qr_combo.currentIndexChanged.connect(on_video_device)
colortheme_combo = QComboBox()
colortheme_combo.addItem(_('Light'), 'default')
colortheme_combo.addItem(_('Dark'), 'dark')
index = colortheme_combo.findData(self.config.get('qt_gui_color_theme', 'default'))
colortheme_combo.setCurrentIndex(index)
colortheme_label = QLabel(_('Color theme') + ':')
def on_colortheme(x):
self.config.set_key('qt_gui_color_theme', colortheme_combo.itemData(x), True)
self.need_restart = True
colortheme_combo.currentIndexChanged.connect(on_colortheme)
updatecheck_cb = QCheckBox(_("Automatically check for software updates"))
updatecheck_cb.setChecked(bool(self.config.get('check_updates', False)))
def on_set_updatecheck(v):
self.config.set_key('check_updates', v == Qt.Checked, save=True)
updatecheck_cb.stateChanged.connect(on_set_updatecheck)
filelogging_cb = QCheckBox(_("Write logs to file"))
filelogging_cb.setChecked(bool(self.config.get('log_to_file', False)))
def on_set_filelogging(v):
self.config.set_key('log_to_file', v == Qt.Checked, save=True)
self.need_restart = True
filelogging_cb.stateChanged.connect(on_set_filelogging)
filelogging_cb.setToolTip(_('Debug logs can be persisted to disk. These are useful for troubleshooting.'))
def fmt_docs(key, klass):
lines = [ln.lstrip(" ") for ln in klass.__doc__.split("\n")]
return '\n'.join([key, "", " ".join(lines)])
choosers = sorted(coinchooser.COIN_CHOOSERS.keys())
if len(choosers) > 1:
chooser_name = coinchooser.get_name(self.config)
msg = _('Choose coin (UTXO) selection method. The following are available:\n\n')
msg += '\n\n'.join(fmt_docs(*item) for item in coinchooser.COIN_CHOOSERS.items())
chooser_label = HelpLabel(_('Coin selection') + ':', msg)
chooser_combo = QComboBox()
chooser_combo.addItems(choosers)
i = choosers.index(chooser_name) if chooser_name in choosers else 0
chooser_combo.setCurrentIndex(i)
def on_chooser(x):
chooser_name = choosers[chooser_combo.currentIndex()]
self.config.set_key('coin_chooser', chooser_name)
chooser_combo.currentIndexChanged.connect(on_chooser)
block_explorers = sorted(util.block_explorer_info().keys())
BLOCK_EX_CUSTOM_ITEM = _("Custom URL")
if BLOCK_EX_CUSTOM_ITEM in block_explorers: # malicious translation?
block_explorers.remove(BLOCK_EX_CUSTOM_ITEM)
block_explorers.append(BLOCK_EX_CUSTOM_ITEM)
msg = _('Choose which online block explorer to use for functions that open a web browser')
block_ex_label = HelpLabel(_('Online Block Explorer') + ':', msg)
block_ex_combo = QComboBox()
block_ex_custom_e = QLineEdit(str(self.config.get('block_explorer_custom') or ''))
block_ex_combo.addItems(block_explorers)
block_ex_combo.setCurrentIndex(
block_ex_combo.findText(util.block_explorer(self.config) or BLOCK_EX_CUSTOM_ITEM))
def showhide_block_ex_custom_e():
block_ex_custom_e.setVisible(block_ex_combo.currentText() == BLOCK_EX_CUSTOM_ITEM)
showhide_block_ex_custom_e()
def on_be_combo(x):
if block_ex_combo.currentText() == BLOCK_EX_CUSTOM_ITEM:
on_be_edit()
else:
be_result = block_explorers[block_ex_combo.currentIndex()]
self.config.set_key('block_explorer_custom', None, False)
self.config.set_key('block_explorer', be_result, True)
showhide_block_ex_custom_e()
block_ex_combo.currentIndexChanged.connect(on_be_combo)
def on_be_edit():
val = block_ex_custom_e.text()
try:
val = ast.literal_eval(val) # to also accept tuples
except:
pass
self.config.set_key('block_explorer_custom', val)
block_ex_custom_e.editingFinished.connect(on_be_edit)
block_ex_hbox = QHBoxLayout()
block_ex_hbox.setContentsMargins(0, 0, 0, 0)
block_ex_hbox.setSpacing(0)
block_ex_hbox.addWidget(block_ex_combo)
block_ex_hbox.addWidget(block_ex_custom_e)
block_ex_hbox_w = QWidget()
block_ex_hbox_w.setLayout(block_ex_hbox)
# Fiat Currency
self.require_history_checkbox = QCheckBox()
ccy_combo = QComboBox()
ex_combo = QComboBox()
def update_currencies():
if not self.fx:
return
currencies = sorted(self.fx.get_currencies(self.require_history_checkbox.isChecked()))
ccy_combo.clear()
ccy_combo.addItems([_('None')] + currencies)
if self.fx.is_enabled():
ccy_combo.setCurrentIndex(ccy_combo.findText(self.fx.get_currency()))
def update_exchanges():
if not self.fx: return
b = self.fx.is_enabled()
ex_combo.setEnabled(b)
if b:
h = self.require_history_checkbox.isChecked()
c = self.fx.get_currency()
exchanges = self.fx.get_exchanges_by_ccy(c, h)
else:
exchanges = self.fx.get_exchanges_by_ccy('USD', False)
ex_combo.blockSignals(True)
ex_combo.clear()
ex_combo.addItems(sorted(exchanges))
ex_combo.setCurrentIndex(ex_combo.findText(self.fx.config_exchange()))
ex_combo.blockSignals(False)
def on_currency(hh):
if not self.fx: return
b = bool(ccy_combo.currentIndex())
ccy = str(ccy_combo.currentText()) if b else None
self.fx.set_enabled(b)
if b and ccy != self.fx.ccy:
self.fx.set_currency(ccy)
update_exchanges()
self.app.update_fiat_signal.emit()
def on_exchange(idx):
exchange = str(ex_combo.currentText())
if self.fx and self.fx.is_enabled() and exchange and exchange != self.fx.exchange.name():
self.fx.set_exchange(exchange)
self.app.update_fiat_signal.emit()
def on_require_history(checked):
if not self.fx:
return
update_exchanges()
update_currencies()
update_exchanges()
ccy_combo.currentIndexChanged.connect(on_currency)
self.require_history_checkbox.stateChanged.connect(on_require_history)
ex_combo.currentIndexChanged.connect(on_exchange)
gui_widgets = []
gui_widgets.append((lang_label, lang_combo))
gui_widgets.append((colortheme_label, colortheme_combo))
gui_widgets.append((block_ex_label, block_ex_hbox_w))
units_widgets = []
units_widgets.append((unit_label, unit_combo))
units_widgets.append((nz_label, nz))
units_widgets.append((msat_cb, None))
units_widgets.append((thousandsep_cb, None))
lightning_widgets = []
lightning_widgets.append((trampoline_cb, None))
lightning_widgets.append((remote_wt_cb, self.watchtower_url_e))
fiat_widgets = []
fiat_widgets.append((QLabel(_('Fiat currency')), ccy_combo))
fiat_widgets.append((QLabel(_('Source')), ex_combo))
fiat_widgets.append((QLabel(_('Show sources with historical data')), self.require_history_checkbox))
misc_widgets = []
misc_widgets.append((updatecheck_cb, None))
misc_widgets.append((filelogging_cb, None))
misc_widgets.append((alias_label, self.alias_e))
misc_widgets.append((qr_label, qr_combo))
if len(choosers) > 1:
misc_widgets.append((chooser_label, chooser_combo))
tabs_info = [
(gui_widgets, _('Appearance')),
(units_widgets, _('Units')),
(fiat_widgets, _('Fiat')),
(lightning_widgets, _('Lightning')),
(misc_widgets, _('Misc')),
]
for widgets, name in tabs_info:
tab = QWidget()
tab_vbox = QVBoxLayout(tab)
grid = QGridLayout()
for a,b in widgets:
i = grid.rowCount()
if b:
if a:
grid.addWidget(a, i, 0)
grid.addWidget(b, i, 1)
else:
grid.addWidget(a, i, 0, 1, 2)
tab_vbox.addLayout(grid)
tab_vbox.addStretch(1)
tabs.addTab(tab, name)
vbox.addWidget(tabs)
vbox.addStretch(1)
vbox.addLayout(Buttons(CloseButton(self)))
self.setLayout(vbox)
@event_listener
def on_event_alias_received(self):
self.app.alias_received_signal.emit()
def set_alias_color(self):
if not self.config.get('alias'):
self.alias_e.setStyleSheet("")
return
if self.wallet.contacts.alias_info:
alias_addr, alias_name, validated = self.wallet.contacts.alias_info
self.alias_e.setStyleSheet((ColorScheme.GREEN if validated else ColorScheme.RED).as_stylesheet(True))
else:
self.alias_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True))
def on_alias_edit(self):
self.alias_e.setStyleSheet("")
alias = str(self.alias_e.text())
self.config.set_key('alias', alias, True)
if alias:
self.wallet.contacts.fetch_openalias(self.config)
def closeEvent(self, event):
self.unregister_callbacks()
try:
self.app.alias_received_signal.disconnect(self.set_alias_color)
except TypeError:
pass # 'method' object is not connected
event.accept()