Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Bower components Debian packages RPM packages NuGet packages

beebox / crossover   deb

Repository URL to install this package:

Version: 18.5.0-1 

/ opt / cxoffice / lib / python / cxinstallerui.py

# (c) Copyright 2009-2015. CodeWeavers, Inc.

# for localization
from cxutils import cxgettext as _

import os
import thread
import time

import gobject
gobject.threads_init()
import gtk

import cxproduct
import cxutils
import bottlequery
import cxlog
import iso8601
import pyop

import c4parser
import c4profiles
import c4profilesmanager
import installtask
import cxaiebase
import cxaiecore
import cxaiengine
import cxaiemedia
import cxfixes

import cxguitools
import bottlecollection
import bottlemanagement
import mountpoints
import cxobservable
import systeminfo

# These are the observable events
DIALOG_CLOSED = 'dialog_closed'
INSTALL_FINISHED = 'install_finished'

download_queue = pyop.PythonOperationQueue(maxThreads=2)



#
# Dialog instance
#

def string_start_match_points(search_string, candidate_string):
    if candidate_string.find(search_string) == 0:
        return 10
    return 0

def word_start_match_points(search_string, candidate_string):
    words = candidate_string.split()
    for word in words:
        if word.find(search_string) == 0:
            return 5

    return 0

