blob: 45b94be02b93b12af38ac4a732b73780afbe241a [file] [log] [blame]
Stepan Salenikovich8eaa13e2016-08-26 16:51:48 -04001/*
Guillaume Roguez2a6150d2017-07-19 18:24:47 -04002 * Copyright (C) 2016-2017 Savoir-faire Linux Inc.
Stepan Salenikovich8eaa13e2016-08-26 16:51:48 -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 "contactpopupmenu.h"
21
22// GTK+ related
23#include <glib/gi18n.h>
24
25// LRC
26#include <contactmethod.h>
27#include <person.h>
28#include <numbercategory.h>
29#include <call.h>
30
31// Ring client
32#include "utils/calling.h"
Stepan Salenikovichf6078222016-10-03 17:31:16 -040033#include "models/gtkqtreemodel.h"
Stepan Salenikovich8eaa13e2016-08-26 16:51:48 -040034#include "utils/menus.h"
35
36static constexpr const char* COPY_DATA_KEY = "copy_data";
37
38struct _ContactPopupMenu
39{
40 GtkMenu parent;
41};
42
43struct _ContactPopupMenuClass
44{
45 GtkMenuClass parent_class;
46};
47
48typedef struct _ContactPopupMenuPrivate ContactPopupMenuPrivate;
49
50struct _ContactPopupMenuPrivate
51{
52 GtkTreeView *treeview;
53};
54
55G_DEFINE_TYPE_WITH_PRIVATE(ContactPopupMenu, contact_popup_menu, GTK_TYPE_MENU);
56
57#define CONTACT_POPUP_MENU_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), CONTACT_POPUP_MENU_TYPE, ContactPopupMenuPrivate))
58
59static void
60copy_contact_info(GtkWidget *item, G_GNUC_UNUSED gpointer user_data)
61{
62 gpointer data = g_object_get_data(G_OBJECT(item), COPY_DATA_KEY);
63 g_return_if_fail(data);
64 gchar* text = (gchar *)data;
65 GtkClipboard* clip = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
66 gtk_clipboard_set_text(clip, text, -1);
67}
68
69static void
70call_contactmethod(G_GNUC_UNUSED GtkWidget *item, ContactMethod *cm)
71{
72 g_return_if_fail(cm);
73 place_new_call(cm);
74}
75
76static void
77remove_contact(GtkWidget *item, Person *person)
78{
79 GtkWidget *dialog = gtk_message_dialog_new(NULL,
80 (GtkDialogFlags)(GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT),
81 GTK_MESSAGE_QUESTION, GTK_BUTTONS_OK_CANCEL,
82 _("Are you sure you want to delete contact \"%s\"?"
83 " It will be removed from your system's addressbook."),
84 person->formattedName().toUtf8().constData());
85
86 /* get parent window so we can center on it */
87 GtkWidget *parent = gtk_widget_get_toplevel(GTK_WIDGET(item));
88 if (gtk_widget_is_toplevel(parent)) {
89 gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(parent));
90 gtk_window_set_position(GTK_WINDOW(dialog), GTK_WIN_POS_CENTER_ON_PARENT);
91 }
92
93 if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_OK) {
94 person->remove();
95 }
96
97 gtk_widget_destroy(dialog);
98}
99
100/**
101 * Update the menu when the selected item in the treeview changes.
102 */
103static void
104update(GtkTreeSelection *selection, ContactPopupMenu *self)
105{
106 ContactPopupMenuPrivate *priv = CONTACT_POPUP_MENU_GET_PRIVATE(self);
107
108 /* clear the current menu */
109 gtk_container_forall(GTK_CONTAINER(self), (GtkCallback)gtk_widget_destroy, nullptr);
110
111 /* we always build a menu, however in some cases some or all of the items will be deactivated;
112 * we prefer this to having an empty menu because GTK+ behaves weird in the empty menu case
113 */
114 auto call_item = gtk_menu_item_new_with_mnemonic(_("_Call"));
115 gtk_widget_set_sensitive(GTK_WIDGET(call_item), FALSE);
116 gtk_menu_shell_append(GTK_MENU_SHELL(self), call_item);
117 auto copy_name_item = gtk_menu_item_new_with_mnemonic(_("_Copy name"));
118 gtk_widget_set_sensitive(GTK_WIDGET(copy_name_item), FALSE);
119 gtk_menu_shell_append(GTK_MENU_SHELL(self), copy_name_item);
120 auto copy_number_item = gtk_menu_item_new_with_mnemonic(_("_Copy number"));
121 gtk_widget_set_sensitive(GTK_WIDGET(copy_number_item), FALSE);
122 gtk_menu_shell_append(GTK_MENU_SHELL(self), copy_number_item);
123 auto add_to_contact_item = gtk_menu_item_new_with_mnemonic(_("_Add to contact"));
124 gtk_widget_set_sensitive(GTK_WIDGET(add_to_contact_item), FALSE);
125 gtk_menu_shell_append(GTK_MENU_SHELL(self), add_to_contact_item);
126 auto remove_contact_item = gtk_menu_item_new_with_mnemonic(_("_Remove contact"));
127 gtk_widget_set_sensitive(GTK_WIDGET(remove_contact_item), FALSE);
128 gtk_menu_shell_append(GTK_MENU_SHELL(self), remove_contact_item);
129
130 /* show all items */
131 gtk_widget_show_all(GTK_WIDGET(self));
132
133 GtkTreeIter iter;
134 GtkTreeModel *model;
135 if (!gtk_tree_selection_get_selected(selection, &model, &iter))
136 return;
137
Stepan Salenikovichf6078222016-10-03 17:31:16 -0400138 QModelIndex idx = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(model), &iter);
Stepan Salenikovich8eaa13e2016-08-26 16:51:48 -0400139
140 auto type = idx.data(static_cast<int>(Ring::Role::ObjectType));
141 auto object = idx.data(static_cast<int>(Ring::Role::Object));
142 if (!type.isValid() || !object.isValid())
143 return; // not a valid Ring::Role::Object, so nothing to do
144
145 /* call */
146 switch (type.value<Ring::ObjectType>()) {
147 case Ring::ObjectType::Person:
148 {
149 /* possiblity for multiple numbers */
150 auto cms = object.value<Person *>()->phoneNumbers();
151 if (cms.size() == 1) {
152 gtk_widget_set_sensitive(GTK_WIDGET(call_item), TRUE);
153 g_signal_connect(call_item,
154 "activate",
155 G_CALLBACK(call_contactmethod),
156 cms.at(0));
157 } else if (cms.size() > 1) {
158 gtk_widget_set_sensitive(GTK_WIDGET(call_item), TRUE);
159 auto call_menu = gtk_menu_new();
160 gtk_menu_item_set_submenu(GTK_MENU_ITEM(call_item), call_menu);
161 for (int i = 0; i < cms.size(); ++i) {
162 gchar *number = nullptr;
163 if (cms.at(i)->category()) {
164 // try to get the number category, eg: "home"
165 number = g_strdup_printf("(%s) %s", cms.at(i)->category()->name().toUtf8().constData(),
166 cms.at(i)->uri().toUtf8().constData());
167 } else {
168 number = g_strdup_printf("%s", cms.at(i)->uri().toUtf8().constData());
169 }
170 auto item = gtk_menu_item_new_with_label(number);
171 g_free(number);
172 gtk_menu_shell_append(GTK_MENU_SHELL(call_menu), item);
173 g_signal_connect(item,
174 "activate",
175 G_CALLBACK(call_contactmethod),
176 cms.at(i));
177 }
178 }
179 }
180 break;
181 case Ring::ObjectType::ContactMethod:
182 {
183 gtk_widget_set_sensitive(GTK_WIDGET(call_item), TRUE);
184 auto cm = object.value<ContactMethod *>();
185 g_signal_connect(call_item,
186 "activate",
187 G_CALLBACK(call_contactmethod),
188 cm);
189 }
190 break;
191 case Ring::ObjectType::Call:
192 {
193 gtk_widget_set_sensitive(GTK_WIDGET(call_item), TRUE);
194 auto call = object.value<Call *>();
195 g_signal_connect(call_item,
196 "activate",
197 G_CALLBACK(call_contactmethod),
198 call->peerContactMethod());
199 }
200 break;
201 case Ring::ObjectType::Media:
202 case Ring::ObjectType::Certificate:
Nicolas Jagerc74f7612017-03-28 09:48:18 -0400203 case Ring::ObjectType::ContactRequest:
Stepan Salenikovich8eaa13e2016-08-26 16:51:48 -0400204 // nothing to do for now
205 case Ring::ObjectType::COUNT__:
206 break;
207 }
208
209 /* copy name */
210 QVariant name_var = idx.data(static_cast<int>(Ring::Role::Name));
211 if (name_var.isValid()) {
212 gtk_widget_set_sensitive(GTK_WIDGET(copy_name_item), TRUE);
213 gchar *name = g_strdup_printf("%s", name_var.value<QString>().toUtf8().constData());
214 g_object_set_data_full(G_OBJECT(copy_name_item), COPY_DATA_KEY, name, (GDestroyNotify)g_free);
215 g_signal_connect(copy_name_item,
216 "activate",
217 G_CALLBACK(copy_contact_info),
218 NULL);
219 }
220
221 /* copy number(s) */
222 switch (type.value<Ring::ObjectType>()) {
223 case Ring::ObjectType::Person:
224 {
225 /* possiblity for multiple numbers */
226 auto cms = object.value<Person *>()->phoneNumbers();
227 if (cms.size() == 1) {
228 gtk_widget_set_sensitive(GTK_WIDGET(copy_number_item), TRUE);
229 gchar *number = g_strdup_printf("%s",cms.at(0)->uri().toUtf8().constData());
230 g_object_set_data_full(G_OBJECT(copy_number_item), COPY_DATA_KEY, number, (GDestroyNotify)g_free);
231 g_signal_connect(copy_number_item,
232 "activate",
233 G_CALLBACK(copy_contact_info),
234 NULL);
235 } else if (cms.size() > 1) {
236 gtk_widget_set_sensitive(GTK_WIDGET(copy_number_item), TRUE);
237 auto copy_menu = gtk_menu_new();
238 gtk_menu_item_set_submenu(GTK_MENU_ITEM(copy_number_item), copy_menu);
239 for (int i = 0; i < cms.size(); ++i) {
Stepan Salenikovicheea501d2016-10-03 11:40:00 -0400240 auto number = g_strdup_printf("%s",cms.at(i)->uri().toUtf8().constData());
241 gchar *category_number = nullptr;
Stepan Salenikovich8eaa13e2016-08-26 16:51:48 -0400242 if (cms.at(i)->category()) {
243 // try to get the number category, eg: "home"
Stepan Salenikovicheea501d2016-10-03 11:40:00 -0400244 category_number = g_strdup_printf("(%s) %s",
245 cms.at(i)->category()->name().toUtf8().constData(),
246 number);
Stepan Salenikovich8eaa13e2016-08-26 16:51:48 -0400247 } else {
Stepan Salenikovicheea501d2016-10-03 11:40:00 -0400248 category_number = g_strdup_printf("%s", number);
Stepan Salenikovich8eaa13e2016-08-26 16:51:48 -0400249 }
Stepan Salenikovicheea501d2016-10-03 11:40:00 -0400250 auto item = gtk_menu_item_new_with_label(category_number);
251 g_free(category_number);
Stepan Salenikovich8eaa13e2016-08-26 16:51:48 -0400252 gtk_menu_shell_append(GTK_MENU_SHELL(copy_menu), item);
253 g_object_set_data_full(G_OBJECT(item), COPY_DATA_KEY, number, (GDestroyNotify)g_free);
254 g_signal_connect(item,
255 "activate",
256 G_CALLBACK(copy_contact_info),
257 NULL);
258 }
259 }
260 }
261 break;
262 case Ring::ObjectType::ContactMethod:
263 case Ring::ObjectType::Call:
264 {
265 QVariant number_var = idx.data(static_cast<int>(Ring::Role::Number));
266 if (number_var.isValid()) {
267 gtk_widget_set_sensitive(GTK_WIDGET(copy_number_item), TRUE);
268 gchar *number = g_strdup_printf("%s", number_var.value<QString>().toUtf8().constData());
269 g_object_set_data_full(G_OBJECT(copy_number_item), COPY_DATA_KEY, number, (GDestroyNotify)g_free);
270 g_signal_connect(copy_number_item,
271 "activate",
272 G_CALLBACK(copy_contact_info),
273 NULL);
274 }
275 }
276 break;
277 case Ring::ObjectType::Media:
278 case Ring::ObjectType::Certificate:
Nicolas Jagerc74f7612017-03-28 09:48:18 -0400279 case Ring::ObjectType::ContactRequest:
Stepan Salenikovich8eaa13e2016-08-26 16:51:48 -0400280 // nothing to do
281 case Ring::ObjectType::COUNT__:
282 break;
283 }
284
285 /* get rectangle to know where to draw the add to contact popup */
286 GdkRectangle rect;
287 auto path = gtk_tree_model_get_path(model, &iter);
288 auto column = gtk_tree_view_get_column(priv->treeview, 0);
289 gtk_tree_view_get_cell_area(priv->treeview, path, column, &rect);
290 gtk_tree_view_convert_bin_window_to_widget_coords(priv->treeview, rect.x, rect.y, &rect.x, &rect.y);
291 gtk_tree_path_free(path);
292
293 /* add to contact - only offer to add CMs which are not already associated with a Person */
294 switch (type.value<Ring::ObjectType>()) {
295 case Ring::ObjectType::Person:
296 // already a contact
297 break;
298 case Ring::ObjectType::ContactMethod:
299 {
300 auto cm = object.value<ContactMethod *>();
301 if (!cm->contact()) {
302 gtk_widget_set_sensitive(GTK_WIDGET(add_to_contact_item), TRUE);
303 menu_item_add_to_contact(GTK_MENU_ITEM(add_to_contact_item), cm, GTK_WIDGET(priv->treeview), &rect);
304 }
305 }
306 break;
307 case Ring::ObjectType::Call:
308 {
309 auto cm = object.value<Call *>()->peerContactMethod();
310 if (!cm->contact()) {
311 gtk_widget_set_sensitive(GTK_WIDGET(add_to_contact_item), TRUE);
312 menu_item_add_to_contact(GTK_MENU_ITEM(add_to_contact_item), cm, GTK_WIDGET(priv->treeview), &rect);
313 }
314 }
315 break;
316 case Ring::ObjectType::Media:
317 case Ring::ObjectType::Certificate:
Nicolas Jagerc74f7612017-03-28 09:48:18 -0400318 case Ring::ObjectType::ContactRequest:
Stepan Salenikovich8eaa13e2016-08-26 16:51:48 -0400319 // nothing to do
320 case Ring::ObjectType::COUNT__:
321 break;
322 }
323
324 /* remove contact */
325 if (type.value<Ring::ObjectType>() == Ring::ObjectType::Person) {
326 gtk_widget_set_sensitive(GTK_WIDGET(remove_contact_item), TRUE);
327 auto person = object.value<Person *>();
328 g_signal_connect(remove_contact_item,
329 "activate",
330 G_CALLBACK(remove_contact),
331 person);
332 }
333
334 /* show all items */
335 gtk_widget_show_all(GTK_WIDGET(self));
336}
337
338static void
339contact_popup_menu_init(G_GNUC_UNUSED ContactPopupMenu *self)
340{
341 // nothing to do
342}
343
344static void
345contact_popup_menu_dispose(GObject *object)
346{
347 G_OBJECT_CLASS(contact_popup_menu_parent_class)->dispose(object);
348}
349
350static void
351contact_popup_menu_finalize(GObject *object)
352{
353 G_OBJECT_CLASS(contact_popup_menu_parent_class)->finalize(object);
354}
355
356static void
357contact_popup_menu_class_init(ContactPopupMenuClass *klass)
358{
359 G_OBJECT_CLASS(klass)->finalize = contact_popup_menu_finalize;
360 G_OBJECT_CLASS(klass)->dispose = contact_popup_menu_dispose;
361}
362
363GtkWidget *
364contact_popup_menu_new(GtkTreeView *treeview)
365{
366 gpointer self = g_object_new(CONTACT_POPUP_MENU_TYPE, NULL);
367 ContactPopupMenuPrivate *priv = CONTACT_POPUP_MENU_GET_PRIVATE(self);
368
369 priv->treeview = treeview;
370 GtkTreeSelection *selection = gtk_tree_view_get_selection(priv->treeview);
371 g_signal_connect(selection, "changed", G_CALLBACK(update), self);
372
Stepan Salenikovich544a1ee2016-09-29 16:10:15 -0400373 // build the menu for the first time
374 update(selection, CONTACT_POPUP_MENU(self));
375
Stepan Salenikovich8eaa13e2016-08-26 16:51:48 -0400376 return (GtkWidget *)self;
377}
378
379gboolean
380contact_popup_menu_show(ContactPopupMenu *self, GdkEventButton *event)
381{
382 /* check for right click */
383 if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_SECONDARY ) {
384 /* the menu will automatically get updated when the selection changes */
385 gtk_menu_popup(GTK_MENU(self), NULL, NULL, NULL, NULL, event->button, event->time);
386 }
387
388 return GDK_EVENT_PROPAGATE; /* so that the item selection changes */
389}