# -*- coding: utf-8; Mode: Python; indent-tabs-mode: nil; tab-width: 4 -*-

# Copyright (C) 2005, 2006, 2007, 2008 Canonical Ltd.
# Written by Tollef Fog Heen <tfheen@ubuntu.com> and
# Colin Watson <cjwatson@ubuntu.com>
#
# This program 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.
#
# This program 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 this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

import os
import re

from ubiquity import gsettings, keyboard_names, misc, osextras, plugin


NAME = 'console_setup'
AFTER = 'language'
WEIGHT = 10


class PageGtk(plugin.PluginUI):
    plugin_title = 'ubiquity/text/keyboard_heading_label'

    def __init__(self, controller, *args, **kwargs):
        self.controller = controller
        self.current_layout = None
        self.keyboard_layout_timeout_id = 0
        self.keyboard_variant_timeout_id = 0
        try:
            from gi.repository import Gtk
            builder = Gtk.Builder()
            self.controller.add_builder(builder)
            builder.add_from_file(os.path.join(
                os.environ['UBIQUITY_GLADE'], 'stepKeyboardConf.ui'))
            builder.connect_signals(self)
            self.page = builder.get_object('stepKeyboardConf')
            self.keyboardlayoutview = builder.get_object('keyboardlayoutview')
            self.keyboardvariantview = builder.get_object(
                'keyboardvariantview')
            self.calculate_keymap_button = builder.get_object('deduce_layout')
            self.keyboard_test = builder.get_object('keyboard_test')
            self.calculate_keymap_button.connect(
                'clicked', self.calculate_clicked)
        except Exception as e:
            self.debug('Could not create keyboard page: %s', e)
            self.page = None
        self.plugin_widgets = self.page

    def plugin_translate(self, lang):
        # TODO Move back into the frontend as we can check
        # isinstance(LabelledEntry) and just call set_label.  We'll need to
        # properly name the debconf keys though (s/inactive_label//)
        test_label = self.controller.get_string('keyboard_test_label', lang)
        self.keyboard_test.set_placeholder_text(test_label)

    def cancel_timeouts(self):
        from gi.repository import GLib
        if self.keyboard_layout_timeout_id:
            GLib.source_remove(self.keyboard_layout_timeout_id)
        if self.keyboard_variant_timeout_id:
            GLib.source_remove(self.keyboard_variant_timeout_id)

    @plugin.only_this_page
    def calculate_result(self, w, keymap):
        ret = self.controller.dbfilter.get_locale()
        keymap = keymap.split(':')
        if len(keymap) == 1:
            keymap.append('')
        layout = keyboard_names.layout_human(ret, keymap[0])
        variant = keyboard_names.variant_human(ret, keymap[0], keymap[1])
        self.set_keyboard(layout)
        self.controller.dbfilter.change_layout(layout)
        self.controller.dbfilter.apply_keyboard(layout, variant)

        # Necessary to clean up references so self.query is garbage collected.
        self.calculate_closed()

    def calculate_closed(self, *args):
        if self.query:
            self.query.destroy()
        self.query = None

    def calculate_clicked(self, *args):
        from ubiquity.frontend.gtk_components.keyboard_query import (
            KeyboardQuery,
        )
        self.query = KeyboardQuery(self.controller._wizard)
        self.query.connect('layout_result', self.calculate_result)
        self.query.connect('delete-event', self.calculate_closed)
        # self.controller._wizard.overlay.set_property('greyed', True)
        self.query.set_transient_for(self.page.get_toplevel())
        self.query.run()

    def on_keyboardlayoutview_row_activated(self, *args):
        self.controller.go_forward()

    @plugin.only_this_page
    def on_keyboard_layout_selected(self, *args):
        if not self.is_automatic:
            # Let's not call this every time the user presses a key.
            from gi.repository import GLib
            if self.keyboard_layout_timeout_id:
                GLib.source_remove(self.keyboard_layout_timeout_id)
            self.keyboard_layout_timeout_id = GLib.timeout_add(
                600, self.keyboard_layout_timeout)
        else:
            self.keyboard_layout_timeout()

    def keyboard_layout_timeout(self, *args):
        layout = self.get_keyboard()
        if layout is not None and layout != self.current_layout:
            self.current_layout = layout
            self.controller.dbfilter.change_layout(layout)
        return False

    def on_keyboardvariantview_row_activated(self, *args):
        self.controller.go_forward()

    @plugin.only_this_page
    def on_keyboard_variant_selected(self, *args):
        if not self.is_automatic:
            # Let's not call this every time the user presses a key.
            from gi.repository import GLib
            if self.keyboard_variant_timeout_id:
                GLib.source_remove(self.keyboard_variant_timeout_id)
            self.keyboard_variant_timeout_id = GLib.timeout_add(
                600, self.keyboard_variant_timeout)
        else:
            self.keyboard_variant_timeout()

    def keyboard_variant_timeout(self, *args):
        layout = self.get_keyboard()
        variant = self.get_keyboard_variant()
        if layout is not None and variant is not None:
            self.controller.dbfilter.apply_keyboard(layout, variant)
        return False

    def set_keyboard_choices(self, choices):
        # Sort the choices including these with accents
        import locale
        try:
            # Try with the current locale (will fail if not generated
            # pre-install)
            locale.setlocale(locale.LC_ALL, '')
        except locale.Error:
            try:
                if os.path.exists("/etc/locale.gen"):
                    # Try with the locale generated by casper
                    with open("/etc/locale.gen", "r") as locale_gen:
                        locale.setlocale(
                            locale.LC_ALL, locale_gen.read().split()[0])
                else:
                    # Try a default, usually working locale
                    locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
                choices.sort(key=lambda c: locale.strxfrm(c.encode('utf-8')))
            except Exception:
                # Let's avoid crashing when we get a bad locale
                choices.sort()

        from gi.repository import Gtk, GObject
        layouts = Gtk.ListStore(GObject.TYPE_STRING)
        self.keyboardlayoutview.set_model(layouts)
        for v in choices:
            layouts.append([v])

        if len(self.keyboardlayoutview.get_columns()) < 1:
            column = Gtk.TreeViewColumn(
                "Layout", Gtk.CellRendererText(), text=0)
            column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
            self.keyboardlayoutview.append_column(column)
            selection = self.keyboardlayoutview.get_selection()
            selection.connect('changed',
                              self.on_keyboard_layout_selected)

        if self.current_layout is not None:
            self.set_keyboard(self.current_layout)

    def set_keyboard(self, layout):
        self.current_layout = layout
        model = self.keyboardlayoutview.get_model()
        if model is None:
            return
        iterator = model.iter_children(None)
        while iterator is not None:
            if misc.utf8(model.get_value(iterator, 0)) == layout:
                path = model.get_path(iterator)
                selection = self.keyboardlayoutview.get_selection()
                if not selection.path_is_selected(path):
                    selection.select_path(path)
                    self.keyboardlayoutview.scroll_to_cell(
                        path, use_align=True, row_align=0.5)
                break
            iterator = model.iter_next(iterator)

    def get_keyboard(self):
        selection = self.keyboardlayoutview.get_selection()
        (model, iterator) = selection.get_selected()
        if iterator is None:
            return None
        else:
            return misc.utf8(model.get_value(iterator, 0))

    def set_keyboard_variant_choices(self, choices):
        from gi.repository import Gtk, GObject
        variants = Gtk.ListStore(GObject.TYPE_STRING)
        self.keyboardvariantview.set_model(variants)
        for v in sorted(choices):
            variants.append([v])

        if len(self.keyboardvariantview.get_columns()) < 1:
            column = Gtk.TreeViewColumn(
                "Variant", Gtk.CellRendererText(), text=0)
            column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
            self.keyboardvariantview.append_column(column)
            selection = self.keyboardvariantview.get_selection()
            selection.connect('changed',
                              self.on_keyboard_variant_selected)

    def set_keyboard_variant(self, variant):
        model = self.keyboardvariantview.get_model()
        if model is None:
            return
        iterator = model.iter_children(None)
        while iterator is not None:
            if misc.utf8(model.get_value(iterator, 0)) == variant:
                path = model.get_path(iterator)
                self.keyboardvariantview.get_selection().select_path(path)
                self.keyboardvariantview.scroll_to_cell(
                    path, use_align=True, row_align=0.5)
                break
            iterator = model.iter_next(iterator)

    def get_keyboard_variant(self):
        selection = self.keyboardvariantview.get_selection()
        (model, iterator) = selection.get_selected()
        if iterator is None:
            return None
        else:
            return misc.utf8(model.get_value(iterator, 0))


