/*
 * Copyright (C) 2013 Red Hat, Inc.
 *
 * 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, see <http://www.gnu.org/licenses/>.
 *
 * Written by:
 *     Jasper St. Pierre <jstpierre@mecheye.net>
 *     Matthias Clasen <mclasen@redhat.com>
 */

#include "config.h"
#include "cc-input-chooser.h"

#include <locale.h>
#include <glib/gi18n.h>
#include <gio/gio.h>

#include <gtk/gtk.h>

#define GNOME_DESKTOP_USE_UNSTABLE_API
#include <libgnome-desktop/gnome-languages.h>
#include <libgnome-desktop/gnome-xkb-info.h>

#ifdef HAVE_IBUS
#include <ibus.h>
#include "cc-ibus-utils.h"
#endif

#include "cc-common-language.h"

#include <glib-object.h>

#define INPUT_SOURCE_TYPE_XKB "xkb"
#define INPUT_SOURCE_TYPE_IBUS "ibus"

#define MIN_ROWS 5

struct _CcInputChooserPrivate
{
        GtkWidget *filter_entry;
        GtkWidget *input_list;
	GHashTable *inputs;

        GtkWidget *scrolled_window;
        GtkWidget *no_results;
        GtkWidget *more_item;

        gboolean showing_extra;
	gchar *locale;
        gchar *id;
	gchar *type;
	GnomeXkbInfo *xkb_info;
#ifdef HAVE_IBUS
        IBusBus *ibus;
        GHashTable *ibus_engines;
        GCancellable *ibus_cancellable;
#endif
};
typedef struct _CcInputChooserPrivate CcInputChooserPrivate;
G_DEFINE_TYPE_WITH_PRIVATE (CcInputChooser, cc_input_chooser, GTK_TYPE_BOX);

enum {
        PROP_0,
        PROP_SHOWING_EXTRA,
        PROP_LAST
};

static GParamSpec *obj_props[PROP_LAST];

enum {
	CHANGED,
        CONFIRM,
	LAST_SIGNAL
};

static guint signals[LAST_SIGNAL] = { 0 };

typedef struct {
        GtkWidget *box;
        GtkWidget *label;
        GtkWidget *checkmark;

        gchar *id;
        gchar *type;
        gchar *name;
        gboolean is_extra;
} InputWidget;

static InputWidget *
get_input_widget (GtkWidget *widget)
{
        return g_object_get_data (G_OBJECT (widget), "input-widget");
}

static GtkWidget *
padded_label_new (char *text)
{
        GtkWidget *widget;
        widget = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10);
        gtk_widget_set_halign (widget, GTK_ALIGN_CENTER);
        gtk_widget_set_margin_top (widget, 10);
        gtk_widget_set_margin_bottom (widget, 10);
        gtk_box_pack_start (GTK_BOX (widget), gtk_label_new (text), FALSE, FALSE, 0);
        return widget;
}

static void
input_widget_free (gpointer data)
{
        InputWidget *widget = data;

        g_free (widget->id);
        g_free (widget->type);
        g_free (widget->name);
        g_free (widget);
}

static gboolean
get_layout (CcInputChooser *chooser,
            const gchar    *type,
	    const gchar	   *id,
	    const gchar   **layout,
            const gchar   **variant)
{
        CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);

	if (g_strcmp0 (type, INPUT_SOURCE_TYPE_XKB) == 0) {
		gnome_xkb_info_get_layout_info (priv->xkb_info,
						id, NULL, NULL,
						layout, variant);
                return TRUE;
        }
#ifdef HAVE_IBUS
	if (g_strcmp0 (type, INPUT_SOURCE_TYPE_IBUS) == 0) {
                IBusEngineDesc *engine_desc = NULL;

		if (priv->ibus_engines)
			engine_desc = g_hash_table_lookup (priv->ibus_engines, id);

		if (!engine_desc)
                        return FALSE;

                *layout = ibus_engine_desc_get_layout (engine_desc);
                *variant = "";
                return TRUE;
	}
#endif
	return FALSE;
}

static gboolean
preview_cb (GtkLabel       *label,
	    const gchar    *uri,
	    CcInputChooser *chooser)
{
	GtkWidget *row;
	InputWidget *widget;
	const gchar *layout;
	const gchar *variant;
	gchar *commandline;

	row = gtk_widget_get_parent (GTK_WIDGET (label));
	widget = get_input_widget (row);

	if (!get_layout (chooser, widget->type, widget->id, &layout, &variant))
		return TRUE;

	if (variant[0])
		commandline = g_strdup_printf ("gkbd-keyboard-display -l \"%s\t%s\"", layout, variant);
	else
		commandline = g_strdup_printf ("gkbd-keyboard-display -l %s", layout);
	g_spawn_command_line_async (commandline, NULL);
	g_free (commandline);

	return TRUE;
}

