blob: 4ee3e43b295a0e1c01507a33acf150e58ffa4979 [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 Salenikovich9d294492015-05-14 16:34:24 -040045#include <QtCore/QItemSelectionModel>
Stepan Salenikovich9816a942015-04-22 17:49:16 -040046
47struct _HistoryView
48{
Stepan Salenikovich7c71bfe2015-05-13 18:08:09 -040049 GtkBox parent;
Stepan Salenikovich9816a942015-04-22 17:49:16 -040050};
51
52struct _HistoryViewClass
53{
Stepan Salenikovich7c71bfe2015-05-13 18:08:09 -040054 GtkBoxClass parent_class;
Stepan Salenikovich9816a942015-04-22 17:49:16 -040055};
56
57typedef struct _HistoryViewPrivate HistoryViewPrivate;
58
59struct _HistoryViewPrivate
60{
Stepan Salenikovich9d294492015-05-14 16:34:24 -040061 CategorizedHistoryModel::SortedProxy *q_sorted_proxy;
62 QMetaObject::Connection category_changed;
Stepan Salenikovich9816a942015-04-22 17:49:16 -040063};
64
Stepan Salenikovich7c71bfe2015-05-13 18:08:09 -040065G_DEFINE_TYPE_WITH_PRIVATE(HistoryView, history_view, GTK_TYPE_BOX);
Stepan Salenikovich9816a942015-04-22 17:49:16 -040066
67#define HISTORY_VIEW_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), HISTORY_VIEW_TYPE, HistoryViewPrivate))
68
69static void
70render_call_direction(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
71 GtkCellRenderer *cell,
72 GtkTreeModel *tree_model,
73 GtkTreeIter *iter,
74 G_GNUC_UNUSED gpointer data)
75{
76 /* check if this is a top level item (the fuzzy date item),
77 * in this case we don't want to show a call direction */
78 gchar *render_direction = NULL;
79 GtkTreeIter parent;
80 if (gtk_tree_model_iter_parent(tree_model, &parent, iter)) {
81 /* get direction and missed values */
82 GValue value = G_VALUE_INIT;
83 gtk_tree_model_get_value(tree_model, iter, 3, &value);
84 Call::Direction direction = (Call::Direction)g_value_get_int(&value);
85 g_value_unset(&value);
86
87 gtk_tree_model_get_value(tree_model, iter, 4, &value);
88 gboolean missed = g_value_get_boolean(&value);
89 g_value_unset(&value);
90
91 switch (direction) {
92 case Call::Direction::INCOMING:
93 if (missed)
94 render_direction = g_strdup_printf("<span fgcolor=\"red\" font=\"monospace\">&#8601;</span>");
95 else
96 render_direction = g_strdup_printf("<span fgcolor=\"green\" font=\"monospace\">&#8601;</span>");
97 break;
98 case Call::Direction::OUTGOING:
99 if (missed)
100 render_direction = g_strdup_printf("<span fgcolor=\"red\" font=\"monospace\">&#8599;</span>");
101 else
102 render_direction = g_strdup_printf("<span fgcolor=\"green\" font=\"monospace\">&#8599;</span>");
103 break;
104 }
105 }
106 g_object_set(G_OBJECT(cell), "markup", render_direction, NULL);
107 g_free(render_direction);
108}
109
110static void
111activate_history_item(GtkTreeView *tree_view,
112 GtkTreePath *path,
113 G_GNUC_UNUSED GtkTreeViewColumn *column,
114 G_GNUC_UNUSED gpointer user_data)
115{
116 GtkTreeModel *model = gtk_tree_view_get_model(tree_view);
117
118 /* expand / collapse row */
119 if (gtk_tree_view_row_expanded(tree_view, path))
120 gtk_tree_view_collapse_row(tree_view, path);
121 else
122 gtk_tree_view_expand_row(tree_view, path, FALSE);
123
124 /* get iter */
125 GtkTreeIter iter;
126 if (gtk_tree_model_get_iter(model, &iter, path)) {
127 QModelIndex idx = gtk_q_sort_filter_tree_model_get_source_idx(GTK_Q_SORT_FILTER_TREE_MODEL(model), &iter);
128
129 QVariant contact_method = idx.data(static_cast<int>(Call::Role::ContactMethod));
130 /* create new call */
131 if (contact_method.value<ContactMethod*>()) {
132 place_new_call(contact_method.value<ContactMethod*>());
133 }
134 }
135}
136
137static void
138copy_history_item(G_GNUC_UNUSED GtkWidget *item, GtkTreeView *treeview)
139{
140 GtkTreeSelection *selection = gtk_tree_view_get_selection(treeview);
141 QModelIndex idx = get_index_from_selection(selection);
142
143 if (idx.isValid()) {
144 GtkClipboard* clip = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
145
146 const gchar* number = idx.data(static_cast<int>(Call::Role::Number)).toString().toUtf8().constData();
147 gtk_clipboard_set_text(clip, number, -1);
148 }
149}
150
151/* TODO: can't seem to delete just one item for now, add when supported in backend
152 * static void
153 * delete_history_item(G_GNUC_UNUSED GtkWidget *item, GtkTreeView *treeview)
154 * {
155 * GtkTreeSelection *selection = gtk_tree_view_get_selection(treeview);
156 * QModelIndex idx = get_index_from_selection(selection);
157 *
158 * if (idx.isValid()) {
159 * g_debug("deleting history item");
160 * CategorizedHistoryModel::instance()->removeRow(idx.row(), idx.parent());
161 * }
162 * }
163 */
164
165static gboolean
166history_popup_menu(G_GNUC_UNUSED GtkWidget *widget, GdkEventButton *event, GtkTreeView *treeview)
167{
168 /* build popup menu when right clicking on history item
169 * user should be able to copy the "number",
170 * delete history item or all of the history,
171 * and eventualy add the number to a contact
172 */
173
174 /* check for right click */
175 if (event->button != BUTTON_RIGHT_CLICK || event->type != GDK_BUTTON_PRESS)
176 return FALSE;
177
178 GtkWidget *menu = gtk_menu_new();
179
180 /* copy */
181 GtkWidget *item = gtk_menu_item_new_with_mnemonic("_Copy");
182 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
183 g_signal_connect(item, "activate", G_CALLBACK(copy_history_item), treeview);
184
185 /* TODO: delete history entry
186 * gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
187 * item = gtk_menu_item_new_with_mnemonic("_Delete entry");
188 * gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
189 * g_signal_connect(item, "activate", G_CALLBACK(delete_history_item), treeview);
190 */
191
192 /* show menu */
193 gtk_widget_show_all(menu);
194 gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, event->button, event->time);
195
196 return FALSE; /* continue to default handler */
197}
198
199static void
Stepan Salenikovich82b1acf2015-05-12 12:33:51 -0400200render_call_photo(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
201 GtkCellRenderer *cell,
202 GtkTreeModel *tree_model,
203 GtkTreeIter *iter,
204 G_GNUC_UNUSED gpointer data)
205{
206 /* check if this is a top level item (category),
207 * in this case we don't want to show a photo */
208 GtkTreePath *path = gtk_tree_model_get_path(tree_model, iter);
209 int depth = gtk_tree_path_get_depth(path);
210 gtk_tree_path_free(path);
211 if (depth == 2) {
212 /* get person */
213 QModelIndex idx = gtk_q_sort_filter_tree_model_get_source_idx(GTK_Q_SORT_FILTER_TREE_MODEL(tree_model), iter);
214 if (idx.isValid()) {
215 QVariant var_c = idx.data(static_cast<int>(Call::Role::Object));
216 Call *c = var_c.value<Call *>();
217 /* get photo */
218 QVariant var_p = PixbufDelegate::instance()->callPhoto(c, QSize(50, 50), false);
219 std::shared_ptr<GdkPixbuf> photo = var_p.value<std::shared_ptr<GdkPixbuf>>();
220 g_object_set(G_OBJECT(cell), "pixbuf", photo.get(), NULL);
221 return;
222 }
223 }
224
225 /* otherwise, make sure its an empty pixbuf */
226 g_object_set(G_OBJECT(cell), "pixbuf", NULL, NULL);
227}
228
229static void
230render_name_and_contact_method(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
231 GtkCellRenderer *cell,
232 GtkTreeModel *tree_model,
233 GtkTreeIter *iter,
234 G_GNUC_UNUSED gpointer data)
235{
236 /**
237 * If call, show the name and the contact method (number) underneath;
238 * otherwise just display the category.
239 */
240 GtkTreePath *path = gtk_tree_model_get_path(tree_model, iter);
241 int depth = gtk_tree_path_get_depth(path);
242 gtk_tree_path_free(path);
243
244 gchar *text = NULL;
245
246 QModelIndex idx = gtk_q_sort_filter_tree_model_get_source_idx(GTK_Q_SORT_FILTER_TREE_MODEL(tree_model), iter);
247 if (idx.isValid()) {
248 QVariant var = idx.data(Qt::DisplayRole);
249 if (depth == 1) {
250 /* category */
251 text = g_strdup_printf("<b>%s</b>", var.value<QString>().toUtf8().constData());
252 } else if (depth == 2) {
253 /* call item */
254 QVariant var_name = idx.data(static_cast<int>(Call::Role::Name));
255 QVariant var_number = idx.data(static_cast<int>(Call::Role::Number));
256 text = g_strdup_printf("%s\n <span fgcolor=\"gray\">%s</span>",
257 var_name.value<QString>().toUtf8().constData(),
258 var_number.value<QString>().toUtf8().constData());
259 }
260 }
261
262 g_object_set(G_OBJECT(cell), "markup", text, NULL);
263 g_free(text);
264}
265
266static void
267render_time(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
268 GtkCellRenderer *cell,
269 GtkTreeModel *tree_model,
270 GtkTreeIter *iter,
271 G_GNUC_UNUSED gpointer data)
272{
273 /**
274 * If call, show the the time;
275 * if category, don't show anything.
276 */
277 GtkTreePath *path = gtk_tree_model_get_path(tree_model, iter);
278 int depth = gtk_tree_path_get_depth(path);
279 gtk_tree_path_free(path);
280
281 gchar *text = NULL;
282
283 QModelIndex idx = gtk_q_sort_filter_tree_model_get_source_idx(GTK_Q_SORT_FILTER_TREE_MODEL(tree_model), iter);
284 if (idx.isValid() && depth == 2) {
285 QVariant var_d = idx.data(static_cast<int>(Call::Role::Date));
286 time_t time = var_d.value<time_t>();
287 QDateTime date_time = QDateTime::fromTime_t(time);
288 text = g_strdup_printf("%s", date_time.time().toString().toUtf8().constData());
289 }
290
291 g_object_set(G_OBJECT(cell), "markup", text, NULL);
292 g_free(text);
293}
294
295static void
296render_date(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
297 GtkCellRenderer *cell,
298 GtkTreeModel *tree_model,
299 GtkTreeIter *iter,
300 G_GNUC_UNUSED gpointer data)
301{
302 /**
303 * If call, show the date;
304 * if category, don't show anything.
305 */
306 GtkTreePath *path = gtk_tree_model_get_path(tree_model, iter);
307 int depth = gtk_tree_path_get_depth(path);
308 gtk_tree_path_free(path);
309
310 gchar *text = NULL;
311
312 QModelIndex idx = gtk_q_sort_filter_tree_model_get_source_idx(GTK_Q_SORT_FILTER_TREE_MODEL(tree_model), iter);
313 if (idx.isValid() && depth == 2) {
314 QVariant var_d = idx.data(static_cast<int>(Call::Role::Date));
315 time_t time = var_d.value<time_t>();
316 QDateTime date_time = QDateTime::fromTime_t(time);
317 text = g_strdup_printf("%s", date_time.date().toString().toUtf8().constData());
318 }
319
320 g_object_set(G_OBJECT(cell), "markup", text, NULL);
321 g_free(text);
322}
323
324static void
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400325history_view_init(HistoryView *self)
326{
327 HistoryViewPrivate *priv = HISTORY_VIEW_GET_PRIVATE(self);
328
Stepan Salenikovich7c71bfe2015-05-13 18:08:09 -0400329 gtk_orientable_set_orientation(GTK_ORIENTABLE(self), GTK_ORIENTATION_VERTICAL);
330 /* need to be able to focus on widget so that we can auto-scroll to it */
331 gtk_widget_set_can_focus(GTK_WIDGET(self), TRUE);
Stepan Salenikovich7be4f622015-05-13 15:36:19 -0400332
Stepan Salenikovich7c71bfe2015-05-13 18:08:09 -0400333 GtkWidget *label_history = gtk_label_new("History");
334 gtk_box_pack_start(GTK_BOX(self), label_history, FALSE, TRUE, 10);
335
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400336 GtkWidget *treeview_history = gtk_tree_view_new();
Stepan Salenikovich7c71bfe2015-05-13 18:08:09 -0400337 /* set can-focus to false so that the scrollwindow doesn't jump to try to
338 * contain the top of the treeview */
339 gtk_widget_set_can_focus(treeview_history, FALSE);
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400340 /* make headers visible to allow column resizing */
341 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(treeview_history), TRUE);
Stepan Salenikovich7c71bfe2015-05-13 18:08:09 -0400342 gtk_box_pack_start(GTK_BOX(self), treeview_history, TRUE, TRUE, 0);
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400343
Stepan Salenikovichb01d7362015-04-27 23:02:00 -0400344 /* disable default search, we will handle it ourselves via LRC;
345 * otherwise the search steals input focus on key presses */
346 gtk_tree_view_set_enable_search(GTK_TREE_VIEW(treeview_history), FALSE);
347
Stepan Salenikovich9d294492015-05-14 16:34:24 -0400348 /* instantiate history proxy model */
349 priv->q_sorted_proxy = CategorizedHistoryModel::SortedProxy::instance();
350
351 /* for now there is no way in the UI to pick whether sorting is ascending
352 * or descending, so we do it in the code when the category changes */
353 priv->category_changed = QObject::connect(
354 priv->q_sorted_proxy->categorySelectionModel(),
355 &QItemSelectionModel::currentChanged,
356 [=] (const QModelIndex &current, G_GNUC_UNUSED const QModelIndex &previous)
357 {
358 if (current.isValid()) {
359 if (current.row() == 0) {
360 /* sort in descending order for the date */
361 priv->q_sorted_proxy->model()->sort(0, Qt::DescendingOrder);
362 } else {
363 /* ascending order for verything else */
364 priv->q_sorted_proxy->model()->sort(0);
365 }
366 }
367 }
368 );
369
370 /* select default category (the first one, which is by date) */
371 priv->q_sorted_proxy->categorySelectionModel()->setCurrentIndex(
372 priv->q_sorted_proxy->categoryModel()->index(0, 0),
373 QItemSelectionModel::ClearAndSelect);
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400374
375 GtkQSortFilterTreeModel *history_model = gtk_q_sort_filter_tree_model_new(
Stepan Salenikovich9d294492015-05-14 16:34:24 -0400376 priv->q_sorted_proxy->model(),
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400377 5,
378 Qt::DisplayRole, G_TYPE_STRING,
379 Call::Role::Number, G_TYPE_STRING,
380 Call::Role::FormattedDate, G_TYPE_STRING,
381 Call::Role::Direction, G_TYPE_INT,
382 Call::Role::Missed, G_TYPE_BOOLEAN);
383 gtk_tree_view_set_model( GTK_TREE_VIEW(treeview_history), GTK_TREE_MODEL(history_model) );
384
Stepan Salenikovich82b1acf2015-05-12 12:33:51 -0400385 /* call direction, photo, name/number column */
386 GtkCellArea *area = gtk_cell_area_box_new();
387 GtkTreeViewColumn *column = gtk_tree_view_column_new_with_area(area);
388 gtk_tree_view_column_set_title(column, "Call");
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400389
390 /* call direction */
391 GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
Stepan Salenikovich82b1acf2015-05-12 12:33:51 -0400392 gtk_cell_area_box_pack_start(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400393
394 /* display the call direction with arrows */
395 gtk_tree_view_column_set_cell_data_func(
396 column,
397 renderer,
398 (GtkTreeCellDataFunc)render_call_direction,
399 NULL,
400 NULL);
401
Stepan Salenikovich82b1acf2015-05-12 12:33:51 -0400402 /* photo renderer */
403 renderer = gtk_cell_renderer_pixbuf_new();
404 gtk_cell_area_box_pack_start(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400405
Stepan Salenikovich82b1acf2015-05-12 12:33:51 -0400406 /* get the photo */
407 gtk_tree_view_column_set_cell_data_func(
408 column,
409 renderer,
410 (GtkTreeCellDataFunc)render_call_photo,
411 NULL,
412 NULL);
413
414 /* name and contact method renderer */
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400415 renderer = gtk_cell_renderer_text_new();
416 g_object_set(G_OBJECT(renderer), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
Stepan Salenikovich82b1acf2015-05-12 12:33:51 -0400417 gtk_cell_area_box_pack_start(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
418
419 gtk_tree_view_column_set_cell_data_func(
420 column,
421 renderer,
422 (GtkTreeCellDataFunc)render_name_and_contact_method,
423 NULL,
424 NULL);
425
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400426 gtk_tree_view_append_column(GTK_TREE_VIEW(treeview_history), column);
427 gtk_tree_view_column_set_resizable(column, TRUE);
428
429 /* date column */
Stepan Salenikovich82b1acf2015-05-12 12:33:51 -0400430 area = gtk_cell_area_box_new();
431 column = gtk_tree_view_column_new_with_area(area);
432 gtk_tree_view_column_set_title(column, "Date");
433
434 /* time renderer */
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400435 renderer = gtk_cell_renderer_text_new ();
Stepan Salenikovich82b1acf2015-05-12 12:33:51 -0400436 gtk_cell_area_box_pack_start(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
437 /* format the time*/
438 gtk_tree_view_column_set_cell_data_func(
439 column,
440 renderer,
441 (GtkTreeCellDataFunc)render_time,
442 NULL,
443 NULL);
444
445 /* date renderer */
446 renderer = gtk_cell_renderer_text_new ();
447 gtk_cell_area_box_pack_start(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400448 g_object_set(G_OBJECT(renderer), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
Stepan Salenikovich82b1acf2015-05-12 12:33:51 -0400449 /* format the date */
450 gtk_tree_view_column_set_cell_data_func(
451 column,
452 renderer,
453 (GtkTreeCellDataFunc)render_date,
454 NULL,
455 NULL);
456
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400457 gtk_tree_view_append_column(GTK_TREE_VIEW(treeview_history), column);
458 gtk_tree_view_column_set_resizable(column, TRUE);
459
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400460 g_signal_connect(treeview_history, "row-activated", G_CALLBACK(activate_history_item), NULL);
461 g_signal_connect(treeview_history, "button-press-event", G_CALLBACK(history_popup_menu), treeview_history);
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400462
463 gtk_widget_show_all(GTK_WIDGET(self));
464}
465
466static void
467history_view_dispose(GObject *object)
468{
Stepan Salenikovich9d294492015-05-14 16:34:24 -0400469 HistoryViewPrivate *priv = HISTORY_VIEW_GET_PRIVATE(object);
470
471 QObject::disconnect(priv->category_changed);
472
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400473 G_OBJECT_CLASS(history_view_parent_class)->dispose(object);
474}
475
476static void
477history_view_finalize(GObject *object)
478{
Stepan Salenikovich9816a942015-04-22 17:49:16 -0400479 G_OBJECT_CLASS(history_view_parent_class)->finalize(object);
480}
481
482static void
483history_view_class_init(HistoryViewClass *klass)
484{
485 G_OBJECT_CLASS(klass)->finalize = history_view_finalize;
486 G_OBJECT_CLASS(klass)->dispose = history_view_dispose;
487}
488
489GtkWidget *
490history_view_new()
491{
492 gpointer self = g_object_new(HISTORY_VIEW_TYPE, NULL);
493
494 return (GtkWidget *)self;
Stepan Salenikovich9d294492015-05-14 16:34:24 -0400495}