/* * 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 . * * Written by: * Jasper St. Pierre * Matthias Clasen */ #include "config.h" #include "cc-input-chooser.h" #include #include #include #include #define GNOME_DESKTOP_USE_UNSTABLE_API #include #include #ifdef HAVE_IBUS #include #include "cc-ibus-utils.h" #endif #include "cc-common-language.h" #include #define INPUT_SOURCE_TYPE_XKB "xkb" #define INPUT_SOURCE_TYPE_IBUS "ibus" #define MIN_ROWS 5 struct _CcInputChooserPrivate { GtkWidget *filter_entry; GtkWidget *input_list; GHashTable *inputs; GtkWidget *no_results; GtkWidget *more_item; gboolean showing_extra; gchar *locale; gchar *id; gchar *type; GnomeXkbInfo *xkb_info; #ifdef HAVE_IBUS IBusBus *ibus; GHashTable *ibus_engines; GCancellable *ibus_cancellable; #endif }; typedef struct _CcInputChooserPrivate CcInputChooserPrivate; G_DEFINE_TYPE_WITH_PRIVATE (CcInputChooser, cc_input_chooser, GTK_TYPE_BOX); enum { PROP_0, PROP_SHOWING_EXTRA, PROP_LAST }; static GParamSpec *obj_props[PROP_LAST]; enum { CHANGED, CONFIRM, LAST_SIGNAL }; static guint signals[LAST_SIGNAL] = { 0 }; typedef struct { GtkWidget *box; GtkWidget *label; GtkWidget *checkmark; gchar *id; gchar *type; gchar *name; gboolean is_extra; } InputWidget; static InputWidget * get_input_widget (GtkWidget *widget) { return g_object_get_data (G_OBJECT (widget), "input-widget"); } static GtkWidget * padded_label_new (char *text) { GtkWidget *widget; widget = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10); gtk_widget_set_halign (widget, GTK_ALIGN_CENTER); gtk_widget_set_margin_top (widget, 10); gtk_widget_set_margin_bottom (widget, 10); gtk_box_append (GTK_BOX (widget), gtk_label_new (text)); return widget; } static void input_widget_free (gpointer data) { InputWidget *widget = data; g_free (widget->id); g_free (widget->type); g_free (widget->name); g_free (widget); } static gboolean get_layout (CcInputChooser *chooser, const gchar *type, const gchar *id, const gchar **layout, const gchar **variant) { CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser); if (g_strcmp0 (type, INPUT_SOURCE_TYPE_XKB) == 0) { gnome_xkb_info_get_layout_info (priv->xkb_info, id, NULL, NULL, layout, variant); return TRUE; } #ifdef HAVE_IBUS if (g_strcmp0 (type, INPUT_SOURCE_TYPE_IBUS) == 0) { IBusEngineDesc *engine_desc = NULL; if (priv->ibus_engines) engine_desc = g_hash_table_lookup (priv->ibus_engines, id); if (!engine_desc) return FALSE; *layout = ibus_engine_desc_get_layout (engine_desc); *variant = ""; return TRUE; } #endif return FALSE; } static gboolean preview_cb (GtkLabel *label, const gchar *uri, CcInputChooser *chooser) { GtkWidget *row; InputWidget *widget; const gchar *layout; const gchar *variant; gchar *commandline; row = gtk_widget_get_parent (GTK_WIDGET (label)); widget = get_input_widget (row); if (!get_layout (chooser, widget->type, widget->id, &layout, &variant)) return TRUE; if (variant[0]) commandline = g_strdup_printf ("gkbd-keyboard-display -l \"%s\t%s\"", layout, variant); else commandline = g_strdup_printf ("gkbd-keyboard-display -l %s", layout); g_spawn_command_line_async (commandline, NULL); g_free (commandline); return TRUE; } static GtkWidget * input_widget_new (CcInputChooser *chooser, const char *type, const char *id, gboolean is_extra) { CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser); GtkWidget *label; InputWidget *widget = g_new0 (InputWidget, 1); const gchar *name; gchar *text; if (g_str_equal (type, INPUT_SOURCE_TYPE_XKB)) { gnome_xkb_info_get_layout_info (priv->xkb_info, id, &name, NULL, NULL, NULL); } #ifdef HAVE_IBUS else if (g_str_equal (type, INPUT_SOURCE_TYPE_IBUS)) { if (priv->ibus_engines) name = engine_get_display_name (g_hash_table_lookup (priv->ibus_engines, id)); else name = id; } #endif else { name = "ERROR"; } widget->id = g_strdup (id); widget->type = g_strdup (type); widget->name = g_strdup (name); widget->is_extra = is_extra; widget->box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10); gtk_widget_set_halign (widget->box, GTK_ALIGN_FILL); gtk_widget_set_margin_top (widget->box, 10); gtk_widget_set_margin_bottom (widget->box, 10); gtk_widget_set_margin_start (widget->box, 10); gtk_widget_set_margin_end (widget->box, 10); widget->label = gtk_label_new (name); gtk_label_set_xalign (GTK_LABEL (widget->label), 0); gtk_label_set_yalign (GTK_LABEL (widget->label), 0.5); gtk_label_set_ellipsize (GTK_LABEL (widget->label), PANGO_ELLIPSIZE_END); gtk_label_set_max_width_chars (GTK_LABEL (widget->label), 40); gtk_label_set_width_chars (GTK_LABEL (widget->label), 40); gtk_box_append (GTK_BOX (widget->box), widget->label); widget->checkmark = gtk_image_new_from_icon_name ("object-select-symbolic"); gtk_box_append (GTK_BOX (widget->box), widget->checkmark); gtk_widget_set_margin_start (widget->checkmark, 10); gtk_widget_set_margin_end (widget->checkmark, 10); gtk_widget_set_halign (widget->box, GTK_ALIGN_START); text = g_strdup_printf ("%s", _("Preview")); label = gtk_label_new (""); gtk_label_set_markup (GTK_LABEL (label), text); g_free (text); g_signal_connect (label, "activate-link", G_CALLBACK (preview_cb), chooser); gtk_box_append (GTK_BOX (widget->box), label); g_object_set_data_full (G_OBJECT (widget->box), "input-widget", widget, input_widget_free); return widget->box; } static void sync_all_checkmarks (CcInputChooser *chooser) { CcInputChooserPrivate *priv; GtkWidget *row; priv = cc_input_chooser_get_instance_private (chooser); row = gtk_widget_get_first_child (priv->input_list); while (row) { InputWidget *widget; GtkWidget *child; gboolean should_be_visible; child = gtk_list_box_row_get_child (GTK_LIST_BOX_ROW (row)); widget = get_input_widget (child); if (widget == NULL) return; if (priv->id == NULL || priv->type == NULL) should_be_visible = FALSE; else should_be_visible = g_strcmp0 (widget->id, priv->id) == 0 && g_strcmp0 (widget->type, priv->type) == 0; gtk_widget_set_opacity (widget->checkmark, should_be_visible ? 1.0 : 0.0); if (widget->is_extra && should_be_visible) widget->is_extra = FALSE; row = gtk_widget_get_next_sibling (row); } gtk_list_box_invalidate_filter (GTK_LIST_BOX (priv->input_list)); } static GtkWidget * more_widget_new (void) { GtkWidget *widget; GtkWidget *arrow; widget = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10); gtk_widget_set_tooltip_text (widget, _("Moreā€¦")); arrow = gtk_image_new_from_icon_name ("view-more-symbolic"); gtk_style_context_add_class (gtk_widget_get_style_context (arrow), "dim-label"); gtk_widget_set_margin_top (widget, 12); gtk_widget_set_margin_bottom (widget, 12); gtk_widget_set_hexpand (arrow, TRUE); gtk_widget_set_halign (arrow, GTK_ALIGN_CENTER); gtk_widget_set_valign (arrow, GTK_ALIGN_CENTER); gtk_box_append (GTK_BOX (widget), arrow); return widget; } static GtkWidget * no_results_widget_new (void) { GtkWidget *widget; /* Translators: a search for input methods or keyboard layouts * did not yield any results */ widget = padded_label_new (_("No inputs found")); gtk_widget_set_sensitive (widget, FALSE); return widget; } static void choose_non_extras (CcInputChooser *chooser) { CcInputChooserPrivate *priv; GtkWidget *row; guint count = 0; priv = cc_input_chooser_get_instance_private (chooser); row = gtk_widget_get_first_child (priv->input_list); while (row) { InputWidget *widget; GtkWidget *child; if (++count > MIN_ROWS) break; child = gtk_list_box_row_get_child (GTK_LIST_BOX_ROW (row)); widget = get_input_widget (child); if (widget == NULL) break; widget->is_extra = FALSE; row = gtk_widget_get_next_sibling (row); } } static void add_rows_to_list (CcInputChooser *chooser, GList *list, const gchar *type, const gchar *default_id) { CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser); const gchar *id; GtkWidget *widget; gchar *key; for (; list; list = list->next) { id = (const gchar *) list->data; if (g_strcmp0 (id, default_id) == 0) continue; key = g_strdup_printf ("%s::%s", type, id); if (g_hash_table_contains (priv->inputs, key)) { g_free (key); continue; } g_hash_table_add (priv->inputs, key); widget = input_widget_new (chooser, type, id, TRUE); gtk_list_box_append (GTK_LIST_BOX (priv->input_list), widget); } } static void add_row_to_list (CcInputChooser *chooser, const gchar *type, const gchar *id) { GList tmp = { 0 }; tmp.data = (gpointer)id; add_rows_to_list (chooser, &tmp, type, NULL); } static void get_locale_infos (CcInputChooser *chooser) { CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser); const gchar *type = NULL; const gchar *id = NULL; gchar *lang, *country; GList *list; if (gnome_get_input_source_from_locale (priv->locale, &type, &id)) { add_row_to_list (chooser, type, id); if (!priv->id) { priv->id = g_strdup (id); priv->type = g_strdup (type); } } if (!gnome_parse_locale (priv->locale, &lang, &country, NULL, NULL)) goto out; list = gnome_xkb_info_get_layouts_for_language (priv->xkb_info, lang); add_rows_to_list (chooser, list, INPUT_SOURCE_TYPE_XKB, id); g_list_free (list); if (country != NULL) { list = gnome_xkb_info_get_layouts_for_country (priv->xkb_info, country); add_rows_to_list (chooser, list, INPUT_SOURCE_TYPE_XKB, id); g_list_free (list); } choose_non_extras (chooser); list = gnome_xkb_info_get_all_layouts (priv->xkb_info); add_rows_to_list (chooser, list, INPUT_SOURCE_TYPE_XKB, id); g_list_free (list); out: g_free (lang); g_free (country); } static gboolean input_visible (GtkListBoxRow *row, gpointer user_data) { CcInputChooser *chooser = user_data; CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser); InputWidget *widget; gboolean visible; GtkWidget *child; const char *search_term; child = gtk_list_box_row_get_child (row); if (child == priv->more_item) return !priv->showing_extra && g_hash_table_size (priv->inputs) > MIN_ROWS; widget = get_input_widget (child); if (!priv->showing_extra && widget->is_extra) return FALSE; search_term = gtk_editable_get_text (GTK_EDITABLE (priv->filter_entry)); if (!search_term || !*search_term) return TRUE; visible = g_str_match_string (search_term, widget->name, TRUE); return visible; } static gint sort_inputs (GtkListBoxRow *a, GtkListBoxRow *b, gpointer data) { InputWidget *la, *lb; la = get_input_widget (gtk_list_box_row_get_child (a)); lb = get_input_widget (gtk_list_box_row_get_child (b)); if (la == NULL) return 1; if (lb == NULL) return -1; if (la->is_extra && !lb->is_extra) return 1; if (!la->is_extra && lb->is_extra) return -1; return strcmp (la->name, lb->name); } static void filter_changed (GtkEntry *entry, CcInputChooser *chooser) { CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser); gtk_list_box_invalidate_filter (GTK_LIST_BOX (priv->input_list)); } static void show_more (CcInputChooser *chooser) { CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser); if (g_hash_table_size (priv->inputs) <= MIN_ROWS) return; gtk_widget_grab_focus (priv->filter_entry); gtk_widget_set_valign (GTK_WIDGET (chooser), GTK_ALIGN_FILL); priv->showing_extra = TRUE; gtk_list_box_invalidate_filter (GTK_LIST_BOX (priv->input_list)); g_object_notify_by_pspec (G_OBJECT (chooser), obj_props[PROP_SHOWING_EXTRA]); } static void set_input (CcInputChooser *chooser, const gchar *id, const gchar *type) { CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser); if (g_strcmp0 (priv->id, id) == 0 && g_strcmp0 (priv->type, type) == 0) return; g_free (priv->id); g_free (priv->type); priv->id = g_strdup (id); priv->type = g_strdup (type); sync_all_checkmarks (chooser); g_signal_emit (chooser, signals[CHANGED], 0); } static gboolean confirm_choice (gpointer data) { GtkWidget *widget = data; g_signal_emit (widget, signals[CONFIRM], 0); return G_SOURCE_REMOVE; } static void row_activated (GtkListBox *box, GtkListBoxRow *row, CcInputChooser *chooser) { CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser); GtkWidget *child; InputWidget *widget; if (row == NULL) return; child = gtk_list_box_row_get_child (row); if (child == priv->more_item) { show_more (chooser); } else { widget = get_input_widget (child); if (widget == NULL) return; if (g_strcmp0 (priv->id, widget->id) == 0 && g_strcmp0 (priv->type, widget->type) == 0) confirm_choice (chooser); else set_input (chooser, widget->id, widget->type); } } #ifdef HAVE_IBUS static void update_ibus_active_sources (CcInputChooser *chooser) { CcInputChooserPrivate *priv; IBusEngineDesc *engine_desc; GtkWidget *child; const gchar *type; const gchar *id; gchar *name; priv = cc_input_chooser_get_instance_private (chooser); child = gtk_widget_get_first_child (priv->input_list); while (child) { InputWidget *row; row = get_input_widget (child); child = gtk_widget_get_next_sibling (child); if (row == NULL) continue; type = row->type; id = row->id; if (g_strcmp0 (type, INPUT_SOURCE_TYPE_IBUS) != 0) continue; engine_desc = g_hash_table_lookup (priv->ibus_engines, id); if (engine_desc) { name = engine_get_display_name (engine_desc); gtk_label_set_text (GTK_LABEL (row->label), name); g_free (name); } } } static void get_ibus_locale_infos (CcInputChooser *chooser) { CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser); GHashTableIter iter; const gchar *engine_id; IBusEngineDesc *engine; if (!priv->ibus_engines) return; g_hash_table_iter_init (&iter, priv->ibus_engines); while (g_hash_table_iter_next (&iter, (gpointer *) &engine_id, (gpointer *) &engine)) add_row_to_list (chooser, INPUT_SOURCE_TYPE_IBUS, engine_id); } static void fetch_ibus_engines_result (GObject *object, GAsyncResult *result, CcInputChooser *chooser) { CcInputChooserPrivate *priv; GList *list, *l; GError *error; error = NULL; list = ibus_bus_list_engines_async_finish (IBUS_BUS (object), result, &error); if (!list && error) { if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) g_warning ("Couldn't finish IBus request: %s", error->message); g_error_free (error); return; } priv = cc_input_chooser_get_instance_private (chooser); g_clear_object (&priv->ibus_cancellable); /* Maps engine ids to engine description objects */ priv->ibus_engines = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, g_object_unref); for (l = list; l; l = l->next) { IBusEngineDesc *engine = l->data; const gchar *engine_id; engine_id = ibus_engine_desc_get_name (engine); if (g_str_has_prefix (engine_id, "xkb:")) g_object_unref (engine); else g_hash_table_replace (priv->ibus_engines, (gpointer)engine_id, engine); } g_list_free (list); update_ibus_active_sources (chooser); get_ibus_locale_infos (chooser); sync_all_checkmarks (chooser); } static void fetch_ibus_engines (CcInputChooser *chooser) { CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser); priv->ibus_cancellable = g_cancellable_new (); ibus_bus_list_engines_async (priv->ibus, -1, priv->ibus_cancellable, (GAsyncReadyCallback)fetch_ibus_engines_result, chooser); /* We've got everything we needed, don't want to be called again. */ g_signal_handlers_disconnect_by_func (priv->ibus, fetch_ibus_engines, chooser); } static void maybe_start_ibus (void) { /* IBus doesn't export API in the session bus. The only thing * we have there is a well known name which we can use as a * sure-fire way to activate it. */ g_bus_unwatch_name (g_bus_watch_name (G_BUS_TYPE_SESSION, IBUS_SERVICE_IBUS, G_BUS_NAME_WATCHER_FLAGS_AUTO_START, NULL, NULL, NULL, NULL)); } #endif static void cc_input_chooser_constructed (GObject *object) { CcInputChooser *chooser = CC_INPUT_CHOOSER (object); CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser); G_OBJECT_CLASS (cc_input_chooser_parent_class)->constructed (object); priv->xkb_info = gnome_xkb_info_new (); #ifdef HAVE_IBUS ibus_init (); if (!priv->ibus) { priv->ibus = ibus_bus_new_async (); if (ibus_bus_is_connected (priv->ibus)) fetch_ibus_engines (chooser); else g_signal_connect_swapped (priv->ibus, "connected", G_CALLBACK (fetch_ibus_engines), chooser); } maybe_start_ibus (); #endif priv->inputs = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); priv->more_item = more_widget_new (); priv->no_results = no_results_widget_new (); gtk_list_box_set_sort_func (GTK_LIST_BOX (priv->input_list), sort_inputs, chooser, NULL); gtk_list_box_set_filter_func (GTK_LIST_BOX (priv->input_list), input_visible, chooser, NULL); gtk_list_box_set_selection_mode (GTK_LIST_BOX (priv->input_list), GTK_SELECTION_NONE); if (priv->locale == NULL) { priv->locale = cc_common_language_get_current_language (); } get_locale_infos (chooser); #ifdef HAVE_IBUS get_ibus_locale_infos (chooser); #endif gtk_list_box_append (GTK_LIST_BOX (priv->input_list), priv->more_item); gtk_list_box_set_placeholder (GTK_LIST_BOX (priv->input_list), priv->no_results); g_signal_connect (priv->filter_entry, "changed", G_CALLBACK (filter_changed), chooser); g_signal_connect (priv->input_list, "row-activated", G_CALLBACK (row_activated), chooser); sync_all_checkmarks (chooser); } static void cc_input_chooser_finalize (GObject *object) { CcInputChooser *chooser = CC_INPUT_CHOOSER (object); CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser); g_clear_object (&priv->xkb_info); g_hash_table_unref (priv->inputs); #ifdef HAVE_IBUS g_clear_object (&priv->ibus); if (priv->ibus_cancellable) g_cancellable_cancel (priv->ibus_cancellable); g_clear_object (&priv->ibus_cancellable); g_clear_pointer (&priv->ibus_engines, g_hash_table_destroy); #endif G_OBJECT_CLASS (cc_input_chooser_parent_class)->finalize (object); } static void cc_input_chooser_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { CcInputChooser *chooser = CC_INPUT_CHOOSER (object); switch (prop_id) { case PROP_SHOWING_EXTRA: g_value_set_boolean (value, cc_input_chooser_get_showing_extra (chooser)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void cc_input_chooser_class_init (CcInputChooserClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (klass), "/org/gnome/initial-setup/input-chooser.ui"); gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), CcInputChooser, filter_entry); gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), CcInputChooser, input_list); object_class->finalize = cc_input_chooser_finalize; object_class->get_property = cc_input_chooser_get_property; object_class->constructed = cc_input_chooser_constructed; obj_props[PROP_SHOWING_EXTRA] = g_param_spec_string ("showing-extra", "", "", "", G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); signals[CHANGED] = g_signal_new ("changed", G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); signals[CONFIRM] = g_signal_new ("confirm", G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); g_object_class_install_properties (object_class, PROP_LAST, obj_props); } static void cc_input_chooser_init (CcInputChooser *chooser) { gtk_widget_init_template (GTK_WIDGET (chooser)); } void cc_input_chooser_clear_filter (CcInputChooser *chooser) { CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser); gtk_editable_set_text (GTK_EDITABLE (priv->filter_entry), ""); } const gchar * cc_input_chooser_get_input_id (CcInputChooser *chooser) { CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser); return priv->id; } const gchar * cc_input_chooser_get_input_type (CcInputChooser *chooser) { CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser); return priv->type; } void cc_input_chooser_get_layout (CcInputChooser *chooser, const gchar **layout, const gchar **variant) { CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser); if (!get_layout (chooser, priv->type, priv->id, layout, variant)) { if (layout != NULL) *layout = NULL; if (variant != NULL) *variant = NULL; } } void cc_input_chooser_set_input (CcInputChooser *chooser, const gchar *id, const gchar *type) { set_input (chooser, id, type); } gboolean cc_input_chooser_get_showing_extra (CcInputChooser *chooser) { CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser); return priv->showing_extra; }