commit 96d3c36e4ae0a46ade0b171ffa77a26edac1c42f
parent 9d595f1fe144dcd8b5017988a7b60ad53487b7bc
Author: ThomasV <thomasv@electrum.org>
Date: Thu, 5 Sep 2019 13:21:18 +0200
Qt: move settings dialog to a separate module
Diffstat:
2 files changed, 505 insertions(+), 436 deletions(-)
diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
@@ -2893,448 +2893,17 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.update_status()
def settings_dialog(self):
- self.need_restart = False
- d = WindowModalDialog(self, _('Preferences'))
- vbox = QVBoxLayout()
- tabs = QTabWidget()
- gui_widgets = []
- fee_widgets = []
- tx_widgets = []
- oa_widgets = []
- server_widgets = []
-
- # language
- lang_help = _('Select which language is used in the GUI (after restart).')
- lang_label = HelpLabel(_('Language') + ':', lang_help)
- lang_combo = QComboBox()
- from electrum.i18n import languages
- 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)
- gui_widgets.append((lang_label, lang_combo))
-
- 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.decimal_point)
- nz.setValue(self.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.num_zeros != value:
- self.num_zeros = value
- self.config.set_key('num_zeros', value, True)
- self.history_list.update()
- self.address_list.update()
- nz.valueChanged.connect(on_nz)
- gui_widgets.append((nz_label, nz))
-
- msg = '\n'.join([
- _('Time based: fee rate is based on average confirmation time estimates'),
- _('Mempool based: fee rate is targeting a depth in the memory pool')
- ]
- )
- fee_type_label = HelpLabel(_('Fee estimation') + ':', msg)
- fee_type_combo = QComboBox()
- fee_type_combo.addItems([_('Static'), _('ETA'), _('Mempool')])
- fee_type_combo.setCurrentIndex((2 if self.config.use_mempool_fees() else 1) if self.config.is_dynfee() else 0)
- def on_fee_type(x):
- self.config.set_key('mempool_fees', x==2)
- self.config.set_key('dynamic_fees', x>0)
- self.fee_slider.update()
- fee_type_combo.currentIndexChanged.connect(on_fee_type)
- fee_widgets.append((fee_type_label, fee_type_combo))
-
- feebox_cb = QCheckBox(_('Edit fees manually'))
- feebox_cb.setChecked(bool(self.config.get('show_fee', False)))
- feebox_cb.setToolTip(_("Show fee edit box in send tab."))
- def on_feebox(x):
- self.config.set_key('show_fee', x == Qt.Checked)
- self.fee_adv_controls.setVisible(bool(x))
- feebox_cb.stateChanged.connect(on_feebox)
- fee_widgets.append((feebox_cb, None))
-
- use_rbf = bool(self.config.get('use_rbf', True))
- use_rbf_cb = QCheckBox(_('Use Replace-By-Fee'))
- use_rbf_cb.setChecked(use_rbf)
- use_rbf_cb.setToolTip(
- _('If you check this box, your transactions will be marked as non-final,') + '\n' + \
- _('and you will have the possibility, while they are unconfirmed, to replace them with transactions that pay higher fees.') + '\n' + \
- _('Note that some merchants do not accept non-final transactions until they are confirmed.'))
- def on_use_rbf(x):
- self.config.set_key('use_rbf', bool(x))
- batch_rbf_cb.setEnabled(bool(x))
- use_rbf_cb.stateChanged.connect(on_use_rbf)
- fee_widgets.append((use_rbf_cb, None))
-
- batch_rbf_cb = QCheckBox(_('Batch RBF transactions'))
- batch_rbf_cb.setChecked(bool(self.config.get('batch_rbf', False)))
- batch_rbf_cb.setEnabled(use_rbf)
- batch_rbf_cb.setToolTip(
- _('If you check this box, your unconfirmed transactions will be consolidated into a single transaction.') + '\n' + \
- _('This will save fees.'))
- def on_batch_rbf(x):
- self.config.set_key('batch_rbf', bool(x))
- batch_rbf_cb.stateChanged.connect(on_batch_rbf)
- fee_widgets.append((batch_rbf_cb, None))
-
- 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','')
- alias_e = QLineEdit(alias)
- def set_alias_color():
- if not self.config.get('alias'):
- alias_e.setStyleSheet("")
- return
- if self.alias_info:
- alias_addr, alias_name, validated = self.alias_info
- alias_e.setStyleSheet((ColorScheme.GREEN if validated else ColorScheme.RED).as_stylesheet(True))
- else:
- alias_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True))
- def on_alias_edit():
- alias_e.setStyleSheet("")
- alias = str(alias_e.text())
- self.config.set_key('alias', alias, True)
- if alias:
- self.fetch_alias()
- set_alias_color()
- self.alias_received_signal.connect(set_alias_color)
- alias_e.editingFinished.connect(on_alias_edit)
- oa_widgets.append((alias_label, alias_e))
-
- # SSL certificate
- msg = ' '.join([
- _('SSL certificate used to sign payment requests.'),
- _('Use setconfig to set ssl_chain and ssl_privkey.'),
- ])
- if self.config.get('ssl_keyfile') and self.config.get('ssl_certfile'):
- try:
- SSL_identity = paymentrequest.check_ssl_config(self.config)
- SSL_error = None
- except BaseException as e:
- SSL_identity = "error"
- SSL_error = repr(e)
- else:
- SSL_identity = ""
- SSL_error = None
- SSL_id_label = HelpLabel(_('SSL certificate') + ':', msg)
- SSL_id_e = QLineEdit(SSL_identity)
- SSL_id_e.setStyleSheet((ColorScheme.RED if SSL_error else ColorScheme.GREEN).as_stylesheet(True) if SSL_identity else '')
- if SSL_error:
- SSL_id_e.setToolTip(SSL_error)
- SSL_id_e.setReadOnly(True)
- server_widgets.append((SSL_id_label, SSL_id_e))
-
- 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.base_unit()))
- def on_unit(x, nz):
- unit_result = units[unit_combo.currentIndex()]
- if self.base_unit() == unit_result:
- return
- edits = self.amount_e, self.fee_e, self.receive_amount_e
- amounts = [edit.get_amount() for edit in edits]
- self.decimal_point = base_unit_name_to_decimal_point(unit_result)
- self.config.set_key('decimal_point', self.decimal_point, True)
- nz.setMaximum(self.decimal_point)
- self.history_list.update()
- self.request_list.update()
- self.address_list.update()
- for edit, amount in zip(edits, amounts):
- edit.setAmount(amount)
- self.update_status()
- unit_combo.currentIndexChanged.connect(lambda x: on_unit(x, nz))
- gui_widgets.append((unit_label, unit_combo))
-
- block_explorers = sorted(util.block_explorer_info().keys())
- 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_combo.addItems(block_explorers)
- block_ex_combo.setCurrentIndex(block_ex_combo.findText(util.block_explorer(self.config)))
- def on_be(x):
- be_result = block_explorers[block_ex_combo.currentIndex()]
- self.config.set_key('block_explorer', be_result, True)
- block_ex_combo.currentIndexChanged.connect(on_be)
- gui_widgets.append((block_ex_label, block_ex_combo))
-
- from electrum import qrscanner
- system_cameras = qrscanner._find_system_cameras()
- qr_combo = QComboBox()
- qr_combo.addItem("Default","default")
- for camera, device in system_cameras.items():
- qr_combo.addItem(camera, device)
- #combo.addItem("Manually specify a device", config.get("video_device"))
- index = qr_combo.findData(self.config.get("video_device"))
- qr_combo.setCurrentIndex(index)
- msg = _("Install the zbar package to enable this.")
- qr_label = HelpLabel(_('Video Device') + ':', msg)
- qr_combo.setEnabled(qrscanner.libzbar is not None)
- on_video_device = lambda x: self.config.set_key("video_device", qr_combo.itemData(x), True)
- qr_combo.currentIndexChanged.connect(on_video_device)
- gui_widgets.append((qr_label, qr_combo))
-
- 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)
- gui_widgets.append((colortheme_label, colortheme_combo))
-
- 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)
- gui_widgets.append((updatecheck_cb, None))
-
- 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.'))
- gui_widgets.append((filelogging_cb, None))
-
- usechange_cb = QCheckBox(_('Use change addresses'))
- usechange_cb.setChecked(self.wallet.use_change)
- if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
- def on_usechange(x):
- usechange_result = x == Qt.Checked
- if self.wallet.use_change != usechange_result:
- self.wallet.use_change = usechange_result
- self.wallet.storage.put('use_change', self.wallet.use_change)
- multiple_cb.setEnabled(self.wallet.use_change)
- usechange_cb.stateChanged.connect(on_usechange)
- usechange_cb.setToolTip(_('Using change addresses makes it more difficult for other people to track your transactions.'))
- tx_widgets.append((usechange_cb, None))
-
- def on_multiple(x):
- multiple = x == Qt.Checked
- if self.wallet.multiple_change != multiple:
- self.wallet.multiple_change = multiple
- self.wallet.storage.put('multiple_change', multiple)
- multiple_change = self.wallet.multiple_change
- multiple_cb = QCheckBox(_('Use multiple change addresses'))
- multiple_cb.setEnabled(self.wallet.use_change)
- multiple_cb.setToolTip('\n'.join([
- _('In some cases, use up to 3 change addresses in order to break '
- 'up large coin amounts and obfuscate the recipient address.'),
- _('This may result in higher transactions fees.')
- ]))
- multiple_cb.setChecked(multiple_change)
- multiple_cb.stateChanged.connect(on_multiple)
- tx_widgets.append((multiple_cb, None))
-
- 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)
- tx_widgets.append((chooser_label, chooser_combo))
-
- def on_unconf(x):
- self.config.set_key('confirmed_only', bool(x))
- conf_only = bool(self.config.get('confirmed_only', False))
- unconf_cb = QCheckBox(_('Spend only confirmed coins'))
- unconf_cb.setToolTip(_('Spend only confirmed inputs.'))
- unconf_cb.setChecked(conf_only)
- unconf_cb.stateChanged.connect(on_unconf)
- tx_widgets.append((unconf_cb, None))
-
- def on_outrounding(x):
- self.config.set_key('coin_chooser_output_rounding', bool(x))
- enable_outrounding = bool(self.config.get('coin_chooser_output_rounding', False))
- outrounding_cb = QCheckBox(_('Enable output value rounding'))
- outrounding_cb.setToolTip(
- _('Set the value of the change output so that it has similar precision to the other outputs.') + '\n' +
- _('This might improve your privacy somewhat.') + '\n' +
- _('If enabled, at most 100 satoshis might be lost due to this, per transaction.'))
- outrounding_cb.setChecked(enable_outrounding)
- outrounding_cb.stateChanged.connect(on_outrounding)
- tx_widgets.append((outrounding_cb, None))
-
- # Fiat Currency
- hist_checkbox = QCheckBox()
- hist_capgains_checkbox = QCheckBox()
- fiat_address_checkbox = QCheckBox()
- ccy_combo = QComboBox()
- ex_combo = QComboBox()
-
- def update_currencies():
- if not self.fx: return
- currencies = sorted(self.fx.get_currencies(self.fx.get_history_config()))
- 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_history_cb():
- if not self.fx: return
- hist_checkbox.setChecked(self.fx.get_history_config())
- hist_checkbox.setEnabled(self.fx.is_enabled())
-
- def update_fiat_address_cb():
- if not self.fx: return
- fiat_address_checkbox.setChecked(self.fx.get_fiat_address_config())
-
- def update_history_capgains_cb():
- if not self.fx: return
- hist_capgains_checkbox.setChecked(self.fx.get_history_capital_gains_config())
- hist_capgains_checkbox.setEnabled(hist_checkbox.isChecked())
-
- def update_exchanges():
- if not self.fx: return
- b = self.fx.is_enabled()
- ex_combo.setEnabled(b)
- if b:
- h = self.fx.get_history_config()
- 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_history_cb()
- update_exchanges()
- self.update_fiat()
-
- 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)
-
- def on_history(checked):
- if not self.fx: return
- self.fx.set_history_config(checked)
- update_exchanges()
- self.history_model.refresh('on_history')
- if self.fx.is_enabled() and checked:
- self.fx.trigger_update()
- update_history_capgains_cb()
-
- def on_history_capgains(checked):
- if not self.fx: return
- self.fx.set_history_capital_gains_config(checked)
- self.history_model.refresh('on_history_capgains')
-
- def on_fiat_address(checked):
- if not self.fx: return
- self.fx.set_fiat_address_config(checked)
- self.address_list.refresh_headers()
- self.address_list.update()
-
- update_currencies()
- update_history_cb()
- update_history_capgains_cb()
- update_fiat_address_cb()
- update_exchanges()
- ccy_combo.currentIndexChanged.connect(on_currency)
- hist_checkbox.stateChanged.connect(on_history)
- hist_capgains_checkbox.stateChanged.connect(on_history_capgains)
- fiat_address_checkbox.stateChanged.connect(on_fiat_address)
- ex_combo.currentIndexChanged.connect(on_exchange)
-
- fiat_widgets = []
- fiat_widgets.append((QLabel(_('Fiat currency')), ccy_combo))
- fiat_widgets.append((QLabel(_('Show history rates')), hist_checkbox))
- fiat_widgets.append((QLabel(_('Show capital gains in history')), hist_capgains_checkbox))
- fiat_widgets.append((QLabel(_('Show Fiat balance for addresses')), fiat_address_checkbox))
- fiat_widgets.append((QLabel(_('Source')), ex_combo))
-
- tabs_info = [
- (fee_widgets, _('Fees')),
- (tx_widgets, _('Transactions')),
- (gui_widgets, _('General')),
- (fiat_widgets, _('Fiat')),
- (server_widgets, _('PayServer')),
- (oa_widgets, _('OpenAlias')),
- ]
- for widgets, name in tabs_info:
- tab = QWidget()
- grid = QGridLayout(tab)
- grid.setColumnStretch(0,1)
- 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)
- tabs.addTab(tab, name)
-
- vbox.addWidget(tabs)
- vbox.addStretch(1)
- vbox.addLayout(Buttons(CloseButton(d)))
- d.setLayout(vbox)
-
- # run the dialog
+ from .settings_dialog import SettingsDialog
+ d = SettingsDialog(self, self.config)
+ self.alias_received_signal.connect(d.set_alias_color)
d.exec_()
-
+ self.alias_received_signal.disconnect(d.set_alias_color)
if self.fx:
self.fx.trigger_update()
-
- self.alias_received_signal.disconnect(set_alias_color)
-
run_hook('close_settings_dialog')
- if self.need_restart:
+ if d.need_restart:
self.show_warning(_('Please restart Electrum to activate the new GUI settings'), title=_('Success'))
-
def closeEvent(self, event):
# It seems in some rare cases this closeEvent() is called twice
if not self.cleaned_up:
diff --git a/electrum/gui/qt/settings_dialog.py b/electrum/gui/qt/settings_dialog.py
@@ -0,0 +1,500 @@
+#!/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 sys
+import time
+import threading
+import os
+import traceback
+import json
+from decimal import Decimal
+
+from PyQt5.QtGui import QPixmap, QKeySequence, QIcon, QCursor
+from PyQt5.QtCore import Qt, QRect, QStringListModel, QSize, pyqtSignal
+from PyQt5.QtWidgets import (QMessageBox, QComboBox, QSystemTrayIcon, QTabWidget,
+ QSpinBox, QMenuBar, QFileDialog, QCheckBox, QLabel,
+ QVBoxLayout, QGridLayout, QLineEdit, QTreeWidgetItem,
+ QHBoxLayout, QPushButton, QScrollArea, QTextEdit,
+ QShortcut, QMainWindow, QCompleter, QInputDialog,
+ QWidget, QMenu, QSizePolicy, QStatusBar)
+
+import electrum
+from electrum.i18n import _
+from electrum import util, coinchooser, paymentrequest
+from electrum.util import (format_time, format_satoshis, format_fee_satoshis,
+ format_satoshis_plain, NotEnoughFunds,
+ UserCancelled, NoDynamicFeeEstimates, profiler,
+ export_meta, import_meta, bh2u, bfh, InvalidPassword,
+ base_units, base_units_list, base_unit_name_to_decimal_point,
+ decimal_point_to_base_unit_name, quantize_feerate,
+ UnknownBaseUnit, DECIMAL_POINT_DEFAULT, UserFacingException,
+ get_new_wallet_name, send_exception_to_crash_reporter,
+ InvalidBitcoinURI, InvoiceError)
+
+from .amountedit import AmountEdit, BTCAmountEdit, MyLineEdit, FeerateEdit
+from .util import (read_QIcon, ColorScheme, text_dialog, icon_path, WaitingDialog,
+ WindowModalDialog, ChoicesLayout, HelpLabel, FromList, Buttons,
+ OkButton, InfoButton, WWLabel, TaskThread, CancelButton,
+ CloseButton, HelpButton, MessageBoxMixin, EnterButton,
+ ButtonsLineEdit, CopyCloseButton, import_meta_gui, export_meta_gui,
+ filename_field, address_field, char_width_in_lineedit, webopen)
+
+from electrum.i18n import languages
+from electrum import qrscanner
+
+class SettingsDialog(WindowModalDialog):
+
+ def __init__(self, parent, config):
+ WindowModalDialog.__init__(self, parent, _('Preferences'))
+ self.config = config
+ self.window = parent
+ self.need_restart = False
+ self.fx = self.window.fx
+ self.wallet = self.window.wallet
+
+ vbox = QVBoxLayout()
+ tabs = QTabWidget()
+ gui_widgets = []
+ fee_widgets = []
+ tx_widgets = []
+ oa_widgets = []
+ server_widgets = []
+
+ # 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)
+ gui_widgets.append((lang_label, lang_combo))
+
+ 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.window.decimal_point)
+ nz.setValue(self.window.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.window.num_zeros != value:
+ self.window.num_zeros = value
+ self.config.set_key('num_zeros', value, True)
+ self.window.history_list.update()
+ self.window.address_list.update()
+ nz.valueChanged.connect(on_nz)
+ gui_widgets.append((nz_label, nz))
+
+ msg = '\n'.join([
+ _('Time based: fee rate is based on average confirmation time estimates'),
+ _('Mempool based: fee rate is targeting a depth in the memory pool')
+ ]
+ )
+ fee_type_label = HelpLabel(_('Fee estimation') + ':', msg)
+ fee_type_combo = QComboBox()
+ fee_type_combo.addItems([_('Static'), _('ETA'), _('Mempool')])
+ fee_type_combo.setCurrentIndex((2 if self.config.use_mempool_fees() else 1) if self.config.is_dynfee() else 0)
+ def on_fee_type(x):
+ self.config.set_key('mempool_fees', x==2)
+ self.config.set_key('dynamic_fees', x>0)
+ self.window.fee_slider.update()
+ fee_type_combo.currentIndexChanged.connect(on_fee_type)
+ fee_widgets.append((fee_type_label, fee_type_combo))
+
+ feebox_cb = QCheckBox(_('Edit fees manually'))
+ feebox_cb.setChecked(bool(self.config.get('show_fee', False)))
+ feebox_cb.setToolTip(_("Show fee edit box in send tab."))
+ def on_feebox(x):
+ self.config.set_key('show_fee', x == Qt.Checked)
+ self.window.fee_adv_controls.setVisible(bool(x))
+ feebox_cb.stateChanged.connect(on_feebox)
+ fee_widgets.append((feebox_cb, None))
+
+ use_rbf = bool(self.config.get('use_rbf', True))
+ use_rbf_cb = QCheckBox(_('Use Replace-By-Fee'))
+ use_rbf_cb.setChecked(use_rbf)
+ use_rbf_cb.setToolTip(
+ _('If you check this box, your transactions will be marked as non-final,') + '\n' + \
+ _('and you will have the possibility, while they are unconfirmed, to replace them with transactions that pay higher fees.') + '\n' + \
+ _('Note that some merchants do not accept non-final transactions until they are confirmed.'))
+ def on_use_rbf(x):
+ self.config.set_key('use_rbf', bool(x))
+ batch_rbf_cb.setEnabled(bool(x))
+ use_rbf_cb.stateChanged.connect(on_use_rbf)
+ fee_widgets.append((use_rbf_cb, None))
+
+ batch_rbf_cb = QCheckBox(_('Batch RBF transactions'))
+ batch_rbf_cb.setChecked(bool(self.config.get('batch_rbf', False)))
+ batch_rbf_cb.setEnabled(use_rbf)
+ batch_rbf_cb.setToolTip(
+ _('If you check this box, your unconfirmed transactions will be consolidated into a single transaction.') + '\n' + \
+ _('This will save fees.'))
+ def on_batch_rbf(x):
+ self.config.set_key('batch_rbf', bool(x))
+ batch_rbf_cb.stateChanged.connect(on_batch_rbf)
+ fee_widgets.append((batch_rbf_cb, None))
+
+ 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)
+ oa_widgets.append((alias_label, self.alias_e))
+
+ # SSL certificate
+ msg = ' '.join([
+ _('SSL certificate used to sign payment requests.'),
+ _('Use setconfig to set ssl_chain and ssl_privkey.'),
+ ])
+ if self.config.get('ssl_keyfile') and self.config.get('ssl_certfile'):
+ try:
+ SSL_identity = paymentrequest.check_ssl_config(self.config)
+ SSL_error = None
+ except BaseException as e:
+ SSL_identity = "error"
+ SSL_error = repr(e)
+ else:
+ SSL_identity = ""
+ SSL_error = None
+ SSL_id_label = HelpLabel(_('SSL certificate') + ':', msg)
+ SSL_id_e = QLineEdit(SSL_identity)
+ SSL_id_e.setStyleSheet((ColorScheme.RED if SSL_error else ColorScheme.GREEN).as_stylesheet(True) if SSL_identity else '')
+ if SSL_error:
+ SSL_id_e.setToolTip(SSL_error)
+ SSL_id_e.setReadOnly(True)
+ server_widgets.append((SSL_id_label, SSL_id_e))
+
+ 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.window.base_unit()))
+ def on_unit(x, nz):
+ unit_result = units[unit_combo.currentIndex()]
+ if self.window.base_unit() == unit_result:
+ return
+ edits = self.window.amount_e, self.window.fee_e, self.window.receive_amount_e
+ amounts = [edit.get_amount() for edit in edits]
+ self.window.decimal_point = base_unit_name_to_decimal_point(unit_result)
+ self.config.set_key('decimal_point', self.window.decimal_point, True)
+ nz.setMaximum(self.window.decimal_point)
+ self.window.history_list.update()
+ self.window.request_list.update()
+ self.window.address_list.update()
+ for edit, amount in zip(edits, amounts):
+ edit.setAmount(amount)
+ self.window.update_status()
+ unit_combo.currentIndexChanged.connect(lambda x: on_unit(x, nz))
+ gui_widgets.append((unit_label, unit_combo))
+
+ block_explorers = sorted(util.block_explorer_info().keys())
+ 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_combo.addItems(block_explorers)
+ block_ex_combo.setCurrentIndex(block_ex_combo.findText(util.block_explorer(self.config)))
+ def on_be(x):
+ be_result = block_explorers[block_ex_combo.currentIndex()]
+ self.config.set_key('block_explorer', be_result, True)
+ block_ex_combo.currentIndexChanged.connect(on_be)
+ gui_widgets.append((block_ex_label, block_ex_combo))
+
+ system_cameras = qrscanner._find_system_cameras()
+ qr_combo = QComboBox()
+ qr_combo.addItem("Default","default")
+ for camera, device in system_cameras.items():
+ qr_combo.addItem(camera, device)
+ #combo.addItem("Manually specify a device", config.get("video_device"))
+ index = qr_combo.findData(self.config.get("video_device"))
+ qr_combo.setCurrentIndex(index)
+ msg = _("Install the zbar package to enable this.")
+ qr_label = HelpLabel(_('Video Device') + ':', msg)
+ qr_combo.setEnabled(qrscanner.libzbar is not None)
+ on_video_device = lambda x: self.config.set_key("video_device", qr_combo.itemData(x), True)
+ qr_combo.currentIndexChanged.connect(on_video_device)
+ gui_widgets.append((qr_label, qr_combo))
+
+ 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)
+ gui_widgets.append((colortheme_label, colortheme_combo))
+
+ 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)
+ gui_widgets.append((updatecheck_cb, None))
+
+ 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.'))
+ gui_widgets.append((filelogging_cb, None))
+
+ usechange_cb = QCheckBox(_('Use change addresses'))
+ usechange_cb.setChecked(self.window.wallet.use_change)
+ if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
+ def on_usechange(x):
+ usechange_result = x == Qt.Checked
+ if self.window.wallet.use_change != usechange_result:
+ self.window.wallet.use_change = usechange_result
+ self.window.wallet.storage.put('use_change', self.window.wallet.use_change)
+ multiple_cb.setEnabled(self.window.wallet.use_change)
+ usechange_cb.stateChanged.connect(on_usechange)
+ usechange_cb.setToolTip(_('Using change addresses makes it more difficult for other people to track your transactions.'))
+ tx_widgets.append((usechange_cb, None))
+
+ def on_multiple(x):
+ multiple = x == Qt.Checked
+ if self.wallet.multiple_change != multiple:
+ self.wallet.multiple_change = multiple
+ self.wallet.storage.put('multiple_change', multiple)
+ multiple_change = self.wallet.multiple_change
+ multiple_cb = QCheckBox(_('Use multiple change addresses'))
+ multiple_cb.setEnabled(self.wallet.use_change)
+ multiple_cb.setToolTip('\n'.join([
+ _('In some cases, use up to 3 change addresses in order to break '
+ 'up large coin amounts and obfuscate the recipient address.'),
+ _('This may result in higher transactions fees.')
+ ]))
+ multiple_cb.setChecked(multiple_change)
+ multiple_cb.stateChanged.connect(on_multiple)
+ tx_widgets.append((multiple_cb, None))
+
+ 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)
+ tx_widgets.append((chooser_label, chooser_combo))
+
+ def on_unconf(x):
+ self.config.set_key('confirmed_only', bool(x))
+ conf_only = bool(self.config.get('confirmed_only', False))
+ unconf_cb = QCheckBox(_('Spend only confirmed coins'))
+ unconf_cb.setToolTip(_('Spend only confirmed inputs.'))
+ unconf_cb.setChecked(conf_only)
+ unconf_cb.stateChanged.connect(on_unconf)
+ tx_widgets.append((unconf_cb, None))
+
+ def on_outrounding(x):
+ self.config.set_key('coin_chooser_output_rounding', bool(x))
+ enable_outrounding = bool(self.config.get('coin_chooser_output_rounding', False))
+ outrounding_cb = QCheckBox(_('Enable output value rounding'))
+ outrounding_cb.setToolTip(
+ _('Set the value of the change output so that it has similar precision to the other outputs.') + '\n' +
+ _('This might improve your privacy somewhat.') + '\n' +
+ _('If enabled, at most 100 satoshis might be lost due to this, per transaction.'))
+ outrounding_cb.setChecked(enable_outrounding)
+ outrounding_cb.stateChanged.connect(on_outrounding)
+ tx_widgets.append((outrounding_cb, None))
+
+ # Fiat Currency
+ hist_checkbox = QCheckBox()
+ hist_capgains_checkbox = QCheckBox()
+ fiat_address_checkbox = QCheckBox()
+ ccy_combo = QComboBox()
+ ex_combo = QComboBox()
+
+ def update_currencies():
+ if not self.window.fx: return
+ currencies = sorted(self.fx.get_currencies(self.fx.get_history_config()))
+ 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_history_cb():
+ if not self.fx: return
+ hist_checkbox.setChecked(self.fx.get_history_config())
+ hist_checkbox.setEnabled(self.fx.is_enabled())
+
+ def update_fiat_address_cb():
+ if not self.fx: return
+ fiat_address_checkbox.setChecked(self.fx.get_fiat_address_config())
+
+ def update_history_capgains_cb():
+ if not self.fx: return
+ hist_capgains_checkbox.setChecked(self.fx.get_history_capital_gains_config())
+ hist_capgains_checkbox.setEnabled(hist_checkbox.isChecked())
+
+ def update_exchanges():
+ if not self.fx: return
+ b = self.fx.is_enabled()
+ ex_combo.setEnabled(b)
+ if b:
+ h = self.fx.get_history_config()
+ 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_history_cb()
+ update_exchanges()
+ self.window.update_fiat()
+
+ 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)
+
+ def on_history(checked):
+ if not self.fx: return
+ self.fx.set_history_config(checked)
+ update_exchanges()
+ self.window.history_model.refresh('on_history')
+ if self.fx.is_enabled() and checked:
+ self.fx.trigger_update()
+ update_history_capgains_cb()
+
+ def on_history_capgains(checked):
+ if not self.fx: return
+ self.fx.set_history_capital_gains_config(checked)
+ self.window.history_model.refresh('on_history_capgains')
+
+ def on_fiat_address(checked):
+ if not self.fx: return
+ self.fx.set_fiat_address_config(checked)
+ self.window.address_list.refresh_headers()
+ self.window.address_list.update()
+
+ update_currencies()
+ update_history_cb()
+ update_history_capgains_cb()
+ update_fiat_address_cb()
+ update_exchanges()
+ ccy_combo.currentIndexChanged.connect(on_currency)
+ hist_checkbox.stateChanged.connect(on_history)
+ hist_capgains_checkbox.stateChanged.connect(on_history_capgains)
+ fiat_address_checkbox.stateChanged.connect(on_fiat_address)
+ ex_combo.currentIndexChanged.connect(on_exchange)
+
+ fiat_widgets = []
+ fiat_widgets.append((QLabel(_('Fiat currency')), ccy_combo))
+ fiat_widgets.append((QLabel(_('Show history rates')), hist_checkbox))
+ fiat_widgets.append((QLabel(_('Show capital gains in history')), hist_capgains_checkbox))
+ fiat_widgets.append((QLabel(_('Show Fiat balance for addresses')), fiat_address_checkbox))
+ fiat_widgets.append((QLabel(_('Source')), ex_combo))
+
+ tabs_info = [
+ (fee_widgets, _('Fees')),
+ (tx_widgets, _('Transactions')),
+ (gui_widgets, _('General')),
+ (fiat_widgets, _('Fiat')),
+ (server_widgets, _('PayServer')),
+ (oa_widgets, _('OpenAlias')),
+ ]
+ for widgets, name in tabs_info:
+ tab = QWidget()
+ grid = QGridLayout(tab)
+ grid.setColumnStretch(0,1)
+ 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)
+ tabs.addTab(tab, name)
+
+ vbox.addWidget(tabs)
+ vbox.addStretch(1)
+ vbox.addLayout(Buttons(CloseButton(self)))
+ self.setLayout(vbox)
+
+ def set_alias_color(self):
+ if not self.config.get('alias'):
+ self.alias_e.setStyleSheet("")
+ return
+ if self.window.alias_info:
+ alias_addr, alias_name, validated = self.window.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.window.fetch_alias()