/* * 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, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA * 02111-1307, USA. */ #include #include #include #define GNOME_DESKTOP_USE_UNSTABLE_API #include #include "cc-common-language.h" #include "cc-util.h" #include "cc-input-chooser.h" #ifdef HAVE_IBUS #include #include "cc-ibus-utils.h" #endif /* HAVE_IBUS */ #define INPUT_SOURCE_TYPE_XKB "xkb" #define INPUT_SOURCE_TYPE_IBUS "ibus" #define MAIN_WINDOW_WIDTH_RATIO 0.60 #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; typedef struct { /* Not owned */ GtkWidget *add_button; GtkWidget *filter_entry; GtkWidget *list; GtkWidget *scrolledwindow; GtkAdjustment *adjustment; GnomeXkbInfo *xkb_info; GHashTable *ibus_engines; /* Owned */ GtkListBoxRow *more_row; GtkWidget *no_results; GHashTable *locales; GHashTable *locales_by_language; gboolean showing_extra; guint filter_timeout_id; gchar **filter_words; } CcInputChooserPrivate; #define GET_PRIVATE(chooser) ((CcInputChooserPrivate *) g_object_get_data (G_OBJECT (chooser), "private")) #define WID(name) ((GtkWidget *) gtk_builder_get_object (builder, name)) 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_object_unref (info->default_input_source_row); g_object_unref (info->locale_row); g_object_unref (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_left (widget, 20); gtk_widget_set_margin_right (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; gdouble alignment; gboolean rtl; rtl = (gtk_widget_get_default_direction () == GTK_TEXT_DIR_RTL); if (position == ROW_LABEL_POSITION_START) alignment = 0.0; else if (position == ROW_LABEL_POSITION_CENTER) alignment = 0.5; else alignment = 1.0; widget = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); if (direction == ROW_TRAVEL_DIRECTION_BACKWARD) { arrow = gtk_image_new_from_icon_name (rtl ? "go-previous-rtl-symbolic" : "go-previous-symbolic", GTK_ICON_SIZE_MENU); gtk_box_pack_start (GTK_BOX (widget), arrow, FALSE, TRUE, 0); } label = gtk_label_new (text); gtk_misc_set_alignment (GTK_MISC (label), alignment, 0.5); set_row_widget_margins (label); gtk_box_pack_start (GTK_BOX (widget), label, TRUE, TRUE, 0); 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 (rtl ? "go-next-rtl-symbolic" : "go-next-symbolic", GTK_ICON_SIZE_MENU); gtk_box_pack_start (GTK_BOX (widget), arrow, FALSE, TRUE, 0); } 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_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_style_context_add_class (gtk_widget_get_style_context (arrow), "dim-label"); set_row_widget_margins (arrow); gtk_misc_set_alignment (GTK_MISC (arrow), 0.5, 0.5); gtk_box_pack_start (GTK_BOX (box), arrow, TRUE, TRUE, 0); 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_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_container_add (GTK_CONTAINER (row), widget); return GTK_LIST_BOX_ROW (row); } static GtkWidget * locale_header_widget_new (const gchar *text) { GtkWidget *widget; widget = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); gtk_box_pack_start (GTK_BOX (widget), gtk_separator_new (GTK_ORIENTATION_HORIZONTAL), FALSE, FALSE, 0); gtk_box_pack_start (GTK_BOX (widget), padded_label_new (text, ROW_LABEL_POSITION_CENTER, ROW_TRAVEL_DIRECTION_NONE, TRUE), FALSE, FALSE, 0); gtk_box_pack_start (GTK_BOX (widget), gtk_separator_new (GTK_ORIENTATION_HORIZONTAL), FALSE, FALSE, 0); gtk_widget_show_all (widget); return widget; } static GtkListBoxRow * input_source_row_new (GtkWidget *chooser, const gchar *type, const gchar *id) { CcInputChooserPrivate *priv = GET_PRIVATE (chooser); GtkWidget *row = NULL; GtkWidget *widget; if (g_str_equal (type, INPUT_SOURCE_TYPE_XKB)) { const gchar *display_name; gnome_xkb_info_get_layout_info (priv->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_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 (priv->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_container_add (GTK_CONTAINER (row), widget); image = gtk_image_new_from_icon_name ("system-run-symbolic", GTK_ICON_SIZE_MENU); set_row_widget_margins (image); gtk_style_context_add_class (gtk_widget_get_style_context (image), "dim-label"); gtk_box_pack_start (GTK_BOX (widget), image, FALSE, TRUE, 0); 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) { GList *list, *l; list = gtk_container_get_children (container); for (l = list; l; l = l->next) gtk_container_remove (container, (GtkWidget *) l->data); g_list_free (list); } static void set_fixed_size (GtkWidget *chooser) { CcInputChooserPrivate *priv = GET_PRIVATE (chooser); GtkPolicyType policy; gint width, height; gtk_scrolled_window_get_policy (GTK_SCROLLED_WINDOW (priv->scrolledwindow), &policy, NULL); if (policy == GTK_POLICY_AUTOMATIC) return; /* Don't let it automatically get wider than the main CC window nor get taller than the initial height */ gtk_window_get_size (gtk_window_get_transient_for (GTK_WINDOW (chooser)), &width, NULL); gtk_window_get_size (GTK_WINDOW (chooser), NULL, &height); gtk_widget_set_size_request (chooser, width * MAIN_WINDOW_WIDTH_RATIO, height); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (priv->scrolledwindow), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); } static void update_header (GtkListBoxRow *row, GtkListBoxRow *before, gpointer user_data) { GtkWidget *current; current = gtk_list_box_row_get_header (row); if (before == NULL) { if (current) gtk_list_box_row_set_header (row, NULL); return; } if (current == NULL || !GTK_IS_SEPARATOR (current)) { current = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL); gtk_widget_show (current); gtk_list_box_row_set_header (row, current); } } static void add_input_source_rows_for_locale (GtkWidget *chooser, LocaleInfo *info) { CcInputChooserPrivate *priv = GET_PRIVATE (chooser); GtkWidget *row; GHashTableIter iter; const gchar *id; if (info->default_input_source_row) gtk_container_add (GTK_CONTAINER (priv->list), 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 (priv->list), 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 (priv->list), row); } static void show_input_sources_for_locale (GtkWidget *chooser, LocaleInfo *info) { CcInputChooserPrivate *priv = GET_PRIVATE (chooser); set_fixed_size (chooser); remove_all_children (GTK_CONTAINER (priv->list)); if (!info->back_row) { info->back_row = g_object_ref_sink (back_row_new (info->name)); 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 (priv->list), GTK_WIDGET (info->back_row)); add_input_source_rows_for_locale (chooser, info); gtk_widget_show_all (priv->list); gtk_adjustment_set_value (priv->adjustment, gtk_adjustment_get_lower (priv->adjustment)); gtk_list_box_set_header_func (GTK_LIST_BOX (priv->list), update_header, NULL, NULL); gtk_list_box_invalidate_filter (GTK_LIST_BOX (priv->list)); gtk_list_box_set_selection_mode (GTK_LIST_BOX (priv->list), GTK_SELECTION_SINGLE); if (gtk_widget_is_visible (priv->filter_entry) && !gtk_widget_is_focus (priv->filter_entry)) gtk_widget_grab_focus (priv->filter_entry); } static gboolean is_current_locale (const gchar *locale) { return g_strcmp0 (setlocale (LC_CTYPE, NULL), locale) == 0; } static void show_locale_rows (GtkWidget *chooser) { CcInputChooserPrivate *priv = GET_PRIVATE (chooser); GHashTable *initial = NULL; LocaleInfo *info; GHashTableIter iter; remove_all_children (GTK_CONTAINER (priv->list)); if (!priv->showing_extra) initial = cc_common_language_get_initial_languages (); g_hash_table_iter_init (&iter, priv->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)); g_object_set_data (G_OBJECT (info->locale_row), "locale-info", info); if (!priv->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 (priv->list), GTK_WIDGET (info->locale_row)); } gtk_container_add (GTK_CONTAINER (priv->list), GTK_WIDGET (priv->more_row)); gtk_widget_show_all (priv->list); gtk_adjustment_set_value (priv->adjustment, gtk_adjustment_get_lower (priv->adjustment)); gtk_list_box_set_header_func (GTK_LIST_BOX (priv->list), update_header, NULL, NULL); gtk_list_box_invalidate_filter (GTK_LIST_BOX (priv->list)); gtk_list_box_set_selection_mode (GTK_LIST_BOX (priv->list), GTK_SELECTION_NONE); if (gtk_widget_is_visible (priv->filter_entry) && !gtk_widget_is_focus (priv->filter_entry)) gtk_widget_grab_focus (priv->filter_entry); if (!priv->showing_extra) g_hash_table_destroy (initial); } static gint list_sort (gconstpointer a, gconstpointer b, gpointer data) { GtkWidget *chooser = data; CcInputChooserPrivate *priv = GET_PRIVATE (chooser); LocaleInfo *ia; LocaleInfo *ib; const gchar *la; const gchar *lb; gint retval; /* Always goes at the end */ if (a == priv->more_row) return 1; if (b == priv->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 list_filter (GtkListBoxRow *row, gpointer user_data) { GtkDialog *chooser = user_data; CcInputChooserPrivate *priv = GET_PRIVATE (chooser); LocaleInfo *info; gboolean is_extra; const gchar *source_name; if (row == priv->more_row) return !priv->showing_extra; is_extra = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (row), "is-extra")); if (!priv->showing_extra && is_extra) return FALSE; if (!priv->filter_words) return TRUE; info = g_object_get_data (G_OBJECT (row), "locale-info"); if (match_all (priv->filter_words, info->unaccented_name)) return TRUE; if (match_all (priv->filter_words, info->untranslated_name)) return TRUE; source_name = g_object_get_data (G_OBJECT (row), "unaccented-name"); if (source_name && match_all (priv->filter_words, source_name)) return TRUE; return FALSE; } static void update_header_filter (GtkListBoxRow *row, GtkListBoxRow *before, gpointer user_data) { LocaleInfo *row_info = NULL; LocaleInfo *before_info = NULL; GtkWidget *current; if (row) row_info = g_object_get_data (G_OBJECT (row), "locale-info"); if (before) before_info = g_object_get_data (G_OBJECT (before), "locale-info"); if (!row_info && !before_info) return; current = gtk_list_box_row_get_header (row); if (row_info == before_info) { /* Create a regular separator if we don't have one */ if (current && !GTK_IS_SEPARATOR (current)) { gtk_list_box_row_set_header (row, NULL); current = NULL; } if (current == NULL) gtk_list_box_row_set_header (row, gtk_separator_new (GTK_ORIENTATION_HORIZONTAL)); } else { /* Create a locale heading separator if we don't have one */ if (current && GTK_IS_SEPARATOR (current)) { gtk_list_box_row_set_header (row, NULL); current = NULL; } if (current == NULL) gtk_list_box_row_set_header (row, locale_header_widget_new (row_info->name)); } } static void show_filter_widgets (GtkWidget *chooser) { CcInputChooserPrivate *priv = GET_PRIVATE (chooser); LocaleInfo *info; GHashTableIter iter; remove_all_children (GTK_CONTAINER (priv->list)); g_hash_table_iter_init (&iter, priv->locales); while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &info)) add_input_source_rows_for_locale (chooser, info); gtk_widget_show_all (priv->list); gtk_adjustment_set_value (priv->adjustment, gtk_adjustment_get_lower (priv->adjustment)); gtk_list_box_set_header_func (GTK_LIST_BOX (priv->list), update_header_filter, NULL, NULL); gtk_list_box_invalidate_filter (GTK_LIST_BOX (priv->list)); gtk_list_box_set_selection_mode (GTK_LIST_BOX (priv->list), GTK_SELECTION_SINGLE); if (gtk_widget_is_visible (priv->filter_entry) && !gtk_widget_is_focus (priv->filter_entry)) gtk_widget_grab_focus (priv->filter_entry); } 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 (GtkWidget *chooser) { CcInputChooserPrivate *priv = GET_PRIVATE (chooser); gboolean was_filtering; gchar **previous_words; gchar *filter_contents = NULL; priv->filter_timeout_id = 0; previous_words = priv->filter_words; was_filtering = previous_words != NULL; filter_contents = cc_util_normalize_casefold_and_unaccent (gtk_entry_get_text (GTK_ENTRY (priv->filter_entry))); if (filter_contents) { priv->filter_words = g_strsplit_set (g_strstrip (filter_contents), " ", 0); g_free (filter_contents); } if (!priv->filter_words || !priv->filter_words[0]) { g_clear_pointer (&priv->filter_words, g_strfreev); if (was_filtering) show_locale_rows (chooser); gtk_list_box_set_placeholder (GTK_LIST_BOX (priv->list), NULL); } else { if (!was_filtering) show_filter_widgets (chooser); else if (strvs_differ (priv->filter_words, previous_words)) { gtk_list_box_invalidate_filter (GTK_LIST_BOX (priv->list)); gtk_list_box_set_placeholder (GTK_LIST_BOX (priv->list), priv->no_results); } } g_strfreev (previous_words); return G_SOURCE_REMOVE; } static void filter_changed (GtkWidget *chooser) { CcInputChooserPrivate *priv = GET_PRIVATE (chooser); if (priv->filter_timeout_id == 0) priv->filter_timeout_id = g_timeout_add (FILTER_TIMEOUT, (GSourceFunc) do_filter, chooser); } static void show_more (GtkWidget *chooser) { CcInputChooserPrivate *priv = GET_PRIVATE (chooser); set_fixed_size (chooser); gtk_widget_show (priv->filter_entry); gtk_widget_grab_focus (priv->filter_entry); priv->showing_extra = TRUE; gtk_list_box_invalidate_filter (GTK_LIST_BOX (priv->list)); } static void row_activated (GtkListBox *box, GtkListBoxRow *row, GtkWidget *chooser) { CcInputChooserPrivate *priv = GET_PRIVATE (chooser); gpointer data; if (!row) return; if (row == priv->more_row) { show_more (chooser); return; } data = g_object_get_data (G_OBJECT (row), "back"); if (data) { show_locale_rows (chooser); return; } data = g_object_get_data (G_OBJECT (row), "name"); if (data) { /* It's an input source, we just want to select it */ return; } data = g_object_get_data (G_OBJECT (row), "locale-info"); if (data) { show_input_sources_for_locale (chooser, (LocaleInfo *) data); return; } } static void row_selected (GtkListBox *box, GtkListBoxRow *row, GtkWidget *chooser) { CcInputChooserPrivate *priv = GET_PRIVATE (chooser); gtk_widget_set_sensitive (priv->add_button, row != NULL); } static void add_default_row (GtkWidget *chooser, LocaleInfo *info, const gchar *type, const gchar *id) { info->default_input_source_row = input_source_row_new (chooser, type, id); if (info->default_input_source_row) { g_object_ref_sink (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 (GtkWidget *chooser, 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 (chooser, type, id); 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 (GtkWidget *chooser, LocaleInfo *info, const gchar *type, const gchar *id) { GList tmp = { 0 }; tmp.data = (gpointer) id; add_rows_to_table (chooser, info, &tmp, type, NULL); } static void add_row_other (GtkWidget *chooser, const gchar *type, const gchar *id) { CcInputChooserPrivate *priv = GET_PRIVATE (chooser); LocaleInfo *info = g_hash_table_lookup (priv->locales, ""); add_row (chooser, info, type, id); } #ifdef HAVE_IBUS static gboolean maybe_set_as_default (GtkWidget *chooser, 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 (chooser, info, type, id); return TRUE; } return FALSE; } static void get_ibus_locale_infos (GtkWidget *chooser) { CcInputChooserPrivate *priv = GET_PRIVATE (chooser); GHashTableIter iter; LocaleInfo *info; const gchar *engine_id; IBusEngineDesc *engine; if (!priv->ibus_engines) return; g_hash_table_iter_init (&iter, priv->ibus_engines); while (g_hash_table_iter_next (&iter, (gpointer *) &engine_id, (gpointer *) &engine)) { gchar *lang_code = NULL; 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) { gchar *locale = g_strdup_printf ("%s_%s.utf8", lang_code, country_code); info = g_hash_table_lookup (priv->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 (chooser, info, type, id); } else { add_row (chooser, info, INPUT_SOURCE_TYPE_IBUS, engine_id); } } else { add_row_other (chooser, INPUT_SOURCE_TYPE_IBUS, engine_id); } g_free (locale); } else if (lang_code != NULL) { GHashTableIter iter; GHashTable *locales_for_language; gchar *language; /* 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 (priv->locales_by_language, language); else locales_for_language = NULL; g_free (language); 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 (chooser, info, engine_id)) add_row (chooser, info, INPUT_SOURCE_TYPE_IBUS, engine_id); } else { add_row_other (chooser, INPUT_SOURCE_TYPE_IBUS, engine_id); } } else { add_row_other (chooser, INPUT_SOURCE_TYPE_IBUS, engine_id); } g_free (country_code); g_free (lang_code); } } #endif /* HAVE_IBUS */ static void add_locale_to_table (GHashTable *table, const gchar *lang_code, LocaleInfo *info) { GHashTable *set; gchar *language; 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); g_free (language); } 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 (GtkWidget *chooser) { CcInputChooserPrivate *priv = GET_PRIVATE (chooser); GHashTable *layouts_with_locale; LocaleInfo *info; gchar **locale_ids; gchar **locale; GList *list, *l; priv->locales = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, locale_info_free); priv->locales_by_language = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_hash_table_destroy); 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) { gchar *lang_code, *country_code; gchar *simple_locale; gchar *tmp; const gchar *type = NULL; const gchar *id = NULL; if (!gnome_parse_locale (*locale, &lang_code, &country_code, NULL, NULL)) continue; simple_locale = g_strdup_printf ("%s_%s.utf8", lang_code, country_code); if (g_hash_table_contains (priv->locales, simple_locale)) { g_free (simple_locale); g_free (country_code); g_free (lang_code); continue; } info = g_new0 (LocaleInfo, 1); info->id = simple_locale; /* Take ownership */ 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_free (tmp); g_hash_table_replace (priv->locales, simple_locale, info); add_locale_to_table (priv->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 (chooser, 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); list = gnome_xkb_info_get_layouts_for_language (priv->xkb_info, lang_code); add_rows_to_table (chooser, info, list, INPUT_SOURCE_TYPE_XKB, id); add_ids_to_set (layouts_with_locale, list); g_list_free (list); list = gnome_xkb_info_get_layouts_for_country (priv->xkb_info, country_code); add_rows_to_table (chooser, info, list, INPUT_SOURCE_TYPE_XKB, id); add_ids_to_set (layouts_with_locale, list); g_list_free (list); g_free (lang_code); g_free (country_code); } g_strfreev (locale_ids); /* 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 (priv->locales, 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); list = gnome_xkb_info_get_all_layouts (priv->xkb_info); for (l = list; l; l = l->next) if (!g_hash_table_contains (layouts_with_locale, l->data)) add_row_other (chooser, INPUT_SOURCE_TYPE_XKB, l->data); g_list_free (list); g_hash_table_destroy (layouts_with_locale); } static void cc_input_chooser_private_free (gpointer data) { CcInputChooserPrivate *priv = data; g_object_unref (priv->more_row); g_object_unref (priv->no_results); g_hash_table_destroy (priv->locales); g_hash_table_destroy (priv->locales_by_language); g_strfreev (priv->filter_words); if (priv->filter_timeout_id) g_source_remove (priv->filter_timeout_id); g_free (priv); } GtkWidget * cc_input_chooser_new (GtkWindow *main_window, GnomeXkbInfo *xkb_info, GHashTable *ibus_engines) { GtkBuilder *builder; GtkWidget *chooser; CcInputChooserPrivate *priv; gint width; GError *error = NULL; builder = gtk_builder_new (); if (gtk_builder_add_from_resource (builder, "/org/gnome/control-center/region/input-chooser.ui", &error) == 0) { g_object_unref (builder); g_warning ("failed to load input chooser: %s", error->message); g_error_free (error); return NULL; } chooser = WID ("input-dialog"); priv = g_new0 (CcInputChooserPrivate, 1); g_object_set_data_full (G_OBJECT (chooser), "private", priv, cc_input_chooser_private_free); g_object_set_data_full (G_OBJECT (chooser), "builder", builder, g_object_unref); priv->xkb_info = xkb_info; priv->ibus_engines = ibus_engines; priv->add_button = WID ("add-button"); priv->filter_entry = WID ("filter-entry"); priv->list = WID ("list"); priv->scrolledwindow = WID ("scrolledwindow"); priv->adjustment = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (priv->scrolledwindow)); priv->more_row = g_object_ref_sink (more_row_new ()); priv->no_results = g_object_ref_sink (no_results_widget_new ()); gtk_widget_show_all (priv->no_results); gtk_list_box_set_adjustment (GTK_LIST_BOX (priv->list), priv->adjustment); gtk_list_box_set_filter_func (GTK_LIST_BOX (priv->list), list_filter, chooser, NULL); gtk_list_box_set_sort_func (GTK_LIST_BOX (priv->list), (GtkListBoxSortFunc)list_sort, chooser, NULL); g_signal_connect (priv->list, "row-activated", G_CALLBACK (row_activated), chooser); g_signal_connect (priv->list, "row-selected", G_CALLBACK (row_selected), chooser); g_signal_connect_swapped (priv->filter_entry, "search-changed", G_CALLBACK (filter_changed), chooser); get_locale_infos (chooser); #ifdef HAVE_IBUS get_ibus_locale_infos (chooser); #endif /* HAVE_IBUS */ show_locale_rows (chooser); /* Try to come up with a sensible width */ gtk_window_get_size (main_window, &width, NULL); gtk_widget_set_size_request (chooser, width * MAIN_WINDOW_WIDTH_RATIO, -1); gtk_window_set_resizable (GTK_WINDOW (chooser), TRUE); gtk_window_set_transient_for (GTK_WINDOW (chooser), main_window); return chooser; } void cc_input_chooser_set_ibus_engines (GtkWidget *chooser, GHashTable *ibus_engines) { #ifdef HAVE_IBUS CcInputChooserPrivate *priv = GET_PRIVATE (chooser); /* 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 (priv->ibus_engines == NULL); priv->ibus_engines = ibus_engines; get_ibus_locale_infos (chooser); show_locale_rows (chooser); #endif /* HAVE_IBUS */ } gboolean cc_input_chooser_get_selected (GtkWidget *chooser, gchar **type, gchar **id, gchar **name) { CcInputChooserPrivate *priv = GET_PRIVATE (chooser); GtkListBoxRow *selected; const gchar *t, *i, *n; selected = gtk_list_box_get_selected_row (GTK_LIST_BOX (priv->list)); if (!selected) return FALSE; t = g_object_get_data (G_OBJECT (selected), "type"); i = g_object_get_data (G_OBJECT (selected), "id"); n = g_object_get_data (G_OBJECT (selected), "name"); if (!t || !i || !n) return FALSE; *type = g_strdup (t); *id = g_strdup (i); *name = g_strdup (n); return TRUE; }