# -*- coding: utf-8; Mode: Python; indent-tabs-mode: nil; tab-width: 4 -*-
#
# «gtk_ui» - GTK user interface
#
# Copyright (C) 2005 Junta de Andalucía
# Copyright (C) 2005, 2006, 2007, 2008, 2009 Canonical Ltd.
#
# Authors:
#
# - Javier Carranza <javier.carranza#interactors._coop>
# - Juan Jesús Ojeda Croissier <juanje#interactors._coop>
# - Antonio Olmo Titos <aolmo#emergya._info>
# - Gumer Coronel Pérez <gcoronel#emergya._info>
# - Colin Watson <cjwatson@ubuntu.com>
# - Evan Dandrea <ev@ubuntu.com>
# - Mario Limonciello <superm1@ubuntu.com>
#
# This file is part of Ubiquity.
#
# Ubiquity is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 2 of the License, or at your option)
# any later version.
#
# Ubiquity is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with Ubiquity; if not, write to the Free Software Foundation, Inc., 51
# Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

import atexit
import configparser
from functools import reduce
import gettext
import gi
import os
import subprocess
import sys
import syslog
import traceback

import dbus
assert dbus  # silence, pyflakes!
from dbus.mainloop.glib import DBusGMainLoop
DBusGMainLoop(set_as_default=True)

# in query mode we won't be in X, but import needs to pass
if 'DISPLAY' in os.environ:
    gi.require_version('Atk', '1.0')
    gi.require_version('Gdk', '3.0')
    gi.require_version('Gtk', '3.0')
    gi.require_version('Gio', '2.0')
    gi.require_version('GLib', '2.0')
    gi.require_version('GObject', '2.0')
    from gi.repository import Gtk, Gdk, GObject, GLib, Atk, Gio
    from ubiquity import gtkwidgets

from ubiquity import (
    filteredcommand, gsettings, i18n, validation, misc, osextras, telemetry)
from ubiquity.components import install, plugininstall, partman_commit
import ubiquity.frontend.base
from ubiquity.frontend.base import BaseFrontend
from ubiquity.plugin import Plugin
import ubiquity.progressposition

# We create class attributes dynamically from UI files, and it's far too
# tedious to list them all.
__pychecker__ = 'no-classattr'

# Define global path
PATH = os.environ.get('UBIQUITY_PATH', False) or '/usr/share/ubiquity'

# Define ui path
UIDIR = os.environ.get('UBIQUITY_GLADE', False) or os.path.join(PATH, 'gtk')
os.environ['UBIQUITY_GLADE'] = UIDIR


# Define locale path
LOCALEDIR = "/usr/share/locale"


def set_root_cursor(cursor=None):
    if cursor is None:
        cursor = Gdk.Cursor.new(Gdk.CursorType.ARROW)
    win = Gdk.get_default_root_window()
    if win:
        win.set_cursor(cursor)
    gtkwidgets.refresh()


class Controller(ubiquity.frontend.base.Controller):
    def add_builder(self, builder):
        self._wizard.builders.append(builder)

    def translate(self, lang=None, just_me=True, not_me=False, reget=False):
        if lang:
            self._wizard.locale = lang
        self._wizard.translate_pages(lang, just_me, not_me, reget)

    def allow_go_forward(self, allowed):
        try:
            self._wizard.allow_go_forward(allowed)
        except AttributeError:
            pass

    def allow_go_backward(self, allowed):
        try:
            self._wizard.allow_go_backward(allowed)
        except AttributeError:
            pass

    def allow_change_step(self, allowed):
        try:
            self._wizard.allow_change_step(allowed)
        except AttributeError:
            pass

    def allowed_change_step(self):
        return self._wizard.allowed_change_step

    def go_forward(self):
        self._wizard.next.activate()

    def go_backward(self):
        self._wizard.back.activate()

    def go_to_page(self, widget):
        self._wizard.set_current_page(self._wizard.steps.page_num(widget))

    def toggle_top_level(self):
        if self._wizard.live_installer.get_property('visible'):
            self._wizard.live_installer.hide()
        else:
            self._wizard.live_installer.show()
        self._wizard.refresh()

    def toggle_progress_section(self):
        if self._wizard.progress_section.get_property('visible'):
            self._wizard.progress_section.hide()
        else:
            self._wizard.progress_section.show()
        self._wizard.refresh()

    def get_string(self, name, lang=None, prefix=None):
        return self._wizard.get_string(name, lang, prefix)

    def toggle_navigation_control(self, hideFlag):
        if hideFlag:
            self._wizard.navigation_control.show()
        else:
            self._wizard.navigation_control.hide()
        self._wizard.refresh()

    def toggle_next_button(self, label='gtk-go-forward', suggested=False):
        self._wizard.toggle_next_button(label, suggested=suggested)

    def toggle_skip_button(self, label='skip'):
        self._wizard.toggle_skip_button(label)

    def switch_to_install_interface(self):
        self._wizard.switch_to_install_interface()


def on_screen_reader_enabled_changed(gsettings, key):
    # handle starting orca only, it exits itself when the key is false
    if key == "screen-reader-enabled":
        # Besides starting orca, also make sure the screen-reader-enabled
        # setting gets passed to the target system.
        if (gsettings.get_boolean(key) and osextras.find_on_path('orca')):
            # Enable
            subprocess.Popen(['orca'], preexec_fn=misc.drop_all_privileges)
            os.environ['UBIQUITY_A11Y_PROFILE'] = 'screen-reader'
        elif 'UBIQUITY_A11Y_PROFILE' in os.environ:
            # Disable
            del os.environ['UBIQUITY_A11Y_PROFILE']


