1 /*
2  * Copyright © 2013 Red Hat, Inc.
3  *
4  * This program is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU General Public License as
6  * published by the Free Software Foundation; either version 2 of the
7  * License, or (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful, but
10  * WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 #include <config.h>
19 #include <locale.h>
20 #include <glib/gi18n.h>
21 
22 #define GNOME_DESKTOP_USE_UNSTABLE_API
23 #include <libgnome-desktop/gnome-languages.h>
24 
25 #include "list-box-helper.h"
26 #include "cc-common-language.h"
27 #include "cc-util.h"
28 #include "cc-input-chooser.h"
29 #include "cc-input-source-ibus.h"
30 #include "cc-input-source-xkb.h"
31 
32 #ifdef HAVE_IBUS
33 #include <ibus.h>
34 #include "cc-ibus-utils.h"
35 #endif  /* HAVE_IBUS */
36 
37 #define INPUT_SOURCE_TYPE_XKB "xkb"
38 #define INPUT_SOURCE_TYPE_IBUS "ibus"
39 
40 #define FILTER_TIMEOUT 150 /* ms */
41 
42 typedef enum
43 {
44   ROW_TRAVEL_DIRECTION_NONE,
45   ROW_TRAVEL_DIRECTION_FORWARD,
46   ROW_TRAVEL_DIRECTION_BACKWARD
47 } RowTravelDirection;
48 
49 typedef enum
50 {
51   ROW_LABEL_POSITION_START,
52   ROW_LABEL_POSITION_CENTER,
53   ROW_LABEL_POSITION_END
54 } RowLabelPosition;
55 
56 struct _CcInputChooser
57 {
58   GtkDialog          parent_instance;
59 
60   GtkButton         *add_button;
61   GtkSearchEntry    *filter_entry;
62   GtkListBox        *input_sources_listbox;
63   GtkLabel          *login_label;
64   GtkListBoxRow     *more_row;
65   GtkWidget         *no_results;
66   GtkAdjustment     *scroll_adjustment;
67 
68   GnomeXkbInfo      *xkb_info;
69   GHashTable        *ibus_engines;
70   GHashTable        *locales;
71   GHashTable        *locales_by_language;
72   gboolean           showing_extra;
73   guint              filter_timeout_id;
74   gchar            **filter_words;
75 
76   gboolean           is_login;
77 };
78 
79 G_DEFINE_TYPE (CcInputChooser, cc_input_chooser, GTK_TYPE_DIALOG)
80 
81 typedef struct
82 {
83   gchar *id;
84   gchar *name;
85   gchar *unaccented_name;
86   gchar *untranslated_name;
87   GtkListBoxRow *default_input_source_row;
88   GtkListBoxRow *locale_row;
89   GtkListBoxRow *back_row;
90   GHashTable *layout_rows_by_id;
91   GHashTable *engine_rows_by_id;
92 } LocaleInfo;
93 
94 static void
locale_info_free(gpointer data)95 locale_info_free (gpointer data)
96 {
97   LocaleInfo *info = data;
98 
99   g_free (info->id);
100   g_free (info->name);
101   g_free (info->unaccented_name);
102   g_free (info->untranslated_name);
103   g_clear_object (&info->default_input_source_row);
104   g_clear_object (&info->locale_row);
105   g_clear_object (&info->back_row);
106   g_hash_table_destroy (info->layout_rows_by_id);
107   g_hash_table_destroy (info->engine_rows_by_id);
108   g_free (info);
109 }
110 
111 static void
set_row_widget_margins(GtkWidget * widget)112 set_row_widget_margins (GtkWidget *widget)
113 {
114   gtk_widget_set_margin_start (widget, 20);
115   gtk_widget_set_margin_end (widget, 20);
116   gtk_widget_set_margin_top (widget, 6);
117   gtk_widget_set_margin_bottom (widget, 6);
118 }
119 
120 static GtkWidget *
padded_label_new(const gchar * text,RowLabelPosition position,RowTravelDirection direction,gboolean dim_label)121 padded_label_new (const gchar        *text,
122                   RowLabelPosition    position,
123                   RowTravelDirection  direction,
124                   gboolean            dim_label)
125 {
126   GtkWidget *widget;
127   GtkWidget *label;
128   GtkWidget *arrow;
129   GtkAlign alignment;
130 
131   if (position == ROW_LABEL_POSITION_START)
132     alignment = GTK_ALIGN_START;
133   else if (position == ROW_LABEL_POSITION_CENTER)
134     alignment = GTK_ALIGN_CENTER;
135   else
136     alignment = GTK_ALIGN_END;
137 
138   widget = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
139 
140   if (direction == ROW_TRAVEL_DIRECTION_BACKWARD)
141     {
142       arrow = gtk_image_new_from_icon_name ("go-previous-symbolic", GTK_ICON_SIZE_MENU);
143       gtk_widget_show (arrow);
144       gtk_container_add (GTK_CONTAINER (widget), arrow);
145     }
146 
147   label = gtk_label_new (text);
148   gtk_widget_show (label);
149   gtk_label_set_ellipsize (GTK_LABEL (label), PANGO_ELLIPSIZE_MIDDLE);
150   gtk_widget_set_hexpand (label, TRUE);
151   gtk_widget_set_halign (label, alignment);
152   set_row_widget_margins (label);
153   gtk_container_add (GTK_CONTAINER (widget), label);
154   if (dim_label)
155     gtk_style_context_add_class (gtk_widget_get_style_context (label), "dim-label");
156 
157   if (direction == ROW_TRAVEL_DIRECTION_FORWARD)
158     {
159       arrow = gtk_image_new_from_icon_name ("go-next-symbolic", GTK_ICON_SIZE_MENU);
160       gtk_widget_show (arrow);
161       gtk_container_add (GTK_CONTAINER (widget), arrow);
162     }
163 
164   return widget;
165 }
166 
167 static GtkListBoxRow *
more_row_new(void)168 more_row_new (void)
169 {
170   GtkWidget *row;
171   GtkWidget *box;
172   GtkWidget *arrow;
173 
174   row = gtk_list_box_row_new ();
175   box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
176   gtk_widget_show (box);
177   gtk_container_add (GTK_CONTAINER (row), box);
178   gtk_widget_set_tooltip_text (row, _("More…"));
179 
180   arrow = gtk_image_new_from_icon_name ("view-more-symbolic", GTK_ICON_SIZE_MENU);
181   gtk_widget_show (arrow);
182   gtk_style_context_add_class (gtk_widget_get_style_context (arrow), "dim-label");
183   gtk_widget_set_hexpand (arrow, TRUE);
184   set_row_widget_margins (arrow);
185   gtk_container_add (GTK_CONTAINER (box), arrow);
186 
187   return GTK_LIST_BOX_ROW (row);
188 }
189 
190 static GtkWidget *
no_results_widget_new(void)191 no_results_widget_new (void)
192 {
193   return padded_label_new (_("No input sources found"), ROW_LABEL_POSITION_CENTER, ROW_TRAVEL_DIRECTION_NONE, TRUE);
194 }
195 
196 static GtkListBoxRow *
back_row_new(const gchar * text)197 back_row_new (const gchar *text)
198 {
199   GtkWidget *row;
200   GtkWidget *widget;
201 
202   row = gtk_list_box_row_new ();
203   widget = padded_label_new (text, ROW_LABEL_POSITION_CENTER, ROW_TRAVEL_DIRECTION_BACKWARD, TRUE);
204   gtk_widget_show (widget);
205   gtk_container_add (GTK_CONTAINER (row), widget);
206 
207   return GTK_LIST_BOX_ROW (row);
208 }
209 
210 static GtkListBoxRow *
locale_row_new(const gchar * text)211 locale_row_new (const gchar *text)
212 {
213   GtkWidget *row;
214   GtkWidget *widget;
215 
216   row = gtk_list_box_row_new ();
217   widget = padded_label_new (text, ROW_LABEL_POSITION_CENTER, ROW_TRAVEL_DIRECTION_NONE, FALSE);
218   gtk_widget_show (widget);
219   gtk_container_add (GTK_CONTAINER (row), widget);
220 
221   return GTK_LIST_BOX_ROW (row);
222 }
223 
224 static GtkListBoxRow *
input_source_row_new(CcInputChooser * self,const gchar * type,const gchar * id)225 input_source_row_new (CcInputChooser *self,
226                       const gchar    *type,
227                       const gchar    *id)
228 {
229   GtkWidget *row = NULL;
230   GtkWidget *widget;
231 
232   if (g_str_equal (type, INPUT_SOURCE_TYPE_XKB))
233     {
234       const gchar *display_name;
235 
236       gnome_xkb_info_get_layout_info (self->xkb_info, id, &display_name, NULL, NULL, NULL);
237 
238       row = gtk_list_box_row_new ();
239       widget = padded_label_new (display_name,
240                                  ROW_LABEL_POSITION_START,
241                                  ROW_TRAVEL_DIRECTION_NONE,
242                                  FALSE);
243       gtk_widget_show (widget);
244       gtk_container_add (GTK_CONTAINER (row), widget);
245       g_object_set_data (G_OBJECT (row), "name", (gpointer) display_name);
246       g_object_set_data_full (G_OBJECT (row), "unaccented-name",
247                               cc_util_normalize_casefold_and_unaccent (display_name), g_free);
248     }
249   else if (g_str_equal (type, INPUT_SOURCE_TYPE_IBUS))
250     {
251 #ifdef HAVE_IBUS
252       gchar *display_name;
253       GtkWidget *image;
254 
255       display_name = engine_get_display_name (g_hash_table_lookup (self->ibus_engines, id));
256 
257       row = gtk_list_box_row_new ();
258       widget = padded_label_new (display_name,
259                                  ROW_LABEL_POSITION_START,
260                                  ROW_TRAVEL_DIRECTION_NONE,
261                                  FALSE);
262       gtk_widget_show (widget);
263       gtk_container_add (GTK_CONTAINER (row), widget);
264       image = gtk_image_new_from_icon_name ("system-run-symbolic", GTK_ICON_SIZE_MENU);
265       gtk_widget_show (image);
266       set_row_widget_margins (image);
267       gtk_style_context_add_class (gtk_widget_get_style_context (image), "dim-label");
268       gtk_container_add (GTK_CONTAINER (widget), image);
269 
270       g_object_set_data_full (G_OBJECT (row), "name", display_name, g_free);
271       g_object_set_data_full (G_OBJECT (row), "unaccented-name",
272                               cc_util_normalize_casefold_and_unaccent (display_name), g_free);
273 #else
274       widget = NULL;
275 #endif  /* HAVE_IBUS */
276     }
277 
278   if (row)
279     {
280       g_object_set_data (G_OBJECT (row), "type", (gpointer) type);
281       g_object_set_data (G_OBJECT (row), "id", (gpointer) id);
282 
283       return GTK_LIST_BOX_ROW (row);
284     }
285 
286   return NULL;
287 }
288 
289 static void
remove_all_children(GtkContainer * container)290 remove_all_children (GtkContainer *container)
291 {
292   g_autoptr(GList) list = NULL;
293   GList *l;
294 
295   list = gtk_container_get_children (container);
296   for (l = list; l; l = l->next)
297     gtk_container_remove (container, (GtkWidget *) l->data);
298 }
299 
300 static void
add_input_source_rows_for_locale(CcInputChooser * self,LocaleInfo * info)301 add_input_source_rows_for_locale (CcInputChooser *self,
302                                   LocaleInfo     *info)
303 {
304   GtkWidget *row;
305   GHashTableIter iter;
306   const gchar *id;
307 
308   if (info->default_input_source_row)
309     gtk_container_add (GTK_CONTAINER (self->input_sources_listbox), GTK_WIDGET (info->default_input_source_row));
310 
311   g_hash_table_iter_init (&iter, info->layout_rows_by_id);
312   while (g_hash_table_iter_next (&iter, (gpointer *) &id, (gpointer *) &row))
313     gtk_container_add (GTK_CONTAINER (self->input_sources_listbox), row);
314 
315   g_hash_table_iter_init (&iter, info->engine_rows_by_id);
316   while (g_hash_table_iter_next (&iter, (gpointer *) &id, (gpointer *) &row))
317     gtk_container_add (GTK_CONTAINER (self->input_sources_listbox), row);
318 }
319 
320 static void
show_input_sources_for_locale(CcInputChooser * self,LocaleInfo * info)321 show_input_sources_for_locale (CcInputChooser *self,
322                                LocaleInfo     *info)
323 {
324   remove_all_children (GTK_CONTAINER (self->input_sources_listbox));
325 
326   if (!info->back_row)
327     {
328       info->back_row = g_object_ref_sink (back_row_new (info->name));
329       gtk_widget_show (GTK_WIDGET (info->back_row));
330       g_object_set_data (G_OBJECT (info->back_row), "back", GINT_TO_POINTER (TRUE));
331       g_object_set_data (G_OBJECT (info->back_row), "locale-info", info);
332     }
333   gtk_container_add (GTK_CONTAINER (self->input_sources_listbox), GTK_WIDGET (info->back_row));
334 
335   add_input_source_rows_for_locale (self, info);
336 
337   gtk_adjustment_set_value (self->scroll_adjustment,
338                             gtk_adjustment_get_lower (self->scroll_adjustment));
339   gtk_list_box_set_header_func (self->input_sources_listbox, cc_list_box_update_header_func, NULL, NULL);
340   gtk_list_box_invalidate_filter (self->input_sources_listbox);
341   gtk_list_box_set_selection_mode (self->input_sources_listbox, GTK_SELECTION_SINGLE);
342   gtk_list_box_set_activate_on_single_click (self->input_sources_listbox, FALSE);
343   gtk_list_box_unselect_all (self->input_sources_listbox);
344 
345   if (gtk_widget_is_visible (GTK_WIDGET (self->filter_entry)) &&
346       !gtk_widget_is_focus (GTK_WIDGET (self->filter_entry)))
347     gtk_widget_grab_focus (GTK_WIDGET (self->filter_entry));
348 }
349 
350 static gboolean
is_current_locale(const gchar * locale)351 is_current_locale (const gchar *locale)
352 {
353   return g_strcmp0 (setlocale (LC_CTYPE, NULL), locale) == 0;
354 }
355 
356 static void
show_locale_rows(CcInputChooser * self)357 show_locale_rows (CcInputChooser *self)
358 {
359   g_autoptr(GHashTable) initial = NULL;
360   LocaleInfo *info;
361   GHashTableIter iter;
362 
363   remove_all_children (GTK_CONTAINER (self->input_sources_listbox));
364 
365   if (!self->showing_extra)
366     initial = cc_common_language_get_initial_languages ();
367 
368   g_hash_table_iter_init (&iter, self->locales);
369   while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &info))
370     {
371       if (!info->default_input_source_row &&
372           !g_hash_table_size (info->layout_rows_by_id) &&
373           !g_hash_table_size (info->engine_rows_by_id))
374         continue;
375 
376       if (!info->locale_row)
377         {
378           info->locale_row = g_object_ref_sink (locale_row_new (info->name));
379           gtk_widget_show (GTK_WIDGET (info->locale_row));
380           g_object_set_data (G_OBJECT (info->locale_row), "locale-info", info);
381 
382           if (!self->showing_extra &&
383               !g_hash_table_contains (initial, info->id) &&
384               !is_current_locale (info->id))
385             g_object_set_data (G_OBJECT (info->locale_row), "is-extra", GINT_TO_POINTER (TRUE));
386         }
387       gtk_container_add (GTK_CONTAINER (self->input_sources_listbox), GTK_WIDGET (info->locale_row));
388     }
389 
390   gtk_container_add (GTK_CONTAINER (self->input_sources_listbox), GTK_WIDGET (self->more_row));
391 
392   gtk_adjustment_set_value (self->scroll_adjustment,
393                             gtk_adjustment_get_lower (self->scroll_adjustment));
394   gtk_list_box_set_header_func (self->input_sources_listbox, cc_list_box_update_header_func, NULL, NULL);
395   gtk_list_box_invalidate_filter (self->input_sources_listbox);
396   gtk_list_box_set_selection_mode (self->input_sources_listbox, GTK_SELECTION_NONE);
397   gtk_list_box_set_activate_on_single_click (self->input_sources_listbox, TRUE);
398 
399   if (gtk_widget_is_visible (GTK_WIDGET (self->filter_entry)) &&
400       !gtk_widget_is_focus (GTK_WIDGET (self->filter_entry)))
401     gtk_widget_grab_focus (GTK_WIDGET (self->filter_entry));
402 }
403 
404 static gint
list_sort(GtkListBoxRow * a,GtkListBoxRow * b,gpointer data)405 list_sort (GtkListBoxRow *a,
406            GtkListBoxRow *b,
407            gpointer       data)
408 {
409   CcInputChooser *self = data;
410   LocaleInfo *ia;
411   LocaleInfo *ib;
412   const gchar *la;
413   const gchar *lb;
414   gint retval;
415 
416   /* Always goes at the end */
417   if (a == self->more_row)
418     return 1;
419   if (b == self->more_row)
420     return -1;
421 
422   ia = g_object_get_data (G_OBJECT (a), "locale-info");
423   ib = g_object_get_data (G_OBJECT (b), "locale-info");
424 
425   /* The "Other" locale always goes at the end */
426   if (!ia->id[0] && ib->id[0])
427     return 1;
428   else if (ia->id[0] && !ib->id[0])
429     return -1;
430 
431   retval = g_strcmp0 (ia->name, ib->name);
432   if (retval)
433     return retval;
434 
435   la = g_object_get_data (G_OBJECT (a), "name");
436   lb = g_object_get_data (G_OBJECT (b), "name");
437 
438   /* Only input sources have a "name" property and they should always
439      go after their respective heading */
440   if (la && !lb)
441     return 1;
442   else if (!la && lb)
443     return -1;
444   else if (!la && !lb)
445     return 0; /* Shouldn't happen */
446 
447   /* The default input source always goes first in its group */
448   if (g_object_get_data (G_OBJECT (a), "default"))
449     return -1;
450   if (g_object_get_data (G_OBJECT (b), "default"))
451     return 1;
452 
453   return g_strcmp0 (la, lb);
454 }
455 
456 static gboolean
match_all(gchar ** words,const gchar * str)457 match_all (gchar       **words,
458            const gchar  *str)
459 {
460   gchar **w;
461 
462   for (w = words; *w; ++w)
463     if (!strstr (str, *w))
464       return FALSE;
465 
466   return TRUE;
467 }
468 
469 static gboolean
match_default_source_in_table(gchar ** words,GtkListBoxRow * row)470 match_default_source_in_table (gchar         **words,
471                                GtkListBoxRow  *row)
472 {
473   const gchar *source_name;
474   source_name = g_object_get_data (G_OBJECT (row), "unaccented-name");
475   if (source_name && match_all (words, source_name))
476     return TRUE;
477   return FALSE;
478 }
479 
480 static gboolean
match_source_in_table(gchar ** words,GHashTable * table)481 match_source_in_table (gchar      **words,
482                        GHashTable  *table)
483 {
484   GHashTableIter iter;
485   gpointer row;
486   const gchar *source_name;
487 
488   g_hash_table_iter_init (&iter, table);
489   while (g_hash_table_iter_next (&iter, NULL, &row))
490     {
491       source_name = g_object_get_data (G_OBJECT (row), "unaccented-name");
492       if (source_name && match_all (words, source_name))
493         return TRUE;
494     }
495   return FALSE;
496 }
497 
498 static gboolean
list_filter(GtkListBoxRow * row,gpointer user_data)499 list_filter (GtkListBoxRow *row,
500              gpointer       user_data)
501 {
502   CcInputChooser *self = user_data;
503   LocaleInfo *info;
504   gboolean is_extra;
505   const gchar *source_name;
506 
507   if (row == self->more_row)
508     return !self->showing_extra;
509 
510   is_extra = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (row), "is-extra"));
511 
512   if (!self->showing_extra && is_extra)
513     return FALSE;
514 
515   if (!self->filter_words)
516     return TRUE;
517 
518   info = g_object_get_data (G_OBJECT (row), "locale-info");
519 
520   if (row == info->back_row)
521     return TRUE;
522 
523   if (match_all (self->filter_words, info->unaccented_name))
524     return TRUE;
525 
526   if (match_all (self->filter_words, info->untranslated_name))
527     return TRUE;
528 
529   source_name = g_object_get_data (G_OBJECT (row), "unaccented-name");
530   if (source_name)
531     {
532       if (match_all (self->filter_words, source_name))
533         return TRUE;
534     }
535   else
536     {
537       if (info->default_input_source_row &&
538           match_default_source_in_table (self->filter_words, info->default_input_source_row))
539         {
540           return TRUE;
541         }
542       if (match_source_in_table (self->filter_words, info->layout_rows_by_id))
543         return TRUE;
544       if (match_source_in_table (self->filter_words, info->engine_rows_by_id))
545         return TRUE;
546     }
547 
548   return FALSE;
549 }
550 
551 static gboolean
strvs_differ(gchar ** av,gchar ** bv)552 strvs_differ (gchar **av,
553               gchar **bv)
554 {
555   gchar **a, **b;
556 
557   for (a = av, b = bv; *a && *b; ++a, ++b)
558     if (!g_str_equal (*a, *b))
559       return TRUE;
560 
561   if (*a == NULL && *b == NULL)
562     return FALSE;
563 
564   return TRUE;
565 }
566 
567 static gboolean
do_filter(CcInputChooser * self)568 do_filter (CcInputChooser *self)
569 {
570   g_auto(GStrv) previous_words = NULL;
571   g_autofree gchar *filter_contents = NULL;
572 
573   self->filter_timeout_id = 0;
574 
575   filter_contents =
576     cc_util_normalize_casefold_and_unaccent (gtk_entry_get_text (GTK_ENTRY (self->filter_entry)));
577 
578   previous_words = self->filter_words;
579   self->filter_words = g_strsplit_set (g_strstrip (filter_contents), " ", 0);
580 
581   if (!self->filter_words[0])
582     {
583       gtk_list_box_invalidate_filter (self->input_sources_listbox);
584       gtk_list_box_set_placeholder (self->input_sources_listbox, NULL);
585     }
586   else if (previous_words == NULL || strvs_differ (self->filter_words, previous_words))
587     {
588       gtk_list_box_invalidate_filter (self->input_sources_listbox);
589       gtk_list_box_set_placeholder (self->input_sources_listbox, self->no_results);
590     }
591 
592   return G_SOURCE_REMOVE;
593 }
594 
595 static void
on_filter_entry_search_changed_cb(CcInputChooser * self)596 on_filter_entry_search_changed_cb (CcInputChooser *self)
597 {
598   if (self->filter_timeout_id == 0)
599     self->filter_timeout_id = g_timeout_add (FILTER_TIMEOUT, (GSourceFunc) do_filter, self);
600 }
601 
602 static void
show_more(CcInputChooser * self)603 show_more (CcInputChooser *self)
604 {
605   gtk_widget_show (GTK_WIDGET (self->filter_entry));
606   gtk_widget_grab_focus (GTK_WIDGET (self->filter_entry));
607 
608   self->showing_extra = TRUE;
609 
610   gtk_list_box_invalidate_filter (self->input_sources_listbox);
611 }
612 
613 static void
on_input_sources_listbox_row_activated_cb(CcInputChooser * self,GtkListBoxRow * row)614 on_input_sources_listbox_row_activated_cb (CcInputChooser *self, GtkListBoxRow  *row)
615 {
616   gpointer data;
617 
618   if (!row)
619     return;
620 
621   if (row == self->more_row)
622     {
623       show_more (self);
624       return;
625     }
626 
627   data = g_object_get_data (G_OBJECT (row), "back");
628   if (data)
629     {
630       show_locale_rows (self);
631       return;
632     }
633 
634   data = g_object_get_data (G_OBJECT (row), "name");
635   if (data)
636     {
637       if (gtk_widget_is_sensitive (GTK_WIDGET (self->add_button)))
638         gtk_dialog_response (GTK_DIALOG (self),
639                              gtk_dialog_get_response_for_widget (GTK_DIALOG (self),
640                                                                  GTK_WIDGET (self->add_button)));
641       return;
642     }
643 
644   data = g_object_get_data (G_OBJECT (row), "locale-info");
645   if (data)
646     {
647       show_input_sources_for_locale (self, (LocaleInfo *) data);
648       return;
649     }
650 }
651 
652 static void
on_input_sources_listbox_selected_rows_changed_cb(CcInputChooser * self)653 on_input_sources_listbox_selected_rows_changed_cb (CcInputChooser *self)
654 {
655   gboolean sensitive = TRUE;
656   GtkListBoxRow *row;
657 
658   row = gtk_list_box_get_selected_row (self->input_sources_listbox);
659   if (!row || g_object_get_data (G_OBJECT (row), "back"))
660     sensitive = FALSE;
661 
662   gtk_widget_set_sensitive (GTK_WIDGET (self->add_button), sensitive);
663 }
664 
665 static gboolean
on_input_sources_listbox_button_release_event_cb(CcInputChooser * self,GdkEvent * event)666 on_input_sources_listbox_button_release_event_cb (CcInputChooser *self, GdkEvent *event)
667 {
668   gdouble x, y;
669   GtkListBoxRow *row;
670 
671   gdk_event_get_coords (event, &x, &y);
672   row = gtk_list_box_get_row_at_y (self->input_sources_listbox, y);
673   if (row && g_object_get_data (G_OBJECT (row), "back"))
674     {
675       g_signal_emit_by_name (row, "activate", NULL);
676       return TRUE;
677     }
678 
679   return FALSE;
680 }
681 
682 static void
add_default_row(CcInputChooser * self,LocaleInfo * info,const gchar * type,const gchar * id)683 add_default_row (CcInputChooser *self,
684                  LocaleInfo     *info,
685                  const gchar    *type,
686                  const gchar    *id)
687 {
688   info->default_input_source_row = input_source_row_new (self, type, id);
689   if (info->default_input_source_row)
690     {
691       gtk_widget_show (GTK_WIDGET (info->default_input_source_row));
692       g_object_ref_sink (GTK_WIDGET (info->default_input_source_row));
693       g_object_set_data (G_OBJECT (info->default_input_source_row), "default", GINT_TO_POINTER (TRUE));
694       g_object_set_data (G_OBJECT (info->default_input_source_row), "locale-info", info);
695     }
696 }
697 
698 static void
add_rows_to_table(CcInputChooser * self,LocaleInfo * info,GList * list,const gchar * type,const gchar * default_id)699 add_rows_to_table (CcInputChooser *self,
700                    LocaleInfo     *info,
701                    GList          *list,
702                    const gchar    *type,
703                    const gchar    *default_id)
704 {
705   GHashTable *table;
706   GtkListBoxRow *row;
707   const gchar *id;
708 
709   if (g_str_equal (type, INPUT_SOURCE_TYPE_XKB))
710     table = info->layout_rows_by_id;
711   else if (g_str_equal (type, INPUT_SOURCE_TYPE_IBUS))
712     table = info->engine_rows_by_id;
713   else
714     return;
715 
716   while (list)
717     {
718       id = (const gchar *) list->data;
719 
720       /* The widget for the default input source lives elsewhere */
721       if (g_strcmp0 (id, default_id))
722         {
723           row = input_source_row_new (self, type, id);
724           gtk_widget_show (GTK_WIDGET (row));
725           if (row)
726             {
727               g_object_set_data (G_OBJECT (row), "locale-info", info);
728               g_hash_table_replace (table, (gpointer) id, g_object_ref_sink (row));
729             }
730         }
731       list = list->next;
732     }
733 }
734 
735 static void
add_row(CcInputChooser * self,LocaleInfo * info,const gchar * type,const gchar * id)736 add_row (CcInputChooser *self,
737          LocaleInfo     *info,
738          const gchar    *type,
739          const gchar    *id)
740 {
741   GList tmp = { 0 };
742   tmp.data = (gpointer) id;
743   add_rows_to_table (self, info, &tmp, type, NULL);
744 }
745 
746 static void
add_row_other(CcInputChooser * self,const gchar * type,const gchar * id)747 add_row_other (CcInputChooser *self,
748                const gchar    *type,
749                const gchar    *id)
750 {
751   LocaleInfo *info = g_hash_table_lookup (self->locales, "");
752   add_row (self, info, type, id);
753 }
754 
755 #ifdef HAVE_IBUS
756 static gboolean
maybe_set_as_default(CcInputChooser * self,LocaleInfo * info,const gchar * engine_id)757 maybe_set_as_default (CcInputChooser *self,
758                       LocaleInfo     *info,
759                       const gchar    *engine_id)
760 {
761   const gchar *type, *id;
762 
763   if (!gnome_get_input_source_from_locale (info->id, &type, &id))
764     return FALSE;
765 
766   if (g_str_equal (type, INPUT_SOURCE_TYPE_IBUS) &&
767       g_str_equal (id, engine_id) &&
768       info->default_input_source_row == NULL)
769     {
770       add_default_row (self, info, type, id);
771       return TRUE;
772     }
773 
774   return FALSE;
775 }
776 
777 static void
get_ibus_locale_infos(CcInputChooser * self)778 get_ibus_locale_infos (CcInputChooser *self)
779 {
780   GHashTableIter iter;
781   LocaleInfo *info;
782   const gchar *engine_id;
783   IBusEngineDesc *engine;
784 
785   if (!self->ibus_engines || self->is_login)
786     return;
787 
788   g_hash_table_iter_init (&iter, self->ibus_engines);
789   while (g_hash_table_iter_next (&iter, (gpointer *) &engine_id, (gpointer *) &engine))
790     {
791       g_autofree gchar *lang_code = NULL;
792       g_autofree gchar *country_code = NULL;
793       const gchar *ibus_locale = ibus_engine_desc_get_language (engine);
794 
795       if (gnome_parse_locale (ibus_locale, &lang_code, &country_code, NULL, NULL) &&
796           lang_code != NULL &&
797           country_code != NULL)
798         {
799           g_autofree gchar *locale = g_strdup_printf ("%s_%s.UTF-8", lang_code, country_code);
800 
801           info = g_hash_table_lookup (self->locales, locale);
802           if (info)
803             {
804               const gchar *type, *id;
805 
806               if (gnome_get_input_source_from_locale (locale, &type, &id) &&
807                   g_str_equal (type, INPUT_SOURCE_TYPE_IBUS) &&
808                   g_str_equal (id, engine_id))
809                 {
810                   add_default_row (self, info, type, id);
811                 }
812               else
813                 {
814                   add_row (self, info, INPUT_SOURCE_TYPE_IBUS, engine_id);
815                 }
816             }
817           else
818             {
819               add_row_other (self, INPUT_SOURCE_TYPE_IBUS, engine_id);
820             }
821         }
822       else if (lang_code != NULL)
823         {
824           GHashTableIter iter;
825           GHashTable *locales_for_language;
826           g_autofree gchar *language = NULL;
827 
828           /* Most IBus engines only specify the language so we try to
829              add them to all locales for that language. */
830 
831           language = gnome_get_language_from_code (lang_code, NULL);
832           if (language)
833             locales_for_language = g_hash_table_lookup (self->locales_by_language, language);
834           else
835             locales_for_language = NULL;
836 
837           if (locales_for_language)
838             {
839               g_hash_table_iter_init (&iter, locales_for_language);
840               while (g_hash_table_iter_next (&iter, (gpointer *) &info, NULL))
841                 if (!maybe_set_as_default (self, info, engine_id))
842                   add_row (self, info, INPUT_SOURCE_TYPE_IBUS, engine_id);
843             }
844           else
845             {
846               add_row_other (self, INPUT_SOURCE_TYPE_IBUS, engine_id);
847             }
848         }
849       else
850         {
851           add_row_other (self, INPUT_SOURCE_TYPE_IBUS, engine_id);
852         }
853     }
854 }
855 #endif  /* HAVE_IBUS */
856 
857 static void
add_locale_to_table(GHashTable * table,const gchar * lang_code,LocaleInfo * info)858 add_locale_to_table (GHashTable  *table,
859                      const gchar *lang_code,
860                      LocaleInfo  *info)
861 {
862   GHashTable *set;
863   g_autofree gchar *language = NULL;
864 
865   language = gnome_get_language_from_code (lang_code, NULL);
866 
867   set = g_hash_table_lookup (table, language);
868   if (!set)
869     {
870       set = g_hash_table_new (NULL, NULL);
871       g_hash_table_replace (table, g_strdup (language), set);
872     }
873   g_hash_table_add (set, info);
874 }
875 
876 static void
add_ids_to_set(GHashTable * set,GList * list)877 add_ids_to_set (GHashTable *set,
878                 GList      *list)
879 {
880   while (list)
881     {
882       g_hash_table_add (set, list->data);
883       list = list->next;
884     }
885 }
886 
887 static void
get_locale_infos(CcInputChooser * self)888 get_locale_infos (CcInputChooser *self)
889 {
890   g_autoptr(GHashTable) layouts_with_locale = NULL;
891   LocaleInfo *info;
892   g_auto(GStrv) locale_ids = NULL;
893   gchar **locale;
894   g_autoptr(GList) all_layouts = NULL;
895   GList *l;
896 
897   self->locales = g_hash_table_new_full (g_str_hash, g_str_equal,
898                                          g_free, locale_info_free);
899   self->locales_by_language = g_hash_table_new_full (g_str_hash, g_str_equal,
900                                                      g_free, (GDestroyNotify) g_hash_table_unref);
901 
902   layouts_with_locale = g_hash_table_new (g_str_hash, g_str_equal);
903 
904   locale_ids = gnome_get_all_locales ();
905   for (locale = locale_ids; *locale; ++locale)
906     {
907       g_autofree gchar *lang_code = NULL;
908       g_autofree gchar *country_code = NULL;
909       g_autofree gchar *simple_locale = NULL;
910       g_autofree gchar *tmp = NULL;
911       const gchar *type = NULL;
912       const gchar *id = NULL;
913       g_autoptr(GList) language_layouts = NULL;
914 
915       if (!gnome_parse_locale (*locale, &lang_code, &country_code, NULL, NULL))
916         continue;
917 
918       if (country_code != NULL)
919 	simple_locale = g_strdup_printf ("%s_%s.UTF-8", lang_code, country_code);
920       else
921 	simple_locale = g_strdup_printf ("%s.UTF-8", lang_code);
922 
923       if (g_hash_table_contains (self->locales, simple_locale))
924           continue;
925 
926       info = g_new0 (LocaleInfo, 1);
927       info->id = g_strdup (simple_locale);
928       info->name = gnome_get_language_from_locale (simple_locale, NULL);
929       info->unaccented_name = cc_util_normalize_casefold_and_unaccent (info->name);
930       tmp = gnome_get_language_from_locale (simple_locale, "C");
931       info->untranslated_name = cc_util_normalize_casefold_and_unaccent (tmp);
932 
933       g_hash_table_replace (self->locales, g_strdup (simple_locale), info);
934       add_locale_to_table (self->locales_by_language, lang_code, info);
935 
936       if (gnome_get_input_source_from_locale (simple_locale, &type, &id) &&
937           g_str_equal (type, INPUT_SOURCE_TYPE_XKB))
938         {
939           add_default_row (self, info, type, id);
940           g_hash_table_add (layouts_with_locale, (gpointer) id);
941         }
942 
943       /* We don't own these ids */
944       info->layout_rows_by_id = g_hash_table_new_full (g_str_hash, g_str_equal,
945                                                        NULL, g_object_unref);
946       info->engine_rows_by_id = g_hash_table_new_full (g_str_hash, g_str_equal,
947                                                        NULL, g_object_unref);
948 
949       language_layouts = gnome_xkb_info_get_layouts_for_language (self->xkb_info, lang_code);
950       add_rows_to_table (self, info, language_layouts, INPUT_SOURCE_TYPE_XKB, id);
951       add_ids_to_set (layouts_with_locale, language_layouts);
952 
953       if (country_code != NULL)
954         {
955           g_autoptr(GList) country_layouts = gnome_xkb_info_get_layouts_for_country (self->xkb_info, country_code);
956           add_rows_to_table (self, info, country_layouts, INPUT_SOURCE_TYPE_XKB, id);
957           add_ids_to_set (layouts_with_locale, country_layouts);
958         }
959     }
960 
961   /* Add a "Other" locale to hold the remaining input sources */
962   info = g_new0 (LocaleInfo, 1);
963   info->id = g_strdup ("");
964   info->name = g_strdup (C_("Input Source", "Other"));
965   info->unaccented_name = g_strdup ("");
966   info->untranslated_name = g_strdup ("");
967   g_hash_table_replace (self->locales, g_strdup (info->id), info);
968 
969   info->layout_rows_by_id = g_hash_table_new_full (g_str_hash, g_str_equal,
970                                                    NULL, g_object_unref);
971   info->engine_rows_by_id = g_hash_table_new_full (g_str_hash, g_str_equal,
972                                                    NULL, g_object_unref);
973 
974   all_layouts = gnome_xkb_info_get_all_layouts (self->xkb_info);
975   for (l = all_layouts; l; l = l->next)
976     if (!g_hash_table_contains (layouts_with_locale, l->data))
977       add_row_other (self, INPUT_SOURCE_TYPE_XKB, l->data);
978 }
979 
980 static gboolean
on_filter_entry_key_release_event_cb(CcInputChooser * self,GdkEventKey * event)981 on_filter_entry_key_release_event_cb (CcInputChooser *self, GdkEventKey *event)
982 {
983   if (event->keyval == GDK_KEY_Escape) {
984     self->showing_extra = FALSE;
985     gtk_entry_set_text (GTK_ENTRY (self->filter_entry), "");
986     gtk_widget_hide (GTK_WIDGET (self->filter_entry));
987     g_clear_pointer (&self->filter_words, g_strfreev);
988     show_locale_rows (self);
989   }
990 
991   return FALSE;
992 }
993 
994 static void
cc_input_chooser_dispose(GObject * object)995 cc_input_chooser_dispose (GObject *object)
996 {
997   CcInputChooser *self = CC_INPUT_CHOOSER (object);
998 
999   g_clear_object (&self->more_row);
1000   g_clear_object (&self->no_results);
1001   g_clear_object (&self->xkb_info);
1002   g_clear_pointer (&self->ibus_engines, g_hash_table_unref);
1003   g_clear_pointer (&self->locales, g_hash_table_unref);
1004   g_clear_pointer (&self->locales_by_language, g_hash_table_unref);
1005   g_clear_pointer (&self->filter_words, g_strfreev);
1006   g_clear_handle_id (&self->filter_timeout_id, g_source_remove);
1007 
1008   G_OBJECT_CLASS (cc_input_chooser_parent_class)->dispose (object);
1009 }
1010 
1011 void
cc_input_chooser_class_init(CcInputChooserClass * klass)1012 cc_input_chooser_class_init (CcInputChooserClass *klass)
1013 {
1014   GObjectClass *object_class = G_OBJECT_CLASS (klass);
1015   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
1016 
1017   object_class->dispose = cc_input_chooser_dispose;
1018 
1019   gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/keyboard/cc-input-chooser.ui");
1020 
1021   gtk_widget_class_bind_template_child (widget_class, CcInputChooser, add_button);
1022   gtk_widget_class_bind_template_child (widget_class, CcInputChooser, filter_entry);
1023   gtk_widget_class_bind_template_child (widget_class, CcInputChooser, input_sources_listbox);
1024   gtk_widget_class_bind_template_child (widget_class, CcInputChooser, login_label);
1025   gtk_widget_class_bind_template_child (widget_class, CcInputChooser, scroll_adjustment);
1026 
1027   gtk_widget_class_bind_template_callback (widget_class, on_input_sources_listbox_row_activated_cb);
1028   gtk_widget_class_bind_template_callback (widget_class, on_input_sources_listbox_selected_rows_changed_cb);
1029   gtk_widget_class_bind_template_callback (widget_class, on_input_sources_listbox_button_release_event_cb);
1030   gtk_widget_class_bind_template_callback (widget_class, on_filter_entry_search_changed_cb);
1031   gtk_widget_class_bind_template_callback (widget_class, on_filter_entry_key_release_event_cb);
1032 }
1033 
1034 void
cc_input_chooser_init(CcInputChooser * self)1035 cc_input_chooser_init (CcInputChooser *self)
1036 {
1037   gtk_widget_init_template (GTK_WIDGET (self));
1038 }
1039 
1040 CcInputChooser *
cc_input_chooser_new(gboolean is_login,GnomeXkbInfo * xkb_info,GHashTable * ibus_engines)1041 cc_input_chooser_new (gboolean      is_login,
1042                       GnomeXkbInfo *xkb_info,
1043                       GHashTable   *ibus_engines)
1044 {
1045   CcInputChooser *self;
1046   g_autoptr(GError) error = NULL;
1047 
1048   self = g_object_new (CC_TYPE_INPUT_CHOOSER,
1049                        "use-header-bar", 1,
1050                        NULL);
1051 
1052   self->is_login = is_login;
1053   self->xkb_info = g_object_ref (xkb_info);
1054   if (ibus_engines)
1055     self->ibus_engines = g_hash_table_ref (ibus_engines);
1056 
1057   self->more_row = g_object_ref_sink (more_row_new ());
1058   gtk_widget_show (GTK_WIDGET (self->more_row));
1059   self->no_results = g_object_ref_sink (no_results_widget_new ());
1060   gtk_widget_show (self->no_results);
1061 
1062   gtk_list_box_set_filter_func (self->input_sources_listbox, list_filter, self, NULL);
1063   gtk_list_box_set_sort_func (self->input_sources_listbox, list_sort, self, NULL);
1064 
1065   if (self->is_login)
1066     gtk_widget_show (GTK_WIDGET (self->login_label));
1067 
1068   get_locale_infos (self);
1069 #ifdef HAVE_IBUS
1070   get_ibus_locale_infos (self);
1071 #endif  /* HAVE_IBUS */
1072   show_locale_rows (self);
1073 
1074   return self;
1075 }
1076 
1077 void
cc_input_chooser_set_ibus_engines(CcInputChooser * self,GHashTable * ibus_engines)1078 cc_input_chooser_set_ibus_engines (CcInputChooser *self,
1079                                    GHashTable     *ibus_engines)
1080 {
1081   g_return_if_fail (CC_IS_INPUT_CHOOSER (self));
1082 
1083 #ifdef HAVE_IBUS
1084   /* This should only be called once when IBus shows up in case it
1085      wasn't up yet when the user opened the input chooser dialog. */
1086   g_return_if_fail (self->ibus_engines == NULL);
1087 
1088   self->ibus_engines = ibus_engines;
1089   get_ibus_locale_infos (self);
1090   show_locale_rows (self);
1091 #endif  /* HAVE_IBUS */
1092 }
1093 
1094 CcInputSource *
cc_input_chooser_get_source(CcInputChooser * self)1095 cc_input_chooser_get_source (CcInputChooser *self)
1096 {
1097   GtkListBoxRow *selected;
1098   const gchar *t, *i;
1099 
1100   g_return_val_if_fail (CC_IS_INPUT_CHOOSER (self), FALSE);
1101 
1102   selected = gtk_list_box_get_selected_row (self->input_sources_listbox);
1103   if (!selected)
1104     return NULL;
1105 
1106   t = g_object_get_data (G_OBJECT (selected), "type");
1107   i = g_object_get_data (G_OBJECT (selected), "id");
1108 
1109   if (!t || !i)
1110     return FALSE;
1111 
1112   if (g_strcmp0 (t, "xkb") == 0)
1113     return CC_INPUT_SOURCE (cc_input_source_xkb_new_from_id (self->xkb_info, i));
1114   else if (g_strcmp0 (t, "ibus") == 0)
1115     return CC_INPUT_SOURCE (cc_input_source_ibus_new (i));
1116   else
1117     return NULL;
1118 }
1119