blob: 581a6f3d4625fcffb633401a8701060d85082ca5 [file] [log] [blame]
Stepan Salenikovichbbb10d82015-05-13 12:26:44 -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 "callsview.h"
32
33#include <gtk/gtk.h>
34#include "models/gtkqtreemodel.h"
35#include <callmodel.h>
36#include <QtCore/QItemSelectionModel>
37#include "utils/models.h"
Stepan Salenikovich7dfd07c2015-05-13 14:18:51 -040038#include "delegates/pixbufdelegate.h"
39#include <QtCore/QSize>
Stepan Salenikovichf2d76c52015-07-17 17:54:56 -040040#include "defines.h"
41#include "utils/menus.h"
Stepan Salenikovichbbb10d82015-05-13 12:26:44 -040042
43struct _CallsView
44{
Stepan Salenikovich7be4f622015-05-13 15:36:19 -040045 GtkRevealer parent;
Stepan Salenikovichbbb10d82015-05-13 12:26:44 -040046};
47
48struct _CallsViewClass
49{
Stepan Salenikovich7be4f622015-05-13 15:36:19 -040050 GtkRevealerClass parent_class;
Stepan Salenikovichbbb10d82015-05-13 12:26:44 -040051};
52
53typedef struct _CallsViewPrivate CallsViewPrivate;
54
55struct _CallsViewPrivate
56{
57 GtkWidget *treeview_calls;
58 QMetaObject::Connection selection_updated;
Stepan Salenikovich7be4f622015-05-13 15:36:19 -040059 QMetaObject::Connection calls_added;
60 QMetaObject::Connection calls_removed;
Stepan Salenikovichbbb10d82015-05-13 12:26:44 -040061};
62
Stepan Salenikovich7be4f622015-05-13 15:36:19 -040063G_DEFINE_TYPE_WITH_PRIVATE(CallsView, calls_view, GTK_TYPE_REVEALER);
Stepan Salenikovichbbb10d82015-05-13 12:26:44 -040064
65#define CALLS_VIEW_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), CALLS_VIEW_TYPE, CallsViewPrivate))
66
67static void
68update_call_model_selection(GtkTreeSelection *selection, G_GNUC_UNUSED gpointer user_data)
69{
70 QModelIndex current = get_index_from_selection(selection);
Stepan Salenikovich744238e2015-07-07 12:55:34 -040071 if (current.isValid()) {
72
Stepan Salenikovichf8b76572015-07-08 18:15:32 -040073 /* make sure we don't call HOLD more than once on the same index, by
74 * checking which one is currently selected */
75 auto current_selection = CallModel::instance()->selectionModel()->currentIndex();
76 if (current != current_selection) {
77 /* if the call is on hold, we want to put it off hold automatically
78 * when switching to it */
79 auto call = CallModel::instance()->getCall(current);
80 if (call->state() == Call::State::HOLD) {
81 call << Call::Action::HOLD;
82 }
Stepan Salenikovich744238e2015-07-07 12:55:34 -040083
Stepan Salenikovichf8b76572015-07-08 18:15:32 -040084 CallModel::instance()->selectionModel()->setCurrentIndex(current, QItemSelectionModel::ClearAndSelect);
85 }
Stepan Salenikovich744238e2015-07-07 12:55:34 -040086 } else {
Stepan Salenikovichbbb10d82015-05-13 12:26:44 -040087 CallModel::instance()->selectionModel()->clearCurrentIndex();
Stepan Salenikovich744238e2015-07-07 12:55:34 -040088 }
Stepan Salenikovichbbb10d82015-05-13 12:26:44 -040089}
90
91static void
Stepan Salenikovich7dfd07c2015-05-13 14:18:51 -040092render_call_photo(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
93 GtkCellRenderer *cell,
94 GtkTreeModel *tree_model,
95 GtkTreeIter *iter,
96 G_GNUC_UNUSED gpointer data)
97{
98 /* get call */
99 QModelIndex idx = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(tree_model), iter);
100 if (idx.isValid()) {
101 QVariant var_c = idx.data(static_cast<int>(Call::Role::Object));
102 Call *c = var_c.value<Call *>();
103
104 /* we only want to show the photo once the call is past creation
105 * since before then we likely don't know the contact yet anyways */
106 if (c->lifeCycleState() != Call::LifeCycleState::CREATION) {
107 /* get photo */
108 QVariant var_p = PixbufDelegate::instance()->callPhoto(c, QSize(50, 50), false);
109 std::shared_ptr<GdkPixbuf> photo = var_p.value<std::shared_ptr<GdkPixbuf>>();
110 g_object_set(G_OBJECT(cell), "pixbuf", photo.get(), NULL);
111 return;
112 }
113 }
114
115 /* otherwise, make sure its an empty pixbuf */
116 g_object_set(G_OBJECT(cell), "pixbuf", NULL, NULL);
117}
118
119static void
120render_name_and_contact_method(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
121 GtkCellRenderer *cell,
122 GtkTreeModel *tree_model,
123 GtkTreeIter *iter,
124 G_GNUC_UNUSED gpointer data)
125{
126 gchar *text = NULL;
127 QModelIndex idx = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(tree_model), iter);
128 if (idx.isValid()) {
129 QVariant state = idx.data(static_cast<int>(Call::Role::LifeCycleState));
130 QVariant name = idx.data(static_cast<int>(Call::Role::Name));
131 QVariant number = idx.data(static_cast<int>(Call::Role::Number));
132
133 /* only show the number being entered while in creation state */
134 if (state.value<Call::LifeCycleState>() == Call::LifeCycleState::CREATION) {
135 text = g_strdup_printf("%s", number.value<QString>().toUtf8().constData());
136 } else {
137 text = g_strdup_printf("%s\n <span fgcolor=\"gray\">%s</span>",
138 name.value<QString>().toUtf8().constData(),
139 number.value<QString>().toUtf8().constData());
140 }
141 }
142
143 g_object_set(G_OBJECT(cell), "markup", text, NULL);
144 g_free(text);
145}
146
Stepan Salenikovichf2d76c52015-07-17 17:54:56 -0400147static gboolean
148create_popup_menu(GtkTreeView *treeview, GdkEventButton *event, G_GNUC_UNUSED gpointer user_data)
149{
150 /* build popup menu when right clicking on a call
151 * user should be able to add the "number" to their contacts if it is not already so
152 */
153
154 /* check for right click */
155 if (event->button != BUTTON_RIGHT_CLICK || event->type != GDK_BUTTON_PRESS)
156 return FALSE;
157
158 GtkWidget *menu = gtk_menu_new();
159
160 /* get the call object from the selected item
161 * check if it is already linked to a person, if not, then offer to either
162 * add to a new or existing contact */
163 auto selection = gtk_tree_view_get_selection(treeview);
164 const auto& idx = get_index_from_selection(selection);
165 const auto& var_c = idx.data(static_cast<int>(Call::Role::Object));
166 if (idx.isValid() && var_c.isValid()) {
167 if (auto call = var_c.value<Call *>()) {
168 auto contactmethod = call->peerContactMethod();
169 if (!contact_method_has_contact(contactmethod)) {
Stepan Salenikovich0cf247d2015-07-24 17:36:32 -0400170 GtkTreeIter iter;
171 GtkTreeModel *model;
172 gtk_tree_selection_get_selected(selection, &model, &iter);
173 auto path = gtk_tree_model_get_path(model, &iter);
174 auto column = gtk_tree_view_get_column(treeview, 0);
175 GdkRectangle rect;
176 gtk_tree_view_get_cell_area(treeview, path, column, &rect);
177 gtk_tree_view_convert_bin_window_to_widget_coords(treeview, rect.x, rect.y, &rect.x, &rect.y);
178 gtk_tree_path_free(path);
179 auto add_to = menu_item_add_to_contact(contactmethod, GTK_WIDGET(treeview), &rect);
Stepan Salenikovichf2d76c52015-07-17 17:54:56 -0400180 gtk_menu_shell_append(GTK_MENU_SHELL(menu), add_to);
181
182 /* no other items, so show menu here */
183 gtk_widget_show_all(menu);
184 gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, event->button, event->time);
185
186 return TRUE;
187 }
188 }
189 }
190
191 return FALSE;
192}
193
Stepan Salenikovich7dfd07c2015-05-13 14:18:51 -0400194static void
Stepan Salenikovichbbb10d82015-05-13 12:26:44 -0400195calls_view_init(CallsView *self)
196{
197 CallsViewPrivate *priv = CALLS_VIEW_GET_PRIVATE(self);
198
Stepan Salenikovich7be4f622015-05-13 15:36:19 -0400199 /* hide if there are no calls */
200 gtk_revealer_set_reveal_child(GTK_REVEALER(self),
201 CallModel::instance()->rowCount());
202 priv->calls_added = QObject::connect(
203 CallModel::instance(),
204 &QAbstractItemModel::rowsInserted,
205 [=] (G_GNUC_UNUSED const QModelIndex &parent,
206 G_GNUC_UNUSED int first,
207 G_GNUC_UNUSED int last)
208 {
209 gtk_revealer_set_reveal_child(GTK_REVEALER(self),
210 CallModel::instance()->rowCount());
211 }
212 );
213
214 priv->calls_removed = QObject::connect(
215 CallModel::instance(),
216 &QAbstractItemModel::rowsRemoved,
217 [=] (G_GNUC_UNUSED const QModelIndex &parent,
218 G_GNUC_UNUSED int first,
219 G_GNUC_UNUSED int last)
220 {
221 gtk_revealer_set_reveal_child(GTK_REVEALER(self),
222 CallModel::instance()->rowCount());
223 }
224 );
225
226 GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
227 gtk_widget_set_margin_bottom(box, 10);
228 gtk_container_add(GTK_CONTAINER(self), box);
229
230 /* current calls label */
231 GtkWidget *label = gtk_label_new("Current Calls");
232 gtk_box_pack_start(GTK_BOX(box), label, FALSE, TRUE, 10);
233
234 GtkWidget *scrolled_window = gtk_scrolled_window_new(NULL, NULL);
235 gtk_box_pack_start(GTK_BOX(box), scrolled_window, FALSE, TRUE, 0);
236
Stepan Salenikovichbbb10d82015-05-13 12:26:44 -0400237 /* disable vertical scroll... we always want all the calls to be visible */
Stepan Salenikovich7be4f622015-05-13 15:36:19 -0400238 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled_window),
239 GTK_POLICY_NEVER,
Stepan Salenikovichbbb10d82015-05-13 12:26:44 -0400240 GTK_POLICY_NEVER);
Stepan Salenikovichbbb10d82015-05-13 12:26:44 -0400241
242 priv->treeview_calls = gtk_tree_view_new();
243 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(priv->treeview_calls), FALSE);
244 /* disable default search, we will handle it ourselves via LRC;
245 * otherwise the search steals input focus on key presses */
246 gtk_tree_view_set_enable_search(GTK_TREE_VIEW(priv->treeview_calls), FALSE);
Stepan Salenikovich7dfd07c2015-05-13 14:18:51 -0400247 gtk_tree_view_set_show_expanders(GTK_TREE_VIEW(priv->treeview_calls), FALSE);
Stepan Salenikovich7be4f622015-05-13 15:36:19 -0400248 gtk_container_add(GTK_CONTAINER(scrolled_window), priv->treeview_calls);
Stepan Salenikovichbbb10d82015-05-13 12:26:44 -0400249
250 /* call model */
251 GtkQTreeModel *call_model;
252 GtkCellRenderer *renderer;
253 GtkTreeViewColumn *column;
254
255 call_model = gtk_q_tree_model_new(
256 CallModel::instance(),
257 4,
258 Call::Role::Name, G_TYPE_STRING,
259 Call::Role::Number, G_TYPE_STRING,
260 Call::Role::Length, G_TYPE_STRING,
261 Call::Role::State, G_TYPE_STRING);
262 gtk_tree_view_set_model(GTK_TREE_VIEW(priv->treeview_calls), GTK_TREE_MODEL(call_model));
263
Stepan Salenikovich7dfd07c2015-05-13 14:18:51 -0400264 /* call photo, name/number column */
265 GtkCellArea *area = gtk_cell_area_box_new();
266 column = gtk_tree_view_column_new_with_area(area);
267 gtk_tree_view_column_set_title(column, "Call");
268
269 /* photo renderer */
270 renderer = gtk_cell_renderer_pixbuf_new();
271 gtk_cell_area_box_pack_start(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
272
273 /* get the photo */
274 gtk_tree_view_column_set_cell_data_func(
275 column,
276 renderer,
277 (GtkTreeCellDataFunc)render_call_photo,
278 NULL,
279 NULL);
280
281 /* name and contact method renderer */
Stepan Salenikovichbbb10d82015-05-13 12:26:44 -0400282 renderer = gtk_cell_renderer_text_new();
283 g_object_set(G_OBJECT(renderer), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
Stepan Salenikovich7dfd07c2015-05-13 14:18:51 -0400284 gtk_cell_area_box_pack_start(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
285
286 gtk_tree_view_column_set_cell_data_func(
287 column,
288 renderer,
289 (GtkTreeCellDataFunc)render_name_and_contact_method,
290 NULL,
291 NULL);
292
Stepan Salenikovichbbb10d82015-05-13 12:26:44 -0400293 gtk_tree_view_append_column(GTK_TREE_VIEW(priv->treeview_calls), column);
Stepan Salenikovich7dfd07c2015-05-13 14:18:51 -0400294 gtk_tree_view_column_set_resizable(column, TRUE);
Stepan Salenikoviche7c4e282015-06-11 17:22:08 -0400295 gtk_tree_view_column_set_expand(column, TRUE);
Stepan Salenikovichbbb10d82015-05-13 12:26:44 -0400296
297 renderer = gtk_cell_renderer_text_new();
298 column = gtk_tree_view_column_new_with_attributes("Duration", renderer, "text", 2, NULL);
299 gtk_tree_view_column_set_sizing(column, GTK_TREE_VIEW_COLUMN_AUTOSIZE);
300 gtk_tree_view_append_column(GTK_TREE_VIEW(priv->treeview_calls), column);
Stepan Salenikoviche7c4e282015-06-11 17:22:08 -0400301 gtk_tree_view_column_set_expand(column, FALSE);
Stepan Salenikovichbbb10d82015-05-13 12:26:44 -0400302
303 /* connect signals to and from the slection model of the call model */
304 priv->selection_updated = QObject::connect(
305 CallModel::instance()->selectionModel(),
306 &QItemSelectionModel::currentChanged,
Stepan Salenikovich8e882cd2015-05-28 14:18:06 -0400307 [=](const QModelIndex current, const QModelIndex & previous) {
Stepan Salenikovichbbb10d82015-05-13 12:26:44 -0400308 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(priv->treeview_calls));
309
310 /* first unselect the previous */
311 if (previous.isValid()) {
312 GtkTreeIter old_iter;
313 if (gtk_q_tree_model_source_index_to_iter(call_model, previous, &old_iter)) {
314 gtk_tree_selection_unselect_iter(selection, &old_iter);
315 } else {
316 g_warning("Trying to unselect invalid GtkTreeIter");
317 }
318 }
319
320 /* select the current */
321 if (current.isValid()) {
322 GtkTreeIter new_iter;
323 if (gtk_q_tree_model_source_index_to_iter(call_model, current, &new_iter)) {
324 gtk_tree_selection_select_iter(selection, &new_iter);
325 } else {
326 g_warning("SelectionModel of CallModel changed to invalid QModelIndex?");
327 }
328 }
329 }
330 );
331
332 GtkTreeSelection *call_selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(priv->treeview_calls));
333 g_signal_connect(call_selection, "changed", G_CALLBACK(update_call_model_selection), NULL);
334
Stepan Salenikovichf2d76c52015-07-17 17:54:56 -0400335 g_signal_connect(priv->treeview_calls, "button-press-event", G_CALLBACK(create_popup_menu), NULL);
336
Stepan Salenikovichbbb10d82015-05-13 12:26:44 -0400337 gtk_widget_show_all(GTK_WIDGET(self));
338}
339
340static void
341calls_view_dispose(GObject *object)
342{
343 CallsView *self = CALLS_VIEW(object);
344 CallsViewPrivate *priv = CALLS_VIEW_GET_PRIVATE(self);
345
346 QObject::disconnect(priv->selection_updated);
Stepan Salenikovich7be4f622015-05-13 15:36:19 -0400347 QObject::disconnect(priv->calls_added);
348 QObject::disconnect(priv->calls_removed);
Stepan Salenikovichbbb10d82015-05-13 12:26:44 -0400349
350 G_OBJECT_CLASS(calls_view_parent_class)->dispose(object);
351}
352
353static void
354calls_view_finalize(GObject *object)
355{
356 G_OBJECT_CLASS(calls_view_parent_class)->finalize(object);
357}
358
359static void
360calls_view_class_init(CallsViewClass *klass)
361{
362 G_OBJECT_CLASS(klass)->finalize = calls_view_finalize;
363 G_OBJECT_CLASS(klass)->dispose = calls_view_dispose;
364}
365
366GtkWidget *
367calls_view_new()
368{
369 gpointer self = g_object_new(CALLS_VIEW_TYPE, NULL);
370
371 return (GtkWidget *)self;
372}
373
374GtkTreeSelection *
375calls_view_get_selection(CallsView *calls_view)
376{
377 g_return_val_if_fail(IS_CALLS_VIEW(calls_view), NULL);
378 CallsViewPrivate *priv = CALLS_VIEW_GET_PRIVATE(calls_view);
379
380 return gtk_tree_view_get_selection(GTK_TREE_VIEW(priv->treeview_calls));
Stepan Salenikovich8e882cd2015-05-28 14:18:06 -0400381}