1from functools import partial
2import threading
3
4from PyQt5.QtCore import Qt, QEventLoop, pyqtSignal
5from PyQt5.QtWidgets import (QVBoxLayout, QLabel, QGridLayout, QPushButton,
6                             QHBoxLayout, QButtonGroup, QGroupBox, QDialog,
7                             QLineEdit, QRadioButton, QCheckBox, QWidget,
8                             QMessageBox, QFileDialog, QSlider, QTabWidget)
9
10from electrum.gui.qt.util import (WindowModalDialog, WWLabel, Buttons, CancelButton,
11                                  OkButton, CloseButton, PasswordLineEdit, getOpenFileName)
12from electrum.i18n import _
13from electrum.plugin import hook
14from electrum.util import bh2u
15
16from ..hw_wallet.qt import QtHandlerBase, QtPluginBase
17from ..hw_wallet.plugin import only_hook_if_libraries_available
18from .trezor import (TrezorPlugin, TIM_NEW, TIM_RECOVER, TrezorInitSettings,
19                     PASSPHRASE_ON_DEVICE, Capability, BackupType, RecoveryDeviceType)
20
21
22PASSPHRASE_HELP_SHORT =_(
23    "Passphrases allow you to access new wallets, each "
24    "hidden behind a particular case-sensitive passphrase.")
25PASSPHRASE_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.")
30RECOMMEND_PIN = _(
31    "You should enable PIN protection.  Your PIN is the only protection "
32    "for your bitcoins if your device is lost or stolen.")
33PASSPHRASE_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.")
37MATRIX_RECOVERY = _(
38    "Enter the recovery words by pressing the buttons according to what "
39    "the device shows on its display.  You can also use your NUMPAD.\n"
40    "Press BACKSPACE to go back a choice or word.\n")
41SEEDLESS_MODE_WARNING = _(
42    "In seedless mode, the mnemonic seed words are never shown to the user.\n"
43    "There is no backup, and the user has a proof of this.\n"
44    "This is an advanced feature, only suggested to be used in redundant multisig setups.")
45
46
47class MatrixDialog(WindowModalDialog):
48
49    def __init__(self, parent):
50        super(MatrixDialog, self).__init__(parent)
51        self.setWindowTitle(_("Trezor Matrix Recovery"))
52        self.num = 9
53        self.loop = QEventLoop()
54
55        vbox = QVBoxLayout(self)
56        vbox.addWidget(WWLabel(MATRIX_RECOVERY))
57
58        grid = QGridLayout()
59        grid.setSpacing(0)
60        self.char_buttons = []
61        for y in range(3):
62            for x in range(3):
63                button = QPushButton('?')
64                button.clicked.connect(partial(self.process_key, ord('1') + y * 3 + x))
65                grid.addWidget(button, 3 - y, x)
66                self.char_buttons.append(button)
67        vbox.addLayout(grid)
68
69        self.backspace_button = QPushButton("<=")
70        self.backspace_button.clicked.connect(partial(self.process_key, Qt.Key_Backspace))
71        self.cancel_button = QPushButton(_("Cancel"))
72        self.cancel_button.clicked.connect(partial(self.process_key, Qt.Key_Escape))
73        buttons = Buttons(self.backspace_button, self.cancel_button)
74        vbox.addSpacing(40)
75        vbox.addLayout(buttons)
76        self.refresh()
77        self.show()
78
79    def refresh(self):
80        for y in range(3):
81            self.char_buttons[3 * y + 1].setEnabled(self.num == 9)
82
83    def is_valid(self, key):
84        return key >= ord('1') and key <= ord('9')
85
86    def process_key(self, key):
87        self.data = None
88        if key == Qt.Key_Backspace:
89            self.data = '\010'
90        elif key == Qt.Key_Escape:
91            self.data = 'x'
92        elif self.is_valid(key):
93            self.char_buttons[key - ord('1')].setFocus()
94            self.data = '%c' % key
95        if self.data:
96            self.loop.exit(0)
97
98    def keyPressEvent(self, event):
99        self.process_key(event.key())
100        if not self.data:
101            QDialog.keyPressEvent(self, event)
102
103    def get_matrix(self, num):
104        self.num = num
105        self.refresh()
106        self.loop.exec_()
107
108
109class QtHandler(QtHandlerBase):
110
111    pin_signal = pyqtSignal(object, object)
112    matrix_signal = pyqtSignal(object)
113    close_matrix_dialog_signal = pyqtSignal()
114
115    def __init__(self, win, pin_matrix_widget_class, device):
116        super(QtHandler, self).__init__(win, device)
117        self.pin_signal.connect(self.pin_dialog)
118        self.matrix_signal.connect(self.matrix_recovery_dialog)
119        self.close_matrix_dialog_signal.connect(self._close_matrix_dialog)
120        self.pin_matrix_widget_class = pin_matrix_widget_class
121        self.matrix_dialog = None
122        self.passphrase_on_device = False
123
124    def get_pin(self, msg, *, show_strength=True):
125        self.done.clear()
126        self.pin_signal.emit(msg, show_strength)
127        self.done.wait()
128        return self.response
129
130    def get_matrix(self, msg):
131        self.done.clear()
132        self.matrix_signal.emit(msg)
133        self.done.wait()
134        data = self.matrix_dialog.data
135        if data == 'x':
136            self.close_matrix_dialog()
137        return data
138
139    def _close_matrix_dialog(self):
140        if self.matrix_dialog:
141            self.matrix_dialog.accept()
142            self.matrix_dialog = None
143
144    def close_matrix_dialog(self):
145        self.close_matrix_dialog_signal.emit()
146
147    def pin_dialog(self, msg, show_strength):
148        # Needed e.g. when resetting a device
149        self.clear_dialog()
150        dialog = WindowModalDialog(self.top_level_window(), _("Enter PIN"))
151        matrix = self.pin_matrix_widget_class(show_strength)
152        vbox = QVBoxLayout()
153        vbox.addWidget(QLabel(msg))
154        vbox.addWidget(matrix)
155        vbox.addLayout(Buttons(CancelButton(dialog), OkButton(dialog)))
156        dialog.setLayout(vbox)
157        dialog.exec_()
158        self.response = str(matrix.get_value())
159        self.done.set()
160
161    def matrix_recovery_dialog(self, msg):
162        if not self.matrix_dialog:
163            self.matrix_dialog = MatrixDialog(self.top_level_window())
164        self.matrix_dialog.get_matrix(msg)
165        self.done.set()
166
167    def passphrase_dialog(self, msg, confirm):
168        # If confirm is true, require the user to enter the passphrase twice
169        parent = self.top_level_window()
170        d = WindowModalDialog(parent, _('Enter Passphrase'))
171
172        OK_button = OkButton(d, _('Enter Passphrase'))
173        OnDevice_button = QPushButton(_('Enter Passphrase on Device'))
174
175        new_pw = PasswordLineEdit()
176        conf_pw = PasswordLineEdit()
177
178        vbox = QVBoxLayout()
179        label = QLabel(msg + "\n")
180        label.setWordWrap(True)
181
182        grid = QGridLayout()
183        grid.setSpacing(8)
184        grid.setColumnMinimumWidth(0, 150)
185        grid.setColumnMinimumWidth(1, 100)
186        grid.setColumnStretch(1,1)
187
188        vbox.addWidget(label)
189
190        grid.addWidget(QLabel(_('Passphrase:')), 0, 0)
191        grid.addWidget(new_pw, 0, 1)
192
193        if confirm:
194            grid.addWidget(QLabel(_('Confirm Passphrase:')), 1, 0)
195            grid.addWidget(conf_pw, 1, 1)
196
197        vbox.addLayout(grid)
198
199        def enable_OK():
200            if not confirm:
201                ok = True
202            else:
203                ok = new_pw.text() == conf_pw.text()
204            OK_button.setEnabled(ok)
205
206        new_pw.textChanged.connect(enable_OK)
207        conf_pw.textChanged.connect(enable_OK)
208
209        vbox.addWidget(OK_button)
210
211        if self.passphrase_on_device:
212            vbox.addWidget(OnDevice_button)
213
214        d.setLayout(vbox)
215
216        self.passphrase = None
217
218        def ok_clicked():
219            self.passphrase = new_pw.text()
220
221        def on_device_clicked():
222            self.passphrase = PASSPHRASE_ON_DEVICE
223
224        OK_button.clicked.connect(ok_clicked)
225        OnDevice_button.clicked.connect(on_device_clicked)
226        OnDevice_button.clicked.connect(d.accept)
227
228        d.exec_()
229        self.done.set()
230
231
232class QtPlugin(QtPluginBase):
233    # Derived classes must provide the following class-static variables:
234    #   icon_file
235    #   pin_matrix_widget_class
236
237    @only_hook_if_libraries_available
238    @hook
239    def receive_menu(self, menu, addrs, wallet):
240        if len(addrs) != 1:
241            return
242        for keystore in wallet.get_keystores():
243            if type(keystore) == self.keystore_class:
244                def show_address(keystore=keystore):
245                    keystore.thread.add(partial(self.show_address, wallet, addrs[0], keystore))
246                device_name = "{} ({})".format(self.device, keystore.label)
247                menu.addAction(_("Show on {}").format(device_name), show_address)
248
249    def show_settings_dialog(self, window, keystore):
250        def connect():
251            device_id = self.choose_device(window, keystore)
252            return device_id
253        def show_dialog(device_id):
254            if device_id:
255                SettingsDialog(window, self, keystore, device_id).exec_()
256        keystore.thread.add(connect, on_success=show_dialog)
257
258    def request_trezor_init_settings(self, wizard, method, device_id):
259        vbox = QVBoxLayout()
260        next_enabled = True
261
262        devmgr = self.device_manager()
263        client = devmgr.client_by_id(device_id)
264        if not client:
265            raise Exception(_("The device was disconnected."))
266        model = client.get_trezor_model()
267        fw_version = client.client.version
268        capabilities = client.client.features.capabilities
269        have_shamir = Capability.Shamir in capabilities
270
271        # label
272        label = QLabel(_("Enter a label to name your device:"))
273        name = QLineEdit()
274        hl = QHBoxLayout()
275        hl.addWidget(label)
276        hl.addWidget(name)
277        hl.addStretch(1)
278        vbox.addLayout(hl)
279
280        # Backup type
281        gb_backuptype = QGroupBox()
282        hbox_backuptype = QHBoxLayout()
283        gb_backuptype.setLayout(hbox_backuptype)
284        vbox.addWidget(gb_backuptype)
285        gb_backuptype.setTitle(_('Select backup type:'))
286        bg_backuptype = QButtonGroup()
287
288        rb_single = QRadioButton(gb_backuptype)
289        rb_single.setText(_('Single seed (BIP39)'))
290        bg_backuptype.addButton(rb_single)
291        bg_backuptype.setId(rb_single, BackupType.Bip39)
292        hbox_backuptype.addWidget(rb_single)
293        rb_single.setChecked(True)
294
295        rb_shamir = QRadioButton(gb_backuptype)
296        rb_shamir.setText(_('Shamir'))
297        bg_backuptype.addButton(rb_shamir)
298        bg_backuptype.setId(rb_shamir, BackupType.Slip39_Basic)
299        hbox_backuptype.addWidget(rb_shamir)
300        rb_shamir.setEnabled(Capability.Shamir in capabilities)
301        rb_shamir.setVisible(False)  # visible with "expert settings"
302
303        rb_shamir_groups = QRadioButton(gb_backuptype)
304        rb_shamir_groups.setText(_('Super Shamir'))
305        bg_backuptype.addButton(rb_shamir_groups)
306        bg_backuptype.setId(rb_shamir_groups, BackupType.Slip39_Advanced)
307        hbox_backuptype.addWidget(rb_shamir_groups)
308        rb_shamir_groups.setEnabled(Capability.ShamirGroups in capabilities)
309        rb_shamir_groups.setVisible(False)  # visible with "expert settings"
310
311        # word count
312        word_count_buttons = {}
313
314        gb_numwords = QGroupBox()
315        hbox1 = QHBoxLayout()
316        gb_numwords.setLayout(hbox1)
317        vbox.addWidget(gb_numwords)
318        gb_numwords.setTitle(_("Select seed/share length:"))
319        bg_numwords = QButtonGroup()
320        for count in (12, 18, 20, 24, 33):
321            rb = QRadioButton(gb_numwords)
322            word_count_buttons[count] = rb
323            rb.setText(_("{:d} words").format(count))
324            bg_numwords.addButton(rb)
325            bg_numwords.setId(rb, count)
326            hbox1.addWidget(rb)
327            rb.setChecked(True)
328
329        def configure_word_counts():
330            if model == "1":
331                checked_wordcount = 24
332            else:
333                checked_wordcount = 12
334
335            if method == TIM_RECOVER:
336                if have_shamir:
337                    valid_word_counts = (12, 18, 20, 24, 33)
338                else:
339                    valid_word_counts = (12, 18, 24)
340            elif rb_single.isChecked():
341                valid_word_counts = (12, 18, 24)
342                gb_numwords.setTitle(_('Select seed length:'))
343            else:
344                valid_word_counts = (20, 33)
345                checked_wordcount = 20
346                gb_numwords.setTitle(_('Select share length:'))
347
348            word_count_buttons[checked_wordcount].setChecked(True)
349            for c, btn in word_count_buttons.items():
350                btn.setVisible(c in valid_word_counts)
351
352        bg_backuptype.buttonClicked.connect(configure_word_counts)
353        configure_word_counts()
354
355        # set up conditional visibility:
356        # 1. backup_type is only visible when creating new seed
357        gb_backuptype.setVisible(method == TIM_NEW)
358        # 2. word_count is not visible when recovering on TT
359        if method == TIM_RECOVER and model != "1":
360            gb_numwords.setVisible(False)
361
362        # PIN
363        cb_pin = QCheckBox(_('Enable PIN protection'))
364        cb_pin.setChecked(True)
365        vbox.addWidget(WWLabel(RECOMMEND_PIN))
366        vbox.addWidget(cb_pin)
367
368        # "expert settings" button
369        expert_vbox = QVBoxLayout()
370        expert_widget = QWidget()
371        expert_widget.setLayout(expert_vbox)
372        expert_widget.setVisible(False)
373        expert_button = QPushButton(_("Show expert settings"))
374        def show_expert_settings():
375            expert_button.setVisible(False)
376            expert_widget.setVisible(True)
377            rb_shamir.setVisible(True)
378            rb_shamir_groups.setVisible(True)
379        expert_button.clicked.connect(show_expert_settings)
380        vbox.addWidget(expert_button)
381
382        # passphrase
383        passphrase_msg = WWLabel(PASSPHRASE_HELP_SHORT)
384        passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN)
385        passphrase_warning.setStyleSheet("color: red")
386        cb_phrase = QCheckBox(_('Enable passphrases'))
387        cb_phrase.setChecked(False)
388        expert_vbox.addWidget(passphrase_msg)
389        expert_vbox.addWidget(passphrase_warning)
390        expert_vbox.addWidget(cb_phrase)
391
392        # ask for recovery type (random word order OR matrix)
393        bg_rectype = None
394        if method == TIM_RECOVER and model == '1':
395            gb_rectype = QGroupBox()
396            hbox_rectype = QHBoxLayout()
397            gb_rectype.setLayout(hbox_rectype)
398            expert_vbox.addWidget(gb_rectype)
399            gb_rectype.setTitle(_("Select recovery type:"))
400            bg_rectype = QButtonGroup()
401
402            rb1 = QRadioButton(gb_rectype)
403            rb1.setText(_('Scrambled words'))
404            bg_rectype.addButton(rb1)
405            bg_rectype.setId(rb1, RecoveryDeviceType.ScrambledWords)
406            hbox_rectype.addWidget(rb1)
407            rb1.setChecked(True)
408
409            rb2 = QRadioButton(gb_rectype)
410            rb2.setText(_('Matrix'))
411            bg_rectype.addButton(rb2)
412            bg_rectype.setId(rb2, RecoveryDeviceType.Matrix)
413            hbox_rectype.addWidget(rb2)
414
415        # no backup
416        cb_no_backup = None
417        if method == TIM_NEW:
418            cb_no_backup = QCheckBox(f'''{_('Enable seedless mode')}''')
419            cb_no_backup.setChecked(False)
420            if (model == '1' and fw_version >= (1, 7, 1)
421                    or model == 'T' and fw_version >= (2, 0, 9)):
422                cb_no_backup.setToolTip(SEEDLESS_MODE_WARNING)
423            else:
424                cb_no_backup.setEnabled(False)
425                cb_no_backup.setToolTip(_('Firmware version too old.'))
426            expert_vbox.addWidget(cb_no_backup)
427
428        vbox.addWidget(expert_widget)
429        wizard.exec_layout(vbox, next_enabled=next_enabled)
430
431        return TrezorInitSettings(
432            word_count=bg_numwords.checkedId(),
433            label=name.text(),
434            pin_enabled=cb_pin.isChecked(),
435            passphrase_enabled=cb_phrase.isChecked(),
436            recovery_type=bg_rectype.checkedId() if bg_rectype else None,
437            backup_type=bg_backuptype.checkedId(),
438            no_backup=cb_no_backup.isChecked() if cb_no_backup else False,
439        )
440
441
442class Plugin(TrezorPlugin, QtPlugin):
443    icon_unpaired = "trezor_unpaired.png"
444    icon_paired = "trezor.png"
445
446    def create_handler(self, window):
447        return QtHandler(window, self.pin_matrix_widget_class(), self.device)
448
449    @classmethod
450    def pin_matrix_widget_class(self):
451        from trezorlib.qt.pinmatrix import PinMatrixWidget
452        return PinMatrixWidget
453
454
455class SettingsDialog(WindowModalDialog):
456    '''This dialog doesn't require a device be paired with a wallet.
457    We want users to be able to wipe a device even if they've forgotten
458    their PIN.'''
459
460    def __init__(self, window, plugin, keystore, device_id):
461        title = _("{} Settings").format(plugin.device)
462        super(SettingsDialog, self).__init__(window, title)
463        self.setMaximumWidth(540)
464
465        devmgr = plugin.device_manager()
466        config = devmgr.config
467        handler = keystore.handler
468        thread = keystore.thread
469        hs_cols, hs_rows = (128, 64)
470
471        def invoke_client(method, *args, **kw_args):
472            unpair_after = kw_args.pop('unpair_after', False)
473
474            def task():
475                client = devmgr.client_by_id(device_id)
476                if not client:
477                    raise RuntimeError("Device not connected")
478                if method:
479                    getattr(client, method)(*args, **kw_args)
480                if unpair_after:
481                    devmgr.unpair_id(device_id)
482                return client.features
483
484            thread.add(task, on_success=update)
485
486        def update(features):
487            self.features = features
488            set_label_enabled()
489            if features.bootloader_hash:
490                bl_hash = bh2u(features.bootloader_hash)
491                bl_hash = "\n".join([bl_hash[:32], bl_hash[32:]])
492            else:
493                bl_hash = "N/A"
494            noyes = [_("No"), _("Yes")]
495            endis = [_("Enable Passphrases"), _("Disable Passphrases")]
496            disen = [_("Disabled"), _("Enabled")]
497            setchange = [_("Set a PIN"), _("Change PIN")]
498
499            version = "%d.%d.%d" % (features.major_version,
500                                    features.minor_version,
501                                    features.patch_version)
502
503            device_label.setText(features.label)
504            pin_set_label.setText(noyes[features.pin_protection])
505            passphrases_label.setText(disen[features.passphrase_protection])
506            bl_hash_label.setText(bl_hash)
507            label_edit.setText(features.label)
508            device_id_label.setText(features.device_id)
509            initialized_label.setText(noyes[features.initialized])
510            version_label.setText(version)
511            clear_pin_button.setVisible(features.pin_protection)
512            clear_pin_warning.setVisible(features.pin_protection)
513            pin_button.setText(setchange[features.pin_protection])
514            pin_msg.setVisible(not features.pin_protection)
515            passphrase_button.setText(endis[features.passphrase_protection])
516            language_label.setText(features.language)
517
518        def set_label_enabled():
519            label_apply.setEnabled(label_edit.text() != self.features.label)
520
521        def rename():
522            invoke_client('change_label', label_edit.text())
523
524        def toggle_passphrase():
525            title = _("Confirm Toggle Passphrase Protection")
526            currently_enabled = self.features.passphrase_protection
527            if currently_enabled:
528                msg = _("After disabling passphrases, you can only pair this "
529                        "Electrum wallet if it had an empty passphrase.  "
530                        "If its passphrase was not empty, you will need to "
531                        "create a new wallet with the install wizard.  You "
532                        "can use this wallet again at any time by re-enabling "
533                        "passphrases and entering its passphrase.")
534            else:
535                msg = _("Your current Electrum wallet can only be used with "
536                        "an empty passphrase.  You must create a separate "
537                        "wallet with the install wizard for other passphrases "
538                        "as each one generates a new set of addresses.")
539            msg += "\n\n" + _("Are you sure you want to proceed?")
540            if not self.question(msg, title=title):
541                return
542            invoke_client('toggle_passphrase', unpair_after=currently_enabled)
543
544        def change_homescreen():
545            filename = getOpenFileName(
546                parent=self,
547                title=_("Choose Homescreen"),
548                config=config,
549            )
550            if not filename:
551                return  # user cancelled
552
553            if filename.endswith('.toif'):
554                img = open(filename, 'rb').read()
555                if img[:8] != b'TOIf\x90\x00\x90\x00':
556                    handler.show_error('File is not a TOIF file with size of 144x144')
557                    return
558            else:
559                from PIL import Image # FIXME
560                im = Image.open(filename)
561                if im.size != (128, 64):
562                    handler.show_error('Image must be 128 x 64 pixels')
563                    return
564                im = im.convert('1')
565                pix = im.load()
566                img = bytearray(1024)
567                for j in range(64):
568                    for i in range(128):
569                        if pix[i, j]:
570                            o = (i + j * 128)
571                            img[o // 8] |= (1 << (7 - o % 8))
572                img = bytes(img)
573            invoke_client('change_homescreen', img)
574
575        def clear_homescreen():
576            invoke_client('change_homescreen', b'\x00')
577
578        def set_pin():
579            invoke_client('set_pin', remove=False)
580
581        def clear_pin():
582            invoke_client('set_pin', remove=True)
583
584        def wipe_device():
585            wallet = window.wallet
586            if wallet and sum(wallet.get_balance()):
587                title = _("Confirm Device Wipe")
588                msg = _("Are you SURE you want to wipe the device?\n"
589                        "Your wallet still has bitcoins in it!")
590                if not self.question(msg, title=title,
591                                     icon=QMessageBox.Critical):
592                    return
593            invoke_client('wipe_device', unpair_after=True)
594
595        def slider_moved():
596            mins = timeout_slider.sliderPosition()
597            timeout_minutes.setText(_("{:2d} minutes").format(mins))
598
599        def slider_released():
600            config.set_session_timeout(timeout_slider.sliderPosition() * 60)
601
602        # Information tab
603        info_tab = QWidget()
604        info_layout = QVBoxLayout(info_tab)
605        info_glayout = QGridLayout()
606        info_glayout.setColumnStretch(2, 1)
607        device_label = QLabel()
608        pin_set_label = QLabel()
609        passphrases_label = QLabel()
610        version_label = QLabel()
611        device_id_label = QLabel()
612        bl_hash_label = QLabel()
613        bl_hash_label.setWordWrap(True)
614        language_label = QLabel()
615        initialized_label = QLabel()
616        rows = [
617            (_("Device Label"), device_label),
618            (_("PIN set"), pin_set_label),
619            (_("Passphrases"), passphrases_label),
620            (_("Firmware Version"), version_label),
621            (_("Device ID"), device_id_label),
622            (_("Bootloader Hash"), bl_hash_label),
623            (_("Language"), language_label),
624            (_("Initialized"), initialized_label),
625        ]
626        for row_num, (label, widget) in enumerate(rows):
627            info_glayout.addWidget(QLabel(label), row_num, 0)
628            info_glayout.addWidget(widget, row_num, 1)
629        info_layout.addLayout(info_glayout)
630
631        # Settings tab
632        settings_tab = QWidget()
633        settings_layout = QVBoxLayout(settings_tab)
634        settings_glayout = QGridLayout()
635
636        # Settings tab - Label
637        label_msg = QLabel(_("Name this {}.  If you have multiple devices "
638                             "their labels help distinguish them.")
639                           .format(plugin.device))
640        label_msg.setWordWrap(True)
641        label_label = QLabel(_("Device Label"))
642        label_edit = QLineEdit()
643        label_edit.setMinimumWidth(150)
644        label_edit.setMaxLength(plugin.MAX_LABEL_LEN)
645        label_apply = QPushButton(_("Apply"))
646        label_apply.clicked.connect(rename)
647        label_edit.textChanged.connect(set_label_enabled)
648        settings_glayout.addWidget(label_label, 0, 0)
649        settings_glayout.addWidget(label_edit, 0, 1, 1, 2)
650        settings_glayout.addWidget(label_apply, 0, 3)
651        settings_glayout.addWidget(label_msg, 1, 1, 1, -1)
652
653        # Settings tab - PIN
654        pin_label = QLabel(_("PIN Protection"))
655        pin_button = QPushButton()
656        pin_button.clicked.connect(set_pin)
657        settings_glayout.addWidget(pin_label, 2, 0)
658        settings_glayout.addWidget(pin_button, 2, 1)
659        pin_msg = QLabel(_("PIN protection is strongly recommended.  "
660                           "A PIN is your only protection against someone "
661                           "stealing your bitcoins if they obtain physical "
662                           "access to your {}.").format(plugin.device))
663        pin_msg.setWordWrap(True)
664        pin_msg.setStyleSheet("color: red")
665        settings_glayout.addWidget(pin_msg, 3, 1, 1, -1)
666
667        # Settings tab - Homescreen
668        homescreen_label = QLabel(_("Homescreen"))
669        homescreen_change_button = QPushButton(_("Change..."))
670        homescreen_clear_button = QPushButton(_("Reset"))
671        homescreen_change_button.clicked.connect(change_homescreen)
672        try:
673            import PIL
674        except ImportError:
675            homescreen_change_button.setDisabled(True)
676            homescreen_change_button.setToolTip(
677                _("Required package 'PIL' is not available - Please install it or use the Trezor website instead.")
678            )
679        homescreen_clear_button.clicked.connect(clear_homescreen)
680        homescreen_msg = QLabel(_("You can set the homescreen on your "
681                                  "device to personalize it.  You must "
682                                  "choose a {} x {} monochrome black and "
683                                  "white image.").format(hs_cols, hs_rows))
684        homescreen_msg.setWordWrap(True)
685        settings_glayout.addWidget(homescreen_label, 4, 0)
686        settings_glayout.addWidget(homescreen_change_button, 4, 1)
687        settings_glayout.addWidget(homescreen_clear_button, 4, 2)
688        settings_glayout.addWidget(homescreen_msg, 5, 1, 1, -1)
689
690        # Settings tab - Session Timeout
691        timeout_label = QLabel(_("Session Timeout"))
692        timeout_minutes = QLabel()
693        timeout_slider = QSlider(Qt.Horizontal)
694        timeout_slider.setRange(1, 60)
695        timeout_slider.setSingleStep(1)
696        timeout_slider.setTickInterval(5)
697        timeout_slider.setTickPosition(QSlider.TicksBelow)
698        timeout_slider.setTracking(True)
699        timeout_msg = QLabel(
700            _("Clear the session after the specified period "
701              "of inactivity.  Once a session has timed out, "
702              "your PIN and passphrase (if enabled) must be "
703              "re-entered to use the device."))
704        timeout_msg.setWordWrap(True)
705        timeout_slider.setSliderPosition(config.get_session_timeout() // 60)
706        slider_moved()
707        timeout_slider.valueChanged.connect(slider_moved)
708        timeout_slider.sliderReleased.connect(slider_released)
709        settings_glayout.addWidget(timeout_label, 6, 0)
710        settings_glayout.addWidget(timeout_slider, 6, 1, 1, 3)
711        settings_glayout.addWidget(timeout_minutes, 6, 4)
712        settings_glayout.addWidget(timeout_msg, 7, 1, 1, -1)
713        settings_layout.addLayout(settings_glayout)
714        settings_layout.addStretch(1)
715
716        # Advanced tab
717        advanced_tab = QWidget()
718        advanced_layout = QVBoxLayout(advanced_tab)
719        advanced_glayout = QGridLayout()
720
721        # Advanced tab - clear PIN
722        clear_pin_button = QPushButton(_("Disable PIN"))
723        clear_pin_button.clicked.connect(clear_pin)
724        clear_pin_warning = QLabel(
725            _("If you disable your PIN, anyone with physical access to your "
726              "{} device can spend your bitcoins.").format(plugin.device))
727        clear_pin_warning.setWordWrap(True)
728        clear_pin_warning.setStyleSheet("color: red")
729        advanced_glayout.addWidget(clear_pin_button, 0, 2)
730        advanced_glayout.addWidget(clear_pin_warning, 1, 0, 1, 5)
731
732        # Advanced tab - toggle passphrase protection
733        passphrase_button = QPushButton()
734        passphrase_button.clicked.connect(toggle_passphrase)
735        passphrase_msg = WWLabel(PASSPHRASE_HELP)
736        passphrase_warning = WWLabel(PASSPHRASE_NOT_PIN)
737        passphrase_warning.setStyleSheet("color: red")
738        advanced_glayout.addWidget(passphrase_button, 3, 2)
739        advanced_glayout.addWidget(passphrase_msg, 4, 0, 1, 5)
740        advanced_glayout.addWidget(passphrase_warning, 5, 0, 1, 5)
741
742        # Advanced tab - wipe device
743        wipe_device_button = QPushButton(_("Wipe Device"))
744        wipe_device_button.clicked.connect(wipe_device)
745        wipe_device_msg = QLabel(
746            _("Wipe the device, removing all data from it.  The firmware "
747              "is left unchanged."))
748        wipe_device_msg.setWordWrap(True)
749        wipe_device_warning = QLabel(
750            _("Only wipe a device if you have the recovery seed written down "
751              "and the device wallet(s) are empty, otherwise the bitcoins "
752              "will be lost forever."))
753        wipe_device_warning.setWordWrap(True)
754        wipe_device_warning.setStyleSheet("color: red")
755        advanced_glayout.addWidget(wipe_device_button, 6, 2)
756        advanced_glayout.addWidget(wipe_device_msg, 7, 0, 1, 5)
757        advanced_glayout.addWidget(wipe_device_warning, 8, 0, 1, 5)
758        advanced_layout.addLayout(advanced_glayout)
759        advanced_layout.addStretch(1)
760
761        tabs = QTabWidget(self)
762        tabs.addTab(info_tab, _("Information"))
763        tabs.addTab(settings_tab, _("Settings"))
764        tabs.addTab(advanced_tab, _("Advanced"))
765        dialog_vbox = QVBoxLayout(self)
766        dialog_vbox.addWidget(tabs)
767        dialog_vbox.addLayout(Buttons(CloseButton(self)))
768
769        # Update information
770        invoke_client(None)
771