class InstallWizardController(installtask.InstallTaskDelegate, cxobservable.Object):

    media_markup_format = '<b>%s</b>\n<small>%s</small>'
    registered_delegate = False

    # This is the list of observable events
    observable_events = frozenset((DIALOG_CLOSED, INSTALL_FINISHED))

    def __init__(self, **kwargs):
        cxobservable.Object.__init__(self)
        installtask.InstallTaskDelegate.__init__(self)
        self.options = kwargs
        self.auto_update = False

        # Import widget modules so gtk.Builder() finds them
        import infobar
        import cxhtmltextview # pylint: disable=W0612
        import placeholderentry # pylint: disable=W0612

        #  Setup the GUI
        self.xml = gtk.Builder()
        self.xml.set_translation_domain("crossover")
        self.xml.add_from_file(cxguitools.get_ui_path("cxinstaller"))
        self.xml.connect_signals(self)

        # Restore settings
        global_config = cxproduct.get_config()

        self.show_untested_apps = (global_config['OfficeSetup'].get('ShowUntestedApps', '0') != '0')

        self.installTask = installtask.InstallTask(self.show_untested_apps, delegate=self)
        self.installTask.profiles = c4profilesmanager.C4ProfilesSet()
        self.installEngine = None
        self.install_in_progress = False
        self.install_finished = False
        self.install_ui = False # True if we're doing something in an install that requires user interaction
        self.nonempty_install_notes = False
        self.failing_tasks = []
        self.profile_tree = {}

        self.autorun_file = None

        # Tweaks not allowed by gtk.Builder.
        install_notes = self.xml.get_object('InstallNotes')
        install_notes.set_size_request(1, -1)
        warning_image = install_notes.render_icon(gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_MENU)
        if warning_image:
            install_notes.images[u'warning'] = warning_image
            self.xml.get_object('BottleErrorText').images[u'warning'] = warning_image
            self.xml.get_object('InstallSummary').images[u'warning'] = warning_image
        error_image = install_notes.render_icon(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_MENU)
        if error_image:
            install_notes.images[u'error'] = error_image
            self.xml.get_object('BottleErrorText').images[u'error'] = error_image
            self.xml.get_object('InstallSummary').images[u'error'] = error_image
        check_image = install_notes.render_icon(gtk.STOCK_APPLY, gtk.ICON_SIZE_MENU)
        if check_image:
            self.xml.get_object('InstallSummary').images[u'check'] = check_image

        self.xml.get_object('PackagesNotes').set_size_request(1, -1)

        self.xml.get_object('PackageDescription').set_size_request(1, -1)

        self.xml.get_object('DownloadInstallationNotes').set_size_request(1, -1)

        self.xml.get_object('InstallResults').set_size_request(1, -1)

        self.xml.get_object('AddDependencyList').append_column(gtk.TreeViewColumn("Name", gtk.CellRendererText(), text=0))

        self.xml.get_object('AddDependencyList').get_selection().set_mode(gtk.SELECTION_MULTIPLE)

        self.xml.get_object('AddDependencyList').get_selection().connect('changed', self.on_AddDependencyList_selection_changed)


        # Add indents to debug channel menu items
        for item in self.xml.get_object("DebugChannelMenu").get_children()[3:]:
            item.set_label('  '+item.get_label())


        # name, appid
        self.removeDependenciesListModel = gtk.ListStore(str, object)

        self.xml.get_object('RemoveDependencyList').set_model(self.removeDependenciesListModel)
        self.xml.get_object('RemoveDependencyList').append_column(gtk.TreeViewColumn("Name", gtk.CellRendererText(), text=0))
        self.xml.get_object('RemoveDependencyList').get_selection().set_mode(gtk.SELECTION_MULTIPLE)
        self.xml.get_object('RemoveDependencyList').get_selection().connect('changed', self.on_RemoveDependencyList_selection_changed)

        # The fields are: label, icon, mountpoint, sort key, markup, temporary
        self.volumesListModel = gtk.ListStore(str, gtk.gdk.Pixbuf, str, str, str, bool)
        self.volumesListModel.set_sort_column_id(3, gtk.SORT_ASCENDING)

        volumesWidget = self.xml.get_object("MediaTreeView")
        renderer = gtk.CellRendererPixbuf()
        renderer.set_property('xpad', 6)
        renderer.set_property('ypad', 6)
        column = gtk.TreeViewColumn("Icon", renderer, pixbuf=1)
        volumesWidget.append_column(column)
        column = gtk.TreeViewColumn("Caption", gtk.CellRendererText(), markup=4)
        volumesWidget.append_column(column)
        volumesWidget.set_model(self.volumesListModel)

        # Add the special 'Browse...' options
        newrow = self.volumesListModel.append()
        self.volumesListModel.set_value(newrow, 0, self.chooseInstallerFileString)
        pixbuf = volumesWidget.render_icon(gtk.STOCK_EXECUTE, gtk.ICON_SIZE_DIALOG)
        self.volumesListModel.set_value(newrow, 1, pixbuf)
        self.volumesListModel.set_value(newrow, 3, '2')
        self.volumesListModel.set_value(newrow, 4, self.media_markup_format %
                                        (self.chooseInstallerFileString,
                                         _("The file you select will be launched to install the application")))

        newrow = self.volumesListModel.append()
        self.volumesListModel.set_value(newrow, 0, self.chooseInstallerFolderString)
        pixbuf = volumesWidget.render_icon(gtk.STOCK_DIRECTORY, gtk.ICON_SIZE_DIALOG)
        self.volumesListModel.set_value(newrow, 1, pixbuf)
        self.volumesListModel.set_value(newrow, 3, '3')
        self.volumesListModel.set_value(newrow, 4, self.media_markup_format %
                                        (self.chooseInstallerFolderString,
                                         _("The folder you select will be treated like an installer disc")))

        bottlesWidget = self.xml.get_object("BottleSelectionView")
        column = gtk.TreeViewColumn(_("Name"), gtk.CellRendererText(), markup=1)
        column.set_resizable(True)
        bottlesWidget.append_column(column)
        column = gtk.TreeViewColumn(_("Type"), gtk.CellRendererText(), text=2)
        column.set_resizable(False)
        bottlesWidget.append_column(column)
        self.bottleListModel = gtk.TreeStore(object, str, str, object) #name, display markup, template, sort key
        self.bottleListModel.set_sort_func(3, self._bottle_list_cmp)
        self.bottleListModel.set_sort_column_id(3, gtk.SORT_ASCENDING)
        bottlesWidget.set_model(self.bottleListModel)

        # locale name, locale id, sortkey
        self.languageTreeStore = gtk.ListStore(str, str, str)
        self.languageTreeStore.set_sort_column_id(2, gtk.SORT_ASCENDING)

        self.package_search_keys = {}

        # appid, sortkey, markup
        self.packageCompletionStore = gtk.ListStore(object, str, str)
        self.packageCompletionStore.set_sort_column_id(1, gtk.SORT_ASCENDING)
        self.packageCompletion = gtk.EntryCompletion()
        self.packageCompletion.set_model(self.packageCompletionStore)
        self.packageCompletion.set_match_func(self.package_completion_match, None)
        self.packageCompletion.connect('match_selected', self.package_completion_selected)

        renderer = gtk.CellRendererText()
        self.packageCompletion.pack_start(renderer)
        self.packageCompletion.add_attribute(renderer, 'markup', 2)

        self.xml.get_object("SimpleSearchBox").set_completion(self.packageCompletion)

        # The fields are: name, appid, sortkey
        self.packageTreeStore = gtk.TreeStore(str, object, str)

        packageTreeView = self.xml.get_object("PackageTreeView")
        packageTreeView.set_model(self.packageTreeStore)
        # The column header is not shown so there is no point localizing it
        column = gtk.TreeViewColumn("Installable Packages", gtk.CellRendererText(), text=0)
        column.set_resizable(False)
        packageTreeView.append_column(column)
        self.packageSearchString = ""

        # text, pulse, value, cxinstallerui.InstallTask, counter
        self.installingTaskStore = gtk.ListStore(str, gobject.TYPE_INT, gobject.TYPE_INT, object, long)

        self.installingTaskStore.set_sort_func(3, _install_task_sort)
        self.installingTaskStore.set_sort_column_id(3, gtk.SORT_ASCENDING)

        installingTaskView = self.xml.get_object("InstallerTasksView")
        installingTaskView.set_model(self.installingTaskStore)
        renderer = gtk.CellRendererProgress()
        renderer.set_property('xpad', 24)
        renderer.set_property('ypad', 24)
        column = gtk.TreeViewColumn("Task", renderer, text=0, pulse=1, value=2)
        column.set_resizable(False)
        installingTaskView.append_column(column)

        # set up tasks view to "stick" to the bottom
        self.tasks_scroll_at_bottom = True
        tasksScroll = self.xml.get_object("InstallerTasksScroll")
        vadjustment = tasksScroll.get_vadjustment()
        vadjustment.connect('changed', self.tasks_scroll_changed)
        vadjustment.connect('value-changed', self.tasks_scroll_moved)

        update = global_config['OfficeSetup'].get('AutoUpdate')

        c4p_url = global_config['OfficeSetup'].get('TieURL', '')
        self.updateOp = UpdateProfilesOperation(c4p_url, self)
        pyop.sharedOperationQueue.enqueue(self.updateOp)

        self.package_update_bar = gtk.ProgressBar()
        self.package_update_bar.set_text(_("Updating"))
        self.package_update_bar.set_pulse_step(0.1)
        self.package_update_bar.show()

        if update == '1':
            self.auto_update_response(None, 3)
        elif update != '0':
            infobar = self.xml.get_object('PackageInfoBar')
            infobar_label = gtk.Label(_("For best results, please allow CrossOver to update its installation recipes:"))
            infobar_label.set_alignment(0.0, 0.5)
            infobar_label.show()
            infobar.get_content_area().add(infobar_label)
            infobar.add_button(_("Update Now"), 3)
            infobar.add_button(_("Never Update"), 1)
            infobar.add_button(_("Always Update"), 0)
            close_image = gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_BUTTON)
            close_image.show()
            close_button = gtk.Button()
            close_button.add(close_image)
            close_button.set_relief(gtk.RELIEF_NONE)
            close_button.show()
            infobar.add_action_widget(close_button, 2)
            infobar.response_callback = self.auto_update_response
            infobar.show()

        # Start parsing package information now
        if self.options.get('c4pfile'):
            self.parse_c4pfile(self.options['c4pfile'])

        collection = bottlecollection.sharedCollection(('basic', 'applications'))
        collection.addChangeDelegate(self)
        collection.addBottleChangeDelegate(self)
        self.registered_delegate = True
        self.bottleCollectionChanged()

        self.profile_id_to_set = self.options.get('profileid')

        self.add_menu_open = False

        if global_config['OfficeSetup'].get('ApplyCxfixes', '1') != '1':
            self.xml.get_object('UnixPackagesInstallCB').set_active(False)

        self.show_dialog()

        self.mountpoints = mountpoints.MountPointsNotifier(self.volumeAdded, self.volumeDeleted)

    def present(self):
        self.xml.get_object("InstallWizard").present()

    def auto_update_response(self, _widget, response):
        if response in (0, 3):
            if response == 0:
                cxproduct.save_setting('OfficeSetup', 'AutoUpdate', '1')
            self.auto_update = True
            if not self.updateOp.is_queued:
                self.updateOp.network = True
                pyop.sharedOperationQueue.enqueue(self.updateOp)
            infobar = self.xml.get_object('PackageInfoBar')
            infobar.clear()
            infobar.get_content_area().pack_start(self.package_update_bar, expand=True, fill=True)
            gobject.timeout_add(300, self.package_update_pulse)
            close_image = gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_BUTTON)
            close_image.show()
            close_button = gtk.Button()
            close_button.add(close_image)
            close_button.set_relief(gtk.RELIEF_NONE)
            close_button.show()
            infobar.add_action_widget(close_button, 2)
            infobar.response_callback = self.auto_update_response
            infobar.show()
        else:
            if response == 1:
                cxproduct.save_setting('OfficeSetup', 'AutoUpdate', '0')
            self.xml.get_object('PackageInfoBar').hide()

    def package_update_pulse(self):
        if self.updateOp.is_queued:
            self.package_update_bar.pulse()
            return True
        return False

    def dropin_c4p(self, c4pfile):
        self.autorun_file = c4profilesmanager.C4ProfilesSet.add_c4p(c4pfile)
        if not self.autorun_file.useful_for_current_product:
            title = _("Invalid file")
            failstring = _("The file '%s' does not contain installation profiles for anything that can be installed with this product.") % c4pfile.filename
            cxguitools.CXMessageDlg(primary=title, secondary=failstring,
                                    message_type=gtk.MESSAGE_WARNING,
                                    parent=self.xml.get_object('InstallWizard'))

    def parseResponse(self, response, userdata):
        if response == 0:
            self.dropin_c4p(userdata)

            pyop.sharedOperationQueue.enqueue(UpdateProfilesOperation(None, self))

    def parse_c4pfile(self, filename):
        # Check if this CrossTie has a valid signature
        (trusted, tmptie, tmpsig) = c4profilesmanager.C4ProfilesSet.taste_new_c4p_file(filename)
        if trusted:
            os.remove(tmptie)
            os.remove(tmpsig)

        # Parse and load in into memory then reject it if it contains malware
        # Note that we load it from the original filename so future error
        # messages are understandable.
        c4pfile = c4parser.C4PFile(filename, trusted, c4profiles.dropin)
        if c4pfile.malware_appid:
            cxguitools.show_malware_dialog(c4pfile, parent=self.xml.get_object('InstallWizard'))
            return

        # Warn about untrusted files
        if not trusted:
            primary = _("'%(filename)s' is not from a trusted source!") % {'filename': cxutils.html_escape(os.path.basename(filename))}
            secondary = _("CodeWeavers has no knowledge or control over what actions this CrossTie file may attempt. <b>Be VERY cautious</b> loading untrusted CrossTies as they can cause great damage to your computer. CodeWeavers will not provide technical support for issues related to the use of untrusted CrossTie files.")
            markup = "<span weight='bold' size='larger'>%s</span>\n\n%s" % (primary, secondary)
            cxguitools.CXMessageDlg(markup=markup,
                                    button_array=[[_("Don't Load"), 1],
                                                  [_("Load"), 0]],
                                    user_data=c4pfile,
                                    response_function=self.parseResponse,
                                    message_type=gtk.MESSAGE_WARNING)
        else:
            self.dropin_c4p(c4pfile)

    def cancel_clicked(self, _caller):
        if self.install_in_progress:
            self.cancel_install()
        else:
            self.destroy()

    def destroy(self):
        if self.registered_delegate:
            collection = bottlecollection.sharedCollection()
            collection.removeChangeDelegate(self)
            collection.removeBottleChangeDelegate(self)
            self.registered_delegate = False
        self.xml.get_object("InstallWizard").destroy()
        self.quit_requested(None)

        self.emit_event(DIALOG_CLOSED)

    def quit_requested(self, _caller):
        # pylint: disable=R0201
        cxguitools.toplevel_quit()

    def close_clicked(self, _arg1, _arg2):
        if not self.install_in_progress:
            self.destroy()
            return False
        primary = _("Abort the installation?")
        secondary = _("Closing this window will interrupt the installation and leave your software partially installed. Do you really want to do this?")
        cxguitools.CXMessageDlg(secondary=secondary, primary=primary,
                                button_array=[[_("Continue Installation"), 0],
                                                  [_("Close"), 1]],
                                response_function=self.close_dlg_response,
                                message_type=gtk.MESSAGE_WARNING)
        return True

    def close_dlg_response(self, response):
        if response == 1:
            self.cancel_install()
            self.destroy()

    def show_dialog(self):
        dialog = self.xml.get_object("InstallWizard")
        self.updatePackageListing()
        self.profileChanged()
        self.updateBottleListing()
        self.sourceChanged()
        self.bottleCreateChanged()
        self.bottleNewnameChanged()
        self.bottleTemplateChanged()

        if self.options.get('bottle'):
            bottlename = cxutils.string_to_unicode(self.options['bottle'])
            try:
                bottle = bottlecollection.sharedCollection().bottleObject(bottlename)
            except KeyError:
                self.installTask.SetNewBottleName(bottlename)
            else:
                if bottle.canInstall():
                    self.installTask.bottlename = bottlename
                else:
                    cxlog.err("The bottle '%s' cannot be used as an install target." % bottlename)

        if self.options.get('installersource'):
            self.set_custom_installer_source(self.options['installersource'])

        self.set_expanded_section('PackageSelect')
        dialog.show()

    def set_expanded_section(self, name):
        for section_name in ['PackageSelect', 'MediaSelect', 'BottleSelect', 'Installer']:
            if section_name == name:
                self.xml.get_object('%sContent' % section_name).show()
                self.xml.get_object('%sToggle' % section_name).set_active(True)
            else:
                self.xml.get_object('%sContent' % section_name).hide()
                self.xml.get_object('%sToggle' % section_name).set_active(False)
        if name == 'PackageSelect':
            self.xml.get_object('SimpleSearchBox').grab_focus()
            self.xml.get_object('BrowseApplicationsButton').show()
            self.xml.get_object('ManageApplicationsButton').show()
        if name != 'PackageSelect':
            self.xml.get_object('BrowseApplicationsButton').hide()
            self.xml.get_object('ManageApplicationsButton').hide()
        if name == 'Installer' and not self.install_in_progress and not self.install_finished:
            self.xml.get_object('AdvancedOptionsButton').show()
        else:
            self.xml.get_object('AdvancedOptionsButton').hide()

        self.on_section_changed[name](self)

    def advance_section(self, can_install=False):
        # Expand the first section that needs work.
        # If can_install is True and no sections need work, start the install.
        if self.installEngine:
            # Install has started already.
            self.destroy()
        elif self.wantPackageSelection():
            self.set_expanded_section('PackageSelect')
            if self.installTask.GetInstallerSource():
                self.xml.get_object('BrowseApplicationsButton').set_active(True)
        elif self.wantMediaSelection():
            self.set_expanded_section('MediaSelect')
        elif self.wantBottleSelection():
            self.set_expanded_section('BottleSelect')
        elif not self.xml.get_object('InstallerToggle').get_active() or \
             self.xml.get_object('AdvancedOptionsButton').get_active():
            self.set_expanded_section('Installer')
            self.xml.get_object('AdvancedOptionsButton').set_active(False)
        elif can_install and self.installTask.can_install():
            self.displayInstallationProgressPanel()
            self.doInstall()
        else:
            self.set_expanded_section(None)

    def next_clicked(self, _widget):
        self.advance_section(True)

    def update_common_widgets(self):
        # update next button
        nextButton = self.xml.get_object("InstallerNext")
        if (self.xml.get_object('PackageSelectContent').get_property('visible') and self.wantPackageSelection()) or \
                (self.xml.get_object('MediaSelectContent').get_property('visible') and self.wantMediaSelection()) or \
                (self.xml.get_object('BottleSelectContent').get_property('visible') and self.wantBottleSelection()):
            nextButton.set_label(_("Continue"))
            nextButton.set_sensitive(False)
        elif self.wantPackageSelection() or self.wantMediaSelection() or self.wantBottleSelection() or \
             not self.xml.get_object('InstallerToggle').get_active() or \
             self.xml.get_object('AdvancedOptionsButton').get_active():
            nextButton.set_label(_("Continue"))
            nextButton.set_sensitive(True)
        elif not self.installTask.can_install():
            # Can't install but no sections need work. Install notes should say why.
            nextButton.set_label(_("Continue"))
            nextButton.set_sensitive(False)
        else:
            nextButton.set_label(_("Install"))
            nextButton.set_sensitive(True)

        self.update_install_notes(True)

        if self.wantPackageSelection() or self.wantMediaSelection() or self.wantBottleSelection():
            if self.xml.get_object('InstallerToggle').get_active():
                self.advance_section(True)
            self.xml.get_object('InstallerToggle').set_sensitive(False)
        else:
            self.xml.get_object('InstallerToggle').set_sensitive(True)

    def get_cxdiag_install_notes(self):
        # Collect the lists of issues cxfixes can take care of, and the
        # remaining cxdiag errors and warnings
        pkg_errids = {}
        warnings = []
        errors = []
        for errid, (level, title, description) in self.installTask.get_cxdiag_messages().iteritems():
            packages = cxfixes.get_packages(errid)
            if packages:
                for pkg in packages:
                    if pkg in pkg_errids:
                        pkg_errids[pkg].append(errid)
                    else:
                        pkg_errids[pkg] = [errid]

            if not packages or not self.installTask.apply_cxfixes:
                if level == 'Require':
                    errors.append('<a style="color:red;" href="%(url)s">%(title)s</a>: %(description)s' % {
                        'url': 'http://www.codeweavers.com/support/wiki/Diag/' + errid,
                        'title': cxutils.html_escape(title),
                        'description': cxutils.html_escape(description)})

                elif level == 'Recommend':
                    warnings.append('<a href="%(url)s">%(title)s</a>: %(description)s' % {
                        'url': 'http://www.codeweavers.com/support/wiki/Diag/' + errid,
                        'title': cxutils.html_escape(title),
                        'description': cxutils.html_escape(description)})

        # Build the package list
        pkg_notes = []
        for pkg in sorted(pkg_errids):
            # pylint: disable=W0631
            if len(pkg_errids[pkg]) == 1:
                pkg_notes.append('<a href="%(url)s">%(pkg)s</a>' % {
                    'url': 'http://www.codeweavers.com/support/wiki/Diag/' + pkg_errids[pkg][0],
                    'pkg': cxutils.html_escape(pkg)})
            else:
                refs = []
                for i in xrange(len(pkg_errids[pkg])):
                    refs.append('<a href="%(url)s">%(index)s</a>' % {
                        'url': 'http://www.codeweavers.com/support/wiki/Diag/' + pkg_errids[pkg][i],
                        'index': i})
                pkg_notes.append('%s <small>[%s]</small>' % (pkg, ' '.join(refs)))

        return (errors, warnings, pkg_notes)

    def get_install_notes(self):
        if self.installTask.profile is None:
            return ""

        notes = self.installTask.installationNotes
        if notes is None:
            notes = ""

        return notes

    def get_install_issues(self):
        errors = []
        warnings = []

        # Update bottle warning text
        if self.installTask.GetCreateNewBottle() and self.installTask.bottlename and \
           self.installTask.bottlename in bottlecollection.sharedCollection().bottleList():
            self.xml.get_object("BottleErrorText").get_buffer().set_text('')
            msg = _("There is already a bottle named '%s'. Select the existing bottle from the list or choose a different bottle name.") % self.installTask.bottlename
            self.xml.get_object("BottleErrorText").display_html('<body><p><img src="error" /> %s</p></body>' % cxutils.html_escape(msg))
            self.xml.get_object("BottleErrorText").show()
            errors.append(msg)
            self.xml.get_object("BottleSelectCheckmark").set_from_stock(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_MENU)
            self.xml.get_object("BottleSelectCheckmark").show()
        elif self.installTask.target_bottle and self.installTask.target_bottle.analyzed() and \
           not self.installTask.target_bottle.compatible:
            self.xml.get_object("BottleErrorText").get_buffer().set_text('')
            if self.installTask.target_bottle.bottlename:
                msg = _("You have selected a bottle which is incompatible with this application. It is strongly recommended to install in a new or compatible bottle.")
            else:
                msg = _("You have chosen to create a new bottle of a type which is incompatible with this application. It is strongly recommended to install in a compatible bottle.")
            self.xml.get_object("BottleErrorText").display_html('<body><p><img src="warning" /> %s</p></body>' % cxutils.html_escape(msg))
            self.xml.get_object("BottleErrorText").show()
            self.xml.get_object("BottleSelectCheckmark").set_from_stock(gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_MENU)
            self.xml.get_object("BottleSelectCheckmark").show()
        else:
            self.xml.get_object("BottleErrorText").hide()
            if self.installTask.bottlename:
                self.xml.get_object("BottleSelectCheckmark").set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_MENU)
                self.xml.get_object("BottleSelectCheckmark").show()
            else:
                self.xml.get_object("BottleSelectCheckmark").hide()

        self.xml.get_object("BottleSideWidgets").set_property('visible', self.xml.get_object("NewBottleWidgets").get_property('visible') or
                                                              self.xml.get_object("BottleErrorText").get_property('visible'))

        if self.installTask.target_bottle and self.installTask.target_bottle.analyzed():
            if self.installTask.target_bottle.missing_profiles:
                missing = ", ".join(self.installTask.target_bottle.missing_profiles)
                errors.append(_("The install cannot continue because one or more profiles it depends on could not be found: %s") % cxutils.html_escape(missing))
            elif self.installTask.target_bottle.missing_media:
                missing = ", ".join(self.installTask.get_profile_names(self.installTask.target_bottle.missing_media))
                errors.append(_("The install cannot continue because no installer could be found for the following dependencies: %s") % cxutils.html_escape(missing))

        if self.installTask.installerSource:
            src_filename = self.installTask.installerSource
            if os.path.isdir(src_filename):
                src_filename = None
                if self.installTask.profile:
                    installer_profile = self.installTask.get_installer_profile()
                    src_filename = cxaiemedia.locate_installer(installer_profile, self.installTask.installerSource)
            if src_filename:
                permissions = cxutils.check_mmap_permissions(src_filename)
                if permissions == cxutils.PERMISSIONS_NOREAD:
                    errors.append(cxutils.html_escape(_("The installer file '%s' cannot be read.") % src_filename))
                elif permissions == cxutils.PERMISSIONS_NOEXEC:
                    warnings.append(cxutils.html_escape(_("The installer file '%s' is on a noexec filesystem. This may cause the install to fail.") % src_filename))

        return (errors, warnings)

    def get_combined_notes(self, installNotes, installErrors, installWarnings, cxdiagErrors, cxdiagWarnings):
        # pylint: disable=R0201
        chunks = ['<body>']
        if installNotes:
            chunks.append(installNotes)
        if installWarnings or installErrors or cxdiagWarnings or cxdiagErrors:
            chunks.append(_('<p><b>The following issues may prevent this application from working.</b> Please follow the links to see how to resolve them:</p>'))

        error_icon_html = '<img src="error" alt="ERROR" />'
        for chunk in installErrors:
            chunks.extend(('<p>', error_icon_html, chunk, '</p>'))
        for chunk in cxdiagErrors:
            chunks.extend(('<p>', error_icon_html, chunk, '</p>'))

        warning_icon_html = '<img src="warning" alt="WARNING" />'
        for chunk in installWarnings:
            chunks.extend(('<p>', warning_icon_html, chunk, '</p>'))
        for chunk in cxdiagWarnings:
            chunks.extend(('<p>', warning_icon_html, chunk, '</p>'))

        chunks.append('</body>')
        return ''.join(chunks)

    def _list_dependencies(self, appid, reasons_text, reasons_rev):
        if appid not in self.installTask.profiles:
            return
        other_app_name = cxutils.html_escape(self.installTask.profiles[appid].name)
        for dep_appid in reasons_rev.get((installtask.REASON_DEPENDENCY, appid), ()):
            if dep_appid not in self.installTask.profiles:
                continue
            name = self.installTask.profiles[dep_appid].name
            if dep_appid not in self.installTask.target_bottle.installers:
                if self.installTask.dependency_overrides.get(dep_appid) == installtask.OVERRIDE_EXCLUDE:
                    format_str = cxutils.html_escape(_("%(app_name)s (Required by %(other_app_name)s, Manually Removed) %(undo_link_start)sUndo%(undo_link_end)s"))
                    reasons_text.append(format_str % {'app_name': '<span style="text-decoration:line-through"><b>'+cxutils.html_escape(name)+'</b></span>',
                                                      'other_app_name': other_app_name,
                                                      'undo_link_start': u'<small><a href="app:%s">' % dep_appid,
                                                      'undo_link_end': u'</a></small>'})
                continue
            format_str = cxutils.html_escape(_("%(app_name)s (Required by %(other_app_name)s)"))
            self.removeDependenciesListModel.append(row=(name, dep_appid))
            reasons_text.append(format_str % {'app_name': '<b>'+cxutils.html_escape(name)+'</b>',
                                              'other_app_name': other_app_name})
        for dep_appid in reasons_rev.get((installtask.REASON_DEPENDENCY, appid), ()):
            if dep_appid not in self.installTask.target_bottle.installers:
                continue
            self._list_dependencies(dep_appid, reasons_text, reasons_rev)

    def on_AddDependencySearch_real_text_changed(self, _widget, _text):
        self.xml.get_object('AddDependencyList').get_model().refilter()

    def _add_deps_filter(self, model, iterator):
        search_text = self.xml.get_object('AddDependencySearch').get_real_text()
        if search_text:
            text = model.get_value(iterator, 0)
            return search_text.lower() in text.lower()
        return True

    def on_AddDependenciesButton_clicked(self, _widget):
        if self.xml.get_object('AddDependencyDialog').get_property('visible'):
            self.xml.get_object('AddDependencyDialog').present()
        else:
            # name, sort key, appid
            add_dependencies_store = gtk.ListStore(str, str, object)

            main_appid = self.installTask.profile.appid

            for appid in self.installTask.SuggestedDependencyOverrides():
                profile = self.installTask.profiles[appid]

                name = profile.name

                if main_appid in profile.app_profile.extra_fors:
                    sortkey = 'A' + name
                else:
                    sortkey = 'B' + name

                add_dependencies_store.append((name, sortkey, appid))

            add_dependencies_store.set_sort_column_id(1, gtk.SORT_ASCENDING)

            add_dependencies_store = add_dependencies_store.filter_new()

            add_dependencies_store.set_visible_func(self._add_deps_filter)

            self.xml.get_object('AddDependencyList').set_model(add_dependencies_store)

            self.xml.get_object('AddDependencyDialog').show()

    def on_AddDependencyDialog_delete_event(self, widget, _event):
        widget.hide()
        return True

    def on_AddDependencyDialog_response(self, widget, response_id):
        if response_id == 1:
            model, rows = self.xml.get_object('AddDependencyList').get_selection().get_selected_rows()
            if not rows:
                return
            appids = []
            for row in rows:
                appids.append(model.get_value(model.get_iter(row), 2))
            self.installTask.AddDependencyOverrides(appids, installtask.OVERRIDE_INCLUDE)
        widget.hide()

    def on_AddDependencyList_button_press_event(self, _widget, event):
        if event.button == 1 and event.type == gtk.gdk._2BUTTON_PRESS: #pylint: disable=W0212
            self.xml.get_object('AddDependencyDialog').response(1)

    def on_AddDependencyList_selection_changed(self, selection):
        self.xml.get_object('AddDependencyDialog').set_response_sensitive(1, selection.count_selected_rows() != 0)

    def on_DependenciesList_url_clicked(self, widget, url, _type):
        if url.startswith(u'app:'):
            self.installTask.RemoveDependencyOverrides((url[4:],))
            widget.emit_stop_by_name('url-clicked')

    def on_RevertDependenciesButton_clicked(self, _widget):
        self.installTask.ClearDependencyOverrides()

    def on_RemoveDependenciesButton_clicked(self, _widget):
        if self.xml.get_object('RemoveDependencyDialog').get_property('visible'):
            self.xml.get_object('RemoveDependencyDialog').present()
        else:
            self.xml.get_object('RemoveDependencyDialog').show()

    def on_RemoveDependencyDialog_response(self, widget, response_id):
        if response_id == 1:
            model, rows = self.xml.get_object('RemoveDependencyList').get_selection().get_selected_rows()
            if not rows:
                return
            appids = []
            for row in rows:
                appids.append(model.get_value(model.get_iter(row), 1))
            self.installTask.AddDependencyOverrides(appids, installtask.OVERRIDE_EXCLUDE)
        widget.hide()

    def on_RemoveDependencyList_button_press_event(self, _widget, event):
        if event.button == 1 and event.type == gtk.gdk._2BUTTON_PRESS: #pylint: disable=W0212
            self.xml.get_object('RemoveDependencyDialog').response(1)

    def on_RemoveDependencyList_selection_changed(self, selection):
        self.xml.get_object('RemoveDependencyDialog').set_response_sensitive(1, selection.count_selected_rows() != 0)

    def update_install_notes(self, show_warnings):
        installNotesHtmlView = self.xml.get_object("InstallNotes")
        unixPackagesInstallCB = self.xml.get_object("UnixPackagesInstallCB")
        packagesNotesHtmlView = self.xml.get_object("PackagesNotes")

        self.removeDependenciesListModel.clear()
        self.xml.get_object("DependenciesList").get_buffer().set_text("")
        reasons_rev = {}
        if self.installTask.target_bottle and self.installTask.target_bottle.analyzed():
            for appid, reason in self.installTask.target_bottle.reasons.iteritems():
                if reason not in reasons_rev:
                    reasons_rev[reason] = []
                reasons_rev[reason].append(appid)
            reasons_text = []
            for appid in reasons_rev.get((installtask.REASON_MAIN, None), ()):
                if appid not in self.installTask.target_bottle.installers:
                    continue
                format_str = cxutils.html_escape(_("%(app_name)s (Selected for install)"))
                reasons_text.append(format_str % {'app_name': '<b>'+cxutils.html_escape(self.installTask.profiles[appid].name)+'</b>'})
                self._list_dependencies(appid, reasons_text, reasons_rev)
            for appid in reasons_rev.get((installtask.REASON_STEAM, None), ()):
                name = self.installTask.profiles[appid].name
                if appid not in self.installTask.target_bottle.installers:
                    if self.installTask.dependency_overrides.get(appid) == installtask.OVERRIDE_EXCLUDE:
                        format_str = cxutils.html_escape(_("%(app_name)s (Installing via Steam, Manually Removed) %(undo_link_start)sUndo%(undo_link_end)s"))
                        reasons_text.append(format_str % {'app_name': '<span style="text-decoration:line-through"><b>'+cxutils.html_escape(name)+'</b></span>',
                                                          'undo_link_start': u'<small><a href="app:%s">' % appid,
                                                          'undo_link_end': u'</a></small>'})
                    continue
                self.removeDependenciesListModel.append(row=(name, appid))
                format_str = cxutils.html_escape(_("%(app_name)s (Installing via Steam)"))
                reasons_text.append(format_str % {'app_name': '<b>'+cxutils.html_escape(name)+'</b>'})
                self._list_dependencies(appid, reasons_text, reasons_rev)
            for appid in reasons_rev.get((installtask.REASON_OVERRIDE, None), ()):
                if appid not in self.installTask.target_bottle.installers:
                    continue
                format_str = cxutils.html_escape(_("%(app_name)s (Manually added) %(undo_link_start)sUndo%(undo_link_end)s"))
                reasons_text.append(format_str % {'app_name': u'<b>'+cxutils.html_escape(self.installTask.profiles[appid].name)+u'</b>',
                                                  'undo_link_start': u'<small><a href="app:%s">' % appid,
                                                  'undo_link_end': u'</a></small>'})
                self._list_dependencies(appid, reasons_text, reasons_rev)
            self.xml.get_object("DependenciesList").display_html_safe('<body>%s</body>' % ''.join('<p>%s</p>' % line for line in reasons_text))
            self.xml.get_object("AddDependenciesButton").set_sensitive(True)
        else:
            self.xml.get_object("AddDependenciesButton").set_sensitive(False)

        self.xml.get_object("RemoveDependenciesButton").set_sensitive(self.removeDependenciesListModel.get_iter_first() is not None)

        if 'CX_LOG' in os.environ:
            self.xml.get_object('LogFileEntry').set_property('placeholder-string', os.environ['CX_LOG'])
        else:
            parts = ['install']
            if self.installTask.bottlename:
                parts.append(self.installTask.bottlename)
            if self.installTask.profile and (not self.installTask.createNewBottle or self.installTask.newBottleNameRequested):
                parts.append(cxutils.sanitize_bottlename(self.installTask.profile.name))
            logpath = os.path.join(cxproduct.get_user_dir(), 'logs')
            logfile = os.path.join(logpath, '%s.cxlog' % '_'.join(str(x) for x in parts))
            if os.path.exists(logfile):
                parts.append(2)
                logfile = os.path.join(logpath, '%s.cxlog' % '_'.join(str(x) for x in parts))
                while os.path.exists(logfile):
                    parts[-1] += 1
                    logfile = os.path.join(logpath, '%s.cxlog' % '_'.join(str(x) for x in parts))
            self.xml.get_object('LogFileEntry').set_property('placeholder-string', logfile)

        installNotes = self.get_install_notes()

        if installNotes:
            html = '<body>%s</body>' % installNotes
            installNotesHtmlView.get_buffer().set_text("")
            try:
                installNotesHtmlView.display_html(html.encode("utf8"))
            except Exception, exception: # pylint: disable=W0703
                cxlog.warn("unable to display the HTML snippet below: %s\n\n%s" % (exception, cxlog.debug_str(html)))
                # Assume the CrossTie's HTML markup is broken and sanitize it
                installNotes = cxutils.html_to_text(installNotes)
                installNotes = cxutils.html_escape(installNotes)
                # Only preserve line feeds
                installNotes = installNotes.replace('\n', '<br/>')
                html = '<body>%s</body>' % installNotes
                installNotesHtmlView.display_html_safe(html.encode("utf8"))

            self.nonempty_install_notes = True

            self.xml.get_object("InstallNotesContent").show()
        else:
            self.nonempty_install_notes = False

        self.xml.get_object("InstallNotesContent").set_property('visible', self.nonempty_install_notes and not self.xml.get_object("AdvancedContent").get_property('visible'))


        if show_warnings:
            (installErrors, installWarnings) = self.get_install_issues()
            (cxdiagErrors, cxdiagWarnings, cxdiagPackages) = self.get_cxdiag_install_notes()
        else:
            installErrors = installWarnings = []
            cxdiagErrors = cxdiagWarnings = cxdiagPackages = []

        self.xml.get_object("InstallNotesContent").set_property('visible', self.nonempty_install_notes and not self.xml.get_object("AdvancedContent").get_property('visible'))

        has_dependencies = False

        packagesNotesHtmlView.get_buffer().set_text("")
        if cxdiagPackages:
            html = '<body>'
            if cxdiagPackages:
                html += '<p>' + ', '.join(cxdiagPackages) + '</p>'
            html += '</body>'
            packagesNotesHtmlView.display_html(html.encode("utf8"))

            if cxdiagPackages:
                unixPackagesInstallCB.show()
            else:
                unixPackagesInstallCB.hide()
            packagesNotesHtmlView.show()
            if self.installTask.apply_cxfixes:
                has_dependencies = True
        else:
            unixPackagesInstallCB.hide()
            packagesNotesHtmlView.hide()

        notes = []

        if self.installTask.target_bottle and self.installTask.target_bottle.analyzed() and \
           len(self.installTask.target_bottle.installers) >= 2:
            has_dependencies = True

        if not self.wantPackageSelection() and \
           not self.wantMediaSelection() and not self.wantBottleSelection() and \
           not self.installTask.target_bottle.analyzed():
            # We have a greyed out Install button
            notes.append('<p>' + cxutils.html_escape(_("The install cannot continue until the '%s' bottle is scanned for dependencies. Please wait.") % self.installTask.bottlename) + '</p>')

        if self.installTask.installerLocale:
            langid = self.installTask.installerLocale
            notes.append('<p><img src="check" /> ' + cxutils.html_escape(_("CrossOver will install the %(language)s version") % {'language':c4profiles.LANGUAGES.get(langid, langid)}) + '</p>')

        if self.installTask.virtual:
            if has_dependencies:
                notes.append('<p><img src="check" /> ' + cxutils.html_escape(_("CrossOver will install dependencies only")) + '</p>')
                has_dependencies = False
            else:
                notes.append('<p><img src="check" /> ' + cxutils.html_escape(_("CrossOver will make configuration changes only")) + '</p>')
        elif self.installTask.installerSource:
            notes.append('<p><img src="check" /> ' + cxutils.html_escape(_("CrossOver will install from %s") % self.installTask.installerSource) + '</p>')
        elif self.installTask.installerDownloadSource:
            notes.append('<p><img src="check" /> ' + cxutils.html_escape(_("CrossOver will download the installer from %s") % self.installTask.installerDownloadSource) + '</p>')
        elif self.installTask.installWithSteam:
            notes.append('<p><img src="check" /> ' + cxutils.html_escape(_("CrossOver will install via Steam")) + '</p>')

        if self.installTask.target_bottle and self.installTask.target_bottle.analyzed():
            if self.installTask.target_bottle.compatible:
                img = 'check'
            else:
                img = 'warning'
            if self.installTask.target_bottle.bottlename:
                msg = _("CrossOver will install into the bottle '%s'") % self.installTask.target_bottle.bottlename
            elif self.installTask.target_bottle.template == 'win98':
                msg = _("CrossOver will install into a new Windows 98 bottle named '%s'") % self.installTask.newBottleName
            elif self.installTask.target_bottle.template == 'win2000':
                msg = _("CrossOver will install into a new Windows 2000 bottle named '%s'") % self.installTask.newBottleName
            elif self.installTask.target_bottle.template == 'winxp':
                msg = _("CrossOver will install into a new Windows XP bottle named '%s'") % self.installTask.newBottleName
            elif self.installTask.target_bottle.template == 'winvista':
                msg = _("CrossOver will install into a new Windows Vista bottle named '%s'") % self.installTask.newBottleName
            elif self.installTask.target_bottle.template == 'win7':
                msg = _("CrossOver will install into a new Windows 7 bottle named '%s'") % self.installTask.newBottleName
            elif self.installTask.target_bottle.template == 'win8':
                msg = _("CrossOver will install into a new Windows 8 bottle named '%s'") % self.installTask.newBottleName
            elif self.installTask.target_bottle.template == 'win10':
                msg = _("CrossOver will install into a new Windows 10 bottle named '%s'") % self.installTask.newBottleName
            elif self.installTask.target_bottle.template == 'winxp_64':
                msg = _("CrossOver will install into a new 64-bit Windows XP bottle named '%s'") % self.installTask.newBottleName
            elif self.installTask.target_bottle.template == 'winvista_64':
                msg = _("CrossOver will install into a new 64-bit Windows Vista bottle named '%s'") % self.installTask.newBottleName
            elif self.installTask.target_bottle.template == 'win7_64':
                msg = _("CrossOver will install into a new 64-bit Windows 7 bottle named '%s'") % self.installTask.newBottleName
            elif self.installTask.target_bottle.template == 'win8_64':
                msg = _("CrossOver will install into a new 64-bit Windows 8 bottle named '%s'") % self.installTask.newBottleName
            elif self.installTask.target_bottle.template == 'win10_64':
                msg = _("CrossOver will install into a new 64-bit Windows 10 bottle named '%s'") % self.installTask.newBottleName
            else:
                # shouldn't happen
                msg = "CrossOver will install into a new %s bottle named '%s'" % (self.installTask.target_bottle.template, self.installTask.newBottleName)
            notes.append('<p><img src="%s" /> ' % img + cxutils.html_escape(msg) + '</p>')

        if has_dependencies:
            notes.append('<p><img src="check" /> ' + cxutils.html_escape(_("CrossOver will also install additional dependencies")) + '</p>')

        if reasons_rev:
            if (installtask.REASON_OVERRIDE, None) in reasons_rev:
                names = []
                for appid in reasons_rev[installtask.REASON_OVERRIDE, None]:
                    if appid not in self.installTask.target_bottle.installers:
                        continue
                    names.append(self.installTask.profiles[appid].name)
                if names:
                    notes.append('<p><img src="check" /> ' + cxutils.html_escape(_("CrossOver will also install the following manually added items: %s") % ', '.join(names)) + '</p>')

            exclude_names = []
            for appid in self.installTask.dependency_overrides:
                if self.installTask.dependency_overrides[appid] != installtask.OVERRIDE_EXCLUDE:
                    continue
                if appid not in self.installTask.target_bottle.reasons:
                    continue
                if appid not in self.installTask.profiles:
                    continue
                exclude_names.append(self.installTask.profiles[appid].name)

            if exclude_names:
                notes.append('<p><img src="warning" /> ' + cxutils.html_escape(_("The following dependencies were manually excluded from the install: %s") % ', '.join(exclude_names)) + '</p>')

        html = self.get_combined_notes(''.join(notes), installErrors, installWarnings, cxdiagErrors, cxdiagWarnings)
        self.xml.get_object('InstallSummary').get_buffer().set_text("")
        self.xml.get_object('InstallSummary').display_html(html.encode("utf8"))

    def set_apply_cxfixes(self, _widget):
        self.installTask.apply_cxfixes = _widget.get_active()

    #-----------------------------------------------------------------------
    #  Package Selection
    #-----------------------------------------------------------------------
    def wantPackageSelection(self):
        return self.installTask.profile is None

    def package_toggle(self, widget):
        if widget.get_active() and not self.xml.get_object('PackageSelectContent').get_property('visible'):
            self.set_expanded_section('PackageSelect')
        elif not widget.get_active() and self.xml.get_object('PackageSelectContent').get_property('visible'):
            widget.set_active(True)

    def package_completion_match(self, _completion, _key_string, _iter, _key_data):
        return True

    def package_completion_selected(self, _completion, model, iterator):
        appid = model.get_value(iterator, 0)
        self.installTask.profile = self.installTask.profiles[appid]
        return True

    def package_match_sortkey_and_markup(self, appid, key_string):
        if appid == 'com.codeweavers.unknown':
            markup = cxutils.html_escape(_("Unlisted application '%s'") % self.xml.get_object('SimpleSearchBox').get_real_text())
            sortkey = 'C'
            return sortkey, markup

        profile = self.installTask.profiles[appid]

        name = profile.name
        index = name.lower().find(key_string)

        if index == 0:
            sortkey = 'A' + name
        else:
            sortkey = 'B' + name

        if index == -1:
            markup = cxutils.html_escape(name)
        else:
            markup = cxutils.html_escape(name[0:index]) + '<b>' + \
                cxutils.html_escape(name[index:index+len(key_string)]) + '</b>' + \
                cxutils.html_escape(name[index+len(key_string):])

        return sortkey, markup

    def on_BrowseApplicationsButton_clicked(self, widget):
        self.xml.get_object("BrowseApplicationsBox").set_property('visible', widget.get_active())

    def on_ManageApplicationsButton_clicked(self, _widget):
        __import__('crossoverui').open_or_show(None)

    def refilter_packages(self):
        if not self.installTask.profiles:
            self.packageCompletionStore.clear()
            self.packageCompletionStore.append(row=(None, 'a', cxutils.html_escape(_(u"Please wait while CrossOver gathers information about available applications\u2026"))))
            self.packageCompletion.complete()
            return

        key_string = self.xml.get_object("SimpleSearchBox").get_real_text().lower()
        if key_string:
            results = set()
            seen_results = set()
            i = 0

            for appid, names in self.package_search_keys.iteritems():
                if key_string in names:
                    results.add(appid)
                    i += 1
                    if i >= 128:
                        # gtk performs poorly if we try to display too many results
                        self.packageCompletionStore.clear()
                        return

            results.add(u'com.codeweavers.unknown')

            iterator = self.packageCompletionStore.get_iter_first()
            while iterator:
                iter_next = self.packageCompletionStore.iter_next(iterator)

                appid = self.packageCompletionStore.get_value(iterator, 0)
                if appid in results:
                    seen_results.add(appid)
                    results.remove(appid)

                    sortkey, markup = self.package_match_sortkey_and_markup(appid, key_string)

                    old_sortkey = self.packageCompletionStore.get_value(iterator, 1)
                    old_markup = self.packageCompletionStore.get_value(iterator, 2)

                    if old_sortkey != sortkey:
                        self.packageCompletionStore.set_value(iterator, 1, sortkey)
                    if old_markup != markup:
                        self.packageCompletionStore.set_value(iterator, 2, markup)
                elif appid not in seen_results:
                    self.packageCompletionStore.remove(iterator)
                iterator = iter_next

            for appid in results:
                sortkey, markup = self.package_match_sortkey_and_markup(appid, key_string)
                self.packageCompletionStore.append(row=(appid, sortkey, markup))
            if self.xml.get_object("SimpleSearchBox").is_focus():
                self.packageCompletion.complete()
        else:
            self.packageCompletionStore.clear()

    def on_SimpleSearchBox_real_text_changed(self, entry, real_text):
        self.refilter_packages()
        self.installTask.SetNewBottleNameForUnknown(real_text)
        if self.installTask.profile and self.installTask.profile.appid == u'com.codeweavers.unknown':
            self.profileChanged()
        if real_text:
            entry.set_property('secondary-icon-sensitive', True)
        else:
            entry.set_property('secondary-icon-sensitive', False)

    def on_SimpleSearchBox_icon_release(self, entry, icon_pos, _event):
        if icon_pos == gtk.ENTRY_ICON_SECONDARY:
            entry.set_text('')

    def update_package_tree(self, treestore, contents, parent_iterator=None):
        to_add = set(contents)

        if parent_iterator:
            iterator = treestore.iter_children(parent_iterator)
        else:
            iterator = treestore.get_iter_first()

        while iterator:
            key = treestore.get_value(iterator, 1)
            iter_next = treestore.iter_next(iterator)
            if key in to_add:
                if isinstance(contents[key], dict):
                    self.update_package_tree(treestore, contents[key], iterator)
                else:
                    current_name = cxutils.string_to_unicode(treestore.get_value(iterator, 0))
                    if current_name != contents[key].name:
                        treestore.set(iterator, 0, contents[key].name, 2, 'B'+contents[key].name)
                to_add.remove(key)
            elif key in contents:
                pass
            else:
                treestore.remove(iterator)
            iterator = iter_next

        for key in to_add:
            obj = contents[key]
            if isinstance(obj, dict):
                newrow = treestore.append(parent_iterator, (key, key, 'A'+key))
                self.update_package_tree(treestore, obj, newrow)
            else:
                treestore.append(parent_iterator, (obj.name, key, 'B'+obj.name))

    def updatePackageListing(self):
        self.package_search_keys = {}

        for appid, profile in self.installTask.profiles.iteritems():
            if not profile.is_for_current_product:
                continue

            if not self.show_untested_apps and not profile.is_ranked and \
                    appid != c4profilesmanager.UNKNOWN:
                continue

            names = []
            for lang in cxutils.get_preferred_languages():
                if lang in profile.localized_names:
                    name = profile.localized_names[lang].lower()
                    if name not in names:
                        names.append(name)
            self.package_search_keys[appid] = '\n'.join(names)

        self.refilter_packages()

        if self.profile_tree:
            self.update_package_tree(self.packageTreeStore, self.profile_tree)

            if self.packageTreeStore.get_sort_column_id() != (2, gtk.SORT_ASCENDING):
                self.packageTreeStore.set_sort_column_id(2, gtk.SORT_ASCENDING)
        else:
            self.packageTreeStore.clear()
            self.packageTreeStore.append(None, (_(u"Please wait while CrossOver gathers information about available applications\u2026"), None, ''))

    def package_changed_gui(self, _widget):
        packageTreeView = self.xml.get_object("PackageTreeView")
        cursor = packageTreeView.get_cursor()
        if cursor and cursor[0]:
            selectedID = self.packageTreeStore.get_value(self.packageTreeStore.get_iter(cursor[0]), 1)
            if selectedID in self.installTask.profiles:
                self.installTask.profile = self.installTask.profiles[selectedID]
                if cxlog.is_on('c4profiles'):
                    cxlog.log_('c4profiles', "selected %s" % cxlog.to_str(self.installTask.profile.appid))
                    self.installTask.profile.dump(cxlog.get_file(), "| ")

    def package_row_activated(self, _widget, path, _column):
        packageTreeView = self.xml.get_object("PackageTreeView")
        appid = self.packageTreeStore.get_value(self.packageTreeStore.get_iter(path), 1)
        if appid in self.installTask.profiles:
            self.advance_section(False)
        else:
            if packageTreeView.row_expanded(path):
                packageTreeView.collapse_row(path)
            else:
                packageTreeView.expand_row(path, False)

    def select_package_by_appid(self, model, path, treeiter, appid):
        if model.get_value(treeiter, 1) == appid:
            treeview = self.xml.get_object("PackageTreeView")
            treeview.expand_to_path(path)
            treeview.set_cursor(path)
            treeview.scroll_to_cell(path)
            return True # stop iteration
        return False # continue iteration

    def select_language_by_id(self, model, path, treeiter, langid):
        if model.get_value(treeiter, 1) == langid:
            self.xml.get_object("LanguageSelectorCombo").set_active(path[0])
            return True # stop iteration
        return False # continue iteration

    medal_names = {
        'gold': _("Gold Medal (Officially Supported)"),
        'silver': _("Silver Medal (Officially Supported)"),
        'bronze': _("Bronze Medal (Officially Supported)"),
        'ungold': _("Gold Medal (Unsupported)"),
        'unsilver': _("Silver Medal (Unsupported)"),
        'unbronze': _("Bronze Medal (Unsupported)"),
        'knownnottowork': _("Known not to work"),
        'untested': _("Untested"),
        'untrusted': _("Untrusted"),
    }

    medal_descriptions = {
        'gold': _("Installs and runs as you would expect in Microsoft Windows. We expect that you can use this application on an everyday basis with good results and only minor bugs."),
        'silver': _("Installs and runs well enough to be usable. However, this application has bugs that prevent it from running flawlessly."),
        'bronze': _("Installs, runs, and can accomplish some portion of its fundamental mission. However, this application has enough bugs that we recommend you use it with caution. Save early/save often, and don't be surprised if there are some bumps along the way."),
        'ungold': _("Installs and runs as you would expect in Microsoft Windows. Our users expect that you can use this application on an everyday basis with good results and only minor bugs."),
        'unsilver': _("Installs and runs well enough to be usable. However, our users find that this application has bugs that prevent it from running flawlessly."),
        'unbronze': _("Installs, runs, and can accomplish some portion of its fundamental mission. However, this application has enough bugs that we recommend you use it with caution. Save early/save often, and don't be surprised if there are some bumps along the way."),
        'knownnottowork': _("This application either does not install or does not run."),
        'untested': _("We do not know whether or not this application will install and run. If you try it, consider letting us know how it runs, using the compatibility database."),
        'untrusted': _("This profile is from an unknown source. Installing may pose a security risk to your system."),
    }

    rating_descriptions = {
        5: _("Runs Great"),
        4: _("Runs Well"),
        3: _("Limited Functionality"),
        2: _("Installs, Will Not Run"),
        1: _("Will Not Install"),
        0: _("Untested"),
    }

    rating_tooltips = {
        5: _("This application installs and runs great, the same as it would in Windows.\n\nPractically all of the application is functional. Using this application in CrossOver should be the same as running the application on native Windows OS."),
        4: _("This application installs and runs quite well, with only minor glitches found.\n\nThe core functionality of this Windows application works fine under CrossOver. There still may be some extra functions that don't work right, but overall this application should be mostly usable. We don't maintain a list of what specific functions work/don't work per-application in our compatibility database, but you may want to check this application's Forums at the CodeWeavers website and see if other users have reported any particular details about running this Windows application in CrossOver."),
        3: _("This application installs and runs, but has problems.\n\nThis application runs, but there are significant functional gaps. CrossOver translates Windows commands to their corresponding Linux commands and back. When a Windows program uses commands that CrossOver doesn't know how to translate the Windows program may not respond when you click things, have garbled output, or crash. You might be able to add different dependencies or try workarounds to get this application to run better, so please check this application's page at the CodeWeavers website. It's also possible that programming added in a future release of CrossOver might help this Windows application run better, so check back later."),
        2: _("This application installs, but fails to run.\n\nThe Windows application immediately closes or freezes when you try to run it. You may be able to add various dependencies or try different workarounds to get this application to start, so please check the Support section of the CodeWeavers website for more information and tutorials. If the program still won't launch then whatever programming this Windows application needs to work doesn't exist yet in CrossOver."),
        1: _("This application won't get through installation.\n\nMost installers are standardized and CrossOver will be able to run them without a problem. However some companies customize the installer, or write their own, and the custom programming fails in CrossOver. The Windows application itself might be functional, but there's no way to get it out of the installer."),
        0: _("No ratings have been reported.\n\nAll application entries start at this rating. Over time staff and users can visit the What Runs page of the CodeWeavers website and rate how far they got in running this Windows application in CrossOver. If you try this application please submit a rating for how this application worked, or didn't, in CrossOver."),
        -1: _("This installation recipe is from an unknown source. Installing may pose a security risk to your system."),
    }

    def update_profile_caption(self):
        button = self.xml.get_object("PackageSelectToggle")
        if self.installTask.profile.appid == u'com.codeweavers.unknown' and \
           self.xml.get_object('SimpleSearchBox').get_real_text():
            name = self.xml.get_object('SimpleSearchBox').get_real_text()
        else:
            name = self.installTask.profile.name
        if self.installTask.installerLocale:
            langid = self.installTask.installerLocale
            button.set_tooltip_text(_("Will install %(application)s (%(language)s version)") % {
                'application': name,
                'language': c4profiles.LANGUAGES.get(langid, langid)})
            self.xml.get_object("PackageSelectCheckmark").show()
        else:
            button.set_tooltip_text(_("Will install %s") % name)
            self.xml.get_object("PackageSelectCheckmark").show()

    def update_package_rating(self):
        self.xml.get_object("PackageRatingView").get_buffer().set_text('')
        self.xml.get_object("PackageRatingView1").get_buffer().set_text('')
        if self.installTask.profile:
            rating = self.installTask.profile.app_profile.medal_rating
            if 1 <= rating <= 5:
                icons = ['star'] * rating + ['star-empty'] * (5 - rating)
            elif rating == 0:
                icons = ['question'] * 5
            else:
                icons = ['untrusted'] * 5

            text = self.rating_descriptions.get(rating, _("Untrusted"))
            tooltip = self.rating_tooltips.get(rating, self.rating_tooltips[-1])

            app = self.installTask.profile.app_profile
            if app.medal_rating and app.medal_version:
                version_text = _("Last Tested: CrossOver %s") % app.medal_version
                try:
                    timestamp = iso8601.parse_date(app.medal_date)
                    version_tooltip = ((_("Last Ranked: %s") % timestamp.strftime('%Y-%m-%d')) + '\n' +
                                       (_("Number of Rankings: %s") % app.medal_count))
                except iso8601.ParseError:
                    version_tooltip = None
            else:
                version_text = None
                version_tooltip = None

            html = '<body><p>%s</p></body>' % ' '.join('<img src="%s"/>' % i for i in icons)

            self.xml.get_object("PackageRatingView").display_html(html)
            self.xml.get_object("PackageRatingView1").display_html(html)

            self.xml.get_object("PackageRatingLabel").set_text(text)
            self.xml.get_object("PackageRatingLabel").show()
            self.xml.get_object("PackageRatingLabel1").set_text(text)
            self.xml.get_object("PackageRatingLabel1").show()

            if version_text:
                self.xml.get_object("PackageLastTested").set_text(version_text)
                self.xml.get_object("PackageLastTested").set_tooltip_text(version_tooltip)
                self.xml.get_object("PackageLastTested").show()
                self.xml.get_object("PackageLastTested1").set_text(version_text)
                self.xml.get_object("PackageLastTested1").set_tooltip_text(version_tooltip)
                self.xml.get_object("PackageLastTested1").show()
            else:
                self.xml.get_object("PackageLastTested").hide()
                self.xml.get_object("PackageLastTested1").hide()

            self.xml.get_object("PackageRatingView").set_tooltip_text(tooltip)
            self.xml.get_object("PackageRatingView1").set_tooltip_text(tooltip)
            self.xml.get_object("PackageRatingLabel").set_tooltip_text(tooltip)
            self.xml.get_object("PackageRatingLabel1").set_tooltip_text(tooltip)
        else:
            self.xml.get_object("PackageRatingLabel").hide()
            self.xml.get_object("PackageRatingLabel1").hide()
            self.xml.get_object("PackageLastTested").hide()
            self.xml.get_object("PackageLastTested1").hide()

    def on_PackageRatingView_style_set(self, widget, _previous_style):
        blended_fg = cxguitools.blend_colors(widget.style.text[gtk.STATE_NORMAL], widget.style.base[gtk.STATE_NORMAL], 3, 1)
        blended_fg2 = cxguitools.blend_colors(widget.style.text[gtk.STATE_NORMAL], widget.style.base[gtk.STATE_NORMAL], 1, 3)
        brightness = max(blended_fg.red, blended_fg.green, blended_fg.blue, blended_fg2.red, blended_fg2.green, blended_fg2.blue)

        star_icon = gtk.gdk.pixbuf_new_from_file(os.path.join(cxutils.CX_ROOT, 'share/images/rating_star.png'))
        if star_icon:
            star_icon = cxguitools.recolor_pixbuf(star_icon, gtk.gdk.Color(brightness, brightness, brightness//3))
            widget.images[u'star'] = star_icon

        star_empty_icon = gtk.gdk.pixbuf_new_from_file(os.path.join(cxutils.CX_ROOT, 'share/images/rating_star_empty.png'))
        if star_empty_icon:
            star_empty_icon = cxguitools.recolor_pixbuf(star_empty_icon, cxguitools.blend_colors(widget.style.text[gtk.STATE_NORMAL], widget.style.base[gtk.STATE_NORMAL], 1, 2))
            widget.images[u'star-empty'] = star_empty_icon

        untrusted_icon = gtk.gdk.pixbuf_new_from_file(os.path.join(cxutils.CX_ROOT, 'share/images/rating_untrusted.png'))
        if untrusted_icon:
            untrusted_icon = cxguitools.recolor_pixbuf(untrusted_icon, widget.style.text[gtk.STATE_NORMAL])
            widget.images[u'untrusted'] = untrusted_icon

        question_icon = gtk.gdk.pixbuf_new_from_file(os.path.join(cxutils.CX_ROOT, 'share/images/rating_question.png'))
        if question_icon:
            question_icon = cxguitools.recolor_pixbuf(question_icon, widget.style.text[gtk.STATE_NORMAL])
            widget.images[u'question'] = question_icon

        self.update_package_rating()

    def profileChanged(self):
        descriptionScroll = self.xml.get_object("PackageDescriptionScroll")
        descriptionTextView = self.xml.get_object("PackageDescription")
        package_button = self.xml.get_object("PackageSelectToggle")
        MediaSelectToggle = self.xml.get_object("MediaSelectToggle")
        packageTreeView = self.xml.get_object("PackageTreeView")
        installNotesScroll = self.xml.get_object("InstallNotesScroll")

        self.update_package_rating()

        if self.installTask.profile:
            description_lines = []

            cursor = packageTreeView.get_cursor()

            if not (cursor and cursor[0] and
                    self.packageTreeStore.get_value(self.packageTreeStore.get_iter(cursor[0]), 1) == self.installTask.profile.appid):
                # selection changed
                self.packageTreeStore.foreach(self.select_package_by_appid, self.installTask.profile.appid)

            if self.installTask.profile.appid == u'com.codeweavers.unknown' and \
               self.xml.get_object('SimpleSearchBox').get_real_text():
                name = _("Unlisted application '%s'") % self.xml.get_object('SimpleSearchBox').get_real_text()
            else:
                name = self.installTask.profile.name

            self.xml.get_object("PackageNameLabel").set_text(name)

            ready_text = _("CrossOver is ready to install\n%s")
            ready_markup = '<big><big><big><big>%s</big></big></big></big>' % (ready_text % ('<b>%s</b>' % cxutils.html_escape(name)))
            self.xml.get_object("ReadyToInstallLabel").set_markup(ready_markup)

            self.xml.get_object("InstallingHeader").set_text(_("Installing %(application)s") % {'application':name})

            description_lines.append('%s' % self.installTask.profile.app_profile.description)

            if self.installTask.profile.details_url:
                description_lines.append('<a href="%s">%s</a>' % (
                    cxutils.html_escape(self.installTask.profile.details_url),
                    cxutils.html_escape(_("More info"))))

            if self.installTask.profile.contributors:
                description_lines.append(_("Tie contributors: %s") % self.installTask.profile.contributors_html.decode('utf8'))

            description = '<body>%s</body>' % ''.join('<p>%s</p>' % line for line in description_lines)
            #  Encode description as utf8 because htmltextview likes that better.
            descriptionTextView.display_html_safe(description.encode('utf8'))

            # Move the description view back to the top-left corner
            adjust = descriptionScroll.get_hadjustment()
            adjust.set_value(adjust.get_lower())
            adjust = descriptionScroll.get_vadjustment()
            adjust.set_value(adjust.get_lower())

            # Move the installation notes view back to the top-left corner
            adjust = installNotesScroll.get_hadjustment()
            adjust.set_value(adjust.get_lower())
            adjust = installNotesScroll.get_vadjustment()
            adjust.set_value(adjust.get_lower())

            self.update_profile_caption()

            self.xml.get_object("PackageDescriptionBox").show()

        else:
            packageTreeView = self.xml.get_object("PackageTreeView")
            packageTreeView.get_selection().unselect_all()
            descriptionTextView.get_buffer().set_text("")
            package_button.set_tooltip_text(_("Select an application to install"))
            self.xml.get_object("PackageSelectCheckmark").hide()
            self.xml.get_object("PackageDescriptionBox").hide()

        languages = self.installTask.profile_languages()
        if languages:
            self.xml.get_object("LanguageSelectorCombo").set_model(None)
            self.languageTreeStore.clear()
            for langid in languages:
                if langid == '':
                    langname = _("Other language")
                    langsort = '1'
                else:
                    langname = c4profiles.LANGUAGES.get(langid, langid)
                    langsort = u'0'+langname
                self.languageTreeStore.append((langname, langid, langsort))
            self.xml.get_object("LanguageSelectorCombo").set_model(self.languageTreeStore)
            self.xml.get_object("LanguageSelectorBox").show()
            self.languageTreeStore.foreach(self.select_language_by_id, self.installTask.installerLocale)
        else:
            self.xml.get_object("LanguageSelectorBox").hide()
            self.update_download_item()
            self.update_steam_item()

        MediaSelectToggle.set_sensitive(not self.installTask.virtual)

        self.updateBottleListing()

        self.updateMediaListing()
        self.sourceChanged() # update "Select Installer" button if changing between virtual/non-virtual
        self.update_common_widgets()

    def locale_changed(self, _widget):
        index = self.xml.get_object('LanguageSelectorCombo').get_active()
        treeiter = self.languageTreeStore.get_iter((index,))
        langid = self.languageTreeStore.get_value(treeiter, 1)
        self.installTask.SetLocale(langid)

        self.update_download_item()
        self.update_steam_item()
        self.update_profile_caption()
        self.update_common_widgets()

    def on_AdvancedOptionsButton_clicked(self, widget):
        self.xml.get_object('SummaryContent').set_property('visible', not widget.get_active())
        self.xml.get_object('AdvancedContent').set_property('visible', widget.get_active())
        self.xml.get_object("InstallNotesContent").set_property('visible', self.nonempty_install_notes and not self.xml.get_object("AdvancedContent").get_property('visible'))
        self.update_common_widgets()

    #-----------------------------------------------------------------------
    #  Installer Media Selection
    #-----------------------------------------------------------------------

    def browse_for_installer_file(self, caller):
        filePicker = gtk.FileChooserDialog(_("Choose a Windows installer file"), caller, gtk.FILE_CHOOSER_ACTION_OPEN)

        cxguitools.add_filters(filePicker, cxguitools.FILTERS_INSTALLABLE | cxguitools.FILTERS_ALLFILES)
        filePicker.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
        filePicker.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)

        if filePicker.run() == gtk.RESPONSE_OK:
            selectedFile = filePicker.get_filename()
        else:
            selectedFile = None

        filePicker.destroy()

        return selectedFile


    def browse_for_installer_dir(self, caller):
        filePicker = gtk.FileChooserDialog(_("Choose the directory containing your installer"), caller, gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)

        filePicker.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
        filePicker.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)

        if filePicker.run() == gtk.RESPONSE_OK:
            selectedDir = filePicker.get_filename()
        else:
            selectedDir = None

        filePicker.destroy()

        return selectedDir


    def volumeAdded(self, volume, temporary=False):
        if volume.mountpoint == "/" or not volume.is_disc:
            # unlikely that the user wants to use this.
            return

        # We need to check this is not a duplicate because volumeAdded() is
        # also used for the user-selected installers and directories
        volumesIter = self.volumesListModel.iter_children(None)
        while volumesIter != None:
            if self.volumesListModel.get_value(volumesIter, 2) == volume.mountpoint:
                # This is a duplicate
                if not temporary:
                    # Just in case this was added by installtask first
                    self.volumesListModel.set_value(volumesIter, 5, False)
                return
            volumesIter = self.volumesListModel.iter_next(volumesIter)

        if volume.label:
            label = volume.label
        else:
            label = os.path.basename(volume.mountpoint)
        volumesWidget = self.xml.get_object("MediaTreeView")
        self.installTask.add_source_media(volume.mountpoint, volume.label, volume.device)

        newrow = self.volumesListModel.append()
        self.volumesListModel.set_value(newrow, 0, label)
        self.volumesListModel.set_value(newrow, 2, volume.mountpoint)
        if volume.is_disc:
            pixbuf = volumesWidget.render_icon(gtk.STOCK_CDROM, gtk.ICON_SIZE_DIALOG)
        else:
            pixbuf = volumesWidget.render_icon(gtk.STOCK_HARDDISK, gtk.ICON_SIZE_DIALOG)
        self.volumesListModel.set_value(newrow, 1, pixbuf)
        self.volumesListModel.set_value(newrow, 3, '1'+self.volumesListModel.get_value(newrow, 0))
        self.volumesListModel.set_value(newrow, 4, self.media_markup_format % (cxutils.html_escape(label), cxutils.html_escape(volume.mountpoint)))
        self.volumesListModel.set_value(newrow, 5, temporary)


    def volumeDeleted(self, volume, temporary=False):
        volumesIter = self.volumesListModel.iter_children(None)
        while volumesIter != None:
            if self.volumesListModel.get_value(volumesIter, 2) == volume.mountpoint and \
               (not temporary or self.volumesListModel.get_value(volumesIter, 5)):
                self.volumesListModel.remove(volumesIter)
                self.installTask.remove_source_media(volume.mountpoint)
                if self.installTask.GetInstallerSource() == volume.mountpoint:
                    self.installTask.ClearInstallerSource()
                break
            else:
                volumesIter = self.volumesListModel.iter_next(volumesIter)
        else:
            # currentVolumeLabel not found
            cxlog.log("Warning: removed volume does not exist: %s" % cxlog.debug_str(volume))

    def profileMediaAdded_(self, filename):
        volume = mountpoints.Volume()
        volume.mountpoint = filename
        volume.is_disc = True # Lie so it's taken into account
        self.volumeAdded(volume, True)

    def profileMediaRemoved_(self, filename):
        volume = mountpoints.Volume()
        volume.mountpoint = filename
        volume.is_disc = True # Lie so it's taken into account
        self.volumeDeleted(volume, True)


    def wantMediaSelection(self):
        return not (self.installTask.GetInstallerSource() or
                    self.installTask.installerDownloadSource or
                    self.installTask.virtual or
                    self.installTask.installWithSteam)

    def media_toggle(self, widget):
        if widget.get_active() and not self.xml.get_object('MediaSelectContent').get_property('visible'):
            self.set_expanded_section('MediaSelect')
        elif not widget.get_active() and self.xml.get_object('MediaSelectContent').get_property('visible'):
            widget.set_active(True)


    chooseInstallerFileString = _(u"Choose Installer File\u2026")
    chooseInstallerFolderString = _(u"Choose Installer Folder\u2026")
    downloadInstallerString = _("Download installer")
    steamInstallerString = _("Install Via Steam")
    def updateMediaListing(self):
        app_profile = self.installTask.profile and self.installTask.profile.app_profile
        downloadNotes = self.xml.get_object("DownloadInstallationNotes")

        if app_profile and app_profile.download_page_urls:
            if app_profile.download_page_urls:
                _lang, url = cxutils.get_language_value(app_profile.download_page_urls)
                note = _('This software can be downloaded from its <a href="%(url)s">home page</a>.') % {'url': cxutils.html_escape(url)}

                fullNote = "<body> %s </body>" % note
                downloadNotes.display_html_safe(fullNote.encode('utf8'))
                downloadNotes.show()
            else:
                downloadNotes.hide()
        else:
            downloadNotes.hide()


    def select_media_by_mountpoint(self, model, path, treeiter, mountpoint):
        if model.get_value(treeiter, 2) == mountpoint:
            self.xml.get_object("MediaTreeView").set_cursor(path)
            return True # stop iteration
        return False # continue iteration

    def select_download(self, model, path, treeiter, _userdata):
        if model.get_value(treeiter, 0) == self.downloadInstallerString:
            self.xml.get_object("MediaTreeView").set_cursor(path)
            return True # stop iteration
        return False # continue iteration

    def select_steam(self, model, path, treeiter, _userdata):
        if model.get_value(treeiter, 0) == self.steamInstallerString:
            self.xml.get_object("MediaTreeView").set_cursor(path)
            return True # stop iteration
        return False # continue iteration

    def sourceChanged(self):
        ToggleButton = self.xml.get_object("MediaSelectToggle")

        if self.installTask.virtual:
            self.xml.get_object("MediaTreeView").get_selection().unselect_all()

            ToggleButton.set_tooltip_text(_("Selecting an installer is not required for '%s'") % self.installTask.profile.name)
            self.xml.get_object("MediaSelectCheckmark").show()

        elif self.installTask.installWithSteam:
            self.volumesListModel.foreach(self.select_steam, self.installTask.installerSource)

            ToggleButton.set_tooltip_text(_("Will install via Steam"))
            self.xml.get_object("MediaSelectCheckmark").show()
        elif self.installTask.installerSource:
            self.volumesListModel.foreach(self.select_media_by_mountpoint, self.installTask.installerSource)

            ToggleButton.set_tooltip_text(_("Will install from %s") % self.installTask.installerSource)
            self.xml.get_object("MediaSelectCheckmark").show()

        elif self.installTask.installerDownloadSource:
            self.volumesListModel.foreach(self.select_download, self.installTask.installerSource)

            ToggleButton.set_tooltip_text(_("Will download the installer from %s") % self.installTask.installerDownloadSource)
            self.xml.get_object("MediaSelectCheckmark").show()

        else:
            self.xml.get_object("MediaTreeView").get_selection().unselect_all()

            ToggleButton.set_tooltip_text(_("Select an installer"))
            self.xml.get_object("MediaSelectCheckmark").hide()

        self.update_common_widgets()

    def update_download_item(self):
        url = self.installTask.GetDownloadURL()

        volumesIter = self.volumesListModel.iter_children(None)
        while volumesIter != None:
            if self.volumesListModel.get_value(volumesIter, 0) == self.downloadInstallerString:
                if url:
                    break
                else:
                    self.volumesListModel.remove(volumesIter)
                    return
            volumesIter = self.volumesListModel.iter_next(volumesIter)
        else:
            if url:
                # No download item; add it.
                volumesIter = self.volumesListModel.append()
                self.volumesListModel.set_value(volumesIter, 0, self.downloadInstallerString)
                pixbuf = self.xml.get_object('MediaTreeView').render_icon(gtk.STOCK_NETWORK, gtk.ICON_SIZE_DIALOG)
                self.volumesListModel.set_value(volumesIter, 1, pixbuf)
                self.volumesListModel.set_value(volumesIter, 3, '0')
                if self.installTask.installerDownloadSource:
                    self.xml.get_object("MediaTreeView").set_cursor(self.volumesListModel.get_path(volumesIter))
            else:
                return
        self.volumesListModel.set_value(volumesIter, 4, self.media_markup_format % (self.downloadInstallerString, cxutils.html_escape(url)))

    def update_steam_item(self):
        volumesIter = self.volumesListModel.iter_children(None)
        while volumesIter != None:
            if self.volumesListModel.get_value(volumesIter, 0) == self.steamInstallerString:
                if self.installTask.GetSteamID():
                    break
                else:
                    self.volumesListModel.remove(volumesIter)
                    return
            volumesIter = self.volumesListModel.iter_next(volumesIter)
        else:
            if self.installTask.GetSteamID():
                volumesIter = self.volumesListModel.append()
                pixbuf = gtk.gdk.pixbuf_new_from_file(os.path.join(cxutils.CX_ROOT, 'share/images/steam_logo.png'))
                self.volumesListModel.set_value(volumesIter, 1, pixbuf)
                self.volumesListModel.set_value(volumesIter, 3, '0')
                self.xml.get_object("MediaTreeView").set_cursor(self.volumesListModel.get_path(volumesIter))
                self.volumesListModel.set_value(volumesIter, 0, self.steamInstallerString)
                self.volumesListModel.set_value(volumesIter, 4, self.media_markup_format % (self.steamInstallerString, ""))
                if self.installTask.installWithSteam:
                    self.xml.get_object("MediaTreeView").set_cursor(self.volumesListModel.get_path(volumesIter))


    def set_custom_installer_source(self, path):
        volume = mountpoints.Volume()
        volume.mountpoint = path
        volume.is_disc = True # Lie so it's taken into account
        self.volumeAdded(volume)
        self.installTask.SetInstallerSource(path)
        self.update_common_widgets()

    def source_changed_gui(self, _widget):
        mediaTreeView = self.xml.get_object("MediaTreeView")
        selections = mediaTreeView.get_selection().get_selected_rows()[1]

        if selections:
            selectedPath = selections[0]
            selectedIter = self.volumesListModel.get_iter(selectedPath)
            volumeName = self.volumesListModel.get_value(selectedIter, 0)
            mountPoint = self.volumesListModel.get_value(selectedIter, 2)

            if volumeName == self.chooseInstallerFileString:
                selectedFile = self.browse_for_installer_file(self.xml.get_object("InstallWizard"))
                if selectedFile:
                    self.set_custom_installer_source(selectedFile)
                else:
                    self.installTask.ClearInstallerSource()
                    mediaTreeView.get_selection().unselect_all()
            elif volumeName == self.chooseInstallerFolderString:
                selectedDir = self.browse_for_installer_dir(self.xml.get_object("InstallWizard"))
                if selectedDir:
                    self.set_custom_installer_source(selectedDir)
                else:
                    self.installTask.ClearInstallerSource()
                    mediaTreeView.get_selection().unselect_all()
            elif volumeName == self.downloadInstallerString:
                self.installTask.SetInstallerDownload()
            elif volumeName == self.steamInstallerString:
                self.installTask.SetInstallerSteam()
            else:
                self.installTask.SetInstallerSource(mountPoint)
        else:
            self.installTask.ClearInstallerSource()

        self.update_common_widgets()

    #-----------------------------------------------------------------------
    #  Bottle Selection
    #-----------------------------------------------------------------------

    category_markup = {
        installtask.CAT_NONE: "<b>"+_("Bottles")+"</b>",
        installtask.CAT_RECOMMENDED: "<b>"+_("Recommended Bottles")+"</b>",
        installtask.CAT_COMPATIBLE: "<b>"+_("Compatible Bottles")+"</b>",
        installtask.CAT_INCOMPATIBLE: "<b>"+_("Incompatible Bottles")+"</b>"
        }

    category_sort = {
        installtask.CAT_NONE: '0',
        installtask.CAT_RECOMMENDED: '1',
        installtask.CAT_COMPATIBLE: '2',
        installtask.CAT_INCOMPATIBLE: '3',
        }

    def categorizedBottle_(self, _target_bottle):
        self.updateBottleListing()

    def categorizedAllBottles(self):
        pass

    def analyzedBottle_(self, target_bottle):
        if target_bottle is self.installTask.target_bottle:
            self.update_common_widgets()

    def invalidNewBottleName(self):
        cxlog.log("invalid new bottle name [%s]" % cxlog.to_str(self.installTask.newBottleName))
        # FIXME: Todo

    def wantBottleSelection(self):
        if not self.installTask.bottlename:
            # no bottle selected
            return True
        if self.installTask.GetCreateNewBottle():
            # check that the name doesn't match an existing bottle
            for bottlename in bottlecollection.sharedCollection().bottleList():
                if self.installTask.bottlename.lower() == bottlename.lower():
                    return True
        return False


    def bottle_toggle(self, widget):
        if widget.get_active() and not self.xml.get_object('BottleSelectContent').get_property('visible'):
            self.set_expanded_section('BottleSelect')
        elif not widget.get_active() and self.xml.get_object('BottleSelectContent').get_property('visible'):
            widget.set_active(True)


    def update_bottle_selection_label(self):
        ToggleButton = self.xml.get_object("BottleSelectToggle")
        bottlename = self.installTask.bottlename
        if not bottlename:
            ToggleButton.set_tooltip_text(_("Select a bottle into which to install"))
        elif self.installTask.GetCreateNewBottle():
            ToggleButton.set_tooltip_text(_("Will install into a new %(template)s bottle '%(name)s'") % {
                'template': bottlemanagement.get_template_name(self.installTask.newBottleTemplate),
                'name': bottlename})
        else:
            ToggleButton.set_tooltip_text(_("Will install into the bottle '%(name)s'") % {'name': bottlename})

        if bottlename and \
            (self.installTask.newBottleNameRequested if self.installTask.GetCreateNewBottle() else self.installTask.targetBottleRequested):
            self.xml.get_object("PackageSelectHeader").set_text(_("Select an Application to Install Into '%s'") % bottlename)
        else:
            self.xml.get_object("PackageSelectHeader").set_text(_("Select an Application to Install"))

    def select_bottle(self, model, path, treeiter, _userdata):
        if self.installTask.GetCreateNewBottle():
            bottle_string = '.' + self.installTask.GetNewBottleTemplate()
        elif self.installTask.bottlename:
            bottle_string = self.installTask.bottlename
        else:
            bottle_string = None

        if model.get_value(treeiter, 0) == bottle_string:
            self.xml.get_object("BottleSelectionView").set_cursor(path)
            return True # stop iteration
        return False # continue iteration

    def _bottle_list_cmp(self, treemodel, iter1, iter2):
        key1 = treemodel.get(iter1, 3)
        key2 = treemodel.get(iter2, 3)
        return cmp(key1, key2)

    def updateBottleListing(self):
        if self.installTask.profile:
            bottles = self.installTask.bottles.values()
            bottles_completed = 0
            for target_bottle in bottles:
                if target_bottle.has_category():
                    bottles_completed += 1

            if bottles_completed != len(bottles):
                progressbar = self.xml.get_object("BottleSelectProgress")
                progressbar.set_fraction(float(bottles_completed) / len(bottles))
                self.xml.get_object("BottleSelectProgress").show()
                self.xml.get_object("BottleSelectListing").hide()
                return

        self.bottleListModel.clear()

        bottles_by_category = {}

        for target_bottle in self.installTask.bottles.values() + self.installTask.templates.values():
            category = target_bottle.get_category()
            if category in bottles_by_category:
                bottles_by_category[category].append(target_bottle)
            else:
                bottles_by_category[category] = [target_bottle]

        for category in bottles_by_category:
            category_row = self.bottleListModel.append(None, row=('', self.category_markup[category], '', self.category_sort[category]))
            for target_bottle in bottles_by_category[category]:
                if isinstance(target_bottle, installtask.TargetTemplate):
                    name = '.' + target_bottle.template
                    markup = _("(New %(template)s bottle)") % {'template': bottlemanagement.get_template_name(target_bottle.template)}
                    sortkey = (0,) + bottlemanagement.get_template_key(target_bottle.template)
                else:
                    name = target_bottle.bottlename
                    markup = cxutils.html_escape(target_bottle.bottlename)
                    sortkey = (1, target_bottle.bottlename)
                template = target_bottle.template
                self.bottleListModel.append(parent=category_row, row=(name, markup, bottlemanagement.get_template_name(template), sortkey))

        self.xml.get_object("BottleSelectionView").expand_all()
        self.bottleListModel.foreach(self.select_bottle, None)

        self.xml.get_object("BottleSelectProgress").hide()
        self.xml.get_object("BottleSelectListing").show()


    def bottleCreateChanged(self):
        self.bottleListModel.foreach(self.select_bottle, None)
        if self.installTask.GetCreateNewBottle():
            self.xml.get_object("NewBottleWidgets").show()
        else:
            self.xml.get_object("NewBottleWidgets").hide()
        self.xml.get_object("BottleSideWidgets").set_property('visible', self.xml.get_object("NewBottleWidgets").get_property('visible') or
                                                              self.xml.get_object("BottleErrorText").get_property('visible'))
        self.update_bottle_selection_label()
        self.update_common_widgets()

    def bottleNameChanged(self):
        self.bottleListModel.foreach(self.select_bottle, None)
        self.update_bottle_selection_label()
        self.update_common_widgets()

    def bottleNewnameChanged(self):
        if self.installTask.newBottleName:
            self.xml.get_object("NewBottleNameEntry").set_text(self.installTask.newBottleName)
        self.update_bottle_selection_label()
        self.update_common_widgets()

    def bottleTemplateChanged(self):
        self.bottleListModel.foreach(self.select_bottle, None)
        self.update_bottle_selection_label()
        self.update_download_item()
        self.update_common_widgets()

    # BottleCollection delegate functions
    def bottleCollectionChanged(self):
        if self.install_in_progress or self.install_finished:
            return

        removed_bottles = set(self.installTask.bottles)

        for bottle in bottlecollection.sharedCollection().bottles():
            if not bottle.canInstall():
                continue
            if bottle.name in removed_bottles:
                removed_bottles.remove(bottle.name)
            else:
                self.installTask.add_bottle(bottle)

        for bottlename in removed_bottles:
            self.installTask.remove_bottle_by_name(bottlename)

        self.updateBottleListing()

    # BottleWrapper delegate functions
    def bottleChanged(self, bottle):
        if self.install_in_progress or self.install_finished:
            return
        if bottle.canInstall() != (bottle.name in self.installTask.bottles):
            self.bottleCollectionChanged()
        if bottle.installed_packages_ready:
            self.installTask.installed_applications_ready(bottle)

    def bottle_selection_changed_gui(self, _widget):
        bottlesWidget = self.xml.get_object("BottleSelectionView")
        tmodel, titer = bottlesWidget.get_selection().get_selected()
        if titer is None:
            # This can happen while we're rebuilding the bottle list
            return
        selectedBottleName = tmodel.get_value(titer, 0)

        if selectedBottleName == '':
            # For categories, select the first child.
            self.xml.get_object("BottleSelectionView").set_cursor(tmodel.get_path(tmodel.iter_children(titer)))
        elif selectedBottleName.startswith('.'):
            # A bottle template.
            self.installTask.SetCreateNewBottle(create=True, template=selectedBottleName[1:])
        else:
            self.installTask.bottlename = selectedBottleName

    def bottle_newname_changed_gui(self, _widget):
        self.installTask.SetCreateNewBottle(
            newbottlename=self.xml.get_object("NewBottleNameEntry").get_text().decode('utf8'))

    def new_bottle_delete_text(self, caller, start, stop):
        # pylint: disable=R0201
        name = caller.get_text()
        name = name[:start] + name[stop:]
        if not cxutils.is_valid_bottlename(name):
            caller.emit_stop_by_name("delete-text")

    def new_bottle_insert_text(self, caller, new_text, _length, _user_data):
        # pylint: disable=R0201
        name = caller.get_text()
        position = caller.get_position()
        name = name[:position] + new_text + name[position:]
        if not cxutils.is_valid_bottlename(name):
            caller.emit_stop_by_name("insert-text")

    def no_section_activated(self):
        if self.installEngine:
            # Install was started already.
            return

        self.update_common_widgets()

    def add_button_pos(self, menu):
        _menu_width, menu_height = menu.size_request()

        button = self.xml.get_object("DbgChannelButton")

        button_x, button_y = button.window.get_origin()
        button_allocation = button.get_allocation()
        button_x += button_allocation.x
        button_y += button_allocation.y
        button_height = button_allocation.height
        button_screen = button.window.get_screen()
        monitor_frame = button_screen.get_monitor_geometry(button_screen.get_monitor_at_window(button.window))

        fits_below = (button_y + button_height + menu_height <= monitor_frame.y + monitor_frame.height)
        fits_above = (button_y - menu_height >= monitor_frame.y)

        if fits_below:
            return button_x, button_y + button_height, True
        if fits_above:
            return button_x, button_y - menu_height, True
        return button_x, button_y + button_height, False

    def on_DbgChannelButton_button_press_event(self, widget, event):
        if self.add_menu_open or event.button != 1:
            return False # propagate

        menu = self.xml.get_object("DebugChannelMenu")
        menu.popup(None, None, self.add_button_pos, event.button, event.time)

        self.add_menu_open = True

        widget.set_active(True)

        return True

    def on_DbgChannelButton_clicked(self, widget):
        if self.add_menu_open or not widget.get_active():
            return

        menu = self.xml.get_object("DebugChannelMenu")
        menu.popup(None, None, self.add_button_pos, 0, 0)

        self.add_menu_open = True

    def on_DebugChannelMenu_deactivate(self, _menushell):
        self.add_menu_open = False
        button = self.xml.get_object("DbgChannelButton")
        button.set_active(False)

    def on_DbgClearMenu_activate(self, _menu):
        self.xml.get_object("DbgChannelEntry").set_text('')

    def add_debug_channels(self, menu):
        channels = self.xml.get_object("DbgChannelEntry").get_real_text().split(',')
        for channel in menu.get_tooltip_text().split(','):
            if channel not in channels:
                channels.append(channel)
        self.xml.get_object("DbgChannelEntry").set_text(','.join(x for x in channels if x))

    def on_LogFileBrowse_clicked(self, _widget):
        fileSaver = gtk.FileChooserDialog(_("Specify where to save the log file"), self.xml.get_object("InstallWizard"), gtk.FILE_CHOOSER_ACTION_SAVE)
        fileFilter = gtk.FileFilter()
        fileFilter.add_pattern("*.cxlog")
        fileFilter.set_name(_("CrossOver Log Files"))
        fileSaver.add_filter(fileFilter)

        fileSaver.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
        fileSaver.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
        fileSaver.set_do_overwrite_confirmation(True)

        logpath = os.path.join(cxproduct.get_user_dir(), 'logs')

        cxutils.mkdirs(logpath)

        fileSaver.set_current_folder(logpath)

        cxguitools.set_default_extension(fileSaver, 'cxlog')

        if fileSaver.run() == gtk.RESPONSE_OK:
            self.xml.get_object('LogFileEntry').set_text(fileSaver.get_filename())
        fileSaver.destroy()


    #-----------------------------------------------------------------------
    #  Install
    #-----------------------------------------------------------------------


    def doInstall(self):
        self.tasks = {}
        self.tasklist = []
        self.cancelInstallation = False
        self.install_in_progress = True
        if self.xml.get_object('DbgChannelEntry').get_real_text():
            log_channels = self.xml.get_object('DbgChannelEntry').get_real_text()
        else:
            log_channels = None
        if self.xml.get_object('LogFileEntry').get_real_text() or 'CX_LOG' not in os.environ:
            log_filename = os.path.join(cxproduct.get_user_dir(), 'logs', self.xml.get_object('LogFileEntry').get_text())
        else:
            log_filename = None
        self.installEngine = cxaiengine.Engine(self.installTask,
                                               log_filename=log_filename,
                                               log_channels=log_channels)

        if 'logfifo_file' in self.installEngine.state:
            log_file = self.installEngine.state['logfifo_file']
            log_file.write(systeminfo.system_info_string())
            log_file.flush()
        elif 'logfile' in self.installEngine.state:
            try:
                f = open(self.installEngine.state['logfile'], 'a')
            except IOError, ioe:
                cxlog.log("unable to open log file: " + ioe.strerror)
            else:
                f.write(systeminfo.system_info_string())
                f.close()

        self.totalTasks = len(self.installEngine.get_sorted_tasks())
        self.someTasksCanceled = False
        self.someTasksSkipped = False

        if self.registered_delegate:
            collection = bottlecollection.sharedCollection()
            collection.removeChangeDelegate(self)
            collection.removeBottleChangeDelegate(self)
            self.registered_delegate = False

        self.spoolAvailableTasks()


    def displayInstallationProgressPanel(self):
        self.xml.get_object("PackageSelectToggle").set_sensitive(False)
        self.xml.get_object("MediaSelectToggle").set_sensitive(False)
        self.xml.get_object("BottleSelectToggle").set_sensitive(False)
        self.xml.get_object("InstallerNext").hide()
        self.xml.get_object("InstallerEject").show()
        self.xml.get_object("SummaryContent").hide()
        self.xml.get_object("AdvancedContent").hide()
        self.xml.get_object("PackageInfoBar").hide()
        self.xml.get_object("InstallingContent").show()
        gobject.timeout_add(100, self.update_tasks)
        self.set_expanded_section('Installer')
        self.xml.get_object("AdvancedOptionsButton").hide()
        self.update_install_notes(False)

    def continueInstallation(self):
        if not self.install_in_progress:
            return

        if self.cancelInstallation:
            cxlog.log("Canceled Installation")

            self.install_in_progress = False

            self.displayInstallationFinishedPanel(True)
        elif self.installEngine.all_done():
            cxlog.log("Installation finished")

            self.install_in_progress = False

            self.displayInstallationFinishedPanel(False)
        else:
            self.spoolAvailableTasks()

    def spoolAvailableTasks(self):
        # Show a failure dialog if necessary
        if self.failing_tasks and not self.install_ui:
            self.failing_tasks.pop(0).show_failure_dialog()
        # Add all the tasks that are currently runnable to the operation queue.
        for task in sorted(self.installEngine.runnable(), cmp=cxaiebase.tasks_cmp):
            if not self.install_ui or not isinstance(task, interactive_tasks):
                if self.installEngine.schedule(task):
                    self.tasks[task] = InstallTask(task, self)
                    self.tasklist.append(self.tasks[task])

    def add_failing_task(self, task):
        self.failing_tasks.append(task)
        self.continueInstallation()

    def cancel_install(self):
        self.cancelInstallation = True
        self.someTasksCanceled = True
        for task in self.tasks.itervalues():
            task.cancel()
        self.continueInstallation()

    def install_toggle(self, widget):
        if widget.get_active() and not self.xml.get_object('InstallerContent').get_property('visible'):
            self.set_expanded_section('Installer')
        elif not widget.get_active() and self.xml.get_object('InstallerContent').get_property('visible'):
            widget.set_active(True)

    def eject_clicked(self, _widget):
        bottlemanagement.wine_eject(self.installTask.bottlename)


    def displayInstallationFinishedPanel(self, inCanceled):
        self.install_finished = True
        completionMessage = ""
        if self.someTasksCanceled:
            completionMessage = _("The installation failed.")
        elif self.installTask.installWithSteam:
            completionMessage = _("CrossOver has been successfully configured for '%s'. You will need to finish the installation in Steam.") % self.installTask.profile.name
        elif self.someTasksSkipped:
            completionMessage = _("The installation completed, but some tasks were skipped.")
        else:
            completionMessage = _("The installation completed.")

        if 'logfile' in self.installEngine.state:
            url = self.installEngine.state["logfile"]
        elif 'CX_LOG' in self.installEngine.state["environ"]:
            url = self.installEngine.state["environ"]["CX_LOG"]
        else:
            url = None

        if url:
            captionHTML = _("<body> <p> %(completion)s </p> <p> More diagnostic information can be found in the <a href=\"file://%(url)s\">debug log</a>. </p> </body>") % {'completion': cxutils.html_escape(completionMessage), 'url': cxutils.html_escape(url)}
        else:
            captionHTML = "<body> <p> %s </p> </body>" % cxutils.html_escape(completionMessage)

        #  Encode description as utf8 because htmltextview likes that better.
        caption = self.xml.get_object("InstallResults")
        caption.display_html_safe(captionHTML.encode('utf8'))

        self.xml.get_object("InstallerCancel").hide()
        self.xml.get_object("InstallingContent").hide()
        self.xml.get_object("FinishedContent").show()
        self.xml.get_object("InstallerCheckmark").show()

        nextButton = self.xml.get_object("InstallerNext")
        nextButton.set_sensitive(True)
        nextButton.show()
        stock_close = gtk.stock_lookup(gtk.STOCK_CLOSE)
        if stock_close:
            _stock_id, label, _modifier, _keyval, _translation_domain = stock_close
            nextButton.set_label(label)

        self.xml.get_object("InstallerEject").hide()

        bottle = bottlecollection.sharedCollection().bottleObject(self.installTask.bottlename)
        bottlecollection.sharedCollection().queue_bottle_updates((bottle,), ("dxvk"))

        try:
            # Touch the config file so the bottle manager knows that
            #  something is happening.
            if self.installTask.createNewBottle:
                os.utime(bottlequery.config_file_path(self.installTask.newBottleName), None)
            else:
                os.utime(bottlequery.config_file_path(self.installTask.existingBottleName), None)
        except bottlequery.NotFoundError:
            # The bottle wasn't created yet. Do nothing.
            pass

        if not inCanceled and not self.someTasksCanceled:
            postinstallurl = self.installTask.get_installer_profile().post_install_url
            if postinstallurl:
                cxutils.launch_url(postinstallurl)

    def finished_toggle(self, widget):
        if widget.get_active() and not self.xml.get_object('FinishedContent').get_property('visible'):
            self.set_expanded_section('Finished')
        elif not widget.get_active() and self.xml.get_object('FinishedContent').get_property('visible'):
            widget.set_active(True)

    def finished_section_activated(self):
        self.emit_event(INSTALL_FINISHED)


    #Used to update global state (such as the "Continue" button name/sensitivity)
    # when the active section changes.
    on_section_changed = {
        'PackageSelect': update_common_widgets,
        'MediaSelect': update_common_widgets,
        'BottleSelect': update_common_widgets,
        'Installer': update_common_widgets,
        'Finished': finished_section_activated,
        None: no_section_activated,
    }


    def update_tasks(self):
        if self.install_finished:
            return False

        model = self.installingTaskStore
        iterator = model.get_iter_first()
        while iterator is not None:
            iter_next = model.iter_next(iterator)

            pulse, value, task, counter = model.get(iterator, 1, 2, 3, 4)

            use_value = isinstance(task.aietask, cxaiemedia.AIEDownload) and \
                        task.aietask.size > 0

            if task.finished:
                if use_value:
                    if value != 100:
                        model.set_value(iterator, 2, 100)
                else:
                    if pulse != gobject.G_MAXINT:
                        model.set_value(iterator, 1, gobject.G_MAXINT)
            elif isinstance(task.aietask, cxaiemedia.AIEDownload):
                size = task.aietask.size
                downloaded = task.aietask.downloaded
                if use_value:
                    new_value = downloaded * 99 // size + 1 # always display incomplete downloads as 1-99%
                    if new_value != value:
                        model.set_value(iterator, 2, new_value)
                else:
                    if downloaded != counter:
                        model.set_value(iterator, 1, max(pulse, 0)+1)
                        model.set_value(iterator, 4, downloaded)
            else:
                if counter % 3 == 0:
                    model.set_value(iterator, 1, max(pulse, 0)+1)
                counter += 1
                model.set_value(iterator, 4, counter)

            iterator = iter_next

        return True

    def tasks_scroll_changed(self, adjustment):
        if self.tasks_scroll_at_bottom:
            adjustment.set_value(adjustment.upper - adjustment.page_size)

    def tasks_scroll_moved(self, adjustment):
        self.tasks_scroll_at_bottom = (adjustment.value + adjustment.page_size >= adjustment.upper)

    def create_task_menu(self):
        model, iterator = self.xml.get_object('InstallerTasksView').get_selection().get_selected()

        if not iterator:
            return None

        task = model.get_value(iterator, 3)

        if not task or task.finished:
            return None

        result = gtk.Menu()

        cancel_item = gtk.ImageMenuItem(gtk.STOCK_CANCEL)
        cancel_item.show()
        cancel_item.connect('activate', task.cancel)
        result.append(cancel_item)

        return result

    def on_InstallerTasksView_popup_menu(self, _widget):
        menu = self.create_task_menu()
        if menu:
            # FIXME: position menu based on the selection location?
            menu.popup(None, None, None, 0, 0)
            return True # popped up
        return False

    def on_InstallerTasksView_button_press_event(self, treeview, event):
        if event.button == 3: # right click
            path = treeview.get_path_at_pos(int(event.x), int(event.y))
            if path is None:
                return False # propagate
            path, _column, _x, _y = path
            treeview.get_selection().select_path(path)
            menu = self.create_task_menu()
            if menu:
                menu.popup(None, None, None, event.button, event.time)
                return True
        return False # propagate


