diff options
Diffstat (limited to 'panels/region')
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 & 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)", ¤t_locale); + + if (!gnome_parse_locale (current_locale, + ¤t_lang_code, + ¤t_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> |