GCC Code Coverage Report


Directory: ./
File: panels/keyboard/cc-input-chooser.c
Date: 2024-05-04 07:58:27
Exec Total Coverage
Lines: 0 531 0.0%
Functions: 0 47 0.0%
Branches: 0 301 0.0%

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