static GtkWidget *
input_widget_new (CcInputChooser *chooser,
		   const char *type,
		   const char *id,
                   gboolean    is_extra)
{
        CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);
	GtkWidget *label;
        InputWidget *widget = g_new0 (InputWidget, 1);
	const gchar *name;
	gchar *text;

	if (g_str_equal (type, INPUT_SOURCE_TYPE_XKB)) {
		gnome_xkb_info_get_layout_info (priv->xkb_info, id, &name, NULL, NULL, NULL);
	}
#ifdef HAVE_IBUS
        else if (g_str_equal (type, INPUT_SOURCE_TYPE_IBUS)) {
                if (priv->ibus_engines)
                        name = engine_get_display_name (g_hash_table_lookup (priv->ibus_engines, id));
                else
                        name = id;
	}
#endif
	else {
		name = "ERROR";
	}

        widget->id = g_strdup (id);
	widget->type = g_strdup (type);
	widget->name = g_strdup (name);
	widget->is_extra = is_extra;

	widget->box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10);
	gtk_widget_set_halign (widget->box, GTK_ALIGN_FILL);
	gtk_widget_set_margin_top (widget->box, 10);
	gtk_widget_set_margin_bottom (widget->box, 10);
	gtk_widget_set_margin_start (widget->box, 10);
	gtk_widget_set_margin_end (widget->box, 10);
	widget->label = gtk_label_new (name);
        gtk_label_set_xalign (GTK_LABEL (widget->label), 0);
        gtk_label_set_yalign (GTK_LABEL (widget->label), 0.5);
        gtk_label_set_ellipsize (GTK_LABEL (widget->label), PANGO_ELLIPSIZE_END);
        gtk_label_set_max_width_chars (GTK_LABEL (widget->label), 40);
	gtk_label_set_width_chars (GTK_LABEL (widget->label), 40);
	gtk_box_pack_start (GTK_BOX (widget->box), widget->label, FALSE, FALSE, 0);
	widget->checkmark = gtk_image_new_from_icon_name ("object-select-symbolic", GTK_ICON_SIZE_MENU);
	gtk_box_pack_start (GTK_BOX (widget->box), widget->checkmark, TRUE, TRUE, 0);
	gtk_widget_set_margin_start (widget->checkmark, 10);
	gtk_widget_set_margin_end (widget->checkmark, 10);
	gtk_widget_set_halign (widget->box, GTK_ALIGN_START);

	text = g_strdup_printf ("<a href='preview'>%s</a>", _("Preview"));
	label = gtk_label_new ("");
	gtk_label_set_markup (GTK_LABEL (label), text);
	g_free (text);
	g_signal_connect (label, "activate-link",
			  G_CALLBACK (preview_cb), chooser);
	gtk_box_pack_start (GTK_BOX (widget->box), label, TRUE, TRUE, 0);

	gtk_widget_show_all (widget->box);

	g_object_set_data_full (G_OBJECT (widget->box), "input-widget", widget,
				input_widget_free);

	return widget->box;
}

static void
sync_checkmark (GtkWidget *row,
                gpointer   user_data)
{
	CcInputChooser *chooser = user_data;
        CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);
        GtkWidget *child;
        InputWidget *widget;
        gboolean should_be_visible;

        child = gtk_bin_get_child (GTK_BIN (row));
        widget = get_input_widget (child);

        if (widget == NULL)
                return;

	if (priv->id == NULL || priv->type == NULL)
		should_be_visible = FALSE;
	else
	        should_be_visible = g_strcmp0 (widget->id, priv->id) == 0 && g_strcmp0 (widget->type, priv->type) == 0;
        gtk_widget_set_opacity (widget->checkmark, should_be_visible ? 1.0 : 0.0);

        if (widget->is_extra && should_be_visible)
                widget->is_extra = FALSE;
}

static void
sync_all_checkmarks (CcInputChooser *chooser)
{
        CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);

        gtk_container_foreach (GTK_CONTAINER (priv->input_list),
                               sync_checkmark, chooser);
        gtk_list_box_invalidate_filter (GTK_LIST_BOX (priv->input_list));
}

