Launch dialog to track status of update of specified plugin package.

    .. versionadded:: 0.19

    .. versionchanged:: 0.20
        Add support for updating multiple packages (update all **installed**
        plugins by default).

        Add :data:`update_args` and :data:`update_kwargs` arguments.

    .. versionchanged:: 0.20.1
        Disable "OK" button until update has completed.

        Add "Cancel" button.

    .. versionchanged:: 0.20.3
        Fix typo in handling of attempt to update plugin packages that are not
        installed.

    .. versionchanged:: 0.23
        Add :data:`ignore_not_installed` parameter.

    .. versionchanged:: 0.23.1
        Fix bug in status message markup syntax when no packages are unlinked
        **or** linked.

    Parameters
    ----------
    package_name : str or list, optional
        Conda MicroDrop plugin package name, e.g., `microdrop.mr-box-plugin`.

        Multiple plugin package names may be provided as a list.

        If no package name(s) specified, update **all installed** plugin
        packages.
    update_args : list or tuple, optional
        Extra arguments to pass to :func:`mpm.api.update` call.
    update_kwargs : dict, optional
        Extra keyword arguments to pass to :func:`mpm.api.update` call.
    ignore_not_installed : bool, optional
        If ``True`` (*default*), ignore plugin packages that are not installed
        as Conda packages.

        Otherwise, a :class:`conda_heplers.PackageNotFound` exception will be
        raised for plugins that are not installed as Conda packages.

    Returns
    -------
    dict or None
        Conda install log.

        If dialog is closed or cancelled, ``None`` is returned.

    Notes
    -----
    This function launches two threads; one to pulse the progress bar
    periodically, and one to run the actual update attempt.

