qt.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)