#
# Global dialog management
#

DIALOG = None

def _dialog_closed(_dialog, _event, _data):
    # pylint: disable=W0603
    global DIALOG
    DIALOG = None

def open_or_show(**kwargs):
    # pylint: disable=W0603
    global DIALOG
    if DIALOG is None:
        DIALOG = InstallWizardController(**kwargs)
        DIALOG.add_observer(DIALOG_CLOSED, _dialog_closed)
    else:
        DIALOG.present()
    return DIALOG


#
# InstallTask
#

def _install_task_sort(treemodel, iter1, iter2):
    task1 = treemodel.get_value(iter1, 3)
    task2 = treemodel.get_value(iter2, 3)
    if task1 is None or task2 is None:
        # wtf
        return 0
    # Show pending downloads first
    task1_pending_download = not task1.finished and \
                             isinstance(task1.aietask, cxaiemedia.AIEDownload) and \
                             task1.aietask.size == -1
    task2_pending_download = not task1.finished and \
                             isinstance(task2.aietask, cxaiemedia.AIEDownload) and \
                             task2.aietask.size == -1
    if task1_pending_download and not task2_pending_download:
        return -1
    if not task1_pending_download and task2_pending_download:
        return 1
    # Show finished tasks before in-progress tasks
    if task1.finished != task2.finished:
        if task1.finished and not task2.finished:
            return -1
        elif not task1.finished and task2.finished:
            return 1
    # For finished tasks, order by when they finished
    if task1.finished:
        return cmp(task1.finished_time, task2.finished_time)
    # Show in-progress downloads before other kinds of tasks
    if isinstance(task1.aietask, cxaiemedia.AIEDownload) and not isinstance(task2.aietask, cxaiemedia.AIEDownload):
        return -1
    if not isinstance(task1.aietask, cxaiemedia.AIEDownload) and isinstance(task2.aietask, cxaiemedia.AIEDownload):
        return 1
    # Otherwise, sort by started time, then enqueued time
    return cmp((task1.started_time, task1.enqueued_time), (task2.started_time, task2.enqueued_time))

