electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

settings_dialog.py (25202B)


      1 #!/usr/bin/env python
      2 #
      3 # Electrum - lightweight Bitcoin client
      4 # Copyright (C) 2012 thomasv@gitorious
      5 #
      6 # Permission is hereby granted, free of charge, to any person
      7 # obtaining a copy of this software and associated documentation files
      8 # (the "Software"), to deal in the Software without restriction,
      9 # including without limitation the rights to use, copy, modify, merge,
     10 # publish, distribute, sublicense, and/or sell copies of the Software,
     11 # and to permit persons to whom the Software is furnished to do so,
     12 # subject to the following conditions:
     13 #
     14 # The above copyright notice and this permission notice shall be
     15 # included in all copies or substantial portions of the Software.
     16 #
     17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
     18 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
     19 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
     20 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
     21 # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
     22 # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
     23 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
     24 # SOFTWARE.
     25 
     26 import ast
     27 from typing import Optional, TYPE_CHECKING
     28 
     29 from PyQt5.QtCore import Qt
     30 from PyQt5.QtWidgets import (QComboBox,  QTabWidget,
     31                              QSpinBox,  QFileDialog, QCheckBox, QLabel,
     32                              QVBoxLayout, QGridLayout, QLineEdit,
     33                              QPushButton, QWidget, QHBoxLayout)
     34 
     35 from electrum.i18n import _
     36 from electrum import util, coinchooser, paymentrequest
     37 from electrum.util import base_units_list
     38 
     39 from .util import (ColorScheme, WindowModalDialog, HelpLabel, Buttons,
     40                    CloseButton)
     41 
     42 from electrum.i18n import languages
     43 from electrum import qrscanner
     44 
     45 if TYPE_CHECKING:
     46     from electrum.simple_config import SimpleConfig
     47     from .main_window import ElectrumWindow
     48 
     49 
     50 class SettingsDialog(WindowModalDialog):
     51 
     52     def __init__(self, parent: 'ElectrumWindow', config: 'SimpleConfig'):
     53         WindowModalDialog.__init__(self, parent, _('Preferences'))
     54         self.config = config
     55         self.window = parent
     56         self.need_restart = False
     57         self.fx = self.window.fx
     58         self.wallet = self.window.wallet
     59         
     60         vbox = QVBoxLayout()
     61         tabs = QTabWidget()
     62         gui_widgets = []
     63         tx_widgets = []
     64         oa_widgets = []
     65 
     66         # language
     67         lang_help = _('Select which language is used in the GUI (after restart).')
     68         lang_label = HelpLabel(_('Language') + ':', lang_help)
     69         lang_combo = QComboBox()
     70         lang_combo.addItems(list(languages.values()))
     71         lang_keys = list(languages.keys())
     72         lang_cur_setting = self.config.get("language", '')
     73         try:
     74             index = lang_keys.index(lang_cur_setting)
     75         except ValueError:  # not in list
     76             index = 0
     77         lang_combo.setCurrentIndex(index)
     78         if not self.config.is_modifiable('language'):
     79             for w in [lang_combo, lang_label]: w.setEnabled(False)
     80         def on_lang(x):
     81             lang_request = list(languages.keys())[lang_combo.currentIndex()]
     82             if lang_request != self.config.get('language'):
     83                 self.config.set_key("language", lang_request, True)
     84                 self.need_restart = True
     85         lang_combo.currentIndexChanged.connect(on_lang)
     86         gui_widgets.append((lang_label, lang_combo))
     87 
     88         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"')
     89         nz_label = HelpLabel(_('Zeros after decimal point') + ':', nz_help)
     90         nz = QSpinBox()
     91         nz.setMinimum(0)
     92         nz.setMaximum(self.config.decimal_point)
     93         nz.setValue(self.config.num_zeros)
     94         if not self.config.is_modifiable('num_zeros'):
     95             for w in [nz, nz_label]: w.setEnabled(False)
     96         def on_nz():
     97             value = nz.value()
     98             if self.config.num_zeros != value:
     99                 self.config.num_zeros = value
    100                 self.config.set_key('num_zeros', value, True)
    101                 self.window.history_list.update()
    102                 self.window.address_list.update()
    103         nz.valueChanged.connect(on_nz)
    104         gui_widgets.append((nz_label, nz))
    105 
    106         use_rbf = bool(self.config.get('use_rbf', True))
    107         use_rbf_cb = QCheckBox(_('Use Replace-By-Fee'))
    108         use_rbf_cb.setChecked(use_rbf)
    109         use_rbf_cb.setToolTip(
    110             _('If you check this box, your transactions will be marked as non-final,') + '\n' + \
    111             _('and you will have the possibility, while they are unconfirmed, to replace them with transactions that pay higher fees.') + '\n' + \
    112             _('Note that some merchants do not accept non-final transactions until they are confirmed.'))
    113         def on_use_rbf(x):
    114             self.config.set_key('use_rbf', bool(x))
    115             batch_rbf_cb.setEnabled(bool(x))
    116         use_rbf_cb.stateChanged.connect(on_use_rbf)
    117         tx_widgets.append((use_rbf_cb, None))
    118 
    119         batch_rbf_cb = QCheckBox(_('Batch RBF transactions'))
    120         batch_rbf_cb.setChecked(bool(self.config.get('batch_rbf', False)))
    121         batch_rbf_cb.setEnabled(use_rbf)
    122         batch_rbf_cb.setToolTip(
    123             _('If you check this box, your unconfirmed transactions will be consolidated into a single transaction.') + '\n' + \
    124             _('This will save fees.'))
    125         def on_batch_rbf(x):
    126             self.config.set_key('batch_rbf', bool(x))
    127         batch_rbf_cb.stateChanged.connect(on_batch_rbf)
    128         tx_widgets.append((batch_rbf_cb, None))
    129 
    130         # lightning
    131         lightning_widgets = []
    132 
    133         help_gossip = _("""If this option is enabled, Electrum will download the network
    134 channels graph and compute payment path locally, instead of using trampoline payments. """)
    135         gossip_cb = QCheckBox(_("Download network graph"))
    136         gossip_cb.setToolTip(help_gossip)
    137         gossip_cb.setChecked(bool(self.config.get('use_gossip', False)))
    138         def on_gossip_checked(x):
    139             use_gossip = bool(x)
    140             self.config.set_key('use_gossip', use_gossip)
    141             if use_gossip:
    142                 self.window.network.start_gossip()
    143             else:
    144                 self.window.network.run_from_another_thread(
    145                     self.window.network.stop_gossip())
    146             util.trigger_callback('ln_gossip_sync_progress')
    147             # FIXME: update all wallet windows
    148             util.trigger_callback('channels_updated', self.wallet)
    149         gossip_cb.stateChanged.connect(on_gossip_checked)
    150         lightning_widgets.append((gossip_cb, None))
    151 
    152         help_local_wt = _("""If this option is checked, Electrum will
    153 run a local watchtower and protect your channels even if your wallet is not
    154 open. For this to work, your computer needs to be online regularly.""")
    155         local_wt_cb = QCheckBox(_("Run a local watchtower"))
    156         local_wt_cb.setToolTip(help_local_wt)
    157         local_wt_cb.setChecked(bool(self.config.get('run_local_watchtower', False)))
    158         def on_local_wt_checked(x):
    159             self.config.set_key('run_local_watchtower', bool(x))
    160         local_wt_cb.stateChanged.connect(on_local_wt_checked)
    161         lightning_widgets.append((local_wt_cb, None))
    162 
    163         help_persist = _("""If this option is checked, Electrum will persist after
    164 you close all your wallet windows, and the Electrum icon will be visible in the taskbar.
    165 Use this if you want your local watchtower to keep running after you close your wallet.""")
    166         persist_cb = QCheckBox(_("Persist after all windows are closed"))
    167         persist_cb.setToolTip(help_persist)
    168         persist_cb.setChecked(bool(self.config.get('persist_daemon', False)))
    169         def on_persist_checked(x):
    170             self.config.set_key('persist_daemon', bool(x))
    171         persist_cb.stateChanged.connect(on_persist_checked)
    172         lightning_widgets.append((persist_cb, None))
    173 
    174         help_remote_wt = _("""To use a remote watchtower, enter the corresponding URL here""")
    175         remote_wt_cb = QCheckBox(_("Use a remote watchtower"))
    176         remote_wt_cb.setToolTip(help_remote_wt)
    177         remote_wt_cb.setChecked(bool(self.config.get('use_watchtower', False)))
    178         def on_remote_wt_checked(x):
    179             self.config.set_key('use_watchtower', bool(x))
    180             self.watchtower_url_e.setEnabled(bool(x))
    181         remote_wt_cb.stateChanged.connect(on_remote_wt_checked)
    182         watchtower_url = self.config.get('watchtower_url')
    183         self.watchtower_url_e = QLineEdit(watchtower_url)
    184         self.watchtower_url_e.setEnabled(self.config.get('use_watchtower', False))
    185         def on_wt_url():
    186             url = self.watchtower_url_e.text() or None
    187             watchtower_url = self.config.set_key('watchtower_url', url)
    188         self.watchtower_url_e.editingFinished.connect(on_wt_url)
    189         lightning_widgets.append((remote_wt_cb, self.watchtower_url_e))
    190 
    191         msg = _('OpenAlias record, used to receive coins and to sign payment requests.') + '\n\n'\
    192               + _('The following alias providers are available:') + '\n'\
    193               + '\n'.join(['https://cryptoname.co/', 'http://xmr.link']) + '\n\n'\
    194               + 'For more information, see https://openalias.org'
    195         alias_label = HelpLabel(_('OpenAlias') + ':', msg)
    196         alias = self.config.get('alias','')
    197         self.alias_e = QLineEdit(alias)
    198         self.set_alias_color()
    199         self.alias_e.editingFinished.connect(self.on_alias_edit)
    200         oa_widgets.append((alias_label, self.alias_e))
    201 
    202         # units
    203         units = base_units_list
    204         msg = (_('Base unit of your wallet.')
    205                + '\n1 BTC = 1000 mBTC. 1 mBTC = 1000 bits. 1 bit = 100 sat.\n'
    206                + _('This setting affects the Send tab, and all balance related fields.'))
    207         unit_label = HelpLabel(_('Base unit') + ':', msg)
    208         unit_combo = QComboBox()
    209         unit_combo.addItems(units)
    210         unit_combo.setCurrentIndex(units.index(self.window.base_unit()))
    211         def on_unit(x, nz):
    212             unit_result = units[unit_combo.currentIndex()]
    213             if self.window.base_unit() == unit_result:
    214                 return
    215             edits = self.window.amount_e, self.window.receive_amount_e
    216             amounts = [edit.get_amount() for edit in edits]
    217             self.config.set_base_unit(unit_result)
    218             nz.setMaximum(self.config.decimal_point)
    219             self.window.update_tabs()
    220             for edit, amount in zip(edits, amounts):
    221                 edit.setAmount(amount)
    222             self.window.update_status()
    223         unit_combo.currentIndexChanged.connect(lambda x: on_unit(x, nz))
    224         gui_widgets.append((unit_label, unit_combo))
    225 
    226         system_cameras = qrscanner._find_system_cameras()
    227         qr_combo = QComboBox()
    228         qr_combo.addItem("Default","default")
    229         for camera, device in system_cameras.items():
    230             qr_combo.addItem(camera, device)
    231         #combo.addItem("Manually specify a device", config.get("video_device"))
    232         index = qr_combo.findData(self.config.get("video_device"))
    233         qr_combo.setCurrentIndex(index)
    234         msg = _("Install the zbar package to enable this.")
    235         qr_label = HelpLabel(_('Video Device') + ':', msg)
    236         qr_combo.setEnabled(qrscanner.libzbar is not None)
    237         on_video_device = lambda x: self.config.set_key("video_device", qr_combo.itemData(x), True)
    238         qr_combo.currentIndexChanged.connect(on_video_device)
    239         gui_widgets.append((qr_label, qr_combo))
    240 
    241         colortheme_combo = QComboBox()
    242         colortheme_combo.addItem(_('Light'), 'default')
    243         colortheme_combo.addItem(_('Dark'), 'dark')
    244         index = colortheme_combo.findData(self.config.get('qt_gui_color_theme', 'default'))
    245         colortheme_combo.setCurrentIndex(index)
    246         colortheme_label = QLabel(_('Color theme') + ':')
    247         def on_colortheme(x):
    248             self.config.set_key('qt_gui_color_theme', colortheme_combo.itemData(x), True)
    249             self.need_restart = True
    250         colortheme_combo.currentIndexChanged.connect(on_colortheme)
    251         gui_widgets.append((colortheme_label, colortheme_combo))
    252 
    253         updatecheck_cb = QCheckBox(_("Automatically check for software updates"))
    254         updatecheck_cb.setChecked(bool(self.config.get('check_updates', False)))
    255         def on_set_updatecheck(v):
    256             self.config.set_key('check_updates', v == Qt.Checked, save=True)
    257         updatecheck_cb.stateChanged.connect(on_set_updatecheck)
    258         gui_widgets.append((updatecheck_cb, None))
    259 
    260         filelogging_cb = QCheckBox(_("Write logs to file"))
    261         filelogging_cb.setChecked(bool(self.config.get('log_to_file', False)))
    262         def on_set_filelogging(v):
    263             self.config.set_key('log_to_file', v == Qt.Checked, save=True)
    264             self.need_restart = True
    265         filelogging_cb.stateChanged.connect(on_set_filelogging)
    266         filelogging_cb.setToolTip(_('Debug logs can be persisted to disk. These are useful for troubleshooting.'))
    267         gui_widgets.append((filelogging_cb, None))
    268 
    269         preview_cb = QCheckBox(_('Advanced preview'))
    270         preview_cb.setChecked(bool(self.config.get('advanced_preview', False)))
    271         preview_cb.setToolTip(_("Open advanced transaction preview dialog when 'Pay' is clicked."))
    272         def on_preview(x):
    273             self.config.set_key('advanced_preview', x == Qt.Checked)
    274         preview_cb.stateChanged.connect(on_preview)
    275         tx_widgets.append((preview_cb, None))
    276 
    277         usechange_cb = QCheckBox(_('Use change addresses'))
    278         usechange_cb.setChecked(self.window.wallet.use_change)
    279         if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False)
    280         def on_usechange(x):
    281             usechange_result = x == Qt.Checked
    282             if self.window.wallet.use_change != usechange_result:
    283                 self.window.wallet.use_change = usechange_result
    284                 self.window.wallet.db.put('use_change', self.window.wallet.use_change)
    285                 multiple_cb.setEnabled(self.window.wallet.use_change)
    286         usechange_cb.stateChanged.connect(on_usechange)
    287         usechange_cb.setToolTip(_('Using change addresses makes it more difficult for other people to track your transactions.'))
    288         tx_widgets.append((usechange_cb, None))
    289 
    290         def on_multiple(x):
    291             multiple = x == Qt.Checked
    292             if self.wallet.multiple_change != multiple:
    293                 self.wallet.multiple_change = multiple
    294                 self.wallet.db.put('multiple_change', multiple)
    295         multiple_change = self.wallet.multiple_change
    296         multiple_cb = QCheckBox(_('Use multiple change addresses'))
    297         multiple_cb.setEnabled(self.wallet.use_change)
    298         multiple_cb.setToolTip('\n'.join([
    299             _('In some cases, use up to 3 change addresses in order to break '
    300               'up large coin amounts and obfuscate the recipient address.'),
    301             _('This may result in higher transactions fees.')
    302         ]))
    303         multiple_cb.setChecked(multiple_change)
    304         multiple_cb.stateChanged.connect(on_multiple)
    305         tx_widgets.append((multiple_cb, None))
    306 
    307         def fmt_docs(key, klass):
    308             lines = [ln.lstrip(" ") for ln in klass.__doc__.split("\n")]
    309             return '\n'.join([key, "", " ".join(lines)])
    310 
    311         choosers = sorted(coinchooser.COIN_CHOOSERS.keys())
    312         if len(choosers) > 1:
    313             chooser_name = coinchooser.get_name(self.config)
    314             msg = _('Choose coin (UTXO) selection method.  The following are available:\n\n')
    315             msg += '\n\n'.join(fmt_docs(*item) for item in coinchooser.COIN_CHOOSERS.items())
    316             chooser_label = HelpLabel(_('Coin selection') + ':', msg)
    317             chooser_combo = QComboBox()
    318             chooser_combo.addItems(choosers)
    319             i = choosers.index(chooser_name) if chooser_name in choosers else 0
    320             chooser_combo.setCurrentIndex(i)
    321             def on_chooser(x):
    322                 chooser_name = choosers[chooser_combo.currentIndex()]
    323                 self.config.set_key('coin_chooser', chooser_name)
    324             chooser_combo.currentIndexChanged.connect(on_chooser)
    325             tx_widgets.append((chooser_label, chooser_combo))
    326 
    327         def on_unconf(x):
    328             self.config.set_key('confirmed_only', bool(x))
    329         conf_only = bool(self.config.get('confirmed_only', False))
    330         unconf_cb = QCheckBox(_('Spend only confirmed coins'))
    331         unconf_cb.setToolTip(_('Spend only confirmed inputs.'))
    332         unconf_cb.setChecked(conf_only)
    333         unconf_cb.stateChanged.connect(on_unconf)
    334         tx_widgets.append((unconf_cb, None))
    335 
    336         def on_outrounding(x):
    337             self.config.set_key('coin_chooser_output_rounding', bool(x))
    338         enable_outrounding = bool(self.config.get('coin_chooser_output_rounding', True))
    339         outrounding_cb = QCheckBox(_('Enable output value rounding'))
    340         outrounding_cb.setToolTip(
    341             _('Set the value of the change output so that it has similar precision to the other outputs.') + '\n' +
    342             _('This might improve your privacy somewhat.') + '\n' +
    343             _('If enabled, at most 100 satoshis might be lost due to this, per transaction.'))
    344         outrounding_cb.setChecked(enable_outrounding)
    345         outrounding_cb.stateChanged.connect(on_outrounding)
    346         tx_widgets.append((outrounding_cb, None))
    347 
    348         block_explorers = sorted(util.block_explorer_info().keys())
    349         BLOCK_EX_CUSTOM_ITEM = _("Custom URL")
    350         if BLOCK_EX_CUSTOM_ITEM in block_explorers:  # malicious translation?
    351             block_explorers.remove(BLOCK_EX_CUSTOM_ITEM)
    352         block_explorers.append(BLOCK_EX_CUSTOM_ITEM)
    353         msg = _('Choose which online block explorer to use for functions that open a web browser')
    354         block_ex_label = HelpLabel(_('Online Block Explorer') + ':', msg)
    355         block_ex_combo = QComboBox()
    356         block_ex_custom_e = QLineEdit(self.config.get('block_explorer_custom') or '')
    357         block_ex_combo.addItems(block_explorers)
    358         block_ex_combo.setCurrentIndex(
    359             block_ex_combo.findText(util.block_explorer(self.config) or BLOCK_EX_CUSTOM_ITEM))
    360         def showhide_block_ex_custom_e():
    361             block_ex_custom_e.setVisible(block_ex_combo.currentText() == BLOCK_EX_CUSTOM_ITEM)
    362         showhide_block_ex_custom_e()
    363         def on_be_combo(x):
    364             if block_ex_combo.currentText() == BLOCK_EX_CUSTOM_ITEM:
    365                 on_be_edit()
    366             else:
    367                 be_result = block_explorers[block_ex_combo.currentIndex()]
    368                 self.config.set_key('block_explorer_custom', None, False)
    369                 self.config.set_key('block_explorer', be_result, True)
    370             showhide_block_ex_custom_e()
    371         block_ex_combo.currentIndexChanged.connect(on_be_combo)
    372         def on_be_edit():
    373             val = block_ex_custom_e.text()
    374             try:
    375                 val = ast.literal_eval(val)  # to also accept tuples
    376             except:
    377                 pass
    378             self.config.set_key('block_explorer_custom', val)
    379         block_ex_custom_e.editingFinished.connect(on_be_edit)
    380         block_ex_hbox = QHBoxLayout()
    381         block_ex_hbox.setContentsMargins(0, 0, 0, 0)
    382         block_ex_hbox.setSpacing(0)
    383         block_ex_hbox.addWidget(block_ex_combo)
    384         block_ex_hbox.addWidget(block_ex_custom_e)
    385         block_ex_hbox_w = QWidget()
    386         block_ex_hbox_w.setLayout(block_ex_hbox)
    387         tx_widgets.append((block_ex_label, block_ex_hbox_w))
    388 
    389         # Fiat Currency
    390         hist_checkbox = QCheckBox()
    391         hist_capgains_checkbox = QCheckBox()
    392         fiat_address_checkbox = QCheckBox()
    393         ccy_combo = QComboBox()
    394         ex_combo = QComboBox()
    395 
    396         def update_currencies():
    397             if not self.window.fx: return
    398             currencies = sorted(self.fx.get_currencies(self.fx.get_history_config()))
    399             ccy_combo.clear()
    400             ccy_combo.addItems([_('None')] + currencies)
    401             if self.fx.is_enabled():
    402                 ccy_combo.setCurrentIndex(ccy_combo.findText(self.fx.get_currency()))
    403 
    404         def update_history_cb():
    405             if not self.fx: return
    406             hist_checkbox.setChecked(self.fx.get_history_config())
    407             hist_checkbox.setEnabled(self.fx.is_enabled())
    408 
    409         def update_fiat_address_cb():
    410             if not self.fx: return
    411             fiat_address_checkbox.setChecked(self.fx.get_fiat_address_config())
    412 
    413         def update_history_capgains_cb():
    414             if not self.fx: return
    415             hist_capgains_checkbox.setChecked(self.fx.get_history_capital_gains_config())
    416             hist_capgains_checkbox.setEnabled(hist_checkbox.isChecked())
    417 
    418         def update_exchanges():
    419             if not self.fx: return
    420             b = self.fx.is_enabled()
    421             ex_combo.setEnabled(b)
    422             if b:
    423                 h = self.fx.get_history_config()
    424                 c = self.fx.get_currency()
    425                 exchanges = self.fx.get_exchanges_by_ccy(c, h)
    426             else:
    427                 exchanges = self.fx.get_exchanges_by_ccy('USD', False)
    428             ex_combo.blockSignals(True)
    429             ex_combo.clear()
    430             ex_combo.addItems(sorted(exchanges))
    431             ex_combo.setCurrentIndex(ex_combo.findText(self.fx.config_exchange()))
    432             ex_combo.blockSignals(False)
    433 
    434         def on_currency(hh):
    435             if not self.fx: return
    436             b = bool(ccy_combo.currentIndex())
    437             ccy = str(ccy_combo.currentText()) if b else None
    438             self.fx.set_enabled(b)
    439             if b and ccy != self.fx.ccy:
    440                 self.fx.set_currency(ccy)
    441             update_history_cb()
    442             update_exchanges()
    443             self.window.update_fiat()
    444 
    445         def on_exchange(idx):
    446             exchange = str(ex_combo.currentText())
    447             if self.fx and self.fx.is_enabled() and exchange and exchange != self.fx.exchange.name():
    448                 self.fx.set_exchange(exchange)
    449 
    450         def on_history(checked):
    451             if not self.fx: return
    452             self.fx.set_history_config(checked)
    453             update_exchanges()
    454             self.window.history_model.refresh('on_history')
    455             if self.fx.is_enabled() and checked:
    456                 self.fx.trigger_update()
    457             update_history_capgains_cb()
    458 
    459         def on_history_capgains(checked):
    460             if not self.fx: return
    461             self.fx.set_history_capital_gains_config(checked)
    462             self.window.history_model.refresh('on_history_capgains')
    463 
    464         def on_fiat_address(checked):
    465             if not self.fx: return
    466             self.fx.set_fiat_address_config(checked)
    467             self.window.address_list.refresh_headers()
    468             self.window.address_list.update()
    469 
    470         update_currencies()
    471         update_history_cb()
    472         update_history_capgains_cb()
    473         update_fiat_address_cb()
    474         update_exchanges()
    475         ccy_combo.currentIndexChanged.connect(on_currency)
    476         hist_checkbox.stateChanged.connect(on_history)
    477         hist_capgains_checkbox.stateChanged.connect(on_history_capgains)
    478         fiat_address_checkbox.stateChanged.connect(on_fiat_address)
    479         ex_combo.currentIndexChanged.connect(on_exchange)
    480 
    481         fiat_widgets = []
    482         fiat_widgets.append((QLabel(_('Fiat currency')), ccy_combo))
    483         fiat_widgets.append((QLabel(_('Source')), ex_combo))
    484         fiat_widgets.append((QLabel(_('Show history rates')), hist_checkbox))
    485         fiat_widgets.append((QLabel(_('Show capital gains in history')), hist_capgains_checkbox))
    486         fiat_widgets.append((QLabel(_('Show Fiat balance for addresses')), fiat_address_checkbox))
    487 
    488         tabs_info = [
    489             (gui_widgets, _('General')),
    490             (tx_widgets, _('Transactions')),
    491             (lightning_widgets, _('Lightning')),
    492             (fiat_widgets, _('Fiat')),
    493             (oa_widgets, _('OpenAlias')),
    494         ]
    495         for widgets, name in tabs_info:
    496             tab = QWidget()
    497             tab_vbox = QVBoxLayout(tab)
    498             grid = QGridLayout()
    499             for a,b in widgets:
    500                 i = grid.rowCount()
    501                 if b:
    502                     if a:
    503                         grid.addWidget(a, i, 0)
    504                     grid.addWidget(b, i, 1)
    505                 else:
    506                     grid.addWidget(a, i, 0, 1, 2)
    507             tab_vbox.addLayout(grid)
    508             tab_vbox.addStretch(1)
    509             tabs.addTab(tab, name)
    510 
    511         vbox.addWidget(tabs)
    512         vbox.addStretch(1)
    513         vbox.addLayout(Buttons(CloseButton(self)))
    514         self.setLayout(vbox)
    515         
    516     def set_alias_color(self):
    517         if not self.config.get('alias'):
    518             self.alias_e.setStyleSheet("")
    519             return
    520         if self.window.alias_info:
    521             alias_addr, alias_name, validated = self.window.alias_info
    522             self.alias_e.setStyleSheet((ColorScheme.GREEN if validated else ColorScheme.RED).as_stylesheet(True))
    523         else:
    524             self.alias_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True))
    525 
    526     def on_alias_edit(self):
    527         self.alias_e.setStyleSheet("")
    528         alias = str(self.alias_e.text())
    529         self.config.set_key('alias', alias, True)
    530         if alias:
    531             self.window.fetch_alias()