blob: e7160968ab77f95dbe3170af10d7106530140cfc [file] [log] [blame]
Stepan Salenikovich9816a942015-04-22 17:49:16 -04001/*
Stepan Salenikovichbe87d2c2016-01-25 14:14:34 -05002 * Copyright (C) 2015-2016 Savoir-faire Linux Inc.
Stepan Salenikovich9816a942015-04-22 17:49:16 -04003 * Author: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Stepan Salenikovich9816a942015-04-22 17:49:16 -040018 */
19
20#include "contactsview.h"
21
22#include <gtk/gtk.h>
Stepan Salenikovicha1b8cb32015-09-11 14:58:35 -040023#include <glib/gi18n.h>
Stepan Salenikovichf6078222016-10-03 17:31:16 -040024#include "models/gtkqtreemodel.h"
Stepan Salenikovich9816a942015-04-22 17:49:16 -040025#include <categorizedcontactmodel.h>
26#include <personmodel.h>
27#include "utils/calling.h"
28#include <memory>
Stepan Salenikovichbbd6c132015-08-20 15:21:48 -040029#include <globalinstances.h>
30#include "native/pixbufmanipulator.h"
Stepan Salenikovich9816a942015-04-22 17:49:16 -040031#include <contactmethod.h>
Stepan Salenikovich75a216e2015-04-23 14:08:53 -040032#include "defines.h"
33#include "utils/models.h"
Stepan Salenikovich9d294492015-05-14 16:34:24 -040034#include <QtCore/QItemSelectionModel>
Stepan Salenikovich5039c9b2016-02-12 14:09:51 -050035#include "numbercategory.h"
Stepan Salenikovich8eaa13e2016-08-26 16:51:48 -040036#include "contactpopupmenu.h"
Stepan Salenikovichdcfb5032016-10-26 18:57:56 -040037#include "models/namenumberfilterproxymodel.h"
Stepan Salenikovich9816a942015-04-22 17:49:16 -040038
39struct _ContactsView
40{
Stepan Salenikovichba1fc2d2015-10-29 16:38:10 -040041 GtkTreeView parent;
Stepan Salenikovich9816a942015-04-22 17:49:16 -040042};
43
44struct _ContactsViewClass
45{
Stepan Salenikovichba1fc2d2015-10-29 16:38:10 -040046 GtkTreeViewClass parent_class;
Stepan Salenikovich9816a942015-04-22 17:49:16 -040047};
48
49typedef struct _ContactsViewPrivate ContactsViewPrivate;
50
51struct _ContactsViewPrivate
52{
Stepan Salenikovich8eaa13e2016-08-26 16:51:48 -040053 GtkWidget *popup_menu;
54
Stepan Salenikovichdcfb5032016-10-26 18:57:56 -040055 NameNumberFilterProxy *filterproxy;
56 QMetaObject::Connection expand_inserted_row;
Stepan Salenikovich9816a942015-04-22 17:49:16 -040057};
58
Stepan Salenikovichba1fc2d2015-10-29 16:38:10 -040059G_DEFINE_TYPE_WITH_PRIVATE(ContactsView, contacts_view, GTK_TYPE_TREE_VIEW);
Stepan Salenikovich9816a942015-04-22 17:49:16 -040060
61#define CONTACTS_VIEW_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), CONTACTS_VIEW_TYPE, ContactsViewPrivate))
62
63static void
64render_contact_photo(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
65 GtkCellRenderer *cell,
66 GtkTreeModel *tree_model,
67 GtkTreeIter *iter,
68 G_GNUC_UNUSED gpointer data)
69{
70 /* check if this is a top level item (category),
Stepan Salenikovich81455562015-05-01 16:28:46 -040071 * or a bottom level item (contact method)
Stepan Salenikovich9816a942015-04-22 17:49:16 -040072 * in this case we don't want to show a photo */
73 GtkTreePath *path = gtk_tree_model_get_path(tree_model, iter);
74 int depth = gtk_tree_path_get_depth(path);
75 gtk_tree_path_free(path);
76 if (depth == 2) {
77 /* get person */
Stepan Salenikovichf6078222016-10-03 17:31:16 -040078 QModelIndex idx = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(tree_model), iter);
Stepan Salenikovich9816a942015-04-22 17:49:16 -040079 if (idx.isValid()) {
80 QVariant var_c = idx.data(static_cast<int>(Person::Role::Object));
81 Person *c = var_c.value<Person *>();
82 /* get photo */
Stepan Salenikovichbbd6c132015-08-20 15:21:48 -040083 QVariant var_p = GlobalInstances::pixmapManipulator().contactPhoto(c, QSize(50, 50), false);
Stepan Salenikovich9816a942015-04-22 17:49:16 -040084 std::shared_ptr<GdkPixbuf> photo = var_p.value<std::shared_ptr<GdkPixbuf>>();
85 g_object_set(G_OBJECT(cell), "pixbuf", photo.get(), NULL);
86 return;
87 }
88 }
89
90 /* otherwise, make sure its an empty pixbuf */
91 g_object_set(G_OBJECT(cell), "pixbuf", NULL, NULL);
92}
93
94static void
95render_name_and_contact_method(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
96 GtkCellRenderer *cell,
97 GtkTreeModel *tree_model,
98 GtkTreeIter *iter,
Stepan Salenikoviche4981b22015-10-22 15:22:59 -040099 GtkTreeView *treeview)
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400100{
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400101 // check if this iter is selected
102 gboolean is_selected = FALSE;
103 if (GTK_IS_TREE_VIEW(treeview)) {
104 auto selection = gtk_tree_view_get_selection(treeview);
105 is_selected = gtk_tree_selection_iter_is_selected(selection, iter);
106 }
107
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400108 /**
109 * If contact (person), show the name and the contact method (number)
110 * underneath; if multiple contact methods, then indicate as such
111 *
112 * Otherwise just display the category or contact method
113 */
114 GtkTreePath *path = gtk_tree_model_get_path(tree_model, iter);
115 int depth = gtk_tree_path_get_depth(path);
116 gtk_tree_path_free(path);
117
118 gchar *text = NULL;
119
Stepan Salenikovichf6078222016-10-03 17:31:16 -0400120 QModelIndex idx = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(tree_model), iter);
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400121 if (idx.isValid()) {
122 QVariant var = idx.data(Qt::DisplayRole);
123 if (depth == 1) {
124 /* category */
Stepan Salenikovich0731de02016-05-17 17:47:16 -0400125 text = g_markup_printf_escaped("<b>%s</b>", var.value<QString>().toUtf8().constData());
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400126 } else if (depth == 2) {
127 /* contact, check for contact methods */
128 QVariant var_c = idx.data(static_cast<int>(Person::Role::Object));
129 if (var_c.isValid()) {
130 Person *c = var_c.value<Person *>();
131 switch (c->phoneNumbers().size()) {
132 case 0:
133 text = g_strdup_printf("%s\n", c->formattedName().toUtf8().constData());
134 break;
135 case 1:
136 {
137 QString number;
138 QVariant var_n = c->phoneNumbers().first()->roleData(Qt::DisplayRole);
139 if (var_n.isValid())
140 number = var_n.value<QString>();
141
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400142 /* we want the color of the status text to be the default color if this iter is
143 * selected so that the treeview is able to invert it against the selection color */
144 if (is_selected) {
Stepan Salenikovich0731de02016-05-17 17:47:16 -0400145 text = g_markup_printf_escaped("%s\n %s",
146 c->formattedName().toUtf8().constData(),
147 number.toUtf8().constData());
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400148 } else {
Stepan Salenikovich0731de02016-05-17 17:47:16 -0400149 text = g_markup_printf_escaped("%s\n <span fgcolor=\"gray\">%s</span>",
150 c->formattedName().toUtf8().constData(),
151 number.toUtf8().constData());
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400152 }
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400153 break;
154 }
155 default:
156 /* more than one, for now don't show any of the contact methods */
Stepan Salenikovich0731de02016-05-17 17:47:16 -0400157 text = g_markup_printf_escaped("%s\n", c->formattedName().toUtf8().constData());
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400158 break;
159 }
160 } else {
161 /* should never happen since depth 2 should always be a contact (person) */
Stepan Salenikovich0731de02016-05-17 17:47:16 -0400162 text = g_markup_printf_escaped("%s", var.value<QString>().toUtf8().constData());
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400163 }
164 } else {
Stepan Salenikovich5039c9b2016-02-12 14:09:51 -0500165 auto var_object = idx.data(static_cast<int>(Ring::Role::Object));
166 auto cm = var_object.value<ContactMethod *>();
167 if (cm && cm->category()) {
168 // try to get the number category, eg: "home"
Stepan Salenikovich0731de02016-05-17 17:47:16 -0400169 text = g_markup_printf_escaped("(%s) %s", cm->category()->name().toUtf8().constData(),
170 cm->uri().toUtf8().constData());
Stepan Salenikovich5039c9b2016-02-12 14:09:51 -0500171 } else if (cm) {
Stepan Salenikovich0731de02016-05-17 17:47:16 -0400172 text = g_markup_printf_escaped("%s", cm->uri().toUtf8().constData());
Stepan Salenikovich5039c9b2016-02-12 14:09:51 -0500173 } else {
174 /* should only ever be a CM, so this should never execute */
Stepan Salenikovich0731de02016-05-17 17:47:16 -0400175 text = g_markup_printf_escaped("%s", var.value<QString>().toUtf8().constData());
Stepan Salenikovich5039c9b2016-02-12 14:09:51 -0500176 }
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400177 }
178 }
179
180 g_object_set(G_OBJECT(cell), "markup", text, NULL);
181 g_free(text);
182
183 /* set the colour */
184 if ( depth == 1) {
185 /* nice blue taken from the ring logo */
186 GdkRGBA rgba = {0.2, 0.75294117647, 0.82745098039, 0.1};
187 g_object_set(G_OBJECT(cell), "cell-background-rgba", &rgba, NULL);
188 } else {
189 g_object_set(G_OBJECT(cell), "cell-background", NULL, NULL);
190 }
191}
192
193static void
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400194activate_contact_item(GtkTreeView *tree_view,
195 GtkTreePath *path,
196 G_GNUC_UNUSED GtkTreeViewColumn *column,
197 G_GNUC_UNUSED gpointer user_data)
198{
199 /* expand / contract row */
200 if (gtk_tree_view_row_expanded(tree_view, path))
201 gtk_tree_view_collapse_row(tree_view, path);
202 else
203 gtk_tree_view_expand_row(tree_view, path, FALSE);
204
205 GtkTreeModel *model = gtk_tree_view_get_model(tree_view);
206
207 /* get iter */
208 GtkTreeIter iter;
209 if (gtk_tree_model_get_iter(model, &iter, path)) {
Stepan Salenikovichf6078222016-10-03 17:31:16 -0400210 QModelIndex idx = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(model), &iter);
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400211 if (idx.isValid()) {
212 int depth = gtk_tree_path_get_depth(path);
213 switch (depth) {
214 case 0:
215 case 1:
216 /* category, nothing to do */
217 break;
218 case 2:
219 {
220 /* contact (person), use contact method if there is only one */
221 QVariant var_c = idx.data(static_cast<int>(Person::Role::Object));
222 if (var_c.isValid()) {
223 Person *c = var_c.value<Person *>();
224 if (c->phoneNumbers().size() == 1) {
225 /* call with contact method */
226 place_new_call(c->phoneNumbers().first());
227 }
228 }
229 break;
230 }
231 default:
232 {
233 /* contact method (or deeper) */
234 QVariant var_n = idx.data(static_cast<int>(ContactMethod::Role::Object));
235 if (var_n.isValid()) {
236 /* call with contat method */
237 place_new_call(var_n.value<ContactMethod *>());
238 }
239 break;
240 }
241 }
242 }
243 }
244}
245
246static void
247contacts_view_init(ContactsView *self)
248{
249 ContactsViewPrivate *priv = CONTACTS_VIEW_GET_PRIVATE(self);
Stepan Salenikovich26457ce2015-05-11 14:37:53 -0400250
Stepan Salenikovichba1fc2d2015-10-29 16:38:10 -0400251 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(self), FALSE);
Stepan Salenikovich26457ce2015-05-11 14:37:53 -0400252
Stepan Salenikovichba1fc2d2015-10-29 16:38:10 -0400253 /* disable default search, we will handle it ourselves;
Stepan Salenikovichb01d7362015-04-27 23:02:00 -0400254 * otherwise the search steals input focus on key presses */
Stepan Salenikovichba1fc2d2015-10-29 16:38:10 -0400255 gtk_tree_view_set_enable_search(GTK_TREE_VIEW(self), FALSE);
Stepan Salenikovichb01d7362015-04-27 23:02:00 -0400256
Stepan Salenikovich9d294492015-05-14 16:34:24 -0400257 /* initial set up to be categorized by name and sorted alphabetically */
Stepan Salenikovichdcfb5032016-10-26 18:57:56 -0400258 auto q_sorted_proxy = &CategorizedContactModel::SortedProxy::instance();
Guillaume Roguez5d1514b2015-10-22 15:55:31 -0400259 CategorizedContactModel::instance().setUnreachableHidden(true);
Stepan Salenikovich9d294492015-05-14 16:34:24 -0400260
261 /* for now we always want to sort by ascending order */
Stepan Salenikovichdcfb5032016-10-26 18:57:56 -0400262 q_sorted_proxy->model()->sort(0);
Stepan Salenikovich9d294492015-05-14 16:34:24 -0400263
264 /* select default category (the first one, which is by name) */
Stepan Salenikovichdcfb5032016-10-26 18:57:56 -0400265 q_sorted_proxy->categorySelectionModel()->setCurrentIndex(
266 q_sorted_proxy->categoryModel()->index(0, 0),
Stepan Salenikovich9d294492015-05-14 16:34:24 -0400267 QItemSelectionModel::ClearAndSelect);
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400268
Stepan Salenikovichdcfb5032016-10-26 18:57:56 -0400269 // filtering
270 priv->filterproxy = new NameNumberFilterProxy(q_sorted_proxy->model());
271
Stepan Salenikovichf6078222016-10-03 17:31:16 -0400272 GtkQTreeModel *contact_model = gtk_q_tree_model_new(
Stepan Salenikovichdcfb5032016-10-26 18:57:56 -0400273 priv->filterproxy,
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400274 1,
aviau271bcc22016-05-27 17:25:19 -0400275 0, Qt::DisplayRole, G_TYPE_STRING);
Stepan Salenikovichba1fc2d2015-10-29 16:38:10 -0400276 gtk_tree_view_set_model(GTK_TREE_VIEW(self), GTK_TREE_MODEL(contact_model));
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400277
278 /* photo and name/contact method column */
Stepan Salenikovich7c71bfe2015-05-13 18:08:09 -0400279 GtkCellArea *area = gtk_cell_area_box_new();
280 GtkTreeViewColumn *column = gtk_tree_view_column_new_with_area(area);
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400281
282 /* photo renderer */
Stepan Salenikovich7c71bfe2015-05-13 18:08:09 -0400283 GtkCellRenderer *renderer = gtk_cell_renderer_pixbuf_new();
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400284 gtk_cell_area_box_pack_start(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
285
286 /* get the photo */
287 gtk_tree_view_column_set_cell_data_func(
288 column,
289 renderer,
290 (GtkTreeCellDataFunc)render_contact_photo,
291 NULL,
292 NULL);
293
294 /* name and contact method renderer */
295 renderer = gtk_cell_renderer_text_new();
Stepan Salenikovich81455562015-05-01 16:28:46 -0400296 g_object_set(G_OBJECT(renderer), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400297 gtk_cell_area_box_pack_start(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
298
299 gtk_tree_view_column_set_cell_data_func(
300 column,
301 renderer,
302 (GtkTreeCellDataFunc)render_name_and_contact_method,
Stepan Salenikovichba1fc2d2015-10-29 16:38:10 -0400303 self,
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400304 NULL);
305
Stepan Salenikovichba1fc2d2015-10-29 16:38:10 -0400306 gtk_tree_view_append_column(GTK_TREE_VIEW(self), column);
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400307 gtk_tree_view_column_set_resizable(column, TRUE);
308
Stepan Salenikovichba1fc2d2015-10-29 16:38:10 -0400309 gtk_tree_view_expand_all(GTK_TREE_VIEW(self));
Stepan Salenikovichba1fc2d2015-10-29 16:38:10 -0400310 g_signal_connect(self, "row-activated", G_CALLBACK(activate_contact_item), NULL);
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400311
Stepan Salenikovichdcfb5032016-10-26 18:57:56 -0400312 priv->expand_inserted_row = QObject::connect(
313 priv->filterproxy,
314 &QAbstractItemModel::rowsInserted,
315 [self, priv, contact_model](const QModelIndex & parent, int first, int last) {
316 for( int row = first; row <= last; row++) {
317 auto idx = priv->filterproxy->index(row, 0, parent);
318
319 // we want to expand all Categories, but not the contacts
320 if (!parent.isValid()) {
321 // category, exand any children (contacts)
322 auto children = priv->filterproxy->rowCount(idx);
323 for (int child_row = 0; child_row < children; ++child_row) {
324 auto child_idx = priv->filterproxy->index(child_row, 0, idx);
325 GtkTreeIter iter;
326 if (gtk_q_tree_model_source_index_to_iter(contact_model, child_idx, &iter)) {
327 GtkTreePath *path = gtk_tree_model_get_path(GTK_TREE_MODEL(contact_model), &iter);
328 gtk_tree_view_expand_to_path(GTK_TREE_VIEW(self), path);
329 gtk_tree_path_free(path);
330 }
331 }
332 } else {
333 // contact or ContactMethod; we only want to expand to the contact
334 GtkTreeIter iter;
335 if (gtk_q_tree_model_source_index_to_iter(contact_model, idx, &iter)) {
336 GtkTreePath *path = gtk_tree_model_get_path(GTK_TREE_MODEL(contact_model), &iter);
337
338 if (gtk_tree_path_get_depth(path) == 2) {
339 gtk_tree_view_expand_to_path(GTK_TREE_VIEW(self), path);
340 }
341 gtk_tree_path_free(path);
342 }
343 }
344 }
345 }
346 );
347
Stepan Salenikovich8eaa13e2016-08-26 16:51:48 -0400348 /* init popup menu */
349 priv->popup_menu = contact_popup_menu_new(GTK_TREE_VIEW(self));
350 g_signal_connect_swapped(self, "button-press-event", G_CALLBACK(contact_popup_menu_show), priv->popup_menu);
351
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400352 gtk_widget_show_all(GTK_WIDGET(self));
353}
354
355static void
356contacts_view_dispose(GObject *object)
357{
Stepan Salenikovich8eaa13e2016-08-26 16:51:48 -0400358 ContactsViewPrivate *priv = CONTACTS_VIEW_GET_PRIVATE(object);
359
Stepan Salenikovichdcfb5032016-10-26 18:57:56 -0400360 QObject::disconnect(priv->expand_inserted_row);
Stepan Salenikovich8eaa13e2016-08-26 16:51:48 -0400361 gtk_widget_destroy(priv->popup_menu);
362
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400363 G_OBJECT_CLASS(contacts_view_parent_class)->dispose(object);
364}
365
366static void
367contacts_view_finalize(GObject *object)
368{
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400369 G_OBJECT_CLASS(contacts_view_parent_class)->finalize(object);
370}
371
372static void
373contacts_view_class_init(ContactsViewClass *klass)
374{
375 G_OBJECT_CLASS(klass)->finalize = contacts_view_finalize;
376 G_OBJECT_CLASS(klass)->dispose = contacts_view_dispose;
377}
378
379GtkWidget *
380contacts_view_new()
381{
382 gpointer self = g_object_new(CONTACTS_VIEW_TYPE, NULL);
383
384 return (GtkWidget *)self;
Stepan Salenikovich9d294492015-05-14 16:34:24 -0400385}
Stepan Salenikovichdcfb5032016-10-26 18:57:56 -0400386
387void
388contacts_view_set_filter_string(ContactsView *self, const char* text)
389{
390 ContactsViewPrivate *priv = CONTACTS_VIEW_GET_PRIVATE(self);
391 priv->filterproxy->setFilterRegExp(QRegExp(text, Qt::CaseInsensitive, QRegExp::FixedString));
392}