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