blob: f28b76237fef48a53f6f4e18898b01ad1b38e2f8 [file] [log] [blame]
Stepan Salenikovich9816a942015-04-22 17:49:16 -04001/*
2 * Copyright (C) 2015 Savoir-Faire Linux Inc.
3 * Author: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 *
19 * Additional permission under GNU GPL version 3 section 7:
20 *
21 * If you modify this program, or any covered work, by linking or
22 * combining it with the OpenSSL project's OpenSSL library (or a
23 * modified version of that library), containing parts covered by the
24 * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
25 * grants you additional permission to convey the resulting work.
26 * Corresponding Source for a non-source form of such a combination
27 * shall include the source code for the parts of OpenSSL used as well
28 * as that of the covered work.
29 */
30
31#include "historyview.h"
32
33#include <gtk/gtk.h>
34#include "models/gtkqsortfiltertreemodel.h"
35#include <categorizedhistorymodel.h>
36#include <QtCore/QSortFilterProxyModel>
37#include <personmodel.h>
38#include "utils/calling.h"
39#include <memory>
40#include "delegates/pixbufdelegate.h"
41#include "defines.h"
42#include "utils/models.h"
43#include <contactmethod.h>
Stepan Salenikovich82b1acf2015-05-12 12:33:51 -040044#include <QtCore/QDateTime> // for date time formatting
Stepan Salenikovich9816a942015-04-22 17:49:16 -040045
46struct _HistoryView
47{
Stepan Salenikovich7c71bfe2015-05-13 18:08:09 -040048 GtkBox parent;
Stepan Salenikovich9816a942015-04-22 17:49:16 -040049};
50
51struct _HistoryViewClass
52{
Stepan Salenikovich7c71bfe2015-05-13 18:08:09 -040053 GtkBoxClass parent_class;
Stepan Salenikovich9816a942015-04-22 17:49:16 -040054};
55
56typedef struct _HistoryViewPrivate HistoryViewPrivate;
57
58struct _HistoryViewPrivate
59{
60 QSortFilterProxyModel *q_history_model;
61};
62
Stepan Salenikovich7c71bfe2015-05-13 18:08:09 -040063G_DEFINE_TYPE_WITH_PRIVATE(HistoryView, history_view, GTK_TYPE_BOX);
Stepan Salenikovich9816a942015-04-22 17:49:16 -040064
65#define HISTORY_VIEW_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), HISTORY_VIEW_TYPE, HistoryViewPrivate))
66
67static void
68render_call_direction(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
69 GtkCellRenderer *cell,
70 GtkTreeModel *tree_model,
71 GtkTreeIter *iter,
72 G_GNUC_UNUSED gpointer data)
73{
74 /* check if this is a top level item (the fuzzy date item),
75 * in this case we don't want to show a call direction */
76 gchar *render_direction = NULL;
77 GtkTreeIter parent;
78 if (gtk_tree_model_iter_parent(tree_model, &parent, iter)) {
79 /* get direction and missed values */
80 GValue value = G_VALUE_INIT;
81 gtk_tree_model_get_value(tree_model, iter, 3, &value);
82 Call::Direction direction = (Call::Direction)g_value_get_int(&value);
83 g_value_unset(&value);
84
85 gtk_tree_model_get_value(tree_model, iter, 4, &value);
86 gboolean missed = g_value_get_boolean(&value);
87 g_value_unset(&value);
88
89 switch (direction) {
90 case Call::Direction::INCOMING:
91 if (missed)
92 render_direction = g_strdup_printf("<span fgcolor=\"red\" font=\"monospace\">&#8601;</span>");
93 else
94 render_direction = g_strdup_printf("<span fgcolor=\"green\" font=\"monospace\">&#8601;</span>");
95 break;
96 case Call::Direction::OUTGOING:
97 if (missed)
98 render_direction = g_strdup_printf("<span fgcolor=\"red\" font=\"monospace\">&#8599;</span>");
99 else
100 render_direction = g_strdup_printf("<span fgcolor=\"green\" font=\"monospace\">&#8599;</span>");
101 break;
102 }
103 }
104 g_object_set(G_OBJECT(cell), "markup", render_direction, NULL);
105 g_free(render_direction);
106}
107
108static void
109activate_history_item(GtkTreeView *tree_view,
110 GtkTreePath *path,
111 G_GNUC_UNUSED GtkTreeViewColumn *column,
112 G_GNUC_UNUSED gpointer user_data)
113{
114 GtkTreeModel *model = gtk_tree_view_get_model(tree_view);
115
116 /* expand / collapse row */
117 if (gtk_tree_view_row_expanded(tree_view, path))
118 gtk_tree_view_collapse_row(tree_view, path);
119 else
120 gtk_tree_view_expand_row(tree_view, path, FALSE);
121
122 /* get iter */
123 GtkTreeIter iter;
124 if (gtk_tree_model_get_iter(model, &iter, path)) {
125 QModelIndex idx = gtk_q_sort_filter_tree_model_get_source_idx(GTK_Q_SORT_FILTER_TREE_MODEL(model), &iter);
126
127 QVariant contact_method = idx.data(static_cast<int>(Call::Role::ContactMethod));
128 /* create new call */
129 if (contact_method.value<ContactMethod*>()) {
130 place_new_call(contact_method.value<ContactMethod*>());
131 }
132 }
133}
134
135static void
136copy_history_item(G_GNUC_UNUSED GtkWidget *item, GtkTreeView *treeview)
137{
138 GtkTreeSelection *selection = gtk_tree_view_get_selection(treeview);
139 QModelIndex idx = get_index_from_selection(selection);
140
141 if (idx.isValid()) {
142 GtkClipboard* clip = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
143
144 const gchar* number = idx.data(static_cast<int>(Call::Role::Number)).toString().toUtf8().constData();
145 gtk_clipboard_set_text(clip, number, -1);
146 }
147}
148
149/* TODO: can't seem to delete just one item for now, add when supported in backend
150 * static void
151 * delete_history_item(G_GNUC_UNUSED GtkWidget *item, GtkTreeView *treeview)
152 * {
153 * GtkTreeSelection *selection = gtk_tree_view_get_selection(treeview);
154 * QModelIndex idx = get_index_from_selection(selection);
155 *
156 * if (idx.isValid()) {
157 * g_debug("deleting history item");
158 * CategorizedHistoryModel::instance()->removeRow(idx.row(), idx.parent());
159 * }
160 * }
161 */
162
163static gboolean
164history_popup_menu(G_GNUC_UNUSED GtkWidget *widget, GdkEventButton *event, GtkTreeView *treeview)
165{
166 /* build popup menu when right clicking on history item
167 * user should be able to copy the "number",
168 * delete history item or all of the history,
169 * and eventualy add the number to a contact
170 */
171
172 /* check for right click */
173 if (event->button != BUTTON_RIGHT_CLICK || event->type != GDK_BUTTON_PRESS)
174 return FALSE;
175
176 GtkWidget *menu = gtk_menu_new();
177
178 /* copy */
179 GtkWidget *item = gtk_menu_item_new_with_mnemonic("_Copy");
180 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
181 g_signal_connect(item, "activate", G_CALLBACK(copy_history_item), treeview);
182
183 /* TODO: delete history entry
184 * gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
185 * item = gtk_menu_item_new_with_mnemonic("_Delete entry");
186 * gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
187 * g_signal_connect(item, "activate", G_CALLBACK(delete_history_item), treeview);
188 */
189
190 /* show menu */
191 gtk_widget_show_all(menu);
192 gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, event->button, event->time);
193
194 return FALSE; /* continue to default handler */
195}
196
197static void
198expand_if_child(G_GNUC_UNUSED GtkTreeModel *tree_model,
199 GtkTreePath *path,
200 G_GNUC_UNUSED GtkTreeIter *iter,
201 GtkTreeView *treeview)
202{
203 if (gtk_tree_path_get_depth(path) == 2)
204 gtk_tree_view_expand_to_path(treeview, path);
205}
206
207static void
Stepan Salenikovich82b1acf2015-05-12 12:33:51 -0400208render_call_photo(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
209 GtkCellRenderer *cell,
210 GtkTreeModel *tree_model,
211 GtkTreeIter *iter,
212 G_GNUC_UNUSED gpointer data)
213{
214 /* check if this is a top level item (category),
215 * in this case we don't want to show a photo */
216 GtkTreePath *path = gtk_tree_model_get_path(tree_model, iter);
217 int depth = gtk_tree_path_get_depth(path);
218 gtk_tree_path_free(path);
219 if (depth == 2) {
220 /* get person */
221 QModelIndex idx = gtk_q_sort_filter_tree_model_get_source_idx(GTK_Q_SORT_FILTER_TREE_MODEL(tree_model), iter);
222 if (idx.isValid()) {
223 QVariant var_c = idx.data(static_cast<int>(Call::Role::Object));
224 Call *c = var_c.value<Call *>();
225 /* get photo */
226 QVariant var_p = PixbufDelegate::instance()->callPhoto(c, QSize(50, 50), false);
227 std::shared_ptr<GdkPixbuf> photo = var_p.value<std::shared_ptr<GdkPixbuf>>();
228 g_object_set(G_OBJECT(cell), "pixbuf", photo.get(), NULL);
229 return;
230 }
231 }
232
233 /* otherwise, make sure its an empty pixbuf */
234 g_object_set(G_OBJECT(cell), "pixbuf", NULL, NULL);
235}
236
237static void
238render_name_and_contact_method(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
239 GtkCellRenderer *cell,
240 GtkTreeModel *tree_model,
241 GtkTreeIter *iter,
242 G_GNUC_UNUSED gpointer data)
243{
244 /**
245 * If call, show the name and the contact method (number) underneath;
246 * otherwise just display the category.
247 */
248 GtkTreePath *path = gtk_tree_model_get_path(tree_model, iter);
249 int depth = gtk_tree_path_get_depth(path);
250 gtk_tree_path_free(path);
251
252 gchar *text = NULL;
253
254 QModelIndex idx = gtk_q_sort_filter_tree_model_get_source_idx(GTK_Q_SORT_FILTER_TREE_MODEL(tree_model), iter);
255 if (idx.isValid()) {
256 QVariant var = idx.data(Qt::DisplayRole);
257 if (depth == 1) {
258 /* category */
259 text = g_strdup_printf("<b>%s</b>", var.value<QString>().toUtf8().constData());
260 } else if (depth == 2) {
261 /* call item */
262 QVariant var_name = idx.data(static_cast<int>(Call::Role::Name));
263 QVariant var_number = idx.data(static_cast<int>(Call::Role::Number));
264 text = g_strdup_printf("%s\n <span fgcolor=\"gray\">%s</span>",
265 var_name.value<QString>().toUtf8().constData(),
266 var_number.value<QString>().toUtf8().constData());
267 }
268 }
269
270 g_object_set(G_OBJECT(cell), "markup", text, NULL);
271 g_free(text);
272}
273
274static void
275render_time(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
276 GtkCellRenderer *cell,
277 GtkTreeModel *tree_model,
278 GtkTreeIter *iter,
279 G_GNUC_UNUSED gpointer data)
280{
281 /**
282 * If call, show the the time;
283 * if category, don't show anything.
284 */
285 GtkTreePath *path = gtk_tree_model_get_path(tree_model, iter);
286 int depth = gtk_tree_path_get_depth(path);
287 gtk_tree_path_free(path);
288
289 gchar *text = NULL;
290
291 QModelIndex idx = gtk_q_sort_filter_tree_model_get_source_idx(GTK_Q_SORT_FILTER_TREE_MODEL(tree_model), iter);
292 if (idx.isValid() && depth == 2) {
293 QVariant var_d = idx.data(static_cast<int>(Call::Role::Date));
294 time_t time = var_d.value<time_t>();
295 QDateTime date_time = QDateTime::fromTime_t(time);
296 text = g_strdup_printf("%s", date_time.time().toString().toUtf8().constData());
297 }
298
299 g_object_set(G_OBJECT(cell), "markup", text, NULL);
300 g_free(text);
301}
302
303static void
304render_date(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
305 GtkCellRenderer *cell,
306 GtkTreeModel *tree_model,
307 GtkTreeIter *iter,
308 G_GNUC_UNUSED gpointer data)
309{
310 /**
311 * If call, show the date;
312 * if category, don't show anything.
313 */
314 GtkTreePath *path = gtk_tree_model_get_path(tree_model, iter);
315 int depth = gtk_tree_path_get_depth(path);
316 gtk_tree_path_free(path);
317
318 gchar *text = NULL;
319
320 QModelIndex idx = gtk_q_sort_filter_tree_model_get_source_idx(GTK_Q_SORT_FILTER_TREE_MODEL(tree_model), iter);
321 if (idx.isValid() && depth == 2) {
322 QVariant var_d = idx.data(static_cast<int>(Call::Role::Date));
323 time_t time = var_d.value<time_t>();
324 QDateTime date_time = QDateTime::fromTime_t(time);
325 text = g_strdup_printf("%s", date_time.date().toString().toUtf8().constData());
326 }
327
328 g_object_set(G_OBJECT(cell), "markup", text, NULL);
329 g_free(text);
330}
331
332static void
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400333history_view_init(HistoryView *self)
334{
335 HistoryViewPrivate *priv = HISTORY_VIEW_GET_PRIVATE(self);
336
Stepan Salenikovich7c71bfe2015-05-13 18:08:09 -0400337 gtk_orientable_set_orientation(GTK_ORIENTABLE(self), GTK_ORIENTATION_VERTICAL);
338 /* need to be able to focus on widget so that we can auto-scroll to it */
339 gtk_widget_set_can_focus(GTK_WIDGET(self), TRUE);
Stepan Salenikovich7be4f622015-05-13 15:36:19 -0400340
Stepan Salenikovich7c71bfe2015-05-13 18:08:09 -0400341 GtkWidget *label_history = gtk_label_new("History");
342 gtk_box_pack_start(GTK_BOX(self), label_history, FALSE, TRUE, 10);
343
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400344 GtkWidget *treeview_history = gtk_tree_view_new();
Stepan Salenikovich7c71bfe2015-05-13 18:08:09 -0400345 /* set can-focus to false so that the scrollwindow doesn't jump to try to
346 * contain the top of the treeview */
347 gtk_widget_set_can_focus(treeview_history, FALSE);
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400348 /* make headers visible to allow column resizing */
349 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(treeview_history), TRUE);
Stepan Salenikovich7c71bfe2015-05-13 18:08:09 -0400350 gtk_box_pack_start(GTK_BOX(self), treeview_history, TRUE, TRUE, 0);
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400351
Stepan Salenikovichb01d7362015-04-27 23:02:00 -0400352 /* disable default search, we will handle it ourselves via LRC;
353 * otherwise the search steals input focus on key presses */
354 gtk_tree_view_set_enable_search(GTK_TREE_VIEW(treeview_history), FALSE);
355
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400356 /* sort the history in descending order by date */
357 priv->q_history_model = new QSortFilterProxyModel();
358 priv->q_history_model->setSourceModel(CategorizedHistoryModel::instance());
359 priv->q_history_model->setSortRole(static_cast<int>(Call::Role::Date));
360 priv->q_history_model->sort(0,Qt::DescendingOrder);
361
362 GtkQSortFilterTreeModel *history_model = gtk_q_sort_filter_tree_model_new(
363 priv->q_history_model,
364 5,
365 Qt::DisplayRole, G_TYPE_STRING,
366 Call::Role::Number, G_TYPE_STRING,
367 Call::Role::FormattedDate, G_TYPE_STRING,
368 Call::Role::Direction, G_TYPE_INT,
369 Call::Role::Missed, G_TYPE_BOOLEAN);
370 gtk_tree_view_set_model( GTK_TREE_VIEW(treeview_history), GTK_TREE_MODEL(history_model) );
371
Stepan Salenikovich82b1acf2015-05-12 12:33:51 -0400372 /* call direction, photo, name/number column */
373 GtkCellArea *area = gtk_cell_area_box_new();
374 GtkTreeViewColumn *column = gtk_tree_view_column_new_with_area(area);
375 gtk_tree_view_column_set_title(column, "Call");
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400376
377 /* call direction */
378 GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
Stepan Salenikovich82b1acf2015-05-12 12:33:51 -0400379 gtk_cell_area_box_pack_start(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400380
381 /* display the call direction with arrows */
382 gtk_tree_view_column_set_cell_data_func(
383 column,
384 renderer,
385 (GtkTreeCellDataFunc)render_call_direction,
386 NULL,
387 NULL);
388
Stepan Salenikovich82b1acf2015-05-12 12:33:51 -0400389 /* photo renderer */
390 renderer = gtk_cell_renderer_pixbuf_new();
391 gtk_cell_area_box_pack_start(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400392
Stepan Salenikovich82b1acf2015-05-12 12:33:51 -0400393 /* get the photo */
394 gtk_tree_view_column_set_cell_data_func(
395 column,
396 renderer,
397 (GtkTreeCellDataFunc)render_call_photo,
398 NULL,
399 NULL);
400
401 /* name and contact method renderer */
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400402 renderer = gtk_cell_renderer_text_new();
403 g_object_set(G_OBJECT(renderer), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
Stepan Salenikovich82b1acf2015-05-12 12:33:51 -0400404 gtk_cell_area_box_pack_start(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
405
406 gtk_tree_view_column_set_cell_data_func(
407 column,
408 renderer,
409 (GtkTreeCellDataFunc)render_name_and_contact_method,
410 NULL,
411 NULL);
412
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400413 gtk_tree_view_append_column(GTK_TREE_VIEW(treeview_history), column);
414 gtk_tree_view_column_set_resizable(column, TRUE);
415
416 /* date column */
Stepan Salenikovich82b1acf2015-05-12 12:33:51 -0400417 area = gtk_cell_area_box_new();
418 column = gtk_tree_view_column_new_with_area(area);
419 gtk_tree_view_column_set_title(column, "Date");
420
421 /* time renderer */
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400422 renderer = gtk_cell_renderer_text_new ();
Stepan Salenikovich82b1acf2015-05-12 12:33:51 -0400423 gtk_cell_area_box_pack_start(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
424 /* format the time*/
425 gtk_tree_view_column_set_cell_data_func(
426 column,
427 renderer,
428 (GtkTreeCellDataFunc)render_time,
429 NULL,
430 NULL);
431
432 /* date renderer */
433 renderer = gtk_cell_renderer_text_new ();
434 gtk_cell_area_box_pack_start(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400435 g_object_set(G_OBJECT(renderer), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
Stepan Salenikovich82b1acf2015-05-12 12:33:51 -0400436 /* format the date */
437 gtk_tree_view_column_set_cell_data_func(
438 column,
439 renderer,
440 (GtkTreeCellDataFunc)render_date,
441 NULL,
442 NULL);
443
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400444 gtk_tree_view_append_column(GTK_TREE_VIEW(treeview_history), column);
445 gtk_tree_view_column_set_resizable(column, TRUE);
446
447 /* expand the first row, which should be the most recent calls */
448 gtk_tree_view_expand_row(GTK_TREE_VIEW(treeview_history),
449 gtk_tree_path_new_from_string("0"),
450 FALSE);
451
452 g_signal_connect(treeview_history, "row-activated", G_CALLBACK(activate_history_item), NULL);
453 g_signal_connect(treeview_history, "button-press-event", G_CALLBACK(history_popup_menu), treeview_history);
454 g_signal_connect(history_model, "row-inserted", G_CALLBACK(expand_if_child), treeview_history);
455
456 gtk_widget_show_all(GTK_WIDGET(self));
457}
458
459static void
460history_view_dispose(GObject *object)
461{
462 G_OBJECT_CLASS(history_view_parent_class)->dispose(object);
463}
464
465static void
466history_view_finalize(GObject *object)
467{
468 HistoryView *self = HISTORY_VIEW(object);
469 HistoryViewPrivate *priv = HISTORY_VIEW_GET_PRIVATE(self);
470
471 delete priv->q_history_model;
472
473 G_OBJECT_CLASS(history_view_parent_class)->finalize(object);
474}
475
476static void
477history_view_class_init(HistoryViewClass *klass)
478{
479 G_OBJECT_CLASS(klass)->finalize = history_view_finalize;
480 G_OBJECT_CLASS(klass)->dispose = history_view_dispose;
481}
482
483GtkWidget *
484history_view_new()
485{
486 gpointer self = g_object_new(HISTORY_VIEW_TYPE, NULL);
487
488 return (GtkWidget *)self;
489}