interactive_tasks = (cxaiecore.AIECore, )

class InstallTask(object):
    finished = False
    enqueued_time = 0.0
    started_time = 0.0
    finished_time = 0.0

    def __init__(self, aietask, wizard):
        self.aietask = aietask
        self.wizard = wizard

        if aietask.label:
            store = self.wizard.installingTaskStore

            store.append(row=(aietask.label, -1, 0, self, 0))

        self.enqueue()

    def get_progress(self):
        if self.finished:
            return 1.0
        if isinstance(self.aietask, cxaiemedia.AIEDownload):
            return self.aietask.progress
        return 0.0

    progress = property(get_progress)

    def enqueue(self):
        self.enqueued_time = time.time()
        if isinstance(self.aietask, interactive_tasks):
            self.wizard.install_ui = True
        self.operation = InstallTaskOperation(self)
        if isinstance(self.aietask, cxaiemedia.AIEDownload):
            download_queue.enqueue(self.operation)
        else:
            pyop.sharedOperationQueue.enqueue(self.operation)

    def done(self):
        # This task has either succeeded or failed and will not retry. Update
        # widgets and notify the ai engine.
        if isinstance(self.aietask, interactive_tasks):
            self.wizard.install_ui = False

        self.finished = True
        self.finished_time = time.time()
        self.aietask.done()
        self.wizard.continueInstallation()

    def task_returned(self, success):
        if self.finished:
            return
        if isinstance(self.aietask, interactive_tasks):
            self.wizard.install_ui = False
        self.operation = None
        if success:
            self.done()
        else:
            self.wizard.add_failing_task(self)

    def show_failure_dialog(self):
        if isinstance(self.aietask, cxaiemedia.AIEDownload):
            primary = _("Could not download the %(appname)s installer") % {'appname': cxutils.html_escape(self.aietask.aiecore.name)}
        elif isinstance(self.aietask, cxaiecore.AIECore):
            primary = _("An error occurred while installing %(appname)s") % {'appname': cxutils.html_escape(self.aietask.aiecore.name)}
        else:
            label = self.aietask.label
            if not label:
                label = unicode(self.aietask)
            primary = _("An error occurred while running the '%(task)s' task") % {'task': cxutils.html_escape(label)}

        if self.aietask.error:
            secondary = cxutils.html_escape(self.aietask.error)
        else:
            secondary = _("An unexpected error occurred.")
        if hasattr(self.aietask, "needs_download") and self.aietask.needs_download and hasattr(self.aietask, "url"):
            secondary += _("\n\nIf you have a working Internet connection via your browser, you can download this file here:\n\n<a href=\"%(url)s\">%(url)s</a>") % {'url': cxutils.html_escape(self.aietask.url)}

        message = '<span weight="bold" size="larger">%s</span>\n\n%s' % (primary, secondary)

        buttons = [[_("Try Again"), 1]]
        if isinstance(self.aietask, cxaiemedia.AIEDownload):
            buttons.append([_("Pick Installer File"), 3])
        buttons.extend(([_("Skip This Step"), 0],
                        [_("Cancel Installation"), 2]))
        cxguitools.CXMessageDlg(markup=message,
                                button_array=buttons,
                                response_function=self.abortRetryFail,
                                parent=self.wizard.xml.get_object("InstallWizard"),
                                message_type=gtk.MESSAGE_WARNING)

        self.wizard.install_ui = True

    def skip(self):
        self.wizard.someTasksSkipped = True
        self.done()

    def retry(self):
        self.enqueue()

    def cancel(self, _menuitem=None):
        if self.finished:
            return
        if self.aietask.can_cancel():
            self.aietask.cancel()
        self.done()

    def abortRetryFail(self, response):
        self.wizard.install_ui = False
        if response == 0:
            self.skip()
        elif response == 1:
            self.retry()
        elif response == 2:
            self.wizard.cancel_install()
        elif response == 3:
            panel = self.wizard.xml.get_object("InstallationProgress")
            filePicker = gtk.FileChooserDialog(_("Please locate the installer file for %s") % self.aietask.aiecore.name, panel, gtk.FILE_CHOOSER_ACTION_OPEN)

            cxguitools.add_filters(filePicker, cxguitools.FILTERS_INSTALLABLE)
            filePicker.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
            filePicker.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)

            if filePicker.run() == gtk.RESPONSE_OK:
                self.aietask.aiecore.state['install_source'] = filePicker.get_filename()
                self.retry()
            else:
                # Go back to the previous error dialog
                self.show_failure_dialog()
            filePicker.destroy()
            return

        # Just in case we need to show another failure dialog
        self.wizard.continueInstallation()


    def cancel_clicked(self, _widget):
        self.cancel()


