1 /*
2  * Copyright (C) 2019 Purism SPC
3  *
4  * SPDX-License-Identifier: LGPL-2.1+
5  */
6 
7 #include "config.h"
8 #include <glib/gi18n-lib.h>
9 
10 #include "hdy-preferences-window.h"
11 
12 #include "hdy-action-row.h"
13 #include "hdy-list-box.h"
14 #include "hdy-preferences-group-private.h"
15 #include "hdy-preferences-page-private.h"
16 #include "hdy-squeezer.h"
17 #include "hdy-view-switcher.h"
18 #include "hdy-view-switcher-bar.h"
19 
20 /**
21  * SECTION:hdy-preferences-window
22  * @short_description: A window to present an application's preferences.
23  * @Title: HdyPreferencesWindow
24  *
25  * The #HdyPreferencesWindow widget presents an application's preferences
26  * gathered into pages and groups. The preferences are searchable by the user.
27  *
28  * Since: 0.0.10
29  */
30 
31 typedef struct
32 {
33   GtkStack *content_stack;
34   GtkStack *pages_stack;
35   GtkToggleButton *search_button;
36   GtkSearchEntry *search_entry;
37   GtkListBox *search_results;
38   GtkStack *search_stack;
39   HdySqueezer *squeezer;
40   GtkLabel *title_label;
41   GtkStack *title_stack;
42   HdyViewSwitcherBar *view_switcher_bar;
43   HdyViewSwitcher *view_switcher_narrow;
44   HdyViewSwitcher *view_switcher_wide;
45 
46   gint n_last_search_results;
47 } HdyPreferencesWindowPrivate;
48 
G_DEFINE_TYPE_WITH_PRIVATE(HdyPreferencesWindow,hdy_preferences_window,GTK_TYPE_WINDOW)49 G_DEFINE_TYPE_WITH_PRIVATE (HdyPreferencesWindow, hdy_preferences_window, GTK_TYPE_WINDOW)
50 
51 static gboolean
52 is_title_label_visible (GBinding     *binding,
53                         const GValue *from_value,
54                         GValue       *to_value,
55                         gpointer      user_data)
56 {
57   g_value_set_boolean (to_value, g_value_get_object (from_value) == user_data);
58 
59   return TRUE;
60 }
61 
62 static gboolean
filter_search_results(HdyActionRow * row,HdyPreferencesWindow * self)63 filter_search_results (HdyActionRow         *row,
64                        HdyPreferencesWindow *self)
65 {
66   HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
67   g_autofree gchar *text = g_utf8_casefold (gtk_entry_get_text (GTK_ENTRY (priv->search_entry)), -1);
68   g_autofree gchar *title = g_utf8_casefold (hdy_action_row_get_title (row), -1);
69   g_autofree gchar *subtitle = NULL;
70 
71   if (strstr (title, text)) {
72     priv->n_last_search_results++;
73 
74     return TRUE;
75   }
76 
77   subtitle = g_utf8_casefold (hdy_action_row_get_subtitle (row), -1);
78 
79   if (!!strstr (subtitle, text)) {
80     priv->n_last_search_results++;
81 
82     return TRUE;
83   }
84 
85   return FALSE;
86 }
87 
88 static GtkWidget *
new_search_row_for_preference(HdyPreferencesRow * row,HdyPreferencesWindow * self)89 new_search_row_for_preference (HdyPreferencesRow    *row,
90                                HdyPreferencesWindow *self)
91 {
92   HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
93   HdyActionRow *widget;
94   HdyPreferencesGroup *group;
95   HdyPreferencesPage *page;
96   const gchar *group_title, *page_title;
97   GtkWidget *parent;
98 
99   g_assert (HDY_IS_PREFERENCES_ROW (row));
100 
101   widget = hdy_action_row_new ();
102   g_object_bind_property (row, "title", widget, "title", G_BINDING_SYNC_CREATE);
103   g_object_bind_property (row, "use-underline", widget, "use-underline", G_BINDING_SYNC_CREATE);
104 
105   for (parent = gtk_widget_get_parent (GTK_WIDGET (row));
106        parent != NULL && !HDY_IS_PREFERENCES_GROUP (parent);
107        parent = gtk_widget_get_parent (parent));
108   group = parent != NULL ? HDY_PREFERENCES_GROUP (parent) : NULL;
109   group_title = group != NULL ? hdy_preferences_group_get_title (group) : NULL;
110   if (g_strcmp0 (group_title, "") == 0)
111     group_title = NULL;
112 
113   for (parent = gtk_widget_get_parent (GTK_WIDGET (group));
114        parent != NULL && !HDY_IS_PREFERENCES_PAGE (parent);
115        parent = gtk_widget_get_parent (parent));
116   page = parent != NULL ? HDY_PREFERENCES_PAGE (parent) : NULL;
117   page_title = page != NULL ? hdy_preferences_page_get_title (page) : NULL;
118   if (g_strcmp0 (page_title, "") == 0)
119     page_title = NULL;
120 
121   if (group_title && !gtk_widget_get_visible (GTK_WIDGET (priv->view_switcher_wide)))
122     hdy_action_row_set_subtitle (widget, group_title);
123   if (group_title) {
124     g_autofree gchar *subtitle = g_strdup_printf ("%s → %s", page_title != NULL ? page_title : _("Untitled page"), group_title);
125     hdy_action_row_set_subtitle (widget, subtitle);
126   } else if (page_title)
127     hdy_action_row_set_subtitle (widget, page_title);
128 
129   gtk_widget_show (GTK_WIDGET (widget));
130 
131   g_object_set_data (G_OBJECT (widget), "page", page);
132   g_object_set_data (G_OBJECT (widget), "row", row);
133 
134   return GTK_WIDGET (widget);
135 }
136 
137 static void
update_search_results(HdyPreferencesWindow * self)138 update_search_results (HdyPreferencesWindow *self)
139 {
140   HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
141   g_autoptr (GListStore) model;
142 
143   model = g_list_store_new (HDY_TYPE_PREFERENCES_ROW);
144   gtk_container_foreach (GTK_CONTAINER (priv->pages_stack), (GtkCallback) hdy_preferences_page_add_preferences_to_model, model);
145   gtk_container_foreach (GTK_CONTAINER (priv->search_results), (GtkCallback) gtk_widget_destroy, NULL);
146   for (guint i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (model)); i++)
147     gtk_container_add (GTK_CONTAINER (priv->search_results),
148                        new_search_row_for_preference ((HdyPreferencesRow *) g_list_model_get_item (G_LIST_MODEL (model), i), self));
149 }
150 
151 static void
search_result_activated(HdyPreferencesWindow * self,HdyActionRow * widget)152 search_result_activated (HdyPreferencesWindow *self,
153                          HdyActionRow         *widget)
154 {
155   HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
156   HdyPreferencesPage *page;
157   HdyPreferencesRow *row;
158   GtkAdjustment *adjustment;
159   GtkAllocation allocation;
160   gint y = 0;
161 
162   gtk_toggle_button_set_active (priv->search_button, FALSE);
163   page = HDY_PREFERENCES_PAGE (g_object_get_data (G_OBJECT (widget), "page"));
164   row = HDY_PREFERENCES_ROW (g_object_get_data (G_OBJECT (widget), "row"));
165 
166   g_assert (page != NULL);
167   g_assert (row != NULL);
168 
169   adjustment = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (page));
170 
171   g_assert (adjustment != NULL);
172 
173   gtk_stack_set_visible_child (priv->pages_stack, GTK_WIDGET (page));
174   gtk_widget_set_can_focus (GTK_WIDGET (row), TRUE);
175   gtk_widget_grab_focus (GTK_WIDGET (row));
176 
177   if (!gtk_widget_translate_coordinates (GTK_WIDGET (row), GTK_WIDGET (page), 0, 0, NULL, &y))
178     return;
179 
180   gtk_container_set_focus_child (GTK_CONTAINER (page), GTK_WIDGET (row));
181   y += gtk_adjustment_get_value (adjustment);
182   gtk_widget_get_allocation (GTK_WIDGET (row), &allocation);
183   gtk_adjustment_clamp_page (adjustment, y, y + allocation.height);
184 }
185 
186 static gboolean
key_pressed(GtkWidget * sender,GdkEvent * event,HdyPreferencesWindow * self)187 key_pressed (GtkWidget            *sender,
188              GdkEvent             *event,
189              HdyPreferencesWindow *self)
190 {
191   HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
192   GdkModifierType default_modifiers = gtk_accelerator_get_default_mod_mask ();
193   guint keyval;
194   GdkModifierType state;
195   gunichar c;
196 
197   gdk_event_get_keyval (event, &keyval);
198   gdk_event_get_state (event, &state);
199 
200   if ((keyval == GDK_KEY_f || keyval == GDK_KEY_F) &&
201       (state & default_modifiers) == GDK_CONTROL_MASK) {
202     gtk_toggle_button_set_active (priv->search_button, TRUE);
203 
204     return TRUE;
205   }
206 
207   if (keyval == GDK_KEY_Escape &&
208       gtk_toggle_button_get_active (priv->search_button)) {
209     gtk_toggle_button_set_active (priv->search_button, FALSE);
210 
211     return TRUE;
212   }
213 
214   c = gdk_keyval_to_unicode (keyval);
215   if (g_unichar_isgraph (c)) {
216     gchar text[6] = { 0 };
217     g_unichar_to_utf8 (c, text);
218     gtk_entry_set_text (GTK_ENTRY (priv->search_entry), text);
219     gtk_toggle_button_set_active (priv->search_button, TRUE);
220 
221     return TRUE;
222   }
223 
224   return FALSE;
225 }
226 
227 static void
header_bar_size_allocated(HdyPreferencesWindow * self,GdkRectangle * allocation)228 header_bar_size_allocated (HdyPreferencesWindow *self,
229                            GdkRectangle         *allocation)
230 {
231   HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
232 
233   hdy_squeezer_set_child_enabled (priv->squeezer, GTK_WIDGET (priv->view_switcher_wide), allocation->width > 540);
234   hdy_squeezer_set_child_enabled (priv->squeezer, GTK_WIDGET (priv->view_switcher_narrow), allocation->width > 360);
235 }
236 
237 static void
search_button_activated(HdyPreferencesWindow * self)238 search_button_activated (HdyPreferencesWindow *self)
239 {
240   HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
241 
242   if (gtk_toggle_button_get_active (priv->search_button)) {
243     update_search_results (self);
244     gtk_stack_set_visible_child_name (priv->title_stack, "search");
245     gtk_stack_set_visible_child_name (priv->content_stack, "search");
246     gtk_entry_grab_focus_without_selecting (GTK_ENTRY (priv->search_entry));
247     /* Grabbing without selecting puts the cursor at the start of the buffer, so
248      * for "type to search" to work we must move the cursor at the end. We can't
249      * use GTK_MOVEMENT_BUFFER_ENDS because it causes a sound to be played.
250      */
251     g_signal_emit_by_name (priv->search_entry, "move-cursor",
252                            GTK_MOVEMENT_LOGICAL_POSITIONS, G_MAXINT, FALSE, NULL);
253   } else {
254     gtk_stack_set_visible_child_name (priv->title_stack, "pages");
255     gtk_stack_set_visible_child_name (priv->content_stack, "pages");
256   }
257 }
258 
259 static void
search_changed(HdyPreferencesWindow * self)260 search_changed (HdyPreferencesWindow *self)
261 {
262   HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
263 
264   priv->n_last_search_results = 0;
265   gtk_list_box_invalidate_filter (priv->search_results);
266   gtk_stack_set_visible_child_name (priv->search_stack,
267                                     priv->n_last_search_results > 0 ? "results" : "no-results");
268 }
269 
270 static void
count_children_cb(GtkWidget * widget,gint * count)271 count_children_cb (GtkWidget *widget,
272                    gint      *count)
273 {
274   (*count)++;
275 }
276 
277 static void
update_pages_switcher_visibility(HdyPreferencesWindow * self)278 update_pages_switcher_visibility (HdyPreferencesWindow *self)
279 {
280   HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
281   gint count = 0;
282 
283   gtk_container_foreach (GTK_CONTAINER (priv->pages_stack), (GtkCallback) count_children_cb, &count);
284 
285   gtk_widget_set_visible (GTK_WIDGET (priv->view_switcher_wide), count > 1);
286   gtk_widget_set_visible (GTK_WIDGET (priv->view_switcher_narrow), count > 1);
287   gtk_widget_set_visible (GTK_WIDGET (priv->view_switcher_bar), count > 1);
288 }
289 
290 static void
on_page_icon_name_changed(HdyPreferencesPage * page,GParamSpec * pspec,HdyPreferencesWindow * self)291 on_page_icon_name_changed (HdyPreferencesPage   *page,
292                            GParamSpec           *pspec,
293                            HdyPreferencesWindow *self)
294 {
295   HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
296 
297   gtk_container_child_set (GTK_CONTAINER (priv->pages_stack), GTK_WIDGET (page),
298                            "icon-name", hdy_preferences_page_get_icon_name (page),
299                            NULL);
300 }
301 
302 static void
on_page_title_changed(HdyPreferencesPage * page,GParamSpec * pspec,HdyPreferencesWindow * self)303 on_page_title_changed (HdyPreferencesPage   *page,
304                        GParamSpec           *pspec,
305                        HdyPreferencesWindow *self)
306 {
307   HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
308 
309   gtk_container_child_set (GTK_CONTAINER (priv->pages_stack), GTK_WIDGET (page),
310                            "title", hdy_preferences_page_get_title (page),
311                            NULL);
312 }
313 
314 static void
hdy_preferences_window_add(GtkContainer * container,GtkWidget * child)315 hdy_preferences_window_add (GtkContainer *container,
316                             GtkWidget    *child)
317 {
318   HdyPreferencesWindow *self = HDY_PREFERENCES_WINDOW (container);
319   HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
320 
321   if (priv->content_stack == NULL)
322     GTK_CONTAINER_CLASS (hdy_preferences_window_parent_class)->add (container, child);
323   else if (HDY_IS_PREFERENCES_PAGE (child)) {
324     gtk_container_add (GTK_CONTAINER (priv->pages_stack), child);
325     on_page_icon_name_changed (HDY_PREFERENCES_PAGE (child), NULL, self);
326     on_page_title_changed (HDY_PREFERENCES_PAGE (child), NULL, self);
327     g_signal_connect (child, "notify::icon-name",
328                       G_CALLBACK (on_page_icon_name_changed), self);
329     g_signal_connect (child, "notify::title",
330                       G_CALLBACK (on_page_title_changed), self);
331 
332     update_pages_switcher_visibility (self);
333   } else
334     g_warning ("Can't add children of type %s to %s",
335                G_OBJECT_TYPE_NAME (child),
336                G_OBJECT_TYPE_NAME (container));
337 }
338 
339 static void
hdy_preferences_window_class_init(HdyPreferencesWindowClass * klass)340 hdy_preferences_window_class_init (HdyPreferencesWindowClass *klass)
341 {
342   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
343   GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
344 
345   container_class->add = hdy_preferences_window_add;
346 
347   gtk_widget_class_set_template_from_resource (widget_class,
348                                                "/sm/puri/handy/ui/hdy-preferences-window.ui");
349   gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, content_stack);
350   gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, pages_stack);
351   gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, search_button);
352   gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, search_entry);
353   gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, search_results);
354   gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, search_stack);
355   gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, squeezer);
356   gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, title_label);
357   gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, title_stack);
358   gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, view_switcher_bar);
359   gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, view_switcher_narrow);
360   gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, view_switcher_wide);
361   gtk_widget_class_bind_template_callback (widget_class, header_bar_size_allocated);
362   gtk_widget_class_bind_template_callback (widget_class, key_pressed);
363   gtk_widget_class_bind_template_callback (widget_class, search_button_activated);
364   gtk_widget_class_bind_template_callback (widget_class, search_changed);
365   gtk_widget_class_bind_template_callback (widget_class, search_result_activated);
366 }
367 
368 static void
hdy_preferences_window_init(HdyPreferencesWindow * self)369 hdy_preferences_window_init (HdyPreferencesWindow *self)
370 {
371   HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
372 
373   gtk_widget_init_template (GTK_WIDGET (self));
374 
375   g_object_bind_property_full (priv->squeezer,
376                                "visible-child",
377                                priv->view_switcher_bar,
378                                "reveal",
379                                G_BINDING_SYNC_CREATE,
380                                is_title_label_visible,
381                                NULL,
382                                priv->title_label,
383                                NULL);
384 
385   gtk_list_box_set_header_func (priv->search_results, hdy_list_box_separator_header, NULL, NULL);
386   gtk_list_box_set_filter_func (priv->search_results, (GtkListBoxFilterFunc) filter_search_results, self, NULL);
387 
388   update_pages_switcher_visibility (self);
389 }
390 
391 /**
392  * hdy_preferences_window_new:
393  *
394  * Creates a new #HdyPreferencesWindow.
395  *
396  * Returns: a new #HdyPreferencesWindow
397  *
398  * Since: 0.0.10
399  */
400 HdyPreferencesWindow *
hdy_preferences_window_new(void)401 hdy_preferences_window_new (void)
402 {
403   return g_object_new (HDY_TYPE_PREFERENCES_WINDOW, NULL);
404 }
405