static GtkWidget *
more_widget_new (void)
{
        GtkWidget *widget;
        GtkWidget *arrow;

        widget = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10);
        gtk_widget_set_tooltip_text (widget, _("More…"));

        arrow = gtk_image_new_from_icon_name ("view-more-symbolic", GTK_ICON_SIZE_MENU);
        gtk_style_context_add_class (gtk_widget_get_style_context (arrow), "dim-label");
        gtk_widget_set_margin_top (widget, 10);
        gtk_widget_set_margin_bottom (widget, 10);
        gtk_widget_set_halign (arrow, GTK_ALIGN_CENTER);
        gtk_widget_set_valign (arrow, GTK_ALIGN_CENTER);
        gtk_box_pack_start (GTK_BOX (widget), arrow, TRUE, TRUE, 0);
	gtk_widget_show_all (widget);

        return widget;
}

static GtkWidget *
no_results_widget_new (void)
{
        GtkWidget *widget;

        /* Translators: a search for input methods or keyboard layouts
         * did not yield any results
         */
        widget = padded_label_new (_("No inputs found"));
        gtk_widget_set_sensitive (widget, FALSE);
	gtk_widget_show_all (widget);
        return widget;
}

static void
choose_non_extras_foreach (GtkWidget *row,
                           gpointer   user_data)
{
        GtkWidget *child;
        InputWidget *widget;
        guint *count = user_data;

        *count += 1;
        if (*count > MIN_ROWS)
                return;

        child = gtk_bin_get_child (GTK_BIN (row));
        widget = get_input_widget (child);
        if (widget == NULL)
                return;

        widget->is_extra = FALSE;
}

static void
choose_non_extras (CcInputChooser *chooser)
{
        CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);
        guint count = 0;

        gtk_container_foreach (GTK_CONTAINER (priv->input_list),
                               choose_non_extras_foreach, &count);
}

static void
add_rows_to_list (CcInputChooser  *chooser,
	          GList            *list,
	          const gchar      *type,
	          const gchar      *default_id)
{
        CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);
	const gchar *id;
	GtkWidget *widget;
	gchar *key;

	for (; list; list = list->next) {
		id = (const gchar *) list->data;

		if (g_strcmp0 (id, default_id) == 0)
			continue;

		key = g_strdup_printf ("%s::%s", type, id);
		if (g_hash_table_contains (priv->inputs, key)) {
			g_free (key);
			continue;
		}
		g_hash_table_add (priv->inputs, key);

		widget = input_widget_new (chooser, type, id, TRUE);
		gtk_container_add (GTK_CONTAINER (priv->input_list), widget);
	}
}

static void
add_row_to_list (CcInputChooser *chooser,
		 const gchar     *type,
		 const gchar     *id)
{
	GList tmp = { 0 };
	tmp.data = (gpointer)id;
	add_rows_to_list (chooser, &tmp, type, NULL);
}

static void
get_locale_infos (CcInputChooser *chooser)
{
        CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);
	const gchar *type = NULL;
	const gchar *id = NULL;
	gchar *lang, *country;
	GList *list;

	if (gnome_get_input_source_from_locale (priv->locale, &type, &id)) {
                add_row_to_list (chooser, type, id);
		if (!priv->id) {
			priv->id = g_strdup (id);
			priv->type = g_strdup (type);
		}
	}

	if (!gnome_parse_locale (priv->locale, &lang, &country, NULL, NULL))
		goto out;

	list = gnome_xkb_info_get_layouts_for_language (priv->xkb_info, lang);
	add_rows_to_list (chooser, list, INPUT_SOURCE_TYPE_XKB, id);
	g_list_free (list);

	list = gnome_xkb_info_get_layouts_for_country (priv->xkb_info, country);
	add_rows_to_list (chooser, list, INPUT_SOURCE_TYPE_XKB, id);
	g_list_free (list);

        choose_non_extras (chooser);

	list = gnome_xkb_info_get_all_layouts (priv->xkb_info);
	add_rows_to_list (chooser, list, INPUT_SOURCE_TYPE_XKB, id);
	g_list_free (list);

        gtk_widget_show_all (priv->input_list);

out:
	g_free (lang);
	g_free (country);
}

