blob: 5f6944a36b8a5a50d4cdfd29d6655a8bbb69cd65 [file] [log] [blame]
Stepan Salenikovich8eaa13e2016-08-26 16:51:48 -04001/*
2 * Copyright (C) 2016 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
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:
203 // nothing to do for now
204 case Ring::ObjectType::COUNT__:
205 break;
206 }
207
208 /* copy name */
209 QVariant name_var = idx.data(static_cast<int>(Ring::Role::Name));
210 if (name_var.isValid()) {
211 gtk_widget_set_sensitive(GTK_WIDGET(copy_name_item), TRUE);
212 gchar *name = g_strdup_printf("%s", name_var.value<QString>().toUtf8().constData());
213 g_object_set_data_full(G_OBJECT(copy_name_item), COPY_DATA_KEY, name, (GDestroyNotify)g_free);
214 g_signal_connect(copy_name_item,
215 "activate",
216 G_CALLBACK(copy_contact_info),
217 NULL);
218 }
219
220 /* copy number(s) */
221 switch (type.value<Ring::ObjectType>()) {
222 case Ring::ObjectType::Person:
223 {
224 /* possiblity for multiple numbers */
225 auto cms = object.value<Person *>()->phoneNumbers();
226 if (cms.size() == 1) {
227 gtk_widget_set_sensitive(GTK_WIDGET(copy_number_item), TRUE);
228 gchar *number = g_strdup_printf("%s",cms.at(0)->uri().toUtf8().constData());
229 g_object_set_data_full(G_OBJECT(copy_number_item), COPY_DATA_KEY, number, (GDestroyNotify)g_free);
230 g_signal_connect(copy_number_item,
231 "activate",
232 G_CALLBACK(copy_contact_info),
233 NULL);
234 } else if (cms.size() > 1) {
235 gtk_widget_set_sensitive(GTK_WIDGET(copy_number_item), TRUE);
236 auto copy_menu = gtk_menu_new();
237 gtk_menu_item_set_submenu(GTK_MENU_ITEM(copy_number_item), copy_menu);
238 for (int i = 0; i < cms.size(); ++i) {
Stepan Salenikovicheea501d2016-10-03 11:40:00 -0400239 auto number = g_strdup_printf("%s",cms.at(i)->uri().toUtf8().constData());
240 gchar *category_number = nullptr;
Stepan Salenikovich8eaa13e2016-08-26 16:51:48 -0400241 if (cms.at(i)->category()) {
242 // try to get the number category, eg: "home"
Stepan Salenikovicheea501d2016-10-03 11:40:00 -0400243 category_number = g_strdup_printf("(%s) %s",
244 cms.at(i)->category()->name().toUtf8().constData(),
245 number);
Stepan Salenikovich8eaa13e2016-08-26 16:51:48 -0400246 } else {
Stepan Salenikovicheea501d2016-10-03 11:40:00 -0400247 category_number = g_strdup_printf("%s", number);
Stepan Salenikovich8eaa13e2016-08-26 16:51:48 -0400248 }
Stepan Salenikovicheea501d2016-10-03 11:40:00 -0400249 auto item = gtk_menu_item_new_with_label(category_number);
250 g_free(category_number);
Stepan Salenikovich8eaa13e2016-08-26 16:51:48 -0400251 gtk_menu_shell_append(GTK_MENU_SHELL(copy_menu), item);
252 g_object_set_data_full(G_OBJECT(item), COPY_DATA_KEY, number, (GDestroyNotify)g_free);
253 g_signal_connect(item,
254 "activate",
255 G_CALLBACK(copy_contact_info),
256 NULL);
257 }
258 }
259 }
260 break;
261 case Ring::ObjectType::ContactMethod:
262 case Ring::ObjectType::Call:
263 {
264 QVariant number_var = idx.data(static_cast<int>(Ring::Role::Number));
265 if (number_var.isValid()) {
266 gtk_widget_set_sensitive(GTK_WIDGET(copy_number_item), TRUE);
267 gchar *number = g_strdup_printf("%s", number_var.value<QString>().toUtf8().constData());
268 g_object_set_data_full(G_OBJECT(copy_number_item), COPY_DATA_KEY, number, (GDestroyNotify)g_free);
269 g_signal_connect(copy_number_item,
270 "activate",
271 G_CALLBACK(copy_contact_info),
272 NULL);
273 }
274 }
275 break;
276 case Ring::ObjectType::Media:
277 case Ring::ObjectType::Certificate:
278 // nothing to do
279 case Ring::ObjectType::COUNT__:
280 break;
281 }
282
283 /* get rectangle to know where to draw the add to contact popup */
284 GdkRectangle rect;
285 auto path = gtk_tree_model_get_path(model, &iter);
286 auto column = gtk_tree_view_get_column(priv->treeview, 0);
287 gtk_tree_view_get_cell_area(priv->treeview, path, column, &rect);
288 gtk_tree_view_convert_bin_window_to_widget_coords(priv->treeview, rect.x, rect.y, &rect.x, &rect.y);
289 gtk_tree_path_free(path);
290
291 /* add to contact - only offer to add CMs which are not already associated with a Person */
292 switch (type.value<Ring::ObjectType>()) {
293 case Ring::ObjectType::Person:
294 // already a contact
295 break;
296 case Ring::ObjectType::ContactMethod:
297 {
298 auto cm = object.value<ContactMethod *>();
299 if (!cm->contact()) {
300 gtk_widget_set_sensitive(GTK_WIDGET(add_to_contact_item), TRUE);
301 menu_item_add_to_contact(GTK_MENU_ITEM(add_to_contact_item), cm, GTK_WIDGET(priv->treeview), &rect);
302 }
303 }
304 break;
305 case Ring::ObjectType::Call:
306 {
307 auto cm = object.value<Call *>()->peerContactMethod();
308 if (!cm->contact()) {
309 gtk_widget_set_sensitive(GTK_WIDGET(add_to_contact_item), TRUE);
310 menu_item_add_to_contact(GTK_MENU_ITEM(add_to_contact_item), cm, GTK_WIDGET(priv->treeview), &rect);
311 }
312 }
313 break;
314 case Ring::ObjectType::Media:
315 case Ring::ObjectType::Certificate:
316 // nothing to do
317 case Ring::ObjectType::COUNT__:
318 break;
319 }
320
321 /* remove contact */
322 if (type.value<Ring::ObjectType>() == Ring::ObjectType::Person) {
323 gtk_widget_set_sensitive(GTK_WIDGET(remove_contact_item), TRUE);
324 auto person = object.value<Person *>();
325 g_signal_connect(remove_contact_item,
326 "activate",
327 G_CALLBACK(remove_contact),
328 person);
329 }
330
331 /* show all items */
332 gtk_widget_show_all(GTK_WIDGET(self));
333}
334
335static void
336contact_popup_menu_init(G_GNUC_UNUSED ContactPopupMenu *self)
337{
338 // nothing to do
339}
340
341static void
342contact_popup_menu_dispose(GObject *object)
343{
344 G_OBJECT_CLASS(contact_popup_menu_parent_class)->dispose(object);
345}
346
347static void
348contact_popup_menu_finalize(GObject *object)
349{
350 G_OBJECT_CLASS(contact_popup_menu_parent_class)->finalize(object);
351}
352
353static void
354contact_popup_menu_class_init(ContactPopupMenuClass *klass)
355{
356 G_OBJECT_CLASS(klass)->finalize = contact_popup_menu_finalize;
357 G_OBJECT_CLASS(klass)->dispose = contact_popup_menu_dispose;
358}
359
360GtkWidget *
361contact_popup_menu_new(GtkTreeView *treeview)
362{
363 gpointer self = g_object_new(CONTACT_POPUP_MENU_TYPE, NULL);
364 ContactPopupMenuPrivate *priv = CONTACT_POPUP_MENU_GET_PRIVATE(self);
365
366 priv->treeview = treeview;
367 GtkTreeSelection *selection = gtk_tree_view_get_selection(priv->treeview);
368 g_signal_connect(selection, "changed", G_CALLBACK(update), self);
369
Stepan Salenikovich544a1ee2016-09-29 16:10:15 -0400370 // build the menu for the first time
371 update(selection, CONTACT_POPUP_MENU(self));
372
Stepan Salenikovich8eaa13e2016-08-26 16:51:48 -0400373 return (GtkWidget *)self;
374}
375
376gboolean
377contact_popup_menu_show(ContactPopupMenu *self, GdkEventButton *event)
378{
379 /* check for right click */
380 if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_SECONDARY ) {
381 /* the menu will automatically get updated when the selection changes */
382 gtk_menu_popup(GTK_MENU(self), NULL, NULL, NULL, NULL, event->button, event->time);
383 }
384
385 return GDK_EVENT_PROPAGATE; /* so that the item selection changes */
386}