blob: 69f327ee3e642bde53bfb0b8c282a92133a8d8d0 [file] [log] [blame]
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -04001/*
Stepan Salenikovichbe87d2c2016-01-25 14:14:34 -05002 * Copyright (C) 2015-2016 Savoir-faire Linux Inc.
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -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.
18 */
19
20#include "recentcontactsview.h"
21
22#include <gtk/gtk.h>
23#include <glib/gi18n.h>
24#include "models/gtkqsortfiltertreemodel.h"
25#include "utils/calling.h"
26#include <memory>
27#include <globalinstances.h>
28#include "native/pixbufmanipulator.h"
29#include <contactmethod.h>
30#include "defines.h"
31#include "utils/models.h"
32#include <recentmodel.h>
33#include <call.h>
34#include "utils/menus.h"
35#include <itemdataroles.h>
36#include <callmodel.h>
37#include <QtCore/QItemSelectionModel>
38#include <historytimecategorymodel.h>
39#include <QtCore/QDateTime>
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -050040#include <QtCore/QMimeData>
Stepan Salenikovichd8765072016-01-14 10:58:51 -050041#include "utils/drawing.h"
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040042
43static constexpr const char* COPY_DATA_KEY = "copy_data";
44
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -050045static constexpr const char* CALL_TARGET = "CALL_TARGET";
46static constexpr int CALL_TARGET_ID = 0;
47
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040048struct _RecentContactsView
49{
50 GtkTreeView parent;
51};
52
53struct _RecentContactsViewClass
54{
55 GtkTreeViewClass parent_class;
56};
57
58typedef struct _RecentContactsViewPrivate RecentContactsViewPrivate;
59
60struct _RecentContactsViewPrivate
61{
62 GtkWidget *overlay_button;
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -040063
64 QMetaObject::Connection selection_updated;
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040065};
66
67G_DEFINE_TYPE_WITH_PRIVATE(RecentContactsView, recent_contacts_view, GTK_TYPE_TREE_VIEW);
68
69#define RECENT_CONTACTS_VIEW_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), RECENT_CONTACTS_VIEW_TYPE, RecentContactsViewPrivate))
70
71static void
Stepan Salenikovichc1323422016-01-06 10:54:44 -050072update_selection(GtkTreeSelection *selection, G_GNUC_UNUSED gpointer user_data)
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040073{
74 auto current_proxy = get_index_from_selection(selection);
Guillaume Roguez5d1514b2015-10-22 15:55:31 -040075 auto current = RecentModel::instance().peopleProxy()->mapToSource(current_proxy);
Stepan Salenikovichc1323422016-01-06 10:54:44 -050076
77 RecentModel::instance().selectionModel()->setCurrentIndex(current, QItemSelectionModel::ClearAndSelect);
78
79 // update the CallModel selection since we rely on the UserActionModel
Guillaume Roguez5d1514b2015-10-22 15:55:31 -040080 if (auto call_to_select = RecentModel::instance().getActiveCall(current)) {
81 CallModel::instance().selectCall(call_to_select);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040082 } else {
Guillaume Roguez5d1514b2015-10-22 15:55:31 -040083 CallModel::instance().selectionModel()->clearCurrentIndex();
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040084 }
85}
86
87static void
88copy_contact_info(GtkWidget *item, G_GNUC_UNUSED gpointer user_data)
89{
90 gpointer data = g_object_get_data(G_OBJECT(item), COPY_DATA_KEY);
91 g_return_if_fail(data);
92 gchar* text = (gchar *)data;
93 GtkClipboard* clip = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
94 gtk_clipboard_set_text(clip, text, -1);
95}
96
97static void
98call_contactmethod(G_GNUC_UNUSED GtkWidget *item, ContactMethod *cm)
99{
100 g_return_if_fail(cm);
101 place_new_call(cm);
102}
103
104static void
105render_contact_photo(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
106 GtkCellRenderer *cell,
107 GtkTreeModel *model,
108 GtkTreeIter *iter,
109 G_GNUC_UNUSED gpointer data)
110{
111 QModelIndex idx = gtk_q_sort_filter_tree_model_get_source_idx(GTK_Q_SORT_FILTER_TREE_MODEL(model), iter);
112
Stepan Salenikovichd8765072016-01-14 10:58:51 -0500113 std::shared_ptr<GdkPixbuf> image;
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400114 /* we only want to render a photo for the top nodes: Person, ContactMethod (, later Conference) */
115 QVariant object = idx.data(static_cast<int>(Ring::Role::Object));
116 if (idx.isValid() && object.isValid()) {
117 QVariant var_photo;
118 if (auto person = object.value<Person *>()) {
119 var_photo = GlobalInstances::pixmapManipulator().contactPhoto(person, QSize(50, 50), false);
120 } else if (auto cm = object.value<ContactMethod *>()) {
121 /* get photo, note that this should in all cases be the fallback avatar, since there
122 * shouldn't be a person associated with this contact method */
123 var_photo = GlobalInstances::pixmapManipulator().callPhoto(cm, QSize(50, 50), false);
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500124 } else if (auto call = object.value<Call *>()) {
125 if (call->type() == Call::Type::CONFERENCE) {
126 var_photo = GlobalInstances::pixmapManipulator().callPhoto(call, QSize(50, 50), false);
127 }
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400128 }
Stepan Salenikovichd8765072016-01-14 10:58:51 -0500129 if (var_photo.isValid()) {
130 std::shared_ptr<GdkPixbuf> photo = var_photo.value<std::shared_ptr<GdkPixbuf>>();
131
132 auto unread = idx.data(static_cast<int>(Ring::Role::UnreadTextMessageCount));
133
134 image.reset(ring_draw_unread_messages(photo.get(), unread.toInt()), g_object_unref);
135 } else {
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400136 // set the width of the cell rendered to the with of the photo
137 // so that the other renderers are shifted to the right
138 g_object_set(G_OBJECT(cell), "width", 50, NULL);
139 }
140 }
141
Stepan Salenikovichd8765072016-01-14 10:58:51 -0500142 g_object_set(G_OBJECT(cell), "pixbuf", image.get(), NULL);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400143}
144
145static void
146render_name_and_info(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
147 GtkCellRenderer *cell,
148 GtkTreeModel *model,
149 GtkTreeIter *iter,
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400150 GtkTreeView *treeview)
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400151{
152 gchar *text = NULL;
153
154 QModelIndex idx = gtk_q_sort_filter_tree_model_get_source_idx(GTK_Q_SORT_FILTER_TREE_MODEL(model), iter);
155
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400156 // check if this iter is selected
157 gboolean is_selected = FALSE;
158 if (GTK_IS_TREE_VIEW(treeview)) {
159 auto selection = gtk_tree_view_get_selection(treeview);
160 is_selected = gtk_tree_selection_iter_is_selected(selection, iter);
161 }
162
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400163 auto type = idx.data(static_cast<int>(Ring::Role::ObjectType));
164 if (idx.isValid() && type.isValid()) {
165 switch (type.value<Ring::ObjectType>()) {
166 case Ring::ObjectType::Person:
167 case Ring::ObjectType::ContactMethod:
168 {
169 auto var_name = idx.data(static_cast<int>(Ring::Role::Name));
170 auto var_lastused = idx.data(static_cast<int>(Ring::Role::LastUsed));
171 auto var_status = idx.data(static_cast<int>(Ring::Role::FormattedState));
172
173 QString name, status;
174
175 if (var_name.isValid())
176 name = var_name.value<QString>();
177
178 // show the status if there is a call, otherwise the last used date/time
179 if (var_status.isValid()) {
180 status += var_status.value<QString>();
181 }else if (var_lastused.isValid()) {
182 auto date_time = var_lastused.value<QDateTime>();
183 auto category = HistoryTimeCategoryModel::timeToHistoryConst(date_time.toTime_t());
184
185 // if it is 'today', then we only want to show the time
186 if (category != HistoryTimeCategoryModel::HistoryConst::Today) {
187 status += HistoryTimeCategoryModel::timeToHistoryCategory(date_time.toTime_t());
188 }
189 // we only want to show the time if it is less than a week ago
190 if (category < HistoryTimeCategoryModel::HistoryConst::A_week_ago) {
191 if (!status.isEmpty())
192 status += ", ";
193 status += QLocale::system().toString(date_time.time(), QLocale::ShortFormat);
194 }
195 }
196
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400197 /* we want the color of the status text to be the default color if this iter is
198 * selected so that the treeview is able to invert it against the selection color */
199 if (is_selected) {
200 text = g_strdup_printf("%s\n<span size=\"smaller\">%s</span>",
201 name.toUtf8().constData(),
202 status.toUtf8().constData());
203 } else {
204 text = g_strdup_printf("%s\n<span size=\"smaller\" color=\"gray\">%s</span>",
205 name.toUtf8().constData(),
206 status.toUtf8().constData());
207 }
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400208 }
209 break;
210 case Ring::ObjectType::Call:
211 {
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500212 // check if it is a conference
213 auto idx_source = RecentModel::instance().peopleProxy()->mapToSource(idx);
214 auto is_conference = RecentModel::instance().isConference(idx_source);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400215
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500216 if (is_conference) {
217 auto var_name = idx.data(static_cast<int>(Ring::Role::Name));
218 text = g_markup_printf_escaped("%s", var_name.value<QString>().toUtf8().constData());
219 } else {
220 auto parent_source = RecentModel::instance().peopleProxy()->mapToSource(idx.parent());
221 if (RecentModel::instance().isConference(parent_source)) {
222 // part of conference, simply display the name
223 auto var_name = idx.data(static_cast<int>(Ring::Role::Name));
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400224
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500225 /* we want the color of the name text to be the default color if this iter is
226 * selected so that the treeview is able to invert it against the selection color */
227 if (is_selected)
228 text = g_strdup_printf("<span size=\"smaller\">%s</span>", var_name.value<QString>().toUtf8().constData());
229 else
230 text = g_strdup_printf("<span size=\"smaller\" color=\"gray\">%s</span>", var_name.value<QString>().toUtf8().constData());
231 } else {
232 // just a call, so display the state
233 auto var_status = idx.data(static_cast<int>(Ring::Role::FormattedState));
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400234
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500235 QString status;
236
237 if (var_status.isValid())
238 status += var_status.value<QString>();
239
240 /* we want the color of the status text to be the default color if this iter is
241 * selected so that the treeview is able to invert it against the selection color */
242 if (is_selected)
243 text = g_strdup_printf("<span size=\"smaller\">%s</span>", status.toUtf8().constData());
244 else
245 text = g_strdup_printf("<span size=\"smaller\" color=\"gray\">%s</span>", status.toUtf8().constData());
246 }
247 }
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400248 }
249 break;
250 case Ring::ObjectType::Media:
251 // nothing to do for now
Stepan Salenikovich4e7ea712015-12-24 14:04:37 -0500252 case Ring::ObjectType::COUNT__:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400253 break;
254 }
255 }
256
257 g_object_set(G_OBJECT(cell), "markup", text, NULL);
258 g_free(text);
259}
260
261static void
262render_call_duration(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
263 GtkCellRenderer *cell,
264 GtkTreeModel *model,
265 GtkTreeIter *iter,
266 G_GNUC_UNUSED gpointer data)
267{
268 gchar *text = NULL;
269
270 QModelIndex idx = gtk_q_sort_filter_tree_model_get_source_idx(GTK_Q_SORT_FILTER_TREE_MODEL(model), iter);
271
272 auto type = idx.data(static_cast<int>(Ring::Role::ObjectType));
273 if (idx.isValid() && type.isValid()) {
274 switch (type.value<Ring::ObjectType>()) {
275 case Ring::ObjectType::Person:
276 case Ring::ObjectType::ContactMethod:
277 {
278 // check if there are any children (calls); we need to convert to source model in
279 // case there is only one
Guillaume Roguez5d1514b2015-10-22 15:55:31 -0400280 auto idx_source = RecentModel::instance().peopleProxy()->mapToSource(idx);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400281 auto duration = idx.data(static_cast<int>(Ring::Role::Length));
282 if (idx_source.isValid()
283 && (idx_source.model()->rowCount(idx_source) == 1)
284 && duration.isValid())
285 {
286 text = g_strdup_printf("%s", duration.value<QString>().toUtf8().constData());
287 }
288 }
289 break;
290 case Ring::ObjectType::Call:
291 {
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500292 // do not display the duration if the call is part of a conference
293 auto parent_source = RecentModel::instance().peopleProxy()->mapToSource(idx.parent());
294 auto in_conference = RecentModel::instance().isConference(parent_source);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400295
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500296 if (!in_conference) {
297 auto duration = idx.data(static_cast<int>(Ring::Role::Length));
298
299 if (duration.isValid())
300 text = g_strdup_printf("%s", duration.value<QString>().toUtf8().constData());
301 }
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400302 }
303 break;
304 case Ring::ObjectType::Media:
305 // nothing to do for now
Stepan Salenikovich4e7ea712015-12-24 14:04:37 -0500306 case Ring::ObjectType::COUNT__:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400307 break;
308 }
309 }
310
311 g_object_set(G_OBJECT(cell), "markup", text, NULL);
312 g_free(text);
313}
314
315static void
316activate_item(GtkTreeView *tree_view,
317 GtkTreePath *path,
318 G_GNUC_UNUSED GtkTreeViewColumn *column,
319 G_GNUC_UNUSED gpointer user_data)
320{
321 auto model = gtk_tree_view_get_model(tree_view);
322 GtkTreeIter iter;
323 if (gtk_tree_model_get_iter(model, &iter, path)) {
324 auto idx = gtk_q_sort_filter_tree_model_get_source_idx(GTK_Q_SORT_FILTER_TREE_MODEL(model), &iter);
325 auto type = idx.data(static_cast<int>(Ring::Role::ObjectType));
326 if (idx.isValid() && type.isValid()) {
327 switch (type.value<Ring::ObjectType>()) {
328 case Ring::ObjectType::Person:
329 {
330 // call the last used contact method
331 // TODO: if no contact methods have been used, offer a popup to Choose
332 auto p_var = idx.data(static_cast<int>(Ring::Role::Object));
333 if (p_var.isValid()) {
334 auto person = p_var.value<Person *>();
335 auto cms = person->phoneNumbers();
336
337 if (!cms.isEmpty()) {
338 auto last_used_cm = cms.at(0);
339 for (int i = 1; i < cms.size(); ++i) {
340 auto new_cm = cms.at(i);
341 if (difftime(new_cm->lastUsed(), last_used_cm->lastUsed()) > 0)
342 last_used_cm = new_cm;
343 }
344
345 place_new_call(last_used_cm);
346 }
347 }
348 }
349 break;
350 case Ring::ObjectType::ContactMethod:
351 {
352 // call the contact method
353 auto cm = idx.data(static_cast<int>(Ring::Role::Object));
354 if (cm.isValid())
355 place_new_call(cm.value<ContactMethod *>());
356 }
357 break;
358 case Ring::ObjectType::Call:
359 case Ring::ObjectType::Media:
360 // nothing to do for now
Stepan Salenikovich4e7ea712015-12-24 14:04:37 -0500361 case Ring::ObjectType::COUNT__:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400362 break;
363 }
364 }
365 }
366}
367
368static gboolean
369create_popup_menu(GtkTreeView *treeview, GdkEventButton *event, G_GNUC_UNUSED gpointer user_data)
370{
371 /* build popup menu when right clicking on contact item
372 * user should be able to copy the contact's name or "number".
373 * or add the "number" to his contact list, if not already so
374 */
375
376 /* check for right click */
377 if (event->button != BUTTON_RIGHT_CLICK || event->type != GDK_BUTTON_PRESS)
378 return FALSE;
379
380 GtkTreeIter iter;
381 GtkTreeModel *model;
382 GtkTreeSelection *selection = gtk_tree_view_get_selection(treeview);
383 if (!gtk_tree_selection_get_selected(selection, &model, &iter))
384 return FALSE;
385
386 GtkWidget *menu = gtk_menu_new();
387 QModelIndex idx = gtk_q_sort_filter_tree_model_get_source_idx(GTK_Q_SORT_FILTER_TREE_MODEL(model), &iter);
388
389 /* if Person or CM, give option to call */
390 auto type = idx.data(static_cast<int>(Ring::Role::ObjectType));
391 auto object = idx.data(static_cast<int>(Ring::Role::Object));
392 if (type.isValid() && object.isValid()) {
393 switch (type.value<Ring::ObjectType>()) {
394 case Ring::ObjectType::Person:
395 {
396 /* possiblity for multiple numbers */
397 auto cms = object.value<Person *>()->phoneNumbers();
398 if (cms.size() == 1) {
399 auto item = gtk_menu_item_new_with_mnemonic(_("_Call"));
400 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
401 g_signal_connect(item,
402 "activate",
403 G_CALLBACK(call_contactmethod),
404 cms.at(0));
405 } else if (cms.size() > 1) {
406 auto call_item = gtk_menu_item_new_with_mnemonic(_("_Call"));
407 gtk_menu_shell_append(GTK_MENU_SHELL(menu), call_item);
408 auto call_menu = gtk_menu_new();
409 gtk_menu_item_set_submenu(GTK_MENU_ITEM(call_item), call_menu);
410 for (int i = 0; i < cms.size(); ++i) {
411 auto item = gtk_menu_item_new_with_label(cms.at(i)->uri().toUtf8().constData());
412 gtk_menu_shell_append(GTK_MENU_SHELL(call_menu), item);
413 g_signal_connect(item,
414 "activate",
415 G_CALLBACK(call_contactmethod),
416 cms.at(i));
417 }
418 }
419 }
420 break;
421 case Ring::ObjectType::ContactMethod:
422 {
423 auto cm = object.value<ContactMethod *>();
424 auto item = gtk_menu_item_new_with_mnemonic(_("_Call"));
425 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
426 g_signal_connect(item,
427 "activate",
428 G_CALLBACK(call_contactmethod),
429 cm);
430 }
431 break;
432 case Ring::ObjectType::Call:
433 case Ring::ObjectType::Media:
434 // nothing to do for now
Stepan Salenikovich4e7ea712015-12-24 14:04:37 -0500435 case Ring::ObjectType::COUNT__:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400436 break;
437 }
438 }
439
440 /* copy name */
441 QVariant name_var = idx.data(static_cast<int>(Ring::Role::Name));
442 if (name_var.isValid()) {
443 gchar *name = g_strdup_printf("%s", name_var.value<QString>().toUtf8().constData());
444 GtkWidget *item = gtk_menu_item_new_with_mnemonic(_("_Copy name"));
445 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
446 g_object_set_data_full(G_OBJECT(item), COPY_DATA_KEY, name, (GDestroyNotify)g_free);
447 g_signal_connect(item,
448 "activate",
449 G_CALLBACK(copy_contact_info),
450 NULL);
451 }
452
453 /* copy number(s) */
454 if (type.isValid() && object.isValid()) {
455 switch (type.value<Ring::ObjectType>()) {
456 case Ring::ObjectType::Person:
457 {
458 /* possiblity for multiple numbers */
459 auto cms = object.value<Person *>()->phoneNumbers();
460 if (cms.size() == 1) {
461 gchar *number = g_strdup_printf("%s",cms.at(0)->uri().toUtf8().constData());
462 auto item = gtk_menu_item_new_with_mnemonic(_("_Copy number"));
463 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
464 g_object_set_data_full(G_OBJECT(item), COPY_DATA_KEY, number, (GDestroyNotify)g_free);
465 g_signal_connect(item,
466 "activate",
467 G_CALLBACK(copy_contact_info),
468 NULL);
469 } else if (cms.size() > 1) {
470 auto copy_item = gtk_menu_item_new_with_mnemonic(_("_Copy number"));
471 gtk_menu_shell_append(GTK_MENU_SHELL(menu), copy_item);
472 auto copy_menu = gtk_menu_new();
473 gtk_menu_item_set_submenu(GTK_MENU_ITEM(copy_item), copy_menu);
474 for (int i = 0; i < cms.size(); ++i) {
475 gchar *number = g_strdup_printf("%s",cms.at(i)->uri().toUtf8().constData());
476 auto item = gtk_menu_item_new_with_label(cms.at(i)->uri().toUtf8().constData());
477 gtk_menu_shell_append(GTK_MENU_SHELL(copy_menu), item);
478 g_object_set_data_full(G_OBJECT(item), COPY_DATA_KEY, number, (GDestroyNotify)g_free);
479 g_signal_connect(item,
480 "activate",
481 G_CALLBACK(copy_contact_info),
482 NULL);
483 }
484 }
485 }
486 break;
487 case Ring::ObjectType::ContactMethod:
488 case Ring::ObjectType::Call:
489 case Ring::ObjectType::Media:
Stepan Salenikovich4e7ea712015-12-24 14:04:37 -0500490 {
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400491 QVariant number_var = idx.data(static_cast<int>(Ring::Role::Number));
492 if (number_var.isValid()) {
493 gchar *number = g_strdup_printf("%s", number_var.value<QString>().toUtf8().constData());
494 GtkWidget *item = gtk_menu_item_new_with_mnemonic(_("_Copy number"));
495 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
496 g_object_set_data_full(G_OBJECT(item), COPY_DATA_KEY, number, (GDestroyNotify)g_free);
497 g_signal_connect(item,
498 "activate",
499 G_CALLBACK(copy_contact_info),
500 NULL);
501 }
Stepan Salenikovich4e7ea712015-12-24 14:04:37 -0500502 }
503 break;
504 case Ring::ObjectType::COUNT__:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400505 break;
506 }
507 }
508
509 /* if the object is a CM, then give the option to add to contacts*/
510 if (type.isValid() && object.isValid()) {
511 if (type.value<Ring::ObjectType>() == Ring::ObjectType::ContactMethod) {
512 /* get rectangle */
513 auto path = gtk_tree_model_get_path(model, &iter);
514 auto column = gtk_tree_view_get_column(treeview, 0);
515 GdkRectangle rect;
516 gtk_tree_view_get_cell_area(treeview, path, column, &rect);
517 gtk_tree_view_convert_bin_window_to_widget_coords(treeview, rect.x, rect.y, &rect.x, &rect.y);
518 gtk_tree_path_free(path);
519 auto add_to = menu_item_add_to_contact(object.value<ContactMethod *>(), GTK_WIDGET(treeview), &rect);
520 gtk_menu_shell_append(GTK_MENU_SHELL(menu), add_to);
521 }
522 }
523
524 /* show menu */
525 gtk_widget_show_all(menu);
526 gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, event->button, event->time);
527
528 return TRUE; /* we handled the event */
529}
530
531static void
532expand_if_child(G_GNUC_UNUSED GtkTreeModel *tree_model,
533 GtkTreePath *path,
534 G_GNUC_UNUSED GtkTreeIter *iter,
535 GtkTreeView *treeview)
536{
537 if (gtk_tree_path_get_depth(path) > 1)
538 gtk_tree_view_expand_to_path(treeview, path);
539}
540
541static void
Stepan Salenikovichec0862f2015-10-16 15:49:42 -0400542scroll_to_selection(GtkTreeSelection *selection)
543{
544 GtkTreeModel *model = nullptr;
545 GtkTreeIter iter;
546 if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
547 auto path = gtk_tree_model_get_path(model, &iter);
548 auto treeview = gtk_tree_selection_get_tree_view(selection);
549 gtk_tree_view_scroll_to_cell(treeview, path, nullptr, FALSE, 0.0, 0.0);
550 }
551}
552
553static void
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500554on_drag_data_get(GtkWidget *treeview,
555 G_GNUC_UNUSED GdkDragContext *context,
556 GtkSelectionData *data,
557 G_GNUC_UNUSED guint info,
558 G_GNUC_UNUSED guint time,
559 G_GNUC_UNUSED gpointer user_data)
560{
561 g_return_if_fail(IS_RECENT_CONTACTS_VIEW(treeview));
562
563 /* we always drag the selected row */
564 auto selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(treeview));
565 GtkTreeModel *model = NULL;
566 GtkTreeIter iter;
567
568 if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
569 auto path_str = gtk_tree_model_get_string_from_iter(model, &iter);
570
571 gtk_selection_data_set(data,
572 gdk_atom_intern_static_string(CALL_TARGET),
573 8, /* bytes */
574 (guchar *)path_str,
575 strlen(path_str) + 1);
576
577 g_free(path_str);
578 } else {
579 g_warning("drag selection not valid");
580 }
581}
582
583static gboolean
584on_drag_drop(GtkWidget *treeview,
585 GdkDragContext *context,
586 gint x,
587 gint y,
588 guint time,
589 G_GNUC_UNUSED gpointer user_data)
590{
591 g_return_val_if_fail(IS_RECENT_CONTACTS_VIEW(treeview), FALSE);
592
593 GtkTreePath *path = NULL;
594 GtkTreeViewDropPosition drop_pos;
595
596 if (gtk_tree_view_get_dest_row_at_pos(GTK_TREE_VIEW(treeview),
597 x, y, &path, &drop_pos)) {
598
599 GdkAtom target_type = gtk_drag_dest_find_target(treeview, context, NULL);
600
601 if (target_type != GDK_NONE) {
602 g_debug("can drop");
603 gtk_drag_get_data(treeview, context, target_type, time);
604 return TRUE;
605 }
606
607 gtk_tree_path_free(path);
608 }
609
610 return FALSE;
611}
612
613static gboolean
614on_drag_motion(GtkWidget *treeview,
615 GdkDragContext *context,
616 gint x,
617 gint y,
618 guint time,
619 G_GNUC_UNUSED gpointer user_data)
620{
621 g_return_val_if_fail(IS_RECENT_CONTACTS_VIEW(treeview), FALSE);
622
623 GtkTreePath *path = NULL;
624 GtkTreeViewDropPosition drop_pos;
625
626 if (gtk_tree_view_get_dest_row_at_pos(GTK_TREE_VIEW(treeview),
627 x, y, &path, &drop_pos)) {
628 // we only want to drop on a row, not before or after
629 if (drop_pos == GTK_TREE_VIEW_DROP_BEFORE) {
630 gtk_tree_view_set_drag_dest_row(GTK_TREE_VIEW(treeview), path, GTK_TREE_VIEW_DROP_INTO_OR_BEFORE);
631 } else if (drop_pos == GTK_TREE_VIEW_DROP_AFTER) {
632 gtk_tree_view_set_drag_dest_row(GTK_TREE_VIEW(treeview), path, GTK_TREE_VIEW_DROP_INTO_OR_AFTER);
633 }
634 gdk_drag_status(context, gdk_drag_context_get_suggested_action(context), time);
635 return TRUE;
636 } else {
637 // not a row in the treeview, so we cannot drop
638 return FALSE;
639 }
640}
641
642static void
643on_drag_data_received(GtkWidget *treeview,
644 GdkDragContext *context,
645 gint x,
646 gint y,
647 GtkSelectionData *data,
648 G_GNUC_UNUSED guint info,
649 guint time,
650 G_GNUC_UNUSED gpointer user_data)
651{
652 g_return_if_fail(IS_RECENT_CONTACTS_VIEW(treeview));
653
654 gboolean success = FALSE;
655
656 /* get the source and destination calls */
657 auto path_str_source = (gchar *)gtk_selection_data_get_data(data);
658 auto type = gtk_selection_data_get_data_type(data);
659 g_debug("data type: %s", gdk_atom_name(type));
660 if (path_str_source && strlen(path_str_source) > 0) {
661 g_debug("source path: %s", path_str_source);
662
663 /* get the destination path */
664 GtkTreePath *dest_path = NULL;
665 if (gtk_tree_view_get_dest_row_at_pos(GTK_TREE_VIEW(treeview), x, y, &dest_path, NULL)) {
666 auto model = gtk_tree_view_get_model(GTK_TREE_VIEW(treeview));
667
668 GtkTreeIter source;
669 gtk_tree_model_get_iter_from_string(model, &source, path_str_source);
670 auto idx_source_proxy = gtk_q_sort_filter_tree_model_get_source_idx(GTK_Q_SORT_FILTER_TREE_MODEL(model), &source);
671
672 GtkTreeIter dest;
673 gtk_tree_model_get_iter(model, &dest, dest_path);
674 auto idx_dest_proxy = gtk_q_sort_filter_tree_model_get_source_idx(GTK_Q_SORT_FILTER_TREE_MODEL(model), &dest);
675
676 // get call objects and indeces from RecentModel indeces being drag and dropped
677 auto idx_source = RecentModel::instance().peopleProxy()->mapToSource(idx_source_proxy);
678 auto idx_dest = RecentModel::instance().peopleProxy()->mapToSource(idx_dest_proxy);
679 auto call_source = RecentModel::instance().getActiveCall(idx_source);
680 auto call_dest = RecentModel::instance().getActiveCall(idx_dest);
681 auto idx_call_source = CallModel::instance().getIndex(call_source);
682 auto idx_call_dest = CallModel::instance().getIndex(call_dest);
683
684 if (idx_call_source.isValid() && idx_call_dest.isValid()) {
685 QModelIndexList source_list;
686 source_list << idx_call_source;
687 auto mimeData = CallModel::instance().mimeData(source_list);
688 auto action = Call::DropAction::Conference;
689 mimeData->setProperty("dropAction", action);
690
691 if (CallModel::instance().dropMimeData(mimeData, Qt::MoveAction, idx_call_dest.row(), idx_call_dest.column(), idx_call_dest.parent())) {
692 success = TRUE;
693 } else {
694 g_warning("could not drop mime data");
695 }
696 } else {
697 g_warning("source or dest call not valid");
698 }
699
700 gtk_tree_path_free(dest_path);
701 }
702 }
703
704 gtk_drag_finish(context, success, FALSE, time);
705}
706
707static void
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400708recent_contacts_view_init(RecentContactsView *self)
709{
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400710 RecentContactsViewPrivate *priv = RECENT_CONTACTS_VIEW_GET_PRIVATE(self);
711
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400712 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(self), FALSE);
713 /* no need to show the expander since it will always be expanded */
714 gtk_tree_view_set_show_expanders(GTK_TREE_VIEW(self), FALSE);
715 /* disable default search, we will handle it ourselves via LRC;
716 * otherwise the search steals input focus on key presses */
717 gtk_tree_view_set_enable_search(GTK_TREE_VIEW(self), FALSE);
718
719 GtkQSortFilterTreeModel *recent_model = gtk_q_sort_filter_tree_model_new(
Guillaume Roguez5d1514b2015-10-22 15:55:31 -0400720 RecentModel::instance().peopleProxy(),
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400721 1,
722 Qt::DisplayRole, G_TYPE_STRING);
723
724 gtk_tree_view_set_model(GTK_TREE_VIEW(self),
725 GTK_TREE_MODEL(recent_model));
726
727 /* photo and name/contact method column */
728 GtkCellArea *area = gtk_cell_area_box_new();
729 GtkTreeViewColumn *column = gtk_tree_view_column_new_with_area(area);
730
731 /* photo renderer */
732 GtkCellRenderer *renderer = gtk_cell_renderer_pixbuf_new();
733 gtk_cell_area_box_pack_start(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
734
735 /* get the photo */
736 gtk_tree_view_column_set_cell_data_func(
737 column,
738 renderer,
739 (GtkTreeCellDataFunc)render_contact_photo,
740 NULL,
741 NULL);
742
743 /* name/cm and status renderer */
744 renderer = gtk_cell_renderer_text_new();
745 g_object_set(G_OBJECT(renderer), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
746 gtk_cell_area_box_pack_start(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
747
748 gtk_tree_view_column_set_cell_data_func(
749 column,
750 renderer,
751 (GtkTreeCellDataFunc)render_name_and_info,
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400752 self,
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400753 NULL);
754
Stepan Salenikoviche8ef9352015-10-27 19:07:46 -0400755 /* call duration */
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400756 renderer = gtk_cell_renderer_text_new();
Stepan Salenikoviche8ef9352015-10-27 19:07:46 -0400757 g_object_set(G_OBJECT(renderer), "xalign", 1.0, NULL);
758 gtk_cell_area_box_pack_end(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400759 gtk_tree_view_column_set_cell_data_func(
760 column,
761 renderer,
762 (GtkTreeCellDataFunc)render_call_duration,
763 NULL,
764 NULL);
765
Stepan Salenikoviche8ef9352015-10-27 19:07:46 -0400766 gtk_tree_view_append_column(GTK_TREE_VIEW(self), column);
767 gtk_tree_view_column_set_resizable(column, TRUE);
768 gtk_tree_view_column_set_expand(column, TRUE);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400769 gtk_tree_view_expand_all(GTK_TREE_VIEW(self));
770
771 g_signal_connect(self, "button-press-event", G_CALLBACK(create_popup_menu), NULL);
772 g_signal_connect(self, "row-activated", G_CALLBACK(activate_item), NULL);
773 g_signal_connect(recent_model, "row-inserted", G_CALLBACK(expand_if_child), self);
774
775 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(self));
Stepan Salenikovichc1323422016-01-06 10:54:44 -0500776 g_signal_connect(selection, "changed", G_CALLBACK(update_selection), NULL);
Stepan Salenikovichec0862f2015-10-16 15:49:42 -0400777 g_signal_connect(selection, "changed", G_CALLBACK(scroll_to_selection), NULL);
778 g_signal_connect_swapped(recent_model, "rows-reordered", G_CALLBACK(scroll_to_selection), selection);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400779
Stepan Salenikovichc1323422016-01-06 10:54:44 -0500780 /* update the selection based on the RecentModel */
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400781 priv->selection_updated = QObject::connect(
Stepan Salenikovichc1323422016-01-06 10:54:44 -0500782 RecentModel::instance().selectionModel(),
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400783 &QItemSelectionModel::currentChanged,
784 [self, recent_model](const QModelIndex current, G_GNUC_UNUSED const QModelIndex & previous) {
Stepan Salenikovichc1323422016-01-06 10:54:44 -0500785 auto selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(self));
786
787 auto current_proxy = RecentModel::instance().peopleProxy()->mapFromSource(current);
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400788
789 /* select the current */
Stepan Salenikovichc1323422016-01-06 10:54:44 -0500790 GtkTreeIter new_iter;
791 if (gtk_q_sort_filter_tree_model_source_index_to_iter(recent_model, current_proxy, &new_iter)) {
792 gtk_tree_selection_select_iter(selection, &new_iter);
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400793 }
794 }
795 );
796
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500797 /* drag and drop */
798 static GtkTargetEntry targetentries[] = {
799 { (gchar *)CALL_TARGET, GTK_TARGET_SAME_WIDGET, CALL_TARGET_ID },
800 };
801
802 gtk_tree_view_enable_model_drag_source(GTK_TREE_VIEW(self),
803 GDK_BUTTON1_MASK, targetentries, 1, (GdkDragAction)(GDK_ACTION_DEFAULT | GDK_ACTION_MOVE));
804
805 gtk_tree_view_enable_model_drag_dest(GTK_TREE_VIEW(self),
806 targetentries, 1, GDK_ACTION_DEFAULT);
807
808 g_signal_connect(self, "drag-data-get", G_CALLBACK(on_drag_data_get), nullptr);
809 g_signal_connect(self, "drag-drop", G_CALLBACK(on_drag_drop), nullptr);
810 g_signal_connect(self, "drag-motion", G_CALLBACK(on_drag_motion), nullptr);
811 g_signal_connect(self, "drag_data_received", G_CALLBACK(on_drag_data_received), nullptr);
812
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400813 gtk_widget_show_all(GTK_WIDGET(self));
814}
815
816static void
817recent_contacts_view_dispose(GObject *object)
818{
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400819 RecentContactsView *self = RECENT_CONTACTS_VIEW(object);
820 RecentContactsViewPrivate *priv = RECENT_CONTACTS_VIEW_GET_PRIVATE(self);
821
822 QObject::disconnect(priv->selection_updated);
823
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400824 G_OBJECT_CLASS(recent_contacts_view_parent_class)->dispose(object);
825}
826
827static void
828recent_contacts_view_finalize(GObject *object)
829{
830 G_OBJECT_CLASS(recent_contacts_view_parent_class)->finalize(object);
831}
832
833static void
834recent_contacts_view_class_init(RecentContactsViewClass *klass)
835{
836 G_OBJECT_CLASS(klass)->finalize = recent_contacts_view_finalize;
837 G_OBJECT_CLASS(klass)->dispose = recent_contacts_view_dispose;
838}
839
840GtkWidget *
841recent_contacts_view_new()
842{
843 gpointer self = g_object_new(RECENT_CONTACTS_VIEW_TYPE, NULL);
844
845 return (GtkWidget *)self;
846}