tqt.py - electrum - Electrum Bitcoin wallet
HTML git clone https://git.parazyd.org/electrum
DIR Log
DIR Files
DIR Refs
DIR Submodules
---
tqt.py (30784B)
---
1 from functools import partial
2 import threading
3
4 from PyQt5.QtCore import Qt, QEventLoop, pyqtSignal
5 from PyQt5.QtWidgets import (QVBoxLayout, QLabel, QGridLayout, QPushButton,
6 QHBoxLayout, QButtonGroup, QGroupBox, QDialog,
7 QLineEdit, QRadioButton, QCheckBox, QWidget,
8 QMessageBox, QFileDialog, QSlider, QTabWidget)
9
10 from electrum.gui.qt.util import (WindowModalDialog, WWLabel, Buttons, CancelButton,
11 OkButton, CloseButton, PasswordLineEdit, getOpenFileName)
12 from electrum.i18n import _
13 from electrum.plugin import hook
14 from electrum.util import bh2u
15
16 from ..hw_wallet.qt import QtHandlerBase, QtPluginBase
17 from ..hw_wallet.plugin import only_hook_if_libraries_available
18 from .trezor import (TrezorPlugin, TIM_NEW, TIM_RECOVER, TrezorInitSettings,
19 PASSPHRASE_ON_DEVICE, Capability, BackupType, RecoveryDeviceType)
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 MATRIX_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")
41 SEEDLESS_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
47 class 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
109 class 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
232 class 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
442 class 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
455 class 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)