tTreozr: improved settings screen - electrum - Electrum Bitcoin wallet
 (HTM) git clone https://git.parazyd.org/electrum
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) Submodules
       ---
 (DIR) commit a3bced210d8b66fc52ecf6ddaae63daa4fae0f44
 (DIR) parent b21ea13940c123bfebc82b8607e3131446754717
 (HTM) Author: Neil Booth <kyuupichan@gmail.com>
       Date:   Fri,  8 Jan 2016 00:31:02 +0900
       
       Treozr: improved settings screen
       
       Nicer layout.  Split into three tabs.
       
       Diffstat:
         M plugins/trezor/client.py            |       8 ++++++++
         M plugins/trezor/plugin.py            |       8 +++-----
         M plugins/trezor/qt_generic.py        |     332 +++++++++++++++++++++----------
       
       3 files changed, 238 insertions(+), 110 deletions(-)
       ---
 (DIR) diff --git a/plugins/trezor/client.py b/plugins/trezor/client.py
       t@@ -14,6 +14,7 @@ class GuiMixin(object):
                10: _("Confirm address on %s device to continue"),
                'change pin': _("Confirm PIN change on %s device to continue"),
                'default': _("Check %s device to continue"),
       +        'homescreen': _("Confirm home screen change on %s device to continue"),
                'label': _("Confirm label change on %s device to continue"),
                'remove pin': _("Confirm removal of PIN on %s device to continue"),
                'passphrase': _("Confirm on %s device to continue"),
       t@@ -142,6 +143,13 @@ def trezor_client_class(protocol_mixin, base_client, proto):
                    finally:
                        self.msg_code_override = None
        
       +        def change_homescreen(self, homescreen):
       +            self.msg_code_override = 'homescreen'
       +            try:
       +                self.apply_settings(homescreen=homescreen)
       +            finally:
       +                self.msg_code_override = None
       +
                def set_pin(self, remove):
                    self.msg_code_override = 'remove pin' if remove else 'change pin'
                    try:
 (DIR) diff --git a/plugins/trezor/plugin.py b/plugins/trezor/plugin.py
       t@@ -60,10 +60,6 @@ class TrezorCompatibleWallet(BIP44_Wallet):
                self.print_error("connected")
                self.handler.watching_only_changed()
        
       -    def wiped(self):
       -        self.print_error("wiped")
       -        self.handler.watching_only_changed()
       -
            def timeout(self):
                '''Informs the wallet it timed out.  Note this is called from
                the Plugins thread.'''
       t@@ -164,6 +160,8 @@ class TrezorCompatiblePlugin(BasePlugin, ThreadJob):
            #     libraries_available, libraries_URL, minimum_firmware,
            #     wallet_class, ckd_public, types, HidTransport
        
       +    MAX_LABEL_LEN = 32
       +
            def __init__(self, parent, config, name):
                BasePlugin.__init__(self, parent, config, name)
                self.device = self.wallet_class.device
       t@@ -257,7 +255,7 @@ class TrezorCompatiblePlugin(BasePlugin, ThreadJob):
                        "The first two are secure as no secret information is entered "
                        "onto your computer.\nFor the last two methods you enter "
                        "secrets into your computer and upload them to the device, "
       -                "and so should only be done on a computer you know to be "
       +                "and so you should do those on a computer you know to be "
                        "trustworthy and free of malware."
                ) % self.device
        
 (DIR) diff --git a/plugins/trezor/qt_generic.py b/plugins/trezor/qt_generic.py
       t@@ -1,5 +1,6 @@
        from functools import partial
        import threading
       +from PIL import Image
        
        from PyQt4.Qt import Qt
        from PyQt4.Qt import QGridLayout, QInputDialog, QPushButton
       t@@ -168,47 +169,63 @@ def qt_plugin_class(base_plugin_class):
                                   lambda: self.show_address(wallet, addrs[0]))
        
            def settings_dialog(self, window):
       +        dialog = SettingsDialog(window, self)
       +        window.wallet.handler.exec_dialog(dialog)
       +
       +  return QtPlugin
        
       -        def get_client(lookup=DeviceMgr.PAIRED):
       -            return self.get_client(wallet, lookup)
        
       -        def add_rows_to_layout(layout, rows):
       -            for row_num, items in enumerate(rows):
       -                for col_num, txt in enumerate(items):
       -                    widget = txt if isinstance(txt, QWidget) else QLabel(txt)
       -                    layout.addWidget(widget, row_num, col_num)
       +class SettingsDialog(WindowModalDialog):
       +
       +    def __init__(self, window, plugin):
       +        self.plugin = plugin
       +        self.window = window  # The main electrum window
       +        title = _("%s Settings") % plugin.device
       +        super(SettingsDialog, self).__init__(window, title)
       +        self.setMaximumWidth(540)
       +        hs_rows, hs_cols = (64, 128)
       +
       +        def get_client(lookup=DeviceMgr.PAIRED):
       +            return self.plugin.get_client(wallet, lookup)
        
       -        def refresh():
       +        def update():
                    features = get_client(DeviceMgr.PAIRED).features
       -            bl_hash = features.bootloader_hash.encode('hex').upper()
       -            bl_hash = "%s...%s" % (bl_hash[:10], bl_hash[-10:])
       +            self.features = features
       +            # The above was for outer scopes.  Now the real logic.
       +            set_label_enabled()
       +            bl_hash = features.bootloader_hash.encode('hex')
       +            bl_hash = "\n".join([bl_hash[:32], bl_hash[32:]])
       +            noyes = [_("No"), _("Yes")]
       +            endis = [_("Enable Passphrases"), _("Disable Passphrases")]
       +            setchange = [_("Set a PIN"), _("Change PIN")]
       +
                    version = "%d.%d.%d" % (features.major_version,
                                            features.minor_version,
                                            features.patch_version)
                    coins = ", ".join(coin.coin_name for coin in features.coins)
        
       -            bl_hash_label.setText(bl_hash)
                    device_label.setText(features.label)
       +            pin_set_label.setText(noyes[features.pin_protection])
       +            bl_hash_label.setText(bl_hash)
       +            label_edit.setText(features.label)
                    device_id_label.setText(features.device_id)
                    initialized_label.setText(noyes[features.initialized])
                    version_label.setText(version)
                    coins_label.setText(coins)
       -            pin_label.setText(noyes[features.pin_protection])
       -            passphrase_label.setText(noyes[features.passphrase_protection])
       +            clear_pin_button.setVisible(features.pin_protection)
       +            clear_pin_warning.setVisible(features.pin_protection)
       +            pin_button.setText(setchange[features.pin_protection])
       +            pin_msg.setVisible(not features.pin_protection)
       +            passphrase_button.setText(endis[features.passphrase_protection])
       +
                    language_label.setText(features.language)
        
       -            pin_button.setText(_("Change") if features.pin_protection
       -                               else _("Set"))
       -            clear_pin_button.setVisible(features.pin_protection)
       +        def set_label_enabled():
       +            label_apply.setEnabled(label_edit.text() != self.features.label)
        
                def rename():
       -            title = _("Set Device Label")
       -            msg = _("Enter new label:")
       -            response = QInputDialog().getText(dialog, title, msg)
       -            if not response[1]:
       -                return
       -            get_client().change_label(str(response[0]))
       -            refresh()
       +            get_client().change_label(unicode(label_edit.text()))
       +            update()
        
                def toggle_passphrase():
                    title = _("Confirm Toggle Passphrase Protection")
       t@@ -216,38 +233,44 @@ def qt_plugin_class(base_plugin_class):
                            "unless your passphrase was or will be empty.\n\n"
                            "This is because addresses will no "
                            "longer correspond to those used by your %s.\n\n"
       -                    "If your passphrase is not or was not empty you will "
       -                    "need to create a new Electrum wallet with the install "
       -                    "wizard so that they match.\n\n"
       -                    "Are you sure you want to proceed?") % device
       +                    "You will need to create a new Electrum wallet "
       +                    "with the install wizard so that they match.\n\n"
       +                    "Are you sure you want to proceed?") % plugin.device
                    if not dialog.question(msg, title=title):
                        return
                    get_client().toggle_passphrase()
                    self.device_manager().close_wallet(wallet)  # Unpair
       -            refresh()
       -
       -        def set_pin():
       -            get_client().set_pin(remove=False)
       -            refresh()
       +            update()
       +
       +        def change_homescreen():
       +            dialog = QFileDialog(self, _("Choose Homescreen"))
       +            filename = dialog.getOpenFileName()
       +            if filename:
       +                im = Image.open(str(filename))
       +                if im.size != (hs_cols, hs_rows):
       +                    raise Exception('Image must be 64 x 128 pixels')
       +                im = im.convert('1')
       +                pix = im.load()
       +                img = ''
       +                for j in range(hs_rows):
       +                    for i in range(hs_cols):
       +                        img += '1' if pix[i, j] else '0'
       +                img = ''.join(chr(int(img[i:i + 8], 2))
       +                              for i in range(0, len(img), 8))
       +                get_client().change_homescreen(img)
       +
       +        def clear_homescreen():
       +            get_client().change_homescreen('\x00')
       +
       +        def set_pin(remove=False):
       +            get_client().set_pin(remove=remove)
       +            update()
        
                def clear_pin():
       -            title = _("Confirm Clear PIN")
       -            msg = _("WARNING: if your clear your PIN, anyone with physical "
       -                    "access to your %s device can spend your bitcoins.\n\n"
       -                    "Are you certain you want to remove your PIN?") % device
       -            if not dialog.question(msg, title=title):
       -                return
       -            get_client().set_pin(remove=True)
       -            refresh()
       +            set_pin(remove=True)
        
                def wipe_device():
                    # FIXME: cannot yet wipe a device that is only plugged in
       -            title = _("Confirm Device Wipe")
       -            msg = _("Are you sure you want to wipe the device?  "
       -                    "You should make sure you have a copy of your recovery "
       -                    "seed and that your wallet holds no bitcoins.")
       -            if not dialog.question(msg, title=title):
       -                return
                    if sum(wallet.get_balance()):
                        title = _("Confirm Device Wipe")
                        msg = _("Are you SURE you want to wipe the device?\n"
       t@@ -258,56 +281,107 @@ def qt_plugin_class(base_plugin_class):
                    # Note: we use PRESENT so that a user who has forgotten
                    # their PIN is not prevented from wiping their device
                    get_client(DeviceMgr.PRESENT).wipe_device()
       -            wallet.wiped()
                    self.device_manager().close_wallet(wallet)
       -            refresh()
       +            update()
        
                def slider_moved():
                    mins = timeout_slider.sliderPosition()
       -            timeout_label.setText(_("%2d minutes") % mins)
       +            timeout_minutes.setText(_("%2d minutes") % mins)
        
                wallet = window.wallet
                handler = wallet.handler
       -        device = self.device
       +        device = plugin.device
       +        dialog_vbox = QVBoxLayout(self)
        
       +        # Information tab
                info_tab = QWidget()
       -        tab_layout = QVBoxLayout(info_tab)
       -        info_layout = QGridLayout()
       -        noyes = [_("No"), _("Yes")]
       +        info_layout = QVBoxLayout(info_tab)
       +        info_glayout = QGridLayout()
       +        info_glayout.setColumnStretch(2, 1)
       +        device_label = QLabel()
       +        pin_set_label = QLabel()
       +        version_label = QLabel()
       +        device_id_label = QLabel()
                bl_hash_label = QLabel()
       +        bl_hash_label.setWordWrap(True)
                coins_label = QLabel()
                coins_label.setWordWrap(True)
       -        device_label = QLabel()
       -        passphrase_label = QLabel()
       -        initialized_label = QLabel()
       -        device_id_label = QLabel()
       -        version_label = QLabel()
       -        pin_label = QLabel()
                language_label = QLabel()
       -        rename_button = QPushButton(_("Rename"))
       -        rename_button.clicked.connect(rename)
       -        toggle_passphrase_button = QPushButton(_("Toggle"))
       -        toggle_passphrase_button.clicked.connect(toggle_passphrase)
       -        pin_button = QPushButton()
       -        pin_button.clicked.connect(set_pin)
       -        clear_pin_button = QPushButton(_("Clear"))
       -        clear_pin_button.clicked.connect(clear_pin)
       -
       -        add_rows_to_layout(info_layout, [
       -            (_("Device Label"), device_label, rename_button),
       -            (_("Has Passphrase"), passphrase_label, toggle_passphrase_button),
       -            (_("Has PIN"), pin_label, pin_button, clear_pin_button),
       -            (_("Initialized"), initialized_label),
       -            (_("Device ID"), device_id_label),
       -            (_("Bootloader Hash"), bl_hash_label),
       +        initialized_label = QLabel()
       +        rows = [
       +            (_("Device Label"), device_label),
       +            (_("PIN set"), pin_set_label),
                    (_("Firmware Version"), version_label),
       +            (_("Serial Number"), device_id_label),
       +            (_("Bootloader Hash"), bl_hash_label),
                    (_("Supported Coins"), coins_label),
                    (_("Language"), language_label),
       -        ])
       -
       -        timeout_layout = QHBoxLayout()
       -        timeout_label = QLabel()
       -        timeout_slider = QSlider(Qt.Horizontal)
       +            (_("Initialized"), initialized_label),
       +        ]
       +        for row_num, (label, widget) in enumerate(rows):
       +            info_glayout.addWidget(QLabel(label), row_num, 0)
       +            info_glayout.addWidget(widget, row_num, 1)
       +        info_layout.addLayout(info_glayout)
       +
       +        # Settings tab
       +        settings_tab = QWidget()
       +        settings_layout = QVBoxLayout(settings_tab)
       +        settings_glayout = QGridLayout()
       +        #settings_glayout.setColumnStretch(3, 1)
       +
       +        # Settings tab - Label
       +        label_msg = QLabel(_("Name this %s.  If you have mutiple devices "
       +                             "their labels help distinguish them.")
       +                           % plugin.device)
       +        label_msg.setWordWrap(True)
       +        label_label = QLabel(_("Device Label"))
       +        label_edit = QLineEdit()
       +        label_edit.setMinimumWidth(150)
       +        label_edit.setMaxLength(self.plugin.MAX_LABEL_LEN)
       +        label_apply = QPushButton(_("Apply"))
       +        label_apply.clicked.connect(rename)
       +        label_edit.textChanged.connect(set_label_enabled)
       +        settings_glayout.addWidget(label_label, 0, 0)
       +        settings_glayout.addWidget(label_edit, 0, 1, 1, 2)
       +        settings_glayout.addWidget(label_apply, 0, 3)
       +        settings_glayout.addWidget(label_msg, 1, 1, 1, -1)
       +
       +        # Settings tab - PIN
       +        pin_label = QLabel(_("PIN Protection"))
       +        pin_button = QPushButton()
       +        pin_button.clicked.connect(set_pin)
       +        settings_glayout.addWidget(pin_label, 2, 0)
       +        settings_glayout.addWidget(pin_button, 2, 1)
       +        pin_msg = QLabel(_("PIN protection is strongly recommended.  "
       +                           "A PIN is your only protection against someone "
       +                           "stealing your bitcoins if they obtain physical "
       +                           "access to your %s.") % plugin.device)
       +        pin_msg.setWordWrap(True)
       +        pin_msg.setStyleSheet("color: red")
       +        settings_glayout.addWidget(pin_msg, 3, 1, 1, -1)
       +        settings_layout.addLayout(settings_glayout)
       +
       +        # Settings tab - Homescreen
       +        homescreen_layout = QHBoxLayout()
       +        homescreen_label = QLabel(_("Homescreen"))
       +        homescreen_change_button = QPushButton(_("Change..."))
       +        homescreen_clear_button = QPushButton(_("Reset"))
       +        homescreen_change_button.clicked.connect(change_homescreen)
       +        homescreen_clear_button.clicked.connect(clear_homescreen)
       +        homescreen_msg = QLabel(_("You can set the homescreen on your device "
       +                                  "to personalize it.  You must choose a "
       +                                  "%d x %d monochrome black and white image.")
       +                                % (hs_rows, hs_cols))
       +        homescreen_msg.setWordWrap(True)
       +        settings_glayout.addWidget(homescreen_label, 4, 0)
       +        settings_glayout.addWidget(homescreen_change_button, 4, 1)
       +        settings_glayout.addWidget(homescreen_clear_button, 4, 2)
       +        settings_glayout.addWidget(homescreen_msg, 5, 1, 1, -1)
       +
       +        # Settings tab - Session Timeout
       +        timeout_label = QLabel(_("Session Timeout"))
       +        timeout_minutes = QLabel()
       +        timeout_slider = self.slider = QSlider(Qt.Horizontal)
                timeout_slider.setRange(1, 60)
                timeout_slider.setSingleStep(1)
                timeout_slider.setSliderPosition(wallet.session_timeout // 60)
       t@@ -315,37 +389,85 @@ def qt_plugin_class(base_plugin_class):
                timeout_slider.setTickPosition(QSlider.TicksBelow)
                timeout_slider.setTracking(True)
                timeout_slider.valueChanged.connect(slider_moved)
       -        timeout_layout.addWidget(QLabel(_("Session Timeout")))
       -        timeout_layout.addWidget(timeout_slider)
       -        timeout_layout.addWidget(timeout_label)
       -
       +        timeout_msg = QLabel(_("Clear the session after the specified period "
       +                               "of inactivity.  Once a session has timed out, "
       +                               "your PIN and passphrase (if enabled) must be "
       +                               "re-entered to use the device."))
       +        timeout_msg.setWordWrap(True)
       +        settings_glayout.addWidget(timeout_label, 6, 0)
       +        settings_glayout.addWidget(timeout_slider, 6, 1, 1, 3)
       +        settings_glayout.addWidget(timeout_minutes, 6, 4)
       +        settings_glayout.addWidget(timeout_msg, 7, 1, 1, -1)
       +
       +        # Advanced tab
                advanced_tab = QWidget()
       -        advanced_layout = QGridLayout(advanced_tab)
       +        advanced_layout = QVBoxLayout(advanced_tab)
       +        advanced_glayout = QGridLayout()
       +
       +        # Advanced tab - clear PIN
       +        clear_pin_button = QPushButton(_("Disable PIN"))
       +        clear_pin_button.clicked.connect(clear_pin)
       +        clear_pin_warning = QLabel(_("If you disable your PIN, anyone with "
       +                                     "physical access to your %s device can "
       +                                     "spend your bitcoins.") % plugin.device)
       +        clear_pin_warning.setWordWrap(True)
       +        clear_pin_warning.setStyleSheet("color: red")
       +        advanced_glayout.addWidget(clear_pin_button, 0, 2)
       +        advanced_glayout.addWidget(clear_pin_warning, 1, 0, 1, 5)
       +
       +        # Advanced tab - toggle passphrase protection
       +        passphrase_button = QPushButton()
       +        passphrase_button.clicked.connect(toggle_passphrase)
       +        passphrase_msg = QLabel(
       +            _("Passphrases allow you to access new wallets, each "
       +              "hidden behind a particular case-sensitive passphrase.  You "
       +              "need to create a separate Electrum wallet for each passphrase "
       +              "you use as they each generate different addresses.  Changing "
       +              "your passphrase does not lose other wallets, each is still "
       +              "accessible behind its own passphrase."))
       +        passphrase_msg.setWordWrap(True)
       +        passphrase_warning = QLabel(
       +            _("If you forget a passphrase you will be unable to access any "
       +              "bitcoins in the wallet behind it.  A passphrase is not a PIN. "
       +              "Only change this if you are sure you understand it."))
       +        passphrase_warning.setWordWrap(True)
       +        passphrase_warning.setStyleSheet("color: red")
       +        advanced_glayout.addWidget(passphrase_button, 3, 2)
       +        advanced_glayout.addWidget(passphrase_msg, 4, 0, 1, 5)
       +        advanced_glayout.addWidget(passphrase_warning, 5, 0, 1, 5)
       +
       +        # Advanced tab - wipe device
                wipe_device_button = QPushButton(_("Wipe Device"))
                wipe_device_button.clicked.connect(wipe_device)
       -        add_rows_to_layout(advanced_layout, [
       -            (wipe_device_button, ),
       -        ])
       -
       -        dialog = WindowModalDialog(window, _("%s Settings") % device)
       -        vbox = QVBoxLayout()
       -        tabs = QTabWidget()
       +        wipe_device_msg = QLabel(
       +            _("Wipe the device, removing all data from it.  The firmware "
       +              "is left unchanged."))
       +        wipe_device_msg.setWordWrap(True)
       +        wipe_device_warning = QLabel(
       +            _("Only wipe a device if you have the recovery seed written down "
       +              "and the device wallet(s) are empty, otherwise the bitcoins "
       +              "will be lost forever."))
       +        wipe_device_warning.setWordWrap(True)
       +        wipe_device_warning.setStyleSheet("color: red")
       +        advanced_glayout.addWidget(wipe_device_button, 6, 2)
       +        advanced_glayout.addWidget(wipe_device_msg, 7, 0, 1, 5)
       +        advanced_glayout.addWidget(wipe_device_warning, 8, 0, 1, 5)
       +        advanced_layout.addLayout(advanced_glayout)
       +        advanced_layout.addStretch(1)
       +
       +        tabs = QTabWidget(self)
                tabs.addTab(info_tab, _("Information"))
       +        tabs.addTab(settings_tab, _("Settings"))
                tabs.addTab(advanced_tab, _("Advanced"))
       -        vbox.addWidget(tabs)
       -        vbox.addStretch(1)
        
       -        # Show values
       +        # Update information and then connect change slots
       +        update()
                slider_moved()
       -        refresh()
        
       -        # QT on MacOSX is sensitive to layout ordering so these are last
       -        tab_layout.addLayout(info_layout)
       -        tab_layout.addLayout(timeout_layout)
       -        vbox.addLayout(Buttons(CloseButton(dialog)))
       -        dialog.setLayout(vbox)
       +        dialog_vbox.addWidget(tabs)
       +        dialog_vbox.addLayout(Buttons(CloseButton(self)))
        
       -        handler.exec_dialog(dialog)
       -        wallet.set_session_timeout(timeout_slider.sliderPosition() * 60)
       -
       -  return QtPlugin
       +    def closeEvent(self, event):
       +        seconds = self.slider.sliderPosition() * 60
       +        self.window.wallet.set_session_timeout(seconds)
       +        event.accept()