blob: 6b5fb11d817a14aee0988ff93b9c90155bdacf56 [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 Salenikoviche084a8c2017-05-11 17:06:18 -0400115 image = var_photo.value<std::shared_ptr<GdkPixbuf>>();
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400116 }
117
Stepan Salenikoviche084a8c2017-05-11 17:06:18 -0400118 // set the width of the cell rendered to the width of the photo
119 // so that the other renderers are shifted to the right
120 g_object_set(G_OBJECT(cell), "width", 50, NULL);
Stepan Salenikovichd8765072016-01-14 10:58:51 -0500121 g_object_set(G_OBJECT(cell), "pixbuf", image.get(), NULL);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400122}
123
Stepan Salenikoviche084a8c2017-05-11 17:06:18 -0400124/**
125 * This is the 2nd column in the treeview; for Person and ContactMethod items we want to DisplayRole
126 * the name in the first row and the number (Ring registered name or URI) in the second row. for
127 * Conferences we simply display the name, and for calls we simply display the Call status (note:
128 * that if the item is a Call, it is not a top level item)
129 */
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400130static void
Stepan Salenikoviche084a8c2017-05-11 17:06:18 -0400131render_name_and_number(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
132 GtkCellRenderer *cell,
133 GtkTreeModel *model,
134 GtkTreeIter *iter,
135 GtkTreeView *treeview)
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400136{
Stepan Salenikoviche084a8c2017-05-11 17:06:18 -0400137 gchar *text = nullptr;
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400138
Stepan Salenikovichf6078222016-10-03 17:31:16 -0400139 QModelIndex idx = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(model), iter);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400140
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400141 // check if this iter is selected
142 gboolean is_selected = FALSE;
143 if (GTK_IS_TREE_VIEW(treeview)) {
144 auto selection = gtk_tree_view_get_selection(treeview);
145 is_selected = gtk_tree_selection_iter_is_selected(selection, iter);
146 }
147
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400148 auto type = idx.data(static_cast<int>(Ring::Role::ObjectType));
149 if (idx.isValid() && type.isValid()) {
150 switch (type.value<Ring::ObjectType>()) {
151 case Ring::ObjectType::Person:
152 case Ring::ObjectType::ContactMethod:
153 {
Stepan Salenikoviche084a8c2017-05-11 17:06:18 -0400154 auto name = idx.data(static_cast<int>(Ring::Role::Name)).toString();
155 auto number = idx.data(static_cast<int>(Ring::Role::Number)).toString();
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400156
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400157 /* we want the color of the status text to be the default color if this iter is
158 * selected so that the treeview is able to invert it against the selection color */
159 if (is_selected) {
Stepan Salenikoviche084a8c2017-05-11 17:06:18 -0400160 text = g_markup_printf_escaped(
161 "<span font_weight=\"bold\">%s</span>\n<span size=\"smaller\">%s</span>",
162 // "%s\n<span size=\"smaller\">%s</span>",
163 name.toUtf8().constData(),
164 number.toUtf8().constData()
165 );
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400166 } else {
Stepan Salenikoviche084a8c2017-05-11 17:06:18 -0400167 text = g_markup_printf_escaped(
168 "<span font_weight=\"bold\">%s</span>\n<span color=\"gray\" size=\"smaller\">%s</span>",
169 // "%s\n<span color=\"gray\" size=\"smaller\">%s</span>",
170 name.toUtf8().constData(),
171 number.toUtf8().constData()
172 );
173 }
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400174 }
175 break;
176 case Ring::ObjectType::Call:
177 {
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500178 // check if it is a conference
179 auto idx_source = RecentModel::instance().peopleProxy()->mapToSource(idx);
180 auto is_conference = RecentModel::instance().isConference(idx_source);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400181
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500182 if (is_conference) {
183 auto var_name = idx.data(static_cast<int>(Ring::Role::Name));
Stepan Salenikovich0731de02016-05-17 17:47:16 -0400184 text = g_markup_escape_text(var_name.value<QString>().toUtf8().constData(), -1);
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500185 } else {
186 auto parent_source = RecentModel::instance().peopleProxy()->mapToSource(idx.parent());
187 if (RecentModel::instance().isConference(parent_source)) {
188 // part of conference, simply display the name
189 auto var_name = idx.data(static_cast<int>(Ring::Role::Name));
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400190
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500191 /* we want the color of the name text to be the default color if this iter is
192 * selected so that the treeview is able to invert it against the selection color */
Stepan Salenikovich0731de02016-05-17 17:47:16 -0400193 if (is_selected) {
194 text = g_markup_printf_escaped("<span size=\"smaller\">%s</span>",
195 var_name.value<QString>().toUtf8().constData());
196 } else {
197 text = g_markup_printf_escaped("<span size=\"smaller\" color=\"gray\">%s</span>",
198 var_name.value<QString>().toUtf8().constData());
199 }
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500200 } else {
201 // just a call, so display the state
202 auto var_status = idx.data(static_cast<int>(Ring::Role::FormattedState));
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400203
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500204 QString status;
205
206 if (var_status.isValid())
207 status += var_status.value<QString>();
208
209 /* we want the color of the status 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 status.toUtf8().constData());
214 } else {
215 text = g_markup_printf_escaped("<span size=\"smaller\" color=\"gray\">%s</span>",
216 status.toUtf8().constData());
217 }
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500218 }
219 }
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400220 }
221 break;
222 case Ring::ObjectType::Media:
aviau9a277472016-12-13 14:54:58 -0500223 case Ring::ObjectType::Certificate:
Nicolas Jagerc74f7612017-03-28 09:48:18 -0400224 case Ring::ObjectType::ContactRequest:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400225 // nothing to do for now
Stepan Salenikovich4e7ea712015-12-24 14:04:37 -0500226 case Ring::ObjectType::COUNT__:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400227 break;
228 }
229 }
230
231 g_object_set(G_OBJECT(cell), "markup", text, NULL);
232 g_free(text);
233}
234
Stepan Salenikoviche084a8c2017-05-11 17:06:18 -0400235/**
236 * This is the 3rd column in the treeview. For Person and ContactMethod items we want to display
237 * in the first row the call status or else the last used date. In the second row we want to display
238 * either the call duration or else the number of unread messages. In the case of a Call item we
239 * simply display the call duration.
240 */
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400241static void
Stepan Salenikoviche084a8c2017-05-11 17:06:18 -0400242render_info(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
243 GtkCellRenderer *cell,
244 GtkTreeModel *model,
245 GtkTreeIter *iter,
246 GtkTreeView *treeview)
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400247{
Stepan Salenikoviche084a8c2017-05-11 17:06:18 -0400248 gchar *text = nullptr;
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400249
Stepan Salenikovichf6078222016-10-03 17:31:16 -0400250 QModelIndex idx = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(model), iter);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400251
Stepan Salenikoviche084a8c2017-05-11 17:06:18 -0400252 // check if this iter is selected
253 gboolean is_selected = FALSE;
254 if (GTK_IS_TREE_VIEW(treeview)) {
255 auto selection = gtk_tree_view_get_selection(treeview);
256 is_selected = gtk_tree_selection_iter_is_selected(selection, iter);
257 }
258
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400259 auto type = idx.data(static_cast<int>(Ring::Role::ObjectType));
260 if (idx.isValid() && type.isValid()) {
261 switch (type.value<Ring::ObjectType>()) {
262 case Ring::ObjectType::Person:
263 case Ring::ObjectType::ContactMethod:
264 {
Stepan Salenikoviche084a8c2017-05-11 17:06:18 -0400265 gchar *row0 = nullptr; // either call status or last used
266 gchar *row1 = nullptr; // either call duration or unread msg count
267
268 QString status;
269 auto var_status = idx.data(static_cast<int>(Ring::Role::FormattedState));
270 auto var_lastused = idx.data(static_cast<int>(Ring::Role::LastUsed));
271
272 // show the status if there is a call, otherwise the last used date/time
273 if (var_status.isValid()) {
274 status = var_status.value<QString>();
275 } else if (var_lastused.isValid()) {
276 auto date_time = var_lastused.value<QDateTime>();
277 auto category = HistoryTimeCategoryModel::timeToHistoryConst(date_time.toTime_t());
278
279 /* If it is 'today', then we show the time; otherwise we will show date category
280 * (the day or long ago it was). The day and the time together take up too much
281 * space */
282 if (category == HistoryTimeCategoryModel::HistoryConst::Today) {
283 status = QLocale::system().toString(date_time.time(), QLocale::ShortFormat);
284 } else {
285 status = HistoryTimeCategoryModel::timeToHistoryCategory(date_time.toTime_t());
286 }
287 }
288
289 if (is_selected) {
290 row0 = g_markup_printf_escaped(
291 "<span size=\"smaller\">%s</span>",
292 status.toUtf8().constData()
293 );
294 } else {
295 row0 = g_markup_printf_escaped(
296 "<span size=\"smaller\" color=\"gray\">%s</span>",
297 status.toUtf8().constData()
298 );
299 }
300
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400301 // check if there are any children (calls); we need to convert to source model in
302 // case there is only one
Guillaume Roguez5d1514b2015-10-22 15:55:31 -0400303 auto idx_source = RecentModel::instance().peopleProxy()->mapToSource(idx);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400304 auto duration = idx.data(static_cast<int>(Ring::Role::Length));
305 if (idx_source.isValid()
306 && (idx_source.model()->rowCount(idx_source) == 1)
307 && duration.isValid())
308 {
Stepan Salenikoviche084a8c2017-05-11 17:06:18 -0400309 row1 = g_markup_printf_escaped("%s", duration.toString().toUtf8().constData());
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400310 }
aviauc372e812016-12-01 16:13:16 -0500311 else
312 {
313 auto unread = idx.data(static_cast<int>(Ring::Role::UnreadTextMessageCount)).toInt();
Stepan Salenikoviche084a8c2017-05-11 17:06:18 -0400314 if (unread > 0) {
315 if (is_selected) {
316 row1 = g_markup_printf_escaped(
317 "<span font_weight=\"bold\">%d</span>",
318 unread
319 );
320 } else {
321 row1 = g_markup_printf_escaped(
322 "<span color=\"red\" font_weight=\"bold\">%d</span>",
323 unread
324 );
325 }
aviauc372e812016-12-01 16:13:16 -0500326 }
327 }
Stepan Salenikoviche084a8c2017-05-11 17:06:18 -0400328
329 text = g_strconcat(row0, "\n", row1, nullptr);
330 g_free(row0);
331 g_free(row1);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400332 }
333 break;
334 case Ring::ObjectType::Call:
335 {
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500336 // do not display the duration if the call is part of a conference
337 auto parent_source = RecentModel::instance().peopleProxy()->mapToSource(idx.parent());
338 auto in_conference = RecentModel::instance().isConference(parent_source);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400339
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500340 if (!in_conference) {
341 auto duration = idx.data(static_cast<int>(Ring::Role::Length));
342
343 if (duration.isValid())
Stepan Salenikovich0731de02016-05-17 17:47:16 -0400344 text = g_markup_escape_text(duration.value<QString>().toUtf8().constData(), -1);
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500345 }
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400346 }
347 break;
348 case Ring::ObjectType::Media:
aviau9a277472016-12-13 14:54:58 -0500349 case Ring::ObjectType::Certificate:
Nicolas Jagerc74f7612017-03-28 09:48:18 -0400350 case Ring::ObjectType::ContactRequest:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400351 // nothing to do for now
Stepan Salenikovich4e7ea712015-12-24 14:04:37 -0500352 case Ring::ObjectType::COUNT__:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400353 break;
354 }
355 }
356
357 g_object_set(G_OBJECT(cell), "markup", text, NULL);
358 g_free(text);
359}
360
361static void
362activate_item(GtkTreeView *tree_view,
363 GtkTreePath *path,
364 G_GNUC_UNUSED GtkTreeViewColumn *column,
365 G_GNUC_UNUSED gpointer user_data)
366{
367 auto model = gtk_tree_view_get_model(tree_view);
368 GtkTreeIter iter;
369 if (gtk_tree_model_get_iter(model, &iter, path)) {
Stepan Salenikovichf6078222016-10-03 17:31:16 -0400370 QModelIndex idx = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(model), &iter);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400371 auto type = idx.data(static_cast<int>(Ring::Role::ObjectType));
372 if (idx.isValid() && type.isValid()) {
373 switch (type.value<Ring::ObjectType>()) {
374 case Ring::ObjectType::Person:
375 {
376 // call the last used contact method
377 // TODO: if no contact methods have been used, offer a popup to Choose
378 auto p_var = idx.data(static_cast<int>(Ring::Role::Object));
379 if (p_var.isValid()) {
380 auto person = p_var.value<Person *>();
381 auto cms = person->phoneNumbers();
382
383 if (!cms.isEmpty()) {
384 auto last_used_cm = cms.at(0);
385 for (int i = 1; i < cms.size(); ++i) {
386 auto new_cm = cms.at(i);
387 if (difftime(new_cm->lastUsed(), last_used_cm->lastUsed()) > 0)
388 last_used_cm = new_cm;
389 }
390
391 place_new_call(last_used_cm);
392 }
393 }
394 }
395 break;
396 case Ring::ObjectType::ContactMethod:
397 {
398 // call the contact method
399 auto cm = idx.data(static_cast<int>(Ring::Role::Object));
400 if (cm.isValid())
401 place_new_call(cm.value<ContactMethod *>());
402 }
403 break;
404 case Ring::ObjectType::Call:
405 case Ring::ObjectType::Media:
aviau9a277472016-12-13 14:54:58 -0500406 case Ring::ObjectType::Certificate:
Nicolas Jagerc74f7612017-03-28 09:48:18 -0400407 case Ring::ObjectType::ContactRequest:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400408 // nothing to do for now
Stepan Salenikovich4e7ea712015-12-24 14:04:37 -0500409 case Ring::ObjectType::COUNT__:
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400410 break;
411 }
412 }
413 }
414}
415
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400416static void
417expand_if_child(G_GNUC_UNUSED GtkTreeModel *tree_model,
418 GtkTreePath *path,
419 G_GNUC_UNUSED GtkTreeIter *iter,
420 GtkTreeView *treeview)
421{
422 if (gtk_tree_path_get_depth(path) > 1)
423 gtk_tree_view_expand_to_path(treeview, path);
424}
425
426static void
Stepan Salenikovichec0862f2015-10-16 15:49:42 -0400427scroll_to_selection(GtkTreeSelection *selection)
428{
Stepan Salenikovich3026bb32017-04-27 13:31:48 -0400429 auto treeview = gtk_tree_selection_get_tree_view(selection);
430 auto model = gtk_tree_view_get_model(treeview);
431 if (gtk_q_tree_model_is_layout_changing(GTK_Q_TREE_MODEL(model))) {
432 /* during a layout change, the GtkTreeModel items will be removed, so the GTK selection will
433 * be empty, we want to ignore this */
434 return;
435 }
436
Stepan Salenikovichec0862f2015-10-16 15:49:42 -0400437 GtkTreeIter iter;
438 if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
439 auto path = gtk_tree_model_get_path(model, &iter);
440 auto treeview = gtk_tree_selection_get_tree_view(selection);
441 gtk_tree_view_scroll_to_cell(treeview, path, nullptr, FALSE, 0.0, 0.0);
442 }
443}
444
445static void
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500446on_drag_data_get(GtkWidget *treeview,
447 G_GNUC_UNUSED GdkDragContext *context,
448 GtkSelectionData *data,
449 G_GNUC_UNUSED guint info,
450 G_GNUC_UNUSED guint time,
451 G_GNUC_UNUSED gpointer user_data)
452{
453 g_return_if_fail(IS_RECENT_CONTACTS_VIEW(treeview));
454
455 /* we always drag the selected row */
456 auto selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(treeview));
457 GtkTreeModel *model = NULL;
458 GtkTreeIter iter;
459
460 if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
461 auto path_str = gtk_tree_model_get_string_from_iter(model, &iter);
462
463 gtk_selection_data_set(data,
464 gdk_atom_intern_static_string(CALL_TARGET),
465 8, /* bytes */
466 (guchar *)path_str,
467 strlen(path_str) + 1);
468
469 g_free(path_str);
470 } else {
471 g_warning("drag selection not valid");
472 }
473}
474
475static gboolean
476on_drag_drop(GtkWidget *treeview,
477 GdkDragContext *context,
478 gint x,
479 gint y,
480 guint time,
481 G_GNUC_UNUSED gpointer user_data)
482{
483 g_return_val_if_fail(IS_RECENT_CONTACTS_VIEW(treeview), FALSE);
484
485 GtkTreePath *path = NULL;
486 GtkTreeViewDropPosition drop_pos;
487
488 if (gtk_tree_view_get_dest_row_at_pos(GTK_TREE_VIEW(treeview),
489 x, y, &path, &drop_pos)) {
490
491 GdkAtom target_type = gtk_drag_dest_find_target(treeview, context, NULL);
492
493 if (target_type != GDK_NONE) {
494 g_debug("can drop");
495 gtk_drag_get_data(treeview, context, target_type, time);
496 return TRUE;
497 }
498
499 gtk_tree_path_free(path);
500 }
501
502 return FALSE;
503}
504
505static gboolean
506on_drag_motion(GtkWidget *treeview,
507 GdkDragContext *context,
508 gint x,
509 gint y,
510 guint time,
511 G_GNUC_UNUSED gpointer user_data)
512{
513 g_return_val_if_fail(IS_RECENT_CONTACTS_VIEW(treeview), FALSE);
514
515 GtkTreePath *path = NULL;
516 GtkTreeViewDropPosition drop_pos;
517
518 if (gtk_tree_view_get_dest_row_at_pos(GTK_TREE_VIEW(treeview),
519 x, y, &path, &drop_pos)) {
520 // we only want to drop on a row, not before or after
521 if (drop_pos == GTK_TREE_VIEW_DROP_BEFORE) {
522 gtk_tree_view_set_drag_dest_row(GTK_TREE_VIEW(treeview), path, GTK_TREE_VIEW_DROP_INTO_OR_BEFORE);
523 } else if (drop_pos == GTK_TREE_VIEW_DROP_AFTER) {
524 gtk_tree_view_set_drag_dest_row(GTK_TREE_VIEW(treeview), path, GTK_TREE_VIEW_DROP_INTO_OR_AFTER);
525 }
526 gdk_drag_status(context, gdk_drag_context_get_suggested_action(context), time);
527 return TRUE;
528 } else {
529 // not a row in the treeview, so we cannot drop
530 return FALSE;
531 }
532}
533
534static void
535on_drag_data_received(GtkWidget *treeview,
536 GdkDragContext *context,
537 gint x,
538 gint y,
539 GtkSelectionData *data,
540 G_GNUC_UNUSED guint info,
541 guint time,
542 G_GNUC_UNUSED gpointer user_data)
543{
544 g_return_if_fail(IS_RECENT_CONTACTS_VIEW(treeview));
545
546 gboolean success = FALSE;
547
548 /* get the source and destination calls */
549 auto path_str_source = (gchar *)gtk_selection_data_get_data(data);
550 auto type = gtk_selection_data_get_data_type(data);
551 g_debug("data type: %s", gdk_atom_name(type));
552 if (path_str_source && strlen(path_str_source) > 0) {
553 g_debug("source path: %s", path_str_source);
554
555 /* get the destination path */
556 GtkTreePath *dest_path = NULL;
557 if (gtk_tree_view_get_dest_row_at_pos(GTK_TREE_VIEW(treeview), x, y, &dest_path, NULL)) {
558 auto model = gtk_tree_view_get_model(GTK_TREE_VIEW(treeview));
559
560 GtkTreeIter source;
561 gtk_tree_model_get_iter_from_string(model, &source, path_str_source);
Stepan Salenikovichf6078222016-10-03 17:31:16 -0400562 auto idx_source_proxy = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(model), &source);
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500563
564 GtkTreeIter dest;
565 gtk_tree_model_get_iter(model, &dest, dest_path);
Stepan Salenikovichf6078222016-10-03 17:31:16 -0400566 auto idx_dest_proxy = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(model), &dest);
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500567
568 // get call objects and indeces from RecentModel indeces being drag and dropped
569 auto idx_source = RecentModel::instance().peopleProxy()->mapToSource(idx_source_proxy);
570 auto idx_dest = RecentModel::instance().peopleProxy()->mapToSource(idx_dest_proxy);
571 auto call_source = RecentModel::instance().getActiveCall(idx_source);
572 auto call_dest = RecentModel::instance().getActiveCall(idx_dest);
573 auto idx_call_source = CallModel::instance().getIndex(call_source);
574 auto idx_call_dest = CallModel::instance().getIndex(call_dest);
575
576 if (idx_call_source.isValid() && idx_call_dest.isValid()) {
577 QModelIndexList source_list;
578 source_list << idx_call_source;
579 auto mimeData = CallModel::instance().mimeData(source_list);
580 auto action = Call::DropAction::Conference;
581 mimeData->setProperty("dropAction", action);
582
583 if (CallModel::instance().dropMimeData(mimeData, Qt::MoveAction, idx_call_dest.row(), idx_call_dest.column(), idx_call_dest.parent())) {
584 success = TRUE;
585 } else {
586 g_warning("could not drop mime data");
587 }
588 } else {
589 g_warning("source or dest call not valid");
590 }
591
592 gtk_tree_path_free(dest_path);
593 }
594 }
595
596 gtk_drag_finish(context, success, FALSE, time);
597}
598
Stepan Salenikovich3026bb32017-04-27 13:31:48 -0400599static gboolean
600synchronize_selection(RecentContactsView *self)
601{
602 auto idx = RecentModel::instance().selectionModel()->currentIndex();
603 auto selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(self));
604 auto model = gtk_tree_view_get_model(GTK_TREE_VIEW(self));
605
606 if (gtk_q_tree_model_is_layout_changing(GTK_Q_TREE_MODEL(model))) {
607 /* during a layout change, the GtkTreeModel items will be removed, so the GTK selection will
608 * be empty, we want to avoid trying to sync during this time, and reschedule it to try
609 * again later */
610 return G_SOURCE_CONTINUE;
611 }
612
613 auto idx_proxy = RecentModel::instance().peopleProxy()->mapFromSource(idx);
614
615 if (idx_proxy.isValid()) {
616 /* select the current */
617 GtkTreeIter iter;
618 if (gtk_q_tree_model_source_index_to_iter(GTK_Q_TREE_MODEL(model), idx_proxy, &iter)) {
619 gtk_tree_selection_select_iter(selection, &iter);
620 }
621 } else {
622 gtk_tree_selection_unselect_all(selection);
623 }
624
625 return G_SOURCE_REMOVE;
626}
627
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500628static void
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400629recent_contacts_view_init(RecentContactsView *self)
630{
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400631 RecentContactsViewPrivate *priv = RECENT_CONTACTS_VIEW_GET_PRIVATE(self);
632
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400633 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(self), FALSE);
634 /* no need to show the expander since it will always be expanded */
635 gtk_tree_view_set_show_expanders(GTK_TREE_VIEW(self), FALSE);
636 /* disable default search, we will handle it ourselves via LRC;
637 * otherwise the search steals input focus on key presses */
638 gtk_tree_view_set_enable_search(GTK_TREE_VIEW(self), FALSE);
639
Stepan Salenikovichf6078222016-10-03 17:31:16 -0400640 GtkQTreeModel *recent_model = gtk_q_tree_model_new(
Guillaume Roguez5d1514b2015-10-22 15:55:31 -0400641 RecentModel::instance().peopleProxy(),
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400642 1,
aviau271bcc22016-05-27 17:25:19 -0400643 0, Qt::DisplayRole, G_TYPE_STRING);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400644
645 gtk_tree_view_set_model(GTK_TREE_VIEW(self),
646 GTK_TREE_MODEL(recent_model));
647
648 /* photo and name/contact method column */
649 GtkCellArea *area = gtk_cell_area_box_new();
650 GtkTreeViewColumn *column = gtk_tree_view_column_new_with_area(area);
651
652 /* photo renderer */
653 GtkCellRenderer *renderer = gtk_cell_renderer_pixbuf_new();
654 gtk_cell_area_box_pack_start(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
655
656 /* get the photo */
657 gtk_tree_view_column_set_cell_data_func(
658 column,
659 renderer,
660 (GtkTreeCellDataFunc)render_contact_photo,
661 NULL,
662 NULL);
663
664 /* name/cm and status renderer */
665 renderer = gtk_cell_renderer_text_new();
666 g_object_set(G_OBJECT(renderer), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
667 gtk_cell_area_box_pack_start(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
668
669 gtk_tree_view_column_set_cell_data_func(
670 column,
671 renderer,
Stepan Salenikoviche084a8c2017-05-11 17:06:18 -0400672 (GtkTreeCellDataFunc)render_name_and_number,
Stepan Salenikoviche4981b22015-10-22 15:22:59 -0400673 self,
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400674 NULL);
675
aviauc372e812016-12-01 16:13:16 -0500676 /* call duration or unread messages */
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400677 renderer = gtk_cell_renderer_text_new();
Stepan Salenikoviche8ef9352015-10-27 19:07:46 -0400678 g_object_set(G_OBJECT(renderer), "xalign", 1.0, NULL);
679 gtk_cell_area_box_pack_end(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400680 gtk_tree_view_column_set_cell_data_func(
681 column,
682 renderer,
Stepan Salenikoviche084a8c2017-05-11 17:06:18 -0400683 (GtkTreeCellDataFunc)render_info,
684 self,
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400685 NULL);
686
Stepan Salenikoviche8ef9352015-10-27 19:07:46 -0400687 gtk_tree_view_append_column(GTK_TREE_VIEW(self), column);
688 gtk_tree_view_column_set_resizable(column, TRUE);
689 gtk_tree_view_column_set_expand(column, TRUE);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400690 gtk_tree_view_expand_all(GTK_TREE_VIEW(self));
691
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400692 g_signal_connect(self, "row-activated", G_CALLBACK(activate_item), NULL);
693 g_signal_connect(recent_model, "row-inserted", G_CALLBACK(expand_if_child), self);
694
695 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(self));
Stepan Salenikovichc1323422016-01-06 10:54:44 -0500696 g_signal_connect(selection, "changed", G_CALLBACK(update_selection), NULL);
Stepan Salenikovichec0862f2015-10-16 15:49:42 -0400697 g_signal_connect(selection, "changed", G_CALLBACK(scroll_to_selection), NULL);
698 g_signal_connect_swapped(recent_model, "rows-reordered", G_CALLBACK(scroll_to_selection), selection);
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400699
Stepan Salenikovich709ff152017-04-28 16:38:15 -0400700 /* sync initial selection */
701 g_idle_add((GSourceFunc)synchronize_selection, self);
702
Stepan Salenikovich3026bb32017-04-27 13:31:48 -0400703 auto synchronize_selection_idle = [self] () { g_idle_add((GSourceFunc)synchronize_selection, self); };
704
Stepan Salenikovichc1323422016-01-06 10:54:44 -0500705 /* update the selection based on the RecentModel */
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400706 priv->selection_updated = QObject::connect(
Stepan Salenikovichc1323422016-01-06 10:54:44 -0500707 RecentModel::instance().selectionModel(),
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400708 &QItemSelectionModel::currentChanged,
Stepan Salenikovich3026bb32017-04-27 13:31:48 -0400709 synchronize_selection_idle
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400710 );
711
Stepan Salenikovichf53128f2016-10-07 10:32:16 -0400712 /* we may need to update the selection when the layout changes */
713 priv->layout_changed = QObject::connect(
714 RecentModel::instance().peopleProxy(),
715 &QAbstractItemModel::layoutChanged,
Stepan Salenikovich3026bb32017-04-27 13:31:48 -0400716 synchronize_selection_idle
Stepan Salenikovichf53128f2016-10-07 10:32:16 -0400717 );
718
Stepan Salenikovich0c17cb62015-11-03 13:01:50 -0500719 /* drag and drop */
720 static GtkTargetEntry targetentries[] = {
721 { (gchar *)CALL_TARGET, GTK_TARGET_SAME_WIDGET, CALL_TARGET_ID },
722 };
723
724 gtk_tree_view_enable_model_drag_source(GTK_TREE_VIEW(self),
725 GDK_BUTTON1_MASK, targetentries, 1, (GdkDragAction)(GDK_ACTION_DEFAULT | GDK_ACTION_MOVE));
726
727 gtk_tree_view_enable_model_drag_dest(GTK_TREE_VIEW(self),
728 targetentries, 1, GDK_ACTION_DEFAULT);
729
730 g_signal_connect(self, "drag-data-get", G_CALLBACK(on_drag_data_get), nullptr);
731 g_signal_connect(self, "drag-drop", G_CALLBACK(on_drag_drop), nullptr);
732 g_signal_connect(self, "drag-motion", G_CALLBACK(on_drag_motion), nullptr);
733 g_signal_connect(self, "drag_data_received", G_CALLBACK(on_drag_data_received), nullptr);
734
Stepan Salenikovich8eaa13e2016-08-26 16:51:48 -0400735 /* init popup menu */
736 priv->popup_menu = contact_popup_menu_new(GTK_TREE_VIEW(self));
737 g_signal_connect_swapped(self, "button-press-event", G_CALLBACK(contact_popup_menu_show), priv->popup_menu);
738
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400739 gtk_widget_show_all(GTK_WIDGET(self));
740}
741
742static void
743recent_contacts_view_dispose(GObject *object)
744{
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400745 RecentContactsView *self = RECENT_CONTACTS_VIEW(object);
746 RecentContactsViewPrivate *priv = RECENT_CONTACTS_VIEW_GET_PRIVATE(self);
747
748 QObject::disconnect(priv->selection_updated);
Stepan Salenikovichf53128f2016-10-07 10:32:16 -0400749 QObject::disconnect(priv->layout_changed);
Stepan Salenikovich8eaa13e2016-08-26 16:51:48 -0400750 gtk_widget_destroy(priv->popup_menu);
Stepan Salenikoviche8fa6812015-10-16 15:34:59 -0400751
Stepan Salenikovich2f8b4492015-09-21 17:10:36 -0400752 G_OBJECT_CLASS(recent_contacts_view_parent_class)->dispose(object);
753}
754
755static void
756recent_contacts_view_finalize(GObject *object)
757{
758 G_OBJECT_CLASS(recent_contacts_view_parent_class)->finalize(object);
759}
760
761static void
762recent_contacts_view_class_init(RecentContactsViewClass *klass)
763{
764 G_OBJECT_CLASS(klass)->finalize = recent_contacts_view_finalize;
765 G_OBJECT_CLASS(klass)->dispose = recent_contacts_view_dispose;
766}
767
768GtkWidget *
769recent_contacts_view_new()
770{
771 gpointer self = g_object_new(RECENT_CONTACTS_VIEW_TYPE, NULL);
772
773 return (GtkWidget *)self;
774}