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 : : #if EDS_CHECK_VERSION (3, 59, 0)
212 : : g_autoptr (GList) attrs = NULL;
213 : : #else
214 : 0 : g_autolist (EVCardAttribute) attrs = NULL;
215 : : #endif
216 : 0 : g_autofree char *number = NULL;
217 : 0 : unsigned int n_attrs;
218 : :
219 : : #if EDS_CHECK_VERSION (3, 59, 0)
220 : : attrs = e_vcard_get_attributes_by_name (E_VCARD (contact), EVC_TEL);
221 : : #else
222 : 0 : attrs = e_contact_get_attributes (contact, E_CONTACT_TEL);
223 : : #endif
224 : 0 : n_attrs = g_list_length (attrs);
225 : :
226 : 0 : g_object_get (contact, "primary-phone", &number, NULL);
227 [ # # # # ]: 0 : if (number == NULL || *number == '\0')
228 : : {
229 : 0 : g_free (number);
230 : 0 : number = e_vcard_attribute_get_value ((EVCardAttribute *)attrs->data);
231 : : }
232 : :
233 [ # # ]: 0 : if (n_attrs > 1)
234 : : {
235 : 0 : g_autofree char *tmp = g_steal_pointer (&number);
236 : :
237 : 0 : number = g_strdup_printf (ngettext ("%s and %u more…",
238 : : "%s and %u more…",
239 : : n_attrs - 1),
240 : : tmp, n_attrs - 1);
241 : : }
242 : :
243 : 0 : row = g_object_new (VALENT_TYPE_CONTACT_ROW,
244 : : "contact", contact,
245 : : "contact-medium", number,
246 : : NULL);
247 : :
248 [ # # ]: 0 : if (n_attrs > 1)
249 : : {
250 : 0 : gtk_accessible_update_state (GTK_ACCESSIBLE (row),
251 : : GTK_ACCESSIBLE_STATE_EXPANDED, FALSE,
252 : : -1);
253 : : }
254 : :
255 : 0 : return row;
256 : : }
257 : :
258 : : static void
259 : 0 : on_contact_medium_selected (AdwActionRow *row,
260 : : ValentContactPage *self)
261 : : {
262 : 0 : EContact *contact;
263 : 0 : const char *medium;
264 : :
265 [ # # ]: 0 : g_assert (ADW_IS_ACTION_ROW (row));
266 : :
267 : 0 : contact = g_object_get_data (G_OBJECT (row), "contact");
268 : 0 : medium = adw_preferences_row_get_title (ADW_PREFERENCES_ROW (row));
269 : 0 : g_signal_emit (G_OBJECT (self), signals [SELECTED], 0, contact, medium);
270 : :
271 : 0 : adw_dialog_close (self->details_dialog);
272 : 0 : }
273 : :
274 : : static void
275 : 0 : on_contact_row_collapsed (AdwDialog *dialog,
276 : : GtkWidget *row)
277 : : {
278 : 0 : gtk_accessible_reset_relation (GTK_ACCESSIBLE (row),
279 : : GTK_ACCESSIBLE_RELATION_CONTROLS);
280 : 0 : gtk_accessible_update_state (GTK_ACCESSIBLE (row),
281 : : GTK_ACCESSIBLE_STATE_EXPANDED, FALSE,
282 : : -1);
283 : 0 : g_signal_handlers_disconnect_by_func (dialog, on_contact_row_collapsed, row);
284 : 0 : }
285 : :
286 : : static void
287 : 0 : on_contact_selected (ValentContactPage *self)
288 : : {
289 : : #if EDS_CHECK_VERSION (3, 59, 0)
290 : : g_autoptr (GList) attrs = NULL;
291 : : #else
292 : 0 : g_autolist (EVCardAttribute) attrs = NULL;
293 : : #endif
294 : 0 : GtkListBoxRow *row;
295 : 0 : EContact *contact;
296 : :
297 [ # # ]: 0 : g_assert (VALENT_IS_CONTACT_PAGE (self));
298 : :
299 : 0 : row = gtk_list_box_get_selected_row (GTK_LIST_BOX (self->contact_list));
300 [ # # ]: 0 : if (row == NULL)
301 : 0 : return;
302 : :
303 : 0 : contact = valent_contact_row_get_contact (VALENT_CONTACT_ROW (row));
304 : : #if EDS_CHECK_VERSION (3, 59, 0)
305 : : attrs = e_vcard_get_attributes_by_name (E_VCARD (contact), EVC_TEL);
306 : : #else
307 : 0 : attrs = e_contact_get_attributes (contact, E_CONTACT_TEL);
308 : : #endif
309 : :
310 [ # # ]: 0 : if (g_list_length (attrs) == 1)
311 : : {
312 : 0 : g_autofree char *medium = NULL;
313 : :
314 : 0 : g_object_get (row, "contact-medium", &medium, NULL);
315 : 0 : g_signal_emit (G_OBJECT (self), signals [SELECTED], 0, contact, medium);
316 : : }
317 : : else
318 : : {
319 : 0 : gtk_list_box_remove_all (GTK_LIST_BOX (self->medium_list));
320 [ # # ]: 0 : for (const GList *iter = attrs; iter; iter = iter->next)
321 : : {
322 : 0 : EVCardAttribute *attr = iter->data;
323 : 0 : GtkWidget *medium_row;
324 : 0 : g_autofree char *number = NULL;
325 : 0 : const char *type_ = NULL;
326 : :
327 [ # # ]: 0 : if (e_vcard_attribute_has_type (attr, "WORK"))
328 : 0 : type_ = _("Work");
329 [ # # ]: 0 : else if (e_vcard_attribute_has_type (attr, "CELL"))
330 : 0 : type_ = _("Mobile");
331 [ # # ]: 0 : else if (e_vcard_attribute_has_type (attr, "HOME"))
332 : 0 : type_ = _("Home");
333 : : else
334 : 0 : type_ = _("Other");
335 : :
336 : 0 : number = e_vcard_attribute_get_value (attr);
337 : 0 : medium_row = g_object_new (ADW_TYPE_ACTION_ROW,
338 : : "activatable", TRUE,
339 : : "title", number,
340 : : "subtitle", type_,
341 : : NULL);
342 : 0 : g_object_set_data_full (G_OBJECT (medium_row),
343 : : "contact",
344 : : g_object_ref (contact),
345 : : g_object_unref);
346 : 0 : g_signal_connect_object (medium_row,
347 : : "activated",
348 : : G_CALLBACK (on_contact_medium_selected),
349 : : self,
350 : : G_CONNECT_DEFAULT);
351 : :
352 : 0 : gtk_list_box_insert (self->medium_list, medium_row, -1);
353 : : }
354 : :
355 : : /* Present the dialog and match the expanded state
356 : : */
357 : 0 : gtk_accessible_update_state (GTK_ACCESSIBLE (row),
358 : : GTK_ACCESSIBLE_STATE_EXPANDED, TRUE,
359 : : -1);
360 : 0 : gtk_accessible_update_relation (GTK_ACCESSIBLE (row),
361 : : GTK_ACCESSIBLE_RELATION_CONTROLS, self->details_dialog, NULL,
362 : : -1);
363 : 0 : g_signal_connect_object (self->details_dialog,
364 : : "closed",
365 : : G_CALLBACK (on_contact_row_collapsed),
366 : : row,
367 : : G_CONNECT_DEFAULT);
368 : 0 : adw_dialog_present (self->details_dialog, GTK_WIDGET (self));
369 : : }
370 : : }
371 : :
372 : : /*
373 : : * AdwNavigationPage
374 : : */
375 : : static void
376 : 0 : valent_contact_page_shown (AdwNavigationPage *page)
377 : : {
378 : 0 : ValentContactPage *self = VALENT_CONTACT_PAGE (page);
379 : :
380 : 0 : gtk_widget_grab_focus (GTK_WIDGET (self->search_entry));
381 : :
382 [ # # ]: 0 : if (ADW_NAVIGATION_PAGE_CLASS (valent_contact_page_parent_class)->shown)
383 : 0 : ADW_NAVIGATION_PAGE_CLASS (valent_contact_page_parent_class)->shown (page);
384 : 0 : }
385 : :
386 : : /*
387 : : * GObject
388 : : */
389 : : static void
390 : 1 : valent_contact_page_finalize (GObject *object)
391 : : {
392 : 1 : ValentContactPage *self = VALENT_CONTACT_PAGE (object);
393 : :
394 [ - + ]: 1 : g_clear_object (&self->contacts);
395 [ - + ]: 1 : g_clear_pointer (&self->search_query, g_free);
396 : :
397 : 1 : G_OBJECT_CLASS (valent_contact_page_parent_class)->finalize (object);
398 : 1 : }
399 : :
400 : : static void
401 : 3 : valent_contact_page_get_property (GObject *object,
402 : : guint prop_id,
403 : : GValue *value,
404 : : GParamSpec *pspec)
405 : : {
406 : 3 : ValentContactPage *self = VALENT_CONTACT_PAGE (object);
407 : :
408 [ + - ]: 3 : switch ((ValentContactPageProperty)prop_id)
409 : : {
410 : 3 : case PROP_CONTACTS:
411 : 3 : g_value_set_object (value, self->contacts);
412 : 3 : break;
413 : :
414 : 0 : default:
415 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
416 : : }
417 : 3 : }
418 : :
419 : : static void
420 : 1 : valent_contact_page_set_property (GObject *object,
421 : : guint prop_id,
422 : : const GValue *value,
423 : : GParamSpec *pspec)
424 : : {
425 : 1 : ValentContactPage *self = VALENT_CONTACT_PAGE (object);
426 : :
427 [ + - ]: 1 : switch ((ValentContactPageProperty)prop_id)
428 : : {
429 : 1 : case PROP_CONTACTS:
430 : 1 : g_set_object (&self->contacts, g_value_get_object (value));
431 : 1 : break;
432 : :
433 : 0 : default:
434 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
435 : : }
436 : 1 : }
437 : :
438 : : static void
439 : 1 : valent_contact_page_class_init (ValentContactPageClass *klass)
440 : : {
441 : 1 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
442 : 1 : GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
443 : 1 : AdwNavigationPageClass *page_class = ADW_NAVIGATION_PAGE_CLASS (klass);
444 : :
445 : 1 : object_class->finalize = valent_contact_page_finalize;
446 : 1 : object_class->get_property = valent_contact_page_get_property;
447 : 1 : object_class->set_property = valent_contact_page_set_property;
448 : :
449 : 1 : gtk_widget_class_set_template_from_resource (widget_class, "/plugins/gnome/valent-contact-page.ui");
450 : 1 : gtk_widget_class_bind_template_child (widget_class, ValentContactPage, search_entry);
451 : 1 : gtk_widget_class_bind_template_child (widget_class, ValentContactPage, contact_list);
452 : 1 : gtk_widget_class_bind_template_child (widget_class, ValentContactPage, details_dialog);
453 : 1 : gtk_widget_class_bind_template_child (widget_class, ValentContactPage, medium_list);
454 : 1 : gtk_widget_class_bind_template_child (widget_class, ValentContactPage, model);
455 : 1 : gtk_widget_class_bind_template_child (widget_class, ValentContactPage, filter);
456 : 1 : gtk_widget_class_bind_template_child (widget_class, ValentContactPage, sorter);
457 : 1 : gtk_widget_class_bind_template_callback (widget_class, on_search_changed);
458 : 1 : gtk_widget_class_bind_template_callback (widget_class, on_contact_selected);
459 : :
460 : 1 : page_class->shown = valent_contact_page_shown;
461 : :
462 : : /**
463 : : * ValentContactPage:contacts:
464 : : *
465 : : * The `ValentContactsAdapter` providing contacts.
466 : : */
467 : 2 : properties [PROP_CONTACTS] =
468 : 1 : g_param_spec_object ("contacts", NULL, NULL,
469 : : VALENT_TYPE_CONTACTS_ADAPTER,
470 : : (G_PARAM_READWRITE |
471 : : G_PARAM_CONSTRUCT |
472 : : G_PARAM_STATIC_STRINGS));
473 : :
474 : 1 : g_object_class_install_properties (object_class, G_N_ELEMENTS (properties), properties);
475 : :
476 : : /**
477 : : * ValentContactPage::selected:
478 : : * @conversation: a `ValentContactPage`
479 : : * @contact: an `EContact`
480 : : * @medium: a contact medium
481 : : *
482 : : * The `ValentContactPage`::selected signal is emitted when a contact
483 : : * medium (e.g. phone number, email, etc) is selected.
484 : : */
485 : 2 : signals [SELECTED] =
486 : 1 : g_signal_new ("selected",
487 : : G_TYPE_FROM_CLASS (klass),
488 : : G_SIGNAL_RUN_LAST,
489 : : 0,
490 : : NULL, NULL, NULL,
491 : : G_TYPE_NONE, 2, E_TYPE_CONTACT, G_TYPE_STRING);
492 : 1 : }
493 : :
494 : : static void
495 : 1 : valent_contact_page_init (ValentContactPage *self)
496 : : {
497 : 1 : gtk_widget_init_template (GTK_WIDGET (self));
498 : :
499 : 1 : gtk_custom_filter_set_filter_func (GTK_CUSTOM_FILTER (self->filter),
500 : : valent_contact_page_filter,
501 : : self, NULL);
502 : 1 : gtk_list_box_bind_model (self->contact_list,
503 : : self->model,
504 : : contact_list_create,
505 : : self, NULL);
506 : 1 : }
507 : :
|