blob: bb3f563ba5a047b53e77f05d83fd599bbd1efcde [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 Salenikovichf53128f2016-10-07 10:32:16 -040065 QMetaObject::Connection layout_changed;
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
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040089render_contact_photo(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
90 GtkCellRenderer *cell,
91 GtkTreeModel *model,
92 GtkTreeIter *iter,
93 G_GNUC_UNUSED gpointer data)
94{
Stepan Salenikovichf6078222016-10-03 17:31:16 -040095 QModelIndex idx = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(model), iter);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040096
Stepan Salenikovichd8765072016-01-14 10:58:51 -050097 std::shared_ptr<GdkPixbuf> image;
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040098 /* we only want to render a photo for the top nodes: Person, ContactMethod (, later Conference) */
99 QVariant object = idx.data(static_cast<int>(Ring::Role::Object));
100 if (idx.isValid() && object.isValid()) {
101 QVariant var_photo;
102 if (auto person = object.value<Person *>()) {
103 var_photo = GlobalInstances::pixmapManipulator().contactPhoto(person, QSize(50, 50), false);
104 } else if (auto cm = object.value<ContactMethod *>()) {
105 /* get photo, note that this should in all cases be the fallback avatar, since there
106 * shouldn't be a person associated with this contact method */
107 var_photo = GlobalInstances::pixmapManipulator().callPhoto(cm, QSize(50, 50), false);
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500108 } else if (auto call = object.value<Call *>()) {
109 if (call->type() == Call::Type::CONFERENCE) {
110 var_photo = GlobalInstances::pixmapManipulator().callPhoto(call, QSize(50, 50), false);
111 }
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400112 }
Stepan Salenikovichd8765072016-01-14 10:58:51 -0500113 if (var_photo.isValid()) {
114 std::shared_ptr<GdkPixbuf> photo = var_photo.value<std::shared_ptr<GdkPixbuf>>();
115
116 auto unread = idx.data(static_cast<int>(Ring::Role::UnreadTextMessageCount));
117
118 image.reset(ring_draw_unread_messages(photo.get(), unread.toInt()), g_object_unref);
119 } else {
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400120 // set the width of the cell rendered to the with of the photo
121 // so that the other renderers are shifted to the right
122 g_object_set(G_OBJECT(cell), "width", 50, NULL);
123 }
124 }
125
Stepan Salenikovichd8765072016-01-14 10:58:51 -0500126 g_object_set(G_OBJECT(cell), "pixbuf", image.get(), NULL);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400127}
128
129static void
130render_name_and_info(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
131 GtkCellRenderer *cell,
132 GtkTreeModel *model,
133 GtkTreeIter *iter,
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400134 GtkTreeView *treeview)
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400135{
136 gchar *text = NULL;
137
Stepan Salenikovichf6078222016-10-03 17:31:16 -0400138 QModelIndex idx = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(model), iter);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400139
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400140 // check if this iter is selected
141 gboolean is_selected = FALSE;
142 if (GTK_IS_TREE_VIEW(treeview)) {
143 auto selection = gtk_tree_view_get_selection(treeview);
144 is_selected = gtk_tree_selection_iter_is_selected(selection, iter);
145 }
146
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400147 auto type = idx.data(static_cast<int>(Ring::Role::ObjectType));
148 if (idx.isValid() && type.isValid()) {
149 switch (type.value<Ring::ObjectType>()) {
150 case Ring::ObjectType::Person:
151 case Ring::ObjectType::ContactMethod:
152 {
153 auto var_name = idx.data(static_cast<int>(Ring::Role::Name));
154 auto var_lastused = idx.data(static_cast<int>(Ring::Role::LastUsed));
155 auto var_status = idx.data(static_cast<int>(Ring::Role::FormattedState));
156
157 QString name, status;
158
159 if (var_name.isValid())
160 name = var_name.value<QString>();
161
162 // show the status if there is a call, otherwise the last used date/time
163 if (var_status.isValid()) {
164 status += var_status.value<QString>();
165 }else if (var_lastused.isValid()) {
166 auto date_time = var_lastused.value<QDateTime>();
167 auto category = HistoryTimeCategoryModel::timeToHistoryConst(date_time.toTime_t());
168
169 // if it is 'today', then we only want to show the time
170 if (category != HistoryTimeCategoryModel::HistoryConst::Today) {
171 status += HistoryTimeCategoryModel::timeToHistoryCategory(date_time.toTime_t());
172 }
173 // we only want to show the time if it is less than a week ago
174 if (category < HistoryTimeCategoryModel::HistoryConst::A_week_ago) {
175 if (!status.isEmpty())
176 status += ", ";
177 status += QLocale::system().toString(date_time.time(), QLocale::ShortFormat);
178 }
179 }
180
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400181 /* we want the color of the status text to be the default color if this iter is
182 * selected so that the treeview is able to invert it against the selection color */
183 if (is_selected) {
Stepan Salenikovich0731de02016-05-17 17:47:16 -0400184 text = g_markup_printf_escaped("%s\n<span size=\"smaller\">%s</span>",
185 name.toUtf8().constData(),
186 status.toUtf8().constData());
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400187 } else {
Stepan Salenikovich0731de02016-05-17 17:47:16 -0400188 text = g_markup_printf_escaped("%s\n<span size=\"smaller\" color=\"gray\">%s</span>",
189 name.toUtf8().constData(),
190 status.toUtf8().constData());
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400191 }
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400192 }
193 break;
194 case Ring::ObjectType::Call:
195 {
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500196 // check if it is a conference
197 auto idx_source = RecentModel::instance().peopleProxy()->mapToSource(idx);
198 auto is_conference = RecentModel::instance().isConference(idx_source);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400199
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500200 if (is_conference) {
201 auto var_name = idx.data(static_cast<int>(Ring::Role::Name));
Stepan Salenikovich0731de02016-05-17 17:47:16 -0400202 text = g_markup_escape_text(var_name.value<QString>().toUtf8().constData(), -1);
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500203 } else {
204 auto parent_source = RecentModel::instance().peopleProxy()->mapToSource(idx.parent());
205 if (RecentModel::instance().isConference(parent_source)) {
206 // part of conference, simply display the name
207 auto var_name = idx.data(static_cast<int>(Ring::Role::Name));
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400208
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500209 /* we want the color of the name text to be the default color if this iter is
210 * selected so that the treeview is able to invert it against the selection color */
Stepan Salenikovich0731de02016-05-17 17:47:16 -0400211 if (is_selected) {
212 text = g_markup_printf_escaped("<span size=\"smaller\">%s</span>",
213 var_name.value<QString>().toUtf8().constData());
214 } else {
215 text = g_markup_printf_escaped("<span size=\"smaller\" color=\"gray\">%s</span>",
216 var_name.value<QString>().toUtf8().constData());
217 }
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500218 } else {
219 // just a call, so display the state
220 auto var_status = idx.data(static_cast<int>(Ring::Role::FormattedState));
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400221
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500222 QString status;
223
224 if (var_status.isValid())
225 status += var_status.value<QString>();
226
227 /* we want the color of the status text to be the default color if this iter is
228 * selected so that the treeview is able to invert it against the selection color */
Stepan Salenikovich0731de02016-05-17 17:47:16 -0400229 if (is_selected) {
230 text = g_markup_printf_escaped("<span size=\"smaller\">%s</span>",
231 status.toUtf8().constData());
232 } else {
233 text = g_markup_printf_escaped("<span size=\"smaller\" color=\"gray\">%s</span>",
234 status.toUtf8().constData());
235 }
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500236 }
237 }
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400238 }
239 break;
240 case Ring::ObjectType::Media:
aviau9a277472016-12-13 14:54:58 -0500241 case Ring::ObjectType::Certificate:
Nicolas Jagerc74f7612017-03-28 09:48:18 -0400242 case Ring::ObjectType::ContactRequest:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400243 // nothing to do for now
Stepan Salenikovich4e7ea712015-12-24 14:04:37 -0500244 case Ring::ObjectType::COUNT__:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400245 break;
246 }
247 }
248
249 g_object_set(G_OBJECT(cell), "markup", text, NULL);
250 g_free(text);
251}
252
253static void
254render_call_duration(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
255 GtkCellRenderer *cell,
256 GtkTreeModel *model,
257 GtkTreeIter *iter,
258 G_GNUC_UNUSED gpointer data)
259{
260 gchar *text = NULL;
261
Stepan Salenikovichf6078222016-10-03 17:31:16 -0400262 QModelIndex idx = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(model), iter);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400263
264 auto type = idx.data(static_cast<int>(Ring::Role::ObjectType));
265 if (idx.isValid() && type.isValid()) {
266 switch (type.value<Ring::ObjectType>()) {
267 case Ring::ObjectType::Person:
268 case Ring::ObjectType::ContactMethod:
269 {
270 // check if there are any children (calls); we need to convert to source model in
271 // case there is only one
Guillaume Roguez5d1514b2015-10-22 15:55:31 -0400272 auto idx_source = RecentModel::instance().peopleProxy()->mapToSource(idx);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400273 auto duration = idx.data(static_cast<int>(Ring::Role::Length));
274 if (idx_source.isValid()
275 && (idx_source.model()->rowCount(idx_source) == 1)
276 && duration.isValid())
277 {
Stepan Salenikovich0731de02016-05-17 17:47:16 -0400278 text = g_markup_escape_text(duration.value<QString>().toUtf8().constData(), -1);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400279 }
280 }
281 break;
282 case Ring::ObjectType::Call:
283 {
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500284 // do not display the duration if the call is part of a conference
285 auto parent_source = RecentModel::instance().peopleProxy()->mapToSource(idx.parent());
286 auto in_conference = RecentModel::instance().isConference(parent_source);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400287
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500288 if (!in_conference) {
289 auto duration = idx.data(static_cast<int>(Ring::Role::Length));
290
291 if (duration.isValid())
Stepan Salenikovich0731de02016-05-17 17:47:16 -0400292 text = g_markup_escape_text(duration.value<QString>().toUtf8().constData(), -1);
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500293 }
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400294 }
295 break;
296 case Ring::ObjectType::Media:
aviau9a277472016-12-13 14:54:58 -0500297 case Ring::ObjectType::Certificate:
Nicolas Jagerc74f7612017-03-28 09:48:18 -0400298 case Ring::ObjectType::ContactRequest:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400299 // nothing to do for now
Stepan Salenikovich4e7ea712015-12-24 14:04:37 -0500300 case Ring::ObjectType::COUNT__:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400301 break;
302 }
303 }
304
305 g_object_set(G_OBJECT(cell), "markup", text, NULL);
306 g_free(text);
307}
308
309static void
310activate_item(GtkTreeView *tree_view,
311 GtkTreePath *path,
312 G_GNUC_UNUSED GtkTreeViewColumn *column,
313 G_GNUC_UNUSED gpointer user_data)
314{
315 auto model = gtk_tree_view_get_model(tree_view);
316 GtkTreeIter iter;
317 if (gtk_tree_model_get_iter(model, &iter, path)) {
Stepan Salenikovichf6078222016-10-03 17:31:16 -0400318 QModelIndex idx = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(model), &iter);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400319 auto type = idx.data(static_cast<int>(Ring::Role::ObjectType));
320 if (idx.isValid() && type.isValid()) {
321 switch (type.value<Ring::ObjectType>()) {
322 case Ring::ObjectType::Person:
323 {
324 // call the last used contact method
325 // TODO: if no contact methods have been used, offer a popup to Choose
326 auto p_var = idx.data(static_cast<int>(Ring::Role::Object));
327 if (p_var.isValid()) {
328 auto person = p_var.value<Person *>();
329 auto cms = person->phoneNumbers();
330
331 if (!cms.isEmpty()) {
332 auto last_used_cm = cms.at(0);
333 for (int i = 1; i < cms.size(); ++i) {
334 auto new_cm = cms.at(i);
335 if (difftime(new_cm->lastUsed(), last_used_cm->lastUsed()) > 0)
336 last_used_cm = new_cm;
337 }
338
339 place_new_call(last_used_cm);
340 }
341 }
342 }
343 break;
344 case Ring::ObjectType::ContactMethod:
345 {
346 // call the contact method
347 auto cm = idx.data(static_cast<int>(Ring::Role::Object));
348 if (cm.isValid())
349 place_new_call(cm.value<ContactMethod *>());
350 }
351 break;
352 case Ring::ObjectType::Call:
353 case Ring::ObjectType::Media:
aviau9a277472016-12-13 14:54:58 -0500354 case Ring::ObjectType::Certificate:
Nicolas Jagerc74f7612017-03-28 09:48:18 -0400355 case Ring::ObjectType::ContactRequest:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400356 // nothing to do for now
Stepan Salenikovich4e7ea712015-12-24 14:04:37 -0500357 case Ring::ObjectType::COUNT__:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400358 break;
359 }
360 }
361 }
362}
363
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400364static void
365expand_if_child(G_GNUC_UNUSED GtkTreeModel *tree_model,
366 GtkTreePath *path,
367 G_GNUC_UNUSED GtkTreeIter *iter,
368 GtkTreeView *treeview)
369{
370 if (gtk_tree_path_get_depth(path) > 1)
371 gtk_tree_view_expand_to_path(treeview, path);
372}
373
374static void
Stepan Salenikovichec0862f2015-10-16 15:49:42 -0400375scroll_to_selection(GtkTreeSelection *selection)
376{
377 GtkTreeModel *model = nullptr;
378 GtkTreeIter iter;
379 if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
380 auto path = gtk_tree_model_get_path(model, &iter);
381 auto treeview = gtk_tree_selection_get_tree_view(selection);
382 gtk_tree_view_scroll_to_cell(treeview, path, nullptr, FALSE, 0.0, 0.0);
383 }
384}
385
386static void
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500387on_drag_data_get(GtkWidget *treeview,
388 G_GNUC_UNUSED GdkDragContext *context,
389 GtkSelectionData *data,
390 G_GNUC_UNUSED guint info,
391 G_GNUC_UNUSED guint time,
392 G_GNUC_UNUSED gpointer user_data)
393{
394 g_return_if_fail(IS_RECENT_CONTACTS_VIEW(treeview));
395
396 /* we always drag the selected row */
397 auto selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(treeview));
398 GtkTreeModel *model = NULL;
399 GtkTreeIter iter;
400
401 if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
402 auto path_str = gtk_tree_model_get_string_from_iter(model, &iter);
403
404 gtk_selection_data_set(data,
405 gdk_atom_intern_static_string(CALL_TARGET),
406 8, /* bytes */
407 (guchar *)path_str,
408 strlen(path_str) + 1);
409
410 g_free(path_str);
411 } else {
412 g_warning("drag selection not valid");
413 }
414}
415
416static gboolean
417on_drag_drop(GtkWidget *treeview,
418 GdkDragContext *context,
419 gint x,
420 gint y,
421 guint time,
422 G_GNUC_UNUSED gpointer user_data)
423{
424 g_return_val_if_fail(IS_RECENT_CONTACTS_VIEW(treeview), FALSE);
425
426 GtkTreePath *path = NULL;
427 GtkTreeViewDropPosition drop_pos;
428
429 if (gtk_tree_view_get_dest_row_at_pos(GTK_TREE_VIEW(treeview),
430 x, y, &path, &drop_pos)) {
431
432 GdkAtom target_type = gtk_drag_dest_find_target(treeview, context, NULL);
433
434 if (target_type != GDK_NONE) {
435 g_debug("can drop");
436 gtk_drag_get_data(treeview, context, target_type, time);
437 return TRUE;
438 }
439
440 gtk_tree_path_free(path);
441 }
442
443 return FALSE;
444}
445
446static gboolean
447on_drag_motion(GtkWidget *treeview,
448 GdkDragContext *context,
449 gint x,
450 gint y,
451 guint time,
452 G_GNUC_UNUSED gpointer user_data)
453{
454 g_return_val_if_fail(IS_RECENT_CONTACTS_VIEW(treeview), FALSE);
455
456 GtkTreePath *path = NULL;
457 GtkTreeViewDropPosition drop_pos;
458
459 if (gtk_tree_view_get_dest_row_at_pos(GTK_TREE_VIEW(treeview),
460 x, y, &path, &drop_pos)) {
461 // we only want to drop on a row, not before or after
462 if (drop_pos == GTK_TREE_VIEW_DROP_BEFORE) {
463 gtk_tree_view_set_drag_dest_row(GTK_TREE_VIEW(treeview), path, GTK_TREE_VIEW_DROP_INTO_OR_BEFORE);
464 } else if (drop_pos == GTK_TREE_VIEW_DROP_AFTER) {
465 gtk_tree_view_set_drag_dest_row(GTK_TREE_VIEW(treeview), path, GTK_TREE_VIEW_DROP_INTO_OR_AFTER);
466 }
467 gdk_drag_status(context, gdk_drag_context_get_suggested_action(context), time);
468 return TRUE;
469 } else {
470 // not a row in the treeview, so we cannot drop
471 return FALSE;
472 }
473}
474
475static void
476on_drag_data_received(GtkWidget *treeview,
477 GdkDragContext *context,
478 gint x,
479 gint y,
480 GtkSelectionData *data,
481 G_GNUC_UNUSED guint info,
482 guint time,
483 G_GNUC_UNUSED gpointer user_data)
484{
485 g_return_if_fail(IS_RECENT_CONTACTS_VIEW(treeview));
486
487 gboolean success = FALSE;
488
489 /* get the source and destination calls */
490 auto path_str_source = (gchar *)gtk_selection_data_get_data(data);
491 auto type = gtk_selection_data_get_data_type(data);
492 g_debug("data type: %s", gdk_atom_name(type));
493 if (path_str_source && strlen(path_str_source) > 0) {
494 g_debug("source path: %s", path_str_source);
495
496 /* get the destination path */
497 GtkTreePath *dest_path = NULL;
498 if (gtk_tree_view_get_dest_row_at_pos(GTK_TREE_VIEW(treeview), x, y, &dest_path, NULL)) {
499 auto model = gtk_tree_view_get_model(GTK_TREE_VIEW(treeview));
500
501 GtkTreeIter source;
502 gtk_tree_model_get_iter_from_string(model, &source, path_str_source);
Stepan Salenikovichf6078222016-10-03 17:31:16 -0400503 auto idx_source_proxy = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(model), &source);
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500504
505 GtkTreeIter dest;
506 gtk_tree_model_get_iter(model, &dest, dest_path);
Stepan Salenikovichf6078222016-10-03 17:31:16 -0400507 auto idx_dest_proxy = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(model), &dest);
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500508
509 // get call objects and indeces from RecentModel indeces being drag and dropped
510 auto idx_source = RecentModel::instance().peopleProxy()->mapToSource(idx_source_proxy);
511 auto idx_dest = RecentModel::instance().peopleProxy()->mapToSource(idx_dest_proxy);
512 auto call_source = RecentModel::instance().getActiveCall(idx_source);
513 auto call_dest = RecentModel::instance().getActiveCall(idx_dest);
514 auto idx_call_source = CallModel::instance().getIndex(call_source);
515 auto idx_call_dest = CallModel::instance().getIndex(call_dest);
516
517 if (idx_call_source.isValid() && idx_call_dest.isValid()) {
518 QModelIndexList source_list;
519 source_list << idx_call_source;
520 auto mimeData = CallModel::instance().mimeData(source_list);
521 auto action = Call::DropAction::Conference;
522 mimeData->setProperty("dropAction", action);
523
524 if (CallModel::instance().dropMimeData(mimeData, Qt::MoveAction, idx_call_dest.row(), idx_call_dest.column(), idx_call_dest.parent())) {
525 success = TRUE;
526 } else {
527 g_warning("could not drop mime data");
528 }
529 } else {
530 g_warning("source or dest call not valid");
531 }
532
533 gtk_tree_path_free(dest_path);
534 }
535 }
536
537 gtk_drag_finish(context, success, FALSE, time);
538}
539
540static void
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400541recent_contacts_view_init(RecentContactsView *self)
542{
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400543 RecentContactsViewPrivate *priv = RECENT_CONTACTS_VIEW_GET_PRIVATE(self);
544
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400545 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(self), FALSE);
546 /* no need to show the expander since it will always be expanded */
547 gtk_tree_view_set_show_expanders(GTK_TREE_VIEW(self), FALSE);
548 /* disable default search, we will handle it ourselves via LRC;
549 * otherwise the search steals input focus on key presses */
550 gtk_tree_view_set_enable_search(GTK_TREE_VIEW(self), FALSE);
551
Stepan Salenikovichf6078222016-10-03 17:31:16 -0400552 GtkQTreeModel *recent_model = gtk_q_tree_model_new(
Guillaume Roguez5d1514b2015-10-22 15:55:31 -0400553 RecentModel::instance().peopleProxy(),
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400554 1,
aviau271bcc22016-05-27 17:25:19 -0400555 0, Qt::DisplayRole, G_TYPE_STRING);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400556
557 gtk_tree_view_set_model(GTK_TREE_VIEW(self),
558 GTK_TREE_MODEL(recent_model));
559
560 /* photo and name/contact method column */
561 GtkCellArea *area = gtk_cell_area_box_new();
562 GtkTreeViewColumn *column = gtk_tree_view_column_new_with_area(area);
563
564 /* photo renderer */
565 GtkCellRenderer *renderer = gtk_cell_renderer_pixbuf_new();
566 gtk_cell_area_box_pack_start(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
567
568 /* get the photo */
569 gtk_tree_view_column_set_cell_data_func(
570 column,
571 renderer,
572 (GtkTreeCellDataFunc)render_contact_photo,
573 NULL,
574 NULL);
575
576 /* name/cm and status renderer */
577 renderer = gtk_cell_renderer_text_new();
578 g_object_set(G_OBJECT(renderer), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
579 gtk_cell_area_box_pack_start(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
580
581 gtk_tree_view_column_set_cell_data_func(
582 column,
583 renderer,
584 (GtkTreeCellDataFunc)render_name_and_info,
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400585 self,
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400586 NULL);
587
Stepan Salenikoviche8ef9352015-10-27 19:07:46 -0400588 /* call duration */
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400589 renderer = gtk_cell_renderer_text_new();
Stepan Salenikoviche8ef9352015-10-27 19:07:46 -0400590 g_object_set(G_OBJECT(renderer), "xalign", 1.0, NULL);
591 gtk_cell_area_box_pack_end(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400592 gtk_tree_view_column_set_cell_data_func(
593 column,
594 renderer,
595 (GtkTreeCellDataFunc)render_call_duration,
596 NULL,
597 NULL);
598
Stepan Salenikoviche8ef9352015-10-27 19:07:46 -0400599 gtk_tree_view_append_column(GTK_TREE_VIEW(self), column);
600 gtk_tree_view_column_set_resizable(column, TRUE);
601 gtk_tree_view_column_set_expand(column, TRUE);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400602 gtk_tree_view_expand_all(GTK_TREE_VIEW(self));
603
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400604 g_signal_connect(self, "row-activated", G_CALLBACK(activate_item), NULL);
605 g_signal_connect(recent_model, "row-inserted", G_CALLBACK(expand_if_child), self);
606
607 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(self));
Stepan Salenikovichc1323422016-01-06 10:54:44 -0500608 g_signal_connect(selection, "changed", G_CALLBACK(update_selection), NULL);
Stepan Salenikovichec0862f2015-10-16 15:49:42 -0400609 g_signal_connect(selection, "changed", G_CALLBACK(scroll_to_selection), NULL);
610 g_signal_connect_swapped(recent_model, "rows-reordered", G_CALLBACK(scroll_to_selection), selection);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400611
Stepan Salenikovichc1323422016-01-06 10:54:44 -0500612 /* update the selection based on the RecentModel */
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400613 priv->selection_updated = QObject::connect(
Stepan Salenikovichc1323422016-01-06 10:54:44 -0500614 RecentModel::instance().selectionModel(),
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400615 &QItemSelectionModel::currentChanged,
616 [self, recent_model](const QModelIndex current, G_GNUC_UNUSED const QModelIndex & previous) {
Stepan Salenikovichc1323422016-01-06 10:54:44 -0500617 auto selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(self));
618
619 auto current_proxy = RecentModel::instance().peopleProxy()->mapFromSource(current);
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400620
Stepan Salenikovich8043a562016-03-18 13:56:40 -0400621 if (current.isValid()) {
622 /* select the current */
623 GtkTreeIter new_iter;
Stepan Salenikovichf6078222016-10-03 17:31:16 -0400624 if (gtk_q_tree_model_source_index_to_iter(recent_model, current_proxy, &new_iter)) {
Stepan Salenikovich8043a562016-03-18 13:56:40 -0400625 gtk_tree_selection_select_iter(selection, &new_iter);
626 }
627 } else {
628 gtk_tree_selection_unselect_all(selection);
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400629 }
630 }
631 );
632
Stepan Salenikovichf53128f2016-10-07 10:32:16 -0400633 /* we may need to update the selection when the layout changes */
634 priv->layout_changed = QObject::connect(
635 RecentModel::instance().peopleProxy(),
636 &QAbstractItemModel::layoutChanged,
637 [self, recent_model]() {
638 auto idx = RecentModel::instance().selectionModel()->currentIndex();
639 auto selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(self));
640
641 auto idx_proxy = RecentModel::instance().peopleProxy()->mapFromSource(idx);
642
643 if (idx_proxy.isValid()) {
644 /* select the current */
645 GtkTreeIter iter;
646 if (gtk_q_tree_model_source_index_to_iter(recent_model, idx_proxy, &iter)) {
647 gtk_tree_selection_select_iter(selection, &iter);
648 }
649 } else {
650 gtk_tree_selection_unselect_all(selection);
651 }
652 }
653 );
654
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500655 /* drag and drop */
656 static GtkTargetEntry targetentries[] = {
657 { (gchar *)CALL_TARGET, GTK_TARGET_SAME_WIDGET, CALL_TARGET_ID },
658 };
659
660 gtk_tree_view_enable_model_drag_source(GTK_TREE_VIEW(self),
661 GDK_BUTTON1_MASK, targetentries, 1, (GdkDragAction)(GDK_ACTION_DEFAULT | GDK_ACTION_MOVE));
662
663 gtk_tree_view_enable_model_drag_dest(GTK_TREE_VIEW(self),
664 targetentries, 1, GDK_ACTION_DEFAULT);
665
666 g_signal_connect(self, "drag-data-get", G_CALLBACK(on_drag_data_get), nullptr);
667 g_signal_connect(self, "drag-drop", G_CALLBACK(on_drag_drop), nullptr);
668 g_signal_connect(self, "drag-motion", G_CALLBACK(on_drag_motion), nullptr);
669 g_signal_connect(self, "drag_data_received", G_CALLBACK(on_drag_data_received), nullptr);
670
Stepan Salenikovich8eaa13e2016-08-26 16:51:48 -0400671 /* init popup menu */
672 priv->popup_menu = contact_popup_menu_new(GTK_TREE_VIEW(self));
673 g_signal_connect_swapped(self, "button-press-event", G_CALLBACK(contact_popup_menu_show), priv->popup_menu);
674
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400675 gtk_widget_show_all(GTK_WIDGET(self));
676}
677
678static void
679recent_contacts_view_dispose(GObject *object)
680{
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400681 RecentContactsView *self = RECENT_CONTACTS_VIEW(object);
682 RecentContactsViewPrivate *priv = RECENT_CONTACTS_VIEW_GET_PRIVATE(self);
683
684 QObject::disconnect(priv->selection_updated);
Stepan Salenikovichf53128f2016-10-07 10:32:16 -0400685 QObject::disconnect(priv->layout_changed);
Stepan Salenikovich8eaa13e2016-08-26 16:51:48 -0400686 gtk_widget_destroy(priv->popup_menu);
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400687
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400688 G_OBJECT_CLASS(recent_contacts_view_parent_class)->dispose(object);
689}
690
691static void
692recent_contacts_view_finalize(GObject *object)
693{
694 G_OBJECT_CLASS(recent_contacts_view_parent_class)->finalize(object);
695}
696
697static void
698recent_contacts_view_class_init(RecentContactsViewClass *klass)
699{
700 G_OBJECT_CLASS(klass)->finalize = recent_contacts_view_finalize;
701 G_OBJECT_CLASS(klass)->dispose = recent_contacts_view_dispose;
702}
703
704GtkWidget *
705recent_contacts_view_new()
706{
707 gpointer self = g_object_new(RECENT_CONTACTS_VIEW_TYPE, NULL);
708
709 return (GtkWidget *)self;
710}