class PageKde(plugin.PluginUI):
    plugin_breadcrumb = 'ubiquity/text/breadcrumb_keyboard'

    def __init__(self, controller, *args, **kwargs):
        self.controller = controller
        self.current_layout = None
        self.default_keyboard_layout = None
        self.default_keyboard_variant = None

        try:
            from PyQt5 import uic
            from PyQt5.QtWidgets import QVBoxLayout
            from ubiquity.frontend.kde_components.Keyboard import Keyboard

            self.page = uic.loadUi(
                '/usr/share/ubiquity/qt/stepKeyboardConf.ui')
            self.keyboardDisplay = Keyboard(self.page.keyboard_frame)
            self.page.keyboard_frame.setLayout(QVBoxLayout())
            self.page.keyboard_frame.layout().addWidget(self.keyboardDisplay)
            # use activated instead of changed because we only want to act
            # when the user changes the selection not when we are populating
            # the combo box
            self.page.keyboard_layout_combobox.activated.connect(
                self.on_keyboard_layout_selected)
            self.page.keyboard_variant_combobox.activated.connect(
                self.on_keyboard_variant_selected)
        except Exception as e:
            self.debug('Could not create keyboard page: %s', e)
            self.page = None
        self.plugin_widgets = self.page

    @plugin.only_this_page
    def on_keyboard_layout_selected(self, *args):
        layout = self.get_keyboard()
        lang = self.controller.dbfilter.get_locale()
        if layout is not None:
            # skip updating keyboard if not using display
            if self.keyboardDisplay:
                try:
                    ly = keyboard_names.layout_id(lang, misc.utf8(layout))
                except KeyError:
                    ly = keyboard_names.layout_id('C', misc.utf8(layout))
                self.keyboardDisplay.setLayout(ly)

                # no variants, force update by setting none
                # if not keyboard_names.has_variants(l, ly):
                #    self.keyboardDisplay.setVariant(None)

                self.current_layout = layout
                self.controller.dbfilter.change_layout(layout)

    @plugin.only_this_page
    def on_keyboard_variant_selected(self, *args):
        layout = self.get_keyboard()
        variant = self.get_keyboard_variant()

        if self.keyboardDisplay:
            var = None
            lang = self.controller.dbfilter.get_locale()
            try:
                ly = keyboard_names.layout_id(lang, layout)
            except KeyError:
                ly = keyboard_names.layout_id('C', layout)
            if variant:
                try:
                    var = keyboard_names.variant_id(lang, ly,
                                                    misc.utf8(variant))
                except KeyError:
                    var = None

            self.keyboardDisplay.setVariant(var)

        if layout is not None and variant is not None:
            self.controller.dbfilter.apply_keyboard(layout, variant)

    def set_keyboard_choices(self, choices):
        self.page.keyboard_layout_combobox.clear()
        for choice in sorted(choices):
            self.page.keyboard_layout_combobox.addItem(misc.utf8(choice))

        if self.current_layout is not None:
            self.set_keyboard(self.current_layout)

    @plugin.only_this_page
    def set_keyboard(self, layout):
        index = self.page.keyboard_layout_combobox.findText(misc.utf8(layout))

        if index > -1:
            self.page.keyboard_layout_combobox.setCurrentIndex(index)

        if self.keyboardDisplay:
            lang = self.controller.dbfilter.get_locale()
            try:
                ly = keyboard_names.layout_id(lang, misc.utf8(layout))
            except KeyError:
                ly = keyboard_names.layout_id('C', misc.utf8(layout))
            self.keyboardDisplay.setLayout(ly)

    def get_keyboard(self):
        if self.page.keyboard_layout_combobox.currentIndex() < 0:
            return None

        return str(self.page.keyboard_layout_combobox.currentText())

    def set_keyboard_variant_choices(self, choices):
        self.page.keyboard_variant_combobox.clear()
        for choice in sorted(choices):
            self.page.keyboard_variant_combobox.addItem(misc.utf8(choice))

    @plugin.only_this_page
    def set_keyboard_variant(self, variant):
        index = self.page.keyboard_variant_combobox.findText(
            misc.utf8(variant))

        if index > -1:
            self.page.keyboard_variant_combobox.setCurrentIndex(index)

        if self.keyboardDisplay:
            lang = self.controller.dbfilter.get_locale()
            try:
                layout = keyboard_names.layout_id(lang, self.get_keyboard())
            except KeyError:
                layout = keyboard_names.layout_id('C', self.get_keyboard())
            if variant:
                try:
                    var = keyboard_names.variant_id(
                        lang, layout, misc.utf8(variant))
                except KeyError:
                    var = None

            self.keyboardDisplay.setVariant(var)

    def get_keyboard_variant(self):
        if self.page.keyboard_variant_combobox.currentIndex() < 0:
            return None

        return str(self.page.keyboard_variant_combobox.currentText())


