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