blob: 5a849808560c13e965c7e886620b91d4128b1f62 [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 Salenikovich5039c9b2016-02-12 14:09:51 -050042#include "numbercategory.h"
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040043
44static constexpr const char* COPY_DATA_KEY = "copy_data";
45
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -050046static constexpr const char* CALL_TARGET = "CALL_TARGET";
47static constexpr int CALL_TARGET_ID = 0;
48
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040049struct _RecentContactsView
50{
51 GtkTreeView parent;
52};
53
54struct _RecentContactsViewClass
55{
56 GtkTreeViewClass parent_class;
57};
58
59typedef struct _RecentContactsViewPrivate RecentContactsViewPrivate;
60
61struct _RecentContactsViewPrivate
62{
63 GtkWidget *overlay_button;
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -040064
65 QMetaObject::Connection selection_updated;
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040066};
67
68G_DEFINE_TYPE_WITH_PRIVATE(RecentContactsView, recent_contacts_view, GTK_TYPE_TREE_VIEW);
69
70#define RECENT_CONTACTS_VIEW_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), RECENT_CONTACTS_VIEW_TYPE, RecentContactsViewPrivate))
71
72static void
Stepan Salenikovichc1323422016-01-06 10:54:44 -050073update_selection(GtkTreeSelection *selection, G_GNUC_UNUSED gpointer user_data)
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040074{
75 auto current_proxy = get_index_from_selection(selection);
Guillaume Roguez5d1514b2015-10-22 15:55:31 -040076 auto current = RecentModel::instance().peopleProxy()->mapToSource(current_proxy);
Stepan Salenikovichc1323422016-01-06 10:54:44 -050077
78 RecentModel::instance().selectionModel()->setCurrentIndex(current, QItemSelectionModel::ClearAndSelect);
79
80 // update the CallModel selection since we rely on the UserActionModel
Guillaume Roguez5d1514b2015-10-22 15:55:31 -040081 if (auto call_to_select = RecentModel::instance().getActiveCall(current)) {
82 CallModel::instance().selectCall(call_to_select);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040083 } else {
Guillaume Roguez5d1514b2015-10-22 15:55:31 -040084 CallModel::instance().selectionModel()->clearCurrentIndex();
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040085 }
86}
87
88static void
89copy_contact_info(GtkWidget *item, G_GNUC_UNUSED gpointer user_data)
90{
91 gpointer data = g_object_get_data(G_OBJECT(item), COPY_DATA_KEY);
92 g_return_if_fail(data);
93 gchar* text = (gchar *)data;
94 GtkClipboard* clip = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
95 gtk_clipboard_set_text(clip, text, -1);
96}
97
98static void
99call_contactmethod(G_GNUC_UNUSED GtkWidget *item, ContactMethod *cm)
100{
101 g_return_if_fail(cm);
102 place_new_call(cm);
103}
104
105static void
106render_contact_photo(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
107 GtkCellRenderer *cell,
108 GtkTreeModel *model,
109 GtkTreeIter *iter,
110 G_GNUC_UNUSED gpointer data)
111{
112 QModelIndex idx = gtk_q_sort_filter_tree_model_get_source_idx(GTK_Q_SORT_FILTER_TREE_MODEL(model), iter);
113
Stepan Salenikovichd8765072016-01-14 10:58:51 -0500114 std::shared_ptr<GdkPixbuf> image;
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400115 /* we only want to render a photo for the top nodes: Person, ContactMethod (, later Conference) */
116 QVariant object = idx.data(static_cast<int>(Ring::Role::Object));
117 if (idx.isValid() && object.isValid()) {
118 QVariant var_photo;
119 if (auto person = object.value<Person *>()) {
120 var_photo = GlobalInstances::pixmapManipulator().contactPhoto(person, QSize(50, 50), false);
121 } else if (auto cm = object.value<ContactMethod *>()) {
122 /* get photo, note that this should in all cases be the fallback avatar, since there
123 * shouldn't be a person associated with this contact method */
124 var_photo = GlobalInstances::pixmapManipulator().callPhoto(cm, QSize(50, 50), false);
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500125 } else if (auto call = object.value<Call *>()) {
126 if (call->type() == Call::Type::CONFERENCE) {
127 var_photo = GlobalInstances::pixmapManipulator().callPhoto(call, QSize(50, 50), false);
128 }
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400129 }
Stepan Salenikovichd8765072016-01-14 10:58:51 -0500130 if (var_photo.isValid()) {
131 std::shared_ptr<GdkPixbuf> photo = var_photo.value<std::shared_ptr<GdkPixbuf>>();
132
133 auto unread = idx.data(static_cast<int>(Ring::Role::UnreadTextMessageCount));
134
135 image.reset(ring_draw_unread_messages(photo.get(), unread.toInt()), g_object_unref);
136 } else {
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400137 // set the width of the cell rendered to the with of the photo
138 // so that the other renderers are shifted to the right
139 g_object_set(G_OBJECT(cell), "width", 50, NULL);
140 }
141 }
142
Stepan Salenikovichd8765072016-01-14 10:58:51 -0500143 g_object_set(G_OBJECT(cell), "pixbuf", image.get(), NULL);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400144}
145
146static void
147render_name_and_info(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
148 GtkCellRenderer *cell,
149 GtkTreeModel *model,
150 GtkTreeIter *iter,
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400151 GtkTreeView *treeview)
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400152{
153 gchar *text = NULL;
154
155 QModelIndex idx = gtk_q_sort_filter_tree_model_get_source_idx(GTK_Q_SORT_FILTER_TREE_MODEL(model), iter);
156
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400157 // check if this iter is selected
158 gboolean is_selected = FALSE;
159 if (GTK_IS_TREE_VIEW(treeview)) {
160 auto selection = gtk_tree_view_get_selection(treeview);
161 is_selected = gtk_tree_selection_iter_is_selected(selection, iter);
162 }
163
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400164 auto type = idx.data(static_cast<int>(Ring::Role::ObjectType));
165 if (idx.isValid() && type.isValid()) {
166 switch (type.value<Ring::ObjectType>()) {
167 case Ring::ObjectType::Person:
168 case Ring::ObjectType::ContactMethod:
169 {
170 auto var_name = idx.data(static_cast<int>(Ring::Role::Name));
171 auto var_lastused = idx.data(static_cast<int>(Ring::Role::LastUsed));
172 auto var_status = idx.data(static_cast<int>(Ring::Role::FormattedState));
173
174 QString name, status;
175
176 if (var_name.isValid())
177 name = var_name.value<QString>();
178
179 // show the status if there is a call, otherwise the last used date/time
180 if (var_status.isValid()) {
181 status += var_status.value<QString>();
182 }else if (var_lastused.isValid()) {
183 auto date_time = var_lastused.value<QDateTime>();
184 auto category = HistoryTimeCategoryModel::timeToHistoryConst(date_time.toTime_t());
185
186 // if it is 'today', then we only want to show the time
187 if (category != HistoryTimeCategoryModel::HistoryConst::Today) {
188 status += HistoryTimeCategoryModel::timeToHistoryCategory(date_time.toTime_t());
189 }
190 // we only want to show the time if it is less than a week ago
191 if (category < HistoryTimeCategoryModel::HistoryConst::A_week_ago) {
192 if (!status.isEmpty())
193 status += ", ";
194 status += QLocale::system().toString(date_time.time(), QLocale::ShortFormat);
195 }
196 }
197
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400198 /* we want the color of the status text to be the default color if this iter is
199 * selected so that the treeview is able to invert it against the selection color */
200 if (is_selected) {
201 text = g_strdup_printf("%s\n<span size=\"smaller\">%s</span>",
202 name.toUtf8().constData(),
203 status.toUtf8().constData());
204 } else {
205 text = g_strdup_printf("%s\n<span size=\"smaller\" color=\"gray\">%s</span>",
206 name.toUtf8().constData(),
207 status.toUtf8().constData());
208 }
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400209 }
210 break;
211 case Ring::ObjectType::Call:
212 {
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500213 // check if it is a conference
214 auto idx_source = RecentModel::instance().peopleProxy()->mapToSource(idx);
215 auto is_conference = RecentModel::instance().isConference(idx_source);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400216
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500217 if (is_conference) {
218 auto var_name = idx.data(static_cast<int>(Ring::Role::Name));
219 text = g_markup_printf_escaped("%s", var_name.value<QString>().toUtf8().constData());
220 } else {
221 auto parent_source = RecentModel::instance().peopleProxy()->mapToSource(idx.parent());
222 if (RecentModel::instance().isConference(parent_source)) {
223 // part of conference, simply display the name
224 auto var_name = idx.data(static_cast<int>(Ring::Role::Name));
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400225
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500226 /* we want the color of the name text to be the default color if this iter is
227 * selected so that the treeview is able to invert it against the selection color */
228 if (is_selected)
229 text = g_strdup_printf("<span size=\"smaller\">%s</span>", var_name.value<QString>().toUtf8().constData());
230 else
231 text = g_strdup_printf("<span size=\"smaller\" color=\"gray\">%s</span>", var_name.value<QString>().toUtf8().constData());
232 } else {
233 // just a call, so display the state
234 auto var_status = idx.data(static_cast<int>(Ring::Role::FormattedState));
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400235
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500236 QString status;
237
238 if (var_status.isValid())
239 status += var_status.value<QString>();
240
241 /* we want the color of the status text to be the default color if this iter is
242 * selected so that the treeview is able to invert it against the selection color */
243 if (is_selected)
244 text = g_strdup_printf("<span size=\"smaller\">%s</span>", status.toUtf8().constData());
245 else
246 text = g_strdup_printf("<span size=\"smaller\" color=\"gray\">%s</span>", status.toUtf8().constData());
247 }
248 }
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400249 }
250 break;
251 case Ring::ObjectType::Media:
252 // nothing to do for now
Stepan Salenikovich4e7ea712015-12-24 14:04:37 -0500253 case Ring::ObjectType::COUNT__:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400254 break;
255 }
256 }
257
258 g_object_set(G_OBJECT(cell), "markup", text, NULL);
259 g_free(text);
260}
261
262static void
263render_call_duration(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
264 GtkCellRenderer *cell,
265 GtkTreeModel *model,
266 GtkTreeIter *iter,
267 G_GNUC_UNUSED gpointer data)
268{
269 gchar *text = NULL;
270
271 QModelIndex idx = gtk_q_sort_filter_tree_model_get_source_idx(GTK_Q_SORT_FILTER_TREE_MODEL(model), iter);
272
273 auto type = idx.data(static_cast<int>(Ring::Role::ObjectType));
274 if (idx.isValid() && type.isValid()) {
275 switch (type.value<Ring::ObjectType>()) {
276 case Ring::ObjectType::Person:
277 case Ring::ObjectType::ContactMethod:
278 {
279 // check if there are any children (calls); we need to convert to source model in
280 // case there is only one
Guillaume Roguez5d1514b2015-10-22 15:55:31 -0400281 auto idx_source = RecentModel::instance().peopleProxy()->mapToSource(idx);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400282 auto duration = idx.data(static_cast<int>(Ring::Role::Length));
283 if (idx_source.isValid()
284 && (idx_source.model()->rowCount(idx_source) == 1)
285 && duration.isValid())
286 {
287 text = g_strdup_printf("%s", duration.value<QString>().toUtf8().constData());
288 }
289 }
290 break;
291 case Ring::ObjectType::Call:
292 {
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500293 // do not display the duration if the call is part of a conference
294 auto parent_source = RecentModel::instance().peopleProxy()->mapToSource(idx.parent());
295 auto in_conference = RecentModel::instance().isConference(parent_source);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400296
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500297 if (!in_conference) {
298 auto duration = idx.data(static_cast<int>(Ring::Role::Length));
299
300 if (duration.isValid())
301 text = g_strdup_printf("%s", duration.value<QString>().toUtf8().constData());
302 }
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400303 }
304 break;
305 case Ring::ObjectType::Media:
306 // nothing to do for now
Stepan Salenikovich4e7ea712015-12-24 14:04:37 -0500307 case Ring::ObjectType::COUNT__:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400308 break;
309 }
310 }
311
312 g_object_set(G_OBJECT(cell), "markup", text, NULL);
313 g_free(text);
314}
315
316static void
317activate_item(GtkTreeView *tree_view,
318 GtkTreePath *path,
319 G_GNUC_UNUSED GtkTreeViewColumn *column,
320 G_GNUC_UNUSED gpointer user_data)
321{
322 auto model = gtk_tree_view_get_model(tree_view);
323 GtkTreeIter iter;
324 if (gtk_tree_model_get_iter(model, &iter, path)) {
325 auto idx = gtk_q_sort_filter_tree_model_get_source_idx(GTK_Q_SORT_FILTER_TREE_MODEL(model), &iter);
326 auto type = idx.data(static_cast<int>(Ring::Role::ObjectType));
327 if (idx.isValid() && type.isValid()) {
328 switch (type.value<Ring::ObjectType>()) {
329 case Ring::ObjectType::Person:
330 {
331 // call the last used contact method
332 // TODO: if no contact methods have been used, offer a popup to Choose
333 auto p_var = idx.data(static_cast<int>(Ring::Role::Object));
334 if (p_var.isValid()) {
335 auto person = p_var.value<Person *>();
336 auto cms = person->phoneNumbers();
337
338 if (!cms.isEmpty()) {
339 auto last_used_cm = cms.at(0);
340 for (int i = 1; i < cms.size(); ++i) {
341 auto new_cm = cms.at(i);
342 if (difftime(new_cm->lastUsed(), last_used_cm->lastUsed()) > 0)
343 last_used_cm = new_cm;
344 }
345
346 place_new_call(last_used_cm);
347 }
348 }
349 }
350 break;
351 case Ring::ObjectType::ContactMethod:
352 {
353 // call the contact method
354 auto cm = idx.data(static_cast<int>(Ring::Role::Object));
355 if (cm.isValid())
356 place_new_call(cm.value<ContactMethod *>());
357 }
358 break;
359 case Ring::ObjectType::Call:
360 case Ring::ObjectType::Media:
361 // nothing to do for now
Stepan Salenikovich4e7ea712015-12-24 14:04:37 -0500362 case Ring::ObjectType::COUNT__:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400363 break;
364 }
365 }
366 }
367}
368
369static gboolean
370create_popup_menu(GtkTreeView *treeview, GdkEventButton *event, G_GNUC_UNUSED gpointer user_data)
371{
372 /* build popup menu when right clicking on contact item
373 * user should be able to copy the contact's name or "number".
374 * or add the "number" to his contact list, if not already so
375 */
376
377 /* check for right click */
378 if (event->button != BUTTON_RIGHT_CLICK || event->type != GDK_BUTTON_PRESS)
379 return FALSE;
380
381 GtkTreeIter iter;
382 GtkTreeModel *model;
383 GtkTreeSelection *selection = gtk_tree_view_get_selection(treeview);
384 if (!gtk_tree_selection_get_selected(selection, &model, &iter))
385 return FALSE;
386
387 GtkWidget *menu = gtk_menu_new();
388 QModelIndex idx = gtk_q_sort_filter_tree_model_get_source_idx(GTK_Q_SORT_FILTER_TREE_MODEL(model), &iter);
389
390 /* if Person or CM, give option to call */
391 auto type = idx.data(static_cast<int>(Ring::Role::ObjectType));
392 auto object = idx.data(static_cast<int>(Ring::Role::Object));
393 if (type.isValid() && object.isValid()) {
394 switch (type.value<Ring::ObjectType>()) {
395 case Ring::ObjectType::Person:
396 {
397 /* possiblity for multiple numbers */
398 auto cms = object.value<Person *>()->phoneNumbers();
399 if (cms.size() == 1) {
400 auto item = gtk_menu_item_new_with_mnemonic(_("_Call"));
401 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
402 g_signal_connect(item,
403 "activate",
404 G_CALLBACK(call_contactmethod),
405 cms.at(0));
406 } else if (cms.size() > 1) {
407 auto call_item = gtk_menu_item_new_with_mnemonic(_("_Call"));
408 gtk_menu_shell_append(GTK_MENU_SHELL(menu), call_item);
409 auto call_menu = gtk_menu_new();
410 gtk_menu_item_set_submenu(GTK_MENU_ITEM(call_item), call_menu);
411 for (int i = 0; i < cms.size(); ++i) {
Stepan Salenikovich5039c9b2016-02-12 14:09:51 -0500412 gchar *number = nullptr;
413 if (cms.at(i)->category()) {
414 // try to get the number category, eg: "home"
415 number = g_strdup_printf("(%s) %s", cms.at(i)->category()->name().toUtf8().constData(),
416 cms.at(i)->uri().toUtf8().constData());
417 } else {
418 number = g_strdup_printf("%s", cms.at(i)->uri().toUtf8().constData());
419 }
420 auto item = gtk_menu_item_new_with_label(number);
421 g_free(number);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400422 gtk_menu_shell_append(GTK_MENU_SHELL(call_menu), item);
423 g_signal_connect(item,
424 "activate",
425 G_CALLBACK(call_contactmethod),
426 cms.at(i));
427 }
428 }
429 }
430 break;
431 case Ring::ObjectType::ContactMethod:
432 {
433 auto cm = object.value<ContactMethod *>();
434 auto item = gtk_menu_item_new_with_mnemonic(_("_Call"));
435 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
436 g_signal_connect(item,
437 "activate",
438 G_CALLBACK(call_contactmethod),
439 cm);
440 }
441 break;
442 case Ring::ObjectType::Call:
443 case Ring::ObjectType::Media:
444 // nothing to do for now
Stepan Salenikovich4e7ea712015-12-24 14:04:37 -0500445 case Ring::ObjectType::COUNT__:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400446 break;
447 }
448 }
449
450 /* copy name */
451 QVariant name_var = idx.data(static_cast<int>(Ring::Role::Name));
452 if (name_var.isValid()) {
453 gchar *name = g_strdup_printf("%s", name_var.value<QString>().toUtf8().constData());
454 GtkWidget *item = gtk_menu_item_new_with_mnemonic(_("_Copy name"));
455 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
456 g_object_set_data_full(G_OBJECT(item), COPY_DATA_KEY, name, (GDestroyNotify)g_free);
457 g_signal_connect(item,
458 "activate",
459 G_CALLBACK(copy_contact_info),
460 NULL);
461 }
462
463 /* copy number(s) */
464 if (type.isValid() && object.isValid()) {
465 switch (type.value<Ring::ObjectType>()) {
466 case Ring::ObjectType::Person:
467 {
468 /* possiblity for multiple numbers */
469 auto cms = object.value<Person *>()->phoneNumbers();
470 if (cms.size() == 1) {
471 gchar *number = g_strdup_printf("%s",cms.at(0)->uri().toUtf8().constData());
472 auto item = gtk_menu_item_new_with_mnemonic(_("_Copy number"));
473 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
474 g_object_set_data_full(G_OBJECT(item), COPY_DATA_KEY, number, (GDestroyNotify)g_free);
475 g_signal_connect(item,
476 "activate",
477 G_CALLBACK(copy_contact_info),
478 NULL);
479 } else if (cms.size() > 1) {
480 auto copy_item = gtk_menu_item_new_with_mnemonic(_("_Copy number"));
481 gtk_menu_shell_append(GTK_MENU_SHELL(menu), copy_item);
482 auto copy_menu = gtk_menu_new();
483 gtk_menu_item_set_submenu(GTK_MENU_ITEM(copy_item), copy_menu);
484 for (int i = 0; i < cms.size(); ++i) {
Stepan Salenikovich5039c9b2016-02-12 14:09:51 -0500485 gchar *number = nullptr;
486 if (cms.at(i)->category()) {
487 // try to get the number category, eg: "home"
488 number = g_strdup_printf("(%s) %s", cms.at(i)->category()->name().toUtf8().constData(),
489 cms.at(i)->uri().toUtf8().constData());
490 } else {
491 number = g_strdup_printf("%s", cms.at(i)->uri().toUtf8().constData());
492 }
493 auto item = gtk_menu_item_new_with_label(number);
494 g_free(number);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400495 gtk_menu_shell_append(GTK_MENU_SHELL(copy_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 }
502 }
503 }
504 break;
505 case Ring::ObjectType::ContactMethod:
506 case Ring::ObjectType::Call:
507 case Ring::ObjectType::Media:
Stepan Salenikovich4e7ea712015-12-24 14:04:37 -0500508 {
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400509 QVariant number_var = idx.data(static_cast<int>(Ring::Role::Number));
510 if (number_var.isValid()) {
511 gchar *number = g_strdup_printf("%s", number_var.value<QString>().toUtf8().constData());
512 GtkWidget *item = gtk_menu_item_new_with_mnemonic(_("_Copy number"));
513 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
514 g_object_set_data_full(G_OBJECT(item), COPY_DATA_KEY, number, (GDestroyNotify)g_free);
515 g_signal_connect(item,
516 "activate",
517 G_CALLBACK(copy_contact_info),
518 NULL);
519 }
Stepan Salenikovich4e7ea712015-12-24 14:04:37 -0500520 }
521 break;
522 case Ring::ObjectType::COUNT__:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400523 break;
524 }
525 }
526
527 /* if the object is a CM, then give the option to add to contacts*/
528 if (type.isValid() && object.isValid()) {
529 if (type.value<Ring::ObjectType>() == Ring::ObjectType::ContactMethod) {
530 /* get rectangle */
531 auto path = gtk_tree_model_get_path(model, &iter);
532 auto column = gtk_tree_view_get_column(treeview, 0);
533 GdkRectangle rect;
534 gtk_tree_view_get_cell_area(treeview, path, column, &rect);
535 gtk_tree_view_convert_bin_window_to_widget_coords(treeview, rect.x, rect.y, &rect.x, &rect.y);
536 gtk_tree_path_free(path);
537 auto add_to = menu_item_add_to_contact(object.value<ContactMethod *>(), GTK_WIDGET(treeview), &rect);
538 gtk_menu_shell_append(GTK_MENU_SHELL(menu), add_to);
539 }
540 }
541
542 /* show menu */
543 gtk_widget_show_all(menu);
544 gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, event->button, event->time);
545
546 return TRUE; /* we handled the event */
547}
548
549static void
550expand_if_child(G_GNUC_UNUSED GtkTreeModel *tree_model,
551 GtkTreePath *path,
552 G_GNUC_UNUSED GtkTreeIter *iter,
553 GtkTreeView *treeview)
554{
555 if (gtk_tree_path_get_depth(path) > 1)
556 gtk_tree_view_expand_to_path(treeview, path);
557}
558
559static void
Stepan Salenikovichec0862f2015-10-16 15:49:42 -0400560scroll_to_selection(GtkTreeSelection *selection)
561{
562 GtkTreeModel *model = nullptr;
563 GtkTreeIter iter;
564 if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
565 auto path = gtk_tree_model_get_path(model, &iter);
566 auto treeview = gtk_tree_selection_get_tree_view(selection);
567 gtk_tree_view_scroll_to_cell(treeview, path, nullptr, FALSE, 0.0, 0.0);
568 }
569}
570
571static void
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500572on_drag_data_get(GtkWidget *treeview,
573 G_GNUC_UNUSED GdkDragContext *context,
574 GtkSelectionData *data,
575 G_GNUC_UNUSED guint info,
576 G_GNUC_UNUSED guint time,
577 G_GNUC_UNUSED gpointer user_data)
578{
579 g_return_if_fail(IS_RECENT_CONTACTS_VIEW(treeview));
580
581 /* we always drag the selected row */
582 auto selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(treeview));
583 GtkTreeModel *model = NULL;
584 GtkTreeIter iter;
585
586 if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
587 auto path_str = gtk_tree_model_get_string_from_iter(model, &iter);
588
589 gtk_selection_data_set(data,
590 gdk_atom_intern_static_string(CALL_TARGET),
591 8, /* bytes */
592 (guchar *)path_str,
593 strlen(path_str) + 1);
594
595 g_free(path_str);
596 } else {
597 g_warning("drag selection not valid");
598 }
599}
600
601static gboolean
602on_drag_drop(GtkWidget *treeview,
603 GdkDragContext *context,
604 gint x,
605 gint y,
606 guint time,
607 G_GNUC_UNUSED gpointer user_data)
608{
609 g_return_val_if_fail(IS_RECENT_CONTACTS_VIEW(treeview), FALSE);
610
611 GtkTreePath *path = NULL;
612 GtkTreeViewDropPosition drop_pos;
613
614 if (gtk_tree_view_get_dest_row_at_pos(GTK_TREE_VIEW(treeview),
615 x, y, &path, &drop_pos)) {
616
617 GdkAtom target_type = gtk_drag_dest_find_target(treeview, context, NULL);
618
619 if (target_type != GDK_NONE) {
620 g_debug("can drop");
621 gtk_drag_get_data(treeview, context, target_type, time);
622 return TRUE;
623 }
624
625 gtk_tree_path_free(path);
626 }
627
628 return FALSE;
629}
630
631static gboolean
632on_drag_motion(GtkWidget *treeview,
633 GdkDragContext *context,
634 gint x,
635 gint y,
636 guint time,
637 G_GNUC_UNUSED gpointer user_data)
638{
639 g_return_val_if_fail(IS_RECENT_CONTACTS_VIEW(treeview), FALSE);
640
641 GtkTreePath *path = NULL;
642 GtkTreeViewDropPosition drop_pos;
643
644 if (gtk_tree_view_get_dest_row_at_pos(GTK_TREE_VIEW(treeview),
645 x, y, &path, &drop_pos)) {
646 // we only want to drop on a row, not before or after
647 if (drop_pos == GTK_TREE_VIEW_DROP_BEFORE) {
648 gtk_tree_view_set_drag_dest_row(GTK_TREE_VIEW(treeview), path, GTK_TREE_VIEW_DROP_INTO_OR_BEFORE);
649 } else if (drop_pos == GTK_TREE_VIEW_DROP_AFTER) {
650 gtk_tree_view_set_drag_dest_row(GTK_TREE_VIEW(treeview), path, GTK_TREE_VIEW_DROP_INTO_OR_AFTER);
651 }
652 gdk_drag_status(context, gdk_drag_context_get_suggested_action(context), time);
653 return TRUE;
654 } else {
655 // not a row in the treeview, so we cannot drop
656 return FALSE;
657 }
658}
659
660static void
661on_drag_data_received(GtkWidget *treeview,
662 GdkDragContext *context,
663 gint x,
664 gint y,
665 GtkSelectionData *data,
666 G_GNUC_UNUSED guint info,
667 guint time,
668 G_GNUC_UNUSED gpointer user_data)
669{
670 g_return_if_fail(IS_RECENT_CONTACTS_VIEW(treeview));
671
672 gboolean success = FALSE;
673
674 /* get the source and destination calls */
675 auto path_str_source = (gchar *)gtk_selection_data_get_data(data);
676 auto type = gtk_selection_data_get_data_type(data);
677 g_debug("data type: %s", gdk_atom_name(type));
678 if (path_str_source && strlen(path_str_source) > 0) {
679 g_debug("source path: %s", path_str_source);
680
681 /* get the destination path */
682 GtkTreePath *dest_path = NULL;
683 if (gtk_tree_view_get_dest_row_at_pos(GTK_TREE_VIEW(treeview), x, y, &dest_path, NULL)) {
684 auto model = gtk_tree_view_get_model(GTK_TREE_VIEW(treeview));
685
686 GtkTreeIter source;
687 gtk_tree_model_get_iter_from_string(model, &source, path_str_source);
688 auto idx_source_proxy = gtk_q_sort_filter_tree_model_get_source_idx(GTK_Q_SORT_FILTER_TREE_MODEL(model), &source);
689
690 GtkTreeIter dest;
691 gtk_tree_model_get_iter(model, &dest, dest_path);
692 auto idx_dest_proxy = gtk_q_sort_filter_tree_model_get_source_idx(GTK_Q_SORT_FILTER_TREE_MODEL(model), &dest);
693
694 // get call objects and indeces from RecentModel indeces being drag and dropped
695 auto idx_source = RecentModel::instance().peopleProxy()->mapToSource(idx_source_proxy);
696 auto idx_dest = RecentModel::instance().peopleProxy()->mapToSource(idx_dest_proxy);
697 auto call_source = RecentModel::instance().getActiveCall(idx_source);
698 auto call_dest = RecentModel::instance().getActiveCall(idx_dest);
699 auto idx_call_source = CallModel::instance().getIndex(call_source);
700 auto idx_call_dest = CallModel::instance().getIndex(call_dest);
701
702 if (idx_call_source.isValid() && idx_call_dest.isValid()) {
703 QModelIndexList source_list;
704 source_list << idx_call_source;
705 auto mimeData = CallModel::instance().mimeData(source_list);
706 auto action = Call::DropAction::Conference;
707 mimeData->setProperty("dropAction", action);
708
709 if (CallModel::instance().dropMimeData(mimeData, Qt::MoveAction, idx_call_dest.row(), idx_call_dest.column(), idx_call_dest.parent())) {
710 success = TRUE;
711 } else {
712 g_warning("could not drop mime data");
713 }
714 } else {
715 g_warning("source or dest call not valid");
716 }
717
718 gtk_tree_path_free(dest_path);
719 }
720 }
721
722 gtk_drag_finish(context, success, FALSE, time);
723}
724
725static void
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400726recent_contacts_view_init(RecentContactsView *self)
727{
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400728 RecentContactsViewPrivate *priv = RECENT_CONTACTS_VIEW_GET_PRIVATE(self);
729
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400730 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(self), FALSE);
731 /* no need to show the expander since it will always be expanded */
732 gtk_tree_view_set_show_expanders(GTK_TREE_VIEW(self), FALSE);
733 /* disable default search, we will handle it ourselves via LRC;
734 * otherwise the search steals input focus on key presses */
735 gtk_tree_view_set_enable_search(GTK_TREE_VIEW(self), FALSE);
736
737 GtkQSortFilterTreeModel *recent_model = gtk_q_sort_filter_tree_model_new(
Guillaume Roguez5d1514b2015-10-22 15:55:31 -0400738 RecentModel::instance().peopleProxy(),
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400739 1,
740 Qt::DisplayRole, G_TYPE_STRING);
741
742 gtk_tree_view_set_model(GTK_TREE_VIEW(self),
743 GTK_TREE_MODEL(recent_model));
744
745 /* photo and name/contact method column */
746 GtkCellArea *area = gtk_cell_area_box_new();
747 GtkTreeViewColumn *column = gtk_tree_view_column_new_with_area(area);
748
749 /* photo renderer */
750 GtkCellRenderer *renderer = gtk_cell_renderer_pixbuf_new();
751 gtk_cell_area_box_pack_start(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
752
753 /* get the photo */
754 gtk_tree_view_column_set_cell_data_func(
755 column,
756 renderer,
757 (GtkTreeCellDataFunc)render_contact_photo,
758 NULL,
759 NULL);
760
761 /* name/cm and status renderer */
762 renderer = gtk_cell_renderer_text_new();
763 g_object_set(G_OBJECT(renderer), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
764 gtk_cell_area_box_pack_start(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
765
766 gtk_tree_view_column_set_cell_data_func(
767 column,
768 renderer,
769 (GtkTreeCellDataFunc)render_name_and_info,
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400770 self,
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400771 NULL);
772
Stepan Salenikoviche8ef9352015-10-27 19:07:46 -0400773 /* call duration */
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400774 renderer = gtk_cell_renderer_text_new();
Stepan Salenikoviche8ef9352015-10-27 19:07:46 -0400775 g_object_set(G_OBJECT(renderer), "xalign", 1.0, NULL);
776 gtk_cell_area_box_pack_end(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400777 gtk_tree_view_column_set_cell_data_func(
778 column,
779 renderer,
780 (GtkTreeCellDataFunc)render_call_duration,
781 NULL,
782 NULL);
783
Stepan Salenikoviche8ef9352015-10-27 19:07:46 -0400784 gtk_tree_view_append_column(GTK_TREE_VIEW(self), column);
785 gtk_tree_view_column_set_resizable(column, TRUE);
786 gtk_tree_view_column_set_expand(column, TRUE);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400787 gtk_tree_view_expand_all(GTK_TREE_VIEW(self));
788
789 g_signal_connect(self, "button-press-event", G_CALLBACK(create_popup_menu), NULL);
790 g_signal_connect(self, "row-activated", G_CALLBACK(activate_item), NULL);
791 g_signal_connect(recent_model, "row-inserted", G_CALLBACK(expand_if_child), self);
792
793 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(self));
Stepan Salenikovichc1323422016-01-06 10:54:44 -0500794 g_signal_connect(selection, "changed", G_CALLBACK(update_selection), NULL);
Stepan Salenikovichec0862f2015-10-16 15:49:42 -0400795 g_signal_connect(selection, "changed", G_CALLBACK(scroll_to_selection), NULL);
796 g_signal_connect_swapped(recent_model, "rows-reordered", G_CALLBACK(scroll_to_selection), selection);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400797
Stepan Salenikovichc1323422016-01-06 10:54:44 -0500798 /* update the selection based on the RecentModel */
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400799 priv->selection_updated = QObject::connect(
Stepan Salenikovichc1323422016-01-06 10:54:44 -0500800 RecentModel::instance().selectionModel(),
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400801 &QItemSelectionModel::currentChanged,
802 [self, recent_model](const QModelIndex current, G_GNUC_UNUSED const QModelIndex & previous) {
Stepan Salenikovichc1323422016-01-06 10:54:44 -0500803 auto selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(self));
804
805 auto current_proxy = RecentModel::instance().peopleProxy()->mapFromSource(current);
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400806
Stepan Salenikovich8043a562016-03-18 13:56:40 -0400807 if (current.isValid()) {
808 /* select the current */
809 GtkTreeIter new_iter;
810 if (gtk_q_sort_filter_tree_model_source_index_to_iter(recent_model, current_proxy, &new_iter)) {
811 gtk_tree_selection_select_iter(selection, &new_iter);
812 }
813 } else {
814 gtk_tree_selection_unselect_all(selection);
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400815 }
816 }
817 );
818
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500819 /* drag and drop */
820 static GtkTargetEntry targetentries[] = {
821 { (gchar *)CALL_TARGET, GTK_TARGET_SAME_WIDGET, CALL_TARGET_ID },
822 };
823
824 gtk_tree_view_enable_model_drag_source(GTK_TREE_VIEW(self),
825 GDK_BUTTON1_MASK, targetentries, 1, (GdkDragAction)(GDK_ACTION_DEFAULT | GDK_ACTION_MOVE));
826
827 gtk_tree_view_enable_model_drag_dest(GTK_TREE_VIEW(self),
828 targetentries, 1, GDK_ACTION_DEFAULT);
829
830 g_signal_connect(self, "drag-data-get", G_CALLBACK(on_drag_data_get), nullptr);
831 g_signal_connect(self, "drag-drop", G_CALLBACK(on_drag_drop), nullptr);
832 g_signal_connect(self, "drag-motion", G_CALLBACK(on_drag_motion), nullptr);
833 g_signal_connect(self, "drag_data_received", G_CALLBACK(on_drag_data_received), nullptr);
834
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400835 gtk_widget_show_all(GTK_WIDGET(self));
836}
837
838static void
839recent_contacts_view_dispose(GObject *object)
840{
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400841 RecentContactsView *self = RECENT_CONTACTS_VIEW(object);
842 RecentContactsViewPrivate *priv = RECENT_CONTACTS_VIEW_GET_PRIVATE(self);
843
844 QObject::disconnect(priv->selection_updated);
845
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400846 G_OBJECT_CLASS(recent_contacts_view_parent_class)->dispose(object);
847}
848
849static void
850recent_contacts_view_finalize(GObject *object)
851{
852 G_OBJECT_CLASS(recent_contacts_view_parent_class)->finalize(object);
853}
854
855static void
856recent_contacts_view_class_init(RecentContactsViewClass *klass)
857{
858 G_OBJECT_CLASS(klass)->finalize = recent_contacts_view_finalize;
859 G_OBJECT_CLASS(klass)->dispose = recent_contacts_view_dispose;
860}
861
862GtkWidget *
863recent_contacts_view_new()
864{
865 gpointer self = g_object_new(RECENT_CONTACTS_VIEW_TYPE, NULL);
866
867 return (GtkWidget *)self;
868}