1 /*
2  * Copyright (C) 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  * Written by:
18  *     Matthias Clasen
19  */
20 
21 #define _GNU_SOURCE
22 #include <config.h>
23 #include "cc-format-chooser.h"
24 
25 #include <errno.h>
26 #include <locale.h>
27 #include <langinfo.h>
28 #include <string.h>
29 #include <glib/gi18n.h>
30 #include <handy.h>
31 
32 #include "list-box-helper.h"
33 #include "cc-common-language.h"
34 #include "cc-format-preview.h"
35 #include "cc-util.h"
36 
37 #define GNOME_DESKTOP_USE_UNSTABLE_API
38 #include <libgnome-desktop/gnome-languages.h>
39 
40 struct _CcFormatChooser {
41         GtkDialog parent_instance;
42 
43         GtkWidget *title_bar;
44         GtkWidget *title_buttons;
45         GtkWidget *cancel_button;
46         GtkWidget *back_button;
47         GtkWidget *done_button;
48         GtkWidget *empty_results_view;
49         GtkWidget *main_leaflet;
50         GtkWidget *region_filter_entry;
51         GtkWidget *region_list;
52         GtkWidget *region_list_stack;
53         GtkWidget *common_region_title;
54         GtkWidget *common_region_listbox;
55         GtkWidget *region_title;
56         GtkWidget *region_listbox;
57         CcFormatPreview *format_preview;
58         gboolean adding;
59         gboolean showing_extra;
60         gboolean no_results;
61         gchar *region;
62         gchar *preview_region;
63         gchar **filter_words;
64 };
65 
G_DEFINE_TYPE(CcFormatChooser,cc_format_chooser,GTK_TYPE_DIALOG)66 G_DEFINE_TYPE (CcFormatChooser, cc_format_chooser, GTK_TYPE_DIALOG)
67 
68 static void
69 update_check_button_for_list (GtkWidget   *list_box,
70                               const gchar *locale_id)
71 {
72   g_autoptr(GList) children = NULL;
73   GList *l;
74 
75   g_assert (GTK_IS_CONTAINER (list_box));
76 
77   children = gtk_container_get_children (GTK_CONTAINER (list_box));
78   for (l = children; l; l = l->next)
79     {
80       GtkWidget *row = l->data;
81       GtkWidget *check = g_object_get_data (G_OBJECT (row), "check");
82       const gchar *region = g_object_get_data (G_OBJECT (row), "locale-id");
83       if (check == NULL || region == NULL)
84         continue;
85 
86       if (g_strcmp0 (locale_id, region) == 0)
87         gtk_widget_set_opacity (check, 1.0);
88       else
89         gtk_widget_set_opacity (check, 0.0);
90     }
91 }
92 
93 static void
set_locale_id(CcFormatChooser * chooser,const gchar * locale_id)94 set_locale_id (CcFormatChooser *chooser,
95                const gchar     *locale_id)
96 {
97         g_free (chooser->region);
98         chooser->region = g_strdup (locale_id);
99 
100         update_check_button_for_list (chooser->region_listbox, locale_id);
101         update_check_button_for_list (chooser->common_region_listbox, locale_id);
102         cc_format_preview_set_region (chooser->format_preview, locale_id);
103 }
104 
105 static gint
sort_regions(gconstpointer a,gconstpointer b,gpointer data)106 sort_regions (gconstpointer a,
107               gconstpointer b,
108               gpointer      data)
109 {
110         const gchar *la;
111         const gchar *lb;
112 
113         if (g_object_get_data (G_OBJECT (a), "locale-id") == NULL)
114                 return 1;
115         if (g_object_get_data (G_OBJECT (b), "locale-id") == NULL)
116                 return -1;
117 
118         la = g_object_get_data (G_OBJECT (a), "locale-name");
119         lb = g_object_get_data (G_OBJECT (b), "locale-name");
120 
121         return g_strcmp0 (la, lb);
122 }
123 
124 static GtkWidget *
padded_label_new(const char * text)125 padded_label_new (const char *text)
126 {
127         GtkWidget *widget, *label;
128 
129         widget = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10);
130         g_object_set (widget, "margin-top", 4, NULL);
131         g_object_set (widget, "margin-bottom", 4, NULL);
132         g_object_set (widget, "margin-start", 10, NULL);
133         g_object_set (widget, "margin-end", 10, NULL);
134 
135         label = gtk_label_new (text);
136         gtk_label_set_ellipsize (GTK_LABEL (label), PANGO_ELLIPSIZE_END);
137         gtk_widget_show (label);
138         gtk_container_add (GTK_CONTAINER (widget), label);
139 
140         return widget;
141 }
142 
143 static void
format_chooser_back_button_clicked_cb(CcFormatChooser * self)144 format_chooser_back_button_clicked_cb (CcFormatChooser *self)
145 {
146   g_assert (CC_IS_FORMAT_CHOOSER (self));
147 
148   gtk_header_bar_set_title (GTK_HEADER_BAR (self->title_bar), _("Formats"));
149   hdy_leaflet_set_visible_child_name (HDY_LEAFLET (self->main_leaflet), "region-list");
150   gtk_stack_set_visible_child (GTK_STACK (self->title_buttons), self->cancel_button);
151   gtk_widget_show (self->done_button);
152 }
153 
154 static void
cc_format_chooser_preview_button_set_visible(GtkListBoxRow * row,gpointer user_data)155 cc_format_chooser_preview_button_set_visible (GtkListBoxRow *row,
156                                               gpointer       user_data)
157 {
158   GtkWidget *button;
159   gboolean visible;
160 
161   g_assert (GTK_IS_LIST_BOX_ROW (row));
162 
163   visible = GPOINTER_TO_INT (user_data);
164   button = g_object_get_data (G_OBJECT (row), "preview-button");
165   g_assert (button);
166 
167   gtk_widget_set_opacity (button, visible);
168   gtk_widget_set_sensitive (button, visible);
169 }
170 
171 static void
format_chooser_leaflet_fold_changed_cb(CcFormatChooser * self)172 format_chooser_leaflet_fold_changed_cb (CcFormatChooser *self)
173 {
174   gboolean folded;
175 
176   g_assert (CC_IS_FORMAT_CHOOSER (self));
177 
178   folded = hdy_leaflet_get_folded (HDY_LEAFLET (self->main_leaflet));
179   gtk_container_foreach (GTK_CONTAINER (self->common_region_listbox),
180                          (GtkCallback)cc_format_chooser_preview_button_set_visible,
181                          GINT_TO_POINTER (folded));
182   gtk_container_foreach (GTK_CONTAINER (self->region_listbox),
183                          (GtkCallback)cc_format_chooser_preview_button_set_visible,
184                          GINT_TO_POINTER (folded));
185 
186   if (!folded)
187     {
188       cc_format_preview_set_region (self->format_preview, self->region);
189       gtk_header_bar_set_title (GTK_HEADER_BAR (self->title_bar), _("Formats"));
190       hdy_leaflet_set_visible_child_name (HDY_LEAFLET (self->main_leaflet), "region-list");
191       gtk_stack_set_visible_child (GTK_STACK (self->title_buttons), self->cancel_button);
192       gtk_widget_show (self->done_button);
193     }
194 }
195 
196 static void
preview_button_clicked_cb(CcFormatChooser * self,GtkWidget * button)197 preview_button_clicked_cb (CcFormatChooser *self,
198                            GtkWidget       *button)
199 {
200   GtkWidget *row;
201   const gchar *region, *locale_name;
202 
203   g_assert (CC_IS_FORMAT_CHOOSER (self));
204   g_assert (GTK_IS_WIDGET (button));
205 
206   row = gtk_widget_get_ancestor (button, GTK_TYPE_LIST_BOX_ROW);
207   g_assert (row);
208 
209   region = g_object_get_data (G_OBJECT (row), "locale-id");
210   locale_name = g_object_get_data (G_OBJECT (row), "locale-name");
211   cc_format_preview_set_region (self->format_preview, region);
212 
213   hdy_leaflet_set_visible_child_name (HDY_LEAFLET (self->main_leaflet), "preview");
214   gtk_stack_set_visible_child (GTK_STACK (self->title_buttons), self->back_button);
215   gtk_widget_hide (self->done_button);
216 
217   if (locale_name)
218     gtk_header_bar_set_title (GTK_HEADER_BAR (self->title_bar), locale_name);
219 }
220 
221 static GtkWidget *
region_widget_new(CcFormatChooser * self,const gchar * locale_id)222 region_widget_new (CcFormatChooser *self,
223                    const gchar     *locale_id)
224 {
225         gchar *locale_name;
226         gchar *locale_current_name;
227         gchar *locale_untranslated_name;
228         GtkWidget *row, *box, *button;
229         GtkWidget *check;
230 
231         locale_name = gnome_get_country_from_locale (locale_id, locale_id);
232         if (!locale_name)
233           return NULL;
234 
235         locale_current_name = gnome_get_country_from_locale (locale_id, NULL);
236         locale_untranslated_name = gnome_get_country_from_locale (locale_id, "C");
237 
238         row = gtk_list_box_row_new ();
239         gtk_widget_show (row);
240         box = padded_label_new (locale_name);
241         gtk_widget_show (box);
242         gtk_container_add (GTK_CONTAINER (row), box);
243 
244         button = gtk_button_new_from_icon_name ("view-layout-symbolic", GTK_ICON_SIZE_BUTTON);
245         g_signal_connect_object (button, "clicked", G_CALLBACK (preview_button_clicked_cb),
246                                  self, G_CONNECT_SWAPPED);
247         gtk_widget_show (button);
248         gtk_box_pack_end (GTK_BOX (box), button, FALSE, TRUE, 0);
249 
250         check = gtk_image_new ();
251         gtk_widget_show (check);
252         gtk_image_set_from_icon_name (GTK_IMAGE (check), "object-select-symbolic", GTK_ICON_SIZE_MENU);
253         gtk_widget_set_opacity (check, 0.0);
254         g_object_set (check, "icon-size", GTK_ICON_SIZE_MENU, NULL);
255         gtk_container_add (GTK_CONTAINER (box), check);
256 
257         g_object_set_data (G_OBJECT (row), "check", check);
258         g_object_set_data (G_OBJECT (row), "preview-button", button);
259         g_object_set_data_full (G_OBJECT (row), "locale-id", g_strdup (locale_id), g_free);
260         g_object_set_data_full (G_OBJECT (row), "locale-name", locale_name, g_free);
261         g_object_set_data_full (G_OBJECT (row), "locale-current-name", locale_current_name, g_free);
262         g_object_set_data_full (G_OBJECT (row), "locale-untranslated-name", locale_untranslated_name, g_free);
263 
264         return row;
265 }
266 
267 static void
add_regions(CcFormatChooser * chooser,gchar ** locale_ids,GHashTable * initial)268 add_regions (CcFormatChooser *chooser,
269              gchar          **locale_ids,
270              GHashTable      *initial)
271 {
272         g_autoptr(GList) initial_locales = NULL;
273         GtkWidget *widget;
274         GList *l;
275 
276         chooser->adding = TRUE;
277         initial_locales = g_hash_table_get_keys (initial);
278 
279         /* Populate Common Locales */
280         for (l = initial_locales; l != NULL; l = l->next) {
281                 if (!cc_common_language_has_font (l->data))
282                         continue;
283 
284                 widget = region_widget_new (chooser, l->data);
285                 if (!widget)
286                         continue;
287 
288                 gtk_widget_show (widget);
289                 gtk_container_add (GTK_CONTAINER (chooser->common_region_listbox), widget);
290           }
291 
292         /* Populate All locales */
293         while (*locale_ids) {
294                 gchar *locale_id;
295 
296                 locale_id = *locale_ids;
297                 locale_ids ++;
298 
299                 if (!cc_common_language_has_font (locale_id))
300                         continue;
301 
302                 widget = region_widget_new (chooser, locale_id);
303                 if (!widget)
304                   continue;
305 
306                 gtk_widget_show (widget);
307                 gtk_container_add (GTK_CONTAINER (chooser->region_listbox), widget);
308         }
309 
310         chooser->adding = FALSE;
311 }
312 
313 static void
add_all_regions(CcFormatChooser * chooser)314 add_all_regions (CcFormatChooser *chooser)
315 {
316         g_auto(GStrv) locale_ids = NULL;
317         g_autoptr(GHashTable) initial = NULL;
318 
319         locale_ids = gnome_get_all_locales ();
320         initial = cc_common_language_get_initial_languages ();
321         add_regions (chooser, locale_ids, initial);
322 }
323 
324 static gboolean
match_all(gchar ** words,const gchar * str)325 match_all (gchar       **words,
326            const gchar  *str)
327 {
328         gchar **w;
329 
330         for (w = words; *w; ++w)
331                 if (!strstr (str, *w))
332                         return FALSE;
333 
334         return TRUE;
335 }
336 
337 static gboolean
region_visible(GtkListBoxRow * row,gpointer user_data)338 region_visible (GtkListBoxRow *row,
339                 gpointer   user_data)
340 {
341         CcFormatChooser *chooser = user_data;
342         g_autofree gchar *locale_name = NULL;
343         g_autofree gchar *locale_current_name = NULL;
344         g_autofree gchar *locale_untranslated_name = NULL;
345         gboolean match = TRUE;
346 
347         if (!chooser->filter_words)
348           goto end;
349 
350         locale_name =
351                 cc_util_normalize_casefold_and_unaccent (g_object_get_data (G_OBJECT (row), "locale-name"));
352         if (match_all (chooser->filter_words, locale_name))
353           goto end;
354 
355         locale_current_name =
356                 cc_util_normalize_casefold_and_unaccent (g_object_get_data (G_OBJECT (row), "locale-current-name"));
357         if (match_all (chooser->filter_words, locale_current_name))
358           goto end;
359 
360         locale_untranslated_name =
361                 cc_util_normalize_casefold_and_unaccent (g_object_get_data (G_OBJECT (row), "locale-untranslated-name"));
362 
363         match = match_all (chooser->filter_words, locale_untranslated_name);
364 
365  end:
366         if (match)
367           chooser->no_results = FALSE;
368         return match;
369 }
370 
371 static void
filter_changed(CcFormatChooser * chooser)372 filter_changed (CcFormatChooser *chooser)
373 {
374         g_autofree gchar *filter_contents = NULL;
375         gboolean visible;
376 
377         g_clear_pointer (&chooser->filter_words, g_strfreev);
378 
379         filter_contents =
380                 cc_util_normalize_casefold_and_unaccent (gtk_entry_get_text (GTK_ENTRY (chooser->region_filter_entry)));
381 
382         /* The popular listbox is shown only if search is empty */
383         visible = filter_contents == NULL || *filter_contents == '\0';
384         gtk_widget_set_visible (chooser->common_region_listbox, visible);
385         gtk_widget_set_visible (chooser->common_region_title, visible);
386         gtk_widget_set_visible (chooser->region_title, visible);
387 
388         /* Reset cached search state */
389         chooser->no_results = TRUE;
390 
391         if (!filter_contents) {
392                 gtk_list_box_invalidate_filter (GTK_LIST_BOX (chooser->region_listbox));
393                 gtk_list_box_set_placeholder (GTK_LIST_BOX (chooser->region_listbox), NULL);
394                 return;
395         }
396         chooser->filter_words = g_strsplit_set (g_strstrip (filter_contents), " ", 0);
397         gtk_list_box_invalidate_filter (GTK_LIST_BOX (chooser->region_listbox));
398 
399         if (chooser->no_results)
400           gtk_stack_set_visible_child (GTK_STACK (chooser->region_list_stack),
401                                        GTK_WIDGET (chooser->empty_results_view));
402         else
403           gtk_stack_set_visible_child (GTK_STACK (chooser->region_list_stack),
404                                        GTK_WIDGET (chooser->region_list));
405 }
406 
407 static void
row_activated(CcFormatChooser * chooser,GtkListBoxRow * row)408 row_activated (CcFormatChooser *chooser,
409                GtkListBoxRow   *row)
410 {
411         const gchar *new_locale_id;
412 
413         if (chooser->adding)
414                 return;
415 
416         new_locale_id = g_object_get_data (G_OBJECT (row), "locale-id");
417         if (g_strcmp0 (new_locale_id, chooser->region) == 0) {
418                 gtk_dialog_response (GTK_DIALOG (chooser),
419                                      gtk_dialog_get_response_for_widget (GTK_DIALOG (chooser),
420                                                                          chooser->done_button));
421         } else {
422                 set_locale_id (chooser, new_locale_id);
423         }
424 }
425 
426 static void
activate_default(CcFormatChooser * chooser)427 activate_default (CcFormatChooser *chooser)
428 {
429         GtkWidget *focus;
430         const gchar *locale_id;
431 
432         focus = gtk_window_get_focus (GTK_WINDOW (chooser));
433         if (!focus)
434                 return;
435 
436         locale_id = g_object_get_data (G_OBJECT (focus), "locale-id");
437         if (g_strcmp0 (locale_id, chooser->region) == 0)
438                 return;
439 
440         g_signal_stop_emission_by_name (chooser, "activate-default");
441         gtk_widget_activate (focus);
442 }
443 
444 static void
cc_format_chooser_dispose(GObject * object)445 cc_format_chooser_dispose (GObject *object)
446 {
447         CcFormatChooser *chooser = CC_FORMAT_CHOOSER (object);
448 
449         g_clear_pointer (&chooser->filter_words, g_strfreev);
450         g_clear_pointer (&chooser->region, g_free);
451 
452         G_OBJECT_CLASS (cc_format_chooser_parent_class)->dispose (object);
453 }
454 
455 void
cc_format_chooser_class_init(CcFormatChooserClass * klass)456 cc_format_chooser_class_init (CcFormatChooserClass *klass)
457 {
458         GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
459         GObjectClass *object_class = G_OBJECT_CLASS (klass);
460 
461         object_class->dispose = cc_format_chooser_dispose;
462 
463         g_type_ensure (CC_TYPE_FORMAT_PREVIEW);
464 
465         gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/region/cc-format-chooser.ui");
466 
467         gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, title_bar);
468         gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, title_buttons);
469         gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, cancel_button);
470         gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, back_button);
471         gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, done_button);
472         gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, main_leaflet);
473         gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, region_filter_entry);
474         gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, common_region_title);
475         gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, common_region_listbox);
476         gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, region_title);
477         gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, region_listbox);
478         gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, region_list);
479         gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, region_list_stack);
480         gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, empty_results_view);
481         gtk_widget_class_bind_template_child (widget_class, CcFormatChooser, format_preview);
482 
483         gtk_widget_class_bind_template_callback (widget_class, format_chooser_back_button_clicked_cb);
484         gtk_widget_class_bind_template_callback (widget_class, format_chooser_leaflet_fold_changed_cb);
485         gtk_widget_class_bind_template_callback (widget_class, filter_changed);
486         gtk_widget_class_bind_template_callback (widget_class, row_activated);
487 }
488 
489 void
cc_format_chooser_init(CcFormatChooser * chooser)490 cc_format_chooser_init (CcFormatChooser *chooser)
491 {
492         gtk_widget_init_template (GTK_WIDGET (chooser));
493 
494         gtk_list_box_set_sort_func (GTK_LIST_BOX (chooser->common_region_listbox),
495                                     (GtkListBoxSortFunc)sort_regions, chooser, NULL);
496         gtk_list_box_set_sort_func (GTK_LIST_BOX (chooser->region_listbox),
497                                     (GtkListBoxSortFunc)sort_regions, chooser, NULL);
498         gtk_list_box_set_filter_func (GTK_LIST_BOX (chooser->region_listbox),
499                                       region_visible, chooser, NULL);
500         gtk_list_box_set_header_func (GTK_LIST_BOX (chooser->region_listbox),
501                                       cc_list_box_update_header_func, NULL, NULL);
502         gtk_list_box_set_header_func (GTK_LIST_BOX (chooser->common_region_listbox),
503                                       cc_list_box_update_header_func, NULL, NULL);
504 
505         add_all_regions (chooser);
506         gtk_list_box_invalidate_filter (GTK_LIST_BOX (chooser->region_listbox));
507         format_chooser_leaflet_fold_changed_cb (chooser);
508 
509         g_signal_connect_object (chooser, "activate-default",
510                                  G_CALLBACK (activate_default), chooser, G_CONNECT_SWAPPED);
511 }
512 
513 CcFormatChooser *
cc_format_chooser_new(void)514 cc_format_chooser_new (void)
515 {
516         return CC_FORMAT_CHOOSER (g_object_new (CC_TYPE_FORMAT_CHOOSER,
517                                                 "use-header-bar", 1,
518                                                 NULL));
519 }
520 
521 void
cc_format_chooser_clear_filter(CcFormatChooser * chooser)522 cc_format_chooser_clear_filter (CcFormatChooser *chooser)
523 {
524         g_return_if_fail (CC_IS_FORMAT_CHOOSER (chooser));
525         gtk_entry_set_text (GTK_ENTRY (chooser->region_filter_entry), "");
526 }
527 
528 const gchar *
cc_format_chooser_get_region(CcFormatChooser * chooser)529 cc_format_chooser_get_region (CcFormatChooser *chooser)
530 {
531         g_return_val_if_fail (CC_IS_FORMAT_CHOOSER (chooser), NULL);
532         return chooser->region;
533 }
534 
535 void
cc_format_chooser_set_region(CcFormatChooser * chooser,const gchar * region)536 cc_format_chooser_set_region (CcFormatChooser *chooser,
537                               const gchar     *region)
538 {
539         g_return_if_fail (CC_IS_FORMAT_CHOOSER (chooser));
540         set_locale_id (chooser, region);
541 }
542