Branch data Line data Source code
1 : : // SPDX-License-Identifier: GPL-3.0-or-later
2 : : // SPDX-FileCopyrightText: Andy Holmes <andrew.g.r.holmes@gmail.com>
3 : :
4 : : #define G_LOG_DOMAIN "valent-contact-page"
5 : :
6 : : #include "config.h"
7 : :
8 : : #include <adwaita.h>
9 : : #include <glib/gi18n.h>
10 : : #include <gtk/gtk.h>
11 : : #include <libebook-contacts/libebook-contacts.h>
12 : : #include <valent.h>
13 : :
14 : : #include "valent-contact-row.h"
15 : :
16 : : #include "valent-contact-page.h"
17 : :
18 : :
19 : : struct _ValentContactPage
20 : : {
21 : : AdwNavigationPage parent_instance;
22 : :
23 : : GListModel *contacts;
24 : : GtkWidget *placeholder_contact;
25 : : char *search_query;
26 : :
27 : : /* template */
28 : : GtkWidget *search_entry;
29 : : GtkListBox *contact_list;
30 : : GListModel *model;
31 : : GtkFilter *filter;
32 : : GtkStringSorter *sorter;
33 : :
34 : : AdwDialog *details_dialog;
35 : : GtkListBox *medium_list;
36 : : };
37 : :
38 [ + + + - ]: 5 : G_DEFINE_FINAL_TYPE (ValentContactPage, valent_contact_page, ADW_TYPE_NAVIGATION_PAGE)
39 : :
40 : : typedef enum {
41 : : PROP_CONTACTS = 1,
42 : : } ValentContactPageProperty;
43 : :
44 : : static GParamSpec *properties[PROP_CONTACTS + 1] = { NULL, };
45 : :
46 : : typedef enum {
47 : : SELECTED,
48 : : } ValentContactPageSignal;
49 : :
50 : : static guint signals[SELECTED + 1] = { 0, };
51 : :
52 : : static char *
53 : 0 : _phone_number_normalize (const char *number)
54 : : {
55 : 0 : g_autofree char *normalized = NULL;
56 : 0 : const char *s = number;
57 : 0 : size_t i = 0;
58 : :
59 [ # # ]: 0 : g_assert (number != NULL);
60 : :
61 : 0 : normalized = g_new (char, strlen (number) + 1);
62 [ # # ]: 0 : while (*s != '\0')
63 : : {
64 [ # # ]: 0 : if G_LIKELY (g_ascii_isdigit (*s))
65 : 0 : normalized[i++] = *s;
66 : :
67 : 0 : s++;
68 : : }
69 : 0 : normalized[i] = '\0';
70 : 0 : normalized = g_realloc (normalized, i * sizeof (char));
71 : :
72 : 0 : return g_steal_pointer (&normalized);
73 : : }
74 : :
75 : : static gboolean
76 : 0 : _e_contact_has_number (EContact *contact,
77 : : const char *query)
78 : : {
79 : 0 : GStrv tel_normalized = NULL;
80 : 0 : gboolean ret = FALSE;
81 : :
82 : 0 : tel_normalized = g_object_get_data (G_OBJECT (contact), "tel-normalized");
83 [ # # ]: 0 : if (tel_normalized == NULL)
84 : : {
85 : 0 : g_autoptr (GStrvBuilder) builder = NULL;
86 : 0 : GList *numbers = NULL;
87 : :
88 : 0 : builder = g_strv_builder_new ();
89 : 0 : numbers = e_contact_get (contact, E_CONTACT_TEL);
90 [ # # ]: 0 : for (const GList *iter = numbers; iter != NULL; iter = iter->next)
91 : 0 : g_strv_builder_take (builder, _phone_number_normalize (iter->data));
92 : 0 : g_list_free_full (numbers, g_free);
93 : :
94 : 0 : tel_normalized = g_strv_builder_end (builder);
95 [ # # ]: 0 : g_object_set_data_full (G_OBJECT (contact),
96 : : "tel-normalized",
97 : : tel_normalized,
98 : : (GDestroyNotify)g_strfreev);
99 : : }
100 : :
101 [ # # # # ]: 0 : for (size_t i = 0; tel_normalized != NULL && tel_normalized[i] != NULL; i++)
102 : : {
103 : 0 : ret = strstr (tel_normalized[i], query) != NULL;
104 [ # # ]: 0 : if (ret)
105 : : break;
106 : : }
107 : :
108 : 0 : return ret;
109 : : }
110 : :
111 : : static inline gboolean
112 : 0 : valent_contact_page_filter (gpointer item,
113 : : gpointer user_data)
114 : : {
115 : 0 : EContact *contact = E_CONTACT (item);
116 : 0 : ValentContactPage *self = VALENT_CONTACT_PAGE (user_data);
117 : 0 : g_autolist (EVCardAttribute) attrs = NULL;
118 : 0 : const char *query;
119 : 0 : g_autofree char *query_folded = NULL;
120 : 0 : g_autofree char *name = NULL;
121 : :
122 : 0 : attrs = e_contact_get (contact, E_CONTACT_TEL);
123 [ # # ]: 0 : if (attrs == NULL)
124 : : return FALSE;
125 : :
126 : 0 : query = gtk_editable_get_text (GTK_EDITABLE (self->search_entry));
127 [ # # ]: 0 : if (g_strcmp0 (query, "") == 0)
128 : : return TRUE;
129 : :
130 : : /* Show contact if text is substring of name
131 : : */
132 : 0 : query_folded = g_utf8_casefold (query, -1);
133 : 0 : name = g_utf8_casefold (e_contact_get_const (contact, E_CONTACT_FULL_NAME), -1);
134 [ # # ]: 0 : if (g_strrstr (name, query_folded) != NULL)
135 : : return TRUE;
136 : :
137 [ # # ]: 0 : if (_e_contact_has_number (contact, query))
138 : : return TRUE;
139 : :
140 : : return FALSE;
141 : : }
142 : :
143 : : static void
144 : 0 : on_search_changed (GtkSearchEntry *entry,
145 : : ValentContactPage *self)
146 : : {
147 : 0 : const char *query;
148 : :
149 : 0 : query = gtk_editable_get_text (GTK_EDITABLE (entry));
150 : : #if 0
151 : : if (check_number (query))
152 : : {
153 : : g_autoptr (EContact) contact = NULL;
154 : : g_autofree char *name_label = NULL;
155 : :
156 : : /* TRANSLATORS: dynamic string for phone number input (e.g. "Send to 911")
157 : : */
158 : : name_label = g_strdup_printf (_("Send to %s"), query);
159 : :
160 : : if (self->placeholder_contact == NULL)
161 : : {
162 : : contact = e_contact_new ();
163 : : e_contact_set (contact, E_CONTACT_FULL_NAME, query);
164 : : e_contact_set (contact, E_CONTACT_PHONE_OTHER, query);
165 : :
166 : : self->placeholder_contact = g_object_new (VALENT_TYPE_CONTACT_ROW,
167 : : "contact", contact,
168 : : "contact-name", name_label,
169 : : "contact-medium", query,
170 : : NULL);
171 : : gtk_list_box_insert (self->contact_list,
172 : : self->placeholder_contact,
173 : : -1);
174 : : }
175 : : else
176 : : {
177 : : g_object_get (self->placeholder_contact, "contact", &contact, NULL);
178 : :
179 : : e_contact_set (contact, E_CONTACT_FULL_NAME, query);
180 : : e_contact_set (contact, E_CONTACT_PHONE_OTHER, query);
181 : : g_object_set (self->placeholder_contact,
182 : : "contact-name", name_label,
183 : : "contact-medium", query,
184 : : NULL);
185 : : }
186 : : }
187 : : else if (self->placeholder_contact != NULL)
188 : : {
189 : : gtk_list_box_remove (self->contact_list,
190 : : self->placeholder_contact);
191 : : self->placeholder_contact = NULL;
192 : : }
193 : : #endif
194 : :
195 [ # # # # ]: 0 : if (self->search_query && g_str_has_prefix (query, self->search_query))
196 : 0 : gtk_filter_changed (self->filter, GTK_FILTER_CHANGE_MORE_STRICT);
197 [ # # # # ]: 0 : else if (self->search_query && g_str_has_prefix (self->search_query, query))
198 : 0 : gtk_filter_changed (self->filter, GTK_FILTER_CHANGE_LESS_STRICT);
199 : : else
200 : 0 : gtk_filter_changed (self->filter, GTK_FILTER_CHANGE_DIFFERENT);
201 : :
202 : 0 : g_set_str (&self->search_query, query);
203 : 0 : }
204 : :
205 : : static GtkWidget *
206 : 0 : contact_list_create (gpointer item,
207 : : gpointer user_data)
208 : : {
209 : 0 : EContact *contact = E_CONTACT (item);
210 : 0 : GtkWidget *row;
211 : 0 : g_autolist (EVCardAttribute) attrs = NULL;
212 : 0 : g_autofree char *number = NULL;
213 : 0 : unsigned int n_attrs;
214 : :
215 : 0 : attrs = e_contact_get_attributes (contact, E_CONTACT_TEL);
216 : 0 : n_attrs = g_list_length (attrs);
217 : :
218 : 0 : g_object_get (contact, "primary-phone", &number, NULL);
219 [ # # # # ]: 0 : if (number == NULL || *number == '\0')
220 : : {
221 : 0 : g_free (number);
222 : 0 : number = e_vcard_attribute_get_value ((EVCardAttribute *)attrs->data);
223 : : }
224 : :
225 [ # # ]: 0 : if (n_attrs > 1)
226 : : {
227 : 0 : g_autofree char *tmp = g_steal_pointer (&number);
228 : :
229 : 0 : number = g_strdup_printf (ngettext ("%s and %u more…",
230 : : "%s and %u more…",
231 : : n_attrs - 1),
232 : : tmp, n_attrs - 1);
233 : : }
234 : :
235 : 0 : row = g_object_new (VALENT_TYPE_CONTACT_ROW,
236 : : "contact", contact,
237 : : "contact-medium", number,
238 : : NULL);
239 : :
240 [ # # ]: 0 : if (n_attrs > 1)
241 : : {
242 : 0 : gtk_accessible_update_state (GTK_ACCESSIBLE (row),
243 : : GTK_ACCESSIBLE_STATE_EXPANDED, FALSE,
244 : : -1);
245 : : }
246 : :
247 : 0 : return row;
248 : : }
249 : :
250 : : static void
251 : 0 : on_contact_medium_selected (AdwActionRow *row,
252 : : ValentContactPage *self)
253 : : {
254 : 0 : EContact *contact;
255 : 0 : const char *medium;
256 : :
257 [ # # ]: 0 : g_assert (ADW_IS_ACTION_ROW (row));
258 : :
259 : 0 : contact = g_object_get_data (G_OBJECT (row), "contact");
260 : 0 : medium = adw_preferences_row_get_title (ADW_PREFERENCES_ROW (row));
261 : 0 : g_signal_emit (G_OBJECT (self), signals [SELECTED], 0, contact, medium);
262 : :
263 : 0 : adw_dialog_close (self->details_dialog);
264 : 0 : }
265 : :
266 : : static void
267 : 0 : on_contact_row_collapsed (AdwDialog *dialog,
268 : : GtkWidget *row)
269 : : {
270 : 0 : gtk_accessible_reset_relation (GTK_ACCESSIBLE (row),
271 : : GTK_ACCESSIBLE_RELATION_CONTROLS);
272 : 0 : gtk_accessible_update_state (GTK_ACCESSIBLE (row),
273 : : GTK_ACCESSIBLE_STATE_EXPANDED, FALSE,
274 : : -1);
275 : 0 : g_signal_handlers_disconnect_by_func (dialog, on_contact_row_collapsed, row);
276 : 0 : }
277 : :
278 : : static void
279 : 0 : on_contact_selected (ValentContactPage *self)
280 : : {
281 : 0 : g_autolist (EVCardAttribute) attrs = NULL;
282 : 0 : GtkListBoxRow *row;
283 : 0 : EContact *contact;
284 : :
285 [ # # ]: 0 : g_assert (VALENT_IS_CONTACT_PAGE (self));
286 : :
287 : 0 : row = gtk_list_box_get_selected_row (GTK_LIST_BOX (self->contact_list));
288 [ # # ]: 0 : if (row == NULL)
289 : 0 : return;
290 : :
291 : 0 : contact = valent_contact_row_get_contact (VALENT_CONTACT_ROW (row));
292 : 0 : attrs = e_contact_get_attributes (E_CONTACT (contact), E_CONTACT_TEL);
293 : :
294 [ # # ]: 0 : if (g_list_length (attrs) == 1)
295 : : {
296 : 0 : g_autofree char *medium = NULL;
297 : :
298 : 0 : g_object_get (row, "contact-medium", &medium, NULL);
299 : 0 : g_signal_emit (G_OBJECT (self), signals [SELECTED], 0, contact, medium);
300 : : }
301 : : else
302 : : {
303 : 0 : gtk_list_box_remove_all (GTK_LIST_BOX (self->medium_list));
304 [ # # ]: 0 : for (const GList *iter = attrs; iter; iter = iter->next)
305 : : {
306 : 0 : EVCardAttribute *attr = iter->data;
307 : 0 : GtkWidget *medium_row;
308 : 0 : g_autofree char *number = NULL;
309 : 0 : const char *type_ = NULL;
310 : :
311 [ # # ]: 0 : if (e_vcard_attribute_has_type (attr, "WORK"))
312 : 0 : type_ = _("Work");
313 [ # # ]: 0 : else if (e_vcard_attribute_has_type (attr, "CELL"))
314 : 0 : type_ = _("Mobile");
315 [ # # ]: 0 : else if (e_vcard_attribute_has_type (attr, "HOME"))
316 : 0 : type_ = _("Home");
317 : : else
318 : 0 : type_ = _("Other");
319 : :
320 : 0 : number = e_vcard_attribute_get_value (attr);
321 : 0 : medium_row = g_object_new (ADW_TYPE_ACTION_ROW,
322 : : "activatable", TRUE,
323 : : "title", number,
324 : : "subtitle", type_,
325 : : NULL);
326 : 0 : g_object_set_data_full (G_OBJECT (medium_row),
327 : : "contact",
328 : : g_object_ref (contact),
329 : : g_object_unref);
330 : 0 : g_signal_connect_object (medium_row,
331 : : "activated",
332 : : G_CALLBACK (on_contact_medium_selected),
333 : : self,
334 : : G_CONNECT_DEFAULT);
335 : :
336 : 0 : gtk_list_box_insert (self->medium_list, medium_row, -1);
337 : : }
338 : :
339 : : /* Present the dialog and match the expanded state
340 : : */
341 : 0 : gtk_accessible_update_state (GTK_ACCESSIBLE (row),
342 : : GTK_ACCESSIBLE_STATE_EXPANDED, TRUE,
343 : : -1);
344 : 0 : gtk_accessible_update_relation (GTK_ACCESSIBLE (row),
345 : : GTK_ACCESSIBLE_RELATION_CONTROLS, self->details_dialog, NULL,
346 : : -1);
347 : 0 : g_signal_connect_object (self->details_dialog,
348 : : "closed",
349 : : G_CALLBACK (on_contact_row_collapsed),
350 : : row,
351 : : G_CONNECT_DEFAULT);
352 : 0 : adw_dialog_present (self->details_dialog, GTK_WIDGET (self));
353 : : }
354 : : }
355 : :
356 : : /*
357 : : * AdwNavigationPage
358 : : */
359 : : static void
360 : 0 : valent_contact_page_shown (AdwNavigationPage *page)
361 : : {
362 : 0 : ValentContactPage *self = VALENT_CONTACT_PAGE (page);
363 : :
364 : 0 : gtk_widget_grab_focus (GTK_WIDGET (self->search_entry));
365 : :
366 [ # # ]: 0 : if (ADW_NAVIGATION_PAGE_CLASS (valent_contact_page_parent_class)->shown)
367 : 0 : ADW_NAVIGATION_PAGE_CLASS (valent_contact_page_parent_class)->shown (page);
368 : 0 : }
369 : :
370 : : /*
371 : : * GObject
372 : : */
373 : : static void
374 : 1 : valent_contact_page_finalize (GObject *object)
375 : : {
376 : 1 : ValentContactPage *self = VALENT_CONTACT_PAGE (object);
377 : :
378 [ - + ]: 1 : g_clear_object (&self->contacts);
379 [ - + ]: 1 : g_clear_pointer (&self->search_query, g_free);
380 : :
381 : 1 : G_OBJECT_CLASS (valent_contact_page_parent_class)->finalize (object);
382 : 1 : }
383 : :
384 : : static void
385 : 3 : valent_contact_page_get_property (GObject *object,
386 : : guint prop_id,
387 : : GValue *value,
388 : : GParamSpec *pspec)
389 : : {
390 : 3 : ValentContactPage *self = VALENT_CONTACT_PAGE (object);
391 : :
392 [ + - ]: 3 : switch ((ValentContactPageProperty)prop_id)
393 : : {
394 : 3 : case PROP_CONTACTS:
395 : 3 : g_value_set_object (value, self->contacts);
396 : 3 : break;
397 : :
398 : 0 : default:
399 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
400 : : }
401 : 3 : }
402 : :
403 : : static void
404 : 1 : valent_contact_page_set_property (GObject *object,
405 : : guint prop_id,
406 : : const GValue *value,
407 : : GParamSpec *pspec)
408 : : {
409 : 1 : ValentContactPage *self = VALENT_CONTACT_PAGE (object);
410 : :
411 [ + - ]: 1 : switch ((ValentContactPageProperty)prop_id)
412 : : {
413 : 1 : case PROP_CONTACTS:
414 : 1 : g_set_object (&self->contacts, g_value_get_object (value));
415 : 1 : break;
416 : :
417 : 0 : default:
418 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
419 : : }
420 : 1 : }
421 : :
422 : : static void
423 : 1 : valent_contact_page_class_init (ValentContactPageClass *klass)
424 : : {
425 : 1 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
426 : 1 : GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
427 : 1 : AdwNavigationPageClass *page_class = ADW_NAVIGATION_PAGE_CLASS (klass);
428 : :
429 : 1 : object_class->finalize = valent_contact_page_finalize;
430 : 1 : object_class->get_property = valent_contact_page_get_property;
431 : 1 : object_class->set_property = valent_contact_page_set_property;
432 : :
433 : 1 : gtk_widget_class_set_template_from_resource (widget_class, "/plugins/gnome/valent-contact-page.ui");
434 : 1 : gtk_widget_class_bind_template_child (widget_class, ValentContactPage, search_entry);
435 : 1 : gtk_widget_class_bind_template_child (widget_class, ValentContactPage, contact_list);
436 : 1 : gtk_widget_class_bind_template_child (widget_class, ValentContactPage, details_dialog);
437 : 1 : gtk_widget_class_bind_template_child (widget_class, ValentContactPage, medium_list);
438 : 1 : gtk_widget_class_bind_template_child (widget_class, ValentContactPage, model);
439 : 1 : gtk_widget_class_bind_template_child (widget_class, ValentContactPage, filter);
440 : 1 : gtk_widget_class_bind_template_child (widget_class, ValentContactPage, sorter);
441 : 1 : gtk_widget_class_bind_template_callback (widget_class, on_search_changed);
442 : 1 : gtk_widget_class_bind_template_callback (widget_class, on_contact_selected);
443 : :
444 : 1 : page_class->shown = valent_contact_page_shown;
445 : :
446 : : /**
447 : : * ValentContactPage:contacts:
448 : : *
449 : : * The `ValentContactsAdapter` providing contacts.
450 : : */
451 : 2 : properties [PROP_CONTACTS] =
452 : 1 : g_param_spec_object ("contacts", NULL, NULL,
453 : : VALENT_TYPE_CONTACTS_ADAPTER,
454 : : (G_PARAM_READWRITE |
455 : : G_PARAM_CONSTRUCT |
456 : : G_PARAM_STATIC_STRINGS));
457 : :
458 : 1 : g_object_class_install_properties (object_class, G_N_ELEMENTS (properties), properties);
459 : :
460 : : /**
461 : : * ValentContactPage::selected:
462 : : * @conversation: a `ValentContactPage`
463 : : * @contact: an `EContact`
464 : : * @medium: a contact medium
465 : : *
466 : : * The `ValentContactPage`::selected signal is emitted when a contact
467 : : * medium (e.g. phone number, email, etc) is selected.
468 : : */
469 : 2 : signals [SELECTED] =
470 : 1 : g_signal_new ("selected",
471 : : G_TYPE_FROM_CLASS (klass),
472 : : G_SIGNAL_RUN_LAST,
473 : : 0,
474 : : NULL, NULL, NULL,
475 : : G_TYPE_NONE, 2, E_TYPE_CONTACT, G_TYPE_STRING);
476 : 1 : }
477 : :
478 : : static void
479 : 1 : valent_contact_page_init (ValentContactPage *self)
480 : : {
481 : 1 : gtk_widget_init_template (GTK_WIDGET (self));
482 : :
483 : 1 : gtk_custom_filter_set_filter_func (GTK_CUSTOM_FILTER (self->filter),
484 : : valent_contact_page_filter,
485 : : self, NULL);
486 : 1 : gtk_list_box_bind_model (self->contact_list,
487 : : self->model,
488 : : contact_list_create,
489 : : self, NULL);
490 : 1 : }
491 : :
|