class PageDebconf(plugin.PluginUI):
    plugin_title = 'ubiquity/text/keyboard_heading_label'


class PageNoninteractive(plugin.PluginUI):
    def set_keyboard_choices(self, choices):
        """Set the available keyboard layout choices."""
        pass

    def set_keyboard(self, layout):
        """Set the current keyboard layout."""
        self.current_layout = layout

    def get_keyboard(self):
        """Get the current keyboard layout."""
        return self.current_layout

    def set_keyboard_variant_choices(self, choices):
        """Set the available keyboard variant choices."""
        pass

    def set_keyboard_variant(self, variant):
        """Set the current keyboard variant."""
        self.keyboard_variant = variant

    def get_keyboard_variant(self):
        return self.keyboard_variant


class Page(plugin.Plugin):
    def prepare(self, unfiltered=False):
        self.preseed('console-setup/ask_detect', 'false')

        # We need to get rid of /etc/default/keyboard, or console-setup will
        # think it's already configured and behave differently. Try to save
        # the old file for interest's sake, but it's not a big deal if we
        # can't.
        with misc.raised_privileges():
            osextras.unlink_force('/etc/default/keyboard.pre-ubiquity')
            try:
                os.rename('/etc/default/keyboard',
                          '/etc/default/keyboard.pre-ubiquity')
            except OSError:
                osextras.unlink_force('/etc/default/keyboard')
        # Make sure debconf doesn't do anything with crazy "preseeded"
        # answers to these questions. If you want to preseed these, use the
        # *code variants.
        self.db.fset('keyboard-configuration/layout', 'seen', 'false')
        self.db.fset('keyboard-configuration/variant', 'seen', 'false')
        self.db.fset('keyboard-configuration/model', 'seen', 'false')
        self.db.fset('console-setup/codeset47', 'seen', 'false')

        # Roughly taken from console-setup's config.proto:
        di_locale = self.db.get('debian-installer/locale')
        ret = di_locale.rsplit('.', 1)[0]
        if not keyboard_names.has_language(ret):
            self.debug("No keyboard layout translations for locale '%s'" % ret)
            ret = ret.rsplit('_', 1)[0]
        if not keyboard_names.has_language(ret):
            self.debug("No keyboard layout translations for locale '%s'" % ret)
            # TODO should this be C.UTF-8?!
            ret = 'C'
        self._locale = ret

        self.has_variants = False

        self.gnome_input_sources = self.get_gnome_input_sources()

        # Technically we should provide a version as the second argument,
        # but that isn't currently needed and it would require querying
        # apt/dpkg for the current version, which would be slow, so we don't
        # bother for now.
        command = [
            '/usr/lib/ubiquity/console-setup/keyboard-configuration.postinst',
            'configure',
        ]
        questions = [
            '^keyboard-configuration/layout',
            '^keyboard-configuration/variant',
            '^keyboard-configuration/model',
            '^keyboard-configuration/altgr$',
            '^keyboard-configuration/unsupported_',
        ]
        environ = {
            'OVERRIDE_ALLOW_PRESEEDING': '1',
            'OVERRIDE_USE_DEBCONF_LOCALE': '1',
            'LC_ALL': di_locale,
            'PATH': '/usr/lib/ubiquity/console-setup:' + os.environ['PATH'],
            'DPKG_MAINTSCRIPT_NAME': 'postinst',
            'DPKG_MAINTSCRIPT_PACKAGE': 'keyboard-configuration',
        }
        return command, questions, environ

    # keyboard-configuration has a complex model of whether to store defaults
    # in debconf, induced by its need to run well both in an installer context
    # and as a package.  We need to adjust this while we're running in order
    # that we can go back and forward without keyboard-configuration
    # overwriting our answers with default values.
    def store_defaults(self, store):
        self.preseed_bool(
            'keyboard-configuration/store_defaults_in_debconf_db', store,
            seen=False)

    def run(self, priority, question):
        if self.done:
            return self.succeeded

        if question == 'keyboard-configuration/layout':
            # TODO cjwatson 2006-09-07: no keyboard-configuration support
            # for layout choice translation yet
            self.ui.set_keyboard_choices(
                self.choices_untranslated(question))
            self.ui.set_keyboard(misc.utf8(self.db.get(question)))
            # Reset these in case we just backed up from the variant
            # question.
            self.store_defaults(True)
            self.has_variants = False
            self.succeeded = True
            return True
        elif question in ('keyboard-configuration/variant',
                          'keyboard-configuration/altgr'):
            if question == 'keyboard-configuration/altgr':
                if self.has_variants:
                    return True
                else:
                    # If there's only one variant, it is always the same as
                    # the layout name.
                    single_variant = misc.utf8(self.db.get(
                        'keyboard-configuration/layout'))
                    self.ui.set_keyboard_variant_choices([single_variant])
                    self.ui.set_keyboard_variant(single_variant)
            else:
                # TODO cjwatson 2006-10-02: no keyboard-configuration
                # support for variant choice translation yet
                self.has_variants = True
                self.ui.set_keyboard_variant_choices(
                    self.choices_untranslated(question))
                self.ui.set_keyboard_variant(misc.utf8(self.db.get(question)))
            # keyboard-configuration preseeding is special, and needs to be
            # checked by hand. The seen flag on
            # keyboard-configuration/layout is used internally by
            # keyboard-configuration, so we can't just force it to true.
            if (self.is_automatic and
                self.db.fget(
                    'keyboard-configuration/layoutcode', 'seen') == 'true'):
                return True
            else:
                return plugin.Plugin.run(self, priority, question)
        elif question == 'keyboard-configuration/model':
            # Backing up from the variant question inconveniently goes back
            # to the model question.  Catch this and go forward again so
            # that we can reach the layout question.
            return True
        elif question.startswith('keyboard-configuration/unsupported_'):
            response = self.frontend.question_dialog(
                self.description(question),
                self.extended_description(question),
                ('ubiquity/imported/yes', 'ubiquity/imported/no'))
            if response == 'ubiquity/imported/yes':
                self.preseed(question, 'true')
            else:
                self.preseed(question, 'false')
            return True
        else:
            return True

    def change_layout(self, layout):
        self.preseed('keyboard-configuration/layout', layout)
        self.store_defaults(False)
        # Back up in order to get keyboard-configuration to recalculate the
        # list of possible variants.
        self.succeeded = False
        self.exit_ui_loops()

    def cancel_handler(self):
        if hasattr(self.ui, "cancel_timeouts"):
            self.ui.cancel_timeouts()

        return plugin.Plugin.cancel_handler(self)

    def ok_handler(self):
        variant = self.ui.get_keyboard_variant()
        if variant is not None:
            self.preseed('keyboard-configuration/variant', variant)

        if hasattr(self.ui, "cancel_timeouts"):
            self.ui.cancel_timeouts()

        return plugin.Plugin.ok_handler(self)

    # TODO cjwatson 2006-09-07: This is duplication from
    # keyboard-configuration, but currently difficult to avoid; we need to
    # apply the keymap immediately when the user selects it in the UI (and
    # before they move to the next page), so this needs to be fast and
    # moving through keyboard-configuration to get the corrections it
    # applies will be too slow.
    def adjust_keyboard(self, model, layout, variant, options):
        """Apply any necessary tweaks to the supplied model, layout, variant,
        and options."""

        if layout in ('am', 'ara', 'ben', 'bd', 'bg', 'bt', 'by', 'deva', 'ge',
                      'gh', 'gr', 'guj', 'guru', 'il', 'in', 'ir', 'iku',
                      'kan', 'kh', 'kz', 'la', 'lao', 'lk', 'mk', 'mm', 'mn',
                      'mv', 'mal', 'ori', 'pk', 'ru', 'scc', 'sy', 'syr',
                      'tel', 'th', 'tj', 'tam', 'ua', 'uz'):
            latin = False
            real_layout = 'us,%s' % layout
        elif layout == 'jp':
            if variant in ('106', 'common', 'OADG109A', 'nicola_f_bs', ''):
                latin = True
                real_layout = layout
            else:
                latin = False
                real_layout = 'jp,jp'
        elif layout == 'lt':
            latin = False
            real_layout = 'lt,lt'
        elif layout == 'me':
            if variant == 'basic' or variant.startswith('latin'):
                latin = True
                real_layout = layout
            else:
                latin = False
                real_layout = 'me,me'
        elif layout == 'rs':
            if variant == 'basic' or variant.startswith('latin'):
                latin = True
                real_layout = layout
            else:
                latin = False
                real_layout = 'rs,rs'
        else:
            latin = True
            real_layout = layout

        if latin:
            real_variant = variant
        elif real_layout == 'jp,jp':
            real_variant = '106,%s' % variant
        elif real_layout == 'lt,lt':
            if variant == 'us':
                real_variant = 'us,'
            else:
                real_variant = '%s,us' % variant
        elif real_layout == 'me,me':
            if variant == 'cyrillicyz':
                real_variant = 'latinyz,%s' % variant
            elif variant == 'cyrillicalternatequotes':
                real_variant = 'latinalternatequotes,%s' % variant
            else:
                real_variant = 'basic,%s' % variant
        elif real_layout == 'rs,rs':
            if variant == 'yz':
                real_variant = 'latinyz,%s' % variant
            elif variant == 'alternatequotes':
                real_variant = 'latinalternatequotes,%s' % variant
            else:
                real_variant = 'latin,%s' % variant
        else:
            real_variant = ',%s' % variant

        real_options = [opt for opt in options if not opt.startswith('lv3:')]
        if not latin:
            toggle = re.compile(r'^grp:.*toggle$')
            real_options = [
                opt for opt in real_options if not toggle.match(opt)]
            # TODO cjwatson 2006-09-07: honour crazy preseeding; probably
            # not quite right, especially for Apples which may need a level
            # 3 shift
            real_options.append('grp:alt_shift_toggle')
        if layout != 'us':
            real_options.append('lv3:ralt_switch')

        real_model = model
        if model == 'pc105':
            if real_layout == 'br':
                real_model = 'abnt2'
            elif real_layout == 'jp':
                real_model = 'jp106'

        return (real_model, real_layout, real_variant, real_options)

    def get_locale(self):
        return self._locale

    def apply_keyboard(self, layout_name, variant_name):
        model = self.db.get('keyboard-configuration/modelcode')

        ret = self.get_locale()
        try:
            layout = keyboard_names.layout_id(ret, layout_name)
        except KeyError:
            self.debug("Unknown keyboard layout '%s'" % layout_name)
            return

        if not keyboard_names.has_variants(ret, layout):
            self.debug("No known variants for layout '%s'" % layout)
            variant = ''
        else:
            try:
                variant = keyboard_names.variant_id(
                    ret, layout, variant_name)
            except KeyError:
                self.debug("Unknown keyboard variant '%s' for layout '%s'" %
                           (variant_name, layout_name))
                return

        (model, layout, variant, options) = \
            self.adjust_keyboard(model, layout, variant, [])
        self.set_gnome_keyboard_layout(layout, variant)
        self.debug("Setting keyboard layout: %s %s %s %s" %
                   (model, layout, variant, options))
        self.apply_real_keyboard(model, layout, variant, options)

    @staticmethod
    def get_gnome_input_sources():
        return gsettings.get_list("org.gnome.desktop.input-sources", "sources")

    def set_gnome_input_sources(self, input_sources):
        self.debug(
            "Setting org.gnome.desktop.input-sources sources to %r",
            input_sources,
        )
        gsettings.set_list(
            "org.gnome.desktop.input-sources", "sources", input_sources
        )

    def set_gnome_keyboard_layout(self, layout, variant):
        if variant:
            input_source = ("xkb", f"{layout}+{variant}")
        else:
            input_source = ("xkb", f"{layout}")

        # The only reliable way to change the keyboard layout in GNOME is to
        # set the input sources to exacly the desired layout. See also
        # https://discourse.gnome.org/t/how-to-set-gnome-keyboard-layout-programatically/9459
        self.set_gnome_input_sources([input_source])
        if not self.gnome_input_sources:
            return
        if input_source in self.gnome_input_sources:
            input_sources = self.gnome_input_sources
        else:
            input_sources = [input_source] + self.gnome_input_sources
        self.set_gnome_input_sources(input_sources)

    def apply_real_keyboard(self, model, layout, variant, options):
        args = []
        if model is not None and model != '':
            args.extend(("-model", model))
        args.extend(("-layout", layout))
        if variant != '':
            args.extend(("-variant", variant))
        args.extend(("-option", ""))
        for option in options:
            args.extend(("-option", option))
        misc.execute("setxkbmap", *args)

    @misc.raise_privileges
    def rewrite_xorg_conf(self, model, layout, variant, options):
        oldconfigfile = '/etc/X11/xorg.conf'
        newconfigfile = '/etc/X11/xorg.conf.new'
        try:
            oldconfig = open(oldconfigfile)
        except IOError:
            # Did they remove /etc/X11/xorg.conf or something? Oh well,
            # better to carry on than to crash.
            return
        newconfig = open(newconfigfile, 'w')

        re_section_inputdevice = re.compile(r'\s*Section\s+"InputDevice"\s*$')
        re_driver_kbd = re.compile(r'\s*Driver\s+"kbd"\s*$')
        re_endsection = re.compile(r'\s*EndSection\s*$')
        re_option_xkbmodel = re.compile(r'(\s*Option\s*"XkbModel"\s*).*')
        re_option_xkblayout = re.compile(r'(\s*Option\s*"XkbLayout"\s*).*')
        re_option_xkbvariant = re.compile(r'(\s*Option\s*"XkbVariant"\s*).*')
        re_option_xkboptions = re.compile(r'(\s*Option\s*"XkbOptions"\s*).*')
        in_inputdevice = False
        in_inputdevice_kbd = False
        done = {'model': model == '', 'layout': False,
                'variant': variant == '', 'options': options == ''}

        for line in oldconfig:
            line = line.rstrip('\n')
            if re_section_inputdevice.match(line) is not None:
                in_inputdevice = True
            elif in_inputdevice and re_driver_kbd.match(line) is not None:
                in_inputdevice_kbd = True
            elif re_endsection.match(line) is not None:
                if in_inputdevice_kbd:
                    if not done['model']:
                        print('\tOption\t\t"XkbModel"\t"%s"' % model,
                              file=newconfig)
                    if not done['layout']:
                        print('\tOption\t\t"XkbLayout"\t"%s"' % layout,
                              file=newconfig)
                    if not done['variant']:
                        print('\tOption\t\t"XkbVariant"\t"%s"' % variant,
                              file=newconfig)
                    if not done['options']:
                        print('\tOption\t\t"XkbOptions"\t"%s"' % options,
                              file=newconfig)
                in_inputdevice = False
                in_inputdevice_kbd = False
                done = {'model': model == '', 'layout': False,
                        'variant': variant == '', 'options': options == ''}
            elif in_inputdevice_kbd:
                match = re_option_xkbmodel.match(line)
                if match is not None:
                    if model == '':
                        # hmm, not quite sure what to do here; guessing that
                        # forcing to pc105 will be reasonable
                        line = match.group(1) + '"pc105"'
                    else:
                        line = match.group(1) + '"%s"' % model
                    done['model'] = True
                else:
                    match = re_option_xkblayout.match(line)
                    if match is not None:
                        line = match.group(1) + '"%s"' % layout
                        done['layout'] = True
                    else:
                        match = re_option_xkbvariant.match(line)
                        if match is not None:
                            if variant == '':
                                continue  # delete this line
                            else:
                                line = match.group(1) + '"%s"' % variant
                            done['variant'] = True
                        else:
                            match = re_option_xkboptions.match(line)
                            if match is not None:
                                if options == '':
                                    continue  # delete this line
                                else:
                                    line = match.group(1) + '"%s"' % options
                                done['options'] = True
            print(line, file=newconfig)

        newconfig.close()
        oldconfig.close()
        os.rename(newconfigfile, oldconfigfile)

    def cleanup(self):
        # TODO cjwatson 2006-09-07: I'd use dexconf, but it seems reasonable
        # for somebody to edit /etc/X11/xorg.conf on the live CD and expect
        # that to be carried over to the installed system (indeed, we've
        # always supported that up to now). So we get this horrible mess
        # instead ...

        model = self.db.get('keyboard-configuration/modelcode')
        layout = self.db.get('keyboard-configuration/layoutcode')
        variant = self.db.get('keyboard-configuration/variantcode')
        options = self.db.get('keyboard-configuration/optionscode')
        if options:
            options_list = options.split(',')
        else:
            options_list = []
        self.set_gnome_keyboard_layout(layout, variant)
        self.apply_real_keyboard(model, layout, variant, options_list)

        plugin.Plugin.cleanup(self)

        if layout == '':
            return

        self.rewrite_xorg_conf(model, layout, variant, options)


class Install(plugin.InstallPlugin):
    def prepare(self, unfiltered=False):
        return (['/usr/share/ubiquity/console-setup-apply'], [])

    def install(self, target, progress, *args, **kwargs):
        progress.info('ubiquity/install/keyboard')
        return plugin.InstallPlugin.install(
            self, target, progress, *args, **kwargs)