#
# Operations
#

class UpdateProfilesOperation(pyop.PythonOperation):

    _refresh_lock = thread.allocate_lock()

    def __init__(self, url, wizard):
        pyop.PythonOperation.__init__(self)
        self.url = url
        self.wizard = wizard
        self.refresh = False
        self.network = False
        self.profile_tree = None
        self.is_queued = False

    def __unicode__(self):
        return "%s - %s" % (self.__class__.__name__, unicode(self.url))

    def enqueued(self):
        pyop.PythonOperation.enqueued(self)
        self.is_queued = True

    def main(self):
        if self.network and self.url:
            self.refresh = c4profilesmanager.update_online_profiles(self.url)
        else:
            self.refresh = True
        if self.refresh:
            self._refresh_lock.acquire()
            self.profiles = c4profilesmanager.C4ProfilesSet.all_profiles()

            self.profile_tree = {}
            categories = {}

            for appid, profile in self.profiles.iteritems():
                if not profile.is_for_current_product:
                    continue

                if not self.wizard.show_untested_apps and not profile.is_ranked and \
                        appid != c4profilesmanager.UNKNOWN:
                    continue

                category = self.profile_tree
                category_path = profile.app_profile.category

                if appid == c4profilesmanager.UNKNOWN:
                    pass # Put this in the top level
                elif category_path in categories:
                    category = categories[category_path]
                else:
                    category = self.profile_tree
                    for category_name in category_path.split(u'/'):
                        if category_name not in category:
                            category[category_name] = {}
                        category = category[category_name]
                    categories[category_path] = category

                category[appid] = profile

    def finish(self):
        self.is_queued = False
        if self.refresh:
            self._refresh_lock.release()
        if self.refresh and not (self.wizard.install_in_progress or self.wizard.install_finished):
            self.wizard.installTask.profiles = self.profiles
            self.wizard.profile_tree = self.profile_tree
            if self.wizard.xml.get_object('InstallWizard').get_property('visible'):
                self.wizard.updatePackageListing()
            self.wizard.profileChanged()
            self.wizard.package_changed_gui(None)
            if self.wizard.autorun_file:
                self.wizard.installTask.use_autorun_file(self.wizard.autorun_file)
                self.wizard.autorun_file = None
            if self.wizard.profile_id_to_set and self.wizard.profile_id_to_set in self.profiles:
                self.wizard.installTask.profile = self.profiles[self.wizard.profile_id_to_set]
                self.wizard.profile_id_to_set = None
            self.wizard.installTask.AutoFillSettings()
            if not self.network and self.url and self.wizard.auto_update:
                self.network = True
                pyop.sharedOperationQueue.enqueue(self)
        if self.network and not self.is_queued:
            self.wizard.xml.get_object('PackageInfoBar').hide()

class InstallTaskOperation(pyop.PythonOperation):

    def __init__(self, task):
        pyop.PythonOperation.__init__(self)
        self.task = task
        self.success = False

    def __unicode__(self):
        return "%s - %s" % (self.__class__.__name__, unicode(self.task))

    def main(self):
        if not self.task.wizard.cancelInstallation and not self.task.finished:
            self.task.started_time = time.time()
            self.success = self.task.aietask.main()
        else:
            self.success = False

    def finish(self):
        self.task.task_returned(self.success)