blob: 76ce5d4a30ca86b0e38a6c762585afee044bc25f [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>
Stepan Salenikovichf6078222016-10-03 17:31:16 -040024#include "models/gtkqtreemodel.h"
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040025#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 Salenikovich8eaa13e2016-08-26 16:51:48 -040042#include <numbercategory.h>
43#include "contactpopupmenu.h"
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040044
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -050045static constexpr const char* CALL_TARGET = "CALL_TARGET";
46static constexpr int CALL_TARGET_ID = 0;
47
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040048struct _RecentContactsView
49{
50 GtkTreeView parent;
51};
52
53struct _RecentContactsViewClass
54{
55 GtkTreeViewClass parent_class;
56};
57
58typedef struct _RecentContactsViewPrivate RecentContactsViewPrivate;
59
60struct _RecentContactsViewPrivate
61{
Stepan Salenikovich8eaa13e2016-08-26 16:51:48 -040062 GtkWidget *popup_menu;
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -040063
64 QMetaObject::Connection selection_updated;
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040065};
66
67G_DEFINE_TYPE_WITH_PRIVATE(RecentContactsView, recent_contacts_view, GTK_TYPE_TREE_VIEW);
68
69#define RECENT_CONTACTS_VIEW_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), RECENT_CONTACTS_VIEW_TYPE, RecentContactsViewPrivate))
70
71static void
Stepan Salenikovichc1323422016-01-06 10:54:44 -050072update_selection(GtkTreeSelection *selection, G_GNUC_UNUSED gpointer user_data)
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040073{
74 auto current_proxy = get_index_from_selection(selection);
Guillaume Roguez5d1514b2015-10-22 15:55:31 -040075 auto current = RecentModel::instance().peopleProxy()->mapToSource(current_proxy);
Stepan Salenikovichc1323422016-01-06 10:54:44 -050076
77 RecentModel::instance().selectionModel()->setCurrentIndex(current, QItemSelectionModel::ClearAndSelect);
78
79 // update the CallModel selection since we rely on the UserActionModel
Guillaume Roguez5d1514b2015-10-22 15:55:31 -040080 if (auto call_to_select = RecentModel::instance().getActiveCall(current)) {
81 CallModel::instance().selectCall(call_to_select);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040082 } else {
Guillaume Roguez5d1514b2015-10-22 15:55:31 -040083 CallModel::instance().selectionModel()->clearCurrentIndex();
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040084 }
85}
86
87static void
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040088render_contact_photo(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
89 GtkCellRenderer *cell,
90 GtkTreeModel *model,
91 GtkTreeIter *iter,
92 G_GNUC_UNUSED gpointer data)
93{
Stepan Salenikovichf6078222016-10-03 17:31:16 -040094 QModelIndex idx = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(model), iter);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040095
Stepan Salenikovichd8765072016-01-14 10:58:51 -050096 std::shared_ptr<GdkPixbuf> image;
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040097 /* we only want to render a photo for the top nodes: Person, ContactMethod (, later Conference) */
98 QVariant object = idx.data(static_cast<int>(Ring::Role::Object));
99 if (idx.isValid() && object.isValid()) {
100 QVariant var_photo;
101 if (auto person = object.value<Person *>()) {
102 var_photo = GlobalInstances::pixmapManipulator().contactPhoto(person, QSize(50, 50), false);
103 } else if (auto cm = object.value<ContactMethod *>()) {
104 /* get photo, note that this should in all cases be the fallback avatar, since there
105 * shouldn't be a person associated with this contact method */
106 var_photo = GlobalInstances::pixmapManipulator().callPhoto(cm, QSize(50, 50), false);
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500107 } else if (auto call = object.value<Call *>()) {
108 if (call->type() == Call::Type::CONFERENCE) {
109 var_photo = GlobalInstances::pixmapManipulator().callPhoto(call, QSize(50, 50), false);
110 }
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400111 }
Stepan Salenikovichd8765072016-01-14 10:58:51 -0500112 if (var_photo.isValid()) {
113 std::shared_ptr<GdkPixbuf> photo = var_photo.value<std::shared_ptr<GdkPixbuf>>();
114
115 auto unread = idx.data(static_cast<int>(Ring::Role::UnreadTextMessageCount));
116
117 image.reset(ring_draw_unread_messages(photo.get(), unread.toInt()), g_object_unref);
118 } else {
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400119 // set the width of the cell rendered to the with of the photo
120 // so that the other renderers are shifted to the right
121 g_object_set(G_OBJECT(cell), "width", 50, NULL);
122 }
123 }
124
Stepan Salenikovichd8765072016-01-14 10:58:51 -0500125 g_object_set(G_OBJECT(cell), "pixbuf", image.get(), NULL);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400126}
127
128static void
129render_name_and_info(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
130 GtkCellRenderer *cell,
131 GtkTreeModel *model,
132 GtkTreeIter *iter,
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400133 GtkTreeView *treeview)
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400134{
135 gchar *text = NULL;
136
Stepan Salenikovichf6078222016-10-03 17:31:16 -0400137 QModelIndex idx = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(model), iter);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400138
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400139 // check if this iter is selected
140 gboolean is_selected = FALSE;
141 if (GTK_IS_TREE_VIEW(treeview)) {
142 auto selection = gtk_tree_view_get_selection(treeview);
143 is_selected = gtk_tree_selection_iter_is_selected(selection, iter);
144 }
145
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400146 auto type = idx.data(static_cast<int>(Ring::Role::ObjectType));
147 if (idx.isValid() && type.isValid()) {
148 switch (type.value<Ring::ObjectType>()) {
149 case Ring::ObjectType::Person:
150 case Ring::ObjectType::ContactMethod:
151 {
152 auto var_name = idx.data(static_cast<int>(Ring::Role::Name));
153 auto var_lastused = idx.data(static_cast<int>(Ring::Role::LastUsed));
154 auto var_status = idx.data(static_cast<int>(Ring::Role::FormattedState));
155
156 QString name, status;
157
158 if (var_name.isValid())
159 name = var_name.value<QString>();
160
161 // show the status if there is a call, otherwise the last used date/time
162 if (var_status.isValid()) {
163 status += var_status.value<QString>();
164 }else if (var_lastused.isValid()) {
165 auto date_time = var_lastused.value<QDateTime>();
166 auto category = HistoryTimeCategoryModel::timeToHistoryConst(date_time.toTime_t());
167
168 // if it is 'today', then we only want to show the time
169 if (category != HistoryTimeCategoryModel::HistoryConst::Today) {
170 status += HistoryTimeCategoryModel::timeToHistoryCategory(date_time.toTime_t());
171 }
172 // we only want to show the time if it is less than a week ago
173 if (category < HistoryTimeCategoryModel::HistoryConst::A_week_ago) {
174 if (!status.isEmpty())
175 status += ", ";
176 status += QLocale::system().toString(date_time.time(), QLocale::ShortFormat);
177 }
178 }
179
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400180 /* we want the color of the status text to be the default color if this iter is
181 * selected so that the treeview is able to invert it against the selection color */
182 if (is_selected) {
Stepan Salenikovich0731de02016-05-17 17:47:16 -0400183 text = g_markup_printf_escaped("%s\n<span size=\"smaller\">%s</span>",
184 name.toUtf8().constData(),
185 status.toUtf8().constData());
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400186 } else {
Stepan Salenikovich0731de02016-05-17 17:47:16 -0400187 text = g_markup_printf_escaped("%s\n<span size=\"smaller\" color=\"gray\">%s</span>",
188 name.toUtf8().constData(),
189 status.toUtf8().constData());
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400190 }
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400191 }
192 break;
193 case Ring::ObjectType::Call:
194 {
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500195 // check if it is a conference
196 auto idx_source = RecentModel::instance().peopleProxy()->mapToSource(idx);
197 auto is_conference = RecentModel::instance().isConference(idx_source);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400198
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500199 if (is_conference) {
200 auto var_name = idx.data(static_cast<int>(Ring::Role::Name));
Stepan Salenikovich0731de02016-05-17 17:47:16 -0400201 text = g_markup_escape_text(var_name.value<QString>().toUtf8().constData(), -1);
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500202 } else {
203 auto parent_source = RecentModel::instance().peopleProxy()->mapToSource(idx.parent());
204 if (RecentModel::instance().isConference(parent_source)) {
205 // part of conference, simply display the name
206 auto var_name = idx.data(static_cast<int>(Ring::Role::Name));
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400207
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500208 /* we want the color of the name text to be the default color if this iter is
209 * selected so that the treeview is able to invert it against the selection color */
Stepan Salenikovich0731de02016-05-17 17:47:16 -0400210 if (is_selected) {
211 text = g_markup_printf_escaped("<span size=\"smaller\">%s</span>",
212 var_name.value<QString>().toUtf8().constData());
213 } else {
214 text = g_markup_printf_escaped("<span size=\"smaller\" color=\"gray\">%s</span>",
215 var_name.value<QString>().toUtf8().constData());
216 }
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500217 } else {
218 // just a call, so display the state
219 auto var_status = idx.data(static_cast<int>(Ring::Role::FormattedState));
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400220
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500221 QString status;
222
223 if (var_status.isValid())
224 status += var_status.value<QString>();
225
226 /* we want the color of the status 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 */
Stepan Salenikovich0731de02016-05-17 17:47:16 -0400228 if (is_selected) {
229 text = g_markup_printf_escaped("<span size=\"smaller\">%s</span>",
230 status.toUtf8().constData());
231 } else {
232 text = g_markup_printf_escaped("<span size=\"smaller\" color=\"gray\">%s</span>",
233 status.toUtf8().constData());
234 }
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500235 }
236 }
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400237 }
238 break;
239 case Ring::ObjectType::Media:
240 // nothing to do for now
Stepan Salenikovich4e7ea712015-12-24 14:04:37 -0500241 case Ring::ObjectType::COUNT__:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400242 break;
243 }
244 }
245
246 g_object_set(G_OBJECT(cell), "markup", text, NULL);
247 g_free(text);
248}
249
250static void
251render_call_duration(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
252 GtkCellRenderer *cell,
253 GtkTreeModel *model,
254 GtkTreeIter *iter,
255 G_GNUC_UNUSED gpointer data)
256{
257 gchar *text = NULL;
258
Stepan Salenikovichf6078222016-10-03 17:31:16 -0400259 QModelIndex idx = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(model), iter);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400260
261 auto type = idx.data(static_cast<int>(Ring::Role::ObjectType));
262 if (idx.isValid() && type.isValid()) {
263 switch (type.value<Ring::ObjectType>()) {
264 case Ring::ObjectType::Person:
265 case Ring::ObjectType::ContactMethod:
266 {
267 // check if there are any children (calls); we need to convert to source model in
268 // case there is only one
Guillaume Roguez5d1514b2015-10-22 15:55:31 -0400269 auto idx_source = RecentModel::instance().peopleProxy()->mapToSource(idx);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400270 auto duration = idx.data(static_cast<int>(Ring::Role::Length));
271 if (idx_source.isValid()
272 && (idx_source.model()->rowCount(idx_source) == 1)
273 && duration.isValid())
274 {
Stepan Salenikovich0731de02016-05-17 17:47:16 -0400275 text = g_markup_escape_text(duration.value<QString>().toUtf8().constData(), -1);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400276 }
277 }
278 break;
279 case Ring::ObjectType::Call:
280 {
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500281 // do not display the duration if the call is part of a conference
282 auto parent_source = RecentModel::instance().peopleProxy()->mapToSource(idx.parent());
283 auto in_conference = RecentModel::instance().isConference(parent_source);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400284
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500285 if (!in_conference) {
286 auto duration = idx.data(static_cast<int>(Ring::Role::Length));
287
288 if (duration.isValid())
Stepan Salenikovich0731de02016-05-17 17:47:16 -0400289 text = g_markup_escape_text(duration.value<QString>().toUtf8().constData(), -1);
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500290 }
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400291 }
292 break;
293 case Ring::ObjectType::Media:
294 // nothing to do for now
Stepan Salenikovich4e7ea712015-12-24 14:04:37 -0500295 case Ring::ObjectType::COUNT__:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400296 break;
297 }
298 }
299
300 g_object_set(G_OBJECT(cell), "markup", text, NULL);
301 g_free(text);
302}
303
304static void
305activate_item(GtkTreeView *tree_view,
306 GtkTreePath *path,
307 G_GNUC_UNUSED GtkTreeViewColumn *column,
308 G_GNUC_UNUSED gpointer user_data)
309{
310 auto model = gtk_tree_view_get_model(tree_view);
311 GtkTreeIter iter;
312 if (gtk_tree_model_get_iter(model, &iter, path)) {
Stepan Salenikovichf6078222016-10-03 17:31:16 -0400313 QModelIndex idx = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(model), &iter);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400314 auto type = idx.data(static_cast<int>(Ring::Role::ObjectType));
315 if (idx.isValid() && type.isValid()) {
316 switch (type.value<Ring::ObjectType>()) {
317 case Ring::ObjectType::Person:
318 {
319 // call the last used contact method
320 // TODO: if no contact methods have been used, offer a popup to Choose
321 auto p_var = idx.data(static_cast<int>(Ring::Role::Object));
322 if (p_var.isValid()) {
323 auto person = p_var.value<Person *>();
324 auto cms = person->phoneNumbers();
325
326 if (!cms.isEmpty()) {
327 auto last_used_cm = cms.at(0);
328 for (int i = 1; i < cms.size(); ++i) {
329 auto new_cm = cms.at(i);
330 if (difftime(new_cm->lastUsed(), last_used_cm->lastUsed()) > 0)
331 last_used_cm = new_cm;
332 }
333
334 place_new_call(last_used_cm);
335 }
336 }
337 }
338 break;
339 case Ring::ObjectType::ContactMethod:
340 {
341 // call the contact method
342 auto cm = idx.data(static_cast<int>(Ring::Role::Object));
343 if (cm.isValid())
344 place_new_call(cm.value<ContactMethod *>());
345 }
346 break;
347 case Ring::ObjectType::Call:
348 case Ring::ObjectType::Media:
349 // nothing to do for now
Stepan Salenikovich4e7ea712015-12-24 14:04:37 -0500350 case Ring::ObjectType::COUNT__:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400351 break;
352 }
353 }
354 }
355}
356
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400357static void
358expand_if_child(G_GNUC_UNUSED GtkTreeModel *tree_model,
359 GtkTreePath *path,
360 G_GNUC_UNUSED GtkTreeIter *iter,
361 GtkTreeView *treeview)
362{
363 if (gtk_tree_path_get_depth(path) > 1)
364 gtk_tree_view_expand_to_path(treeview, path);
365}
366
367static void
Stepan Salenikovichec0862f2015-10-16 15:49:42 -0400368scroll_to_selection(GtkTreeSelection *selection)
369{
370 GtkTreeModel *model = nullptr;
371 GtkTreeIter iter;
372 if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
373 auto path = gtk_tree_model_get_path(model, &iter);
374 auto treeview = gtk_tree_selection_get_tree_view(selection);
375 gtk_tree_view_scroll_to_cell(treeview, path, nullptr, FALSE, 0.0, 0.0);
376 }
377}
378
379static void
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500380on_drag_data_get(GtkWidget *treeview,
381 G_GNUC_UNUSED GdkDragContext *context,
382 GtkSelectionData *data,
383 G_GNUC_UNUSED guint info,
384 G_GNUC_UNUSED guint time,
385 G_GNUC_UNUSED gpointer user_data)
386{
387 g_return_if_fail(IS_RECENT_CONTACTS_VIEW(treeview));
388
389 /* we always drag the selected row */
390 auto selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(treeview));
391 GtkTreeModel *model = NULL;
392 GtkTreeIter iter;
393
394 if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
395 auto path_str = gtk_tree_model_get_string_from_iter(model, &iter);
396
397 gtk_selection_data_set(data,
398 gdk_atom_intern_static_string(CALL_TARGET),
399 8, /* bytes */
400 (guchar *)path_str,
401 strlen(path_str) + 1);
402
403 g_free(path_str);
404 } else {
405 g_warning("drag selection not valid");
406 }
407}
408
409static gboolean
410on_drag_drop(GtkWidget *treeview,
411 GdkDragContext *context,
412 gint x,
413 gint y,
414 guint time,
415 G_GNUC_UNUSED gpointer user_data)
416{
417 g_return_val_if_fail(IS_RECENT_CONTACTS_VIEW(treeview), FALSE);
418
419 GtkTreePath *path = NULL;
420 GtkTreeViewDropPosition drop_pos;
421
422 if (gtk_tree_view_get_dest_row_at_pos(GTK_TREE_VIEW(treeview),
423 x, y, &path, &drop_pos)) {
424
425 GdkAtom target_type = gtk_drag_dest_find_target(treeview, context, NULL);
426
427 if (target_type != GDK_NONE) {
428 g_debug("can drop");
429 gtk_drag_get_data(treeview, context, target_type, time);
430 return TRUE;
431 }
432
433 gtk_tree_path_free(path);
434 }
435
436 return FALSE;
437}
438
439static gboolean
440on_drag_motion(GtkWidget *treeview,
441 GdkDragContext *context,
442 gint x,
443 gint y,
444 guint time,
445 G_GNUC_UNUSED gpointer user_data)
446{
447 g_return_val_if_fail(IS_RECENT_CONTACTS_VIEW(treeview), FALSE);
448
449 GtkTreePath *path = NULL;
450 GtkTreeViewDropPosition drop_pos;
451
452 if (gtk_tree_view_get_dest_row_at_pos(GTK_TREE_VIEW(treeview),
453 x, y, &path, &drop_pos)) {
454 // we only want to drop on a row, not before or after
455 if (drop_pos == GTK_TREE_VIEW_DROP_BEFORE) {
456 gtk_tree_view_set_drag_dest_row(GTK_TREE_VIEW(treeview), path, GTK_TREE_VIEW_DROP_INTO_OR_BEFORE);
457 } else if (drop_pos == GTK_TREE_VIEW_DROP_AFTER) {
458 gtk_tree_view_set_drag_dest_row(GTK_TREE_VIEW(treeview), path, GTK_TREE_VIEW_DROP_INTO_OR_AFTER);
459 }
460 gdk_drag_status(context, gdk_drag_context_get_suggested_action(context), time);
461 return TRUE;
462 } else {
463 // not a row in the treeview, so we cannot drop
464 return FALSE;
465 }
466}
467
468static void
469on_drag_data_received(GtkWidget *treeview,
470 GdkDragContext *context,
471 gint x,
472 gint y,
473 GtkSelectionData *data,
474 G_GNUC_UNUSED guint info,
475 guint time,
476 G_GNUC_UNUSED gpointer user_data)
477{
478 g_return_if_fail(IS_RECENT_CONTACTS_VIEW(treeview));
479
480 gboolean success = FALSE;
481
482 /* get the source and destination calls */
483 auto path_str_source = (gchar *)gtk_selection_data_get_data(data);
484 auto type = gtk_selection_data_get_data_type(data);
485 g_debug("data type: %s", gdk_atom_name(type));
486 if (path_str_source && strlen(path_str_source) > 0) {
487 g_debug("source path: %s", path_str_source);
488
489 /* get the destination path */
490 GtkTreePath *dest_path = NULL;
491 if (gtk_tree_view_get_dest_row_at_pos(GTK_TREE_VIEW(treeview), x, y, &dest_path, NULL)) {
492 auto model = gtk_tree_view_get_model(GTK_TREE_VIEW(treeview));
493
494 GtkTreeIter source;
495 gtk_tree_model_get_iter_from_string(model, &source, path_str_source);
Stepan Salenikovichf6078222016-10-03 17:31:16 -0400496 auto idx_source_proxy = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(model), &source);
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500497
498 GtkTreeIter dest;
499 gtk_tree_model_get_iter(model, &dest, dest_path);
Stepan Salenikovichf6078222016-10-03 17:31:16 -0400500 auto idx_dest_proxy = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(model), &dest);
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500501
502 // get call objects and indeces from RecentModel indeces being drag and dropped
503 auto idx_source = RecentModel::instance().peopleProxy()->mapToSource(idx_source_proxy);
504 auto idx_dest = RecentModel::instance().peopleProxy()->mapToSource(idx_dest_proxy);
505 auto call_source = RecentModel::instance().getActiveCall(idx_source);
506 auto call_dest = RecentModel::instance().getActiveCall(idx_dest);
507 auto idx_call_source = CallModel::instance().getIndex(call_source);
508 auto idx_call_dest = CallModel::instance().getIndex(call_dest);
509
510 if (idx_call_source.isValid() && idx_call_dest.isValid()) {
511 QModelIndexList source_list;
512 source_list << idx_call_source;
513 auto mimeData = CallModel::instance().mimeData(source_list);
514 auto action = Call::DropAction::Conference;
515 mimeData->setProperty("dropAction", action);
516
517 if (CallModel::instance().dropMimeData(mimeData, Qt::MoveAction, idx_call_dest.row(), idx_call_dest.column(), idx_call_dest.parent())) {
518 success = TRUE;
519 } else {
520 g_warning("could not drop mime data");
521 }
522 } else {
523 g_warning("source or dest call not valid");
524 }
525
526 gtk_tree_path_free(dest_path);
527 }
528 }
529
530 gtk_drag_finish(context, success, FALSE, time);
531}
532
533static void
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400534recent_contacts_view_init(RecentContactsView *self)
535{
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400536 RecentContactsViewPrivate *priv = RECENT_CONTACTS_VIEW_GET_PRIVATE(self);
537
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400538 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(self), FALSE);
539 /* no need to show the expander since it will always be expanded */
540 gtk_tree_view_set_show_expanders(GTK_TREE_VIEW(self), FALSE);
541 /* disable default search, we will handle it ourselves via LRC;
542 * otherwise the search steals input focus on key presses */
543 gtk_tree_view_set_enable_search(GTK_TREE_VIEW(self), FALSE);
544
Stepan Salenikovichf6078222016-10-03 17:31:16 -0400545 GtkQTreeModel *recent_model = gtk_q_tree_model_new(
Guillaume Roguez5d1514b2015-10-22 15:55:31 -0400546 RecentModel::instance().peopleProxy(),
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400547 1,
aviau271bcc22016-05-27 17:25:19 -0400548 0, Qt::DisplayRole, G_TYPE_STRING);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400549
550 gtk_tree_view_set_model(GTK_TREE_VIEW(self),
551 GTK_TREE_MODEL(recent_model));
552
553 /* photo and name/contact method column */
554 GtkCellArea *area = gtk_cell_area_box_new();
555 GtkTreeViewColumn *column = gtk_tree_view_column_new_with_area(area);
556
557 /* photo renderer */
558 GtkCellRenderer *renderer = gtk_cell_renderer_pixbuf_new();
559 gtk_cell_area_box_pack_start(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
560
561 /* get the photo */
562 gtk_tree_view_column_set_cell_data_func(
563 column,
564 renderer,
565 (GtkTreeCellDataFunc)render_contact_photo,
566 NULL,
567 NULL);
568
569 /* name/cm and status renderer */
570 renderer = gtk_cell_renderer_text_new();
571 g_object_set(G_OBJECT(renderer), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
572 gtk_cell_area_box_pack_start(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
573
574 gtk_tree_view_column_set_cell_data_func(
575 column,
576 renderer,
577 (GtkTreeCellDataFunc)render_name_and_info,
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400578 self,
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400579 NULL);
580
Stepan Salenikoviche8ef9352015-10-27 19:07:46 -0400581 /* call duration */
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400582 renderer = gtk_cell_renderer_text_new();
Stepan Salenikoviche8ef9352015-10-27 19:07:46 -0400583 g_object_set(G_OBJECT(renderer), "xalign", 1.0, NULL);
584 gtk_cell_area_box_pack_end(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400585 gtk_tree_view_column_set_cell_data_func(
586 column,
587 renderer,
588 (GtkTreeCellDataFunc)render_call_duration,
589 NULL,
590 NULL);
591
Stepan Salenikoviche8ef9352015-10-27 19:07:46 -0400592 gtk_tree_view_append_column(GTK_TREE_VIEW(self), column);
593 gtk_tree_view_column_set_resizable(column, TRUE);
594 gtk_tree_view_column_set_expand(column, TRUE);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400595 gtk_tree_view_expand_all(GTK_TREE_VIEW(self));
596
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400597 g_signal_connect(self, "row-activated", G_CALLBACK(activate_item), NULL);
598 g_signal_connect(recent_model, "row-inserted", G_CALLBACK(expand_if_child), self);
599
600 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(self));
Stepan Salenikovichc1323422016-01-06 10:54:44 -0500601 g_signal_connect(selection, "changed", G_CALLBACK(update_selection), NULL);
Stepan Salenikovichec0862f2015-10-16 15:49:42 -0400602 g_signal_connect(selection, "changed", G_CALLBACK(scroll_to_selection), NULL);
603 g_signal_connect_swapped(recent_model, "rows-reordered", G_CALLBACK(scroll_to_selection), selection);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400604
Stepan Salenikovichc1323422016-01-06 10:54:44 -0500605 /* update the selection based on the RecentModel */
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400606 priv->selection_updated = QObject::connect(
Stepan Salenikovichc1323422016-01-06 10:54:44 -0500607 RecentModel::instance().selectionModel(),
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400608 &QItemSelectionModel::currentChanged,
609 [self, recent_model](const QModelIndex current, G_GNUC_UNUSED const QModelIndex & previous) {
Stepan Salenikovichc1323422016-01-06 10:54:44 -0500610 auto selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(self));
611
612 auto current_proxy = RecentModel::instance().peopleProxy()->mapFromSource(current);
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400613
Stepan Salenikovich8043a562016-03-18 13:56:40 -0400614 if (current.isValid()) {
615 /* select the current */
616 GtkTreeIter new_iter;
Stepan Salenikovichf6078222016-10-03 17:31:16 -0400617 if (gtk_q_tree_model_source_index_to_iter(recent_model, current_proxy, &new_iter)) {
Stepan Salenikovich8043a562016-03-18 13:56:40 -0400618 gtk_tree_selection_select_iter(selection, &new_iter);
619 }
620 } else {
621 gtk_tree_selection_unselect_all(selection);
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400622 }
623 }
624 );
625
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500626 /* drag and drop */
627 static GtkTargetEntry targetentries[] = {
628 { (gchar *)CALL_TARGET, GTK_TARGET_SAME_WIDGET, CALL_TARGET_ID },
629 };
630
631 gtk_tree_view_enable_model_drag_source(GTK_TREE_VIEW(self),
632 GDK_BUTTON1_MASK, targetentries, 1, (GdkDragAction)(GDK_ACTION_DEFAULT | GDK_ACTION_MOVE));
633
634 gtk_tree_view_enable_model_drag_dest(GTK_TREE_VIEW(self),
635 targetentries, 1, GDK_ACTION_DEFAULT);
636
637 g_signal_connect(self, "drag-data-get", G_CALLBACK(on_drag_data_get), nullptr);
638 g_signal_connect(self, "drag-drop", G_CALLBACK(on_drag_drop), nullptr);
639 g_signal_connect(self, "drag-motion", G_CALLBACK(on_drag_motion), nullptr);
640 g_signal_connect(self, "drag_data_received", G_CALLBACK(on_drag_data_received), nullptr);
641
Stepan Salenikovich8eaa13e2016-08-26 16:51:48 -0400642 /* init popup menu */
643 priv->popup_menu = contact_popup_menu_new(GTK_TREE_VIEW(self));
644 g_signal_connect_swapped(self, "button-press-event", G_CALLBACK(contact_popup_menu_show), priv->popup_menu);
645
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400646 gtk_widget_show_all(GTK_WIDGET(self));
647}
648
649static void
650recent_contacts_view_dispose(GObject *object)
651{
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400652 RecentContactsView *self = RECENT_CONTACTS_VIEW(object);
653 RecentContactsViewPrivate *priv = RECENT_CONTACTS_VIEW_GET_PRIVATE(self);
654
655 QObject::disconnect(priv->selection_updated);
Stepan Salenikovich8eaa13e2016-08-26 16:51:48 -0400656 gtk_widget_destroy(priv->popup_menu);
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400657
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400658 G_OBJECT_CLASS(recent_contacts_view_parent_class)->dispose(object);
659}
660
661static void
662recent_contacts_view_finalize(GObject *object)
663{
664 G_OBJECT_CLASS(recent_contacts_view_parent_class)->finalize(object);
665}
666
667static void
668recent_contacts_view_class_init(RecentContactsViewClass *klass)
669{
670 G_OBJECT_CLASS(klass)->finalize = recent_contacts_view_finalize;
671 G_OBJECT_CLASS(klass)->dispose = recent_contacts_view_dispose;
672}
673
674GtkWidget *
675recent_contacts_view_new()
676{
677 gpointer self = g_object_new(RECENT_CONTACTS_VIEW_TYPE, NULL);
678
679 return (GtkWidget *)self;
680}