def update_plugin_dialog(package_name=None, update_args=None,
                         update_kwargs=None, ignore_not_installed=True):
    '''
    Launch dialog to track status of update of specified plugin package.

    .. versionadded:: 0.19

    .. versionchanged:: 0.20
        Add support for updating multiple packages (update all **installed**
        plugins by default).

        Add :data:`update_args` and :data:`update_kwargs` arguments.

    .. versionchanged:: 0.20.1
        Disable "OK" button until update has completed.

        Add "Cancel" button.

    .. versionchanged:: 0.20.3
        Fix typo in handling of attempt to update plugin packages that are not
        installed.

    .. versionchanged:: 0.23
        Add :data:`ignore_not_installed` parameter.

    .. versionchanged:: 0.23.1
        Fix bug in status message markup syntax when no packages are unlinked
        **or** linked.

    Parameters
    ----------
    package_name : str or list, optional
        Conda MicroDrop plugin package name, e.g., `microdrop.mr-box-plugin`.

        Multiple plugin package names may be provided as a list.

        If no package name(s) specified, update **all installed** plugin
        packages.
    update_args : list or tuple, optional
        Extra arguments to pass to :func:`mpm.api.update` call.
    update_kwargs : dict, optional
        Extra keyword arguments to pass to :func:`mpm.api.update` call.
    ignore_not_installed : bool, optional
        If ``True`` (*default*), ignore plugin packages that are not installed
        as Conda packages.

        Otherwise, a :class:`conda_heplers.PackageNotFound` exception will be
        raised for plugins that are not installed as Conda packages.

    Returns
    -------
    dict or None
        Conda install log.

        If dialog is closed or cancelled, ``None`` is returned.

    Notes
    -----
    This function launches two threads; one to pulse the progress bar
    periodically, and one to run the actual update attempt.
    '''
    thread_context = {}

    if package_name is None:
        # No plugin package specified.  Update all plugins which are installed
        # as Conda packages.
        installed_plugins_ = installed_plugins(only_conda=True)
        installed_package_names = [plugin_i['package_name']
                                   for plugin_i in installed_plugins_]
        package_name = installed_package_names
        logger.info('Update all plugins installed as Conda packages.')
    else:
        # At least one plugin package name was explicitly specified.
        if isinstance(package_name, types.StringTypes):
            package_name = [package_name]

        # Only update plugins that are installed as Conda packages.
        try:
            conda_package_infos = ch.package_version(package_name, verbose=False)
        except ch.PackageNotFound, exception:
            # At least one specified plugin package name did not correspond to an
            # installed Conda package.
            if not ignore_not_installed:
                # Raise error indicating at least one plugin is not installed
                # as a Conda package.
                raise
            logger.warning(str(exception))
            conda_package_infos = exception.available
        # Extract name from each Conda plugin package.
        package_name = [package_i['name'] for package_i in conda_package_infos]
        logger.info('Update the following plugins: %s',
                    ', '.join('`{}`'.format(name_i)
                              for name_i in package_name))

    # Format string list of packages.
    package_name_list = ', '.join('`{}`'.format(name_i)
                                  for name_i in package_name)
    # Format multiple-lines string list of packages.
    package_name_lines = '\n'.join(' - {}'.format(name_i)
                                   for name_i in package_name)

    def _update(update_complete, package_name):
        '''
        Attempt to update plugin.  Display status message when complete.

        Parameters
        ----------
        update_complete : threading.Event
            Set when update operation has been completed.
        package_name : str, list, or None
            Conda MicroDrop plugin package name(s) (default to all installed
            plugins).
        '''
        try:
            with lh.logging_restore(clear_handlers=True):
                args = update_args or []
                kwargs = update_kwargs or {}
                kwargs['package_name'] = package_name
                # Pass extra args and kwargs to `.api.update` (if specified).
                update_response = update(*args, **kwargs)
                thread_context['update_response'] = update_response

            # Display prompt indicating update status.

            # Get list of unlinked and linked packages.
            install_info_ = ch.install_info(update_response)

            if any(install_info_):
                # At least one package was uninstalled or installed (or
                # "unlinked"/"linked" in Conda lingo).
                def _status():
                    updated_packages = []
                    for package_name_i in package_name:
                        if any(linked_i[0].startswith(package_name_i)
                               for linked_i in install_info_[1]):
                            # Plugin package was updated.
                            updated_packages.append(package_name_i)

                    def _version_lines(package_info_tuples):
                        return (['<tt>'] + list(' - {} (from {})'
                                                .format(name_i, channel_i)
                                                for name_i, channel_i in
                                                package_info_tuples) +
                                ['</tt>'])
                    detailed_message_lines = []
                    if install_info_[0]:
                        detailed_message_lines.append('<b>Uninstalled:</b>')
                        (detailed_message_lines
                         .extend(_version_lines(install_info_[0])))
                    if install_info_[1]:
                        detailed_message_lines.append('<b>Installed:</b>')
                        (detailed_message_lines
                         .extend(_version_lines(install_info_[1])))

                    dialog.props.secondary_text = \
                        '\n'.join(detailed_message_lines)
                    dialog.props.secondary_use_markup = True
                    dialog.props.use_markup = True

                    if updated_packages:
                        message = ('The following plugin(s) were updated '
                                   'successfully:\n<b><tt>{}</tt></b>'
                                   .format(package_name_lines))
                    else:
                        message = ('Plugin dependencies were updated '
                                   'successfully.')
                    dialog.props.text = message
            else:
                # No packages were unlinked **or** linked.
                def _status():
                    #  1. Success (with previous version and new version).
                    dialog.props.text = ('The latest version of the following '
                                         'plugin(s) are already installed: '
                                         '<tt><b>`{}`</b></tt>'
                                         .format(package_name_list))
                    dialog.props.use_markup = True
            gobject.idle_add(_status)
        except Exception, exception:
            # Failure updating plugin.
            def _error():
                dialog.props.text = ('Error updating plugin(s):\n<tt>{}</tt>'
                                     .format(package_name_lines))
                dialog.props.use_markup = True
                exception_markup = gobject.markup_escape_text(str(exception))
                exception_markup = exception_markup.replace(r'\n', '\n')

                content_area = dialog.get_content_area()
                label = gtk.Label()
                label.set_markup(exception_markup)
                error_scroll = gtk.ScrolledWindow()
                error_scroll.set_policy(gtk.POLICY_AUTOMATIC,
                                        gtk.POLICY_AUTOMATIC)
                error_scroll.add_with_viewport(label)
                error_scroll.show_all()

                content_area.pack_start(error_scroll, expand=True, fill=True)
            gobject.idle_add(_error)
            thread_context['update_response'] = None
        update_complete.set()

    def _pulse(update_complete, progress_bar):
        '''
        Show pulsing progress bar to indicate activity.
        '''
        while not update_complete.wait(1. / 16):
            gobject.idle_add(progress_bar.pulse)
        def _on_complete():
            progress_bar.set_fraction(1.)
            progress_bar.hide()
            # Enable "OK" button and focus it.
            dialog.action_area.get_children()[1].props.sensitive = True
            dialog.action_area.get_children()[1].grab_focus()
        gobject.idle_add(_on_complete)

    dialog = gtk.MessageDialog(buttons=gtk.BUTTONS_OK_CANCEL)
    dialog.set_position(gtk.WIN_POS_MOUSE)
    dialog.props.resizable = True
    progress_bar = gtk.ProgressBar()
    content_area = dialog.get_content_area()
    content_area.pack_start(progress_bar, True, True, 5)
    content_area.show_all()
    # Disable "OK" button until update has completed.
    dialog.action_area.get_children()[1].props.sensitive = False

    dialog.props.title = 'Update plugin'
    if len(package_name) > 1:
        # Multiple packages were specified.
        dialog.props.text = ('Searching for updates for:\n<tt>{}</tt>'
                             .format(package_name_lines))
    else:
        # A single package was specified.
        dialog.props.text = ('Searching for updates for <tt>{}</tt>...'
                             .format(package_name[0]))
    dialog.props.use_markup = True

    # Event to keep progress bar pulsing while waiting for update to
    # complete.
    update_complete = threading.Event()

    # Launch thread to periodically pulse progress bar.
    progress_thread = threading.Thread(target=_pulse, args=(update_complete,
                                                            progress_bar, ))
    progress_thread.daemon = True
    progress_thread.start()

    # Launch thread to attempt plugin update.
    update_thread = threading.Thread(target=_update, args=(update_complete,
                                                           package_name, ))
    update_thread.daemon = True
    update_thread.start()

    # Show dialog.
    dialog.run()
    dialog.destroy()

    # Return response from `conda_helpers.api.update` call.
    return thread_context.get('update_response')