blob: d74665d811567637a9e320bc53a8201ed1366ed3 [file] [log] [blame]
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -04001/*
2 * Copyright (C) 2015 Savoir-faire Linux Inc.
3 * 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 Salenikovich2f8b4492015-09-21 17:10:36 -040041
42static constexpr const char* COPY_DATA_KEY = "copy_data";
43
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -050044static constexpr const char* CALL_TARGET = "CALL_TARGET";
45static constexpr int CALL_TARGET_ID = 0;
46
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040047struct _RecentContactsView
48{
49 GtkTreeView parent;
50};
51
52struct _RecentContactsViewClass
53{
54 GtkTreeViewClass parent_class;
55};
56
57typedef struct _RecentContactsViewPrivate RecentContactsViewPrivate;
58
59struct _RecentContactsViewPrivate
60{
61 GtkWidget *overlay_button;
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -040062
63 QMetaObject::Connection selection_updated;
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040064};
65
66G_DEFINE_TYPE_WITH_PRIVATE(RecentContactsView, recent_contacts_view, GTK_TYPE_TREE_VIEW);
67
68#define RECENT_CONTACTS_VIEW_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), RECENT_CONTACTS_VIEW_TYPE, RecentContactsViewPrivate))
69
70static void
71update_call_model_selection(GtkTreeSelection *selection, G_GNUC_UNUSED gpointer user_data)
72{
73 auto current_proxy = get_index_from_selection(selection);
Guillaume Roguez5d1514b2015-10-22 15:55:31 -040074 auto current = RecentModel::instance().peopleProxy()->mapToSource(current_proxy);
75 if (auto call_to_select = RecentModel::instance().getActiveCall(current)) {
76 CallModel::instance().selectCall(call_to_select);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040077 } else {
Guillaume Roguez5d1514b2015-10-22 15:55:31 -040078 CallModel::instance().selectionModel()->clearCurrentIndex();
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040079 }
80}
81
82static void
83copy_contact_info(GtkWidget *item, G_GNUC_UNUSED gpointer user_data)
84{
85 gpointer data = g_object_get_data(G_OBJECT(item), COPY_DATA_KEY);
86 g_return_if_fail(data);
87 gchar* text = (gchar *)data;
88 GtkClipboard* clip = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
89 gtk_clipboard_set_text(clip, text, -1);
90}
91
92static void
93call_contactmethod(G_GNUC_UNUSED GtkWidget *item, ContactMethod *cm)
94{
95 g_return_if_fail(cm);
96 place_new_call(cm);
97}
98
99static void
100render_contact_photo(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
101 GtkCellRenderer *cell,
102 GtkTreeModel *model,
103 GtkTreeIter *iter,
104 G_GNUC_UNUSED gpointer data)
105{
106 QModelIndex idx = gtk_q_sort_filter_tree_model_get_source_idx(GTK_Q_SORT_FILTER_TREE_MODEL(model), iter);
107
108 std::shared_ptr<GdkPixbuf> photo;
109 /* we only want to render a photo for the top nodes: Person, ContactMethod (, later Conference) */
110 QVariant object = idx.data(static_cast<int>(Ring::Role::Object));
111 if (idx.isValid() && object.isValid()) {
112 QVariant var_photo;
113 if (auto person = object.value<Person *>()) {
114 var_photo = GlobalInstances::pixmapManipulator().contactPhoto(person, QSize(50, 50), false);
115 } else if (auto cm = object.value<ContactMethod *>()) {
116 /* get photo, note that this should in all cases be the fallback avatar, since there
117 * shouldn't be a person associated with this contact method */
118 var_photo = GlobalInstances::pixmapManipulator().callPhoto(cm, QSize(50, 50), false);
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500119 } else if (auto call = object.value<Call *>()) {
120 if (call->type() == Call::Type::CONFERENCE) {
121 var_photo = GlobalInstances::pixmapManipulator().callPhoto(call, QSize(50, 50), false);
122 }
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400123 }
124 if (var_photo.isValid())
125 photo = var_photo.value<std::shared_ptr<GdkPixbuf>>();
126 else {
127 // set the width of the cell rendered to the with of the photo
128 // so that the other renderers are shifted to the right
129 g_object_set(G_OBJECT(cell), "width", 50, NULL);
130 }
131 }
132
133 g_object_set(G_OBJECT(cell), "pixbuf", photo.get(), NULL);
134}
135
136static void
137render_name_and_info(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
138 GtkCellRenderer *cell,
139 GtkTreeModel *model,
140 GtkTreeIter *iter,
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400141 GtkTreeView *treeview)
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400142{
143 gchar *text = NULL;
144
145 QModelIndex idx = gtk_q_sort_filter_tree_model_get_source_idx(GTK_Q_SORT_FILTER_TREE_MODEL(model), iter);
146
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400147 // check if this iter is selected
148 gboolean is_selected = FALSE;
149 if (GTK_IS_TREE_VIEW(treeview)) {
150 auto selection = gtk_tree_view_get_selection(treeview);
151 is_selected = gtk_tree_selection_iter_is_selected(selection, iter);
152 }
153
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400154 auto type = idx.data(static_cast<int>(Ring::Role::ObjectType));
155 if (idx.isValid() && type.isValid()) {
156 switch (type.value<Ring::ObjectType>()) {
157 case Ring::ObjectType::Person:
158 case Ring::ObjectType::ContactMethod:
159 {
160 auto var_name = idx.data(static_cast<int>(Ring::Role::Name));
161 auto var_lastused = idx.data(static_cast<int>(Ring::Role::LastUsed));
162 auto var_status = idx.data(static_cast<int>(Ring::Role::FormattedState));
163
164 QString name, status;
165
166 if (var_name.isValid())
167 name = var_name.value<QString>();
168
169 // show the status if there is a call, otherwise the last used date/time
170 if (var_status.isValid()) {
171 status += var_status.value<QString>();
172 }else if (var_lastused.isValid()) {
173 auto date_time = var_lastused.value<QDateTime>();
174 auto category = HistoryTimeCategoryModel::timeToHistoryConst(date_time.toTime_t());
175
176 // if it is 'today', then we only want to show the time
177 if (category != HistoryTimeCategoryModel::HistoryConst::Today) {
178 status += HistoryTimeCategoryModel::timeToHistoryCategory(date_time.toTime_t());
179 }
180 // we only want to show the time if it is less than a week ago
181 if (category < HistoryTimeCategoryModel::HistoryConst::A_week_ago) {
182 if (!status.isEmpty())
183 status += ", ";
184 status += QLocale::system().toString(date_time.time(), QLocale::ShortFormat);
185 }
186 }
187
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400188 /* we want the color of the status text to be the default color if this iter is
189 * selected so that the treeview is able to invert it against the selection color */
190 if (is_selected) {
191 text = g_strdup_printf("%s\n<span size=\"smaller\">%s</span>",
192 name.toUtf8().constData(),
193 status.toUtf8().constData());
194 } else {
195 text = g_strdup_printf("%s\n<span size=\"smaller\" color=\"gray\">%s</span>",
196 name.toUtf8().constData(),
197 status.toUtf8().constData());
198 }
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400199 }
200 break;
201 case Ring::ObjectType::Call:
202 {
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500203 // check if it is a conference
204 auto idx_source = RecentModel::instance().peopleProxy()->mapToSource(idx);
205 auto is_conference = RecentModel::instance().isConference(idx_source);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400206
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500207 if (is_conference) {
208 auto var_name = idx.data(static_cast<int>(Ring::Role::Name));
209 text = g_markup_printf_escaped("%s", var_name.value<QString>().toUtf8().constData());
210 } else {
211 auto parent_source = RecentModel::instance().peopleProxy()->mapToSource(idx.parent());
212 if (RecentModel::instance().isConference(parent_source)) {
213 // part of conference, simply display the name
214 auto var_name = idx.data(static_cast<int>(Ring::Role::Name));
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400215
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500216 /* we want the color of the name text to be the default color if this iter is
217 * selected so that the treeview is able to invert it against the selection color */
218 if (is_selected)
219 text = g_strdup_printf("<span size=\"smaller\">%s</span>", var_name.value<QString>().toUtf8().constData());
220 else
221 text = g_strdup_printf("<span size=\"smaller\" color=\"gray\">%s</span>", var_name.value<QString>().toUtf8().constData());
222 } else {
223 // just a call, so display the state
224 auto var_status = idx.data(static_cast<int>(Ring::Role::FormattedState));
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400225
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500226 QString status;
227
228 if (var_status.isValid())
229 status += var_status.value<QString>();
230
231 /* we want the color of the status text to be the default color if this iter is
232 * selected so that the treeview is able to invert it against the selection color */
233 if (is_selected)
234 text = g_strdup_printf("<span size=\"smaller\">%s</span>", status.toUtf8().constData());
235 else
236 text = g_strdup_printf("<span size=\"smaller\" color=\"gray\">%s</span>", status.toUtf8().constData());
237 }
238 }
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400239 }
240 break;
241 case Ring::ObjectType::Media:
242 // nothing to do for now
Stepan Salenikovich4e7ea712015-12-24 14:04:37 -0500243 case Ring::ObjectType::COUNT__:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400244 break;
245 }
246 }
247
248 g_object_set(G_OBJECT(cell), "markup", text, NULL);
249 g_free(text);
250}
251
252static void
253render_call_duration(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
254 GtkCellRenderer *cell,
255 GtkTreeModel *model,
256 GtkTreeIter *iter,
257 G_GNUC_UNUSED gpointer data)
258{
259 gchar *text = NULL;
260
261 QModelIndex idx = gtk_q_sort_filter_tree_model_get_source_idx(GTK_Q_SORT_FILTER_TREE_MODEL(model), iter);
262
263 auto type = idx.data(static_cast<int>(Ring::Role::ObjectType));
264 if (idx.isValid() && type.isValid()) {
265 switch (type.value<Ring::ObjectType>()) {
266 case Ring::ObjectType::Person:
267 case Ring::ObjectType::ContactMethod:
268 {
269 // check if there are any children (calls); we need to convert to source model in
270 // case there is only one
Guillaume Roguez5d1514b2015-10-22 15:55:31 -0400271 auto idx_source = RecentModel::instance().peopleProxy()->mapToSource(idx);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400272 auto duration = idx.data(static_cast<int>(Ring::Role::Length));
273 if (idx_source.isValid()
274 && (idx_source.model()->rowCount(idx_source) == 1)
275 && duration.isValid())
276 {
277 text = g_strdup_printf("%s", duration.value<QString>().toUtf8().constData());
278 }
279 }
280 break;
281 case Ring::ObjectType::Call:
282 {
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500283 // do not display the duration if the call is part of a conference
284 auto parent_source = RecentModel::instance().peopleProxy()->mapToSource(idx.parent());
285 auto in_conference = RecentModel::instance().isConference(parent_source);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400286
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500287 if (!in_conference) {
288 auto duration = idx.data(static_cast<int>(Ring::Role::Length));
289
290 if (duration.isValid())
291 text = g_strdup_printf("%s", duration.value<QString>().toUtf8().constData());
292 }
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400293 }
294 break;
295 case Ring::ObjectType::Media:
296 // nothing to do for now
Stepan Salenikovich4e7ea712015-12-24 14:04:37 -0500297 case Ring::ObjectType::COUNT__:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400298 break;
299 }
300 }
301
302 g_object_set(G_OBJECT(cell), "markup", text, NULL);
303 g_free(text);
304}
305
306static void
307activate_item(GtkTreeView *tree_view,
308 GtkTreePath *path,
309 G_GNUC_UNUSED GtkTreeViewColumn *column,
310 G_GNUC_UNUSED gpointer user_data)
311{
312 auto model = gtk_tree_view_get_model(tree_view);
313 GtkTreeIter iter;
314 if (gtk_tree_model_get_iter(model, &iter, path)) {
315 auto idx = gtk_q_sort_filter_tree_model_get_source_idx(GTK_Q_SORT_FILTER_TREE_MODEL(model), &iter);
316 auto type = idx.data(static_cast<int>(Ring::Role::ObjectType));
317 if (idx.isValid() && type.isValid()) {
318 switch (type.value<Ring::ObjectType>()) {
319 case Ring::ObjectType::Person:
320 {
321 // call the last used contact method
322 // TODO: if no contact methods have been used, offer a popup to Choose
323 auto p_var = idx.data(static_cast<int>(Ring::Role::Object));
324 if (p_var.isValid()) {
325 auto person = p_var.value<Person *>();
326 auto cms = person->phoneNumbers();
327
328 if (!cms.isEmpty()) {
329 auto last_used_cm = cms.at(0);
330 for (int i = 1; i < cms.size(); ++i) {
331 auto new_cm = cms.at(i);
332 if (difftime(new_cm->lastUsed(), last_used_cm->lastUsed()) > 0)
333 last_used_cm = new_cm;
334 }
335
336 place_new_call(last_used_cm);
337 }
338 }
339 }
340 break;
341 case Ring::ObjectType::ContactMethod:
342 {
343 // call the contact method
344 auto cm = idx.data(static_cast<int>(Ring::Role::Object));
345 if (cm.isValid())
346 place_new_call(cm.value<ContactMethod *>());
347 }
348 break;
349 case Ring::ObjectType::Call:
350 case Ring::ObjectType::Media:
351 // nothing to do for now
Stepan Salenikovich4e7ea712015-12-24 14:04:37 -0500352 case Ring::ObjectType::COUNT__:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400353 break;
354 }
355 }
356 }
357}
358
359static gboolean
360create_popup_menu(GtkTreeView *treeview, GdkEventButton *event, G_GNUC_UNUSED gpointer user_data)
361{
362 /* build popup menu when right clicking on contact item
363 * user should be able to copy the contact's name or "number".
364 * or add the "number" to his contact list, if not already so
365 */
366
367 /* check for right click */
368 if (event->button != BUTTON_RIGHT_CLICK || event->type != GDK_BUTTON_PRESS)
369 return FALSE;
370
371 GtkTreeIter iter;
372 GtkTreeModel *model;
373 GtkTreeSelection *selection = gtk_tree_view_get_selection(treeview);
374 if (!gtk_tree_selection_get_selected(selection, &model, &iter))
375 return FALSE;
376
377 GtkWidget *menu = gtk_menu_new();
378 QModelIndex idx = gtk_q_sort_filter_tree_model_get_source_idx(GTK_Q_SORT_FILTER_TREE_MODEL(model), &iter);
379
380 /* if Person or CM, give option to call */
381 auto type = idx.data(static_cast<int>(Ring::Role::ObjectType));
382 auto object = idx.data(static_cast<int>(Ring::Role::Object));
383 if (type.isValid() && object.isValid()) {
384 switch (type.value<Ring::ObjectType>()) {
385 case Ring::ObjectType::Person:
386 {
387 /* possiblity for multiple numbers */
388 auto cms = object.value<Person *>()->phoneNumbers();
389 if (cms.size() == 1) {
390 auto item = gtk_menu_item_new_with_mnemonic(_("_Call"));
391 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
392 g_signal_connect(item,
393 "activate",
394 G_CALLBACK(call_contactmethod),
395 cms.at(0));
396 } else if (cms.size() > 1) {
397 auto call_item = gtk_menu_item_new_with_mnemonic(_("_Call"));
398 gtk_menu_shell_append(GTK_MENU_SHELL(menu), call_item);
399 auto call_menu = gtk_menu_new();
400 gtk_menu_item_set_submenu(GTK_MENU_ITEM(call_item), call_menu);
401 for (int i = 0; i < cms.size(); ++i) {
402 auto item = gtk_menu_item_new_with_label(cms.at(i)->uri().toUtf8().constData());
403 gtk_menu_shell_append(GTK_MENU_SHELL(call_menu), item);
404 g_signal_connect(item,
405 "activate",
406 G_CALLBACK(call_contactmethod),
407 cms.at(i));
408 }
409 }
410 }
411 break;
412 case Ring::ObjectType::ContactMethod:
413 {
414 auto cm = object.value<ContactMethod *>();
415 auto item = gtk_menu_item_new_with_mnemonic(_("_Call"));
416 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
417 g_signal_connect(item,
418 "activate",
419 G_CALLBACK(call_contactmethod),
420 cm);
421 }
422 break;
423 case Ring::ObjectType::Call:
424 case Ring::ObjectType::Media:
425 // nothing to do for now
Stepan Salenikovich4e7ea712015-12-24 14:04:37 -0500426 case Ring::ObjectType::COUNT__:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400427 break;
428 }
429 }
430
431 /* copy name */
432 QVariant name_var = idx.data(static_cast<int>(Ring::Role::Name));
433 if (name_var.isValid()) {
434 gchar *name = g_strdup_printf("%s", name_var.value<QString>().toUtf8().constData());
435 GtkWidget *item = gtk_menu_item_new_with_mnemonic(_("_Copy name"));
436 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
437 g_object_set_data_full(G_OBJECT(item), COPY_DATA_KEY, name, (GDestroyNotify)g_free);
438 g_signal_connect(item,
439 "activate",
440 G_CALLBACK(copy_contact_info),
441 NULL);
442 }
443
444 /* copy number(s) */
445 if (type.isValid() && object.isValid()) {
446 switch (type.value<Ring::ObjectType>()) {
447 case Ring::ObjectType::Person:
448 {
449 /* possiblity for multiple numbers */
450 auto cms = object.value<Person *>()->phoneNumbers();
451 if (cms.size() == 1) {
452 gchar *number = g_strdup_printf("%s",cms.at(0)->uri().toUtf8().constData());
453 auto item = gtk_menu_item_new_with_mnemonic(_("_Copy number"));
454 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
455 g_object_set_data_full(G_OBJECT(item), COPY_DATA_KEY, number, (GDestroyNotify)g_free);
456 g_signal_connect(item,
457 "activate",
458 G_CALLBACK(copy_contact_info),
459 NULL);
460 } else if (cms.size() > 1) {
461 auto copy_item = gtk_menu_item_new_with_mnemonic(_("_Copy number"));
462 gtk_menu_shell_append(GTK_MENU_SHELL(menu), copy_item);
463 auto copy_menu = gtk_menu_new();
464 gtk_menu_item_set_submenu(GTK_MENU_ITEM(copy_item), copy_menu);
465 for (int i = 0; i < cms.size(); ++i) {
466 gchar *number = g_strdup_printf("%s",cms.at(i)->uri().toUtf8().constData());
467 auto item = gtk_menu_item_new_with_label(cms.at(i)->uri().toUtf8().constData());
468 gtk_menu_shell_append(GTK_MENU_SHELL(copy_menu), item);
469 g_object_set_data_full(G_OBJECT(item), COPY_DATA_KEY, number, (GDestroyNotify)g_free);
470 g_signal_connect(item,
471 "activate",
472 G_CALLBACK(copy_contact_info),
473 NULL);
474 }
475 }
476 }
477 break;
478 case Ring::ObjectType::ContactMethod:
479 case Ring::ObjectType::Call:
480 case Ring::ObjectType::Media:
Stepan Salenikovich4e7ea712015-12-24 14:04:37 -0500481 {
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400482 QVariant number_var = idx.data(static_cast<int>(Ring::Role::Number));
483 if (number_var.isValid()) {
484 gchar *number = g_strdup_printf("%s", number_var.value<QString>().toUtf8().constData());
485 GtkWidget *item = gtk_menu_item_new_with_mnemonic(_("_Copy number"));
486 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
487 g_object_set_data_full(G_OBJECT(item), COPY_DATA_KEY, number, (GDestroyNotify)g_free);
488 g_signal_connect(item,
489 "activate",
490 G_CALLBACK(copy_contact_info),
491 NULL);
492 }
Stepan Salenikovich4e7ea712015-12-24 14:04:37 -0500493 }
494 break;
495 case Ring::ObjectType::COUNT__:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400496 break;
497 }
498 }
499
500 /* if the object is a CM, then give the option to add to contacts*/
501 if (type.isValid() && object.isValid()) {
502 if (type.value<Ring::ObjectType>() == Ring::ObjectType::ContactMethod) {
503 /* get rectangle */
504 auto path = gtk_tree_model_get_path(model, &iter);
505 auto column = gtk_tree_view_get_column(treeview, 0);
506 GdkRectangle rect;
507 gtk_tree_view_get_cell_area(treeview, path, column, &rect);
508 gtk_tree_view_convert_bin_window_to_widget_coords(treeview, rect.x, rect.y, &rect.x, &rect.y);
509 gtk_tree_path_free(path);
510 auto add_to = menu_item_add_to_contact(object.value<ContactMethod *>(), GTK_WIDGET(treeview), &rect);
511 gtk_menu_shell_append(GTK_MENU_SHELL(menu), add_to);
512 }
513 }
514
515 /* show menu */
516 gtk_widget_show_all(menu);
517 gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, event->button, event->time);
518
519 return TRUE; /* we handled the event */
520}
521
522static void
523expand_if_child(G_GNUC_UNUSED GtkTreeModel *tree_model,
524 GtkTreePath *path,
525 G_GNUC_UNUSED GtkTreeIter *iter,
526 GtkTreeView *treeview)
527{
528 if (gtk_tree_path_get_depth(path) > 1)
529 gtk_tree_view_expand_to_path(treeview, path);
530}
531
532static void
Stepan Salenikovichec0862f2015-10-16 15:49:42 -0400533scroll_to_selection(GtkTreeSelection *selection)
534{
535 GtkTreeModel *model = nullptr;
536 GtkTreeIter iter;
537 if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
538 auto path = gtk_tree_model_get_path(model, &iter);
539 auto treeview = gtk_tree_selection_get_tree_view(selection);
540 gtk_tree_view_scroll_to_cell(treeview, path, nullptr, FALSE, 0.0, 0.0);
541 }
542}
543
544static void
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500545on_drag_data_get(GtkWidget *treeview,
546 G_GNUC_UNUSED GdkDragContext *context,
547 GtkSelectionData *data,
548 G_GNUC_UNUSED guint info,
549 G_GNUC_UNUSED guint time,
550 G_GNUC_UNUSED gpointer user_data)
551{
552 g_return_if_fail(IS_RECENT_CONTACTS_VIEW(treeview));
553
554 /* we always drag the selected row */
555 auto selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(treeview));
556 GtkTreeModel *model = NULL;
557 GtkTreeIter iter;
558
559 if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
560 auto path_str = gtk_tree_model_get_string_from_iter(model, &iter);
561
562 gtk_selection_data_set(data,
563 gdk_atom_intern_static_string(CALL_TARGET),
564 8, /* bytes */
565 (guchar *)path_str,
566 strlen(path_str) + 1);
567
568 g_free(path_str);
569 } else {
570 g_warning("drag selection not valid");
571 }
572}
573
574static gboolean
575on_drag_drop(GtkWidget *treeview,
576 GdkDragContext *context,
577 gint x,
578 gint y,
579 guint time,
580 G_GNUC_UNUSED gpointer user_data)
581{
582 g_return_val_if_fail(IS_RECENT_CONTACTS_VIEW(treeview), FALSE);
583
584 GtkTreePath *path = NULL;
585 GtkTreeViewDropPosition drop_pos;
586
587 if (gtk_tree_view_get_dest_row_at_pos(GTK_TREE_VIEW(treeview),
588 x, y, &path, &drop_pos)) {
589
590 GdkAtom target_type = gtk_drag_dest_find_target(treeview, context, NULL);
591
592 if (target_type != GDK_NONE) {
593 g_debug("can drop");
594 gtk_drag_get_data(treeview, context, target_type, time);
595 return TRUE;
596 }
597
598 gtk_tree_path_free(path);
599 }
600
601 return FALSE;
602}
603
604static gboolean
605on_drag_motion(GtkWidget *treeview,
606 GdkDragContext *context,
607 gint x,
608 gint y,
609 guint time,
610 G_GNUC_UNUSED gpointer user_data)
611{
612 g_return_val_if_fail(IS_RECENT_CONTACTS_VIEW(treeview), FALSE);
613
614 GtkTreePath *path = NULL;
615 GtkTreeViewDropPosition drop_pos;
616
617 if (gtk_tree_view_get_dest_row_at_pos(GTK_TREE_VIEW(treeview),
618 x, y, &path, &drop_pos)) {
619 // we only want to drop on a row, not before or after
620 if (drop_pos == GTK_TREE_VIEW_DROP_BEFORE) {
621 gtk_tree_view_set_drag_dest_row(GTK_TREE_VIEW(treeview), path, GTK_TREE_VIEW_DROP_INTO_OR_BEFORE);
622 } else if (drop_pos == GTK_TREE_VIEW_DROP_AFTER) {
623 gtk_tree_view_set_drag_dest_row(GTK_TREE_VIEW(treeview), path, GTK_TREE_VIEW_DROP_INTO_OR_AFTER);
624 }
625 gdk_drag_status(context, gdk_drag_context_get_suggested_action(context), time);
626 return TRUE;
627 } else {
628 // not a row in the treeview, so we cannot drop
629 return FALSE;
630 }
631}
632
633static void
634on_drag_data_received(GtkWidget *treeview,
635 GdkDragContext *context,
636 gint x,
637 gint y,
638 GtkSelectionData *data,
639 G_GNUC_UNUSED guint info,
640 guint time,
641 G_GNUC_UNUSED gpointer user_data)
642{
643 g_return_if_fail(IS_RECENT_CONTACTS_VIEW(treeview));
644
645 gboolean success = FALSE;
646
647 /* get the source and destination calls */
648 auto path_str_source = (gchar *)gtk_selection_data_get_data(data);
649 auto type = gtk_selection_data_get_data_type(data);
650 g_debug("data type: %s", gdk_atom_name(type));
651 if (path_str_source && strlen(path_str_source) > 0) {
652 g_debug("source path: %s", path_str_source);
653
654 /* get the destination path */
655 GtkTreePath *dest_path = NULL;
656 if (gtk_tree_view_get_dest_row_at_pos(GTK_TREE_VIEW(treeview), x, y, &dest_path, NULL)) {
657 auto model = gtk_tree_view_get_model(GTK_TREE_VIEW(treeview));
658
659 GtkTreeIter source;
660 gtk_tree_model_get_iter_from_string(model, &source, path_str_source);
661 auto idx_source_proxy = gtk_q_sort_filter_tree_model_get_source_idx(GTK_Q_SORT_FILTER_TREE_MODEL(model), &source);
662
663 GtkTreeIter dest;
664 gtk_tree_model_get_iter(model, &dest, dest_path);
665 auto idx_dest_proxy = gtk_q_sort_filter_tree_model_get_source_idx(GTK_Q_SORT_FILTER_TREE_MODEL(model), &dest);
666
667 // get call objects and indeces from RecentModel indeces being drag and dropped
668 auto idx_source = RecentModel::instance().peopleProxy()->mapToSource(idx_source_proxy);
669 auto idx_dest = RecentModel::instance().peopleProxy()->mapToSource(idx_dest_proxy);
670 auto call_source = RecentModel::instance().getActiveCall(idx_source);
671 auto call_dest = RecentModel::instance().getActiveCall(idx_dest);
672 auto idx_call_source = CallModel::instance().getIndex(call_source);
673 auto idx_call_dest = CallModel::instance().getIndex(call_dest);
674
675 if (idx_call_source.isValid() && idx_call_dest.isValid()) {
676 QModelIndexList source_list;
677 source_list << idx_call_source;
678 auto mimeData = CallModel::instance().mimeData(source_list);
679 auto action = Call::DropAction::Conference;
680 mimeData->setProperty("dropAction", action);
681
682 if (CallModel::instance().dropMimeData(mimeData, Qt::MoveAction, idx_call_dest.row(), idx_call_dest.column(), idx_call_dest.parent())) {
683 success = TRUE;
684 } else {
685 g_warning("could not drop mime data");
686 }
687 } else {
688 g_warning("source or dest call not valid");
689 }
690
691 gtk_tree_path_free(dest_path);
692 }
693 }
694
695 gtk_drag_finish(context, success, FALSE, time);
696}
697
698static void
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400699recent_contacts_view_init(RecentContactsView *self)
700{
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400701 RecentContactsViewPrivate *priv = RECENT_CONTACTS_VIEW_GET_PRIVATE(self);
702
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400703 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(self), FALSE);
704 /* no need to show the expander since it will always be expanded */
705 gtk_tree_view_set_show_expanders(GTK_TREE_VIEW(self), FALSE);
706 /* disable default search, we will handle it ourselves via LRC;
707 * otherwise the search steals input focus on key presses */
708 gtk_tree_view_set_enable_search(GTK_TREE_VIEW(self), FALSE);
709
710 GtkQSortFilterTreeModel *recent_model = gtk_q_sort_filter_tree_model_new(
Guillaume Roguez5d1514b2015-10-22 15:55:31 -0400711 RecentModel::instance().peopleProxy(),
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400712 1,
713 Qt::DisplayRole, G_TYPE_STRING);
714
715 gtk_tree_view_set_model(GTK_TREE_VIEW(self),
716 GTK_TREE_MODEL(recent_model));
717
718 /* photo and name/contact method column */
719 GtkCellArea *area = gtk_cell_area_box_new();
720 GtkTreeViewColumn *column = gtk_tree_view_column_new_with_area(area);
721
722 /* photo renderer */
723 GtkCellRenderer *renderer = gtk_cell_renderer_pixbuf_new();
724 gtk_cell_area_box_pack_start(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
725
726 /* get the photo */
727 gtk_tree_view_column_set_cell_data_func(
728 column,
729 renderer,
730 (GtkTreeCellDataFunc)render_contact_photo,
731 NULL,
732 NULL);
733
734 /* name/cm and status renderer */
735 renderer = gtk_cell_renderer_text_new();
736 g_object_set(G_OBJECT(renderer), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
737 gtk_cell_area_box_pack_start(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
738
739 gtk_tree_view_column_set_cell_data_func(
740 column,
741 renderer,
742 (GtkTreeCellDataFunc)render_name_and_info,
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400743 self,
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400744 NULL);
745
Stepan Salenikoviche8ef9352015-10-27 19:07:46 -0400746 /* call duration */
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400747 renderer = gtk_cell_renderer_text_new();
Stepan Salenikoviche8ef9352015-10-27 19:07:46 -0400748 g_object_set(G_OBJECT(renderer), "xalign", 1.0, NULL);
749 gtk_cell_area_box_pack_end(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400750 gtk_tree_view_column_set_cell_data_func(
751 column,
752 renderer,
753 (GtkTreeCellDataFunc)render_call_duration,
754 NULL,
755 NULL);
756
Stepan Salenikoviche8ef9352015-10-27 19:07:46 -0400757 gtk_tree_view_append_column(GTK_TREE_VIEW(self), column);
758 gtk_tree_view_column_set_resizable(column, TRUE);
759 gtk_tree_view_column_set_expand(column, TRUE);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400760 gtk_tree_view_expand_all(GTK_TREE_VIEW(self));
761
762 g_signal_connect(self, "button-press-event", G_CALLBACK(create_popup_menu), NULL);
763 g_signal_connect(self, "row-activated", G_CALLBACK(activate_item), NULL);
764 g_signal_connect(recent_model, "row-inserted", G_CALLBACK(expand_if_child), self);
765
766 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(self));
767 g_signal_connect(selection, "changed", G_CALLBACK(update_call_model_selection), NULL);
Stepan Salenikovichec0862f2015-10-16 15:49:42 -0400768 g_signal_connect(selection, "changed", G_CALLBACK(scroll_to_selection), NULL);
769 g_signal_connect_swapped(recent_model, "rows-reordered", G_CALLBACK(scroll_to_selection), selection);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400770
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400771 /* try to select the same call as the call model */
772 priv->selection_updated = QObject::connect(
Guillaume Roguez5d1514b2015-10-22 15:55:31 -0400773 CallModel::instance().selectionModel(),
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400774 &QItemSelectionModel::currentChanged,
775 [self, recent_model](const QModelIndex current, G_GNUC_UNUSED const QModelIndex & previous) {
776 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(self));
777
778 /* select the current */
Guillaume Roguez5d1514b2015-10-22 15:55:31 -0400779 auto call = CallModel::instance().getCall(current);
780 auto recent_idx = RecentModel::instance().getIndex(call);
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400781 if (recent_idx.isValid()) {
782 QModelIndex proxy_selection; // the index to select in the peopleProxy
Guillaume Roguez5d1514b2015-10-22 15:55:31 -0400783 if (RecentModel::instance().rowCount(recent_idx.parent()) > 1) {
784 proxy_selection = RecentModel::instance().peopleProxy()->mapFromSource(recent_idx);
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400785 } else {
786 // if single call, select the parent
Guillaume Roguez5d1514b2015-10-22 15:55:31 -0400787 proxy_selection = RecentModel::instance().peopleProxy()->mapFromSource(recent_idx.parent());
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400788 }
789
790 GtkTreeIter new_iter;
791 if (gtk_q_sort_filter_tree_model_source_index_to_iter(recent_model, proxy_selection, &new_iter)) {
792 gtk_tree_selection_select_iter(selection, &new_iter);
793 }
794 }
795 }
796 );
797
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500798 /* drag and drop */
799 static GtkTargetEntry targetentries[] = {
800 { (gchar *)CALL_TARGET, GTK_TARGET_SAME_WIDGET, CALL_TARGET_ID },
801 };
802
803 gtk_tree_view_enable_model_drag_source(GTK_TREE_VIEW(self),
804 GDK_BUTTON1_MASK, targetentries, 1, (GdkDragAction)(GDK_ACTION_DEFAULT | GDK_ACTION_MOVE));
805
806 gtk_tree_view_enable_model_drag_dest(GTK_TREE_VIEW(self),
807 targetentries, 1, GDK_ACTION_DEFAULT);
808
809 g_signal_connect(self, "drag-data-get", G_CALLBACK(on_drag_data_get), nullptr);
810 g_signal_connect(self, "drag-drop", G_CALLBACK(on_drag_drop), nullptr);
811 g_signal_connect(self, "drag-motion", G_CALLBACK(on_drag_motion), nullptr);
812 g_signal_connect(self, "drag_data_received", G_CALLBACK(on_drag_data_received), nullptr);
813
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400814 gtk_widget_show_all(GTK_WIDGET(self));
815}
816
817static void
818recent_contacts_view_dispose(GObject *object)
819{
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400820 RecentContactsView *self = RECENT_CONTACTS_VIEW(object);
821 RecentContactsViewPrivate *priv = RECENT_CONTACTS_VIEW_GET_PRIVATE(self);
822
823 QObject::disconnect(priv->selection_updated);
824
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400825 G_OBJECT_CLASS(recent_contacts_view_parent_class)->dispose(object);
826}
827
828static void
829recent_contacts_view_finalize(GObject *object)
830{
831 G_OBJECT_CLASS(recent_contacts_view_parent_class)->finalize(object);
832}
833
834static void
835recent_contacts_view_class_init(RecentContactsViewClass *klass)
836{
837 G_OBJECT_CLASS(klass)->finalize = recent_contacts_view_finalize;
838 G_OBJECT_CLASS(klass)->dispose = recent_contacts_view_dispose;
839}
840
841GtkWidget *
842recent_contacts_view_new()
843{
844 gpointer self = g_object_new(RECENT_CONTACTS_VIEW_TYPE, NULL);
845
846 return (GtkWidget *)self;
847}