static gboolean
input_visible (GtkListBoxRow *row,
                  gpointer       user_data)
{
        CcInputChooser *chooser = user_data;
        CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);
        InputWidget *widget;
        gboolean visible;
        GtkWidget *child;
        const char *search_term;

        child = gtk_bin_get_child (GTK_BIN (row));
        if (child == priv->more_item)
                return !priv->showing_extra && g_hash_table_size (priv->inputs) > MIN_ROWS;

        widget = get_input_widget (child);

        if (!priv->showing_extra && widget->is_extra)
                return FALSE;

        search_term = gtk_entry_get_text (GTK_ENTRY (priv->filter_entry));
        if (!search_term || !*search_term)
                return TRUE;

        visible = g_str_match_string (search_term, widget->name, TRUE);
        return visible;
}

static gint
sort_inputs (GtkListBoxRow *a,
                GtkListBoxRow *b,
                gpointer       data)
{
        InputWidget *la, *lb;

        la = get_input_widget (gtk_bin_get_child (GTK_BIN (a)));
        lb = get_input_widget (gtk_bin_get_child (GTK_BIN (b)));

        if (la == NULL)
                return 1;

        if (lb == NULL)
                return -1;

        if (la->is_extra && !lb->is_extra)
                return 1;

        if (!la->is_extra && lb->is_extra)
                return -1;

        return strcmp (la->name, lb->name);
}

static void
filter_changed (GtkEntry        *entry,
                CcInputChooser *chooser)
{
        CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);
        gtk_list_box_invalidate_filter (GTK_LIST_BOX (priv->input_list));
}

static void
show_more (CcInputChooser *chooser)
{
        CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);

	if (g_hash_table_size (priv->inputs) <= MIN_ROWS)
		return;

        gtk_widget_show (priv->filter_entry);
        gtk_widget_grab_focus (priv->filter_entry);

	gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (priv->scrolled_window),
					GTK_POLICY_NEVER,
					GTK_POLICY_AUTOMATIC);
	gtk_widget_set_valign (GTK_WIDGET (chooser), GTK_ALIGN_FILL);

        priv->showing_extra = TRUE;
        gtk_list_box_invalidate_filter (GTK_LIST_BOX (priv->input_list));
        g_object_notify_by_pspec (G_OBJECT (chooser), obj_props[PROP_SHOWING_EXTRA]);
}

static void
set_input (CcInputChooser *chooser,
           const gchar    *id,
	   const gchar    *type)
{
        CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);

        if (g_strcmp0 (priv->id, id) == 0 &&
            g_strcmp0 (priv->type, type) == 0)
                return;

        g_free (priv->id);
	g_free (priv->type);
        priv->id = g_strdup (id);
	priv->type = g_strdup (type);

        sync_all_checkmarks (chooser);

	g_signal_emit (chooser, signals[CHANGED], 0);
}

static gboolean
confirm_choice (gpointer data)
{
        GtkWidget *widget = data;

        g_signal_emit (widget, signals[CONFIRM], 0);

        return G_SOURCE_REMOVE;
}

static void
row_activated (GtkListBox        *box,
               GtkListBoxRow     *row,
               CcInputChooser *chooser)
{
        CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);
        GtkWidget *child;
        InputWidget *widget;

        if (row == NULL)
                return;

        child = gtk_bin_get_child (GTK_BIN (row));
        if (child == priv->more_item) {
                show_more (chooser);
        } else {
                widget = get_input_widget (child);
                if (widget == NULL)
                        return;
                if (g_strcmp0 (priv->id, widget->id) == 0 &&
                    g_strcmp0 (priv->type, widget->type) == 0)
                        confirm_choice (chooser);
                else
                        set_input (chooser, widget->id, widget->type);
        }
}

static void
update_header_func (GtkListBoxRow *child,
                    GtkListBoxRow *before,
                    gpointer       user_data)
{
        GtkWidget *header;

        if (before == NULL)
                return;

        header = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
        gtk_list_box_row_set_header (child, header);
        gtk_widget_show (header);
}

#ifdef HAVE_IBUS
static void
update_ibus_active_sources (CcInputChooser *chooser)
{
        CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);
        GList *rows, *l;
        InputWidget *row;
        const gchar *type;
        const gchar *id;
        IBusEngineDesc *engine_desc;
        gchar *name;

        rows = gtk_container_get_children (GTK_CONTAINER (priv->input_list));
        for (l = rows; l; l = l->next) {
		row = get_input_widget (gtk_bin_get_child (GTK_BIN (l->data)));
		if (row == NULL)
			continue;

                type = row->type;
                id = row->id;
                if (g_strcmp0 (type, INPUT_SOURCE_TYPE_IBUS) != 0)
                        continue;

                engine_desc = g_hash_table_lookup (priv->ibus_engines, id);
                if (engine_desc) {
                        name = engine_get_display_name (engine_desc);
                        gtk_label_set_text (GTK_LABEL (row->label), name);
                        g_free (name);
                }
        }
        g_list_free (rows);
}

