electrum

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

qt.py (23765B)


      1 from functools import partial
      2 import threading
      3 
      4 from PyQt5.QtCore import Qt, QEventLoop, pyqtSignal, QRegExp
      5 from PyQt5.QtGui import QRegExpValidator
      6 from PyQt5.QtWidgets import (QVBoxLayout, QLabel, QGridLayout, QPushButton,
      7                              QHBoxLayout, QButtonGroup, QGroupBox, QDialog,
      8                              QTextEdit, QLineEdit, QRadioButton, QCheckBox, QWidget,
      9                              QMessageBox, QFileDialog, QSlider, QTabWidget)
     10 
     11 from electrum.gui.qt.util import (WindowModalDialog, WWLabel, Buttons, CancelButton,
     12                                   OkButton, CloseButton)
     13 from electrum.i18n import _
     14 from electrum.plugin import hook
     15 from electrum.util import bh2u
     16 
     17 from ..hw_wallet.qt import QtHandlerBase, QtPluginBase
     18 from ..hw_wallet.plugin import only_hook_if_libraries_available
     19 from .keepkey import KeepKeyPlugin, TIM_NEW, TIM_RECOVER, TIM_MNEMONIC
     20 
     21 
     22 PASSPHRASE_HELP_SHORT =_(
     23     "Passphrases allow you to access new wallets, each "
     24     "hidden behind a particular case-sensitive passphrase.")
     25 PASSPHRASE_HELP = PASSPHRASE_HELP_SHORT + "  " + _(
     26     "You need to create a separate Electrum wallet for each passphrase "
     27     "you use as they each generate different addresses.  Changing "
     28     "your passphrase does not lose other wallets, each is still "
     29     "accessible behind its own passphrase.")
     30 RECOMMEND_PIN = _(
     31     "You should enable PIN protection.  Your PIN is the only protection "
     32     "for your bitcoins if your device is lost or stolen.")
     33 PASSPHRASE_NOT_PIN = _(
     34     "If you forget a passphrase you will be unable to access any "
     35     "bitcoins in the wallet behind it.  A passphrase is not a PIN. "
     36     "Only change this if you are sure you understand it.")
     37 CHARACTER_RECOVERY = (
     38     "Use the recovery cipher shown on your device to input your seed words.  "
     39     "The cipher changes with every keypress.\n"
     40     "After at most 4 letters the device will auto-complete a word.\n"
     41     "Press SPACE or the Accept Word button to accept the device's auto-"
     42     "completed word and advance to the next one.\n"
     43     "Press BACKSPACE to go back a character or word.\n"
     44     "Press ENTER or the Seed Entered button once the last word in your "
     45     "seed is auto-completed.")
     46 
     47 class CharacterButton(QPushButton):
     48     def __init__(self, text=None):
     49         QPushButton.__init__(self, text)
     50 
     51     def keyPressEvent(self, event):
     52         event.setAccepted(False)   # Pass through Enter and Space keys
     53 
     54 
     55 class CharacterDialog(WindowModalDialog):
     56 
     57     def __init__(self, parent):
     58         super(CharacterDialog, self).__init__(parent)
     59         self.setWindowTitle(_("KeepKey Seed Recovery"))
     60         self.character_pos = 0
     61         self.word_pos = 0
     62         self.loop = QEventLoop()
     63         self.word_help = QLabel()
     64         self.char_buttons = []
     65 
     66         vbox = QVBoxLayout(self)
     67         vbox.addWidget(WWLabel(CHARACTER_RECOVERY))
     68         hbox = QHBoxLayout()
     69         hbox.addWidget(self.word_help)
     70         for i in range(4):
     71             char_button = CharacterButton('*')
     72             char_button.setMaximumWidth(36)
     73             self.char_buttons.append(char_button)
     74             hbox.addWidget(char_button)
     75         self.accept_button = CharacterButton(_("Accept Word"))
     76         self.accept_button.clicked.connect(partial(self.process_key, 32))
     77         self.rejected.connect(partial(self.loop.exit, 1))
     78         hbox.addWidget(self.accept_button)
     79         hbox.addStretch(1)
     80         vbox.addLayout(hbox)
     81 
     82         self.finished_button = QPushButton(_("Seed Entered"))
     83         self.cancel_button = QPushButton(_("Cancel"))
     84         self.finished_button.clicked.connect(partial(self.process_key,
     85                                                      Qt.Key_Return))
     86         self.cancel_button.clicked.connect(self.rejected)
     87         buttons = Buttons(self.finished_button, self.cancel_button)
     88         vbox.addSpacing(40)
     89         vbox.addLayout(buttons)
     90         self.refresh()
     91         self.show()
     92 
     93     def refresh(self):
     94         self.word_help.setText("Enter seed word %2d:" % (self.word_pos + 1))
     95         self.accept_button.setEnabled(self.character_pos >= 3)
     96         self.finished_button.setEnabled((self.word_pos in (11, 17, 23)
     97                                          and self.character_pos >= 3))
     98         for n, button in enumerate(self.char_buttons):
     99             button.setEnabled(n == self.character_pos)
    100             if n == self.character_pos:
    101                 button.setFocus()
    102 
    103     def is_valid_alpha_space(self, key):
    104         # Auto-completion requires at least 3 characters
    105         if key == ord(' ') and self.character_pos >= 3:
    106             return True
    107         # Firmware aborts protocol if the 5th character is non-space
    108         if self.character_pos >= 4:
    109             return False
    110         return (key >= ord('a') and key <= ord('z')
    111                 or (key >= ord('A') and key <= ord('Z')))
    112 
    113     def process_key(self, key):
    114         self.data = None
    115         if key == Qt.Key_Return and self.finished_button.isEnabled():
    116             self.data = {'done': True}
    117         elif key == Qt.Key_Backspace and (self.word_pos or self.character_pos):
    118             self.data = {'delete': True}
    119         elif self.is_valid_alpha_space(key):
    120             self.data = {'character': chr(key).lower()}
    121         if self.data:
    122             self.loop.exit(0)
    123 
    124     def keyPressEvent(self, event):
    125         self.process_key(event.key())
    126         if not self.data:
    127             QDialog.keyPressEvent(self, event)
    128 
    129     def get_char(self, word_pos, character_pos):
    130         self.word_pos = word_pos
    131         self.character_pos = character_pos
    132         self.refresh()
    133         if self.loop.exec_():
    134             self.data = None  # User cancelled
    135 
    136 
    137 class QtHandler(QtHandlerBase):
    138 
    139     char_signal = pyqtSignal(object)
    140     pin_signal = pyqtSignal(object, object)
    141     close_char_dialog_signal = pyqtSignal()
    142 
    143     def __init__(self, win, pin_matrix_widget_class, device):
    144         super(QtHandler, self).__init__(win, device)
    145         self.char_signal.connect(self.update_character_dialog)
    146         self.pin_signal.connect(self.pin_dialog)
    147         self.close_char_dialog_signal.connect(self._close_char_dialog)
    148         self.pin_matrix_widget_class = pin_matrix_widget_class
    149         self.character_dialog = None
    150 
    151     def get_char(self, msg):
    152         self.done.clear()
    153         self.char_signal.emit(msg)
    154         self.done.wait()
    155         data = self.character_dialog.data
    156         if not data or 'done' in data:
    157             self.close_char_dialog_signal.emit()
    158         return data
    159 
    160     def _close_char_dialog(self):
    161         if self.character_dialog:
    162             self.character_dialog.accept()
    163             self.character_dialog = None
    164 
    165     def get_pin(self, msg, *, show_strength=True):
    166         self.done.clear()
    167         self.pin_signal.emit(msg, show_strength)
    168         self.done.wait()
    169         return self.response
    170 
    171     def pin_dialog(self, msg, show_strength):
    172         # Needed e.g. when resetting a device
    173         self.clear_dialog()
    174         dialog = WindowModalDialog(self.top_level_window(), _("Enter PIN"))
    175         matrix = self.pin_matrix_widget_class(show_strength)
    176         vbox = QVBoxLayout()
    177         vbox.addWidget(QLabel(msg))
    178         vbox.addWidget(matrix)
    179         vbox.addLayout(Buttons(CancelButton(dialog), OkButton(dialog)))
    180         dialog.setLayout(vbox)
    181         dialog.exec_()
    182         self.response = str(matrix.get_value())
    183         self.done.set()
    184 
    185     def update_character_dialog(self, msg):
    186         if not self.character_dialog:
    187             self.character_dialog = CharacterDialog(self.top_level_window())
    188         self.character_dialog.get_char(msg.word_pos, msg.character_pos)
    189         self.done.set()
    190 
    191 
    192 
    193 class QtPlugin(QtPluginBase):
    194     # Derived classes must provide the following class-static variables:
    195     #   icon_file
    196     #   pin_matrix_widget_class
    197 
    198     @only_hook_if_libraries_available
    199     @hook
    200     def receive_menu(self, menu, addrs, wallet):
    201         if len(addrs) != 1:
    202             return
    203         for keystore in wallet.get_keystores():
    204             if type(keystore) == self.keystore_class:
    205                 def show_address():
    206                     keystore.thread.add(partial(self.show_address, wallet, addrs[0], keystore))
    207                 device_name = "{} ({})".format(self.device, keystore.label)
    208                 menu.addAction(_("Show on {}").format(device_name), show_address)
    209 
    210     def show_settings_dialog(self, window, keystore):
    211         def connect():
    212             device_id = self.choose_device(window, keystore)
    213             return device_id
    214         def show_dialog(device_id):
    215             if device_id:
    216                 SettingsDialog(window, self, keystore, device_id).exec_()
    217         keystore.thread.add(connect, on_success=show_dialog)
    218 
    219     def request_trezor_init_settings(self, wizard, method, device):
    220         vbox = QVBoxLayout()
    221         next_enabled = True
    222         label = QLabel(_("Enter a label to name your device:"))
    223         name = QLineEdit()
    224         hl = QHBoxLayout()
    225         hl.addWidget(label)
    226         hl.addWidget(name)
    227         hl.addStretch(1)
    228         vbox.addLayout(hl)
    229 
    230         def clean_text(widget):
    231             text = widget.toPlainText().strip()
    232             return ' '.join(text.split())
    233 
    234         if method in [TIM_NEW, TIM_RECOVER]:
    235             gb = QGroupBox()
    236             hbox1 = QHBoxLayout()
    237             gb.setLayout(hbox1)
    238             # KeepKey recovery doesn't need a word count
    239             if method == TIM_NEW:
    240                 vbox.addWidget(gb)
    241             gb.setTitle(_("Select your seed length:"))
    242             bg = QButtonGroup()
    243             for i, count in enumerate([12, 18, 24]):
    244                 rb = QRadioButton(gb)
    245                 rb.setText(_("{} words").format(count))
    246                 bg.addButton(rb)
    247                 bg.setId(rb, i)
    248                 hbox1.addWidget(rb)
    249                 rb.setChecked(True)
    250             cb_pin = QCheckBox(_('Enable PIN protection'))
    251             cb_pin.setChecked(True)
    252         else:
    253             text = QTextEdit()
    254             text.setMaximumHeight(60)
    255             if method == TIM_MNEMONIC:
    256                 msg = _("Enter your BIP39 mnemonic:")
    257             else:
    258                 msg = _("Enter the master private key beginning with xprv:")
    259                 def set_enabled():
    260                     from electrum.bip32 import is_xprv
    261                     wizard.next_button.setEnabled(is_xprv(clean_text(text)))
    262                 text.textChanged.connect(set_enabled)
    263                 next_enabled = False
    264 
    265             vbox.addWidget(QLabel(msg))
    266             vbox.addWidget(text)
    267             pin = QLineEdit()
    268             pin.setValidator(QRegExpValidator(QRegExp('[1-9]{0,9}')))
    269             pin.setMaximumWidth(100)
    270             hbox_pin = QHBoxLayout()
    271             hbox_pin.addWidget(QLabel(_("Enter your PIN (digits 1-9):")))
    272             hbox_pin.addWidget(pin)
    273             hbox_pin.addStretch(1)
    274 
    275         if method in [TIM_NEW, TIM_RECOVER]:
    276             vbox.addWidget(WWLabel(RECOMMEND_PIN))
    277             vbox.addWidget(cb_pin)
    278         else:
    279             vbox.addLayout(hbox_pin)
    280 
    281         passphrase_msg = WWLabel(PASSPHRASE_HELP_SHORT)
    282         passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN)
    283         passphrase_warning.setStyleSheet("color: red")
    284         cb_phrase = QCheckBox(_('Enable passphrases'))
    285         cb_phrase.setChecked(False)
    286         vbox.addWidget(passphrase_msg)
    287         vbox.addWidget(passphrase_warning)
    288         vbox.addWidget(cb_phrase)
    289 
    290         wizard.exec_layout(vbox, next_enabled=next_enabled)
    291 
    292         if method in [TIM_NEW, TIM_RECOVER]:
    293             item = bg.checkedId()
    294             pin = cb_pin.isChecked()
    295         else:
    296             item = ' '.join(str(clean_text(text)).split())
    297             pin = str(pin.text())
    298 
    299         return (item, name.text(), pin, cb_phrase.isChecked())
    300 
    301 
    302 class Plugin(KeepKeyPlugin, QtPlugin):
    303     icon_paired = "keepkey.png"
    304     icon_unpaired = "keepkey_unpaired.png"
    305 
    306     def create_handler(self, window):
    307         return QtHandler(window, self.pin_matrix_widget_class(), self.device)
    308 
    309     @classmethod
    310     def pin_matrix_widget_class(self):
    311         from keepkeylib.qt.pinmatrix import PinMatrixWidget
    312         return PinMatrixWidget
    313 
    314 
    315 class SettingsDialog(WindowModalDialog):
    316     '''This dialog doesn't require a device be paired with a wallet.
    317     We want users to be able to wipe a device even if they've forgotten
    318     their PIN.'''
    319 
    320     def __init__(self, window, plugin, keystore, device_id):
    321         title = _("{} Settings").format(plugin.device)
    322         super(SettingsDialog, self).__init__(window, title)
    323         self.setMaximumWidth(540)
    324 
    325         devmgr = plugin.device_manager()
    326         config = devmgr.config
    327         handler = keystore.handler
    328         thread = keystore.thread
    329 
    330         def invoke_client(method, *args, **kw_args):
    331             unpair_after = kw_args.pop('unpair_after', False)
    332 
    333             def task():
    334                 client = devmgr.client_by_id(device_id)
    335                 if not client:
    336                     raise RuntimeError("Device not connected")
    337                 if method:
    338                     getattr(client, method)(*args, **kw_args)
    339                 if unpair_after:
    340                     devmgr.unpair_id(device_id)
    341                 return client.features
    342 
    343             thread.add(task, on_success=update)
    344 
    345         def update(features):
    346             self.features = features
    347             set_label_enabled()
    348             bl_hash = bh2u(features.bootloader_hash)
    349             bl_hash = "\n".join([bl_hash[:32], bl_hash[32:]])
    350             noyes = [_("No"), _("Yes")]
    351             endis = [_("Enable Passphrases"), _("Disable Passphrases")]
    352             disen = [_("Disabled"), _("Enabled")]
    353             setchange = [_("Set a PIN"), _("Change PIN")]
    354 
    355             version = "%d.%d.%d" % (features.major_version,
    356                                     features.minor_version,
    357                                     features.patch_version)
    358             coins = ", ".join(coin.coin_name for coin in features.coins)
    359 
    360             device_label.setText(features.label)
    361             pin_set_label.setText(noyes[features.pin_protection])
    362             passphrases_label.setText(disen[features.passphrase_protection])
    363             bl_hash_label.setText(bl_hash)
    364             label_edit.setText(features.label)
    365             device_id_label.setText(features.device_id)
    366             initialized_label.setText(noyes[features.initialized])
    367             version_label.setText(version)
    368             coins_label.setText(coins)
    369             clear_pin_button.setVisible(features.pin_protection)
    370             clear_pin_warning.setVisible(features.pin_protection)
    371             pin_button.setText(setchange[features.pin_protection])
    372             pin_msg.setVisible(not features.pin_protection)
    373             passphrase_button.setText(endis[features.passphrase_protection])
    374             language_label.setText(features.language)
    375 
    376         def set_label_enabled():
    377             label_apply.setEnabled(label_edit.text() != self.features.label)
    378 
    379         def rename():
    380             invoke_client('change_label', label_edit.text())
    381 
    382         def toggle_passphrase():
    383             title = _("Confirm Toggle Passphrase Protection")
    384             currently_enabled = self.features.passphrase_protection
    385             if currently_enabled:
    386                 msg = _("After disabling passphrases, you can only pair this "
    387                         "Electrum wallet if it had an empty passphrase.  "
    388                         "If its passphrase was not empty, you will need to "
    389                         "create a new wallet with the install wizard.  You "
    390                         "can use this wallet again at any time by re-enabling "
    391                         "passphrases and entering its passphrase.")
    392             else:
    393                 msg = _("Your current Electrum wallet can only be used with "
    394                         "an empty passphrase.  You must create a separate "
    395                         "wallet with the install wizard for other passphrases "
    396                         "as each one generates a new set of addresses.")
    397             msg += "\n\n" + _("Are you sure you want to proceed?")
    398             if not self.question(msg, title=title):
    399                 return
    400             invoke_client('toggle_passphrase', unpair_after=currently_enabled)
    401 
    402         def set_pin():
    403             invoke_client('set_pin', remove=False)
    404 
    405         def clear_pin():
    406             invoke_client('set_pin', remove=True)
    407 
    408         def wipe_device():
    409             wallet = window.wallet
    410             if wallet and sum(wallet.get_balance()):
    411                 title = _("Confirm Device Wipe")
    412                 msg = _("Are you SURE you want to wipe the device?\n"
    413                         "Your wallet still has bitcoins in it!")
    414                 if not self.question(msg, title=title,
    415                                      icon=QMessageBox.Critical):
    416                     return
    417             invoke_client('wipe_device', unpair_after=True)
    418 
    419         def slider_moved():
    420             mins = timeout_slider.sliderPosition()
    421             timeout_minutes.setText(_("{:2d} minutes").format(mins))
    422 
    423         def slider_released():
    424             config.set_session_timeout(timeout_slider.sliderPosition() * 60)
    425 
    426         # Information tab
    427         info_tab = QWidget()
    428         info_layout = QVBoxLayout(info_tab)
    429         info_glayout = QGridLayout()
    430         info_glayout.setColumnStretch(2, 1)
    431         device_label = QLabel()
    432         pin_set_label = QLabel()
    433         passphrases_label = QLabel()
    434         version_label = QLabel()
    435         device_id_label = QLabel()
    436         bl_hash_label = QLabel()
    437         bl_hash_label.setWordWrap(True)
    438         coins_label = QLabel()
    439         coins_label.setWordWrap(True)
    440         language_label = QLabel()
    441         initialized_label = QLabel()
    442         rows = [
    443             (_("Device Label"), device_label),
    444             (_("PIN set"), pin_set_label),
    445             (_("Passphrases"), passphrases_label),
    446             (_("Firmware Version"), version_label),
    447             (_("Device ID"), device_id_label),
    448             (_("Bootloader Hash"), bl_hash_label),
    449             (_("Supported Coins"), coins_label),
    450             (_("Language"), language_label),
    451             (_("Initialized"), initialized_label),
    452         ]
    453         for row_num, (label, widget) in enumerate(rows):
    454             info_glayout.addWidget(QLabel(label), row_num, 0)
    455             info_glayout.addWidget(widget, row_num, 1)
    456         info_layout.addLayout(info_glayout)
    457 
    458         # Settings tab
    459         settings_tab = QWidget()
    460         settings_layout = QVBoxLayout(settings_tab)
    461         settings_glayout = QGridLayout()
    462 
    463         # Settings tab - Label
    464         label_msg = QLabel(_("Name this {}.  If you have multiple devices "
    465                              "their labels help distinguish them.")
    466                            .format(plugin.device))
    467         label_msg.setWordWrap(True)
    468         label_label = QLabel(_("Device Label"))
    469         label_edit = QLineEdit()
    470         label_edit.setMinimumWidth(150)
    471         label_edit.setMaxLength(plugin.MAX_LABEL_LEN)
    472         label_apply = QPushButton(_("Apply"))
    473         label_apply.clicked.connect(rename)
    474         label_edit.textChanged.connect(set_label_enabled)
    475         settings_glayout.addWidget(label_label, 0, 0)
    476         settings_glayout.addWidget(label_edit, 0, 1, 1, 2)
    477         settings_glayout.addWidget(label_apply, 0, 3)
    478         settings_glayout.addWidget(label_msg, 1, 1, 1, -1)
    479 
    480         # Settings tab - PIN
    481         pin_label = QLabel(_("PIN Protection"))
    482         pin_button = QPushButton()
    483         pin_button.clicked.connect(set_pin)
    484         settings_glayout.addWidget(pin_label, 2, 0)
    485         settings_glayout.addWidget(pin_button, 2, 1)
    486         pin_msg = QLabel(_("PIN protection is strongly recommended.  "
    487                            "A PIN is your only protection against someone "
    488                            "stealing your bitcoins if they obtain physical "
    489                            "access to your {}.").format(plugin.device))
    490         pin_msg.setWordWrap(True)
    491         pin_msg.setStyleSheet("color: red")
    492         settings_glayout.addWidget(pin_msg, 3, 1, 1, -1)
    493 
    494         # Settings tab - Session Timeout
    495         timeout_label = QLabel(_("Session Timeout"))
    496         timeout_minutes = QLabel()
    497         timeout_slider = QSlider(Qt.Horizontal)
    498         timeout_slider.setRange(1, 60)
    499         timeout_slider.setSingleStep(1)
    500         timeout_slider.setTickInterval(5)
    501         timeout_slider.setTickPosition(QSlider.TicksBelow)
    502         timeout_slider.setTracking(True)
    503         timeout_msg = QLabel(
    504             _("Clear the session after the specified period "
    505               "of inactivity.  Once a session has timed out, "
    506               "your PIN and passphrase (if enabled) must be "
    507               "re-entered to use the device."))
    508         timeout_msg.setWordWrap(True)
    509         timeout_slider.setSliderPosition(config.get_session_timeout() // 60)
    510         slider_moved()
    511         timeout_slider.valueChanged.connect(slider_moved)
    512         timeout_slider.sliderReleased.connect(slider_released)
    513         settings_glayout.addWidget(timeout_label, 6, 0)
    514         settings_glayout.addWidget(timeout_slider, 6, 1, 1, 3)
    515         settings_glayout.addWidget(timeout_minutes, 6, 4)
    516         settings_glayout.addWidget(timeout_msg, 7, 1, 1, -1)
    517         settings_layout.addLayout(settings_glayout)
    518         settings_layout.addStretch(1)
    519 
    520         # Advanced tab
    521         advanced_tab = QWidget()
    522         advanced_layout = QVBoxLayout(advanced_tab)
    523         advanced_glayout = QGridLayout()
    524 
    525         # Advanced tab - clear PIN
    526         clear_pin_button = QPushButton(_("Disable PIN"))
    527         clear_pin_button.clicked.connect(clear_pin)
    528         clear_pin_warning = QLabel(
    529             _("If you disable your PIN, anyone with physical access to your "
    530               "{} device can spend your bitcoins.").format(plugin.device))
    531         clear_pin_warning.setWordWrap(True)
    532         clear_pin_warning.setStyleSheet("color: red")
    533         advanced_glayout.addWidget(clear_pin_button, 0, 2)
    534         advanced_glayout.addWidget(clear_pin_warning, 1, 0, 1, 5)
    535 
    536         # Advanced tab - toggle passphrase protection
    537         passphrase_button = QPushButton()
    538         passphrase_button.clicked.connect(toggle_passphrase)
    539         passphrase_msg = WWLabel(PASSPHRASE_HELP)
    540         passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN)
    541         passphrase_warning.setStyleSheet("color: red")
    542         advanced_glayout.addWidget(passphrase_button, 3, 2)
    543         advanced_glayout.addWidget(passphrase_msg, 4, 0, 1, 5)
    544         advanced_glayout.addWidget(passphrase_warning, 5, 0, 1, 5)
    545 
    546         # Advanced tab - wipe device
    547         wipe_device_button = QPushButton(_("Wipe Device"))
    548         wipe_device_button.clicked.connect(wipe_device)
    549         wipe_device_msg = QLabel(
    550             _("Wipe the device, removing all data from it.  The firmware "
    551               "is left unchanged."))
    552         wipe_device_msg.setWordWrap(True)
    553         wipe_device_warning = QLabel(
    554             _("Only wipe a device if you have the recovery seed written down "
    555               "and the device wallet(s) are empty, otherwise the bitcoins "
    556               "will be lost forever."))
    557         wipe_device_warning.setWordWrap(True)
    558         wipe_device_warning.setStyleSheet("color: red")
    559         advanced_glayout.addWidget(wipe_device_button, 6, 2)
    560         advanced_glayout.addWidget(wipe_device_msg, 7, 0, 1, 5)
    561         advanced_glayout.addWidget(wipe_device_warning, 8, 0, 1, 5)
    562         advanced_layout.addLayout(advanced_glayout)
    563         advanced_layout.addStretch(1)
    564 
    565         tabs = QTabWidget(self)
    566         tabs.addTab(info_tab, _("Information"))
    567         tabs.addTab(settings_tab, _("Settings"))
    568         tabs.addTab(advanced_tab, _("Advanced"))
    569         dialog_vbox = QVBoxLayout(self)
    570         dialog_vbox.addWidget(tabs)
    571         dialog_vbox.addLayout(Buttons(CloseButton(self)))
    572 
    573         # Update information
    574         invoke_client(None)