URI: 
       tImproved multi-device handling - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit f8ed7b058dae25a345160333da916b15a472cf5c
   DIR parent a0ef42d572377a4d90047058e5a7f8b3e100521e
  HTML Author: Neil Booth <kyuupichan@gmail.com>
       Date:   Sun, 24 Jan 2016 13:01:04 +0900
       
       Improved multi-device handling
       
       Ask user which device to use when there are many.  If there
       is only one skip the question.  We used to just pick the
       first one we found; user had no way to switch.
       
       We have to handle querying from the non-GUI thread.
       
       Diffstat:
         M gui/qt/main_window.py               |      10 ++++++++++
         M lib/plugins.py                      |      61 ++++++++++++++++++++++---------
         M plugins/trezor/plugin.py            |      38 ++++---------------------------
         M plugins/trezor/qt_generic.py        |      26 +++++++++++++++-----------
       
       4 files changed, 72 insertions(+), 63 deletions(-)
       ---
   DIR diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py
       t@@ -1337,6 +1337,16 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
                WaitingDialog(self, _('Broadcasting transaction...'),
                              broadcast_thread, broadcast_done, self.on_error)
        
       +    def query_choice(self, msg, choices):
       +        # Needed by QtHandler for hardware wallets
       +        dialog = WindowModalDialog(self.top_level_window())
       +        clayout = ChoicesLayout(msg, choices)
       +        vbox = QVBoxLayout(dialog)
       +        vbox.addLayout(clayout.layout())
       +        vbox.addLayout(Buttons(OkButton(dialog)))
       +        dialog.exec_()
       +        return clayout.selected_index()
       +
            def prepare_for_payment_request(self):
                self.tabs.setCurrentIndex(1)
                self.payto_e.is_pr = True
   DIR diff --git a/lib/plugins.py b/lib/plugins.py
       t@@ -228,6 +228,7 @@ class BasePlugin(PrintError):
                pass
        
        Device = namedtuple("Device", "path id_ product_key")
       +DeviceInfo = namedtuple("DeviceInfo", "device description initialized")
        
        class DeviceMgr(PrintError):
            '''Manages hardware clients.  A client communicates over a hardware
       t@@ -328,10 +329,6 @@ class DeviceMgr(PrintError):
            def paired_wallets(self):
                return list(self.wallets.keys())
        
       -    def unpaired_devices(self, handler):
       -        devices = self.scan_devices(handler)
       -        return [dev for dev in devices if not self.wallet_by_id(dev.id_)]
       -
            def client_lookup(self, id_):
                with self.lock:
                    for client, (path, client_id) in self.clients.items():
       t@@ -362,28 +359,56 @@ class DeviceMgr(PrintError):
        
                if force_pair:
                    first_address, derivation = wallet.first_address()
       -            # Wallets don't have a first address in the install wizard
       -            # until account creation
       -            if not first_address:
       -                self.print_error("no first address for ", wallet)
       -                return None
       -
       -            # The wallet has not been previously paired, so get the
       -            # first address of all unpaired clients and compare.
       -            for device in devices:
       -                # Skip already-paired devices
       -                if self.wallet_by_id(device.id_):
       -                    continue
       -                client = self.create_client(device, wallet.handler, plugin)
       +            assert first_address
       +
       +            # The wallet has not been previously paired, so let the user
       +            # choose an unpaired device and compare its first address.
       +            info = self.select_device(wallet, plugin, devices)
       +            if info:
       +                client = self.client_lookup(info.device.id_)
                        if client and not client.features.bootloader_mode:
                            # This will trigger a PIN/passphrase entry request
                            client_first_address = client.first_address(derivation)
                            if client_first_address == first_address:
       -                        self.pair_wallet(wallet, device.id_)
       +                        self.pair_wallet(wallet, info.device.id_)
                                return client
        
                return None
        
       +    def unpaired_device_infos(self, handler, plugin, devices=None):
       +        '''Returns a list of DeviceInfo objects: one for each connected,
       +        unpaired device accepted by the plugin.'''
       +        if devices is None:
       +            devices = self.scan_devices(handler)
       +        devices = [dev for dev in devices if not self.wallet_by_id(dev.id_)]
       +
       +        states = [_("wiped"), _("initialized")]
       +        infos = []
       +        for device in devices:
       +            if not device.product_key in plugin.DEVICE_IDS:
       +                continue
       +            client = self.create_client(device, handler, plugin)
       +            if not client:
       +                continue
       +            state = states[client.is_initialized()]
       +            label = client.label() or _("An unnamed %s") % plugin.device
       +            descr = "%s (%s)" % (label, state)
       +            infos.append(DeviceInfo(device, descr, client.is_initialized()))
       +
       +        return infos
       +
       +    def select_device(self, wallet, plugin, devices=None):
       +        '''Ask the user to select a device to use if there is more than one,
       +        and return the DeviceInfo for the device.'''
       +        infos = self.unpaired_device_infos(wallet.handler, plugin, devices)
       +        if not infos:
       +            return None
       +        if len(infos) == 1:
       +            return infos[0]
       +        msg = _("Please select which %s device to use:") % plugin.device
       +        descriptions = [info.description for info in infos]
       +        return infos[wallet.handler.query_choice(msg, descriptions)]
       +
            def scan_devices(self, handler):
                # All currently supported hardware libraries use hid, so we
                # assume it here.  This can be easily abstracted if necessary.
   DIR diff --git a/plugins/trezor/plugin.py b/plugins/trezor/plugin.py
       t@@ -25,9 +25,6 @@ TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY = range(0, 4)
        class DeviceDisconnectedError(Exception):
            pass
        
       -class OutdatedFirmwareError(Exception):
       -    pass
       -
        class TrezorCompatibleWallet(BIP44_Wallet):
            # Extend BIP44 Wallet as required by hardware implementation.
            # Derived classes must set:
       t@@ -332,42 +329,15 @@ class TrezorCompatiblePlugin(BasePlugin, ThreadJob):
                '''Called when creating a new wallet.  Select the device to use.  If
                the device is uninitialized, go through the intialization
                process.  Then create the wallet accounts.'''
       -        initialized = self.select_device(wallet)
       -        if initialized:
       +        devmgr = self.device_manager()
       +        device_info = devmgr.select_device(wallet, self)
       +        devmgr.pair_wallet(wallet, device_info.device.id_)
       +        if device_info.initialized:
                    task = partial(wallet.create_hd_account, None)
                else:
                    task = self.initialize_device(wallet)
                wallet.thread.add(task, on_done=on_done, on_error=on_error)
        
       -    def unpaired_devices(self, handler):
       -        '''Returns all connected, unpaired devices as a list of clients and a
       -        list of descriptions.'''
       -        devmgr = self.device_manager()
       -        devices = devmgr.unpaired_devices(handler)
       -
       -        states = [_("wiped"), _("initialized")]
       -        infos = []
       -        for device in devices:
       -            if not device.product_key in self.DEVICE_IDS:
       -                continue
       -            client = self.device_manager().create_client(device, handler, self)
       -            if not client:
       -                continue
       -            state = states[client.is_initialized()]
       -            label = client.label() or _("An unnamed %s") % self.device
       -            descr = "%s (%s)" % (label, state)
       -            infos.append((device, descr, client.is_initialized()))
       -
       -        return infos
       -
       -    def select_device(self, wallet):
       -        msg = _("Please select which %s device to use:") % self.device
       -        infos = self.unpaired_devices(wallet.handler)
       -        labels = [info[1] for info in infos]
       -        device, descr, init = infos[wallet.handler.query_choice(msg, labels)]
       -        self.device_manager().pair_wallet(wallet, device.id_)
       -        return init
       -
            def on_restore_wallet(self, wallet, wizard):
                assert isinstance(wallet, self.wallet_class)
        
   DIR diff --git a/plugins/trezor/qt_generic.py b/plugins/trezor/qt_generic.py
       t@@ -134,6 +134,7 @@ class QtHandler(QObject, PrintError):
            Trezor protocol; derived classes can customize it.'''
        
            charSig = pyqtSignal(object)
       +    qcSig = pyqtSignal(object, object)
        
            def __init__(self, win, pin_matrix_widget_class, device):
                super(QtHandler, self).__init__()
       t@@ -144,6 +145,7 @@ class QtHandler(QObject, PrintError):
                win.connect(win, SIGNAL('passphrase_dialog'), self.passphrase_dialog)
                win.connect(win, SIGNAL('word_dialog'), self.word_dialog)
                self.charSig.connect(self.update_character_dialog)
       +        self.qcSig.connect(self.win_query_choice)
                self.win = win
                self.pin_matrix_widget_class = pin_matrix_widget_class
                self.device = device
       t@@ -157,6 +159,12 @@ class QtHandler(QObject, PrintError):
            def watching_only_changed(self):
                self.win.emit(SIGNAL('watching_only_changed'))
        
       +    def query_choice(self, msg, labels):
       +        self.done.clear()
       +        self.qcSig.emit(msg, labels)
       +        self.done.wait()
       +        return self.choice
       +
            def show_message(self, msg, on_cancel=None):
                self.win.emit(SIGNAL('message_dialog'), msg, on_cancel)
        
       t@@ -256,8 +264,9 @@ class QtHandler(QObject, PrintError):
                    self.dialog.accept()
                    self.dialog = None
        
       -    def query_choice(self, msg, labels):
       -        return self.win.query_choice(msg, labels)
       +    def win_query_choice(self, msg, labels):
       +        self.choice = self.win.query_choice(msg, labels)
       +        self.done.set()
        
            def request_trezor_init_settings(self, method, device):
                wizard = self.win
       t@@ -399,18 +408,13 @@ def qt_plugin_class(base_plugin_class):
            def choose_device(self, window):
                '''This dialog box should be usable even if the user has
                forgotten their PIN or it is in bootloader mode.'''
       -        handler = window.wallet.handler
                device_id = self.device_manager().wallet_id(window.wallet)
                if not device_id:
       -            infos = self.unpaired_devices(handler)
       -            if infos:
       -                labels = [info[1] for info in infos]
       -                msg = _("Select a %s device:") % self.device
       -                choice = self.query_choice(window, msg, labels)
       -                if choice is not None:
       -                    device_id = infos[choice][0].id_
       +            info = self.device_manager().select_device(window.wallet, self)
       +            if info:
       +                device_id = info.device.id_
                    else:
       -                handler.show_error(_("No devices found"))
       +                window.wallet.handler.show_error(_("No devices found"))
                return device_id
        
            def query_choice(self, window, msg, choices):