static void
get_ibus_locale_infos (CcInputChooser *chooser)
{
	CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);
	GHashTableIter iter;
	const gchar *engine_id;
	IBusEngineDesc *engine;

	if (!priv->ibus_engines)
		return;

	g_hash_table_iter_init (&iter, priv->ibus_engines);
	while (g_hash_table_iter_next (&iter, (gpointer *) &engine_id, (gpointer *) &engine))
                add_row_to_list (chooser, INPUT_SOURCE_TYPE_IBUS, engine_id);
}

static void
fetch_ibus_engines_result (GObject       *object,
                           GAsyncResult  *result,
                           CcInputChooser *chooser)
{
        CcInputChooserPrivate *priv;
        GList *list, *l;
        GError *error;

        error = NULL;
        list = ibus_bus_list_engines_async_finish (IBUS_BUS (object), result, &error);
        if (!list && error) {
                if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
                        g_warning ("Couldn't finish IBus request: %s", error->message);
                g_error_free (error);
                return;
        }

        priv = cc_input_chooser_get_instance_private (chooser);
        g_clear_object (&priv->ibus_cancellable);

        /* Maps engine ids to engine description objects */
        priv->ibus_engines = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, g_object_unref);

        for (l = list; l; l = l->next) {
                IBusEngineDesc *engine = l->data;
                const gchar *engine_id;

		engine_id = ibus_engine_desc_get_name (engine);
                if (g_str_has_prefix (engine_id, "xkb:"))
                        g_object_unref (engine);
                else
			g_hash_table_replace (priv->ibus_engines, (gpointer)engine_id, engine);
	}
	g_list_free (list);

	update_ibus_active_sources (chooser);
	get_ibus_locale_infos (chooser);

        sync_all_checkmarks (chooser);
}

static void
fetch_ibus_engines (CcInputChooser *chooser)
{
        CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);

        priv->ibus_cancellable = g_cancellable_new ();

        ibus_bus_list_engines_async (priv->ibus,
                                     -1,
                                     priv->ibus_cancellable,
                                     (GAsyncReadyCallback)fetch_ibus_engines_result,
                                     chooser);

	/* We've got everything we needed, don't want to be called again. */
	g_signal_handlers_disconnect_by_func (priv->ibus, fetch_ibus_engines, chooser);
}

static void
maybe_start_ibus (void)
{
        /* IBus doesn't export API in the session bus. The only thing
	 * we have there is a well known name which we can use as a
	 * sure-fire way to activate it.
	 */
        g_bus_unwatch_name (g_bus_watch_name (G_BUS_TYPE_SESSION,
                                              IBUS_SERVICE_IBUS,
                                              G_BUS_NAME_WATCHER_FLAGS_AUTO_START,
                                              NULL,
                                              NULL,
                                              NULL,
                                              NULL));
}
#endif

static void
cc_input_chooser_constructed (GObject *object)
{
        CcInputChooser *chooser = CC_INPUT_CHOOSER (object);
        CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);

        G_OBJECT_CLASS (cc_input_chooser_parent_class)->constructed (object);

	priv->xkb_info = gnome_xkb_info_new ();

#ifdef HAVE_IBUS
        ibus_init ();
        if (!priv->ibus) {
                priv->ibus = ibus_bus_new_async ();
                if (ibus_bus_is_connected (priv->ibus))
                        fetch_ibus_engines (chooser);
                else
                        g_signal_connect_swapped (priv->ibus, "connected",
                                                  G_CALLBACK (fetch_ibus_engines), chooser);
        }
        maybe_start_ibus ();
#endif

	priv->inputs = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
        priv->more_item = more_widget_new ();
        priv->no_results = no_results_widget_new ();

        gtk_list_box_set_sort_func (GTK_LIST_BOX (priv->input_list),
                                    sort_inputs, chooser, NULL);
        gtk_list_box_set_filter_func (GTK_LIST_BOX (priv->input_list),
                                      input_visible, chooser, NULL);
        gtk_list_box_set_header_func (GTK_LIST_BOX (priv->input_list),
                                      update_header_func, chooser, NULL);
        gtk_list_box_set_selection_mode (GTK_LIST_BOX (priv->input_list),
                                         GTK_SELECTION_NONE);

	if (priv->locale == NULL) {
		priv->locale = cc_common_language_get_current_language ();
	}

        get_locale_infos (chooser);