class Wizard(BaseFrontend):
    def __init__(self, distro):
        def add_subpage(self, steps, name):
            """Inserts a subpage into the notebook.  This assumes the file
            shares the same base name as the page you are looking for."""
            widget = None
            uifile = UIDIR + '/' + name + '.ui'
            if os.path.exists(uifile):
                self.builder.add_from_file(uifile)
                widget = self.builder.get_object(name)
                steps.append_page(widget, None)
            else:
                print('Could not find ui file %s' % name, file=sys.stderr)
            return widget

        def add_widget(self, widget):
            """Make a widget callable by the toplevel."""
            if not isinstance(widget, Gtk.Widget):
                return
            name = Gtk.Buildable.get_name(widget)
            widget.set_name(name)
            if 'UBIQUITY_LDTP' in os.environ:
                atk_desc = widget.get_accessible()
                atk_desc.set_name(name)
            self.all_widgets.add(widget)
            setattr(self, widget.get_name(), widget)
            # We generally want labels to be selectable so that people can
            # easily report problems in them
            # (https://launchpad.net/bugs/41618), but GTK+ likes to put
            # selectable labels in the focus chain, and I can't seem to turn
            # this off in glade and have it stick. Accordingly, make sure
            # labels are unfocusable here.
            label = None
            if isinstance(widget, Gtk.Label):
                label = widget
            elif isinstance(widget, gtkwidgets.StateBox):
                label = widget.label

            if label:
                label.set_selectable(True)
                label.set_property('can-focus', False)

        BaseFrontend.__init__(self, distro)
        self.previous_excepthook = sys.excepthook
        sys.excepthook = self.excepthook

        # declare attributes
        self.all_widgets = set()
        self.gsettings_previous = {}
        self.thunar_previous = {}
        self.language_questions = ('live_installer', 'quit', 'back', 'next',
                                   'warning_dialog', 'warning_dialog_label',
                                   'cancelbutton', 'exitbutton',
                                   'install_button',
                                   'restart_button',
                                   'restart_to_continue')
        self.current_page = None
        self.backup = None
        self.allowed_change_step = True
        self.allowed_go_backward = True
        self.allowed_go_forward = True
        self.stay_on_page = False
        self.progress_position = ubiquity.progressposition.ProgressPosition()
        self.progress_cancelled = False
        self.installing = False
        self.installing_no_return = False
        self.partitioned = False
        self.timezone_set = False
        self.ubuntu_drivers = None
        self.returncode = 0
        self.history = []
        self.builder = Gtk.Builder()
        self.grub_options = Gtk.ListStore(
            GObject.TYPE_STRING, GObject.TYPE_STRING)
        self.finished_installing = False
        self.finished_pages = False
        self.parallel_db = None
        self.timeout_id = None
        self.screen_reader = False
        self.orca_process = None
        self.a11y_settings = None
        self.have_apt_updated = False

        # To get a "busy mouse":
        self.watch = Gdk.Cursor.new(Gdk.CursorType.WATCH)
        self.set_busy_cursor(True)
        atexit.register(set_root_cursor)

        # Are we running alongside Orca?
        with open('/proc/cmdline') as fp:
            if 'access=v3' in fp.read():
                self.screen_reader = True

        if 'UBIQUITY_ONLY' in os.environ:
            # do not run this as root. The API pretends to be synchronous but
            # it is actually asynchronous. If you become root before it
            # finishes then D-Bus will reject our connection due to a
            # mismatched user between the requestor and the owner of the
            # session bus.

            # handle orca only in ubiquity-dm where there is no gnome-session
            self.a11y_settings = Gio.Settings.new(
                "org.gnome.desktop.a11y.applications")
            self.a11y_settings.connect("changed::screen-reader-enabled",
                                       on_screen_reader_enabled_changed)
            # enable if needed and a key read is needed to connect the signal
            on_screen_reader_enabled_changed(self.a11y_settings,
                                             "screen-reader-enabled")

        # set default language
        self.locale = i18n.reset_locale(self)

        # set custom language
        self.set_locales()

        # Get the default window background color for the the current
        # theme and set it as the background for the inline toolbar
        # Make a thin Progress bar
        provider = Gtk.CssProvider()
        provider.load_from_data(b'''\
            toolbar {
                background: @theme_bg_color;
                border-color: transparent;
                border-width: 0px;
                padding: 0px;
            }
            progressbar trough {
              min-height: 10px;
              min-width: 11px;
              border: none;
            }
            progressbar progress {
              min-height: 10px;
              border-radius: 4px;
              border: none;
            }
            paned separator {
                min-width: 10px;
            }
            ''')
        Gtk.StyleContext.add_provider_for_screen(
            Gdk.Screen.get_default(),
            provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)

        # load the main interface
        self.builder.add_from_file('%s/ubiquity.ui' % UIDIR)

        self.builders = [self.builder]
        self.pages = []
        self.pagesindex = 0
        self.pageslen = 0
        steps = self.builder.get_object("steps")
        found_install = False
        for mod in self.modules:
            if hasattr(mod.module, 'PageGtk'):
                mod.ui_class = mod.module.PageGtk
                mod.controller = Controller(self)
                mod.ui = mod.ui_class(mod.controller)
                mod.title = mod.ui.get('plugin_title')
                widgets = mod.ui.get('plugin_widgets')
                optional_widgets = mod.ui.get('plugin_optional_widgets')
                if not found_install:
                    found_install = mod.ui.get('plugin_is_install')
                if widgets or optional_widgets:
                    def fill_out(widget_list):
                        rv = []
                        if not isinstance(widget_list, list):
                            widget_list = [widget_list]
                        for w in widget_list:
                            if not w:
                                continue
                            if isinstance(w, str):
                                w = add_subpage(self, steps, w)
                            else:
                                steps.append_page(w, None)
                            rv.append(w)
                        return rv
                    mod.widgets = fill_out(widgets)
                    mod.optional_widgets = fill_out(optional_widgets)
                    mod.all_widgets = mod.widgets + mod.optional_widgets
                    self.pageslen += 1
                    self.pages.append(mod)

        # If no plugins declare they are install, then we'll say the last one
        # is
        if not found_install:
            self.pages[self.pageslen - 1].ui.plugin_is_install = True

        self.toplevels = set()
        for builder in self.builders:
            for widget in builder.get_objects():
                add_widget(self, widget)
                if isinstance(widget, Gtk.Window):
                    self.toplevels.add(widget)
        self.builder.connect_signals(self)

        for mod in self.pages:
            progress = Gtk.ProgressBar()
            progress.get_accessible().set_role(Atk.Role.INVALID)
            progress.set_size_request(10, 10)
            progress.set_fraction(0)
            self.dot_grid.add(progress)

        next_style = self.next.get_style_context()
        next_style.add_class('ubiquity-next')

        self.progress_pages = {
            'empty': 0,
            'dot_grid': 1,
            'progress_bar': 2,
        }

        self.stop_debconf()
        self.translate_widgets(reget=True)

        self.customize_installer()

        # Put up the a11y indicator.
        if osextras.find_on_path('a11y-profile-manager-indicator'):
            try:
                subprocess.Popen(['a11y-profile-manager-indicator',
                                  '-i'], preexec_fn=misc.drop_all_privileges)
            except Exception:
                print("Unable to set up accessibility profile support",
                      file=sys.stderr)
            self.live_installer.connect(
                'key-press-event', self.a11y_profile_keys)

        # Wait for sound.target to become ready
        self.soundWatcher = misc.SystemdUnitWatcher('sound.target',
                                                    self.play_system_ready)

        self.save_oem_metapackages_list()

    def play_system_ready(self):
        # play the system ready sound
        if osextras.find_on_path('canberra-gtk-play'):
            subprocess.Popen(
                ['canberra-gtk-play', '--id=system-ready'],
                preexec_fn=misc.drop_all_privileges)

    def save_oem_metapackages_list(self, wait_finished=False):
        ''' If we can, update the apt indexes. Then run 'ubuntu-drivers
        list-oem' to find any OEM metapackages for this system. If we've
        already done this with updated apt indexes before, there's no point
        running again, so just return. '''

        # We already did it
        if self.have_apt_updated:
            syslog.syslog(syslog.LOG_INFO, "We've already apt updated and "
                          "run, not doing so again.")
            return

        with misc.raised_privileges():
            try:
                import apt
                syslog.syslog(syslog.LOG_INFO, "Trying to update apt indexes "
                              "to run ubuntu-drivers against fresh data.")
                cache = apt.cache.Cache()
                cache.update()
                cache.open()
                self.have_apt_updated = True
            except apt.cache.FetchFailedException:
                syslog.syslog(syslog.LOG_INFO,
                              "Failed to update apt indexes; offline? "
                              "Continuing without.")
            except apt.cache.LockFailedException:
                syslog.syslog(syslog.LOG_WARNING,
                              "Failed to update apt indexes, permission "
                              "denied: running under test?")

            if osextras.find_on_path('ubuntu-drivers'):
                # We already ran, were offline then, and are still offline, no
                # point running again.
                if os.path.exists('/run/ubuntu-drivers-oem.autoinstall') and \
                        not self.have_apt_updated:
                    return

                # It's either the first run or a subsequent one but we now
                # managed to update the apt indexes.
                self.ubuntu_drivers = subprocess.Popen(
                    ['ubuntu-drivers',
                     'list-oem',
                     '--package-list',
                     '/run/ubuntu-drivers-oem.autoinstall'],
                    stdin=subprocess.DEVNULL,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.DEVNULL)

                if wait_finished:
                    self.ubuntu_drivers.communicate()
                    self.ubuntu_drivers = None

    def all_children(self, parent):
        if isinstance(parent, Gtk.Container):
            def recurse(x, y):
                return x + self.all_children(y)
            rv = reduce(recurse, parent.get_children(), [parent])
            return rv
        else:
            return [parent]

    def translate_pages(self, lang=None, just_current=True, not_current=False,
                        reget=False):
        current_page = self.pages[self.pagesindex]
        if just_current:
            pages = [current_page]
        else:
            pages = self.pages

        if reget:
            self.translate_reget(lang)

        widgets = []
        for p in pages:
            # There's no sense retranslating the page we're leaving.
            if not_current and p == current_page:
                continue
            prefix = p.ui.get('plugin_prefix')
            for w in p.all_widgets:
                for c in self.all_children(w):
                    widgets.append((c, prefix))
        if not just_current:
            for toplevel in self.toplevels:
                if toplevel.get_name() != 'live_installer':
                    for c in self.all_children(toplevel):
                        widgets.append((c, None))
        self.translate_widgets(lang=lang, widgets=widgets, reget=False)
        self.set_page_title(current_page, lang)

        # Allow plugins to provide a hook for translation.
        for p in pages:
            # There's no sense retranslating the page we're leaving.
            if not_current and p == current_page:
                continue
            if hasattr(p.ui, 'plugin_translate'):
                try:
                    p.ui.plugin_translate(lang or self.locale)
                except Exception as e:
                    print('Could not translate page (%s): %s' %
                          (p.module.NAME, str(e)), file=sys.stderr)

    def excepthook(self, exctype, excvalue, exctb):
        """Crash handler."""

        if (issubclass(exctype, KeyboardInterrupt) or
                issubclass(exctype, SystemExit)):
            return

        # Restore the default cursor if we were using a spinning cursor on the
        # root window.
        try:
            self.set_busy_cursor(False)
        except Exception:
            pass

        tbtext = ''.join(traceback.format_exception(exctype, excvalue, exctb))
        syslog.syslog(syslog.LOG_ERR,
                      "Exception in GTK frontend (invoking crash handler):")
        for line in tbtext.split('\n'):
            syslog.syslog(syslog.LOG_ERR, line)
        print("Exception in GTK frontend (invoking crash handler):",
              file=sys.stderr)
        print(tbtext, file=sys.stderr)

        self.post_mortem(exctype, excvalue, exctb)

        if os.path.exists('/usr/share/apport/apport-gtk'):
            self.previous_excepthook(exctype, excvalue, exctb)
            # In live session mode, update-notifier will pick up the crash
            # report; in only-ubiquity mode we need to bring up the UI
            # ourselves
            # update-notifier doesn't work on overlayfs so also run if using
            # maybe-ubiquity

            # FIXME: Revert the check to maybe-ubiquity once inotify on
            # overlayfs is fixed (crash will then be detected by
            # update-notifier)
            with open('/proc/cmdline') as fp:
                if 'ubiquity' in fp.read():
                    # we need to drop privileges, we cannot run GTK programs
                    # with non-matching real/effective u/gid
                    misc.drop_all_privileges()
                    misc.execute('/usr/share/apport/apport-gtk')
            sys.exit(1)
        else:
            self.crash_detail_label.set_text(tbtext)
            self.crash_dialog.run()
            self.crash_dialog.hide()
            self.live_installer.hide()
            self.refresh()
            misc.execute_root("apport-bug", "ubiquity")
            sys.exit(1)

    def network_change(self, online=False):
        if not online:
            self.set_online_state(False)
            return
        if self.timeout_id:
            GLib.source_remove(self.timeout_id)
        self.timeout_id = GLib.timeout_add(300, self.check_returncode)

    def set_connectivity_state(self, state):
        for p in self.pages:
            if hasattr(p.ui, 'plugin_set_connectivity_state'):
                p.ui.plugin_set_connectivity_state(state)

    def set_online_state(self, state):
        for p in self.pages:
            if hasattr(p.ui, 'plugin_set_online_state'):
                p.ui.plugin_set_online_state(state)

    def thunar_set_volmanrc(self, fields):
        previous = {}
        if 'SUDO_USER' in os.environ:
            thunar_dir = os.path.expanduser('~%s/.config/Thunar' %
                                            os.environ['SUDO_USER'])
        else:
            thunar_dir = os.path.expanduser('~/.config/Thunar')
        if os.path.isdir(thunar_dir):
            thunar_volmanrc = '%s/volmanrc' % thunar_dir
            parser = configparser.RawConfigParser()
            parser.optionxform = str  # case-sensitive
            parser.read(thunar_volmanrc)
            if not parser.has_section('Configuration'):
                parser.add_section('Configuration')
            for key, value in fields.items():
                if parser.has_option('Configuration', key):
                    previous[key] = parser.get('Configuration', key)
                else:
                    previous[key] = 'TRUE'
                parser.set('Configuration', key, value)
            try:
                with open('%s.new' % thunar_volmanrc,
                          'w') as thunar_volmanrc_new:
                    parser.write(thunar_volmanrc_new)
                os.rename('%s.new' % thunar_volmanrc, thunar_volmanrc)
            except (KeyboardInterrupt, SystemExit):
                raise
            except Exception:
                pass
        return previous

    def disable_terminal(self):
        gs_schema = 'org.gnome.settings-daemon.plugins.media-keys'
        gs_key = 'terminal'
        gs_previous = '%s/%s' % (gs_schema, gs_key)
        if gs_previous in self.gsettings_previous:
            return

        gs_value = gsettings.get(gs_schema, gs_key)
        self.gsettings_previous[gs_previous] = gs_value

        if gs_value:
            gsettings.set(gs_schema, gs_key, '')

        atexit.register(self.enable_terminal)

    def enable_terminal(self):
        gs_schema = 'org.gnome.settings-daemon.plugins.media-keys'
        gs_key = 'terminal'
        gs_previous = '%s/%s' % (gs_schema, gs_key)
        gs_value = self.gsettings_previous[gs_previous]

        gsettings.set(gs_schema, gs_key, gs_value)

    def disable_screensaver(self):
        gs_schema = 'org.gnome.desktop.screensaver'
        gs_key = 'idle-activation-enabled'
        gs_previous = '%s/%s' % (gs_schema, gs_key)
        if gs_previous in self.gsettings_previous:
            return

        gs_value = gsettings.get(gs_schema, gs_key)
        self.gsettings_previous[gs_previous] = gs_value

        if gs_value:
            gsettings.set(gs_schema, gs_key, False)

        atexit.register(self.enable_screensaver)

    def enable_screensaver(self):
        gs_schema = 'org.gnome.desktop.screensaver'
        gs_key = 'idle-activation-enabled'
        gs_previous = '%s/%s' % (gs_schema, gs_key)
        gs_value = self.gsettings_previous[gs_previous]

        gsettings.set(gs_schema, gs_key, gs_value)

    def disable_screen_blanking(self):
        gs_schema = 'org.gnome.desktop.session'
        gs_key = 'idle-delay'
        gs_previous = '%s/%s' % (gs_schema, gs_key)
        if gs_previous in self.gsettings_previous:
            return

        gs_value = gsettings.get(gs_schema, gs_key)
        self.gsettings_previous[gs_previous] = gs_value

        if gs_value:
            gsettings.set(gs_schema, gs_key, 0)

        atexit.register(self.enable_screen_blanking)

    def enable_screen_blanking(self):
        gs_schema = 'org.gnome.desktop.session'
        gs_key = 'idle-delay'
        gs_previous = '%s/%s' % (gs_schema, gs_key)
        gs_value = self.gsettings_previous[gs_previous]

        gsettings.set(gs_schema, gs_key, gs_value)

    def disable_powermgr(self):
        gs_schema = 'org.gnome.settings-daemon.plugins.power'
        gs_key = 'active'
        gs_previous = '%s/%s' % (gs_schema, gs_key)
        if gs_previous in self.gsettings_previous:
            return

        gs_value = gsettings.get(gs_schema, gs_key)
        self.gsettings_previous[gs_previous] = gs_value

        if gs_value:
            gsettings.set(gs_schema, gs_key, False)

        atexit.register(self.enable_powermgr)

    def enable_powermgr(self):
        gs_schema = 'org.gnome.settings-daemon.plugins.power'
        gs_key = 'active'
        gs_previous = '%s/%s' % (gs_schema, gs_key)
        gs_value = self.gsettings_previous[gs_previous]

        gsettings.set(gs_schema, gs_key, gs_value)

    def disable_logout_indicator(self):
        gs_schema = 'com.canonical.indicator.session'
        gs_key = 'suppress-logout-menuitem'
        gs_previous = '%s/%s' % (gs_schema, gs_key)
        if gs_previous in self.gsettings_previous:
            return

        gs_value = gsettings.get(gs_schema, gs_key)
        self.gsettings_previous[gs_previous] = gs_value

        if gs_value:
            gsettings.set(gs_schema, gs_key, True)

        atexit.register(self.enable_logout_indicator)

    def enable_logout_indicator(self):
        gs_schema = 'com.canonical.indicator.session'
        gs_key = 'suppress-logout-menuitem'
        gs_previous = '%s/%s' % (gs_schema, gs_key)
        gs_value = self.gsettings_previous[gs_previous]

        gsettings.set(gs_schema, gs_key, gs_value)

    # Disable gnome-volume-manager automounting to avoid problems during
    # partitioning.
    def disable_volume_manager(self):
        volumes_visible = (
            'org.gnome.nautilus.desktop', 'volumes-visible', False)
        media_automount = (
            'org.gnome.desktop.media-handling', 'automount', False)
        media_automount_open = (
            'org.gnome.desktop.media-handling', 'automount-open', False)
        media_autorun_never = (
            'org.gnome.desktop.media-handling', 'autorun-never', True)
        for keys in (volumes_visible, media_automount, media_automount_open,
                     media_autorun_never):
            gs_schema = keys[0]
            gs_key = keys[1]
            gs_wantedvalue = keys[2]
            gs_previous = '%s/%s' % (gs_schema, gs_key)
            if gs_previous in self.gsettings_previous:
                continue

            gs_value = gsettings.get(gs_schema, gs_key)
            self.gsettings_previous[gs_previous] = gs_value

            if gs_value != gs_wantedvalue:
                gsettings.set(gs_schema, gs_key, gs_wantedvalue)

        self.thunar_previous = self.thunar_set_volmanrc(
            {'AutomountDrives': 'FALSE', 'AutomountMedia': 'FALSE'})

        atexit.register(self.enable_volume_manager)

    def enable_volume_manager(self):
        volumes_visible = ('org.gnome.nautilus.desktop', 'volumes-visible')
        media_automount = ('org.gnome.desktop.media-handling', 'automount')
        media_automount_open = (
            'org.gnome.desktop.media-handling', 'automount-open')
        media_autorun_never = (
            'org.gnome.desktop.media-handling', 'autorun-never')
        for keys in (volumes_visible, media_automount, media_automount_open,
                     media_autorun_never):
            gs_schema = keys[0]
            gs_key = keys[1]
            gs_previous = '%s/%s' % (gs_schema, gs_key)
            gs_value = self.gsettings_previous[gs_previous]

            gsettings.set(gs_schema, gs_key, gs_value)

        if self.thunar_previous:
            self.thunar_set_volmanrc(self.thunar_previous)

    def a11y_profile_keys(self, window, event):
        if osextras.find_on_path('a11y-profile-manager'):
            hc_profile_found = False
            sr_profile_found = False

            subp = subprocess.Popen(['a11y-profile-manager',
                                    '-l'],
                                    stdout=subprocess.PIPE,
                                    preexec_fn=misc.drop_all_privileges,
                                    universal_newlines=True)

            for line in subp.stdout:
                value = line.rstrip('\n')
                if value.endswith('high-contrast'):
                    hc_profile_found = True
                    self.hc_profile_name = value
                if value.endswith('blindness'):
                    sr_profile_found = True
                    self.sr_profile_name = value

            if (hc_profile_found is True and event.state &
                Gdk.ModifierType.CONTROL_MASK and
                    event.keyval == Gdk.keyval_from_name('h')):
                self.a11y_profile_high_contrast_activate()
            elif (sr_profile_found is True and event.state &
                  Gdk.ModifierType.SUPER_MASK and
                    event.state & Gdk.ModifierType.MOD1_MASK and
                    event.keyval == Gdk.keyval_from_name('s')):
                self.a11y_profile_screen_reader_activate()

    def a11y_profile_set(self, value):
        gsettings.set("com.canonical.a11y-profile-manager",
                      "active-profile", value)

    def a11y_profile_high_contrast_activate(self):
        self.a11y_profile_set(self.hc_profile_name)

    def a11y_profile_screen_reader_activate(self, widget=None):
        self.a11y_profile_set(self.sr_profile_name)
        os.environ['UBIQUITY_A11Y_PROFILE'] = 'screen-reader'

    def run(self):
        """run the interface."""

        if os.getuid() != 0:
            title = ('This installer must be run with administrative '
                     'privileges, and cannot continue without them.')
            dialog = Gtk.MessageDialog(
                self.live_installer, Gtk.DialogFlags.MODAL,
                Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, title)
            dialog.run()
            sys.exit(1)

        self.disable_volume_manager()
        self.disable_screensaver()
        self.disable_screen_blanking()
        self.disable_powermgr()

        if 'UBIQUITY_ONLY' in os.environ:
            self.disable_logout_indicator()
            if 'UBIQUITY_DEBUG' not in os.environ:
                self.disable_terminal()

        # show interface
        self.allow_change_step(True)

        # Auto-connecting signals with additional parameters does not work.
        self.grub_new_device_entry.connect(
            'changed', self.grub_verify_loop, self.grub_fail_okbutton)

        if 'UBIQUITY_AUTOMATIC' in os.environ:
            self.debconf_progress_start(
                0, self.pageslen, self.get_string('ubiquity/install/checking'))
            self.debconf_progress_cancellable(False)
            self.refresh()

        telemetry.get().set_installer_type('GTK')
        telemetry.get().set_is_oem(self.oem_config)

        self.set_current_page(0)
        self.live_installer.show()

        while (self.pagesindex < len(self.pages)):
            if self.current_page is None:
                return self.returncode

            page = self.pages[self.pagesindex]
            skip = False
            if hasattr(page.ui, 'plugin_skip_page'):
                if page.ui.plugin_skip_page():
                    skip = True
            automatic = False
            if hasattr(page.ui, 'is_automatic'):
                automatic = page.ui.is_automatic

            if not skip and not page.filter_class:
                # This page is just a UI page
                self.dbfilter = None
                self.dbfilter_status = None
                if self.set_page(page.module.NAME):
                    self.run_main_loop()
            elif not skip:
                old_dbfilter = self.dbfilter
                if issubclass(page.filter_class, Plugin):
                    ui = page.ui
                else:
                    ui = None
                self.start_debconf()
                self.dbfilter = page.filter_class(self, ui=ui)

                if self.dbfilter is not None and self.dbfilter != old_dbfilter:
                    self.allow_change_step(False)
                    GLib.idle_add(
                        lambda: self.dbfilter.start(auto_process=True))

                page.controller.dbfilter = self.dbfilter
                Gtk.main()
                self.pending_quits = max(0, self.pending_quits - 1)
                page.controller.dbfilter = None

            if self.backup or self.dbfilter_handle_status():
                if self.current_page is not None and not self.backup:
                    self.process_step()
                    if not self.stay_on_page:
                        self.pagesindex = self.pagesindex + 1
                    if automatic:
                        # if no debconf_progress, create another one, set
                        # start to pageindex
                        self.debconf_progress_step(1)
                        self.refresh()
                if self.backup:
                    self.pagesindex = self.pop_history()

            self.refresh()

        # There's still work to do (postinstall).  Let's keep the user
        # entertained.
        self.start_slideshow()
        Gtk.main()
        self.pending_quits = max(0, self.pending_quits - 1)
        # postinstall will exit here by calling Gtk.main_quit in
        # find_next_step.

        telemetry.get().done(self.db)

        self.unlock_environment()
        if self.oem_user_config:
            self.quit_installer()
        elif not (self.get_reboot_seen() or self.get_shutdown_seen()):
            self.live_installer.hide()
            if ('UBIQUITY_ONLY' in os.environ or
                    'UBIQUITY_GREETER' in os.environ):
                txt = self.get_string('ubiquity/finished_restart_only')
                self.quit_button.hide()
            else:
                txt = self.finished_label.get_label()
                txt = txt.replace('${RELEASE}', misc.get_release().name)
            self.finished_label.set_label(txt)
            with misc.raised_privileges():
                with open('/var/run/reboot-required', "w"):
                    pass
            self.finished_dialog.set_keep_above(True)
            self.set_busy_cursor(False)
            self.finished_dialog.run()
        elif self.get_reboot():
            self.reboot()
        elif self.get_shutdown():
            self.shutdown()

        return self.returncode

    def on_context_menu(self, unused_web_view, unused_context_menu,
                        unused_event, unused_hit_test_result):
        # True will not show the menu
        return True

    def on_slideshow_link_clicked(self, web_view, decision, decision_type):
        gi.require_version('WebKit2', '4.1')
        from gi.repository import WebKit2
        if decision_type == WebKit2.PolicyDecisionType.NEW_WINDOW_ACTION:
            request = decision.get_request()
            uri = request.get_uri()
            decision.ignore()
            misc.launch_uri(uri)
            return True
        return False

    def start_slideshow(self):
        # WebKit2 spawns a process which we don't want to run as root
        misc.drop_privileges_save()
        self.progress_mode.set_current_page(
            self.progress_pages['progress_bar'])
        telemetry.get().add_stage('user_done')

        if not self.slideshow:
            self.page_mode.hide()
            return

        self.page_section.hide()

        slideshow_locale = self.slideshow_get_available_locale(
            self.slideshow, self.locale)
        slideshow_main = os.path.join(self.slideshow, 'slides', 'index.html')

        parameters = []
        parameters.append('locale=%s' % slideshow_locale)
        ltr = i18n.get_string(
            'default-ltr', slideshow_locale, 'ubiquity/imported')
        if ltr == 'default:RTL':
            parameters.append('rtl')
        parameters_encoded = '&'.join(parameters)

        slides = 'file://%s#%s' % (slideshow_main, parameters_encoded)

        gi.require_version('WebKit2', '4.1')
        from gi.repository import WebKit2
        # We have no significant browsing interface, so there isn't much point
        # in WebKit creating a memory-hungry cache.
        context = WebKit2.WebContext.get_default()
        context.set_cache_model(WebKit2.CacheModel.DOCUMENT_VIEWER)
        webview = WebKit2.WebView()
        # WebKit puts file URLs in their own domain by default.
        # This means that anything which checks for the same origin,
        # such as creating a XMLHttpRequest, will fail unless this
        # is disabled.
        # http://www.gitorious.org/webkit/webkit/commit/624b946
        s = webview.get_settings()
        s.set_property('allow-file-access-from-file-urls', True)
        webview.connect('context-menu', self.on_context_menu)
        if (os.environ.get('UBIQUITY_A11Y_PROFILE') == 'screen-reader'):
            s.set_property('enable-caret-browsing', True)

        webview.connect('decide-policy',
                        self.on_slideshow_link_clicked)

        webview.show()
        self.page_mode.insert_page(webview, None, 1)
        webview.load_uri(slides)
        # TODO do these in a page loaded callback
        self.page_mode.show()
        self.page_mode.set_current_page(1)
        webview.grab_focus()
        misc.regain_privileges_save()

    def customize_installer(self):
        """Initial UI setup."""

        self.live_installer.set_icon_name('ubiquity')
        for eventbox in ['title_eventbox', 'progress_eventbox',
                         'install_details_expander']:
            box = self.builder.get_object(eventbox)
            style = box.get_style_context()
            style.add_class('menubar')

        # TODO lazy load
        import gi
        gi.require_version("Vte", "2.91")
        gi.require_version("Pango", "1.0")
        from gi.repository import Vte, Pango
        misc.drop_privileges_save()
        self.vte = Vte.Terminal()
        self.install_details_sw.add(self.vte)
        tail_cmd = [
            '/bin/busybox', 'tail', '-f', '/var/log/installer/debug',
            '-f', '/var/log/syslog', '-q',
        ]
        self.vte.spawn_sync(0, None, tail_cmd, None, 0, None, None, None)
        fontdesc = Pango.font_description_from_string("Ubuntu Mono 8")
        self.vte.set_font(fontdesc)
        self.vte.show()
        misc.regain_privileges_save()
        # FIXME shrink the window horizontally instead of locking the window
        # size.
        self.live_installer.set_resizable(False)

        def expand(widget):
            if widget.get_property('expanded'):
                self.progress_cancel_button.show()
            else:
                self.progress_cancel_button.hide()

        self.install_details_expander.connect_after('activate', expand)

        if self.custom_title:
            self.live_installer.set_title(self.custom_title)
        elif self.oem_config:
            self.live_installer.set_title(self.get_string('oem_config_title'))
        elif self.oem_user_config:
            self.live_installer.set_title(
                self.get_string('oem_user_config_title'))
            self.live_installer.set_icon_name("preferences-system")
            self.quit.hide()
            self.back.hide()

        self.progress_section.show()

        if 'UBIQUITY_AUTOMATIC' in os.environ:
            # Hide the notebook until the first page is ready.
            self.page_mode.hide()
            self.progress_mode.set_current_page(
                self.progress_pages['progress_bar'])
            self.live_installer.show()
        else:
            self.progress_mode.set_current_page(
                self.progress_pages['dot_grid'])
            self.progress_mode.show_all()
        self.allow_change_step(False)

        # The default instantiation of GtkComboBoxEntry creates a
        # GtkCellRenderer, so reuse it.
        self.grub_new_device_entry.set_model(self.grub_options)
        self.grub_new_device_entry.set_entry_text_column(0)
        renderer = Gtk.CellRendererText()
        self.grub_new_device_entry.pack_start(renderer, True)
        self.grub_new_device_entry.add_attribute(renderer, 'text', 1)

        # Only show the Shutdown Now button if explicitly asked to.
        if not self.show_shutdown_button:
            self.shutdown_button.hide()

        # Parse the slideshow size early to prevent the window from growing
        if (self.oem_user_config and
                os.path.exists('/usr/share/oem-config-slideshow')):
            self.slideshow = '/usr/share/oem-config-slideshow'
        else:
            self.slideshow = '/usr/share/ubiquity-slideshow'

        if os.path.exists(self.slideshow) and not self.hide_slideshow:
            try:
                cfg = configparser.ConfigParser()
                cfg.read(os.path.join(self.slideshow, 'slideshow.conf'))
                config_width = int(cfg.get('Slideshow', 'width'))
                config_height = int(cfg.get('Slideshow', 'height'))
            except Exception:
                config_width = 752
                config_height = 442
            self.webkit_scrolled_window.set_size_request(
                config_width, config_height)
        else:
            self.slideshow = None

        # set initial bottom bar status
        self.allow_go_backward(False)

        misc.add_connection_watch(self.network_change)
        misc.add_connection_watch(self.set_connectivity_state, global_only=False)

    def set_window_hints(self, widget):
        if (self.oem_user_config or
                'UBIQUITY_ONLY' in os.environ or
                'UBIQUITY_GREETER' in os.environ):
            f = (Gdk.WMFunction.RESIZE | Gdk.WMFunction.MAXIMIZE |
                 Gdk.WMFunction.MOVE)
            if not self.oem_user_config:
                f |= Gdk.WMFunction.CLOSE
            widget.get_window().set_functions(f)

    def lockdown_environment(self):
        atexit.register(self.unlock_environment)
        keys = (
            ('com.canonical.indicator.session', 'suppress-logout-menuitem'),
            ('com.canonical.indicator.session',
             'suppress-logout-restart-shutdown'),
            ('com.canonical.indicator.session', 'suppress-restart-menuitem'),
            ('com.canonical.indicator.session', 'suppress-shutdown-menuitem'),
            ('org.gnome.desktop.lockdown', 'disable-user-switching'),
        )
        for key in keys:
            gs_schema = key[0]
            gs_key = key[1]
            gs_previous = '%s/%s' % (gs_schema, gs_key)
            if gs_previous in self.gsettings_previous:
                continue

            gs_value = gsettings.get(gs_schema, gs_key)
            self.gsettings_previous[gs_previous] = gs_value

            gsettings.set(gs_schema, gs_key, True)

        self.quit.hide()
        f = (Gdk.WMFunction.RESIZE | Gdk.WMFunction.MAXIMIZE |
             Gdk.WMFunction.MOVE)
        if 'UBIQUITY_ONLY' not in os.environ:
            f |= Gdk.WMFunction.MINIMIZE
        self.live_installer.get_window().set_functions(f)
        self.allow_change_step(False)
        self.refresh()

    def unlock_environment(self):
        syslog.syslog('Reverting lockdown of the desktop environment.')
        keys = (
            ('com.canonical.indicator.session', 'suppress-logout-menuitem'),
            ('com.canonical.indicator.session',
             'suppress-logout-restart-shutdown'),
            ('com.canonical.indicator.session', 'suppress-restart-menuitem'),
            ('com.canonical.indicator.session', 'suppress-shutdown-menuitem'),
            ('org.gnome.desktop.lockdown', 'disable-user-switching'),
        )
        for key in keys:
            gs_schema = key[0]
            gs_key = key[1]
            gs_previous = '%s/%s' % (gs_schema, gs_key)

            if gs_previous in self.gsettings_previous:
                gs_value = self.gsettings_previous[gs_previous]
                gsettings.set(gs_schema, gs_key, gs_value)

        if not self.oem_user_config:
            self.quit.show()
        f = Gdk.WMFunction.RESIZE | Gdk.WMFunction.MAXIMIZE | \
            Gdk.WMFunction.MOVE | Gdk.WMFunction.CLOSE
        if 'UBIQUITY_ONLY' not in os.environ:
            f |= Gdk.WMFunction.MINIMIZE
        self.refresh()

    def set_locales(self):
        """internationalization config. Use only once."""

        domain = self.distro + '-installer'
        gettext.bindtextdomain(domain, LOCALEDIR)
        self.builder.set_translation_domain(domain)
        gettext.textdomain(domain)
        gettext.install(domain, LOCALEDIR)

    def translate_reget(self, lang):
        if lang is None:
            lang = self.locale
        if lang is None:
            languages = []
        else:
            languages = [lang]

        core_names = ['ubiquity/text/%s' % q for q in self.language_questions]
        core_names.append('ubiquity/text/oem_config_title')
        core_names.append('ubiquity/text/oem_user_config_title')
        core_names.append('ubiquity/imported/default-ltr')
        core_names.append('ubiquity/text/release_notes_only')
        core_names.append('ubiquity/text/update_installer_only')
        core_names.append('ubiquity/text/USB')
        core_names.append('ubiquity/text/CD')
        stock_items = (
            'cancel', 'close', 'go-back', 'go-forward', 'ok', 'quit')
        for stock_item in stock_items:
            core_names.append('ubiquity/imported/%s' % stock_item)
        prefixes = []
        for p in self.pages:
            prefix = p.ui.get('plugin_prefix')
            if not prefix:
                prefix = 'ubiquity/text'
            if p.ui.get('plugin_is_language'):
                children = reduce(
                    lambda x, y: x + self.all_children(y), p.all_widgets, [])
                core_names.extend(
                    [prefix + '/' + c.get_name() for c in children])
                title = p.ui.get('plugin_title')
                if title:
                    core_names.extend([title])
            prefixes.append(prefix)
        i18n.get_translations(
            languages=languages, core_names=core_names,
            extra_prefixes=prefixes)

    # widgets is a set of (widget, prefix) pairs
    def translate_widgets(self, lang=None, widgets=None, reget=True):
        if lang is None:
            lang = self.locale
        if widgets is None:
            widgets = [(x, None) for x in self.all_widgets]

        if reget:
            self.translate_reget(lang)

        # We always translate always-visible widgets
        for q in self.language_questions:
            if hasattr(self, q):
                widgets.append((getattr(self, q), None))

        for widget in widgets:
            self.translate_widget(widget[0], lang=lang, prefix=widget[1])

    def translate_widget(self, widget, lang=None, prefix=None):
        if isinstance(widget, Gtk.Button) and widget.get_use_stock():
            widget.set_label(widget.get_label())

        text = self.get_string(widget.get_name(), lang, prefix)
        if text is None:
            return
        name = widget.get_name()

        if isinstance(widget, Gtk.Label):
            widget.set_markup(text)

        elif isinstance(widget, Gtk.Button):
            question = i18n.map_widget_name(prefix, widget.get_name())
            widget.set_label(text)

            # Workaround for radio button labels disappearing on second
            # translate when not visible. LP: #353090
            widget.realize()

            if question.startswith('ubiquity/imported/'):
                stock_id = question[18:]
                widget.set_use_stock(False)
                widget.set_image(Gtk.Image.new_from_stock(
                    'gtk-%s' % stock_id, Gtk.IconSize.BUTTON))

        elif isinstance(widget, Gtk.Window):
            if name == 'live_installer':
                if self.custom_title:
                    text = self.custom_title
                elif self.oem_config:
                    text = self.get_string('oem_config_title', lang)
                elif self.oem_user_config:
                    text = self.get_string('oem_user_config_title', lang)
            widget.set_title(text)

        elif isinstance(widget, Gtk.MenuItem):
            widget.set_label(text)

    def set_busy_cursor(self, busy):
        if busy:
            cursor = self.watch
        else:
            cursor = None
        if (hasattr(self, "live_installer") and
                self.live_installer.get_parent_window()):
            self.live_installer.get_parent_window().set_cursor(cursor)
        set_root_cursor(cursor)
        self.busy_cursor = busy

    def allow_change_step(self, allowed):
        self.set_busy_cursor(not allowed)
        self.back.set_sensitive(allowed and self.allowed_go_backward)
        self.next.set_sensitive(allowed and self.allowed_go_forward)
        self.allowed_change_step = allowed

    def allow_go_backward(self, allowed):
        self.back.set_sensitive(allowed and self.allowed_change_step)
        self.allowed_go_backward = allowed

    def allow_go_forward(self, allowed):
        self.next.set_sensitive(allowed and self.allowed_change_step)
        self.allowed_go_forward = allowed

    def dbfilter_handle_status(self):
        """If a dbfilter crashed, ask the user if they want to continue anyway.

        Returns True to continue, or False to try again."""

        if not self.dbfilter_status or self.current_page is None:
            return True

        syslog.syslog('dbfilter_handle_status: %s' % str(self.dbfilter_status))

        # TODO cjwatson 2007-04-04: i18n
        text = ('%s failed with exit code %s. Further information may be '
                'found in /var/log/syslog. Do you want to try running this '
                'step again before continuing? If you do not, your '
                'installation may fail entirely or may be broken.' %
                (self.dbfilter_status[0], self.dbfilter_status[1]))
        dialog = Gtk.Dialog('%s crashed' % self.dbfilter_status[0],
                            self.live_installer, Gtk.DialogFlags.MODAL,
                            (Gtk.STOCK_QUIT, Gtk.ResponseType.CLOSE,
                             'Continue anyway', 1,
                             'Try again', 2))
        self.dbfilter_status = None
        label = Gtk.Label(label=text)
        label.set_line_wrap(True)
        label.set_selectable(False)
        dialog.get_content_area().add(label)
        dialog.show_all()
        response = dialog.run()
        dialog.hide()
        syslog.syslog('dbfilter_handle_status: response %d' % response)
        if response == 1:
            return True
        elif response == Gtk.ResponseType.CLOSE:
            self.quit_installer()
        else:
            step = self.step_name(self.steps.get_current_page())
            if step == "partman":
                print('dbfilter_handle_status stepPart')
                self.set_current_page(self.steps.page_num(self.stepPartAuto))
            return False

    def step_name(self, step_index, return_index=False):
        w = self.steps.get_nth_page(step_index)
        name = None
        index = None
        for p in self.pages:
            if w in p.all_widgets:
                name = p.module.NAME
                index = self.pages.index(p)
        if return_index:
            return name, index
        return name

    def page_name(self, step_index):
        return self.steps.get_nth_page(step_index).get_name()

    def toggle_next_button(self, label='gtk-go-forward', suggested=False):
        if label != 'gtk-go-forward':
            self.next.set_label(self.get_string(label))
        else:
            self.next.set_label(label)
            self.translate_widget(self.next)

        style_context = self.next.get_style_context()
        if suggested:
            style_context.add_class('suggested-action')
        else:
            style_context.remove_class('suggested-action')

    def toggle_skip_button(self, label='skip'):
        self.skip.set_label(self.get_string(label))
        self.skip.show()

    def set_page(self, n):
        self.run_automation_error_cmd()
        # We only stop the backup process when we're on a page where questions
        # need to be asked, otherwise you wont be able to back up past
        # pages that do not stop on questions or are preseeded away.
        self.backup = False
        visible = self.live_installer.get_property('visible')
        self.live_installer.show()
        # Work around a bug in the wrap_fix code whereby the layout does not
        # get properly rendered due to the window not being visible.
        if not visible:
            self.live_installer.resize_children()
        self.page_mode.show()
        cur = None
        is_install = False
        if 'UBIQUITY_GREETER' in os.environ:
            for page in self.pages:
                if page.module.NAME == 'language':
                    # The greeter page is quite large.  Hide it upon leaving.
                    page.ui.page.hide()
                    break
        for page in self.pages:
            if page.module.NAME == n:
                # Now ask ui class which page we want to be showing right now
                if hasattr(page.ui, 'plugin_get_current_page'):
                    cur = page.ui.call('plugin_get_current_page')
                    if isinstance(cur, str) and hasattr(self, cur):
                        cur = getattr(self, cur)  # for not-yet-plugins
                elif page.widgets:
                    cur = page.widgets[0]
                elif page.optional_widgets:
                    cur = page.optional_widgets[0]
                if cur:
                    self.set_page_title(page)
                    self.skip.set_visible(
                        hasattr(page.ui, 'plugin_on_skip_clicked'))
                    cur.show()
                    is_install = page.ui.get('plugin_is_install')
                    is_restart = \
                        page.ui.get('plugin_is_restart')
                    break
        if not cur:
            return False

        if is_restart and not self.oem_user_config:
            self.toggle_next_button('restart_button', suggested=True)
        elif is_install and not self.oem_user_config:
            self.toggle_next_button('install_button')
        else:
            self.toggle_next_button()

        num = self.steps.page_num(cur)
        if num < 0:
            print('Invalid page found for %s: %s' % (n, str(cur)),
                  file=sys.stderr)
            return False

        self.add_history(page, cur)
        self.set_current_page(num)

        if self.pagesindex == 0:
            self.allow_go_backward(False)
        elif 'partman' in [page.module.NAME for page in
                           self.pages[:self.pagesindex - 1]]:
            # We're past partitioning.  Unless the install fails, there is no
            # going back.
            self.allow_go_backward(False)
        elif 'UBIQUITY_AUTOMATIC' not in os.environ:
            self.allow_go_backward(True)

        # If we are in oem-config, ensure the back button is displayed if
        # and only if we are not on the first page.
        if self.oem_user_config:
            self.back.set_visible(self.pagesindex > 0)
        return True

    def set_page_title(self, page, lang=None):
        """Fetches and/or retranslates a page title"""
        title = None
        if page.title:
            title = self.get_string(page.title, lang)
            if title:
                title = title.replace('${RELEASE}', misc.get_release().name)
                # TODO: Use attributes instead?  Would save having to
                # hardcode the size in here.
                self.page_title.set_markup(
                    '<span size="xx-large">%s</span>' % title)
                self.title_section.show()
        if not page.title or not title:
            self.title_section.hide()

    def set_focus(self):
        # Make sure that something reasonable has the focus.  If the first
        # focusable item is a label or a button (often, the welcome text label
        # and the quit button), set the focus to the next button.
        if not self.live_installer.get_focus():
            self.live_installer.child_focus(Gtk.DirectionType.TAB_FORWARD)
        focus = self.live_installer.get_focus()
        if focus:
            if focus.__class__ == Gtk.Label:
                # When it got focus, the whole text was selected.
                focus.select_region(-1, -1)
                self.next.grab_focus()
            elif focus.__class__ == Gtk.Button:
                self.next.grab_focus()
        return True

    def set_current_page(self, current):
        if self.steps.get_current_page() == current:
            # self.steps.set_current_page() will do nothing. Update state
            # ourselves.
            self.on_steps_switch_page(
                self.steps, self.steps.get_nth_page(current), current)
        else:
            self.steps.set_current_page(current)

    # Methods

    def reboot(self, *args):
        """Reboot the system after installing."""
        self.returncode = 10
        self.quit_installer()

    def shutdown(self, *args):
        """Shutdown the system after installing."""
        self.returncode = 11
        self.quit_installer()

    def do_reboot(self):
        """Callback for main program to actually reboot the machine."""
        # don't let reboot race with the shutdown of X in ubiquity-dm;
        # reboot might be too fast and X will stay around forever instead
        # of moving to plymouth
        misc.execute_root(
            "sh", "-c",
            "if ! service display-manager status; then killall Xorg; "
            "while pidof X; do sleep 0.5; done; fi; reboot")

    def do_shutdown(self):
        """Callback for main program to actually shutdown the machine."""
        # don't let poweroff race with the shutdown of X in ubiquity-dm;
        # poweroff might be too fast and X will stay around forever instead
        # of moving to plymouth
        misc.execute_root(
            "sh", "-c",
            "if ! service display-manager status; then killall Xorg; "
            "while pidof X; do sleep 0.5; done; fi; poweroff")

    def quit_installer(self, *args):
        """Quit installer cleanly."""
        # Let the user know we're shutting down.
        self.finished_dialog.get_window().set_cursor(self.watch)
        self.set_busy_cursor(True)
        self.quit_button.set_sensitive(False)
        self.reboot_button.set_sensitive(False)
        self.refresh()

        # exiting from application
        self.current_page = None
        self.warning_dialog.hide()
        if self.dbfilter is not None:
            self.dbfilter.cancel_handler()
        self.quit_main_loop()

    # Callbacks

    def on_quit_clicked(self, unused_widget):
        self.warning_dialog.set_transient_for(
            self.live_installer.get_toplevel())
        self.warning_dialog.show_all()
        # Stop processing.
        return True

    def on_quit_cancelled(self, unused_widget):
        self.warning_dialog.hide()

    def on_live_installer_delete_event(self, widget, unused_event):
        return self.on_quit_clicked(widget)

    def on_skip_clicked(self, unused_widget):
        ui = self.pages[self.pagesindex].ui
        if hasattr(ui, 'plugin_on_skip_clicked'):
            ui.plugin_on_skip_clicked()

    def on_next_clicked(self, unused_widget):
        """Callback to control the installation process between steps."""
        if not self.allowed_change_step or not self.allowed_go_forward:
            return

        self.allow_change_step(False)
        ui = self.pages[self.pagesindex].ui
        if hasattr(ui, 'plugin_on_next_clicked'):
            if ui.plugin_on_next_clicked():
                # Stop processing and return to the page.
                self.allow_change_step(True)
                return

        if self.dbfilter is not None:
            self.dbfilter.ok_handler()
            # expect recursive main loops to be exited and
            # debconffilter_done() to be called when the filter exits
        else:
            self.find_next_step(self.pages[self.pagesindex].module.__name__)
            self.quit_main_loop()

    def process_step(self):
        """Process and validate the results of this step."""
        # setting actual step
        step_num = self.steps.get_current_page()
        step = self.page_name(step_num)
        syslog.syslog('Step_before = %s' % step)

    def on_back_clicked(self, unused_widget):
        """Callback to set previous screen."""
        if not self.allowed_change_step:
            return

        self.allow_change_step(False)
        ui = self.pages[self.pagesindex].ui
        if hasattr(ui, 'plugin_on_back_clicked'):
            if ui.plugin_on_back_clicked():
                # Stop processing and return to the page.
                self.allow_change_step(True)
                return

        self.backup = True
        self.stay_on_page = False

        # Enabling next button
        self.allow_go_forward(True)

        if self.dbfilter is not None:
            self.dbfilter.cancel_handler()
            # expect recursive main loops to be exited and
            # debconffilter_done() to be called when the filter exits
        else:
            self.find_next_step(self.pages[self.pagesindex].module.__name__)
            self.quit_main_loop()

    def on_steps_switch_page(self, unused_notebook, unused_page, current):
        self.current_page = current
        name, index = self.step_name(current, return_index=True)
        if 'UBIQUITY_GREETER' in os.environ:
            if name == 'language':
                self.navigation_control.hide()
            else:
                self.navigation_control.show()
        for i in range(len(self.pages))[:index + 1]:
            self.dot_grid.get_child_at(i, 0).set_fraction(1)
        for i in range(len(self.pages))[index + 1:]:
            self.dot_grid.get_child_at(i, 0).set_fraction(0)

        telemetry.get().add_stage(name)
        syslog.syslog('switched to page %s' % name)

    # Callbacks provided to components.

    def watch_debconf_fd(self, from_debconf, process_input):
        GLib.io_add_watch(from_debconf,
                          GLib.IO_IN | GLib.IO_ERR | GLib.IO_HUP,
                          self.watch_debconf_fd_helper, process_input)

    def watch_debconf_fd_helper(self, source, cb_condition, callback):
        debconf_condition = 0
        if (cb_condition & GLib.IO_IN) != 0:
            debconf_condition |= filteredcommand.DEBCONF_IO_IN
        if (cb_condition & GLib.IO_ERR) != 0:
            debconf_condition |= filteredcommand.DEBCONF_IO_ERR
        if (cb_condition & GLib.IO_HUP) != 0:
            debconf_condition |= filteredcommand.DEBCONF_IO_HUP

        return callback(source, debconf_condition)

    def debconf_progress_start(self, progress_min, progress_max,
                               progress_title):
        self.progress_position.start(progress_min, progress_max,
                                     progress_title)
        self.debconf_progress_set(0)
        self.debconf_progress_info(progress_title)

    def debconf_progress_set(self, progress_val):
        if self.progress_cancelled:
            return False
        self.progress_position.set(progress_val)
        fraction = self.progress_position.fraction()
        self.install_progress.set_fraction(fraction)
        return True

    def debconf_progress_step(self, progress_inc):
        if self.progress_cancelled:
            return False
        self.progress_position.step(progress_inc)
        fraction = self.progress_position.fraction()
        self.install_progress.set_fraction(fraction)
        return True

    def debconf_progress_info(self, progress_info):
        if self.progress_cancelled:
            return False
        self.install_progress_text.set_label(progress_info)
        return True

    def debconf_progress_stop(self):
        self.progress_cancelled = False
        self.progress_position.stop()

    def debconf_progress_region(self, region_start, region_end):
        self.progress_position.set_region(region_start, region_end)

    def debconf_progress_cancellable(self, cancellable):
        if cancellable:
            self.progress_cancel_button.set_sensitive(True)
        else:
            self.progress_cancel_button.set_sensitive(False)
            self.progress_cancelled = False

    def on_progress_cancel_button_clicked(self, unused_button):
        self.progress_cancelled = True

    def debconffilter_done(self, dbfilter):
        if not dbfilter.status:
            self.find_next_step(dbfilter.__module__)
        # TODO: This doesn't handle partman-commit failures.
        elif dbfilter.__module__ in ('ubiquity.components.install',
                                     'ubiquity.components.plugininstall'):
            # We don't want to try to retry a failing step here, because it
            # will have the same set of inputs, and thus likely the same
            # result.
            # TODO: We may want to call return_to_partitioning after the crash
            # dialog instead.
            self.crash_dialog.run()
            self.crash_dialog.hide()
            self.live_installer.hide()
            self.refresh()
            misc.execute_root("apport-bug", "ubiquity")
            sys.exit(1)
        if BaseFrontend.debconffilter_done(self, dbfilter):
            self.quit_main_loop()
            return True
        else:
            return False

    def switch_to_install_interface(self):
        if not self.installing:
            telemetry.get().add_stage(telemetry.START_INSTALL_STAGE_TAG)
        self.installing = True
        self.lockdown_environment()

    def maybe_start_installing(self):
        if not (self.partitioned and self.timezone_set):
            syslog.syslog(
                'Not installing yet, partitioned: %s, timezone_set %s' %
                (self.partitioned, self.timezone_set))
            return

        # Setup zfs layout
        use_zfs = self.db.get('ubiquity/use_zfs')
        if use_zfs == 'true':
            env = os.environ.copy()
            zfs_keystore_key = self.db.get('ubiquity/zfs_keystore_key')
            if zfs_keystore_key:
                os.environ['ZFS_KS_KEY'] = zfs_keystore_key
            misc.execute_root('/usr/share/ubiquity/zsys-setup', 'init')
            os.environ.clear()
            os.environ.update(env)

        syslog.syslog('Starting the installation')

        from ubiquity.debconfcommunicator import DebconfCommunicator
        if self.parallel_db is not None:
            self.parallel_db.shutdown()
        env = os.environ.copy()
        # debconf-apt-progress, start_debconf()
        env['DEBCONF_DB_REPLACE'] = 'configdb'
        env['DEBCONF_DB_OVERRIDE'] = 'Pipe{infd:none outfd:none}'
        self.parallel_db = DebconfCommunicator('ubiquity',
                                               cloexec=True,
                                               env=env)
        # Start the actual install
        dbfilter = install.Install(self, db=self.parallel_db)
        dbfilter.start(auto_process=True)

    def find_next_step(self, finished_step):
        # TODO need to handle the case where debconffilters launched from
        # here crash.  Factor code out of dbfilter_handle_status.
        last_page = self.pages[-1].module.__name__
        if finished_step == last_page and not self.backup:
            self.finished_pages = True
            if self.finished_installing or self.oem_user_config:
                self.debconf_progress_info('')
                # thaw container size
                self.progress_section.set_size_request(-1, -1)
                self.install_details_expander.show()
                self.install_progress.show()
                self.progress_section.show()
                dbfilter = plugininstall.Install(self)
                dbfilter.start(auto_process=True)

        elif finished_step == 'ubi-partman':
            # Flush changes to the database so that when the parallel db
            # starts, it does so with the most recent changes.
            self.stop_debconf()
            self.start_debconf()
            options = misc.grub_options()
            self.grub_options.clear()
            for opt in options:
                self.grub_options.append(opt)
            self.switch_to_install_interface()
            from ubiquity.debconfcommunicator import DebconfCommunicator
            if self.parallel_db is not None:
                # Partitioning failed and we're coming back through again.
                self.parallel_db.shutdown()
            env = os.environ.copy()
            # debconf-apt-progress, start_debconf()
            env['DEBCONF_DB_REPLACE'] = 'configdb'
            env['DEBCONF_DB_OVERRIDE'] = 'Pipe{infd:none outfd:none}'
            self.parallel_db = DebconfCommunicator('ubiquity', cloexec=True,
                                                   env=env)
            dbfilter = partman_commit.PartmanCommit(self, db=self.parallel_db)
            dbfilter.start(auto_process=True)

        elif finished_step == 'ubi-timezone':
            self.timezone_set = True
            # Flush changes to the database so that when the parallel db
            # starts, it does so with the most recent changes.
            self.stop_debconf()
            self.start_debconf()
            self.maybe_start_installing()

        elif finished_step == 'ubiquity.components.partman_commit':
            self.partitioned = True
            self.maybe_start_installing()

        # FIXME OH DEAR LORD.  Use isinstance.
        elif finished_step == 'ubiquity.components.install':
            self.finished_installing = True
            if self.finished_pages:
                dbfilter = plugininstall.Install(self)
                dbfilter.start(auto_process=True)
            else:
                # temporarily freeze container size
                allocation = self.progress_section.get_allocation()
                self.progress_section.set_size_request(
                    allocation.width, allocation.height)
                self.install_details_expander.hide()
                self.install_progress.hide()

        elif finished_step == 'ubiquity.components.plugininstall':
            self.installing = False
            self.run_success_cmd()
            self.quit_main_loop()

    def grub_verify_loop(self, widget, okbutton):
        if widget is not None:
            if validation.check_grub_device(widget.get_child().get_text()):
                okbutton.set_sensitive(True)
            else:
                okbutton.set_sensitive(False)

    def return_to_partitioning(self):
        """If the install progress bar is up but still at the partitioning
        stage, then errors can safely return us to partitioning.
        """

        self.page_section.show()
        if self.installing and not self.installing_no_return:
            # Stop the currently displayed page.
            if self.dbfilter is not None:
                self.dbfilter.cancel_handler()
            # Go back to the partitioner and try again.
            self.pagesindex = -1
            for page in self.pages:
                if page.module.NAME == 'partman':
                    self.pagesindex = self.pages.index(page)
                    break
            if self.pagesindex == -1:
                return

            self.start_debconf()
            ui = self.pages[self.pagesindex].ui
            self.dbfilter = self.pages[self.pagesindex].filter_class(
                self, ui=ui)
            self.allow_change_step(False)
            self.dbfilter.start(auto_process=True)
            self.toggle_next_button()
            self.translate_widget(self.next)
            self.installing = False
            self.progress_section.hide()
            self.unlock_environment()

    def error_dialog(self, title, msg, fatal=True):
        # TODO: cancel button as well if capb backup
        self.run_automation_error_cmd()
        saved_busy_cursor = self.busy_cursor
        self.set_busy_cursor(False)
        if not msg:
            msg = title
        dialog = Gtk.MessageDialog(
            self.live_installer, Gtk.DialogFlags.MODAL,
            Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, "")
        dialog.set_title(title)
        dialog.set_markup(msg)
        for label in dialog.get_message_area().get_children():
            if not isinstance(label, Gtk.Label):
                continue
            label.connect('activate-link', self.on_link_clicked)
        dialog.run()
        self.set_busy_cursor(saved_busy_cursor)
        dialog.hide()
        if fatal:
            self.return_to_partitioning()

    def on_link_clicked(self, widget, uri):
        misc.launch_uri(uri)

    def toggle_grub_fail(self, unused_widget):
        if self.grub_no_new_device.get_active():
            self.no_grub_warn.show()
            self.grub_new_device_entry.set_sensitive(False)
            self.abort_warn.hide()
        elif self.grub_fail_option.get_active():
            self.abort_warn.show()
            self.no_grub_warn.hide()
            self.grub_new_device_entry.set_sensitive(False)
        else:
            self.abort_warn.hide()
            self.no_grub_warn.hide()
            self.grub_new_device_entry.set_sensitive(True)

    def bootloader_dialog(self, current_device):
        ret = self.skip_label.get_label()
        ret = ret.replace('${RELEASE}', misc.get_release().name)
        self.skip_label.set_label(ret)
        self.grub_new_device_entry.get_child().set_text(current_device)
        self.grub_new_device_entry.get_child().grab_focus()
        response = self.bootloader_fail_dialog.run()
        self.bootloader_fail_dialog.hide()
        if response == Gtk.ResponseType.OK:
            if self.grub_new_device.get_active():
                return self.grub_new_device_entry.get_child().get_text()
            elif self.grub_no_new_device.get_active():
                return 'skip'
            else:
                return ''
        else:
            return ''

    def question_dialog(self, title, msg, options, use_templates=True):
        self.run_automation_error_cmd()
        saved_busy_cursor = self.busy_cursor
        self.set_busy_cursor(False)
        if not msg:
            msg = title
        buttons = []
        for option in options:
            if use_templates:
                text = self.get_string(option)
            else:
                text = option
            if text is None:
                text = option
            buttons.append((text, len(buttons) + 1))

        self.ubi_question_dialog.set_title(title)
        self.question_label.set_text(msg)
        actions = self.ubi_question_dialog.get_action_area()
        for action in actions.get_children():
            actions.remove(action)
        for text, response_id in buttons:
            self.ubi_question_dialog.add_button(text, response_id)
        self.ubi_question_dialog.show_all()
        response = self.ubi_question_dialog.run()
        self.set_busy_cursor(saved_busy_cursor)
        self.ubi_question_dialog.hide()
        if response < 0:
            # something other than a button press, probably destroyed
            return None
        else:
            return options[response - 1]

    def refresh(self):
        gtkwidgets.refresh()

    def run_main_loop(self):
        """Run the UI's main loop until it returns control to us."""
        self.allow_change_step(True)
        self.set_focus()
        Gtk.main()
        self.pending_quits = max(0, self.pending_quits - 1)

    pending_quits = 0

    def quit_main_loop(self):
        """Return control to the next level up."""
        # We quit in an idle function, because successive calls to
        # main_quit will do nothing if the main loop hasn't had time to
        # quit.  So we stagger calls to make sure that if this function
        # is called multiple times (nested loops), it works as expected.
        def idle_quit():
            if self.pending_quits > 1:
                quit_quit()
            if Gtk.main_level() > 0:
                Gtk.main_quit()
            else:
                self.pending_quits = max(0, self.pending_quits - 1)
            return False

        def quit_quit():
            # Wait until we're actually out of this main loop
            GLib.idle_add(idle_quit)
            return False

        if self.pending_quits == 0:
            quit_quit()
        self.pending_quits += 1

# vim:ai:et:sts=4:tw=80:sw=4:
