1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
2 *
3 * Copyright (C) 2013 Red Hat
4 *
5 * This program is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU General Public License as
7 * published by the Free Software Foundation; either version 2 of the
8 * License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful, but
11 * WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, see <http://www.gnu.org/licenses/>.
17 *
18 * Written by:
19 * Jasper St. Pierre <jstpierre@mecheye.net>
20 * Matthias Clasen <mclasen@redhat.com>
21 */
22
23 #include "config.h"
24 #include "cc-language-chooser.h"
25
26 #include <locale.h>
27 #include <glib/gi18n.h>
28 #include <gio/gio.h>
29
30 #include <gtk/gtk.h>
31
32 #define GNOME_DESKTOP_USE_UNSTABLE_API
33 #include <libgnome-desktop/gnome-languages.h>
34
35 #include "cc-common-language.h"
36 #include "cc-util.h"
37
38 #include <glib-object.h>
39
40 struct _CcLanguageChooserPrivate
41 {
42 GtkWidget *filter_entry;
43 GtkWidget *language_list;
44
45 GtkWidget *scrolled_window;
46 GtkWidget *no_results;
47 GtkWidget *more_item;
48
49 gboolean showing_extra;
50 gchar *language;
51 };
52 typedef struct _CcLanguageChooserPrivate CcLanguageChooserPrivate;
53 G_DEFINE_TYPE_WITH_PRIVATE (CcLanguageChooser, cc_language_chooser, GTK_TYPE_BOX);
54
55 enum {
56 PROP_0,
57 PROP_LANGUAGE,
58 PROP_SHOWING_EXTRA,
59 PROP_LAST,
60 };
61
62 static GParamSpec *obj_props[PROP_LAST];
63
64 enum {
65 CONFIRM,
66 LAST_SIGNAL
67 };
68
69 static guint signals[LAST_SIGNAL] = { 0 };
70
71 typedef struct {
72 GtkWidget *box;
73 GtkWidget *checkmark;
74
75 gchar *locale_id;
76 gchar *locale_name;
77 gchar *locale_current_name;
78 gchar *locale_untranslated_name;
79 gchar *sort_key;
80 gboolean is_extra;
81 } LanguageWidget;
82
83 static LanguageWidget *
get_language_widget(GtkWidget * widget)84 get_language_widget (GtkWidget *widget)
85 {
86 return g_object_get_data (G_OBJECT (widget), "language-widget");
87 }
88
89 static GtkWidget *
padded_label_new(char * text)90 padded_label_new (char *text)
91 {
92 GtkWidget *widget;
93 widget = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10);
94 gtk_widget_set_halign (widget, GTK_ALIGN_CENTER);
95 gtk_widget_set_margin_top (widget, 10);
96 gtk_widget_set_margin_bottom (widget, 10);
97 gtk_box_pack_start (GTK_BOX (widget), gtk_label_new (text), FALSE, FALSE, 0);
98 return widget;
99 }
100
101 static void
language_widget_free(gpointer data)102 language_widget_free (gpointer data)
103 {
104 LanguageWidget *widget = data;
105
106 /* This is called when the box is destroyed,
107 * so don't bother destroying the widget and
108 * children again. */
109 g_free (widget->locale_id);
110 g_free (widget->locale_name);
111 g_free (widget->locale_current_name);
112 g_free (widget->locale_untranslated_name);
113 g_free (widget->sort_key);
114 g_free (widget);
115 }
116
117 static GtkWidget *
language_widget_new(const char * locale_id,gboolean is_extra)118 language_widget_new (const char *locale_id,
119 gboolean is_extra)
120 {
121 GtkWidget *label;
122 gchar *locale_name, *locale_current_name, *locale_untranslated_name;
123 gchar *language = NULL;
124 gchar *language_name;
125 gchar *country = NULL;
126 gchar *country_name = NULL;
127 LanguageWidget *widget = g_new0 (LanguageWidget, 1);
128
129 if (!gnome_parse_locale (locale_id, &language, &country, NULL, NULL))
130 return NULL;
131
132 language_name = gnome_get_language_from_code (language, locale_id);
133 if (language_name == NULL)
134 language_name = gnome_get_language_from_code (language, NULL);
135
136 if (country) {
137 country_name = gnome_get_country_from_code (country, locale_id);
138 if (country_name == NULL)
139 country_name = gnome_get_country_from_code (country, NULL);
140 }
141
142 locale_name = gnome_get_language_from_locale (locale_id, locale_id);
143 locale_current_name = gnome_get_language_from_locale (locale_id, NULL);
144 locale_untranslated_name = gnome_get_language_from_locale (locale_id, "C");
145
146 widget->box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10);
147 gtk_widget_set_margin_top (widget->box, 10);
148 gtk_widget_set_margin_bottom (widget->box, 10);
149 gtk_widget_set_margin_start (widget->box, 10);
150 gtk_widget_set_margin_end (widget->box, 10);
151 gtk_widget_set_halign (widget->box, GTK_ALIGN_FILL);
152
153 label = gtk_label_new (language_name);
154 gtk_label_set_ellipsize (GTK_LABEL (label), PANGO_ELLIPSIZE_END);
155 gtk_label_set_max_width_chars (GTK_LABEL (label), 30);
156 gtk_label_set_xalign (GTK_LABEL (label), 0);
157 gtk_box_pack_start (GTK_BOX (widget->box), label, FALSE, FALSE, 0);
158
159 widget->checkmark = gtk_image_new_from_icon_name ("object-select-symbolic", GTK_ICON_SIZE_MENU);
160 gtk_box_pack_start (GTK_BOX (widget->box), widget->checkmark, FALSE, FALSE, 0);
161 gtk_widget_show (widget->checkmark);
162
163 if (country_name) {
164 label = gtk_label_new (country_name);
165 gtk_label_set_ellipsize (GTK_LABEL (label), PANGO_ELLIPSIZE_END);
166 gtk_label_set_max_width_chars (GTK_LABEL (label), 30);
167 gtk_style_context_add_class (gtk_widget_get_style_context (label), "dim-label");
168 gtk_label_set_xalign (GTK_LABEL (label), 0);
169 gtk_widget_set_halign (label, GTK_ALIGN_END);
170 gtk_box_pack_end (GTK_BOX (widget->box), label, FALSE, FALSE, 0);
171 }
172
173 widget->locale_id = g_strdup (locale_id);
174 widget->locale_name = locale_name;
175 widget->locale_current_name = locale_current_name;
176 widget->locale_untranslated_name = locale_untranslated_name;
177 widget->is_extra = is_extra;
178 widget->sort_key = cc_util_normalize_casefold_and_unaccent (locale_name);
179
180 g_object_set_data_full (G_OBJECT (widget->box), "language-widget", widget,
181 language_widget_free);
182
183 g_free (language);
184 g_free (language_name);
185 g_free (country);
186 g_free (country_name);
187
188 return widget->box;
189 }
190
191 static void
sync_checkmark(GtkWidget * row,gpointer user_data)192 sync_checkmark (GtkWidget *row,
193 gpointer user_data)
194 {
195 GtkWidget *child;
196 LanguageWidget *widget;
197 gchar *locale_id;
198 gboolean should_be_visible;
199
200 child = gtk_bin_get_child (GTK_BIN (row));
201 widget = get_language_widget (child);
202
203 if (widget == NULL)
204 return;
205
206 locale_id = user_data;
207 should_be_visible = g_str_equal (widget->locale_id, locale_id);
208 gtk_widget_set_opacity (widget->checkmark, should_be_visible ? 1.0 : 0.0);
209 }
210
211 static void
sync_all_checkmarks(CcLanguageChooser * chooser)212 sync_all_checkmarks (CcLanguageChooser *chooser)
213 {
214 CcLanguageChooserPrivate *priv = cc_language_chooser_get_instance_private (chooser);
215
216 gtk_container_foreach (GTK_CONTAINER (priv->language_list),
217 sync_checkmark, priv->language);
218 }
219
220 static GtkWidget *
more_widget_new(void)221 more_widget_new (void)
222 {
223 GtkWidget *widget;
224 GtkWidget *arrow;
225
226 widget = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10);
227 gtk_widget_set_tooltip_text (widget, _("More…"));
228
229 arrow = gtk_image_new_from_icon_name ("view-more-symbolic", GTK_ICON_SIZE_MENU);
230 gtk_style_context_add_class (gtk_widget_get_style_context (arrow), "dim-label");
231 gtk_widget_set_margin_top (widget, 10);
232 gtk_widget_set_margin_bottom (widget, 10);
233 gtk_box_pack_start (GTK_BOX (widget), arrow, TRUE, TRUE, 0);
234
235 return widget;
236 }
237
238 static GtkWidget *
no_results_widget_new(void)239 no_results_widget_new (void)
240 {
241 GtkWidget *widget;
242
243 widget = padded_label_new (_("No languages found"));
244 gtk_widget_set_sensitive (widget, FALSE);
245 gtk_widget_show_all (widget);
246 return widget;
247 }
248
249 static void
add_one_language(CcLanguageChooser * chooser,const char * locale_id,gboolean is_initial)250 add_one_language (CcLanguageChooser *chooser,
251 const char *locale_id,
252 gboolean is_initial)
253 {
254 CcLanguageChooserPrivate *priv = cc_language_chooser_get_instance_private (chooser);
255 GtkWidget *widget;
256
257 if (!cc_common_language_has_font (locale_id)) {
258 return;
259 }
260
261 widget = language_widget_new (locale_id, !is_initial);
262 if (widget)
263 gtk_container_add (GTK_CONTAINER (priv->language_list), widget);
264 }
265
266 static void
add_languages(CcLanguageChooser * chooser,char ** locale_ids,GHashTable * initial)267 add_languages (CcLanguageChooser *chooser,
268 char **locale_ids,
269 GHashTable *initial)
270 {
271 CcLanguageChooserPrivate *priv = cc_language_chooser_get_instance_private (chooser);
272 GHashTableIter iter;
273 gchar *key;
274
275 g_hash_table_iter_init (&iter, initial);
276 while (g_hash_table_iter_next (&iter, (gpointer *)&key, NULL)) {
277 add_one_language (chooser, key, TRUE);
278 }
279
280 while (*locale_ids) {
281 const gchar *locale_id;
282
283 locale_id = *locale_ids;
284 locale_ids ++;
285
286 if (!g_hash_table_lookup (initial, locale_id))
287 add_one_language (chooser, locale_id, FALSE);
288 }
289
290 gtk_container_add (GTK_CONTAINER (priv->language_list), priv->more_item);
291 gtk_list_box_set_placeholder (GTK_LIST_BOX (priv->language_list), priv->no_results);
292
293 gtk_widget_show_all (priv->language_list);
294 }
295
296 static void
add_all_languages(CcLanguageChooser * chooser)297 add_all_languages (CcLanguageChooser *chooser)
298 {
299 g_auto(GStrv) locale_ids = NULL;
300 g_autoptr(GHashTable) initial = NULL;
301
302 locale_ids = gnome_get_all_locales ();
303 initial = cc_common_language_get_initial_languages ();
304 add_languages (chooser, locale_ids, initial);
305 }
306
307 static gboolean
language_visible(GtkListBoxRow * row,gpointer user_data)308 language_visible (GtkListBoxRow *row,
309 gpointer user_data)
310 {
311 CcLanguageChooser *chooser = user_data;
312 CcLanguageChooserPrivate *priv = cc_language_chooser_get_instance_private (chooser);
313 LanguageWidget *widget;
314 gboolean visible;
315 GtkWidget *child;
316 const char *search_term;
317
318 child = gtk_bin_get_child (GTK_BIN (row));
319 if (child == priv->more_item)
320 return !priv->showing_extra;
321
322 widget = get_language_widget (child);
323
324 if (!priv->showing_extra && widget->is_extra)
325 return FALSE;
326
327 search_term = gtk_entry_get_text (GTK_ENTRY (priv->filter_entry));
328 if (!search_term || !*search_term)
329 return TRUE;
330
331 visible = FALSE;
332
333 visible = g_str_match_string (search_term, widget->locale_name, TRUE);
334 if (visible)
335 goto out;
336
337 visible = g_str_match_string (search_term, widget->locale_current_name, TRUE);
338 if (visible)
339 goto out;
340
341 visible = g_str_match_string (search_term, widget->locale_untranslated_name, TRUE);
342 if (visible)
343 goto out;
344
345 out:
346 return visible;
347 }
348
349 static gint
sort_languages(GtkListBoxRow * a,GtkListBoxRow * b,gpointer data)350 sort_languages (GtkListBoxRow *a,
351 GtkListBoxRow *b,
352 gpointer data)
353 {
354 LanguageWidget *la, *lb;
355 int ret;
356
357 la = get_language_widget (gtk_bin_get_child (GTK_BIN (a)));
358 lb = get_language_widget (gtk_bin_get_child (GTK_BIN (b)));
359
360 if (la == NULL)
361 return 1;
362
363 if (lb == NULL)
364 return -1;
365
366 if (la->is_extra && !lb->is_extra)
367 return 1;
368
369 if (!la->is_extra && lb->is_extra)
370 return -1;
371
372 ret = g_strcmp0 (la->sort_key, lb->sort_key);
373 if (ret != 0)
374 return ret;
375
376 return g_strcmp0 (la->locale_id, lb->locale_id);
377 }
378
379 static void
filter_changed(GtkEntry * entry,CcLanguageChooser * chooser)380 filter_changed (GtkEntry *entry,
381 CcLanguageChooser *chooser)
382 {
383 CcLanguageChooserPrivate *priv = cc_language_chooser_get_instance_private (chooser);
384 gtk_list_box_invalidate_filter (GTK_LIST_BOX (priv->language_list));
385 }
386
387 static void
show_more(CcLanguageChooser * chooser)388 show_more (CcLanguageChooser *chooser)
389 {
390 CcLanguageChooserPrivate *priv = cc_language_chooser_get_instance_private (chooser);
391
392 gtk_widget_show (priv->filter_entry);
393 gtk_widget_grab_focus (priv->filter_entry);
394
395 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (priv->scrolled_window),
396 GTK_POLICY_NEVER,
397 GTK_POLICY_AUTOMATIC);
398 gtk_widget_set_valign (GTK_WIDGET (chooser), GTK_ALIGN_FILL);
399
400 priv->showing_extra = TRUE;
401 gtk_list_box_invalidate_filter (GTK_LIST_BOX (priv->language_list));
402 g_object_notify_by_pspec (G_OBJECT (chooser), obj_props[PROP_SHOWING_EXTRA]);
403 }
404
405 static void
set_locale_id(CcLanguageChooser * chooser,const gchar * new_locale_id)406 set_locale_id (CcLanguageChooser *chooser,
407 const gchar *new_locale_id)
408 {
409 CcLanguageChooserPrivate *priv = cc_language_chooser_get_instance_private (chooser);
410
411 if (g_strcmp0 (priv->language, new_locale_id) == 0)
412 return;
413
414 g_free (priv->language);
415 priv->language = g_strdup (new_locale_id);
416
417 sync_all_checkmarks (chooser);
418
419 g_object_notify_by_pspec (G_OBJECT (chooser), obj_props[PROP_LANGUAGE]);
420 }
421
422 static gboolean
confirm_choice(gpointer data)423 confirm_choice (gpointer data)
424 {
425 GtkWidget *widget = data;
426
427 g_signal_emit (widget, signals[CONFIRM], 0);
428
429 return G_SOURCE_REMOVE;
430 }
431
432 static void
row_activated(GtkListBox * box,GtkListBoxRow * row,CcLanguageChooser * chooser)433 row_activated (GtkListBox *box,
434 GtkListBoxRow *row,
435 CcLanguageChooser *chooser)
436 {
437 CcLanguageChooserPrivate *priv = cc_language_chooser_get_instance_private (chooser);
438 GtkWidget *child;
439 LanguageWidget *widget;
440
441 if (row == NULL)
442 return;
443
444 child = gtk_bin_get_child (GTK_BIN (row));
445 if (child == priv->more_item) {
446 show_more (chooser);
447 } else {
448 widget = get_language_widget (child);
449 if (widget == NULL)
450 return;
451 if (g_strcmp0 (priv->language, widget->locale_id) == 0)
452 g_idle_add (confirm_choice, chooser);
453 else
454 set_locale_id (chooser, widget->locale_id);
455 }
456 }
457
458 static void
update_header_func(GtkListBoxRow * child,GtkListBoxRow * before,gpointer user_data)459 update_header_func (GtkListBoxRow *child,
460 GtkListBoxRow *before,
461 gpointer user_data)
462 {
463 GtkWidget *header;
464
465 if (before == NULL) {
466 gtk_list_box_row_set_header (child, NULL);
467 return;
468 }
469
470 header = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
471 gtk_list_box_row_set_header (child, header);
472 gtk_widget_show (header);
473 }
474
475 static void
cc_language_chooser_constructed(GObject * object)476 cc_language_chooser_constructed (GObject *object)
477 {
478 CcLanguageChooser *chooser = CC_LANGUAGE_CHOOSER (object);
479 CcLanguageChooserPrivate *priv = cc_language_chooser_get_instance_private (chooser);
480
481 G_OBJECT_CLASS (cc_language_chooser_parent_class)->constructed (object);
482
483 priv->more_item = more_widget_new ();
484 priv->no_results = no_results_widget_new ();
485
486 gtk_list_box_set_sort_func (GTK_LIST_BOX (priv->language_list),
487 sort_languages, chooser, NULL);
488 gtk_list_box_set_filter_func (GTK_LIST_BOX (priv->language_list),
489 language_visible, chooser, NULL);
490 gtk_list_box_set_header_func (GTK_LIST_BOX (priv->language_list),
491 update_header_func, chooser, NULL);
492 gtk_list_box_set_selection_mode (GTK_LIST_BOX (priv->language_list),
493 GTK_SELECTION_NONE);
494 add_all_languages (chooser);
495
496 g_signal_connect (priv->filter_entry, "changed",
497 G_CALLBACK (filter_changed),
498 chooser);
499
500 g_signal_connect (priv->language_list, "row-activated",
501 G_CALLBACK (row_activated), chooser);
502
503 if (priv->language == NULL)
504 priv->language = cc_common_language_get_current_language ();
505
506 sync_all_checkmarks (chooser);
507 }
508
509 static void
cc_language_chooser_finalize(GObject * object)510 cc_language_chooser_finalize (GObject *object)
511 {
512 CcLanguageChooser *chooser = CC_LANGUAGE_CHOOSER (object);
513 CcLanguageChooserPrivate *priv = cc_language_chooser_get_instance_private (chooser);
514
515 g_free (priv->language);
516
517 G_OBJECT_CLASS (cc_language_chooser_parent_class)->finalize (object);
518 }
519
520 static void
cc_language_chooser_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)521 cc_language_chooser_get_property (GObject *object,
522 guint prop_id,
523 GValue *value,
524 GParamSpec *pspec)
525 {
526 CcLanguageChooser *chooser = CC_LANGUAGE_CHOOSER (object);
527 switch (prop_id) {
528 case PROP_LANGUAGE:
529 g_value_set_string (value, cc_language_chooser_get_language (chooser));
530 break;
531 case PROP_SHOWING_EXTRA:
532 g_value_set_boolean (value, cc_language_chooser_get_showing_extra (chooser));
533 break;
534 default:
535 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
536 break;
537 }
538 }
539
540 static void
cc_language_chooser_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)541 cc_language_chooser_set_property (GObject *object,
542 guint prop_id,
543 const GValue *value,
544 GParamSpec *pspec)
545 {
546 CcLanguageChooser *chooser = CC_LANGUAGE_CHOOSER (object);
547 switch (prop_id) {
548 case PROP_LANGUAGE:
549 cc_language_chooser_set_language (chooser, g_value_get_string (value));
550 break;
551 default:
552 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
553 break;
554 }
555 }
556
557 static void
cc_language_chooser_class_init(CcLanguageChooserClass * klass)558 cc_language_chooser_class_init (CcLanguageChooserClass *klass)
559 {
560 GObjectClass *object_class = G_OBJECT_CLASS (klass);
561
562 gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (klass), "/org/gnome/control-center/language-chooser.ui");
563
564 gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), CcLanguageChooser, filter_entry);
565 gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), CcLanguageChooser, language_list);
566 gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), CcLanguageChooser, scrolled_window);
567
568 object_class->finalize = cc_language_chooser_finalize;
569 object_class->get_property = cc_language_chooser_get_property;
570 object_class->set_property = cc_language_chooser_set_property;
571 object_class->constructed = cc_language_chooser_constructed;
572
573 signals[CONFIRM] = g_signal_new ("confirm",
574 G_TYPE_FROM_CLASS (object_class),
575 G_SIGNAL_RUN_FIRST,
576 G_STRUCT_OFFSET (CcLanguageChooserClass, confirm),
577 NULL, NULL,
578 g_cclosure_marshal_VOID__VOID,
579 G_TYPE_NONE, 0);
580
581 obj_props[PROP_LANGUAGE] =
582 g_param_spec_string ("language", "", "", "",
583 G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
584
585 obj_props[PROP_SHOWING_EXTRA] =
586 g_param_spec_string ("showing-extra", "", "", "",
587 G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
588
589 g_object_class_install_properties (object_class, PROP_LAST, obj_props);
590 }
591
592 static void
cc_language_chooser_init(CcLanguageChooser * chooser)593 cc_language_chooser_init (CcLanguageChooser *chooser)
594 {
595 gtk_widget_init_template (GTK_WIDGET (chooser));
596 }
597
598 void
cc_language_chooser_clear_filter(CcLanguageChooser * chooser)599 cc_language_chooser_clear_filter (CcLanguageChooser *chooser)
600 {
601 CcLanguageChooserPrivate *priv = cc_language_chooser_get_instance_private (chooser);
602 gtk_entry_set_text (GTK_ENTRY (priv->filter_entry), "");
603 }
604
605 const gchar *
cc_language_chooser_get_language(CcLanguageChooser * chooser)606 cc_language_chooser_get_language (CcLanguageChooser *chooser)
607 {
608 CcLanguageChooserPrivate *priv = cc_language_chooser_get_instance_private (chooser);
609 return priv->language;
610 }
611
612 void
cc_language_chooser_set_language(CcLanguageChooser * chooser,const gchar * language)613 cc_language_chooser_set_language (CcLanguageChooser *chooser,
614 const gchar *language)
615 {
616 set_locale_id (chooser, language);
617 }
618
619 gboolean
cc_language_chooser_get_showing_extra(CcLanguageChooser * chooser)620 cc_language_chooser_get_showing_extra (CcLanguageChooser *chooser)
621 {
622 CcLanguageChooserPrivate *priv = cc_language_chooser_get_instance_private (chooser);
623 return priv->showing_extra;
624 }
625