blob: f903fbae00e87ff4dfa69b560428023b0662bd2f [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 Salenikovich3026bb32017-04-27 13:31:48 -040073update_selection(GtkTreeSelection *selection)
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040074{
Stepan Salenikovich3026bb32017-04-27 13:31:48 -040075 if (gtk_q_tree_model_ignore_selection_change(selection)) return;
76
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040077 auto current_proxy = get_index_from_selection(selection);
Guillaume Roguez5d1514b2015-10-22 15:55:31 -040078 auto current = RecentModel::instance().peopleProxy()->mapToSource(current_proxy);
Stepan Salenikovichc1323422016-01-06 10:54:44 -050079
80 RecentModel::instance().selectionModel()->setCurrentIndex(current, QItemSelectionModel::ClearAndSelect);
81
82 // update the CallModel selection since we rely on the UserActionModel
Guillaume Roguez5d1514b2015-10-22 15:55:31 -040083 if (auto call_to_select = RecentModel::instance().getActiveCall(current)) {
84 CallModel::instance().selectCall(call_to_select);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040085 } else {
Guillaume Roguez5d1514b2015-10-22 15:55:31 -040086 CallModel::instance().selectionModel()->clearCurrentIndex();
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040087 }
88}
89
90static void
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040091render_contact_photo(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
92 GtkCellRenderer *cell,
93 GtkTreeModel *model,
94 GtkTreeIter *iter,
95 G_GNUC_UNUSED gpointer data)
96{
Stepan Salenikovichf6078222016-10-03 17:31:16 -040097 QModelIndex idx = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(model), iter);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -040098
Stepan Salenikovichd8765072016-01-14 10:58:51 -050099 std::shared_ptr<GdkPixbuf> image;
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400100 /* we only want to render a photo for the top nodes: Person, ContactMethod (, later Conference) */
101 QVariant object = idx.data(static_cast<int>(Ring::Role::Object));
102 if (idx.isValid() && object.isValid()) {
103 QVariant var_photo;
104 if (auto person = object.value<Person *>()) {
aviauc372e812016-12-01 16:13:16 -0500105 var_photo = GlobalInstances::pixmapManipulator().contactPhoto(person, QSize(50, 50), true);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400106 } else if (auto cm = object.value<ContactMethod *>()) {
107 /* get photo, note that this should in all cases be the fallback avatar, since there
108 * shouldn't be a person associated with this contact method */
aviauc372e812016-12-01 16:13:16 -0500109 var_photo = GlobalInstances::pixmapManipulator().callPhoto(cm, QSize(50, 50), true);
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500110 } else if (auto call = object.value<Call *>()) {
111 if (call->type() == Call::Type::CONFERENCE) {
aviauc372e812016-12-01 16:13:16 -0500112 var_photo = GlobalInstances::pixmapManipulator().callPhoto(call, QSize(50, 50), true);
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500113 }
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400114 }
Stepan Salenikovichd8765072016-01-14 10:58:51 -0500115 if (var_photo.isValid()) {
aviauc372e812016-12-01 16:13:16 -0500116 image = var_photo.value<std::shared_ptr<GdkPixbuf>>();
Stepan Salenikovichd8765072016-01-14 10:58:51 -0500117 } else {
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400118 // set the width of the cell rendered to the with of the photo
119 // so that the other renderers are shifted to the right
120 g_object_set(G_OBJECT(cell), "width", 50, NULL);
121 }
122 }
123
Stepan Salenikovichd8765072016-01-14 10:58:51 -0500124 g_object_set(G_OBJECT(cell), "pixbuf", image.get(), NULL);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400125}
126
127static void
128render_name_and_info(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
129 GtkCellRenderer *cell,
130 GtkTreeModel *model,
131 GtkTreeIter *iter,
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400132 GtkTreeView *treeview)
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400133{
134 gchar *text = NULL;
135
Stepan Salenikovichf6078222016-10-03 17:31:16 -0400136 QModelIndex idx = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(model), iter);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400137
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400138 // check if this iter is selected
139 gboolean is_selected = FALSE;
140 if (GTK_IS_TREE_VIEW(treeview)) {
141 auto selection = gtk_tree_view_get_selection(treeview);
142 is_selected = gtk_tree_selection_iter_is_selected(selection, iter);
143 }
144
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400145 auto type = idx.data(static_cast<int>(Ring::Role::ObjectType));
146 if (idx.isValid() && type.isValid()) {
147 switch (type.value<Ring::ObjectType>()) {
148 case Ring::ObjectType::Person:
149 case Ring::ObjectType::ContactMethod:
150 {
151 auto var_name = idx.data(static_cast<int>(Ring::Role::Name));
152 auto var_lastused = idx.data(static_cast<int>(Ring::Role::LastUsed));
153 auto var_status = idx.data(static_cast<int>(Ring::Role::FormattedState));
154
155 QString name, status;
156
157 if (var_name.isValid())
158 name = var_name.value<QString>();
159
160 // show the status if there is a call, otherwise the last used date/time
161 if (var_status.isValid()) {
162 status += var_status.value<QString>();
163 }else if (var_lastused.isValid()) {
164 auto date_time = var_lastused.value<QDateTime>();
165 auto category = HistoryTimeCategoryModel::timeToHistoryConst(date_time.toTime_t());
166
167 // if it is 'today', then we only want to show the time
168 if (category != HistoryTimeCategoryModel::HistoryConst::Today) {
169 status += HistoryTimeCategoryModel::timeToHistoryCategory(date_time.toTime_t());
170 }
171 // we only want to show the time if it is less than a week ago
172 if (category < HistoryTimeCategoryModel::HistoryConst::A_week_ago) {
173 if (!status.isEmpty())
174 status += ", ";
175 status += QLocale::system().toString(date_time.time(), QLocale::ShortFormat);
176 }
177 }
178
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400179 /* we want the color of the status text to be the default color if this iter is
180 * selected so that the treeview is able to invert it against the selection color */
181 if (is_selected) {
Stepan Salenikovich0731de02016-05-17 17:47:16 -0400182 text = g_markup_printf_escaped("%s\n<span size=\"smaller\">%s</span>",
183 name.toUtf8().constData(),
184 status.toUtf8().constData());
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400185 } else {
Stepan Salenikovich0731de02016-05-17 17:47:16 -0400186 text = g_markup_printf_escaped("%s\n<span size=\"smaller\" color=\"gray\">%s</span>",
187 name.toUtf8().constData(),
188 status.toUtf8().constData());
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400189 }
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400190 }
191 break;
192 case Ring::ObjectType::Call:
193 {
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500194 // check if it is a conference
195 auto idx_source = RecentModel::instance().peopleProxy()->mapToSource(idx);
196 auto is_conference = RecentModel::instance().isConference(idx_source);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400197
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500198 if (is_conference) {
199 auto var_name = idx.data(static_cast<int>(Ring::Role::Name));
Stepan Salenikovich0731de02016-05-17 17:47:16 -0400200 text = g_markup_escape_text(var_name.value<QString>().toUtf8().constData(), -1);
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500201 } else {
202 auto parent_source = RecentModel::instance().peopleProxy()->mapToSource(idx.parent());
203 if (RecentModel::instance().isConference(parent_source)) {
204 // part of conference, simply display the name
205 auto var_name = idx.data(static_cast<int>(Ring::Role::Name));
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400206
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500207 /* we want the color of the name text to be the default color if this iter is
208 * selected so that the treeview is able to invert it against the selection color */
Stepan Salenikovich0731de02016-05-17 17:47:16 -0400209 if (is_selected) {
210 text = g_markup_printf_escaped("<span size=\"smaller\">%s</span>",
211 var_name.value<QString>().toUtf8().constData());
212 } else {
213 text = g_markup_printf_escaped("<span size=\"smaller\" color=\"gray\">%s</span>",
214 var_name.value<QString>().toUtf8().constData());
215 }
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500216 } else {
217 // just a call, so display the state
218 auto var_status = idx.data(static_cast<int>(Ring::Role::FormattedState));
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400219
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500220 QString status;
221
222 if (var_status.isValid())
223 status += var_status.value<QString>();
224
225 /* we want the color of the status text to be the default color if this iter is
226 * selected so that the treeview is able to invert it against the selection color */
Stepan Salenikovich0731de02016-05-17 17:47:16 -0400227 if (is_selected) {
228 text = g_markup_printf_escaped("<span size=\"smaller\">%s</span>",
229 status.toUtf8().constData());
230 } else {
231 text = g_markup_printf_escaped("<span size=\"smaller\" color=\"gray\">%s</span>",
232 status.toUtf8().constData());
233 }
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500234 }
235 }
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400236 }
237 break;
238 case Ring::ObjectType::Media:
aviau9a277472016-12-13 14:54:58 -0500239 case Ring::ObjectType::Certificate:
Nicolas Jagerc74f7612017-03-28 09:48:18 -0400240 case Ring::ObjectType::ContactRequest:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400241 // nothing to do for now
Stepan Salenikovich4e7ea712015-12-24 14:04:37 -0500242 case Ring::ObjectType::COUNT__:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400243 break;
244 }
245 }
246
247 g_object_set(G_OBJECT(cell), "markup", text, NULL);
248 g_free(text);
249}
250
251static void
252render_call_duration(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
253 GtkCellRenderer *cell,
254 GtkTreeModel *model,
255 GtkTreeIter *iter,
256 G_GNUC_UNUSED gpointer data)
257{
258 gchar *text = NULL;
259
Stepan Salenikovichf6078222016-10-03 17:31:16 -0400260 QModelIndex idx = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(model), iter);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400261
262 auto type = idx.data(static_cast<int>(Ring::Role::ObjectType));
263 if (idx.isValid() && type.isValid()) {
264 switch (type.value<Ring::ObjectType>()) {
265 case Ring::ObjectType::Person:
266 case Ring::ObjectType::ContactMethod:
267 {
268 // check if there are any children (calls); we need to convert to source model in
269 // case there is only one
Guillaume Roguez5d1514b2015-10-22 15:55:31 -0400270 auto idx_source = RecentModel::instance().peopleProxy()->mapToSource(idx);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400271 auto duration = idx.data(static_cast<int>(Ring::Role::Length));
272 if (idx_source.isValid()
273 && (idx_source.model()->rowCount(idx_source) == 1)
274 && duration.isValid())
275 {
Stepan Salenikovich0731de02016-05-17 17:47:16 -0400276 text = g_markup_escape_text(duration.value<QString>().toUtf8().constData(), -1);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400277 }
aviauc372e812016-12-01 16:13:16 -0500278 else
279 {
280 auto unread = idx.data(static_cast<int>(Ring::Role::UnreadTextMessageCount)).toInt();
281 if (unread > 0){
282 text = g_markup_printf_escaped("<span color=\"red\" font_weight=\"bold\">%d</span>", unread);
283 }
284 }
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400285 }
286 break;
287 case Ring::ObjectType::Call:
288 {
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500289 // do not display the duration if the call is part of a conference
290 auto parent_source = RecentModel::instance().peopleProxy()->mapToSource(idx.parent());
291 auto in_conference = RecentModel::instance().isConference(parent_source);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400292
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500293 if (!in_conference) {
294 auto duration = idx.data(static_cast<int>(Ring::Role::Length));
295
296 if (duration.isValid())
Stepan Salenikovich0731de02016-05-17 17:47:16 -0400297 text = g_markup_escape_text(duration.value<QString>().toUtf8().constData(), -1);
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500298 }
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400299 }
300 break;
301 case Ring::ObjectType::Media:
aviau9a277472016-12-13 14:54:58 -0500302 case Ring::ObjectType::Certificate:
Nicolas Jagerc74f7612017-03-28 09:48:18 -0400303 case Ring::ObjectType::ContactRequest:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400304 // nothing to do for now
Stepan Salenikovich4e7ea712015-12-24 14:04:37 -0500305 case Ring::ObjectType::COUNT__:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400306 break;
307 }
308 }
309
310 g_object_set(G_OBJECT(cell), "markup", text, NULL);
311 g_free(text);
312}
313
314static void
315activate_item(GtkTreeView *tree_view,
316 GtkTreePath *path,
317 G_GNUC_UNUSED GtkTreeViewColumn *column,
318 G_GNUC_UNUSED gpointer user_data)
319{
320 auto model = gtk_tree_view_get_model(tree_view);
321 GtkTreeIter iter;
322 if (gtk_tree_model_get_iter(model, &iter, path)) {
Stepan Salenikovichf6078222016-10-03 17:31:16 -0400323 QModelIndex idx = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(model), &iter);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400324 auto type = idx.data(static_cast<int>(Ring::Role::ObjectType));
325 if (idx.isValid() && type.isValid()) {
326 switch (type.value<Ring::ObjectType>()) {
327 case Ring::ObjectType::Person:
328 {
329 // call the last used contact method
330 // TODO: if no contact methods have been used, offer a popup to Choose
331 auto p_var = idx.data(static_cast<int>(Ring::Role::Object));
332 if (p_var.isValid()) {
333 auto person = p_var.value<Person *>();
334 auto cms = person->phoneNumbers();
335
336 if (!cms.isEmpty()) {
337 auto last_used_cm = cms.at(0);
338 for (int i = 1; i < cms.size(); ++i) {
339 auto new_cm = cms.at(i);
340 if (difftime(new_cm->lastUsed(), last_used_cm->lastUsed()) > 0)
341 last_used_cm = new_cm;
342 }
343
344 place_new_call(last_used_cm);
345 }
346 }
347 }
348 break;
349 case Ring::ObjectType::ContactMethod:
350 {
351 // call the contact method
352 auto cm = idx.data(static_cast<int>(Ring::Role::Object));
353 if (cm.isValid())
354 place_new_call(cm.value<ContactMethod *>());
355 }
356 break;
357 case Ring::ObjectType::Call:
358 case Ring::ObjectType::Media:
aviau9a277472016-12-13 14:54:58 -0500359 case Ring::ObjectType::Certificate:
Nicolas Jagerc74f7612017-03-28 09:48:18 -0400360 case Ring::ObjectType::ContactRequest:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400361 // nothing to do for now
Stepan Salenikovich4e7ea712015-12-24 14:04:37 -0500362 case Ring::ObjectType::COUNT__:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400363 break;
364 }
365 }
366 }
367}
368
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400369static void
370expand_if_child(G_GNUC_UNUSED GtkTreeModel *tree_model,
371 GtkTreePath *path,
372 G_GNUC_UNUSED GtkTreeIter *iter,
373 GtkTreeView *treeview)
374{
375 if (gtk_tree_path_get_depth(path) > 1)
376 gtk_tree_view_expand_to_path(treeview, path);
377}
378
379static void
Stepan Salenikovichec0862f2015-10-16 15:49:42 -0400380scroll_to_selection(GtkTreeSelection *selection)
381{
Stepan Salenikovich3026bb32017-04-27 13:31:48 -0400382 auto treeview = gtk_tree_selection_get_tree_view(selection);
383 auto model = gtk_tree_view_get_model(treeview);
384 if (gtk_q_tree_model_is_layout_changing(GTK_Q_TREE_MODEL(model))) {
385 /* during a layout change, the GtkTreeModel items will be removed, so the GTK selection will
386 * be empty, we want to ignore this */
387 return;
388 }
389
Stepan Salenikovichec0862f2015-10-16 15:49:42 -0400390 GtkTreeIter iter;
391 if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
392 auto path = gtk_tree_model_get_path(model, &iter);
393 auto treeview = gtk_tree_selection_get_tree_view(selection);
394 gtk_tree_view_scroll_to_cell(treeview, path, nullptr, FALSE, 0.0, 0.0);
395 }
396}
397
398static void
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500399on_drag_data_get(GtkWidget *treeview,
400 G_GNUC_UNUSED GdkDragContext *context,
401 GtkSelectionData *data,
402 G_GNUC_UNUSED guint info,
403 G_GNUC_UNUSED guint time,
404 G_GNUC_UNUSED gpointer user_data)
405{
406 g_return_if_fail(IS_RECENT_CONTACTS_VIEW(treeview));
407
408 /* we always drag the selected row */
409 auto selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(treeview));
410 GtkTreeModel *model = NULL;
411 GtkTreeIter iter;
412
413 if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
414 auto path_str = gtk_tree_model_get_string_from_iter(model, &iter);
415
416 gtk_selection_data_set(data,
417 gdk_atom_intern_static_string(CALL_TARGET),
418 8, /* bytes */
419 (guchar *)path_str,
420 strlen(path_str) + 1);
421
422 g_free(path_str);
423 } else {
424 g_warning("drag selection not valid");
425 }
426}
427
428static gboolean
429on_drag_drop(GtkWidget *treeview,
430 GdkDragContext *context,
431 gint x,
432 gint y,
433 guint time,
434 G_GNUC_UNUSED gpointer user_data)
435{
436 g_return_val_if_fail(IS_RECENT_CONTACTS_VIEW(treeview), FALSE);
437
438 GtkTreePath *path = NULL;
439 GtkTreeViewDropPosition drop_pos;
440
441 if (gtk_tree_view_get_dest_row_at_pos(GTK_TREE_VIEW(treeview),
442 x, y, &path, &drop_pos)) {
443
444 GdkAtom target_type = gtk_drag_dest_find_target(treeview, context, NULL);
445
446 if (target_type != GDK_NONE) {
447 g_debug("can drop");
448 gtk_drag_get_data(treeview, context, target_type, time);
449 return TRUE;
450 }
451
452 gtk_tree_path_free(path);
453 }
454
455 return FALSE;
456}
457
458static gboolean
459on_drag_motion(GtkWidget *treeview,
460 GdkDragContext *context,
461 gint x,
462 gint y,
463 guint time,
464 G_GNUC_UNUSED gpointer user_data)
465{
466 g_return_val_if_fail(IS_RECENT_CONTACTS_VIEW(treeview), FALSE);
467
468 GtkTreePath *path = NULL;
469 GtkTreeViewDropPosition drop_pos;
470
471 if (gtk_tree_view_get_dest_row_at_pos(GTK_TREE_VIEW(treeview),
472 x, y, &path, &drop_pos)) {
473 // we only want to drop on a row, not before or after
474 if (drop_pos == GTK_TREE_VIEW_DROP_BEFORE) {
475 gtk_tree_view_set_drag_dest_row(GTK_TREE_VIEW(treeview), path, GTK_TREE_VIEW_DROP_INTO_OR_BEFORE);
476 } else if (drop_pos == GTK_TREE_VIEW_DROP_AFTER) {
477 gtk_tree_view_set_drag_dest_row(GTK_TREE_VIEW(treeview), path, GTK_TREE_VIEW_DROP_INTO_OR_AFTER);
478 }
479 gdk_drag_status(context, gdk_drag_context_get_suggested_action(context), time);
480 return TRUE;
481 } else {
482 // not a row in the treeview, so we cannot drop
483 return FALSE;
484 }
485}
486
487static void
488on_drag_data_received(GtkWidget *treeview,
489 GdkDragContext *context,
490 gint x,
491 gint y,
492 GtkSelectionData *data,
493 G_GNUC_UNUSED guint info,
494 guint time,
495 G_GNUC_UNUSED gpointer user_data)
496{
497 g_return_if_fail(IS_RECENT_CONTACTS_VIEW(treeview));
498
499 gboolean success = FALSE;
500
501 /* get the source and destination calls */
502 auto path_str_source = (gchar *)gtk_selection_data_get_data(data);
503 auto type = gtk_selection_data_get_data_type(data);
504 g_debug("data type: %s", gdk_atom_name(type));
505 if (path_str_source && strlen(path_str_source) > 0) {
506 g_debug("source path: %s", path_str_source);
507
508 /* get the destination path */
509 GtkTreePath *dest_path = NULL;
510 if (gtk_tree_view_get_dest_row_at_pos(GTK_TREE_VIEW(treeview), x, y, &dest_path, NULL)) {
511 auto model = gtk_tree_view_get_model(GTK_TREE_VIEW(treeview));
512
513 GtkTreeIter source;
514 gtk_tree_model_get_iter_from_string(model, &source, path_str_source);
Stepan Salenikovichf6078222016-10-03 17:31:16 -0400515 auto idx_source_proxy = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(model), &source);
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500516
517 GtkTreeIter dest;
518 gtk_tree_model_get_iter(model, &dest, dest_path);
Stepan Salenikovichf6078222016-10-03 17:31:16 -0400519 auto idx_dest_proxy = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(model), &dest);
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500520
521 // get call objects and indeces from RecentModel indeces being drag and dropped
522 auto idx_source = RecentModel::instance().peopleProxy()->mapToSource(idx_source_proxy);
523 auto idx_dest = RecentModel::instance().peopleProxy()->mapToSource(idx_dest_proxy);
524 auto call_source = RecentModel::instance().getActiveCall(idx_source);
525 auto call_dest = RecentModel::instance().getActiveCall(idx_dest);
526 auto idx_call_source = CallModel::instance().getIndex(call_source);
527 auto idx_call_dest = CallModel::instance().getIndex(call_dest);
528
529 if (idx_call_source.isValid() && idx_call_dest.isValid()) {
530 QModelIndexList source_list;
531 source_list << idx_call_source;
532 auto mimeData = CallModel::instance().mimeData(source_list);
533 auto action = Call::DropAction::Conference;
534 mimeData->setProperty("dropAction", action);
535
536 if (CallModel::instance().dropMimeData(mimeData, Qt::MoveAction, idx_call_dest.row(), idx_call_dest.column(), idx_call_dest.parent())) {
537 success = TRUE;
538 } else {
539 g_warning("could not drop mime data");
540 }
541 } else {
542 g_warning("source or dest call not valid");
543 }
544
545 gtk_tree_path_free(dest_path);
546 }
547 }
548
549 gtk_drag_finish(context, success, FALSE, time);
550}
551
Stepan Salenikovich3026bb32017-04-27 13:31:48 -0400552static gboolean
553synchronize_selection(RecentContactsView *self)
554{
555 auto idx = RecentModel::instance().selectionModel()->currentIndex();
556 auto selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(self));
557 auto model = gtk_tree_view_get_model(GTK_TREE_VIEW(self));
558
559 if (gtk_q_tree_model_is_layout_changing(GTK_Q_TREE_MODEL(model))) {
560 /* during a layout change, the GtkTreeModel items will be removed, so the GTK selection will
561 * be empty, we want to avoid trying to sync during this time, and reschedule it to try
562 * again later */
563 return G_SOURCE_CONTINUE;
564 }
565
566 auto idx_proxy = RecentModel::instance().peopleProxy()->mapFromSource(idx);
567
568 if (idx_proxy.isValid()) {
569 /* select the current */
570 GtkTreeIter iter;
571 if (gtk_q_tree_model_source_index_to_iter(GTK_Q_TREE_MODEL(model), idx_proxy, &iter)) {
572 gtk_tree_selection_select_iter(selection, &iter);
573 }
574 } else {
575 gtk_tree_selection_unselect_all(selection);
576 }
577
578 return G_SOURCE_REMOVE;
579}
580
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500581static void
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400582recent_contacts_view_init(RecentContactsView *self)
583{
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400584 RecentContactsViewPrivate *priv = RECENT_CONTACTS_VIEW_GET_PRIVATE(self);
585
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400586 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(self), FALSE);
587 /* no need to show the expander since it will always be expanded */
588 gtk_tree_view_set_show_expanders(GTK_TREE_VIEW(self), FALSE);
589 /* disable default search, we will handle it ourselves via LRC;
590 * otherwise the search steals input focus on key presses */
591 gtk_tree_view_set_enable_search(GTK_TREE_VIEW(self), FALSE);
592
Stepan Salenikovichf6078222016-10-03 17:31:16 -0400593 GtkQTreeModel *recent_model = gtk_q_tree_model_new(
Guillaume Roguez5d1514b2015-10-22 15:55:31 -0400594 RecentModel::instance().peopleProxy(),
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400595 1,
aviau271bcc22016-05-27 17:25:19 -0400596 0, Qt::DisplayRole, G_TYPE_STRING);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400597
598 gtk_tree_view_set_model(GTK_TREE_VIEW(self),
599 GTK_TREE_MODEL(recent_model));
600
601 /* photo and name/contact method column */
602 GtkCellArea *area = gtk_cell_area_box_new();
603 GtkTreeViewColumn *column = gtk_tree_view_column_new_with_area(area);
604
605 /* photo renderer */
606 GtkCellRenderer *renderer = gtk_cell_renderer_pixbuf_new();
607 gtk_cell_area_box_pack_start(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
608
609 /* get the photo */
610 gtk_tree_view_column_set_cell_data_func(
611 column,
612 renderer,
613 (GtkTreeCellDataFunc)render_contact_photo,
614 NULL,
615 NULL);
616
617 /* name/cm and status renderer */
618 renderer = gtk_cell_renderer_text_new();
619 g_object_set(G_OBJECT(renderer), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
620 gtk_cell_area_box_pack_start(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
621
622 gtk_tree_view_column_set_cell_data_func(
623 column,
624 renderer,
625 (GtkTreeCellDataFunc)render_name_and_info,
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400626 self,
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400627 NULL);
628
aviauc372e812016-12-01 16:13:16 -0500629 /* call duration or unread messages */
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400630 renderer = gtk_cell_renderer_text_new();
Stepan Salenikoviche8ef9352015-10-27 19:07:46 -0400631 g_object_set(G_OBJECT(renderer), "xalign", 1.0, NULL);
632 gtk_cell_area_box_pack_end(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400633 gtk_tree_view_column_set_cell_data_func(
634 column,
635 renderer,
636 (GtkTreeCellDataFunc)render_call_duration,
637 NULL,
638 NULL);
639
Stepan Salenikoviche8ef9352015-10-27 19:07:46 -0400640 gtk_tree_view_append_column(GTK_TREE_VIEW(self), column);
641 gtk_tree_view_column_set_resizable(column, TRUE);
642 gtk_tree_view_column_set_expand(column, TRUE);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400643 gtk_tree_view_expand_all(GTK_TREE_VIEW(self));
644
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400645 g_signal_connect(self, "row-activated", G_CALLBACK(activate_item), NULL);
646 g_signal_connect(recent_model, "row-inserted", G_CALLBACK(expand_if_child), self);
647
648 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(self));
Stepan Salenikovichc1323422016-01-06 10:54:44 -0500649 g_signal_connect(selection, "changed", G_CALLBACK(update_selection), NULL);
Stepan Salenikovichec0862f2015-10-16 15:49:42 -0400650 g_signal_connect(selection, "changed", G_CALLBACK(scroll_to_selection), NULL);
651 g_signal_connect_swapped(recent_model, "rows-reordered", G_CALLBACK(scroll_to_selection), selection);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400652
Stepan Salenikovich3026bb32017-04-27 13:31:48 -0400653 auto synchronize_selection_idle = [self] () { g_idle_add((GSourceFunc)synchronize_selection, self); };
654
Stepan Salenikovichc1323422016-01-06 10:54:44 -0500655 /* update the selection based on the RecentModel */
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400656 priv->selection_updated = QObject::connect(
Stepan Salenikovichc1323422016-01-06 10:54:44 -0500657 RecentModel::instance().selectionModel(),
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400658 &QItemSelectionModel::currentChanged,
Stepan Salenikovich3026bb32017-04-27 13:31:48 -0400659 synchronize_selection_idle
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400660 );
661
Stepan Salenikovichf53128f2016-10-07 10:32:16 -0400662 /* we may need to update the selection when the layout changes */
663 priv->layout_changed = QObject::connect(
664 RecentModel::instance().peopleProxy(),
665 &QAbstractItemModel::layoutChanged,
Stepan Salenikovich3026bb32017-04-27 13:31:48 -0400666 synchronize_selection_idle
Stepan Salenikovichf53128f2016-10-07 10:32:16 -0400667 );
668
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500669 /* drag and drop */
670 static GtkTargetEntry targetentries[] = {
671 { (gchar *)CALL_TARGET, GTK_TARGET_SAME_WIDGET, CALL_TARGET_ID },
672 };
673
674 gtk_tree_view_enable_model_drag_source(GTK_TREE_VIEW(self),
675 GDK_BUTTON1_MASK, targetentries, 1, (GdkDragAction)(GDK_ACTION_DEFAULT | GDK_ACTION_MOVE));
676
677 gtk_tree_view_enable_model_drag_dest(GTK_TREE_VIEW(self),
678 targetentries, 1, GDK_ACTION_DEFAULT);
679
680 g_signal_connect(self, "drag-data-get", G_CALLBACK(on_drag_data_get), nullptr);
681 g_signal_connect(self, "drag-drop", G_CALLBACK(on_drag_drop), nullptr);
682 g_signal_connect(self, "drag-motion", G_CALLBACK(on_drag_motion), nullptr);
683 g_signal_connect(self, "drag_data_received", G_CALLBACK(on_drag_data_received), nullptr);
684
Stepan Salenikovich8eaa13e2016-08-26 16:51:48 -0400685 /* init popup menu */
686 priv->popup_menu = contact_popup_menu_new(GTK_TREE_VIEW(self));
687 g_signal_connect_swapped(self, "button-press-event", G_CALLBACK(contact_popup_menu_show), priv->popup_menu);
688
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400689 gtk_widget_show_all(GTK_WIDGET(self));
690}
691
692static void
693recent_contacts_view_dispose(GObject *object)
694{
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400695 RecentContactsView *self = RECENT_CONTACTS_VIEW(object);
696 RecentContactsViewPrivate *priv = RECENT_CONTACTS_VIEW_GET_PRIVATE(self);
697
698 QObject::disconnect(priv->selection_updated);
Stepan Salenikovichf53128f2016-10-07 10:32:16 -0400699 QObject::disconnect(priv->layout_changed);
Stepan Salenikovich8eaa13e2016-08-26 16:51:48 -0400700 gtk_widget_destroy(priv->popup_menu);
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400701
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400702 G_OBJECT_CLASS(recent_contacts_view_parent_class)->dispose(object);
703}
704
705static void
706recent_contacts_view_finalize(GObject *object)
707{
708 G_OBJECT_CLASS(recent_contacts_view_parent_class)->finalize(object);
709}
710
711static void
712recent_contacts_view_class_init(RecentContactsViewClass *klass)
713{
714 G_OBJECT_CLASS(klass)->finalize = recent_contacts_view_finalize;
715 G_OBJECT_CLASS(klass)->dispose = recent_contacts_view_dispose;
716}
717
718GtkWidget *
719recent_contacts_view_new()
720{
721 gpointer self = g_object_new(RECENT_CONTACTS_VIEW_TYPE, NULL);
722
723 return (GtkWidget *)self;
724}