summaryrefslogtreecommitdiffstats
path: root/panels/region
diff options
context:
space:
mode:
Diffstat (limited to 'panels/region')
-rw-r--r--panels/region/.indent.pro2
-rw-r--r--panels/region/cc-format-chooser.c652
-rw-r--r--panels/region/cc-format-chooser.h36
-rw-r--r--panels/region/cc-format-chooser.ui463
-rw-r--r--panels/region/cc-ibus-utils.c43
-rw-r--r--panels/region/cc-ibus-utils.h26
-rw-r--r--panels/region/cc-input-chooser.c1102
-rw-r--r--panels/region/cc-input-chooser.h41
-rw-r--r--panels/region/cc-input-chooser.ui95
-rw-r--r--panels/region/cc-input-row.c273
-rw-r--r--panels/region/cc-input-row.h40
-rw-r--r--panels/region/cc-input-row.ui100
-rw-r--r--panels/region/cc-input-source-ibus.c155
-rw-r--r--panels/region/cc-input-source-ibus.h46
-rw-r--r--panels/region/cc-input-source-xkb.c134
-rw-r--r--panels/region/cc-input-source-xkb.h39
-rw-r--r--panels/region/cc-input-source.c84
-rw-r--r--panels/region/cc-input-source.h49
-rw-r--r--panels/region/cc-region-panel.c1613
-rw-r--r--panels/region/cc-region-panel.h30
-rw-r--r--panels/region/cc-region-panel.ui515
-rw-r--r--panels/region/gnome-region-panel.desktop.in.in18
-rw-r--r--panels/region/meson.build63
-rw-r--r--panels/region/region.gresource.xml12
-rw-r--r--panels/region/view-layout-symbolic.svg65
25 files changed, 5696 insertions, 0 deletions
diff --git a/panels/region/.indent.pro b/panels/region/.indent.pro
new file mode 100644
index 0000000..bdff074
--- /dev/null
+++ b/panels/region/.indent.pro
@@ -0,0 +1,2 @@
+-kr -i8 -pcs -lps -psl
+
diff --git a/panels/region/cc-format-chooser.c b/panels/region/cc-format-chooser.c
new file mode 100644
index 0000000..d88508c
--- /dev/null
+++ b/panels/region/cc-format-chooser.c
@@ -0,0 +1,652 @@
+/*
+ * 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:
+ * Matthias Clasen
+ */
+
+#define _GNU_SOURCE
+#include <config.h>
+#include "cc-format-chooser.h"
+
+#include <errno.h>
+#include <locale.h>
+#include <langinfo.h>
+#include <string.h>
+#include <glib/gi18n.h>
+#include <handy.h>
+
+#include "list-box-helper.h"
+#include "cc-common-language.h"
+#include "cc-util.h"
+
+#define GNOME_DESKTOP_USE_UNSTABLE_API
+#include <libgnome-desktop/gnome-languages.h>
+
+struct _CcFormatChooser {
+ GtkDialog parent_instance;
+
+ GtkWidget *title_bar;
+ GtkWidget *title_buttons;
+ GtkWidget *cancel_button;
+ GtkWidget *back_button;
+ GtkWidget *done_button;
+ GtkWidget *empty_results_view;
+ GtkWidget *main_leaflet;
+ GtkWidget *region_filter_entry;
+ GtkWidget *region_list;
+ GtkWidget *region_list_stack;
+ GtkWidget *common_region_title;
+ GtkWidget *common_region_listbox;
+ GtkWidget *region_title;
+ GtkWidget *region_listbox;
+ GtkWidget *date_format_label;
+ GtkWidget *time_format_label;
+ GtkWidget *date_time_format_label;
+ GtkWidget *number_format_label;
+ GtkWidget *measurement_format_label;
+ GtkWidget *paper_format_label;
+ gboolean adding;
+ gboolean showing_extra;
+ gboolean no_results;
+ gchar *region;
+ gchar *preview_region;
+ gchar **filter_words;
+};
+
+G_DEFINE_TYPE (CcFormatChooser, cc_format_chooser, GTK_TYPE_DIALOG)
+
+static void
+display_date (GtkWidget *label, GDateTime *dt, const gchar *format)
+{
+ g_autofree gchar *s = g_date_time_format (dt, format);
+ gtk_label_set_text (GTK_LABEL (label), g_strstrip (s));
+}
+
+static void
+update_format_examples (CcFormatChooser *chooser,
+ const gchar *region)
+{
+ locale_t locale;
+ locale_t old_locale;
+ g_autoptr(GDateTime) dt = NULL;
+ g_autofree gchar *s = NULL;
+ const gchar *fmt;
+ g_autoptr(GtkPaperSize) paper = NULL;
+
+ if (!region || !*region)
+ return;
+
+ locale = newlocale (LC_TIME_MASK, region, (locale_t) 0);
+ if (locale == (locale_t) 0)
+ g_warning ("Failed to create locale %s: %s", region, g_strerror (errno));
+ else
+ old_locale = uselocale (locale);
+
+ dt = g_date_time_new_now_local ();
+ display_date (chooser->date_format_label, dt, "%x");
+ display_date (chooser->time_format_label, dt, "%X");
+ display_date (chooser->date_time_format_label, dt, "%c");
+
+ if (locale != (locale_t) 0) {
+ uselocale (old_locale);
+ freelocale (locale);
+ }
+
+ locale = newlocale (LC_NUMERIC_MASK, region, (locale_t) 0);
+ if (locale == (locale_t) 0)
+ g_warning ("Failed to create locale %s: %s", region, g_strerror (errno));
+ else
+ old_locale = uselocale (locale);
+
+ s = g_strdup_printf ("%'.2f", 123456789.00);
+ gtk_label_set_text (GTK_LABEL (chooser->number_format_label), s);
+
+ if (locale != (locale_t) 0) {
+ uselocale (old_locale);
+ freelocale (locale);
+ }
+
+#if 0
+ locale = newlocale (LC_MONETARY_MASK, region, (locale_t) 0);
+ if (locale == (locale_t) 0)
+ g_warning ("Failed to create locale %s: %s", region, g_strerror (errno));
+ else
+ old_locale = uselocale (locale);
+
+ num_info = localeconv ();
+ if (num_info != NULL)
+ gtk_label_set_text (GTK_LABEL (chooser->currency_format_label), num_info->currency_symbol);
+
+ if (locale != (locale_t) 0) {
+ uselocale (old_locale);
+ freelocale (locale);
+ }
+#endif
+
+#ifdef LC_MEASUREMENT
+ locale = newlocale (LC_MEASUREMENT_MASK, region, (locale_t) 0);
+ if (locale == (locale_t) 0)
+ g_warning ("Failed to create locale %s: %s", region, g_strerror (errno));
+ else
+ old_locale = uselocale (locale);
+
+ fmt = nl_langinfo (_NL_MEASUREMENT_MEASUREMENT);
+ if (fmt && *fmt == 2)
+ gtk_label_set_text (GTK_LABEL (chooser->measurement_format_label), C_("measurement format", "Imperial"));
+ else
+ gtk_label_set_text (GTK_LABEL (chooser->measurement_format_label), C_("measurement format", "Metric"));
+
+ if (locale != (locale_t) 0) {
+ uselocale (old_locale);
+ freelocale (locale);
+ }
+#endif
+
+#ifdef LC_PAPER
+ locale = newlocale (LC_PAPER_MASK, region, (locale_t) 0);
+ if (locale == (locale_t) 0)
+ g_warning ("Failed to create locale %s: %s", region, g_strerror (errno));
+ else
+ old_locale = uselocale (locale);
+
+ paper = gtk_paper_size_new (gtk_paper_size_get_default ());
+ gtk_label_set_text (GTK_LABEL (chooser->paper_format_label), gtk_paper_size_get_display_name (paper));
+
+ if (locale != (locale_t) 0) {
+ uselocale (old_locale);
+ freelocale (locale);
+ }
+#endif
+}
+
+static void
+update_check_button_for_list (GtkWidget *list_box,
+ const gchar *locale_id)
+{
+ g_autoptr(GList) children = NULL;
+ GList *l;
+
+ g_assert (GTK_IS_CONTAINER (list_box));
+
+ children = gtk_container_get_children (GTK_CONTAINER (list_box));
+ for (l = children; l; l = l->next)
+ {
+ GtkWidget *row = l->data;
+ GtkWidget *check = g_object_get_data (G_OBJECT (row), "check");
+ const gchar *region = g_object_get_data (G_OBJECT (row), "locale-id");
+ if (check == NULL || region == NULL)
+ continue;
+
+ if (g_strcmp0 (locale_id, region) == 0)
+ gtk_widget_set_opacity (check, 1.0);
+ else
+ gtk_widget_set_opacity (check, 0.0);
+ }
+}
+
+static void
+set_locale_id (CcFormatChooser *chooser,
+ const gchar *locale_id)
+{
+ g_free (chooser->region);
+ chooser->region = g_strdup (locale_id);
+
+ update_check_button_for_list (chooser->region_listbox, locale_id);
+ update_check_button_for_list (chooser->common_region_listbox, locale_id);
+ update_format_examples (chooser, locale_id);
+}
+
+static gint
+sort_regions (gconstpointer a,
+ gconstpointer b,
+ gpointer data)
+{
+ const gchar *la;
+ const gchar *lb;
+
+ if (g_object_get_data (G_OBJECT (a), "locale-id") == NULL)
+ return 1;
+ if (g_object_get_data (G_OBJECT (b), "locale-id") == NULL)
+ return -1;
+
+ la = g_object_get_data (G_OBJECT (a), "locale-name");
+ lb = g_object_get_data (G_OBJECT (b), "locale-name");
+
+ return g_strcmp0 (la, lb);
+}
+
+static GtkWidget *
+padded_label_new (const char *text)
+{
+ GtkWidget *widget, *label;
+
+ widget = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10);
+ g_object_set (widget, "margin-top", 4, NULL);
+ g_object_set (widget, "margin-bottom", 4, NULL);
+ g_object_set (widget, "margin-start", 10, NULL);
+ g_object_set (widget, "margin-end", 10, NULL);
+
+ label = gtk_label_new (text);
+ gtk_label_set_ellipsize (GTK_LABEL (label), PANGO_ELLIPSIZE_END);
+ gtk_widget_show (label);
+ gtk_container_add (GTK_CONTAINER (widget), label);
+
+ return widget;
+}
+
+static void
+format_chooser_back_button_clicked_cb (CcFormatChooser *self)
+{
+ g_assert (CC_IS_FORMAT_CHOOSER (self));
+
+ gtk_header_bar_set_title (GTK_HEADER_BAR (self->title_bar), _("Formats"));
+ hdy_leaflet_set_visible_child_name (HDY_LEAFLET (self->main_leaflet), "region-list");
+ gtk_stack_set_visible_child (GTK_STACK (self->title_buttons), self->cancel_button);
+ gtk_widget_show (self->done_button);
+}
+
+static void
+cc_format_chooser_preview_button_set_visible (GtkListBoxRow *row,
+ gpointer user_data)
+{
+ GtkWidget *button;
+ gboolean visible;
+
+ g_assert (GTK_IS_LIST_BOX_ROW (row));
+
+ visible = GPOINTER_TO_INT (user_data);
+ button = g_object_get_data (G_OBJECT (row), "preview-button");
+ g_assert (button);
+
+ gtk_widget_set_opacity (button, visible);
+ gtk_widget_set_sensitive (button, visible);
+}
+
+static void
+format_chooser_leaflet_fold_changed_cb (CcFormatChooser *self)
+{
+ gboolean folded;
+
+ g_assert (CC_IS_FORMAT_CHOOSER (self));
+
+ folded = hdy_leaflet_get_folded (HDY_LEAFLET (self->main_leaflet));
+ gtk_container_foreach (GTK_CONTAINER (self->common_region_listbox),
+ (GtkCallback)cc_format_chooser_preview_button_set_visible,
+ GINT_TO_POINTER (folded));
+ gtk_container_foreach (GTK_CONTAINER (self->region_listbox),
+ (GtkCallback)cc_format_chooser_preview_button_set_visible,
+ GINT_TO_POINTER (folded));
+
+ if (!folded)
+ {
+ update_format_examples (self, self->region);
+ gtk_header_bar_set_title (GTK_HEADER_BAR (self->title_bar), _("Formats"));
+ hdy_leaflet_set_visible_child_name (HDY_LEAFLET (self->main_leaflet), "region-list");
+ gtk_stack_set_visible_child (GTK_STACK (self->title_buttons), self->cancel_button);
+ gtk_widget_show (self->done_button);
+ }
+}
+
+static void
+preview_button_clicked_cb (CcFormatChooser *self,
+ GtkWidget *button)
+{
+ GtkWidget *row;
+ const gchar *region, *locale_name;
+
+ g_assert (CC_IS_FORMAT_CHOOSER (self));
+ g_assert (GTK_IS_WIDGET (button));
+
+ row = gtk_widget_get_ancestor (button, GTK_TYPE_LIST_BOX_ROW);
+ g_assert (row);
+
+ region = g_object_get_data (G_OBJECT (row), "locale-id");
+ locale_name = g_object_get_data (G_OBJECT (row), "locale-name");
+ update_format_examples (self, region);
+
+ hdy_leaflet_set_visible_child_name (HDY_LEAFLET (self->main_leaflet), "preview");
+ gtk_stack_set_visible_child (GTK_STACK (self->title_buttons), self->back_button);
+ gtk_widget_hide (self->done_button);
+
+ if (locale_name)
+ gtk_header_bar_set_title (GTK_HEADER_BAR (self->title_bar), locale_name);
+}
+
+static GtkWidget *
+region_widget_new (CcFormatChooser *self,
+ const gchar *locale_id)
+{
+ gchar *locale_name;
+ gchar *locale_current_name;
+ gchar *locale_untranslated_name;
+ GtkWidget *row, *box, *button;
+ GtkWidget *check;
+
+ locale_name = gnome_get_country_from_locale (locale_id, locale_id);
+ if (!locale_name)
+ return NULL;
+
+ locale_current_name = gnome_get_country_from_locale (locale_id, NULL);
+ locale_untranslated_name = gnome_get_country_from_locale (locale_id, "C");
+
+ row = gtk_list_box_row_new ();
+ gtk_widget_show (row);
+ box = padded_label_new (locale_name);
+ gtk_widget_show (box);
+ gtk_container_add (GTK_CONTAINER (row), box);
+
+ button = gtk_button_new_from_icon_name ("view-layout-symbolic", GTK_ICON_SIZE_BUTTON);
+ g_signal_connect_object (button, "clicked", G_CALLBACK (preview_button_clicked_cb),
+ self, G_CONNECT_SWAPPED);
+ gtk_widget_show (button);
+ gtk_box_pack_end (GTK_BOX (box), button, FALSE, TRUE, 0);
+
+ check = gtk_image_new ();
+ gtk_widget_show (check);
+ gtk_image_set_from_icon_name (GTK_IMAGE (check), "object-select-symbolic", GTK_ICON_SIZE_MENU);
+ gtk_widget_set_opacity (check, 0.0);
+ g_object_set (check, "icon-size", GTK_ICON_SIZE_MENU, NULL);
+ gtk_container_add (GTK_CONTAINER (box), check);
+
+ g_object_set_data (G_OBJECT (row), "check", check);
+ g_object_set_data (G_OBJECT (row), "preview-button", button);
+ g_object_set_data_full (G_OBJECT (row), "locale-id", g_strdup (locale_id), g_free);
+ g_object_set_data_full (G_OBJECT (row), "locale-name", locale_name, g_free);
+ g_object_set_data_full (G_OBJECT (row), "locale-current-name", locale_current_name, g_free);
+ g_object_set_data_full (G_OBJECT (row), "locale-untranslated-name", locale_untranslated_name, g_free);
+
+ return row;
+}
+
+static void
+add_regions (CcFormatChooser *chooser,
+ gchar **locale_ids,
+ GHashTable *initial)
+{
+ g_autoptr(GList) initial_locales = NULL;
+ GtkWidget *widget;
+ GList *l;
+
+ chooser->adding = TRUE;
+ initial_locales = g_hash_table_get_keys (initial);
+
+ /* Populate Common Locales */
+ for (l = initial_locales; l != NULL; l = l->next) {
+ if (!cc_common_language_has_font (l->data))
+ continue;
+
+ widget = region_widget_new (chooser, l->data);
+ if (!widget)
+ continue;
+
+ gtk_widget_show (widget);
+ gtk_container_add (GTK_CONTAINER (chooser->common_region_listbox), widget);
+ }
+
+ /* Populate All locales */
+ while (*locale_ids) {
+ gchar *locale_id;
+
+ locale_id = *locale_ids;
+ locale_ids ++;
+
+ if (!cc_common_language_has_font (locale_id))
+ continue;
+
+ widget = region_widget_new (chooser, locale_id);
+ if (!widget)
+ continue;
+
+ gtk_widget_show (widget);
+ gtk_container_add (GTK_CONTAINER (chooser->region_listbox), widget);
+ }
+
+ chooser->adding = FALSE;
+}
+
+static void
+add_all_regions (CcFormatChooser *chooser)
+{
+ g_auto(GStrv) locale_ids = NULL;
+ g_autoptr(GHashTable) initial = NULL;
+
+ locale_ids = gnome_get_all_locales ();
+ initial = cc_common_language_get_initial_languages ();
+ add_regions (chooser, locale_ids, initial);
+}
+
+static gboolean
+match_all (gchar **words,
+ const gchar *str)
+{
+ gchar **w;
+
+ for (w = words; *w; ++w)
+ if (!strstr (str, *w))
+ return FALSE;
+
+ return TRUE;
+}
+
+static gboolean
+region_visible (GtkListBoxRow *row,
+ gpointer user_data)
+{
+ CcFormatChooser *chooser = user_data;
+ g_autofree gchar *locale_name = NULL;
+ g_autofree gchar *locale_current_name = NULL;
+ g_autofree gchar *locale_untranslated_name = NULL;
+ gboolean match = TRUE;
+
+ if (!chooser->filter_words)
+ goto end;
+
+ locale_name =
+ cc_util_normalize_casefold_and_unaccent (g_object_get_data (G_OBJECT (row), "locale-name"));
+ if (match_all (chooser->filter_words, locale_name))
+ goto end;
+
+ locale_current_name =
+ cc_util_normalize_casefold_and_unaccent (g_object_get_data (G_OBJECT (row), "locale-current-name"));
+ if (match_all (chooser->filter_words, locale_current_name))
+ goto end;
+
+ locale_untranslated_name =
+ cc_util_normalize_casefold_and_unaccent (g_object_get_data (G_OBJECT (row), "locale-untranslated-name"));
+
+ match = match_all (chooser->filter_words, locale_untranslated_name);
+
+ end:
+ if (match)
+ chooser->no_results = FALSE;
+ return match;
+}
+
+static void
+filter_changed (CcFormatChooser *chooser)
+{
+ g_autofree gchar *filter_contents = NULL;
+ gboolean visible;
+
+ g_clear_pointer (&chooser->filter_words, g_strfreev);
+
+ filter_contents =
+ cc_util_normalize_casefold_and_unaccent (gtk_entry_get_text (GTK_ENTRY (chooser->region_filter_entry)));
+
+ /* The popular listbox is shown only if search is empty */
+ visible = filter_contents == NULL || *filter_contents == '\0';
+ gtk_widget_set_visible (chooser->common_region_listbox, visible);
+ gtk_widget_set_visible (chooser->common_region_title, visible);
+ gtk_widget_set_visible (chooser->region_title, visible);
+
+ /* Reset cached search state */
+ chooser->no_results = TRUE;
+
+ if (!filter_contents) {
+ gtk_list_box_invalidate_filter (GTK_LIST_BOX (chooser->region_listbox));
+ gtk_list_box_set_placeholder (GTK_LIST_BOX (chooser->region_listbox), NULL);
+ return;
+ }
+ chooser->filter_words = g_strsplit_set (g_strstrip (filter_contents), " ", 0);
+ gtk_list_box_invalidate_filter (GTK_LIST_BOX (chooser->region_listbox));
+
+ if (chooser->no_results)
+ gtk_stack_set_visible_child (GTK_STACK (chooser->region_list_stack),
+ GTK_WIDGET (chooser->empty_results_view));
+ else
+ gtk_stack_set_visible_child (GTK_STACK (chooser->region_list_stack),
+ GTK_WIDGET (chooser->region_list));
+}
+
+static void
+row_activated (CcFormatChooser *chooser,
+ GtkListBoxRow *row)
+{
+ const gchar *new_locale_id;
+
+ if (chooser->adding)
+ return;
+
+ new_locale_id = g_object_get_data (G_OBJECT (row), "locale-id");
+ if (g_strcmp0 (new_locale_id, chooser->region) == 0) {
+ gtk_dialog_response (GTK_DIALOG (chooser),
+ gtk_dialog_get_response_for_widget (GTK_DIALOG (chooser),
+ chooser->done_button));
+ } else {
+ set_locale_id (chooser, new_locale_id);
+ }
+}
+
+static void
+activate_default (CcFormatChooser *chooser)
+{
+ GtkWidget *focus;
+ const gchar *locale_id;
+
+ focus = gtk_window_get_focus (GTK_WINDOW (chooser));
+ if (!focus)
+ return;
+
+ locale_id = g_object_get_data (G_OBJECT (focus), "locale-id");
+ if (g_strcmp0 (locale_id, chooser->region) == 0)
+ return;
+
+ g_signal_stop_emission_by_name (chooser, "activate-default");
+ gtk_widget_activate (focus);
+}
+
+static void
+cc_format_chooser_dispose (GObject *object)
+{
+ CcFormatChooser *chooser = CC_FORMAT_CHOOSER (object);
+
+ g_clear_pointer (&chooser->filter_words, g_strfreev);
+ g_clear_pointer (&chooser->region, g_free);
+
+ G_OBJECT_CLASS (cc_format_chooser_parent_class)->dispose (object);
+}
+
+void
+cc_format_chooser_class_init (CcFormatChooserClass *klass)
+{
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = cc_format_chooser_dispose;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/region/cc-format-chooser.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, title_bar);
+ gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, title_buttons);
+ gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, cancel_button);
+ gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, back_button);
+ gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, done_button);
+ gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, main_leaflet);
+ gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, region_filter_entry);
+ gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, common_region_title);
+ gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, common_region_listbox);
+ gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, region_title);
+ gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, region_listbox);
+ gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, date_format_label);
+ gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, time_format_label);
+ gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, date_time_format_label);
+ gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, number_format_label);
+ gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, measurement_format_label);
+ gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, paper_format_label);
+ gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, region_list);
+ gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, region_list_stack);
+ gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, empty_results_view);
+
+ gtk_widget_class_bind_template_callback (widget_class, format_chooser_back_button_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, format_chooser_leaflet_fold_changed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, filter_changed);
+ gtk_widget_class_bind_template_callback (widget_class, row_activated);
+}
+
+void
+cc_format_chooser_init (CcFormatChooser *chooser)
+{
+ gtk_widget_init_template (GTK_WIDGET (chooser));
+
+ gtk_list_box_set_sort_func (GTK_LIST_BOX (chooser->common_region_listbox),
+ (GtkListBoxSortFunc)sort_regions, chooser, NULL);
+ gtk_list_box_set_sort_func (GTK_LIST_BOX (chooser->region_listbox),
+ (GtkListBoxSortFunc)sort_regions, chooser, NULL);
+ gtk_list_box_set_filter_func (GTK_LIST_BOX (chooser->region_listbox),
+ region_visible, chooser, NULL);
+ gtk_list_box_set_header_func (GTK_LIST_BOX (chooser->region_listbox),
+ cc_list_box_update_header_func, NULL, NULL);
+ gtk_list_box_set_header_func (GTK_LIST_BOX (chooser->common_region_listbox),
+ cc_list_box_update_header_func, NULL, NULL);
+
+ add_all_regions (chooser);
+ gtk_list_box_invalidate_filter (GTK_LIST_BOX (chooser->region_listbox));
+ format_chooser_leaflet_fold_changed_cb (chooser);
+
+ g_signal_connect_object (chooser, "activate-default",
+ G_CALLBACK (activate_default), chooser, G_CONNECT_SWAPPED);
+}
+
+CcFormatChooser *
+cc_format_chooser_new (void)
+{
+ return CC_FORMAT_CHOOSER (g_object_new (CC_TYPE_FORMAT_CHOOSER,
+ "use-header-bar", 1,
+ NULL));
+}
+
+void
+cc_format_chooser_clear_filter (CcFormatChooser *chooser)
+{
+ g_return_if_fail (CC_IS_FORMAT_CHOOSER (chooser));
+ gtk_entry_set_text (GTK_ENTRY (chooser->region_filter_entry), "");
+}
+
+const gchar *
+cc_format_chooser_get_region (CcFormatChooser *chooser)
+{
+ g_return_val_if_fail (CC_IS_FORMAT_CHOOSER (chooser), NULL);
+ return chooser->region;
+}
+
+void
+cc_format_chooser_set_region (CcFormatChooser *chooser,
+ const gchar *region)
+{
+ g_return_if_fail (CC_IS_FORMAT_CHOOSER (chooser));
+ set_locale_id (chooser, region);
+}
diff --git a/panels/region/cc-format-chooser.h b/panels/region/cc-format-chooser.h
new file mode 100644
index 0000000..0248338
--- /dev/null
+++ b/panels/region/cc-format-chooser.h
@@ -0,0 +1,36 @@
+/*
+ * 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:
+ * Matthias Clasen
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_FORMAT_CHOOSER (cc_format_chooser_get_type ())
+G_DECLARE_FINAL_TYPE (CcFormatChooser, cc_format_chooser, CC, FORMAT_CHOOSER, GtkDialog)
+
+CcFormatChooser *cc_format_chooser_new (void);
+void cc_format_chooser_clear_filter (CcFormatChooser *chooser);
+const gchar *cc_format_chooser_get_region (CcFormatChooser *chooser);
+void cc_format_chooser_set_region (CcFormatChooser *chooser,
+ const gchar *region);
+
+G_END_DECLS
diff --git a/panels/region/cc-format-chooser.ui b/panels/region/cc-format-chooser.ui
new file mode 100644
index 0000000..35cbd8a
--- /dev/null
+++ b/panels/region/cc-format-chooser.ui
@@ -0,0 +1,463 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <!-- interface-requires gtk+ 3.0 -->
+ <template class="CcFormatChooser" parent="GtkDialog">
+ <property name="can_focus">False</property>
+ <property name="title" translatable="yes">Formats</property>
+ <property name="type_hint">dialog</property>
+ <property name="modal">True</property>
+ <property name="default-width">700</property>
+ <property name="default-height">500</property>
+
+ <!-- Titlebar -->
+ <child type="titlebar">
+ <object class="GtkHeaderBar" id="title_bar">
+ <property name="visible">1</property>
+
+ <child>
+ <object class="GtkStack" id="title_buttons">
+ <property name="visible">1</property>
+ <property name="homogeneous">0</property>
+
+ <!-- Cancel button -->
+ <child>
+ <object class="GtkButton" id="cancel_button">
+ <property name="visible">1</property>
+ <property name="valign">center</property>
+ <property name="use-underline">1</property>
+ <property name="label" translatable="yes">_Cancel</property>
+ </object>
+ </child>
+
+ <!-- Back button -->
+ <child>
+ <object class="GtkButton" id="back_button">
+ <property name="visible">1</property>
+ <signal name="clicked" handler="format_chooser_back_button_clicked_cb" object="CcFormatChooser" swapped="yes" />
+ <style>
+ <class name="image-button" />
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">1</property>
+ <property name="icon_name">go-previous-symbolic</property>
+ </object>
+ </child>
+ <child internal-child="accessible">
+ <object class="AtkObject">
+ <property name="accessible-name" translatable="yes">Back</property>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ </object>
+ </child>
+
+ <child>
+ <object class="GtkButton" id="done_button">
+ <property name="visible">1</property>
+ <property name="can-default">1</property>
+ <property name="receives-default">1</property>
+ <property name="valign">center</property>
+ <property name="use-underline">1</property>
+ <property name="label" translatable="yes">_Done</property>
+ <style>
+ <class name="suggested-action" />
+ </style>
+ </object>
+ <packing>
+ <property name="pack-type">end</property>
+ </packing>
+ </child>
+
+ </object>
+ </child>
+ <child internal-child="vbox">
+ <object class="GtkBox" id="dialog-vbox1">
+ <property name="can_focus">False</property>
+ <property name="border-width">0</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="HdyLeaflet" id="main_leaflet">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="hexpand">True</property>
+ <property name="vexpand">True</property>
+ <signal name="notify::fold" handler="format_chooser_leaflet_fold_changed_cb" object="CcFormatChooser" swapped="yes"/>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="margin-top">18</property>
+ <property name="can_focus">False</property>
+ <property name="vexpand">True</property>
+ <property name="orientation">vertical</property>
+
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="margin-start">18</property>
+ <property name="margin-end">18</property>
+ <property name="label" translatable="yes">Choose the format for numbers, dates and currencies. Changes take effect on next login.</property>
+ <property name="wrap">True</property>
+ <property name="max-width-chars">32</property>
+ <property name="xalign">0.0</property>
+ <style>
+ <class name="dim-label" />
+ </style>
+ </object>
+ </child>
+
+ <!-- Language search -->
+ <child>
+ <object class="GtkSearchEntry" id="region_filter_entry">
+ <property name="visible">True</property>
+ <property name="hexpand">True</property>
+ <property name="can_focus">True</property>
+ <property name="placeholder-text" translatable="yes">Search locales...</property>
+ <property name="margin-top">12</property>
+ <property name="margin-bottom">18</property>
+ <property name="margin-start">18</property>
+ <property name="margin-end">18</property>
+ <signal name="search-changed" handler="filter_changed" object="CcFormatChooser" swapped="yes" />
+ </object>
+ </child>
+
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">1</property>
+ <style>
+ <class name="sidebar" />
+ </style>
+ </object>
+ </child>
+
+ <!-- Language format list -->
+ <child>
+ <object class="GtkStack" id="region_list_stack">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkScrolledWindow" id="region_list">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="vexpand">True</property>
+ <property name="hscrollbar_policy">never</property>
+ <property name="vscrollbar_policy">automatic</property>
+ <property name="propagate-natural-height">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin">18</property>
+ <property name="orientation">vertical</property>
+
+ <!-- Common formats title -->
+ <child>
+ <object class="GtkLabel" id="common_region_title">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Common Formats</property>
+ <property name="halign">start</property>
+ <property name="margin-bottom">6</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ <style>
+ <class name="dim-label" />
+ </style>
+ </object>
+ </child>
+
+ <!-- Common formats list -->
+ <child>
+ <object class="GtkListBox" id="common_region_listbox">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="vexpand">True</property>
+ <property name="margin-bottom">24</property>
+ <property name="selection-mode">none</property>
+ <signal name="row-activated" handler="row_activated" object="CcFormatChooser" swapped="yes" />
+ <style>
+ <class name="frame" />
+ </style>
+ </object>
+ </child>
+
+ <!-- Complete formats title -->
+ <child>
+ <object class="GtkLabel" id="region_title">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">All Formats</property>
+ <property name="halign">start</property>
+ <property name="margin-bottom">6</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ <style>
+ <class name="dim-label" />
+ </style>
+ </object>
+ </child>
+
+ <!-- Complete formats list -->
+ <child>
+ <object class="GtkListBox" id="region_listbox">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="vexpand">True</property>
+ <property name="halign">fill</property>
+ <property name="valign">start</property>
+ <property name="selection-mode">none</property>
+ <signal name="row-activated" handler="row_activated" object="CcFormatChooser" swapped="yes" />
+ <style>
+ <class name="frame" />
+ </style>
+ </object>
+ </child>
+
+ </object>
+ </child>
+ </object>
+ </child>
+
+ <!-- Empty search results view -->
+ <child>
+ <object class="GtkBox" id="empty_results_view">
+ <property name="visible">True</property>
+ <property name="valign">center</property>
+ <property name="halign">center</property>
+ <property name="hexpand">False</property>
+ <property name="margin-bottom">18</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">system-search-symbolic</property>
+ <property name="pixel-size">64</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">No Search Results</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ <attribute name="scale" value="1.2"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="wrap">True</property>
+ <property name="max-width-chars">24</property>
+ <property name="justify">center</property>
+ <property name="label" translatable="yes">Searches can be for countries or languages.</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ </object>
+ </child>
+
+ </object>
+ <packing>
+ <property name="name">region-list</property>
+ </packing>
+ </child>
+
+ <child>
+ <object class="GtkSeparator" id="panel_separator">
+ <property name="visible">1</property>
+ <property name="orientation">vertical</property>
+ <style>
+ <class name="sidebar" />
+ </style>
+ </object>
+ </child>
+
+ <!-- Format preview -->
+ <child>
+ <object class="GtkBox">
+ <property name="visible">1</property>
+ <property name="width-request">300</property>
+ <style>
+ <class name="view" />
+ </style>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin">24</property>
+ <property name="hexpand">True</property>
+ <property name="spacing">4</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_bottom">6</property>
+ <property name="halign">center</property>
+ <property name="hexpand">True</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Preview</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ <attribute name="scale" value="1.18" />
+ </attributes>
+ </object>
+ </child>
+
+ <!-- Dates -->
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="margin-top">18</property>
+ <property name="label" translatable="yes">Dates</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="date_format_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label">23 January 2013</property>
+ </object>
+ </child>
+
+ <!-- Times -->
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="margin-top">18</property>
+ <property name="label" translatable="yes">Times</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="time_format_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label">11:31 AM</property>
+ </object>
+ </child>
+
+ <!-- Date & Times -->
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="margin-top">18</property>
+ <property name="label" translatable="yes">Dates &amp; Times</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="date_time_format_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label">Sun Wed 2 11:31:00 KST 2013</property>
+ </object>
+ </child>
+
+ <!-- Numbers -->
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="margin-top">18</property>
+ <property name="label" translatable="yes">Numbers</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="number_format_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label">123,456,789.00</property>
+ </object>
+ </child>
+
+ <!-- Measurement -->
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="margin-top">18</property>
+ <property name="label" translatable="yes">Measurement</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="measurement_format_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label">Metric</property>
+ </object>
+ </child>
+
+ <!-- Paper -->
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="margin-top">18</property>
+ <property name="label" translatable="yes">Paper</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="paper_format_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label">A4</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">preview</property>
+ </packing>
+ </child>
+
+ </object>
+ </child>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="-5" default="true">done_button</action-widget>
+ <action-widget response="-6">cancel_button</action-widget>
+ </action-widgets>
+ </template>
+</interface>
diff --git a/panels/region/cc-ibus-utils.c b/panels/region/cc-ibus-utils.c
new file mode 100644
index 0000000..424c69e
--- /dev/null
+++ b/panels/region/cc-ibus-utils.c
@@ -0,0 +1,43 @@
+/*
+ * 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/>.
+ */
+
+#include <config.h>
+
+#ifdef HAVE_IBUS
+#include "cc-ibus-utils.h"
+
+gchar *
+engine_get_display_name (IBusEngineDesc *engine_desc)
+{
+ const gchar *name;
+ const gchar *language_code;
+ const gchar *language;
+ const gchar *textdomain;
+ gchar *display_name;
+
+ name = ibus_engine_desc_get_longname (engine_desc);
+ language_code = ibus_engine_desc_get_language (engine_desc);
+ language = ibus_get_language_name (language_code);
+ textdomain = ibus_engine_desc_get_textdomain (engine_desc);
+ if (*textdomain != '\0' && *name != '\0')
+ name = g_dgettext (textdomain, name);
+ display_name = g_strdup_printf ("%s (%s)", language, name);
+
+ return display_name;
+}
+
+#endif /* HAVE_IBUS */
diff --git a/panels/region/cc-ibus-utils.h b/panels/region/cc-ibus-utils.h
new file mode 100644
index 0000000..20a0516
--- /dev/null
+++ b/panels/region/cc-ibus-utils.h
@@ -0,0 +1,26 @@
+/*
+ * 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/>.
+ */
+
+#pragma once
+
+#include <ibus.h>
+
+G_BEGIN_DECLS
+
+gchar *engine_get_display_name (IBusEngineDesc *engine_desc);
+
+G_END_DECLS
diff --git a/panels/region/cc-input-chooser.c b/panels/region/cc-input-chooser.c
new file mode 100644
index 0000000..74c8e0c
--- /dev/null
+++ b/panels/region/cc-input-chooser.c
@@ -0,0 +1,1102 @@
+/*
+ * Copyright © 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/>.
+ */
+
+#include <config.h>
+#include <locale.h>
+#include <glib/gi18n.h>
+
+#define GNOME_DESKTOP_USE_UNSTABLE_API
+#include <libgnome-desktop/gnome-languages.h>
+
+#include "list-box-helper.h"
+#include "cc-common-language.h"
+#include "cc-util.h"
+#include "cc-input-chooser.h"
+#include "cc-input-source-ibus.h"
+#include "cc-input-source-xkb.h"
+
+#ifdef HAVE_IBUS
+#include <ibus.h>
+#include "cc-ibus-utils.h"
+#endif /* HAVE_IBUS */
+
+#define INPUT_SOURCE_TYPE_XKB "xkb"
+#define INPUT_SOURCE_TYPE_IBUS "ibus"
+
+#define FILTER_TIMEOUT 150 /* ms */
+
+typedef enum
+{
+ ROW_TRAVEL_DIRECTION_NONE,
+ ROW_TRAVEL_DIRECTION_FORWARD,
+ ROW_TRAVEL_DIRECTION_BACKWARD
+} RowTravelDirection;
+
+typedef enum
+{
+ ROW_LABEL_POSITION_START,
+ ROW_LABEL_POSITION_CENTER,
+ ROW_LABEL_POSITION_END
+} RowLabelPosition;
+
+struct _CcInputChooser
+{
+ GtkDialog parent_instance;
+
+ GtkButton *add_button;
+ GtkSearchEntry *filter_entry;
+ GtkListBox *input_sources_listbox;
+ GtkLabel *login_label;
+ GtkListBoxRow *more_row;
+ GtkWidget *no_results;
+ GtkAdjustment *scroll_adjustment;
+
+ GnomeXkbInfo *xkb_info;
+ GHashTable *ibus_engines;
+ GHashTable *locales;
+ GHashTable *locales_by_language;
+ gboolean showing_extra;
+ guint filter_timeout_id;
+ gchar **filter_words;
+
+ gboolean is_login;
+};
+
+G_DEFINE_TYPE (CcInputChooser, cc_input_chooser, GTK_TYPE_DIALOG)
+
+typedef struct
+{
+ gchar *id;
+ gchar *name;
+ gchar *unaccented_name;
+ gchar *untranslated_name;
+ GtkListBoxRow *default_input_source_row;
+ GtkListBoxRow *locale_row;
+ GtkListBoxRow *back_row;
+ GHashTable *layout_rows_by_id;
+ GHashTable *engine_rows_by_id;
+} LocaleInfo;
+
+static void
+locale_info_free (gpointer data)
+{
+ LocaleInfo *info = data;
+
+ g_free (info->id);
+ g_free (info->name);
+ g_free (info->unaccented_name);
+ g_free (info->untranslated_name);
+ g_clear_object (&info->default_input_source_row);
+ g_clear_object (&info->locale_row);
+ g_clear_object (&info->back_row);
+ g_hash_table_destroy (info->layout_rows_by_id);
+ g_hash_table_destroy (info->engine_rows_by_id);
+ g_free (info);
+}
+
+static void
+set_row_widget_margins (GtkWidget *widget)
+{
+ gtk_widget_set_margin_start (widget, 20);
+ gtk_widget_set_margin_end (widget, 20);
+ gtk_widget_set_margin_top (widget, 6);
+ gtk_widget_set_margin_bottom (widget, 6);
+}
+
+static GtkWidget *
+padded_label_new (const gchar *text,
+ RowLabelPosition position,
+ RowTravelDirection direction,
+ gboolean dim_label)
+{
+ GtkWidget *widget;
+ GtkWidget *label;
+ GtkWidget *arrow;
+ GtkAlign alignment;
+
+ if (position == ROW_LABEL_POSITION_START)
+ alignment = GTK_ALIGN_START;
+ else if (position == ROW_LABEL_POSITION_CENTER)
+ alignment = GTK_ALIGN_CENTER;
+ else
+ alignment = GTK_ALIGN_END;
+
+ widget = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
+
+ if (direction == ROW_TRAVEL_DIRECTION_BACKWARD)
+ {
+ arrow = gtk_image_new_from_icon_name ("go-previous-symbolic", GTK_ICON_SIZE_MENU);
+ gtk_widget_show (arrow);
+ gtk_container_add (GTK_CONTAINER (widget), arrow);
+ }
+
+ label = gtk_label_new (text);
+ gtk_widget_show (label);
+ gtk_label_set_ellipsize (GTK_LABEL (label), PANGO_ELLIPSIZE_MIDDLE);
+ gtk_widget_set_hexpand (label, TRUE);
+ gtk_widget_set_halign (label, alignment);
+ set_row_widget_margins (label);
+ gtk_container_add (GTK_CONTAINER (widget), label);
+ if (dim_label)
+ gtk_style_context_add_class (gtk_widget_get_style_context (label), "dim-label");
+
+ if (direction == ROW_TRAVEL_DIRECTION_FORWARD)
+ {
+ arrow = gtk_image_new_from_icon_name ("go-next-symbolic", GTK_ICON_SIZE_MENU);
+ gtk_widget_show (arrow);
+ gtk_container_add (GTK_CONTAINER (widget), arrow);
+ }
+
+ return widget;
+}
+
+static GtkListBoxRow *
+more_row_new (void)
+{
+ GtkWidget *row;
+ GtkWidget *box;
+ GtkWidget *arrow;
+
+ row = gtk_list_box_row_new ();
+ box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_widget_show (box);
+ gtk_container_add (GTK_CONTAINER (row), box);
+ gtk_widget_set_tooltip_text (row, _("More…"));
+
+ arrow = gtk_image_new_from_icon_name ("view-more-symbolic", GTK_ICON_SIZE_MENU);
+ gtk_widget_show (arrow);
+ gtk_style_context_add_class (gtk_widget_get_style_context (arrow), "dim-label");
+ gtk_widget_set_hexpand (arrow, TRUE);
+ set_row_widget_margins (arrow);
+ gtk_container_add (GTK_CONTAINER (box), arrow);
+
+ return GTK_LIST_BOX_ROW (row);
+}
+
+static GtkWidget *
+no_results_widget_new (void)
+{
+ return padded_label_new (_("No input sources found"), ROW_LABEL_POSITION_CENTER, ROW_TRAVEL_DIRECTION_NONE, TRUE);
+}
+
+static GtkListBoxRow *
+back_row_new (const gchar *text)
+{
+ GtkWidget *row;
+ GtkWidget *widget;
+
+ row = gtk_list_box_row_new ();
+ widget = padded_label_new (text, ROW_LABEL_POSITION_CENTER, ROW_TRAVEL_DIRECTION_BACKWARD, TRUE);
+ gtk_widget_show (widget);
+ gtk_container_add (GTK_CONTAINER (row), widget);
+
+ return GTK_LIST_BOX_ROW (row);
+}
+
+static GtkListBoxRow *
+locale_row_new (const gchar *text)
+{
+ GtkWidget *row;
+ GtkWidget *widget;
+
+ row = gtk_list_box_row_new ();
+ widget = padded_label_new (text, ROW_LABEL_POSITION_CENTER, ROW_TRAVEL_DIRECTION_NONE, FALSE);
+ gtk_widget_show (widget);
+ gtk_container_add (GTK_CONTAINER (row), widget);
+
+ return GTK_LIST_BOX_ROW (row);
+}
+
+static GtkListBoxRow *
+input_source_row_new (CcInputChooser *self,
+ const gchar *type,
+ const gchar *id)
+{
+ GtkWidget *row = NULL;
+ GtkWidget *widget;
+
+ if (g_str_equal (type, INPUT_SOURCE_TYPE_XKB))
+ {
+ const gchar *display_name;
+
+ gnome_xkb_info_get_layout_info (self->xkb_info, id, &display_name, NULL, NULL, NULL);
+
+ row = gtk_list_box_row_new ();
+ widget = padded_label_new (display_name,
+ ROW_LABEL_POSITION_START,
+ ROW_TRAVEL_DIRECTION_NONE,
+ FALSE);
+ gtk_widget_show (widget);
+ gtk_container_add (GTK_CONTAINER (row), widget);
+ g_object_set_data (G_OBJECT (row), "name", (gpointer) display_name);
+ g_object_set_data_full (G_OBJECT (row), "unaccented-name",
+ cc_util_normalize_casefold_and_unaccent (display_name), g_free);
+ }
+ else if (g_str_equal (type, INPUT_SOURCE_TYPE_IBUS))
+ {
+#ifdef HAVE_IBUS
+ gchar *display_name;
+ GtkWidget *image;
+
+ display_name = engine_get_display_name (g_hash_table_lookup (self->ibus_engines, id));
+
+ row = gtk_list_box_row_new ();
+ widget = padded_label_new (display_name,
+ ROW_LABEL_POSITION_START,
+ ROW_TRAVEL_DIRECTION_NONE,
+ FALSE);
+ gtk_widget_show (widget);
+ gtk_container_add (GTK_CONTAINER (row), widget);
+ image = gtk_image_new_from_icon_name ("system-run-symbolic", GTK_ICON_SIZE_MENU);
+ gtk_widget_show (image);
+ set_row_widget_margins (image);
+ gtk_style_context_add_class (gtk_widget_get_style_context (image), "dim-label");
+ gtk_container_add (GTK_CONTAINER (widget), image);
+
+ g_object_set_data_full (G_OBJECT (row), "name", display_name, g_free);
+ g_object_set_data_full (G_OBJECT (row), "unaccented-name",
+ cc_util_normalize_casefold_and_unaccent (display_name), g_free);
+#else
+ widget = NULL;
+#endif /* HAVE_IBUS */
+ }
+
+ if (row)
+ {
+ g_object_set_data (G_OBJECT (row), "type", (gpointer) type);
+ g_object_set_data (G_OBJECT (row), "id", (gpointer) id);
+
+ return GTK_LIST_BOX_ROW (row);
+ }
+
+ return NULL;
+}
+
+static void
+remove_all_children (GtkContainer *container)
+{
+ g_autoptr(GList) list = NULL;
+ GList *l;
+
+ list = gtk_container_get_children (container);
+ for (l = list; l; l = l->next)
+ gtk_container_remove (container, (GtkWidget *) l->data);
+}
+
+static void
+add_input_source_rows_for_locale (CcInputChooser *self,
+ LocaleInfo *info)
+{
+ GtkWidget *row;
+ GHashTableIter iter;
+ const gchar *id;
+
+ if (info->default_input_source_row)
+ gtk_container_add (GTK_CONTAINER (self->input_sources_listbox), GTK_WIDGET (info->default_input_source_row));
+
+ g_hash_table_iter_init (&iter, info->layout_rows_by_id);
+ while (g_hash_table_iter_next (&iter, (gpointer *) &id, (gpointer *) &row))
+ gtk_container_add (GTK_CONTAINER (self->input_sources_listbox), row);
+
+ g_hash_table_iter_init (&iter, info->engine_rows_by_id);
+ while (g_hash_table_iter_next (&iter, (gpointer *) &id, (gpointer *) &row))
+ gtk_container_add (GTK_CONTAINER (self->input_sources_listbox), row);
+}
+
+static void
+show_input_sources_for_locale (CcInputChooser *self,
+ LocaleInfo *info)
+{
+ remove_all_children (GTK_CONTAINER (self->input_sources_listbox));
+
+ if (!info->back_row)
+ {
+ info->back_row = g_object_ref_sink (back_row_new (info->name));
+ gtk_widget_show (GTK_WIDGET (info->back_row));
+ g_object_set_data (G_OBJECT (info->back_row), "back", GINT_TO_POINTER (TRUE));
+ g_object_set_data (G_OBJECT (info->back_row), "locale-info", info);
+ }
+ gtk_container_add (GTK_CONTAINER (self->input_sources_listbox), GTK_WIDGET (info->back_row));
+
+ add_input_source_rows_for_locale (self, info);
+
+ gtk_adjustment_set_value (self->scroll_adjustment,
+ gtk_adjustment_get_lower (self->scroll_adjustment));
+ gtk_list_box_set_header_func (self->input_sources_listbox, cc_list_box_update_header_func, NULL, NULL);
+ gtk_list_box_invalidate_filter (self->input_sources_listbox);
+ gtk_list_box_set_selection_mode (self->input_sources_listbox, GTK_SELECTION_SINGLE);
+ gtk_list_box_set_activate_on_single_click (self->input_sources_listbox, FALSE);
+ gtk_list_box_unselect_all (self->input_sources_listbox);
+
+ if (gtk_widget_is_visible (GTK_WIDGET (self->filter_entry)) &&
+ !gtk_widget_is_focus (GTK_WIDGET (self->filter_entry)))
+ gtk_widget_grab_focus (GTK_WIDGET (self->filter_entry));
+}
+
+static gboolean
+is_current_locale (const gchar *locale)
+{
+ return g_strcmp0 (setlocale (LC_CTYPE, NULL), locale) == 0;
+}
+
+static void
+show_locale_rows (CcInputChooser *self)
+{
+ g_autoptr(GHashTable) initial = NULL;
+ LocaleInfo *info;
+ GHashTableIter iter;
+
+ remove_all_children (GTK_CONTAINER (self->input_sources_listbox));
+
+ if (!self->showing_extra)
+ initial = cc_common_language_get_initial_languages ();
+
+ g_hash_table_iter_init (&iter, self->locales);
+ while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &info))
+ {
+ if (!info->default_input_source_row &&
+ !g_hash_table_size (info->layout_rows_by_id) &&
+ !g_hash_table_size (info->engine_rows_by_id))
+ continue;
+
+ if (!info->locale_row)
+ {
+ info->locale_row = g_object_ref_sink (locale_row_new (info->name));
+ gtk_widget_show (GTK_WIDGET (info->locale_row));
+ g_object_set_data (G_OBJECT (info->locale_row), "locale-info", info);
+
+ if (!self->showing_extra &&
+ !g_hash_table_contains (initial, info->id) &&
+ !is_current_locale (info->id))
+ g_object_set_data (G_OBJECT (info->locale_row), "is-extra", GINT_TO_POINTER (TRUE));
+ }
+ gtk_container_add (GTK_CONTAINER (self->input_sources_listbox), GTK_WIDGET (info->locale_row));
+ }
+
+ gtk_container_add (GTK_CONTAINER (self->input_sources_listbox), GTK_WIDGET (self->more_row));
+
+ gtk_adjustment_set_value (self->scroll_adjustment,
+ gtk_adjustment_get_lower (self->scroll_adjustment));
+ gtk_list_box_set_header_func (self->input_sources_listbox, cc_list_box_update_header_func, NULL, NULL);
+ gtk_list_box_invalidate_filter (self->input_sources_listbox);
+ gtk_list_box_set_selection_mode (self->input_sources_listbox, GTK_SELECTION_NONE);
+ gtk_list_box_set_activate_on_single_click (self->input_sources_listbox, TRUE);
+
+ if (gtk_widget_is_visible (GTK_WIDGET (self->filter_entry)) &&
+ !gtk_widget_is_focus (GTK_WIDGET (self->filter_entry)))
+ gtk_widget_grab_focus (GTK_WIDGET (self->filter_entry));
+}
+
+static gint
+list_sort (GtkListBoxRow *a,
+ GtkListBoxRow *b,
+ gpointer data)
+{
+ CcInputChooser *self = data;
+ LocaleInfo *ia;
+ LocaleInfo *ib;
+ const gchar *la;
+ const gchar *lb;
+ gint retval;
+
+ /* Always goes at the end */
+ if (a == self->more_row)
+ return 1;
+ if (b == self->more_row)
+ return -1;
+
+ ia = g_object_get_data (G_OBJECT (a), "locale-info");
+ ib = g_object_get_data (G_OBJECT (b), "locale-info");
+
+ /* The "Other" locale always goes at the end */
+ if (!ia->id[0] && ib->id[0])
+ return 1;
+ else if (ia->id[0] && !ib->id[0])
+ return -1;
+
+ retval = g_strcmp0 (ia->name, ib->name);
+ if (retval)
+ return retval;
+
+ la = g_object_get_data (G_OBJECT (a), "name");
+ lb = g_object_get_data (G_OBJECT (b), "name");
+
+ /* Only input sources have a "name" property and they should always
+ go after their respective heading */
+ if (la && !lb)
+ return 1;
+ else if (!la && lb)
+ return -1;
+ else if (!la && !lb)
+ return 0; /* Shouldn't happen */
+
+ /* The default input source always goes first in its group */
+ if (g_object_get_data (G_OBJECT (a), "default"))
+ return -1;
+ if (g_object_get_data (G_OBJECT (b), "default"))
+ return 1;
+
+ return g_strcmp0 (la, lb);
+}
+
+static gboolean
+match_all (gchar **words,
+ const gchar *str)
+{
+ gchar **w;
+
+ for (w = words; *w; ++w)
+ if (!strstr (str, *w))
+ return FALSE;
+
+ return TRUE;
+}
+
+static gboolean
+match_source_in_table (gchar **words,
+ GHashTable *table)
+{
+ GHashTableIter iter;
+ gpointer row;
+ const gchar *source_name;
+
+ g_hash_table_iter_init (&iter, table);
+ while (g_hash_table_iter_next (&iter, NULL, &row))
+ {
+ source_name = g_object_get_data (G_OBJECT (row), "unaccented-name");
+ if (source_name && match_all (words, source_name))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static gboolean
+list_filter (GtkListBoxRow *row,
+ gpointer user_data)
+{
+ CcInputChooser *self = user_data;
+ LocaleInfo *info;
+ gboolean is_extra;
+ const gchar *source_name;
+
+ if (row == self->more_row)
+ return !self->showing_extra;
+
+ is_extra = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (row), "is-extra"));
+
+ if (!self->showing_extra && is_extra)
+ return FALSE;
+
+ if (!self->filter_words)
+ return TRUE;
+
+ info = g_object_get_data (G_OBJECT (row), "locale-info");
+
+ if (row == info->back_row)
+ return TRUE;
+
+ if (match_all (self->filter_words, info->unaccented_name))
+ return TRUE;
+
+ if (match_all (self->filter_words, info->untranslated_name))
+ return TRUE;
+
+ source_name = g_object_get_data (G_OBJECT (row), "unaccented-name");
+ if (source_name)
+ {
+ if (match_all (self->filter_words, source_name))
+ return TRUE;
+ }
+ else
+ {
+ if (match_source_in_table (self->filter_words, info->layout_rows_by_id))
+ return TRUE;
+ if (match_source_in_table (self->filter_words, info->engine_rows_by_id))
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static gboolean
+strvs_differ (gchar **av,
+ gchar **bv)
+{
+ gchar **a, **b;
+
+ for (a = av, b = bv; *a && *b; ++a, ++b)
+ if (!g_str_equal (*a, *b))
+ return TRUE;
+
+ if (*a == NULL && *b == NULL)
+ return FALSE;
+
+ return TRUE;
+}
+
+static gboolean
+do_filter (CcInputChooser *self)
+{
+ g_auto(GStrv) previous_words = NULL;
+ g_autofree gchar *filter_contents = NULL;
+
+ self->filter_timeout_id = 0;
+
+ filter_contents =
+ cc_util_normalize_casefold_and_unaccent (gtk_entry_get_text (GTK_ENTRY (self->filter_entry)));
+
+ previous_words = self->filter_words;
+ self->filter_words = g_strsplit_set (g_strstrip (filter_contents), " ", 0);
+
+ if (!self->filter_words[0])
+ {
+ gtk_list_box_invalidate_filter (self->input_sources_listbox);
+ gtk_list_box_set_placeholder (self->input_sources_listbox, NULL);
+ }
+ else if (previous_words == NULL || strvs_differ (self->filter_words, previous_words))
+ {
+ gtk_list_box_invalidate_filter (self->input_sources_listbox);
+ gtk_list_box_set_placeholder (self->input_sources_listbox, self->no_results);
+ }
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+on_filter_entry_search_changed_cb (CcInputChooser *self)
+{
+ if (self->filter_timeout_id == 0)
+ self->filter_timeout_id = g_timeout_add (FILTER_TIMEOUT, (GSourceFunc) do_filter, self);
+}
+
+static void
+show_more (CcInputChooser *self)
+{
+ gtk_widget_show (GTK_WIDGET (self->filter_entry));
+ gtk_widget_grab_focus (GTK_WIDGET (self->filter_entry));
+
+ self->showing_extra = TRUE;
+
+ gtk_list_box_invalidate_filter (self->input_sources_listbox);
+}
+
+static void
+on_input_sources_listbox_row_activated_cb (CcInputChooser *self, GtkListBoxRow *row)
+{
+ gpointer data;
+
+ if (!row)
+ return;
+
+ if (row == self->more_row)
+ {
+ show_more (self);
+ return;
+ }
+
+ data = g_object_get_data (G_OBJECT (row), "back");
+ if (data)
+ {
+ show_locale_rows (self);
+ return;
+ }
+
+ data = g_object_get_data (G_OBJECT (row), "name");
+ if (data)
+ {
+ if (gtk_widget_is_sensitive (GTK_WIDGET (self->add_button)))
+ gtk_dialog_response (GTK_DIALOG (self),
+ gtk_dialog_get_response_for_widget (GTK_DIALOG (self),
+ GTK_WIDGET (self->add_button)));
+ return;
+ }
+
+ data = g_object_get_data (G_OBJECT (row), "locale-info");
+ if (data)
+ {
+ show_input_sources_for_locale (self, (LocaleInfo *) data);
+ return;
+ }
+}
+
+static void
+on_input_sources_listbox_selected_rows_changed_cb (CcInputChooser *self)
+{
+ gboolean sensitive = TRUE;
+ GtkListBoxRow *row;
+
+ row = gtk_list_box_get_selected_row (self->input_sources_listbox);
+ if (!row || g_object_get_data (G_OBJECT (row), "back"))
+ sensitive = FALSE;
+
+ gtk_widget_set_sensitive (GTK_WIDGET (self->add_button), sensitive);
+}
+
+static gboolean
+on_input_sources_listbox_button_release_event_cb (CcInputChooser *self, GdkEvent *event)
+{
+ gdouble x, y;
+ GtkListBoxRow *row;
+
+ gdk_event_get_coords (event, &x, &y);
+ row = gtk_list_box_get_row_at_y (self->input_sources_listbox, y);
+ if (row && g_object_get_data (G_OBJECT (row), "back"))
+ {
+ g_signal_emit_by_name (row, "activate", NULL);
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static void
+add_default_row (CcInputChooser *self,
+ LocaleInfo *info,
+ const gchar *type,
+ const gchar *id)
+{
+ info->default_input_source_row = input_source_row_new (self, type, id);
+ if (info->default_input_source_row)
+ {
+ gtk_widget_show (GTK_WIDGET (info->default_input_source_row));
+ g_object_ref_sink (GTK_WIDGET (info->default_input_source_row));
+ g_object_set_data (G_OBJECT (info->default_input_source_row), "default", GINT_TO_POINTER (TRUE));
+ g_object_set_data (G_OBJECT (info->default_input_source_row), "locale-info", info);
+ }
+}
+
+static void
+add_rows_to_table (CcInputChooser *self,
+ LocaleInfo *info,
+ GList *list,
+ const gchar *type,
+ const gchar *default_id)
+{
+ GHashTable *table;
+ GtkListBoxRow *row;
+ const gchar *id;
+
+ if (g_str_equal (type, INPUT_SOURCE_TYPE_XKB))
+ table = info->layout_rows_by_id;
+ else if (g_str_equal (type, INPUT_SOURCE_TYPE_IBUS))
+ table = info->engine_rows_by_id;
+ else
+ return;
+
+ while (list)
+ {
+ id = (const gchar *) list->data;
+
+ /* The widget for the default input source lives elsewhere */
+ if (g_strcmp0 (id, default_id))
+ {
+ row = input_source_row_new (self, type, id);
+ gtk_widget_show (GTK_WIDGET (row));
+ if (row)
+ {
+ g_object_set_data (G_OBJECT (row), "locale-info", info);
+ g_hash_table_replace (table, (gpointer) id, g_object_ref_sink (row));
+ }
+ }
+ list = list->next;
+ }
+}
+
+static void
+add_row (CcInputChooser *self,
+ LocaleInfo *info,
+ const gchar *type,
+ const gchar *id)
+{
+ GList tmp = { 0 };
+ tmp.data = (gpointer) id;
+ add_rows_to_table (self, info, &tmp, type, NULL);
+}
+
+static void
+add_row_other (CcInputChooser *self,
+ const gchar *type,
+ const gchar *id)
+{
+ LocaleInfo *info = g_hash_table_lookup (self->locales, "");
+ add_row (self, info, type, id);
+}
+
+#ifdef HAVE_IBUS
+static gboolean
+maybe_set_as_default (CcInputChooser *self,
+ LocaleInfo *info,
+ const gchar *engine_id)
+{
+ const gchar *type, *id;
+
+ if (!gnome_get_input_source_from_locale (info->id, &type, &id))
+ return FALSE;
+
+ if (g_str_equal (type, INPUT_SOURCE_TYPE_IBUS) &&
+ g_str_equal (id, engine_id) &&
+ info->default_input_source_row == NULL)
+ {
+ add_default_row (self, info, type, id);
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static void
+get_ibus_locale_infos (CcInputChooser *self)
+{
+ GHashTableIter iter;
+ LocaleInfo *info;
+ const gchar *engine_id;
+ IBusEngineDesc *engine;
+
+ if (!self->ibus_engines || self->is_login)
+ return;
+
+ g_hash_table_iter_init (&iter, self->ibus_engines);
+ while (g_hash_table_iter_next (&iter, (gpointer *) &engine_id, (gpointer *) &engine))
+ {
+ g_autofree gchar *lang_code = NULL;
+ g_autofree gchar *country_code = NULL;
+ const gchar *ibus_locale = ibus_engine_desc_get_language (engine);
+
+ if (gnome_parse_locale (ibus_locale, &lang_code, &country_code, NULL, NULL) &&
+ lang_code != NULL &&
+ country_code != NULL)
+ {
+ g_autofree gchar *locale = g_strdup_printf ("%s_%s.UTF-8", lang_code, country_code);
+
+ info = g_hash_table_lookup (self->locales, locale);
+ if (info)
+ {
+ const gchar *type, *id;
+
+ if (gnome_get_input_source_from_locale (locale, &type, &id) &&
+ g_str_equal (type, INPUT_SOURCE_TYPE_IBUS) &&
+ g_str_equal (id, engine_id))
+ {
+ add_default_row (self, info, type, id);
+ }
+ else
+ {
+ add_row (self, info, INPUT_SOURCE_TYPE_IBUS, engine_id);
+ }
+ }
+ else
+ {
+ add_row_other (self, INPUT_SOURCE_TYPE_IBUS, engine_id);
+ }
+ }
+ else if (lang_code != NULL)
+ {
+ GHashTableIter iter;
+ GHashTable *locales_for_language;
+ g_autofree gchar *language = NULL;
+
+ /* Most IBus engines only specify the language so we try to
+ add them to all locales for that language. */
+
+ language = gnome_get_language_from_code (lang_code, NULL);
+ if (language)
+ locales_for_language = g_hash_table_lookup (self->locales_by_language, language);
+ else
+ locales_for_language = NULL;
+
+ if (locales_for_language)
+ {
+ g_hash_table_iter_init (&iter, locales_for_language);
+ while (g_hash_table_iter_next (&iter, (gpointer *) &info, NULL))
+ if (!maybe_set_as_default (self, info, engine_id))
+ add_row (self, info, INPUT_SOURCE_TYPE_IBUS, engine_id);
+ }
+ else
+ {
+ add_row_other (self, INPUT_SOURCE_TYPE_IBUS, engine_id);
+ }
+ }
+ else
+ {
+ add_row_other (self, INPUT_SOURCE_TYPE_IBUS, engine_id);
+ }
+ }
+}
+#endif /* HAVE_IBUS */
+
+static void
+add_locale_to_table (GHashTable *table,
+ const gchar *lang_code,
+ LocaleInfo *info)
+{
+ GHashTable *set;
+ g_autofree gchar *language = NULL;
+
+ language = gnome_get_language_from_code (lang_code, NULL);
+
+ set = g_hash_table_lookup (table, language);
+ if (!set)
+ {
+ set = g_hash_table_new (NULL, NULL);
+ g_hash_table_replace (table, g_strdup (language), set);
+ }
+ g_hash_table_add (set, info);
+}
+
+static void
+add_ids_to_set (GHashTable *set,
+ GList *list)
+{
+ while (list)
+ {
+ g_hash_table_add (set, list->data);
+ list = list->next;
+ }
+}
+
+static void
+get_locale_infos (CcInputChooser *self)
+{
+ g_autoptr(GHashTable) layouts_with_locale = NULL;
+ LocaleInfo *info;
+ g_auto(GStrv) locale_ids = NULL;
+ gchar **locale;
+ g_autoptr(GList) all_layouts = NULL;
+ GList *l;
+
+ self->locales = g_hash_table_new_full (g_str_hash, g_str_equal,
+ g_free, locale_info_free);
+ self->locales_by_language = g_hash_table_new_full (g_str_hash, g_str_equal,
+ g_free, (GDestroyNotify) g_hash_table_unref);
+
+ layouts_with_locale = g_hash_table_new (g_str_hash, g_str_equal);
+
+ locale_ids = gnome_get_all_locales ();
+ for (locale = locale_ids; *locale; ++locale)
+ {
+ g_autofree gchar *lang_code = NULL;
+ g_autofree gchar *country_code = NULL;
+ g_autofree gchar *simple_locale = NULL;
+ g_autofree gchar *tmp = NULL;
+ const gchar *type = NULL;
+ const gchar *id = NULL;
+ g_autoptr(GList) language_layouts = NULL;
+
+ if (!gnome_parse_locale (*locale, &lang_code, &country_code, NULL, NULL))
+ continue;
+
+ if (country_code != NULL)
+ simple_locale = g_strdup_printf ("%s_%s.UTF-8", lang_code, country_code);
+ else
+ simple_locale = g_strdup_printf ("%s.UTF-8", lang_code);
+
+ if (g_hash_table_contains (self->locales, simple_locale))
+ continue;
+
+ info = g_new0 (LocaleInfo, 1);
+ info->id = g_strdup (simple_locale);
+ info->name = gnome_get_language_from_locale (simple_locale, NULL);
+ info->unaccented_name = cc_util_normalize_casefold_and_unaccent (info->name);
+ tmp = gnome_get_language_from_locale (simple_locale, "C");
+ info->untranslated_name = cc_util_normalize_casefold_and_unaccent (tmp);
+
+ g_hash_table_replace (self->locales, g_strdup (simple_locale), info);
+ add_locale_to_table (self->locales_by_language, lang_code, info);
+
+ if (gnome_get_input_source_from_locale (simple_locale, &type, &id) &&
+ g_str_equal (type, INPUT_SOURCE_TYPE_XKB))
+ {
+ add_default_row (self, info, type, id);
+ g_hash_table_add (layouts_with_locale, (gpointer) id);
+ }
+
+ /* We don't own these ids */
+ info->layout_rows_by_id = g_hash_table_new_full (g_str_hash, g_str_equal,
+ NULL, g_object_unref);
+ info->engine_rows_by_id = g_hash_table_new_full (g_str_hash, g_str_equal,
+ NULL, g_object_unref);
+
+ language_layouts = gnome_xkb_info_get_layouts_for_language (self->xkb_info, lang_code);
+ add_rows_to_table (self, info, language_layouts, INPUT_SOURCE_TYPE_XKB, id);
+ add_ids_to_set (layouts_with_locale, language_layouts);
+
+ if (country_code != NULL)
+ {
+ g_autoptr(GList) country_layouts = gnome_xkb_info_get_layouts_for_country (self->xkb_info, country_code);
+ add_rows_to_table (self, info, country_layouts, INPUT_SOURCE_TYPE_XKB, id);
+ add_ids_to_set (layouts_with_locale, country_layouts);
+ }
+ }
+
+ /* Add a "Other" locale to hold the remaining input sources */
+ info = g_new0 (LocaleInfo, 1);
+ info->id = g_strdup ("");
+ info->name = g_strdup (C_("Input Source", "Other"));
+ info->unaccented_name = g_strdup ("");
+ info->untranslated_name = g_strdup ("");
+ g_hash_table_replace (self->locales, g_strdup (info->id), info);
+
+ info->layout_rows_by_id = g_hash_table_new_full (g_str_hash, g_str_equal,
+ NULL, g_object_unref);
+ info->engine_rows_by_id = g_hash_table_new_full (g_str_hash, g_str_equal,
+ NULL, g_object_unref);
+
+ all_layouts = gnome_xkb_info_get_all_layouts (self->xkb_info);
+ for (l = all_layouts; l; l = l->next)
+ if (!g_hash_table_contains (layouts_with_locale, l->data))
+ add_row_other (self, INPUT_SOURCE_TYPE_XKB, l->data);
+}
+
+static gboolean
+on_filter_entry_key_release_event_cb (CcInputChooser *self, GdkEventKey *event)
+{
+ if (event->keyval == GDK_KEY_Escape) {
+ self->showing_extra = FALSE;
+ gtk_entry_set_text (GTK_ENTRY (self->filter_entry), "");
+ gtk_widget_hide (GTK_WIDGET (self->filter_entry));
+ g_clear_pointer (&self->filter_words, g_strfreev);
+ show_locale_rows (self);
+ }
+
+ return FALSE;
+}
+
+static void
+cc_input_chooser_dispose (GObject *object)
+{
+ CcInputChooser *self = CC_INPUT_CHOOSER (object);
+
+ g_clear_object (&self->more_row);
+ g_clear_object (&self->no_results);
+ g_clear_object (&self->xkb_info);
+ g_clear_pointer (&self->ibus_engines, g_hash_table_unref);
+ g_clear_pointer (&self->locales, g_hash_table_unref);
+ g_clear_pointer (&self->locales_by_language, g_hash_table_unref);
+ g_clear_pointer (&self->filter_words, g_strfreev);
+ g_clear_handle_id (&self->filter_timeout_id, g_source_remove);
+
+ G_OBJECT_CLASS (cc_input_chooser_parent_class)->dispose (object);
+}
+
+void
+cc_input_chooser_class_init (CcInputChooserClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = cc_input_chooser_dispose;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/region/cc-input-chooser.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcInputChooser, add_button);
+ gtk_widget_class_bind_template_child (widget_class, CcInputChooser, filter_entry);
+ gtk_widget_class_bind_template_child (widget_class, CcInputChooser, input_sources_listbox);
+ gtk_widget_class_bind_template_child (widget_class, CcInputChooser, login_label);
+ gtk_widget_class_bind_template_child (widget_class, CcInputChooser, scroll_adjustment);
+
+ gtk_widget_class_bind_template_callback (widget_class, on_input_sources_listbox_row_activated_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_input_sources_listbox_selected_rows_changed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_input_sources_listbox_button_release_event_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_filter_entry_search_changed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_filter_entry_key_release_event_cb);
+}
+
+void
+cc_input_chooser_init (CcInputChooser *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+CcInputChooser *
+cc_input_chooser_new (gboolean is_login,
+ GnomeXkbInfo *xkb_info,
+ GHashTable *ibus_engines)
+{
+ CcInputChooser *self;
+ g_autoptr(GError) error = NULL;
+
+ self = g_object_new (CC_TYPE_INPUT_CHOOSER,
+ "use-header-bar", 1,
+ NULL);
+
+ self->is_login = is_login;
+ self->xkb_info = g_object_ref (xkb_info);
+ if (ibus_engines)
+ self->ibus_engines = g_hash_table_ref (ibus_engines);
+
+ self->more_row = g_object_ref_sink (more_row_new ());
+ gtk_widget_show (GTK_WIDGET (self->more_row));
+ self->no_results = g_object_ref_sink (no_results_widget_new ());
+ gtk_widget_show (self->no_results);
+
+ gtk_list_box_set_filter_func (self->input_sources_listbox, list_filter, self, NULL);
+ gtk_list_box_set_sort_func (self->input_sources_listbox, list_sort, self, NULL);
+
+ if (self->is_login)
+ gtk_widget_show (GTK_WIDGET (self->login_label));
+
+ get_locale_infos (self);
+#ifdef HAVE_IBUS
+ get_ibus_locale_infos (self);
+#endif /* HAVE_IBUS */
+ show_locale_rows (self);
+
+ return self;
+}
+
+void
+cc_input_chooser_set_ibus_engines (CcInputChooser *self,
+ GHashTable *ibus_engines)
+{
+ g_return_if_fail (CC_IS_INPUT_CHOOSER (self));
+
+#ifdef HAVE_IBUS
+ /* This should only be called once when IBus shows up in case it
+ wasn't up yet when the user opened the input chooser dialog. */
+ g_return_if_fail (self->ibus_engines == NULL);
+
+ self->ibus_engines = ibus_engines;
+ get_ibus_locale_infos (self);
+ show_locale_rows (self);
+#endif /* HAVE_IBUS */
+}
+
+CcInputSource *
+cc_input_chooser_get_source (CcInputChooser *self)
+{
+ GtkListBoxRow *selected;
+ const gchar *t, *i;
+
+ g_return_val_if_fail (CC_IS_INPUT_CHOOSER (self), FALSE);
+
+ selected = gtk_list_box_get_selected_row (self->input_sources_listbox);
+ if (!selected)
+ return NULL;
+
+ t = g_object_get_data (G_OBJECT (selected), "type");
+ i = g_object_get_data (G_OBJECT (selected), "id");
+
+ if (!t || !i)
+ return FALSE;
+
+ if (g_strcmp0 (t, "xkb") == 0)
+ return CC_INPUT_SOURCE (cc_input_source_xkb_new_from_id (self->xkb_info, i));
+ else if (g_strcmp0 (t, "ibus") == 0)
+ return CC_INPUT_SOURCE (cc_input_source_ibus_new (i));
+ else
+ return NULL;
+}
diff --git a/panels/region/cc-input-chooser.h b/panels/region/cc-input-chooser.h
new file mode 100644
index 0000000..8363782
--- /dev/null
+++ b/panels/region/cc-input-chooser.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright © 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/>.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include "cc-input-source.h"
+
+#define GNOME_DESKTOP_USE_UNSTABLE_API
+#include <libgnome-desktop/gnome-xkb-info.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_INPUT_CHOOSER (cc_input_chooser_get_type ())
+G_DECLARE_FINAL_TYPE (CcInputChooser, cc_input_chooser, CC, INPUT_CHOOSER, GtkDialog)
+
+CcInputChooser *cc_input_chooser_new (gboolean is_login,
+ GnomeXkbInfo *xkb_info,
+ GHashTable *ibus_engines);
+
+void cc_input_chooser_set_ibus_engines (CcInputChooser *chooser,
+ GHashTable *ibus_engines);
+
+CcInputSource *cc_input_chooser_get_source (CcInputChooser *chooser);
+
+G_END_DECLS
diff --git a/panels/region/cc-input-chooser.ui b/panels/region/cc-input-chooser.ui
new file mode 100644
index 0000000..8d72357
--- /dev/null
+++ b/panels/region/cc-input-chooser.ui
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <!-- interface-requires gtk+ 3.0 -->
+ <template class="CcInputChooser" parent="GtkDialog">
+ <property name="title" translatable="yes">Add an Input Source</property>
+ <property name="modal">True</property>
+ <property name="destroy_with_parent">True</property>
+ <property name="resizable">True</property>
+ <child type="action">
+ <object class="GtkButton" id="cancel_button">
+ <property name="label" translatable="yes">_Cancel</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="use_underline">True</property>
+ <property name="valign">center</property>
+ </object>
+ </child>
+ <child type="action">
+ <object class="GtkButton" id="add_button">
+ <property name="label" translatable="yes">_Add</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="sensitive">False</property>
+ <property name="use_underline">True</property>
+ <property name="valign">center</property>
+ </object>
+ </child>
+ <child internal-child="vbox">
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">0</property>
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="visible">True</property>
+ <property name="hscrollbar-policy">never</property>
+ <property name="vscrollbar-policy">automatic</property>
+ <property name="propagate-natural-height">True</property>
+ <property name="shadow-type">in</property>
+ <property name="margin-start">6</property>
+ <property name="margin-end">6</property>
+ <property name="margin-top">6</property>
+ <property name="margin-bottom">6</property>
+ <property name="min-content-height">300</property>
+ <property name="vadjustment">scroll_adjustment</property>
+ <child>
+ <object class="GtkListBox" id="input_sources_listbox">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="vexpand">True</property>
+ <property name="halign">fill</property>
+ <property name="valign">fill</property>
+ <signal name="row-activated" handler="on_input_sources_listbox_row_activated_cb" object="CcInputChooser" swapped="yes" />
+ <signal name="selected-rows-changed" handler="on_input_sources_listbox_selected_rows_changed_cb" object="CcInputChooser" swapped="yes" />
+ <signal name="button-release-event" handler="on_input_sources_listbox_button_release_event_cb" object="CcInputChooser" swapped="yes" />
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSearchEntry" id="filter_entry">
+ <property name="visible">False</property>
+ <property name="hexpand">True</property>
+ <property name="margin-start">6</property>
+ <property name="margin-end">6</property>
+ <property name="margin-top">6</property>
+ <property name="margin-bottom">6</property>
+ <signal name="search-changed" handler="on_filter_entry_search_changed_cb" object="CcInputChooser" swapped="yes" />
+ <signal name="key-release-event" handler="on_filter_entry_key_release_event_cb" object="CcInputChooser" swapped="yes" />
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="login_label">
+ <property name="visible">False</property>
+ <property name="wrap">True</property>
+ <property name="label" translatable="yes">Input methods can’t be used on the login screen</property>
+ <property name="margin-start">6</property>
+ <property name="margin-end">6</property>
+ <property name="margin-bottom">6</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="-5" default="true">add_button</action-widget>
+ <action-widget response="-6">cancel_button</action-widget>
+ </action-widgets>
+ </template>
+ <object class="GtkAdjustment" id="scroll_adjustment">
+ </object>
+</interface>
diff --git a/panels/region/cc-input-row.c b/panels/region/cc-input-row.c
new file mode 100644
index 0000000..eadc204
--- /dev/null
+++ b/panels/region/cc-input-row.c
@@ -0,0 +1,273 @@
+/*
+ * Copyright © 2018 Canonical Ltd.
+ *
+ * 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/>.
+ */
+
+#include <config.h>
+#include "cc-input-row.h"
+#include "cc-input-source-ibus.h"
+
+struct _CcInputRow
+{
+ GtkListBoxRow parent_instance;
+
+ CcInputSource *source;
+
+ GtkEventBox *drag_handle;
+ GtkLabel *name_label;
+ GtkButton *remove_button;
+ GtkButton *settings_button;
+
+ GtkListBox *drag_widget;
+};
+
+G_DEFINE_TYPE (CcInputRow, cc_input_row, GTK_TYPE_LIST_BOX_ROW)
+
+enum
+{
+ SIGNAL_SHOW_SETTINGS,
+ SIGNAL_SHOW_LAYOUT,
+ SIGNAL_MOVE_ROW,
+ SIGNAL_REMOVE_ROW,
+ SIGNAL_LAST
+};
+
+static guint signals[SIGNAL_LAST] = { 0, };
+
+static void
+drag_begin_cb (CcInputRow *self,
+ GdkDragContext *drag_context)
+{
+ GtkAllocation alloc;
+ gint x = 0, y = 0;
+
+ gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
+
+ gdk_window_get_device_position (gtk_widget_get_window (GTK_WIDGET (self)),
+ gdk_drag_context_get_device (drag_context),
+ &x, &y, NULL);
+
+ self->drag_widget = GTK_LIST_BOX (gtk_list_box_new ());
+ gtk_widget_show (GTK_WIDGET (self->drag_widget));
+ gtk_widget_set_size_request (GTK_WIDGET (self->drag_widget), alloc.width, alloc.height);
+ CcInputRow *drag_row = cc_input_row_new (self->source);
+ gtk_widget_show (GTK_WIDGET (drag_row));
+ gtk_container_add (GTK_CONTAINER (self->drag_widget), GTK_WIDGET (drag_row));
+ gtk_list_box_drag_highlight_row (self->drag_widget, GTK_LIST_BOX_ROW (drag_row));
+
+ gtk_drag_set_icon_widget (drag_context, GTK_WIDGET (self->drag_widget), x - alloc.x, y - alloc.y);
+}
+
+static void
+drag_end_cb (CcInputRow *self)
+{
+ g_clear_pointer ((GtkWidget **) &self->drag_widget, gtk_widget_destroy);
+}
+
+static void
+drag_data_get_cb (CcInputRow *self,
+ GdkDragContext *context,
+ GtkSelectionData *selection_data,
+ guint info,
+ guint time_)
+{
+ gtk_selection_data_set (selection_data,
+ gdk_atom_intern_static_string ("GTK_LIST_BOX_ROW"),
+ 32,
+ (const guchar *)&self,
+ sizeof (gpointer));
+}
+
+static void
+drag_data_received_cb (CcInputRow *self,
+ GdkDragContext *context,
+ gint x,
+ gint y,
+ GtkSelectionData *selection_data,
+ guint info,
+ guint time_)
+{
+ CcInputRow *source;
+
+ source = *((CcInputRow **) gtk_selection_data_get_data (selection_data));
+
+ if (source == self)
+ return;
+
+ g_signal_emit (source,
+ signals[SIGNAL_MOVE_ROW],
+ 0,
+ self);
+}
+
+static void
+settings_button_clicked_cb (CcInputRow *self)
+{
+ g_signal_emit (self,
+ signals[SIGNAL_SHOW_SETTINGS],
+ 0);
+}
+
+static void
+layout_button_clicked_cb (CcInputRow *self)
+{
+ g_signal_emit (self,
+ signals[SIGNAL_SHOW_LAYOUT],
+ 0);
+}
+
+static void
+remove_button_clicked_cb (CcInputRow *self)
+{
+ g_signal_emit (self,
+ signals[SIGNAL_REMOVE_ROW],
+ 0);
+}
+
+static void
+cc_input_row_dispose (GObject *object)
+{
+ CcInputRow *self = CC_INPUT_ROW (object);
+
+ g_clear_object (&self->source);
+
+ G_OBJECT_CLASS (cc_input_row_parent_class)->dispose (object);
+}
+
+void
+cc_input_row_class_init (CcInputRowClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = cc_input_row_dispose;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/region/cc-input-row.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcInputRow, drag_handle);
+ gtk_widget_class_bind_template_child (widget_class, CcInputRow, name_label);
+ gtk_widget_class_bind_template_child (widget_class, CcInputRow, remove_button);
+ gtk_widget_class_bind_template_child (widget_class, CcInputRow, settings_button);
+
+ gtk_widget_class_bind_template_callback (widget_class, drag_data_get_cb);
+ gtk_widget_class_bind_template_callback (widget_class, drag_begin_cb);
+ gtk_widget_class_bind_template_callback (widget_class, drag_end_cb);
+ gtk_widget_class_bind_template_callback (widget_class, drag_data_received_cb);
+ gtk_widget_class_bind_template_callback (widget_class, layout_button_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, settings_button_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, remove_button_clicked_cb);
+
+ signals[SIGNAL_SHOW_SETTINGS] =
+ g_signal_new ("show-settings",
+ G_TYPE_FROM_CLASS (object_class),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL,
+ NULL,
+ G_TYPE_NONE,
+ 0);
+
+ signals[SIGNAL_SHOW_LAYOUT] =
+ g_signal_new ("show-layout",
+ G_TYPE_FROM_CLASS (object_class),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL,
+ NULL,
+ G_TYPE_NONE,
+ 0);
+
+ signals[SIGNAL_MOVE_ROW] =
+ g_signal_new ("move-row",
+ G_TYPE_FROM_CLASS (object_class),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL,
+ NULL,
+ G_TYPE_NONE,
+ 1, CC_TYPE_INPUT_ROW);
+
+ signals[SIGNAL_REMOVE_ROW] =
+ g_signal_new ("remove-row",
+ G_TYPE_FROM_CLASS (object_class),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL,
+ NULL,
+ G_TYPE_NONE,
+ 0);
+}
+
+static GtkTargetEntry entries[] =
+{
+ { "GTK_LIST_BOX_ROW", GTK_TARGET_SAME_APP, 0 }
+};
+
+void
+cc_input_row_init (CcInputRow *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ gtk_drag_source_set (GTK_WIDGET (self->drag_handle), GDK_BUTTON1_MASK, entries, 1, GDK_ACTION_MOVE);
+ gtk_drag_dest_set (GTK_WIDGET (self), GTK_DEST_DEFAULT_ALL, entries, 1, GDK_ACTION_MOVE);
+}
+
+static void
+label_changed_cb (CcInputRow *self)
+{
+ g_autofree gchar *label = cc_input_source_get_label (self->source);
+ gtk_label_set_text (self->name_label, label);
+}
+
+CcInputRow *
+cc_input_row_new (CcInputSource *source)
+{
+ CcInputRow *self;
+
+ self = g_object_new (CC_TYPE_INPUT_ROW, NULL);
+ self->source = g_object_ref (source);
+
+ g_signal_connect_object (source, "label-changed", G_CALLBACK (label_changed_cb), self, G_CONNECT_SWAPPED);
+ label_changed_cb (self);
+
+ gtk_widget_set_visible (GTK_WIDGET (self->settings_button), CC_IS_INPUT_SOURCE_IBUS (source));
+
+ return self;
+}
+
+CcInputSource *
+cc_input_row_get_source (CcInputRow *self)
+{
+ g_return_val_if_fail (CC_IS_INPUT_ROW (self), NULL);
+ return self->source;
+}
+
+void
+cc_input_row_set_removable (CcInputRow *self,
+ gboolean removable)
+{
+ g_return_if_fail (CC_IS_INPUT_ROW (self));
+ gtk_widget_set_sensitive (GTK_WIDGET (self->remove_button), removable);
+}
+
+void
+cc_input_row_set_draggable (CcInputRow *self,
+ gboolean draggable)
+{
+ if (draggable)
+ gtk_drag_source_set (GTK_WIDGET (self->drag_handle), GDK_BUTTON1_MASK, entries, 1, GDK_ACTION_MOVE);
+ else
+ gtk_drag_source_unset (GTK_WIDGET (self->drag_handle));
+}
diff --git a/panels/region/cc-input-row.h b/panels/region/cc-input-row.h
new file mode 100644
index 0000000..5d78337
--- /dev/null
+++ b/panels/region/cc-input-row.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright © 2018 Canonical Ltd.
+ *
+ * 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/>.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <gio/gdesktopappinfo.h>
+
+#include "cc-input-source.h"
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_INPUT_ROW (cc_input_row_get_type ())
+G_DECLARE_FINAL_TYPE (CcInputRow, cc_input_row, CC, INPUT_ROW, GtkListBoxRow)
+
+CcInputRow *cc_input_row_new (CcInputSource *source);
+
+CcInputSource *cc_input_row_get_source (CcInputRow *row);
+
+void cc_input_row_set_removable (CcInputRow *row,
+ gboolean removable);
+
+void cc_input_row_set_draggable (CcInputRow *row,
+ gboolean draggable);
+
+G_END_DECLS
diff --git a/panels/region/cc-input-row.ui b/panels/region/cc-input-row.ui
new file mode 100644
index 0000000..70794fc
--- /dev/null
+++ b/panels/region/cc-input-row.ui
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.20"/>
+ <template class="CcInputRow" parent="GtkListBoxRow">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="selectable">False</property>
+ <signal name="drag_data_received" handler="drag_data_received_cb" object="CcInputRow" swapped="yes"/>
+ <child>
+ <object class="GtkEventBox" id="drag_handle">
+ <property name="visible">True</property>
+ <signal name="drag_data_get" handler="drag_data_get_cb" object="CcInputRow" swapped="yes"/>
+ <signal name="drag_begin" handler="drag_begin_cb" object="CcInputRow" swapped="yes"/>
+ <signal name="drag_end" handler="drag_end_cb" object="CcInputRow" swapped="yes"/>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="height_request">50</property>
+ <property name="margin_start">4</property>
+ <property name="margin_end">12</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon_name">list-drag-handle-symbolic</property>
+ <style>
+ <class name="drag-handle"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="name_label">
+ <property name="visible">True</property>
+ <property name="margin_bottom">8</property>
+ <property name="margin_top">8</property>
+ <property name="ellipsize">end</property>
+ <property name="xalign">0.0</property>
+ <property name="hexpand">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="settings_button">
+ <property name="visible">False</property>
+ <property name="margin_bottom">8</property>
+ <property name="margin_top">8</property>
+ <property name="valign">center</property>
+ <signal name="clicked" handler="settings_button_clicked_cb" object="CcInputRow" swapped="yes"/>
+ <style>
+ <class name="image-button"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon_name">emblem-system-symbolic</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton">
+ <property name="visible">True</property>
+ <property name="margin_bottom">8</property>
+ <property name="margin_top">8</property>
+ <property name="valign">center</property>
+ <signal name="clicked" handler="layout_button_clicked_cb" object="CcInputRow" swapped="yes"/>
+ <style>
+ <class name="image-button"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon_name">view-layout-symbolic</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="remove_button">
+ <property name="visible">True</property>
+ <property name="margin_bottom">8</property>
+ <property name="margin_top">8</property>
+ <property name="valign">center</property>
+ <signal name="clicked" handler="remove_button_clicked_cb" object="CcInputRow" swapped="yes"/>
+ <style>
+ <class name="image-button"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon_name">edit-delete-symbolic</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/panels/region/cc-input-source-ibus.c b/panels/region/cc-input-source-ibus.c
new file mode 100644
index 0000000..1aa1ab8
--- /dev/null
+++ b/panels/region/cc-input-source-ibus.c
@@ -0,0 +1,155 @@
+/*
+ * Copyright © 2018 Canonical Ltd.
+ *
+ * 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/>.
+ */
+
+#include "cc-input-source-ibus.h"
+#ifdef HAVE_IBUS
+#include "cc-ibus-utils.h"
+#endif
+
+struct _CcInputSourceIBus
+{
+ CcInputSource parent_instance;
+
+ gchar *engine_name;
+#ifdef HAVE_IBUS
+ IBusEngineDesc *engine_desc;
+#endif
+};
+
+G_DEFINE_TYPE (CcInputSourceIBus, cc_input_source_ibus, CC_TYPE_INPUT_SOURCE)
+
+static gchar *
+cc_input_source_ibus_get_label (CcInputSource *source)
+{
+ CcInputSourceIBus *self = CC_INPUT_SOURCE_IBUS (source);
+#ifdef HAVE_IBUS
+ if (self->engine_desc)
+ return g_strdup (engine_get_display_name (self->engine_desc));
+ else
+#endif
+ return g_strdup (self->engine_name);
+}
+
+static gboolean
+cc_input_source_ibus_matches (CcInputSource *source,
+ CcInputSource *source2)
+{
+ if (!CC_IS_INPUT_SOURCE_IBUS (source2))
+ return FALSE;
+
+ return g_strcmp0 (CC_INPUT_SOURCE_IBUS (source)->engine_name, CC_INPUT_SOURCE_IBUS (source2)->engine_name) == 0;
+}
+
+static const gchar *
+cc_input_source_ibus_get_layout (CcInputSource *source)
+{
+#ifdef HAVE_IBUS
+ CcInputSourceIBus *self = CC_INPUT_SOURCE_IBUS (source);
+ if (self->engine_desc != NULL)
+ return ibus_engine_desc_get_layout (self->engine_desc);
+ else
+#endif
+ return NULL;
+}
+
+static const gchar *
+cc_input_source_ibus_get_layout_variant (CcInputSource *source)
+{
+#ifdef HAVE_IBUS
+ CcInputSourceIBus *self = CC_INPUT_SOURCE_IBUS (source);
+ if (self->engine_desc != NULL)
+ return ibus_engine_desc_get_layout_variant (self->engine_desc);
+ else
+#endif
+ return NULL;
+}
+
+static void
+cc_input_source_ibus_dispose (GObject *object)
+{
+ CcInputSourceIBus *self = CC_INPUT_SOURCE_IBUS (object);
+
+ g_clear_pointer (&self->engine_name, g_free);
+#ifdef HAVE_IBUS
+ g_clear_object (&self->engine_desc);
+#endif
+
+ G_OBJECT_CLASS (cc_input_source_ibus_parent_class)->dispose (object);
+}
+
+void
+cc_input_source_ibus_class_init (CcInputSourceIBusClass *klass)
+{
+ CcInputSourceClass *input_source_class = CC_INPUT_SOURCE_CLASS (klass);
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ input_source_class->get_label = cc_input_source_ibus_get_label;
+ input_source_class->matches = cc_input_source_ibus_matches;
+ input_source_class->get_layout = cc_input_source_ibus_get_layout;
+ input_source_class->get_layout_variant = cc_input_source_ibus_get_layout_variant;
+ object_class->dispose = cc_input_source_ibus_dispose;
+}
+
+void
+cc_input_source_ibus_init (CcInputSourceIBus *source)
+{
+}
+
+CcInputSourceIBus *
+cc_input_source_ibus_new (const gchar *engine_name)
+{
+ CcInputSourceIBus *source;
+
+ source = g_object_new (CC_TYPE_INPUT_SOURCE_IBUS, NULL);
+ source->engine_name = g_strdup (engine_name);
+
+ return source;
+}
+
+#ifdef HAVE_IBUS
+void
+cc_input_source_ibus_set_engine_desc (CcInputSourceIBus *source,
+ IBusEngineDesc *engine_desc)
+{
+ g_return_if_fail (CC_IS_INPUT_SOURCE_IBUS (source));
+
+ g_clear_object (&source->engine_desc);
+ source->engine_desc = g_object_ref (engine_desc);
+ cc_input_source_emit_label_changed (CC_INPUT_SOURCE (source));
+}
+#endif
+
+const gchar *
+cc_input_source_ibus_get_engine_name (CcInputSourceIBus *source)
+{
+ g_return_val_if_fail (CC_IS_INPUT_SOURCE_IBUS (source), NULL);
+ return source->engine_name;
+}
+
+GDesktopAppInfo *
+cc_input_source_ibus_get_app_info (CcInputSourceIBus *source)
+{
+ g_auto(GStrv) tokens = NULL;
+ g_autofree gchar *desktop_file_name = NULL;
+
+ g_return_val_if_fail (CC_IS_INPUT_SOURCE_IBUS (source), NULL);
+
+ tokens = g_strsplit (source->engine_name, ":", 2);
+ desktop_file_name = g_strdup_printf ("ibus-setup-%s.desktop", tokens[0]);
+
+ return g_desktop_app_info_new (desktop_file_name);
+}
diff --git a/panels/region/cc-input-source-ibus.h b/panels/region/cc-input-source-ibus.h
new file mode 100644
index 0000000..2c09d01
--- /dev/null
+++ b/panels/region/cc-input-source-ibus.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright © 2018 Canonical Ltd.
+ *
+ * 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/>.
+ */
+
+#pragma once
+
+#include <config.h>
+
+#ifdef HAVE_IBUS
+#include <ibus.h>
+#endif
+
+#include <gio/gdesktopappinfo.h>
+
+#include "cc-input-source.h"
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_INPUT_SOURCE_IBUS (cc_input_source_ibus_get_type ())
+G_DECLARE_FINAL_TYPE (CcInputSourceIBus, cc_input_source_ibus, CC, INPUT_SOURCE_IBUS, CcInputSource)
+
+CcInputSourceIBus *cc_input_source_ibus_new (const gchar *engine_name);
+
+#ifdef HAVE_IBUS
+void cc_input_source_ibus_set_engine_desc (CcInputSourceIBus *source,
+ IBusEngineDesc *engine_desc);
+#endif
+
+const gchar *cc_input_source_ibus_get_engine_name (CcInputSourceIBus *source);
+
+GDesktopAppInfo *cc_input_source_ibus_get_app_info (CcInputSourceIBus *source);
+
+G_END_DECLS
diff --git a/panels/region/cc-input-source-xkb.c b/panels/region/cc-input-source-xkb.c
new file mode 100644
index 0000000..2ea30be
--- /dev/null
+++ b/panels/region/cc-input-source-xkb.c
@@ -0,0 +1,134 @@
+/*
+ * Copyright © 2018 Canonical Ltd.
+ *
+ * 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/>.
+ */
+
+#include <config.h>
+#include "cc-input-source-xkb.h"
+
+struct _CcInputSourceXkb
+{
+ CcInputSource parent_instance;
+
+ GnomeXkbInfo *xkb_info;
+ gchar *layout;
+ gchar *variant;
+};
+
+G_DEFINE_TYPE (CcInputSourceXkb, cc_input_source_xkb, CC_TYPE_INPUT_SOURCE)
+
+static gchar *
+cc_input_source_xkb_get_label (CcInputSource *source)
+{
+ CcInputSourceXkb *self = CC_INPUT_SOURCE_XKB (source);
+ g_autofree gchar *id = NULL;
+ const gchar *name;
+
+ id = cc_input_source_xkb_get_id (self);
+ gnome_xkb_info_get_layout_info (self->xkb_info, id, &name, NULL, NULL, NULL);
+ if (name)
+ return g_strdup (name);
+ else
+ return g_strdup (id);
+}
+
+static gboolean
+cc_input_source_xkb_matches (CcInputSource *source,
+ CcInputSource *source2)
+{
+ if (!CC_IS_INPUT_SOURCE_XKB (source2))
+ return FALSE;
+
+ return g_strcmp0 (CC_INPUT_SOURCE_XKB (source)->layout, CC_INPUT_SOURCE_XKB (source2)->layout) == 0 &&
+ g_strcmp0 (CC_INPUT_SOURCE_XKB (source)->variant, CC_INPUT_SOURCE_XKB (source2)->variant) == 0;
+}
+
+static void
+cc_input_source_xkb_dispose (GObject *object)
+{
+ CcInputSourceXkb *self = CC_INPUT_SOURCE_XKB (object);
+
+ g_clear_object (&self->xkb_info);
+ g_clear_pointer (&self->layout, g_free);
+ g_clear_pointer (&self->variant, g_free);
+
+ G_OBJECT_CLASS (cc_input_source_xkb_parent_class)->dispose (object);
+}
+
+static const gchar *
+cc_input_source_xkb_get_layout (CcInputSource *source)
+{
+ return CC_INPUT_SOURCE_XKB (source)->layout;
+}
+
+static const gchar *
+cc_input_source_xkb_get_layout_variant (CcInputSource *source)
+{
+ return CC_INPUT_SOURCE_XKB (source)->variant;
+}
+
+void
+cc_input_source_xkb_class_init (CcInputSourceXkbClass *klass)
+{
+ CcInputSourceClass *input_source_class = CC_INPUT_SOURCE_CLASS (klass);
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ input_source_class->get_label = cc_input_source_xkb_get_label;
+ input_source_class->matches = cc_input_source_xkb_matches;
+ input_source_class->get_layout = cc_input_source_xkb_get_layout;
+ input_source_class->get_layout_variant = cc_input_source_xkb_get_layout_variant;
+ object_class->dispose = cc_input_source_xkb_dispose;
+}
+
+void
+cc_input_source_xkb_init (CcInputSourceXkb *source)
+{
+}
+
+CcInputSourceXkb *
+cc_input_source_xkb_new (GnomeXkbInfo *xkb_info,
+ const gchar *layout,
+ const gchar *variant)
+{
+ CcInputSourceXkb *source;
+
+ source = g_object_new (CC_TYPE_INPUT_SOURCE_XKB, NULL);
+ source->xkb_info = g_object_ref (xkb_info);
+ source->layout = g_strdup (layout);
+ source->variant = g_strdup (variant);
+
+ return source;
+}
+
+CcInputSourceXkb *
+cc_input_source_xkb_new_from_id (GnomeXkbInfo *xkb_info,
+ const gchar *id)
+{
+ g_auto(GStrv) tokens = NULL;
+
+ tokens = g_strsplit (id, "+", 2);
+
+ return cc_input_source_xkb_new (xkb_info, tokens[0], tokens[1]);
+}
+
+gchar *
+cc_input_source_xkb_get_id (CcInputSourceXkb *source)
+{
+ g_return_val_if_fail (CC_IS_INPUT_SOURCE_XKB (source), NULL);
+ if (source->variant != NULL)
+ return g_strdup_printf ("%s+%s", source->layout, source->variant);
+ else
+ return g_strdup (source->layout);
+}
diff --git a/panels/region/cc-input-source-xkb.h b/panels/region/cc-input-source-xkb.h
new file mode 100644
index 0000000..e8886d0
--- /dev/null
+++ b/panels/region/cc-input-source-xkb.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright © 2018 Canonical Ltd.
+ *
+ * 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/>.
+ */
+
+#pragma once
+
+#define GNOME_DESKTOP_USE_UNSTABLE_API
+#include <libgnome-desktop/gnome-xkb-info.h>
+
+#include "cc-input-source.h"
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_INPUT_SOURCE_XKB (cc_input_source_xkb_get_type ())
+G_DECLARE_FINAL_TYPE (CcInputSourceXkb, cc_input_source_xkb, CC, INPUT_SOURCE_XKB, CcInputSource)
+
+CcInputSourceXkb *cc_input_source_xkb_new (GnomeXkbInfo *xkb_info,
+ const gchar *layout,
+ const gchar *variant);
+
+CcInputSourceXkb *cc_input_source_xkb_new_from_id (GnomeXkbInfo *xkb_info,
+ const gchar *id);
+
+gchar *cc_input_source_xkb_get_id (CcInputSourceXkb *source);
+
+G_END_DECLS
diff --git a/panels/region/cc-input-source.c b/panels/region/cc-input-source.c
new file mode 100644
index 0000000..df8db8b
--- /dev/null
+++ b/panels/region/cc-input-source.c
@@ -0,0 +1,84 @@
+/*
+ * Copyright © 2018 Canonical Ltd.
+ *
+ * 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/>.
+ */
+
+#include <config.h>
+#include "cc-input-source.h"
+
+enum
+{
+ SIGNAL_LABEL_CHANGED,
+ SIGNAL_LAST
+};
+
+static guint signals[SIGNAL_LAST] = {0};
+
+G_DEFINE_TYPE (CcInputSource, cc_input_source, G_TYPE_OBJECT)
+
+void
+cc_input_source_class_init (CcInputSourceClass *klass)
+{
+ signals[SIGNAL_LABEL_CHANGED] =
+ g_signal_new ("label-changed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL,
+ NULL,
+ G_TYPE_NONE,
+ 0);
+}
+
+void
+cc_input_source_init (CcInputSource *source)
+{
+}
+
+void
+cc_input_source_emit_label_changed (CcInputSource *source)
+{
+ g_return_if_fail (CC_IS_INPUT_SOURCE (source));
+ g_signal_emit (source, signals[SIGNAL_LABEL_CHANGED], 0);
+}
+
+gchar *
+cc_input_source_get_label (CcInputSource *source)
+{
+ g_return_val_if_fail (CC_IS_INPUT_SOURCE (source), NULL);
+ return CC_INPUT_SOURCE_GET_CLASS (source)->get_label (source);
+}
+
+gboolean
+cc_input_source_matches (CcInputSource *source,
+ CcInputSource *source2)
+{
+ g_return_val_if_fail (CC_IS_INPUT_SOURCE (source), FALSE);
+ return CC_INPUT_SOURCE_GET_CLASS (source)->matches (source, source2);
+}
+
+const gchar *
+cc_input_source_get_layout (CcInputSource *source)
+{
+ g_return_val_if_fail (CC_IS_INPUT_SOURCE (source), NULL);
+ return CC_INPUT_SOURCE_GET_CLASS (source)->get_layout (source);
+}
+
+const gchar *
+cc_input_source_get_layout_variant (CcInputSource *source)
+{
+ g_return_val_if_fail (CC_IS_INPUT_SOURCE (source), NULL);
+ return CC_INPUT_SOURCE_GET_CLASS (source)->get_layout_variant (source);
+}
diff --git a/panels/region/cc-input-source.h b/panels/region/cc-input-source.h
new file mode 100644
index 0000000..5b7865d
--- /dev/null
+++ b/panels/region/cc-input-source.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright © 2018 Canonical Ltd.
+ *
+ * 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/>.
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_INPUT_SOURCE (cc_input_source_get_type ())
+G_DECLARE_DERIVABLE_TYPE (CcInputSource, cc_input_source, CC, INPUT_SOURCE, GObject)
+
+struct _CcInputSourceClass
+{
+ GObjectClass parent_class;
+
+ gchar* (*get_label) (CcInputSource *source);
+ gboolean (*matches) (CcInputSource *source,
+ CcInputSource *source2);
+ const gchar* (*get_layout) (CcInputSource *source);
+ const gchar* (*get_layout_variant) (CcInputSource *source);
+};
+
+void cc_input_source_emit_label_changed (CcInputSource *source);
+
+gchar *cc_input_source_get_label (CcInputSource *source);
+
+gboolean cc_input_source_matches (CcInputSource *source,
+ CcInputSource *source2);
+
+const gchar *cc_input_source_get_layout (CcInputSource *source);
+
+const gchar *cc_input_source_get_layout_variant (CcInputSource *source);
+
+G_END_DECLS
diff --git a/panels/region/cc-region-panel.c b/panels/region/cc-region-panel.c
new file mode 100644
index 0000000..9b59b23
--- /dev/null
+++ b/panels/region/cc-region-panel.c
@@ -0,0 +1,1613 @@
+/*
+ * Copyright (C) 2010 Intel, 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/>.
+ *
+ * Author: Sergey Udaltsov <svu@gnome.org>
+ *
+ */
+
+#include <config.h>
+#include <errno.h>
+#include <locale.h>
+#include <glib/gi18n.h>
+#include <gio/gio.h>
+#include <gio/gdesktopappinfo.h>
+#include <gtk/gtk.h>
+#include <polkit/polkit.h>
+
+#include "list-box-helper.h"
+#include "cc-region-panel.h"
+#include "cc-region-resources.h"
+#include "cc-language-chooser.h"
+#include "cc-format-chooser.h"
+#include "cc-input-chooser.h"
+#include "cc-input-row.h"
+#include "cc-input-source-ibus.h"
+#include "cc-input-source-xkb.h"
+
+#include "cc-common-language.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>
+#endif
+
+#include <act/act.h>
+
+#define GNOME_DESKTOP_INPUT_SOURCES_DIR "org.gnome.desktop.input-sources"
+#define KEY_INPUT_SOURCES "sources"
+
+#define GNOME_SYSTEM_LOCALE_DIR "org.gnome.system.locale"
+#define KEY_REGION "region"
+
+#define DEFAULT_LOCALE "en_US.utf-8"
+
+struct _CcRegionPanel {
+ CcPanel parent_instance;
+
+ GtkListBoxRow *add_input_row;
+ GtkLabel *alt_next_source;
+ GtkLabel *formats_label;
+ GtkListBoxRow *formats_row;
+ GtkListBox *input_list;
+ GtkBox *input_section_box;
+ GtkSizeGroup *input_size_group;
+ GtkToggleButton *login_button;
+ GtkLabel *login_label;
+ GtkLabel *language_label;
+ GtkListBox *language_list;
+ GtkListBoxRow *language_row;
+ GtkFrame *language_section_frame;
+ GtkLabel *next_source;
+ GtkLabel *next_source_label;
+ GtkListBoxRow *no_inputs_row;
+ GtkButton *options_button;
+ GtkRadioButton *per_window_source;
+ GtkLabel *previous_source;
+ GtkLabel *previous_source_label;
+ GtkButton *restart_button;
+ GtkRevealer *restart_revealer;
+ GtkRadioButton *same_source;
+
+ gboolean login;
+ gboolean login_auto_apply;
+ GPermission *permission;
+ GDBusProxy *localed;
+ GDBusProxy *session;
+
+ ActUserManager *user_manager;
+ ActUser *user;
+ GSettings *locale_settings;
+
+ gchar *language;
+ gchar *region;
+ gchar *system_language;
+ gchar *system_region;
+
+ GSettings *input_settings;
+ GnomeXkbInfo *xkb_info;
+#ifdef HAVE_IBUS
+ IBusBus *ibus;
+ GHashTable *ibus_engines;
+#endif
+};
+
+CC_PANEL_REGISTER (CcRegionPanel, cc_region_panel)
+
+typedef struct
+{
+ CcRegionPanel *panel;
+ CcInputRow *source;
+ CcInputRow *dest;
+} RowData;
+
+static RowData *
+row_data_new (CcRegionPanel *panel, CcInputRow *source, CcInputRow *dest)
+{
+ RowData *data = g_malloc0 (sizeof (RowData));
+ data->panel = panel;
+ data->source = g_object_ref (source);
+ if (dest != NULL)
+ data->dest = g_object_ref (dest);
+ return data;
+}
+
+static void
+row_data_free (RowData *data)
+{
+ g_clear_object (&data->source);
+ g_clear_object (&data->dest);
+ g_free (data);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (RowData, row_data_free)
+
+static void
+cc_region_panel_finalize (GObject *object)
+{
+ CcRegionPanel *self = CC_REGION_PANEL (object);
+ GtkWidget *chooser;
+
+ if (self->user_manager) {
+ g_signal_handlers_disconnect_by_data (self->user_manager, self);
+ self->user_manager = NULL;
+ }
+
+ if (self->user) {
+ g_signal_handlers_disconnect_by_data (self->user, self);
+ self->user = NULL;
+ }
+
+ g_clear_object (&self->permission);
+ g_clear_object (&self->localed);
+ g_clear_object (&self->session);
+ g_clear_object (&self->locale_settings);
+ g_clear_object (&self->input_settings);
+ g_clear_object (&self->xkb_info);
+#ifdef HAVE_IBUS
+ g_clear_object (&self->ibus);
+ g_clear_pointer (&self->ibus_engines, g_hash_table_destroy);
+#endif
+ g_free (self->language);
+ g_free (self->region);
+ g_free (self->system_language);
+ g_free (self->system_region);
+
+ chooser = g_object_get_data (G_OBJECT (self), "input-chooser");
+ if (chooser)
+ gtk_widget_destroy (chooser);
+
+ G_OBJECT_CLASS (cc_region_panel_parent_class)->finalize (object);
+}
+
+static void
+cc_region_panel_constructed (GObject *object)
+{
+ CcRegionPanel *self = CC_REGION_PANEL (object);
+
+ G_OBJECT_CLASS (cc_region_panel_parent_class)->constructed (object);
+
+ if (self->permission)
+ cc_shell_embed_widget_in_header (cc_panel_get_shell (CC_PANEL (object)),
+ GTK_WIDGET (self->login_button),
+ GTK_POS_RIGHT);
+}
+
+static const char *
+cc_region_panel_get_help_uri (CcPanel *panel)
+{
+ return "help:gnome-help/prefs-language";
+}
+
+static GFile *
+get_needs_restart_file (void)
+{
+ g_autofree gchar *path = NULL;
+
+ path = g_build_filename (g_get_user_runtime_dir (),
+ "gnome-control-center-region-needs-restart",
+ NULL);
+ return g_file_new_for_path (path);
+}
+
+static void
+restart_now (CcRegionPanel *self)
+{
+ g_autoptr(GFile) file = NULL;
+
+ file = get_needs_restart_file ();
+ g_file_delete (file, NULL, NULL);
+
+ g_dbus_proxy_call (self->session,
+ "Logout",
+ g_variant_new ("(u)", 0),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1, NULL, NULL, NULL);
+}
+
+static void
+set_restart_notification_visible (CcRegionPanel *self,
+ const gchar *locale,
+ gboolean visible)
+{
+ locale_t new_locale;
+ locale_t current_locale;
+ g_autoptr(GFile) file = NULL;
+ g_autoptr(GFileOutputStream) output_stream = NULL;
+ g_autoptr(GError) error = NULL;
+
+ if (locale) {
+ new_locale = newlocale (LC_MESSAGES_MASK, locale, (locale_t) 0);
+ if (new_locale == (locale_t) 0)
+ g_warning ("Failed to create locale %s: %s", locale, g_strerror (errno));
+ else
+ current_locale = uselocale (new_locale);
+ }
+
+ gtk_revealer_set_reveal_child (self->restart_revealer, visible);
+
+ if (locale && new_locale != (locale_t) 0) {
+ uselocale (current_locale);
+ freelocale (new_locale);
+ }
+
+ file = get_needs_restart_file ();
+
+ if (!visible) {
+ g_file_delete (file, NULL, NULL);
+ return;
+ }
+
+ output_stream = g_file_create (file, G_FILE_CREATE_NONE, NULL, &error);
+ if (output_stream == NULL) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS))
+ g_warning ("Unable to create %s: %s", g_file_get_path (file), error->message);
+ }
+}
+
+typedef struct {
+ CcRegionPanel *self;
+ int category;
+ gchar *target_locale;
+} MaybeNotifyData;
+
+static void
+maybe_notify_data_free (MaybeNotifyData *data)
+{
+ g_free (data->target_locale);
+ g_free (data);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (MaybeNotifyData, maybe_notify_data_free)
+
+static void
+maybe_notify_finish (GObject *source,
+ GAsyncResult *res,
+ gpointer data)
+{
+ g_autoptr(MaybeNotifyData) mnd = data;
+ CcRegionPanel *self = mnd->self;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GVariant) retval = NULL;
+ g_autofree gchar *current_lang_code = NULL;
+ g_autofree gchar *current_country_code = NULL;
+ g_autofree gchar *target_lang_code = NULL;
+ g_autofree gchar *target_country_code = NULL;
+ const gchar *current_locale = NULL;
+
+ retval = g_dbus_proxy_call_finish (G_DBUS_PROXY (source), res, &error);
+ if (!retval) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("Failed to get locale: %s\n", error->message);
+ return;
+ }
+
+ g_variant_get (retval, "(&s)", &current_locale);
+
+ if (!gnome_parse_locale (current_locale,
+ &current_lang_code,
+ &current_country_code,
+ NULL,
+ NULL))
+ return;
+
+ if (!gnome_parse_locale (mnd->target_locale,
+ &target_lang_code,
+ &target_country_code,
+ NULL,
+ NULL))
+ return;
+
+ if (g_str_equal (current_lang_code, target_lang_code) == FALSE ||
+ g_str_equal (current_country_code, target_country_code) == FALSE)
+ set_restart_notification_visible (self,
+ mnd->category == LC_MESSAGES ? mnd->target_locale : NULL,
+ TRUE);
+ else
+ set_restart_notification_visible (self,
+ mnd->category == LC_MESSAGES ? mnd->target_locale : NULL,
+ FALSE);
+}
+
+static void
+maybe_notify (CcRegionPanel *self,
+ int category,
+ const gchar *target_locale)
+{
+ MaybeNotifyData *mnd;
+
+ mnd = g_new0 (MaybeNotifyData, 1);
+ mnd->self = self;
+ mnd->category = category;
+ mnd->target_locale = g_strdup (target_locale);
+
+ g_dbus_proxy_call (self->session,
+ "GetLocale",
+ g_variant_new ("(i)", category),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ cc_panel_get_cancellable (CC_PANEL (self)),
+ maybe_notify_finish,
+ mnd);
+}
+
+static void set_localed_locale (CcRegionPanel *self);
+
+static void
+set_system_language (CcRegionPanel *self,
+ const gchar *language)
+{
+ if (g_strcmp0 (language, self->system_language) == 0)
+ return;
+
+ g_free (self->system_language);
+ self->system_language = g_strdup (language);
+
+ set_localed_locale (self);
+}
+
+static void
+update_language (CcRegionPanel *self,
+ const gchar *language)
+{
+ if (self->login) {
+ set_system_language (self, language);
+ } else {
+ if (g_strcmp0 (language, self->language) == 0)
+ return;
+ act_user_set_language (self->user, language);
+ if (self->login_auto_apply)
+ set_system_language (self, language);
+ maybe_notify (self, LC_MESSAGES, language);
+ }
+}
+
+static void
+language_response (CcRegionPanel *self,
+ gint response_id,
+ CcLanguageChooser *chooser)
+{
+ const gchar *language;
+
+ if (response_id == GTK_RESPONSE_OK) {
+ language = cc_language_chooser_get_language (chooser);
+ update_language (self, language);
+ }
+
+ gtk_widget_destroy (GTK_WIDGET (chooser));
+}
+
+static void
+set_system_region (CcRegionPanel *self,
+ const gchar *region)
+{
+ if (g_strcmp0 (region, self->system_region) == 0)
+ return;
+
+ g_free (self->system_region);
+ self->system_region = g_strdup (region);
+
+ set_localed_locale (self);
+}
+
+static void
+update_region (CcRegionPanel *self,
+ const gchar *region)
+{
+ if (self->login) {
+ set_system_region (self, region);
+ } else {
+ if (g_strcmp0 (region, self->region) == 0)
+ return;
+ g_settings_set_string (self->locale_settings, KEY_REGION, region);
+ if (self->login_auto_apply)
+ set_system_region (self, region);
+ maybe_notify (self, LC_TIME, region);
+ }
+}
+
+static void
+format_response (CcRegionPanel *self,
+ gint response_id,
+ CcFormatChooser *chooser)
+{
+ const gchar *region;
+
+ if (response_id == GTK_RESPONSE_OK) {
+ region = cc_format_chooser_get_region (chooser);
+ update_region (self, region);
+ }
+
+ gtk_widget_destroy (GTK_WIDGET (chooser));
+}
+
+static const gchar *
+get_effective_language (CcRegionPanel *self)
+{
+ if (self->login)
+ return self->system_language;
+ else
+ return self->language;
+}
+
+static void
+show_language_chooser (CcRegionPanel *self)
+{
+ CcLanguageChooser *chooser;
+
+ chooser = cc_language_chooser_new ();
+ gtk_window_set_transient_for (GTK_WINDOW (chooser), GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (self))));
+ cc_language_chooser_set_language (chooser, get_effective_language (self));
+ g_signal_connect_object (chooser, "response",
+ G_CALLBACK (language_response), self, G_CONNECT_SWAPPED);
+ gtk_window_present (GTK_WINDOW (chooser));
+}
+
+static const gchar *
+get_effective_region (CcRegionPanel *self)
+{
+ const gchar *region;
+
+ if (self->login)
+ region = self->system_region;
+ else
+ region = self->region;
+
+ /* Region setting might be empty - show the language because
+ * that's what LC_TIME and others will effectively be when the
+ * user logs in again. */
+ if (region == NULL || region[0] == '\0')
+ region = get_effective_language (self);
+
+ return region;
+}
+
+static void
+show_region_chooser (CcRegionPanel *self)
+{
+ CcFormatChooser *chooser;
+
+ chooser = cc_format_chooser_new ();
+ gtk_window_set_transient_for (GTK_WINDOW (chooser), GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (self))));
+ cc_format_chooser_set_region (chooser, get_effective_region (self));
+ g_signal_connect_object (chooser, "response",
+ G_CALLBACK (format_response), self, G_CONNECT_SWAPPED);
+ gtk_window_present (GTK_WINDOW (chooser));
+}
+
+static void show_input_chooser (CcRegionPanel *self);
+
+static gboolean
+permission_acquired (GPermission *permission, GAsyncResult *res, const gchar *action)
+{
+ g_autoptr(GError) error = NULL;
+
+ if (!g_permission_acquire_finish (permission, res, &error)) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("Failed to acquire permission to %s: %s\n", error->message, action);
+ return FALSE;
+ }
+
+ return FALSE;
+}
+
+static void
+choose_language_permission_cb (GObject *source, GAsyncResult *res, gpointer user_data)
+{
+ CcRegionPanel *self = user_data;
+ if (permission_acquired (G_PERMISSION (source), res, "choose language"))
+ show_language_chooser (self);
+}
+
+static void
+choose_region_permission_cb (GObject *source, GAsyncResult *res, gpointer user_data)
+{
+ CcRegionPanel *self = user_data;
+ if (permission_acquired (G_PERMISSION (source), res, "choose region"))
+ show_region_chooser (self);
+}
+
+static void
+activate_language_row (CcRegionPanel *self,
+ GtkListBoxRow *row)
+{
+ if (row == self->language_row) {
+ if (!self->login || g_permission_get_allowed (self->permission)) {
+ show_language_chooser (self);
+ } else if (g_permission_get_can_acquire (self->permission)) {
+ g_permission_acquire_async (self->permission,
+ cc_panel_get_cancellable (CC_PANEL (self)),
+ choose_language_permission_cb,
+ self);
+ }
+ } else if (row == self->formats_row) {
+ if (!self->login || g_permission_get_allowed (self->permission)) {
+ show_region_chooser (self);
+ } else if (g_permission_get_can_acquire (self->permission)) {
+ g_permission_acquire_async (self->permission,
+ cc_panel_get_cancellable (CC_PANEL (self)),
+ choose_region_permission_cb,
+ self);
+ }
+ }
+}
+
+static void
+update_region_label (CcRegionPanel *self)
+{
+ const gchar *region = get_effective_region (self);
+ g_autofree gchar *name = NULL;
+
+ if (region)
+ name = gnome_get_country_from_locale (region, region);
+
+ if (!name)
+ name = gnome_get_country_from_locale (DEFAULT_LOCALE, DEFAULT_LOCALE);
+
+ gtk_label_set_label (self->formats_label, name);
+}
+
+static void
+update_region_from_setting (CcRegionPanel *self)
+{
+ g_free (self->region);
+ self->region = g_settings_get_string (self->locale_settings, KEY_REGION);
+ update_region_label (self);
+}
+
+static void
+update_language_label (CcRegionPanel *self)
+{
+ const gchar *language = get_effective_language (self);
+ g_autofree gchar *name = NULL;
+
+ if (language)
+ name = gnome_get_language_from_locale (language, language);
+
+ if (!name)
+ name = gnome_get_language_from_locale (DEFAULT_LOCALE, DEFAULT_LOCALE);
+
+ gtk_label_set_label (self->language_label, name);
+
+ /* Formats will change too if not explicitly set. */
+ update_region_label (self);
+}
+
+static void
+update_language_from_user (CcRegionPanel *self)
+{
+ const gchar *language = NULL;
+
+ if (act_user_is_loaded (self->user))
+ language = act_user_get_language (self->user);
+
+ if (language == NULL || *language == '\0')
+ language = setlocale (LC_MESSAGES, NULL);
+
+ g_free (self->language);
+ self->language = g_strdup (language);
+ update_language_label (self);
+}
+
+static void
+setup_language_section (CcRegionPanel *self)
+{
+ self->user = act_user_manager_get_user_by_id (self->user_manager, getuid ());
+ g_signal_connect_object (self->user, "notify::language",
+ G_CALLBACK (update_language_from_user), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->user, "notify::is-loaded",
+ G_CALLBACK (update_language_from_user), self, G_CONNECT_SWAPPED);
+
+ self->locale_settings = g_settings_new (GNOME_SYSTEM_LOCALE_DIR);
+ g_signal_connect_object (self->locale_settings, "changed::" KEY_REGION,
+ G_CALLBACK (update_region_from_setting), self, G_CONNECT_SWAPPED);
+
+ gtk_list_box_set_selection_mode (self->language_list,
+ GTK_SELECTION_NONE);
+ gtk_list_box_set_header_func (self->language_list,
+ cc_list_box_update_header_func,
+ NULL, NULL);
+ g_signal_connect_object (self->language_list, "row-activated",
+ G_CALLBACK (activate_language_row), self, G_CONNECT_SWAPPED);
+
+ update_language_from_user (self);
+ update_region_from_setting (self);
+}
+
+#ifdef HAVE_IBUS
+static void
+update_ibus_active_sources (CcRegionPanel *self)
+{
+ g_autoptr(GList) rows = NULL;
+ GList *l;
+
+ rows = gtk_container_get_children (GTK_CONTAINER (self->input_list));
+ for (l = rows; l; l = l->next) {
+ CcInputRow *row;
+ CcInputSourceIBus *source;
+ IBusEngineDesc *engine_desc;
+
+ if (!CC_IS_INPUT_ROW (l->data))
+ continue;
+ row = CC_INPUT_ROW (l->data);
+
+ if (!CC_IS_INPUT_SOURCE_IBUS (cc_input_row_get_source (row)))
+ continue;
+ source = CC_INPUT_SOURCE_IBUS (cc_input_row_get_source (row));
+
+ engine_desc = g_hash_table_lookup (self->ibus_engines, cc_input_source_ibus_get_engine_name (source));
+ if (engine_desc != NULL)
+ cc_input_source_ibus_set_engine_desc (source, engine_desc);
+ }
+}
+
+static void
+fetch_ibus_engines_result (GObject *object,
+ GAsyncResult *result,
+ CcRegionPanel *self)
+{
+ g_autoptr(GList) list = NULL;
+ GList *l;
+ g_autoptr(GError) 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);
+ return;
+ }
+
+ /* Maps engine ids to engine description objects */
+ self->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 = ibus_engine_desc_get_name (engine);
+
+ if (g_str_has_prefix (engine_id, "xkb:"))
+ g_object_unref (engine);
+ else
+ g_hash_table_replace (self->ibus_engines, (gpointer)engine_id, engine);
+ }
+
+ update_ibus_active_sources (self);
+}
+
+static void
+fetch_ibus_engines (CcRegionPanel *self)
+{
+ ibus_bus_list_engines_async (self->ibus,
+ -1,
+ cc_panel_get_cancellable (CC_PANEL (self)),
+ (GAsyncReadyCallback)fetch_ibus_engines_result,
+ self);
+
+ /* We've got everything we needed, don't want to be called again. */
+ g_signal_handlers_disconnect_by_func (self->ibus, fetch_ibus_engines, self);
+}
+
+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
+row_settings_cb (CcRegionPanel *self,
+ CcInputRow *row)
+{
+ CcInputSourceIBus *source;
+ g_autoptr(GdkAppLaunchContext) ctx = NULL;
+ GDesktopAppInfo *app_info;
+ g_autoptr(GError) error = NULL;
+
+ g_return_if_fail (CC_IS_INPUT_SOURCE_IBUS (cc_input_row_get_source (row)));
+ source = CC_INPUT_SOURCE_IBUS (cc_input_row_get_source (row));
+
+ app_info = cc_input_source_ibus_get_app_info (source);
+ if (app_info == NULL)
+ return;
+
+ ctx = gdk_display_get_app_launch_context (gdk_display_get_default ());
+ gdk_app_launch_context_set_timestamp (ctx, gtk_get_current_event_time ());
+
+ g_app_launch_context_setenv (G_APP_LAUNCH_CONTEXT (ctx),
+ "IBUS_ENGINE_NAME", cc_input_source_ibus_get_engine_name (source));
+
+ if (!g_app_info_launch (G_APP_INFO (app_info), NULL, G_APP_LAUNCH_CONTEXT (ctx), &error))
+ g_warning ("Failed to launch input source setup: %s", error->message);
+}
+
+static void
+row_layout_cb (CcRegionPanel *self,
+ CcInputRow *row)
+{
+ CcInputSource *source;
+ const gchar *layout, *layout_variant;
+ g_autofree gchar *commandline = NULL;
+
+ source = cc_input_row_get_source (row);
+
+ layout = cc_input_source_get_layout (source);
+ layout_variant = cc_input_source_get_layout_variant (source);
+
+ if (layout_variant && layout_variant[0])
+ commandline = g_strdup_printf ("gkbd-keyboard-display -l \"%s\t%s\"",
+ layout, layout_variant);
+ else
+ commandline = g_strdup_printf ("gkbd-keyboard-display -l %s",
+ layout);
+
+ g_spawn_command_line_async (commandline, NULL);
+}
+
+static void move_input (CcRegionPanel *self, CcInputRow *source, CcInputRow *dest);
+
+static void
+row_moved_cb (CcRegionPanel *self,
+ CcInputRow *dest_row,
+ CcInputRow *row)
+{
+ move_input (self, row, dest_row);
+}
+
+static void remove_input (CcRegionPanel *self, CcInputRow *row);
+
+static void
+row_removed_cb (CcRegionPanel *self,
+ CcInputRow *row)
+{
+ remove_input (self, row);
+}
+
+static void
+update_input_rows (CcRegionPanel *self)
+{
+ g_autoptr(GList) rows = NULL;
+ GList *l;
+ guint n_input_rows = 0;
+
+ rows = gtk_container_get_children (GTK_CONTAINER (self->input_list));
+ for (l = rows; l; l = l->next)
+ if (CC_IS_INPUT_ROW (l->data))
+ n_input_rows++;
+ for (l = rows; l; l = l->next) {
+ CcInputRow *row;
+
+ if (!CC_IS_INPUT_ROW (l->data))
+ continue;
+ row = CC_INPUT_ROW (l->data);
+
+ cc_input_row_set_removable (row, n_input_rows > 1);
+ cc_input_row_set_draggable (row, n_input_rows > 1);
+ }
+}
+
+static void
+add_input_row (CcRegionPanel *self, CcInputSource *source)
+{
+ CcInputRow *row;
+
+ gtk_widget_set_visible (GTK_WIDGET (self->no_inputs_row), FALSE);
+
+ row = cc_input_row_new (source);
+ gtk_widget_show (GTK_WIDGET (row));
+ gtk_size_group_add_widget (self->input_size_group, GTK_WIDGET (row));
+ g_signal_connect_object (row, "show-settings", G_CALLBACK (row_settings_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (row, "show-layout", G_CALLBACK (row_layout_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (row, "move-row", G_CALLBACK (row_moved_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (row, "remove-row", G_CALLBACK (row_removed_cb), self, G_CONNECT_SWAPPED);
+ gtk_list_box_insert (GTK_LIST_BOX (self->input_list), GTK_WIDGET (row), gtk_list_box_row_get_index (self->add_input_row));
+ update_input_rows (self);
+}
+
+static void
+add_input_sources (CcRegionPanel *self,
+ GVariant *sources)
+{
+ GVariantIter iter;
+ const gchar *type, *id;
+
+ if (g_variant_n_children (sources) < 1) {
+ gtk_widget_set_visible (GTK_WIDGET (self->no_inputs_row), TRUE);
+ return;
+ }
+
+ g_variant_iter_init (&iter, sources);
+ while (g_variant_iter_next (&iter, "(&s&s)", &type, &id)) {
+ g_autoptr(CcInputSource) source = NULL;
+
+ if (g_str_equal (type, "xkb")) {
+ source = CC_INPUT_SOURCE (cc_input_source_xkb_new_from_id (self->xkb_info, id));
+ } else if (g_str_equal (type, "ibus")) {
+ source = CC_INPUT_SOURCE (cc_input_source_ibus_new (id));
+#ifdef HAVE_IBUS
+ if (self->ibus_engines) {
+ IBusEngineDesc *engine_desc = g_hash_table_lookup (self->ibus_engines, id);
+ if (engine_desc != NULL)
+ cc_input_source_ibus_set_engine_desc (CC_INPUT_SOURCE_IBUS (source), engine_desc);
+ }
+#endif
+ } else {
+ g_warning ("Unhandled input source type '%s'", type);
+ continue;
+ }
+
+ add_input_row (self, source);
+ }
+}
+
+static void
+add_input_sources_from_settings (CcRegionPanel *self)
+{
+ g_autoptr(GVariant) sources = NULL;
+ sources = g_settings_get_value (self->input_settings, "sources");
+ add_input_sources (self, sources);
+}
+
+static void
+clear_input_sources (CcRegionPanel *self)
+{
+ g_autoptr(GList) list = NULL;
+ GList *l;
+
+ list = gtk_container_get_children (GTK_CONTAINER (self->input_list));
+ for (l = list; l; l = l->next) {
+ if (CC_IS_INPUT_ROW (l->data))
+ gtk_container_remove (GTK_CONTAINER (self->input_list), GTK_WIDGET (l->data));
+ }
+
+ cc_list_box_adjust_scrolling (self->input_list);
+}
+
+static CcInputRow *
+get_row_by_source (CcRegionPanel *self, CcInputSource *source)
+{
+ g_autoptr(GList) list = NULL;
+ GList *l;
+
+ list = gtk_container_get_children (GTK_CONTAINER (self->input_list));
+ for (l = list; l; l = l->next) {
+ CcInputRow *row;
+
+ if (!CC_IS_INPUT_ROW (l->data))
+ continue;
+ row = CC_INPUT_ROW (l->data);
+
+ if (cc_input_source_matches (source, cc_input_row_get_source (row)))
+ return row;
+ }
+
+ return NULL;
+}
+
+static void
+input_sources_changed (CcRegionPanel *self,
+ const gchar *key)
+{
+ CcInputRow *selected;
+ g_autoptr(CcInputSource) source = NULL;
+
+ selected = CC_INPUT_ROW (gtk_list_box_get_selected_row (self->input_list));
+ if (selected)
+ source = g_object_ref (cc_input_row_get_source (selected));
+ clear_input_sources (self);
+ add_input_sources_from_settings (self);
+ if (source != NULL) {
+ CcInputRow *row = get_row_by_source (self, source);
+ if (row != NULL)
+ gtk_list_box_select_row (GTK_LIST_BOX (self->input_list), GTK_LIST_BOX_ROW (row));
+ }
+}
+
+static void
+set_input_settings (CcRegionPanel *self)
+{
+ GVariantBuilder builder;
+ g_autoptr(GList) list = NULL;
+ GList *l;
+
+ g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(ss)"));
+ list = gtk_container_get_children (GTK_CONTAINER (self->input_list));
+ for (l = list; l; l = l->next) {
+ CcInputRow *row;
+ CcInputSource *source;
+
+ if (!CC_IS_INPUT_ROW (l->data))
+ continue;
+ row = CC_INPUT_ROW (l->data);
+ source = cc_input_row_get_source (row);
+
+ if (CC_IS_INPUT_SOURCE_XKB (source)) {
+ g_autofree gchar *id = cc_input_source_xkb_get_id (CC_INPUT_SOURCE_XKB (source));
+ g_variant_builder_add (&builder, "(ss)", "xkb", id);
+ } else if (CC_IS_INPUT_SOURCE_IBUS (source)) {
+ g_variant_builder_add (&builder, "(ss)", "ibus",
+ cc_input_source_ibus_get_engine_name (CC_INPUT_SOURCE_IBUS (source)));
+ }
+ }
+
+ g_settings_set_value (self->input_settings, KEY_INPUT_SOURCES, g_variant_builder_end (&builder));
+}
+
+static void set_localed_input (CcRegionPanel *self);
+
+static void
+update_input (CcRegionPanel *self)
+{
+ if (self->login) {
+ set_localed_input (self);
+ } else {
+ set_input_settings (self);
+ if (self->login_auto_apply)
+ set_localed_input (self);
+ }
+}
+
+static void
+show_input_chooser (CcRegionPanel *self)
+{
+ CcInputChooser *chooser;
+
+ chooser = cc_input_chooser_new (self->login,
+ self->xkb_info,
+#ifdef HAVE_IBUS
+ self->ibus_engines
+#else
+ NULL
+#endif
+ );
+ gtk_window_set_transient_for (GTK_WINDOW (chooser), GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (self))));
+
+ if (gtk_dialog_run (GTK_DIALOG (chooser)) == GTK_RESPONSE_OK) {
+ CcInputSource *source;
+
+ source = cc_input_chooser_get_source (chooser);
+ if (source != NULL && get_row_by_source (self, source) == NULL) {
+ add_input_row (self, source);
+ update_input (self);
+ }
+ }
+ gtk_widget_destroy (GTK_WIDGET (chooser));
+}
+
+static void
+add_input_permission_cb (GObject *source, GAsyncResult *res, gpointer user_data)
+{
+ CcRegionPanel *self = user_data;
+ if (permission_acquired (G_PERMISSION (source), res, "add input"))
+ show_input_chooser (self);
+}
+
+static void
+add_input (CcRegionPanel *self)
+{
+ if (!self->login) {
+ show_input_chooser (self);
+ } else if (g_permission_get_allowed (self->permission)) {
+ show_input_chooser (self);
+ } else if (g_permission_get_can_acquire (self->permission)) {
+ g_permission_acquire_async (self->permission,
+ cc_panel_get_cancellable (CC_PANEL (self)),
+ add_input_permission_cb,
+ self);
+ }
+}
+
+static GtkWidget *
+find_sibling (GtkContainer *container, GtkWidget *child)
+{
+ g_autoptr(GList) list = NULL;
+ GList *c, *l;
+ GtkWidget *sibling;
+
+ list = gtk_container_get_children (container);
+ c = g_list_find (list, child);
+
+ for (l = c->next; l; l = l->next) {
+ sibling = l->data;
+ if (gtk_widget_get_visible (sibling) && gtk_widget_get_child_visible (sibling))
+ return sibling;
+ }
+
+ for (l = c->prev; l; l = l->prev) {
+ sibling = l->data;
+ if (gtk_widget_get_visible (sibling) && gtk_widget_get_child_visible (sibling))
+ return sibling;
+ }
+
+ return NULL;
+}
+
+static void
+do_remove_input (CcRegionPanel *self, CcInputRow *row)
+{
+ GtkWidget *sibling;
+
+ sibling = find_sibling (GTK_CONTAINER (self->input_list), GTK_WIDGET (row));
+ gtk_container_remove (GTK_CONTAINER (self->input_list), GTK_WIDGET (row));
+ gtk_list_box_select_row (self->input_list, GTK_LIST_BOX_ROW (sibling));
+
+ cc_list_box_adjust_scrolling (self->input_list);
+
+ update_input (self);
+ update_input_rows (self);
+}
+
+static void
+remove_input_permission_cb (GObject *source, GAsyncResult *res, gpointer user_data)
+{
+ RowData *data = user_data;
+ if (permission_acquired (G_PERMISSION (source), res, "remove input"))
+ do_remove_input (data->panel, data->source);
+}
+
+static void
+remove_input (CcRegionPanel *self, CcInputRow *row)
+{
+ if (!self->login) {
+ do_remove_input (self, row);
+ } else if (g_permission_get_allowed (self->permission)) {
+ do_remove_input (self, row);
+ } else if (g_permission_get_can_acquire (self->permission)) {
+ g_permission_acquire_async (self->permission,
+ cc_panel_get_cancellable (CC_PANEL (self)),
+ remove_input_permission_cb,
+ row_data_new (self, row, NULL));
+ }
+}
+
+static void
+do_move_input (CcRegionPanel *self, CcInputRow *source, CcInputRow *dest)
+{
+ gint dest_index;
+
+ dest_index = gtk_list_box_row_get_index (GTK_LIST_BOX_ROW (dest));
+
+ g_object_ref (source);
+ gtk_container_remove (GTK_CONTAINER (self->input_list), GTK_WIDGET (source));
+ gtk_list_box_insert (self->input_list, GTK_WIDGET (source), dest_index);
+ g_object_unref (source);
+
+ cc_list_box_adjust_scrolling (self->input_list);
+
+ update_input (self);
+}
+
+static void
+move_input_permission_cb (GObject *source, GAsyncResult *res, gpointer user_data)
+{
+ RowData *data = user_data;
+ if (permission_acquired (G_PERMISSION (source), res, "move input"))
+ do_move_input (data->panel, data->source, data->dest);
+}
+
+static void
+move_input (CcRegionPanel *self,
+ CcInputRow *source,
+ CcInputRow *dest)
+{
+ if (!self->login) {
+ do_move_input (self, source, dest);
+ } else if (g_permission_get_allowed (self->permission)) {
+ do_move_input (self, source, dest);
+ } else if (g_permission_get_can_acquire (self->permission)) {
+ g_permission_acquire_async (self->permission,
+ cc_panel_get_cancellable (CC_PANEL (self)),
+ move_input_permission_cb,
+ row_data_new (self, source, dest));
+ }
+}
+
+static void
+input_row_activated_cb (CcRegionPanel *self, GtkListBoxRow *row)
+{
+ if (row == self->add_input_row) {
+ add_input (self);
+ }
+}
+
+static void
+update_shortcut_label (GtkLabel *label,
+ const gchar *value)
+{
+ g_autofree gchar *text = NULL;
+ guint accel_key;
+ g_autofree guint *keycode = NULL;
+ GdkModifierType mods;
+
+ if (value == NULL || *value == '\0') {
+ gtk_widget_hide (GTK_WIDGET (label));
+ return;
+ }
+
+ gtk_accelerator_parse_with_keycode (value, &accel_key, &keycode, &mods);
+ if (accel_key == 0 && keycode == NULL && mods == 0) {
+ g_warning ("Failed to parse keyboard shortcut: '%s'", value);
+ gtk_widget_hide (GTK_WIDGET (label));
+ return;
+ }
+
+ text = gtk_accelerator_get_label_with_keycode (gtk_widget_get_display (GTK_WIDGET (label)), accel_key, *keycode, mods);
+ gtk_label_set_text (label, text);
+}
+
+static void
+update_shortcuts (CcRegionPanel *self)
+{
+ g_auto(GStrv) previous = NULL;
+ g_auto(GStrv) next = NULL;
+ g_autofree gchar *previous_shortcut = NULL;
+ g_autoptr(GSettings) settings = NULL;
+
+ settings = g_settings_new ("org.gnome.desktop.wm.keybindings");
+
+ previous = g_settings_get_strv (settings, "switch-input-source-backward");
+ next = g_settings_get_strv (settings, "switch-input-source");
+
+ previous_shortcut = g_strdup (previous[0]);
+
+ update_shortcut_label (self->previous_source, previous_shortcut);
+ update_shortcut_label (self->next_source, next[0]);
+}
+
+static void
+update_modifiers_shortcut (CcRegionPanel *self)
+{
+ g_auto(GStrv) options = NULL;
+ gchar **p;
+ g_autoptr(GSettings) settings = NULL;
+ g_autoptr(GnomeXkbInfo) xkb_info = NULL;
+ const gchar *text;
+
+ xkb_info = gnome_xkb_info_new ();
+ settings = g_settings_new ("org.gnome.desktop.input-sources");
+ options = g_settings_get_strv (settings, "xkb-options");
+
+ for (p = options; p && *p; ++p)
+ if (g_str_has_prefix (*p, "grp:"))
+ break;
+
+ if (p && *p) {
+ text = gnome_xkb_info_description_for_option (xkb_info, "grp", *p);
+ gtk_label_set_text (self->alt_next_source, text);
+ } else {
+ gtk_widget_hide (GTK_WIDGET (self->alt_next_source));
+ }
+}
+
+static void
+setup_input_section (CcRegionPanel *self)
+{
+ self->input_settings = g_settings_new (GNOME_DESKTOP_INPUT_SOURCES_DIR);
+
+ self->xkb_info = gnome_xkb_info_new ();
+
+#ifdef HAVE_IBUS
+ ibus_init ();
+ if (!self->ibus) {
+ self->ibus = ibus_bus_new_async ();
+ if (ibus_bus_is_connected (self->ibus))
+ fetch_ibus_engines (self);
+ else
+ g_signal_connect_object (self->ibus, "connected",
+ G_CALLBACK (fetch_ibus_engines), self,
+ G_CONNECT_SWAPPED);
+ }
+ maybe_start_ibus ();
+#endif
+
+ gtk_list_box_set_header_func (self->input_list,
+ cc_list_box_update_header_func,
+ NULL, NULL);
+
+ g_signal_connect_object (self->input_settings, "changed::" KEY_INPUT_SOURCES,
+ G_CALLBACK (input_sources_changed), self, G_CONNECT_SWAPPED);
+
+ add_input_sources_from_settings (self);
+
+ g_object_bind_property (self->previous_source, "visible",
+ self->previous_source_label, "visible",
+ G_BINDING_DEFAULT);
+ g_object_bind_property (self->next_source, "visible",
+ self->next_source_label, "visible",
+ G_BINDING_DEFAULT);
+
+ g_settings_bind (self->input_settings, "per-window",
+ self->per_window_source, "active",
+ G_SETTINGS_BIND_DEFAULT);
+ g_settings_bind (self->input_settings, "per-window",
+ self->same_source, "active",
+ G_SETTINGS_BIND_DEFAULT | G_SETTINGS_BIND_INVERT_BOOLEAN);
+
+ update_shortcuts (self);
+ update_modifiers_shortcut (self);
+}
+
+static void
+on_localed_properties_changed (CcRegionPanel *self,
+ GVariant *changed_properties,
+ const gchar **invalidated_properties)
+{
+ g_autoptr(GVariant) v = NULL;
+
+ v = g_dbus_proxy_get_cached_property (G_DBUS_PROXY (self->localed), "Locale");
+ if (v) {
+ g_autofree const gchar **strv = NULL;
+ gsize len;
+ gint i;
+ const gchar *lang, *messages, *time;
+
+ strv = g_variant_get_strv (v, &len);
+
+ lang = messages = time = NULL;
+ for (i = 0; strv[i]; i++) {
+ if (g_str_has_prefix (strv[i], "LANG=")) {
+ lang = strv[i] + strlen ("LANG=");
+ } else if (g_str_has_prefix (strv[i], "LC_MESSAGES=")) {
+ messages = strv[i] + strlen ("LC_MESSAGES=");
+ } else if (g_str_has_prefix (strv[i], "LC_TIME=")) {
+ time = strv[i] + strlen ("LC_TIME=");
+ }
+ }
+ if (!lang) {
+ lang = setlocale (LC_MESSAGES, NULL);
+ }
+ if (!messages) {
+ messages = lang;
+ }
+ g_free (self->system_language);
+ self->system_language = g_strdup (messages);
+ g_free (self->system_region);
+ self->system_region = g_strdup (time);
+
+ update_language_label (self);
+ }
+}
+
+static void
+add_input_sources_from_localed (CcRegionPanel *self)
+{
+ g_autoptr(GVariant) layout_property = NULL;
+ g_autoptr(GVariant) variant_property = NULL;
+ const gchar *s;
+ g_auto(GStrv) layouts = NULL;
+ g_auto(GStrv) variants = NULL;
+ gint i, n;
+
+ if (!self->localed)
+ return;
+
+ layout_property = g_dbus_proxy_get_cached_property (self->localed, "X11Layout");
+ if (layout_property) {
+ s = g_variant_get_string (layout_property, NULL);
+ layouts = g_strsplit (s, ",", -1);
+ }
+
+ variant_property = g_dbus_proxy_get_cached_property (self->localed, "X11Variant");
+ if (variant_property) {
+ s = g_variant_get_string (variant_property, NULL);
+ if (s && *s)
+ variants = g_strsplit (s, ",", -1);
+ }
+
+ if (variants && variants[0])
+ n = MIN (g_strv_length (layouts), g_strv_length (variants));
+ else if (layouts && layouts[0])
+ n = g_strv_length (layouts);
+ else
+ n = 0;
+
+ for (i = 0; i < n && layouts[i][0]; i++) {
+ const char *variant = variants ? variants[i] : NULL;
+ g_autoptr(CcInputSourceXkb) source = cc_input_source_xkb_new (self->xkb_info, layouts[i], variant);
+ add_input_row (self, CC_INPUT_SOURCE (source));
+ }
+ gtk_widget_set_visible (GTK_WIDGET (self->no_inputs_row), n == 0);
+}
+
+static void
+set_localed_locale (CcRegionPanel *self)
+{
+ g_autoptr(GVariantBuilder) b = NULL;
+ g_autofree gchar *lang_value = NULL;
+
+ b = g_variant_builder_new (G_VARIANT_TYPE ("as"));
+ lang_value = g_strconcat ("LANG=", self->system_language, NULL);
+ g_variant_builder_add (b, "s", lang_value);
+
+ if (self->system_region != NULL) {
+ g_autofree gchar *time_value = NULL;
+ g_autofree gchar *numeric_value = NULL;
+ g_autofree gchar *monetary_value = NULL;
+ g_autofree gchar *measurement_value = NULL;
+ g_autofree gchar *paper_value = NULL;
+ time_value = g_strconcat ("LC_TIME=", self->system_region, NULL);
+ g_variant_builder_add (b, "s", time_value);
+ numeric_value = g_strconcat ("LC_NUMERIC=", self->system_region, NULL);
+ g_variant_builder_add (b, "s", numeric_value);
+ monetary_value = g_strconcat ("LC_MONETARY=", self->system_region, NULL);
+ g_variant_builder_add (b, "s", monetary_value);
+ measurement_value = g_strconcat ("LC_MEASUREMENT=", self->system_region, NULL);
+ g_variant_builder_add (b, "s", measurement_value);
+ paper_value = g_strconcat ("LC_PAPER=", self->system_region, NULL);
+ g_variant_builder_add (b, "s", paper_value);
+ }
+ g_dbus_proxy_call (self->localed,
+ "SetLocale",
+ g_variant_new ("(asb)", b, TRUE),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1, NULL, NULL, NULL);
+}
+
+static void
+set_localed_input (CcRegionPanel *self)
+{
+ g_autoptr(GString) layouts = NULL;
+ g_autoptr(GString) variants = NULL;
+ g_autoptr(GList) list = NULL;
+ GList *li;
+
+ layouts = g_string_new ("");
+ variants = g_string_new ("");
+
+ list = gtk_container_get_children (GTK_CONTAINER (self->input_list));
+ for (li = list; li; li = li->next) {
+ CcInputRow *row;
+ CcInputSourceXkb *source;
+ g_autofree gchar *id = NULL;
+ const gchar *l, *v;
+
+ if (!CC_IS_INPUT_ROW (li->data))
+ continue;
+ row = CC_INPUT_ROW (li->data);
+
+ if (!CC_IS_INPUT_SOURCE_XKB (cc_input_row_get_source (row)))
+ continue;
+ source = CC_INPUT_SOURCE_XKB (cc_input_row_get_source (row));
+
+ id = cc_input_source_xkb_get_id (source);
+ if (gnome_xkb_info_get_layout_info (self->xkb_info, id, NULL, NULL, &l, &v)) {
+ if (layouts->str[0]) {
+ g_string_append_c (layouts, ',');
+ g_string_append_c (variants, ',');
+ }
+ g_string_append (layouts, l);
+ g_string_append (variants, v);
+ }
+ }
+
+ g_dbus_proxy_call (self->localed,
+ "SetX11Keyboard",
+ g_variant_new ("(ssssbb)", layouts->str, "", variants->str, "", TRUE, TRUE),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1, NULL, NULL, NULL);
+}
+
+static void
+localed_proxy_ready (GObject *source,
+ GAsyncResult *res,
+ gpointer data)
+{
+ CcRegionPanel *self = data;
+ GDBusProxy *proxy;
+ g_autoptr(GError) error = NULL;
+
+ proxy = g_dbus_proxy_new_finish (res, &error);
+
+ if (!proxy) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("Failed to contact localed: %s\n", error->message);
+ return;
+ }
+
+ self->localed = proxy;
+
+ gtk_widget_set_sensitive (GTK_WIDGET (self->login_button), TRUE);
+
+ g_signal_connect_object (self->localed, "g-properties-changed",
+ G_CALLBACK (on_localed_properties_changed), self, G_CONNECT_SWAPPED);
+ on_localed_properties_changed (self, NULL, NULL);
+}
+
+static void
+login_changed (CcRegionPanel *self)
+{
+ gboolean can_acquire;
+
+ self->login = gtk_toggle_button_get_active (self->login_button);
+ gtk_widget_set_visible (GTK_WIDGET (self->login_label), self->login);
+
+ can_acquire = self->permission &&
+ (g_permission_get_allowed (self->permission) ||
+ g_permission_get_can_acquire (self->permission));
+ /* FIXME: insensitive doesn't look quite right for this */
+ gtk_widget_set_sensitive (GTK_WIDGET (self->language_section_frame), !self->login || can_acquire);
+ gtk_widget_set_sensitive (GTK_WIDGET (self->input_section_box), !self->login || can_acquire);
+
+ clear_input_sources (self);
+ if (self->login)
+ add_input_sources_from_localed (self);
+ else
+ add_input_sources_from_settings (self);
+
+ update_language_label (self);
+}
+
+static void
+set_login_button_visibility (CcRegionPanel *self)
+{
+ gboolean has_multiple_users;
+ gboolean loaded;
+
+ g_object_get (self->user_manager, "is-loaded", &loaded, NULL);
+ if (!loaded)
+ return;
+
+ g_object_get (self->user_manager, "has-multiple-users", &has_multiple_users, NULL);
+
+ self->login_auto_apply = !has_multiple_users && g_permission_get_allowed (self->permission);
+ gtk_widget_set_visible (GTK_WIDGET (self->login_button), !self->login_auto_apply);
+
+ g_signal_handlers_disconnect_by_func (self->user_manager, set_login_button_visibility, self);
+}
+
+static void
+setup_login_button (CcRegionPanel *self)
+{
+ g_autoptr(GDBusConnection) bus = NULL;
+ gboolean loaded;
+ g_autoptr(GError) error = NULL;
+
+ self->permission = polkit_permission_new_sync ("org.freedesktop.locale1.set-locale", NULL, NULL, &error);
+ if (self->permission == NULL) {
+ g_warning ("Could not get 'org.freedesktop.locale1.set-locale' permission: %s",
+ error->message);
+ return;
+ }
+
+ bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, NULL);
+ g_dbus_proxy_new (bus,
+ G_DBUS_PROXY_FLAGS_GET_INVALIDATED_PROPERTIES,
+ NULL,
+ "org.freedesktop.locale1",
+ "/org/freedesktop/locale1",
+ "org.freedesktop.locale1",
+ cc_panel_get_cancellable (CC_PANEL (self)),
+ (GAsyncReadyCallback) localed_proxy_ready,
+ self);
+
+ self->login_button = GTK_TOGGLE_BUTTON (gtk_toggle_button_new_with_mnemonic (_("Login _Screen")));
+ gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (self->login_button)),
+ "text-button");
+ gtk_widget_set_valign (GTK_WIDGET (self->login_button), GTK_ALIGN_CENTER);
+ gtk_widget_set_visible (GTK_WIDGET (self->login_button), FALSE);
+ gtk_widget_set_sensitive (GTK_WIDGET (self->login_button), FALSE);
+ g_signal_connect_object (self->login_button, "notify::active",
+ G_CALLBACK (login_changed), self, G_CONNECT_SWAPPED);
+
+ g_object_get (self->user_manager, "is-loaded", &loaded, NULL);
+ if (loaded)
+ set_login_button_visibility (self);
+ else
+ g_signal_connect_object (self->user_manager, "notify::is-loaded",
+ G_CALLBACK (set_login_button_visibility), self, G_CONNECT_SWAPPED);
+}
+
+static void
+session_proxy_ready (GObject *source,
+ GAsyncResult *res,
+ gpointer data)
+{
+ CcRegionPanel *self = data;
+ GDBusProxy *proxy;
+ g_autoptr(GError) error = NULL;
+
+ proxy = g_dbus_proxy_new_for_bus_finish (res, &error);
+
+ if (!proxy) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("Failed to contact gnome-session: %s\n", error->message);
+ return;
+ }
+
+ self->session = proxy;
+}
+
+static void
+cc_region_panel_class_init (CcRegionPanelClass * klass)
+{
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ CcPanelClass *panel_class = CC_PANEL_CLASS (klass);
+
+ panel_class->get_help_uri = cc_region_panel_get_help_uri;
+
+ object_class->constructed = cc_region_panel_constructed;
+ object_class->finalize = cc_region_panel_finalize;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/region/cc-region-panel.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcRegionPanel, add_input_row);
+ gtk_widget_class_bind_template_child (widget_class, CcRegionPanel, alt_next_source);
+ gtk_widget_class_bind_template_child (widget_class, CcRegionPanel, formats_label);
+ gtk_widget_class_bind_template_child (widget_class, CcRegionPanel, formats_row);
+ gtk_widget_class_bind_template_child (widget_class, CcRegionPanel, input_list);
+ gtk_widget_class_bind_template_child (widget_class, CcRegionPanel, input_section_box);
+ gtk_widget_class_bind_template_child (widget_class, CcRegionPanel, input_size_group);
+ gtk_widget_class_bind_template_child (widget_class, CcRegionPanel, login_label);
+ gtk_widget_class_bind_template_child (widget_class, CcRegionPanel, language_label);
+ gtk_widget_class_bind_template_child (widget_class, CcRegionPanel, language_list);
+ gtk_widget_class_bind_template_child (widget_class, CcRegionPanel, language_row);
+ gtk_widget_class_bind_template_child (widget_class, CcRegionPanel, language_section_frame);
+ gtk_widget_class_bind_template_child (widget_class, CcRegionPanel, next_source);
+ gtk_widget_class_bind_template_child (widget_class, CcRegionPanel, next_source_label);
+ gtk_widget_class_bind_template_child (widget_class, CcRegionPanel, no_inputs_row);
+ gtk_widget_class_bind_template_child (widget_class, CcRegionPanel, options_button);
+ gtk_widget_class_bind_template_child (widget_class, CcRegionPanel, per_window_source);
+ gtk_widget_class_bind_template_child (widget_class, CcRegionPanel, previous_source);
+ gtk_widget_class_bind_template_child (widget_class, CcRegionPanel, previous_source_label);
+ gtk_widget_class_bind_template_child (widget_class, CcRegionPanel, restart_button);
+ gtk_widget_class_bind_template_child (widget_class, CcRegionPanel, restart_revealer);
+ gtk_widget_class_bind_template_child (widget_class, CcRegionPanel, same_source);
+
+ gtk_widget_class_bind_template_callback (widget_class, input_row_activated_cb);
+ gtk_widget_class_bind_template_callback (widget_class, restart_now);
+}
+
+static void
+cc_region_panel_init (CcRegionPanel *self)
+{
+ g_autoptr(GFile) needs_restart_file = NULL;
+
+ g_resources_register (cc_region_get_resource ());
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ self->user_manager = act_user_manager_get_default ();
+
+ g_dbus_proxy_new_for_bus (G_BUS_TYPE_SESSION,
+ G_DBUS_PROXY_FLAGS_NONE,
+ NULL,
+ "org.gnome.SessionManager",
+ "/org/gnome/SessionManager",
+ "org.gnome.SessionManager",
+ cc_panel_get_cancellable (CC_PANEL (self)),
+ session_proxy_ready,
+ self);
+
+ setup_login_button (self);
+ setup_language_section (self);
+ setup_input_section (self);
+
+ needs_restart_file = get_needs_restart_file ();
+ if (g_file_query_exists (needs_restart_file, NULL))
+ set_restart_notification_visible (self, NULL, TRUE);
+}
diff --git a/panels/region/cc-region-panel.h b/panels/region/cc-region-panel.h
new file mode 100644
index 0000000..c0c8dac
--- /dev/null
+++ b/panels/region/cc-region-panel.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2010 Intel, 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/>.
+ *
+ * Author: Sergey Udaltsov <svu@gnome.org>
+ *
+ */
+
+#pragma once
+
+#include <shell/cc-panel.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_REGION_PANEL (cc_region_panel_get_type ())
+G_DECLARE_FINAL_TYPE (CcRegionPanel, cc_region_panel, CC, REGION_PANEL, CcPanel)
+
+G_END_DECLS
diff --git a/panels/region/cc-region-panel.ui b/panels/region/cc-region-panel.ui
new file mode 100644
index 0000000..b779142
--- /dev/null
+++ b/panels/region/cc-region-panel.ui
@@ -0,0 +1,515 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <!-- interface-requires gtk+ 3.0 -->
+ <template class="CcRegionPanel" parent="CcPanel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="hscrollbar-policy">never</property>
+ <child>
+ <object class="HdyClamp">
+ <property name="visible">True</property>
+ <property name="margin_top">32</property>
+ <property name="margin_bottom">32</property>
+ <property name="margin_start">12</property>
+ <property name="margin_end">12</property>
+
+ <!-- Content -->
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">12</property>
+ <property name="hexpand">True</property>
+ <child>
+ <object class="GtkFrame" id="language_section_frame">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_bottom">18</property>
+ <property name="shadow_type">in</property>
+ <child>
+ <object class="GtkListBox" id="language_list">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <child>
+ <object class="GtkListBoxRow" id="language_row">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox" id="language_box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="height_request">50</property>
+ <property name="margin_start">12</property>
+ <property name="margin_end">12</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_bottom">8</property>
+ <property name="margin_top">8</property>
+ <property name="ellipsize">end</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">_Language</property>
+ <property name="use_underline">True</property>
+ <property name="hexpand">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="language_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">1</property>
+ <property name="margin_bottom">8</property>
+ <property name="margin_top">8</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkRevealer" id="restart_revealer">
+ <property name="visible">True</property>
+ <property name="transition_type">slide-up</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="margin_start">12</property>
+ <property name="margin_end">12</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="margin_bottom">8</property>
+ <property name="margin_top">8</property>
+ <property name="xalign">0</property>
+ <property name="hexpand">True</property>
+ <property name="wrap">True</property>
+ <property name="max-width-chars">35</property>
+ <property name="label" translatable="yes">Restart the session for changes to take effect</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ <attributes>
+ <attribute name="scale" value="0.9"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="restart_button">
+ <property name="visible">True</property>
+ <property name="margin_bottom">8</property>
+ <property name="margin_top">8</property>
+ <property name="valign">end</property>
+ <property name="label" translatable="yes">Restart…</property>
+ <style>
+ <class name="suggested-action"/>
+ </style>
+ <signal name="clicked" handler="restart_now" object="CcRegionPanel" swapped="yes"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkListBoxRow" id="formats_row">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="height_request">50</property>
+ <property name="margin_start">12</property>
+ <property name="margin_end">12</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_bottom">8</property>
+ <property name="margin_top">8</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">_Formats</property>
+ <property name="use_underline">True</property>
+ <property name="hexpand">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="formats_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">1</property>
+ <property name="margin_bottom">8</property>
+ <property name="margin_top">8</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label_item">
+ <placeholder/>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="input_section_box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="hexpand">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">6</property>
+ <property name="margin_bottom">12</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkLabel" id="input_heading_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">Input Sources</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ <accessibility>
+ <relation target="input_list" type="label-for"/>
+ </accessibility>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Choose keyboard layouts or input methods.</property>
+ <property name="wrap">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuButton" id="options_button">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="popover">options_popover</property>
+ <property name="hexpand">True</property>
+ <property name="halign">end</property>
+ <property name="valign">start</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="icon_name">emblem-system-symbolic</property>
+ </object>
+ </child>
+ <style>
+ <class name="image-button"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkFrame">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label_xalign">0</property>
+ <property name="shadow_type">in</property>
+ <child>
+ <object class="GtkListBox" id="input_list">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="selection-mode">none</property>
+ <signal name="row_activated" handler="input_row_activated_cb" object="CcRegionPanel" swapped="yes"/>
+ <accessibility>
+ <relation target="input_heading_label" type="labelled-by"/>
+ </accessibility>
+ <child>
+ <object class="GtkListBoxRow" id="no_inputs_row">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="selectable">False</property>
+ <property name="activatable">False</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">No input source selected</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkListBoxRow" id="add_input_row">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="selectable">False</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="hexpand">True</property>
+ <property name="height_request">50</property>
+ <property name="margin_start">12</property>
+ <property name="margin_end">12</property>
+ <property name="icon-name">list-add-symbolic</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="login_label">
+ <property name="valign">end</property>
+ <property name="vexpand">True</property>
+ <property name="hexpand">True</property>
+ <property name="label" translatable="yes">Login settings are used by all users when logging into the system</property>
+ <property name="margin_bottom">12</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+ <object class="GtkPopover" id="options_popover">
+ <property name="can_focus">False</property>
+ <property name="border_width">12</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">2</property>
+ <child>
+ <object class="GtkGrid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_start">6</property>
+ <property name="margin_top">6</property>
+ <property name="margin_bottom">6</property>
+ <property name="row_spacing">6</property>
+ <property name="column_spacing">6</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">6</property>
+ <property name="label" translatable="yes">Input Source Options</property>
+ <property name="margin_bottom">6</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">2</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRadioButton" id="same_source">
+ <property name="label" translatable="yes">Use the _same source for all windows</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="xalign">0</property>
+ <property name="active">True</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ <property name="width">2</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRadioButton" id="per_window_source">
+ <property name="label" translatable="yes">Allow _different sources for each window</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ <property name="xalign">0</property>
+ <property name="active">True</property>
+ <property name="draw_indicator">True</property>
+ <property name="group">same_source</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ <property name="width">2</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">12</property>
+ <property name="margin_bottom">6</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Keyboard Shortcuts</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">3</property>
+ <property name="width">2</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="previous_source_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">Previous source</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">4</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="previous_source">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="hexpand">True</property>
+ <property name="label" translatable="yes">Super+Shift+Space</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">4</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="next_source_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">Next source</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">5</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="next_source">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="hexpand">True</property>
+ <property name="label" translatable="yes">Super+Space</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">5</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="alt_next_source">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="hexpand">True</property>
+ <property name="label" translatable="yes">Left+Right Alt</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">6</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">12</property>
+ <property name="wrap">True</property>
+ <property name="max_width_chars">40</property>
+ <property name="label" translatable="yes">These keyboard shortcuts can be changed in the keyboard settings</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">7</property>
+ <property name="width">2</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <object class="GtkSizeGroup" id="input_size_group">
+ <property name="mode">vertical</property>
+ <widgets>
+ <widget name="no_inputs_row"/>
+ </widgets>
+ </object>
+</interface>
diff --git a/panels/region/gnome-region-panel.desktop.in.in b/panels/region/gnome-region-panel.desktop.in.in
new file mode 100644
index 0000000..34430b2
--- /dev/null
+++ b/panels/region/gnome-region-panel.desktop.in.in
@@ -0,0 +1,18 @@
+[Desktop Entry]
+Name=Region & Language
+Comment=Select your display language, formats, keyboard layouts and input sources
+Exec=gnome-control-center region
+# Translators: Do NOT translate or transliterate this text (this is an icon file name)!
+Icon=preferences-desktop-locale
+Terminal=false
+Type=Application
+NoDisplay=true
+StartupNotify=true
+Categories=GNOME;GTK;Settings;DesktopSettings;X-GNOME-Settings-Panel;X-GNOME-DetailsSettings;
+OnlyShowIn=GNOME;Unity;
+X-GNOME-Bugzilla-Bugzilla=GNOME
+X-GNOME-Bugzilla-Product=gnome-control-center
+X-GNOME-Bugzilla-Component=region
+X-GNOME-Bugzilla-Version=@VERSION@
+# Translators: Search terms to find the Region and Language panel. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon!
+Keywords=Language;Layout;Keyboard;Input;
diff --git a/panels/region/meson.build b/panels/region/meson.build
new file mode 100644
index 0000000..e709eb6
--- /dev/null
+++ b/panels/region/meson.build
@@ -0,0 +1,63 @@
+panels_list += cappletname
+desktop = 'gnome-@0@-panel.desktop'.format(cappletname)
+
+desktop_in = configure_file(
+ input: desktop + '.in.in',
+ output: desktop + '.in',
+ configuration: desktop_conf
+)
+
+i18n.merge_file(
+ desktop,
+ type: 'desktop',
+ input: desktop_in,
+ output: desktop,
+ po_dir: po_dir,
+ install: true,
+ install_dir: control_center_desktopdir
+)
+
+sources = files(
+ 'cc-region-panel.c',
+ 'cc-format-chooser.c',
+ 'cc-ibus-utils.c',
+ 'cc-input-chooser.c',
+ 'cc-input-row.c',
+ 'cc-input-source.c',
+ 'cc-input-source-ibus.c',
+ 'cc-input-source-xkb.c',
+)
+
+resource_data = files(
+ 'cc-format-chooser.ui',
+ 'cc-input-chooser.ui',
+ 'cc-region-panel.ui',
+ 'view-layout-symbolic.svg',
+)
+
+sources += gnome.compile_resources(
+ 'cc-' + cappletname + '-resources',
+ cappletname + '.gresource.xml',
+ c_name: 'cc_' + cappletname,
+ dependencies: resource_data,
+ export: true
+)
+
+deps = common_deps + [
+ accounts_dep,
+ gnome_desktop_dep,
+ liblanguage_dep,
+ polkit_gobject_dep
+]
+
+if enable_ibus
+ deps += ibus_dep
+endif
+
+panels_libs += static_library(
+ cappletname,
+ sources: sources,
+ include_directories: top_inc,
+ dependencies: deps,
+ c_args: cflags
+)
diff --git a/panels/region/region.gresource.xml b/panels/region/region.gresource.xml
new file mode 100644
index 0000000..5b57dde
--- /dev/null
+++ b/panels/region/region.gresource.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/control-center/region">
+ <file preprocess="xml-stripblanks">cc-format-chooser.ui</file>
+ <file preprocess="xml-stripblanks">cc-input-chooser.ui</file>
+ <file preprocess="xml-stripblanks">cc-input-row.ui</file>
+ <file preprocess="xml-stripblanks">cc-region-panel.ui</file>
+ </gresource>
+ <gresource prefix="/org/gnome/ControlCenter/icons/scalable/actions">
+ <file>view-layout-symbolic.svg</file>
+ </gresource>
+</gresources>
diff --git a/panels/region/view-layout-symbolic.svg b/panels/region/view-layout-symbolic.svg
new file mode 100644
index 0000000..dc8b5d0
--- /dev/null
+++ b/panels/region/view-layout-symbolic.svg
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ width="16"
+ viewBox="0 0 16 16"
+ version="1.1"
+ id="svg7384"
+ height="16">
+ <metadata
+ id="metadata90">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title>Gnome Symbolic Icon Theme</dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <title
+ id="title9167">Gnome Symbolic Icon Theme</title>
+ <defs
+ id="defs7386">
+ <linearGradient
+ osb:paint="solid"
+ id="linearGradient7212">
+ <stop
+ style="stop-color:#000000;stop-opacity:1;"
+ offset="0"
+ id="stop7214" />
+ </linearGradient>
+ </defs>
+ <g
+ transform="translate(-341.0002,-13.000323)"
+ style="display:inline"
+ id="layer9" />
+ <g
+ transform="translate(-100,-380.00032)"
+ id="layer1" />
+ <g
+ transform="translate(-100,-380.00032)"
+ style="display:inline"
+ id="layer10">
+ <path
+ d="m 108,382 a 8,8 0 0 0 -7.73828,6.00977 A 8,8 0 0 0 108,394 8,8 0 0 0 115.73828,387.99023 8,8 0 0 0 108,382 Z m 0,2 a 4,4 0 0 1 4,4 4,4 0 0 1 -4,4 4,4 0 0 1 -4,-4 4,4 0 0 1 4,-4 z"
+ id="path2314"
+ style="opacity:1;vector-effect:none;fill:#2e3436;fill-opacity:1;stroke:none;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal" />
+ <path
+ id="path2318"
+ d="m 110,388.00003 a 2,2 0 0 1 -2,2 2,2 0 0 1 -2,-2 2,2 0 0 1 2,-2 2,2 0 0 1 2,2 z"
+ style="vector-effect:none;fill:#2e3436;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ </g>
+ <g
+ transform="translate(-100,-380.00032)"
+ id="g6387" />
+ <g
+ transform="translate(-100,-380.00032)"
+ id="layer11" />
+</svg>