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