#ifdef HAVE_IBUS
	get_ibus_locale_infos (chooser);
#endif

        gtk_container_add (GTK_CONTAINER (priv->input_list), priv->more_item);
        gtk_list_box_set_placeholder (GTK_LIST_BOX (priv->input_list), priv->no_results);

        g_signal_connect (priv->filter_entry, "changed",
                          G_CALLBACK (filter_changed),
                          chooser);

        g_signal_connect (priv->input_list, "row-activated",
                          G_CALLBACK (row_activated), chooser);

        sync_all_checkmarks (chooser);
}

static void
cc_input_chooser_finalize (GObject *object)
{
	CcInputChooser *chooser = CC_INPUT_CHOOSER (object);
        CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);

	g_clear_object (&priv->xkb_info);
	g_hash_table_unref (priv->inputs);
#ifdef HAVE_IBUS
        g_clear_object (&priv->ibus);
        if (priv->ibus_cancellable)
                g_cancellable_cancel (priv->ibus_cancellable);
        g_clear_object (&priv->ibus_cancellable);
        g_clear_pointer (&priv->ibus_engines, g_hash_table_destroy);
#endif

	G_OBJECT_CLASS (cc_input_chooser_parent_class)->finalize (object);
}

static void
cc_input_chooser_get_property (GObject      *object,
                                  guint         prop_id,
                                  GValue       *value,
                                  GParamSpec   *pspec)
{
        CcInputChooser *chooser = CC_INPUT_CHOOSER (object);
        switch (prop_id) {
        case PROP_SHOWING_EXTRA:
                g_value_set_boolean (value, cc_input_chooser_get_showing_extra (chooser));
                break;
        default:
                G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
                break;
        }
}

static void
cc_input_chooser_class_init (CcInputChooserClass *klass)
{
        GObjectClass *object_class = G_OBJECT_CLASS (klass);

        gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (klass), "/org/gnome/initial-setup/input-chooser.ui");

        gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), CcInputChooser, filter_entry);
        gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), CcInputChooser, input_list);
        gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), CcInputChooser, scrolled_window);

	object_class->finalize = cc_input_chooser_finalize;
        object_class->get_property = cc_input_chooser_get_property;
        object_class->constructed = cc_input_chooser_constructed;

        obj_props[PROP_SHOWING_EXTRA] =
                g_param_spec_string ("showing-extra", "", "", "",
                                     G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	signals[CHANGED] = 
		g_signal_new ("changed",
			      G_TYPE_FROM_CLASS (object_class),
			      G_SIGNAL_RUN_FIRST,
			      0,
			      NULL, NULL,
			      g_cclosure_marshal_VOID__VOID,
			      G_TYPE_NONE, 0);

        signals[CONFIRM] =
                g_signal_new ("confirm",
                              G_TYPE_FROM_CLASS (object_class),
                              G_SIGNAL_RUN_FIRST,
                              0,
                              NULL, NULL,
                              g_cclosure_marshal_VOID__VOID,
                              G_TYPE_NONE, 0);

        g_object_class_install_properties (object_class, PROP_LAST, obj_props);
}

static void
cc_input_chooser_init (CcInputChooser *chooser)
{
        gtk_widget_init_template (GTK_WIDGET (chooser));
}

void
cc_input_chooser_clear_filter (CcInputChooser *chooser)
{
        CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);
        gtk_entry_set_text (GTK_ENTRY (priv->filter_entry), "");
}

const gchar *
cc_input_chooser_get_input_id (CcInputChooser *chooser)
{
        CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);
        return priv->id;
}

const gchar *
cc_input_chooser_get_input_type (CcInputChooser *chooser)
{
        CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);
        return priv->type;
}

void
cc_input_chooser_get_layout (CcInputChooser *chooser,
			     const gchar    **layout,
			     const gchar    **variant)
{
        CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);

	get_layout (chooser, priv->type, priv->id, layout, variant);
}

void
cc_input_chooser_set_input (CcInputChooser *chooser,
                            const gchar    *id,
			    const gchar    *type)
{
        set_input (chooser, id, type);
}

gboolean
cc_input_chooser_get_showing_extra (CcInputChooser *chooser)
{
        CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);
        return priv->showing_extra;
}
