| /* |
| * Copyright (C) 2016 Savoir-faire Linux Inc. |
| * Author: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com> |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation; either version 3 of the License, or |
| * (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
| */ |
| |
| #include "contactpopupmenu.h" |
| |
| // GTK+ related |
| #include <glib/gi18n.h> |
| |
| // LRC |
| #include <contactmethod.h> |
| #include <person.h> |
| #include <numbercategory.h> |
| #include <call.h> |
| |
| // Ring client |
| #include "utils/calling.h" |
| #include "models/gtkqtreemodel.h" |
| #include "utils/menus.h" |
| |
| static constexpr const char* COPY_DATA_KEY = "copy_data"; |
| |
| struct _ContactPopupMenu |
| { |
| GtkMenu parent; |
| }; |
| |
| struct _ContactPopupMenuClass |
| { |
| GtkMenuClass parent_class; |
| }; |
| |
| typedef struct _ContactPopupMenuPrivate ContactPopupMenuPrivate; |
| |
| struct _ContactPopupMenuPrivate |
| { |
| GtkTreeView *treeview; |
| }; |
| |
| G_DEFINE_TYPE_WITH_PRIVATE(ContactPopupMenu, contact_popup_menu, GTK_TYPE_MENU); |
| |
| #define CONTACT_POPUP_MENU_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), CONTACT_POPUP_MENU_TYPE, ContactPopupMenuPrivate)) |
| |
| static void |
| copy_contact_info(GtkWidget *item, G_GNUC_UNUSED gpointer user_data) |
| { |
| gpointer data = g_object_get_data(G_OBJECT(item), COPY_DATA_KEY); |
| g_return_if_fail(data); |
| gchar* text = (gchar *)data; |
| GtkClipboard* clip = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD); |
| gtk_clipboard_set_text(clip, text, -1); |
| } |
| |
| static void |
| call_contactmethod(G_GNUC_UNUSED GtkWidget *item, ContactMethod *cm) |
| { |
| g_return_if_fail(cm); |
| place_new_call(cm); |
| } |
| |
| static void |
| remove_contact(GtkWidget *item, Person *person) |
| { |
| GtkWidget *dialog = gtk_message_dialog_new(NULL, |
| (GtkDialogFlags)(GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT), |
| GTK_MESSAGE_QUESTION, GTK_BUTTONS_OK_CANCEL, |
| _("Are you sure you want to delete contact \"%s\"?" |
| " It will be removed from your system's addressbook."), |
| person->formattedName().toUtf8().constData()); |
| |
| /* get parent window so we can center on it */ |
| GtkWidget *parent = gtk_widget_get_toplevel(GTK_WIDGET(item)); |
| if (gtk_widget_is_toplevel(parent)) { |
| gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(parent)); |
| gtk_window_set_position(GTK_WINDOW(dialog), GTK_WIN_POS_CENTER_ON_PARENT); |
| } |
| |
| if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_OK) { |
| person->remove(); |
| } |
| |
| gtk_widget_destroy(dialog); |
| } |
| |
| /** |
| * Update the menu when the selected item in the treeview changes. |
| */ |
| static void |
| update(GtkTreeSelection *selection, ContactPopupMenu *self) |
| { |
| ContactPopupMenuPrivate *priv = CONTACT_POPUP_MENU_GET_PRIVATE(self); |
| |
| /* clear the current menu */ |
| gtk_container_forall(GTK_CONTAINER(self), (GtkCallback)gtk_widget_destroy, nullptr); |
| |
| /* we always build a menu, however in some cases some or all of the items will be deactivated; |
| * we prefer this to having an empty menu because GTK+ behaves weird in the empty menu case |
| */ |
| auto call_item = gtk_menu_item_new_with_mnemonic(_("_Call")); |
| gtk_widget_set_sensitive(GTK_WIDGET(call_item), FALSE); |
| gtk_menu_shell_append(GTK_MENU_SHELL(self), call_item); |
| auto copy_name_item = gtk_menu_item_new_with_mnemonic(_("_Copy name")); |
| gtk_widget_set_sensitive(GTK_WIDGET(copy_name_item), FALSE); |
| gtk_menu_shell_append(GTK_MENU_SHELL(self), copy_name_item); |
| auto copy_number_item = gtk_menu_item_new_with_mnemonic(_("_Copy number")); |
| gtk_widget_set_sensitive(GTK_WIDGET(copy_number_item), FALSE); |
| gtk_menu_shell_append(GTK_MENU_SHELL(self), copy_number_item); |
| auto add_to_contact_item = gtk_menu_item_new_with_mnemonic(_("_Add to contact")); |
| gtk_widget_set_sensitive(GTK_WIDGET(add_to_contact_item), FALSE); |
| gtk_menu_shell_append(GTK_MENU_SHELL(self), add_to_contact_item); |
| auto remove_contact_item = gtk_menu_item_new_with_mnemonic(_("_Remove contact")); |
| gtk_widget_set_sensitive(GTK_WIDGET(remove_contact_item), FALSE); |
| gtk_menu_shell_append(GTK_MENU_SHELL(self), remove_contact_item); |
| |
| /* show all items */ |
| gtk_widget_show_all(GTK_WIDGET(self)); |
| |
| GtkTreeIter iter; |
| GtkTreeModel *model; |
| if (!gtk_tree_selection_get_selected(selection, &model, &iter)) |
| return; |
| |
| QModelIndex idx = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(model), &iter); |
| |
| auto type = idx.data(static_cast<int>(Ring::Role::ObjectType)); |
| auto object = idx.data(static_cast<int>(Ring::Role::Object)); |
| if (!type.isValid() || !object.isValid()) |
| return; // not a valid Ring::Role::Object, so nothing to do |
| |
| /* call */ |
| switch (type.value<Ring::ObjectType>()) { |
| case Ring::ObjectType::Person: |
| { |
| /* possiblity for multiple numbers */ |
| auto cms = object.value<Person *>()->phoneNumbers(); |
| if (cms.size() == 1) { |
| gtk_widget_set_sensitive(GTK_WIDGET(call_item), TRUE); |
| g_signal_connect(call_item, |
| "activate", |
| G_CALLBACK(call_contactmethod), |
| cms.at(0)); |
| } else if (cms.size() > 1) { |
| gtk_widget_set_sensitive(GTK_WIDGET(call_item), TRUE); |
| auto call_menu = gtk_menu_new(); |
| gtk_menu_item_set_submenu(GTK_MENU_ITEM(call_item), call_menu); |
| for (int i = 0; i < cms.size(); ++i) { |
| gchar *number = nullptr; |
| if (cms.at(i)->category()) { |
| // try to get the number category, eg: "home" |
| number = g_strdup_printf("(%s) %s", cms.at(i)->category()->name().toUtf8().constData(), |
| cms.at(i)->uri().toUtf8().constData()); |
| } else { |
| number = g_strdup_printf("%s", cms.at(i)->uri().toUtf8().constData()); |
| } |
| auto item = gtk_menu_item_new_with_label(number); |
| g_free(number); |
| gtk_menu_shell_append(GTK_MENU_SHELL(call_menu), item); |
| g_signal_connect(item, |
| "activate", |
| G_CALLBACK(call_contactmethod), |
| cms.at(i)); |
| } |
| } |
| } |
| break; |
| case Ring::ObjectType::ContactMethod: |
| { |
| gtk_widget_set_sensitive(GTK_WIDGET(call_item), TRUE); |
| auto cm = object.value<ContactMethod *>(); |
| g_signal_connect(call_item, |
| "activate", |
| G_CALLBACK(call_contactmethod), |
| cm); |
| } |
| break; |
| case Ring::ObjectType::Call: |
| { |
| gtk_widget_set_sensitive(GTK_WIDGET(call_item), TRUE); |
| auto call = object.value<Call *>(); |
| g_signal_connect(call_item, |
| "activate", |
| G_CALLBACK(call_contactmethod), |
| call->peerContactMethod()); |
| } |
| break; |
| case Ring::ObjectType::Media: |
| case Ring::ObjectType::Certificate: |
| case Ring::ObjectType::ContactRequest: |
| // nothing to do for now |
| case Ring::ObjectType::COUNT__: |
| break; |
| } |
| |
| /* copy name */ |
| QVariant name_var = idx.data(static_cast<int>(Ring::Role::Name)); |
| if (name_var.isValid()) { |
| gtk_widget_set_sensitive(GTK_WIDGET(copy_name_item), TRUE); |
| gchar *name = g_strdup_printf("%s", name_var.value<QString>().toUtf8().constData()); |
| g_object_set_data_full(G_OBJECT(copy_name_item), COPY_DATA_KEY, name, (GDestroyNotify)g_free); |
| g_signal_connect(copy_name_item, |
| "activate", |
| G_CALLBACK(copy_contact_info), |
| NULL); |
| } |
| |
| /* copy number(s) */ |
| switch (type.value<Ring::ObjectType>()) { |
| case Ring::ObjectType::Person: |
| { |
| /* possiblity for multiple numbers */ |
| auto cms = object.value<Person *>()->phoneNumbers(); |
| if (cms.size() == 1) { |
| gtk_widget_set_sensitive(GTK_WIDGET(copy_number_item), TRUE); |
| gchar *number = g_strdup_printf("%s",cms.at(0)->uri().toUtf8().constData()); |
| g_object_set_data_full(G_OBJECT(copy_number_item), COPY_DATA_KEY, number, (GDestroyNotify)g_free); |
| g_signal_connect(copy_number_item, |
| "activate", |
| G_CALLBACK(copy_contact_info), |
| NULL); |
| } else if (cms.size() > 1) { |
| gtk_widget_set_sensitive(GTK_WIDGET(copy_number_item), TRUE); |
| auto copy_menu = gtk_menu_new(); |
| gtk_menu_item_set_submenu(GTK_MENU_ITEM(copy_number_item), copy_menu); |
| for (int i = 0; i < cms.size(); ++i) { |
| auto number = g_strdup_printf("%s",cms.at(i)->uri().toUtf8().constData()); |
| gchar *category_number = nullptr; |
| if (cms.at(i)->category()) { |
| // try to get the number category, eg: "home" |
| category_number = g_strdup_printf("(%s) %s", |
| cms.at(i)->category()->name().toUtf8().constData(), |
| number); |
| } else { |
| category_number = g_strdup_printf("%s", number); |
| } |
| auto item = gtk_menu_item_new_with_label(category_number); |
| g_free(category_number); |
| gtk_menu_shell_append(GTK_MENU_SHELL(copy_menu), item); |
| g_object_set_data_full(G_OBJECT(item), COPY_DATA_KEY, number, (GDestroyNotify)g_free); |
| g_signal_connect(item, |
| "activate", |
| G_CALLBACK(copy_contact_info), |
| NULL); |
| } |
| } |
| } |
| break; |
| case Ring::ObjectType::ContactMethod: |
| case Ring::ObjectType::Call: |
| { |
| QVariant number_var = idx.data(static_cast<int>(Ring::Role::Number)); |
| if (number_var.isValid()) { |
| gtk_widget_set_sensitive(GTK_WIDGET(copy_number_item), TRUE); |
| gchar *number = g_strdup_printf("%s", number_var.value<QString>().toUtf8().constData()); |
| g_object_set_data_full(G_OBJECT(copy_number_item), COPY_DATA_KEY, number, (GDestroyNotify)g_free); |
| g_signal_connect(copy_number_item, |
| "activate", |
| G_CALLBACK(copy_contact_info), |
| NULL); |
| } |
| } |
| break; |
| case Ring::ObjectType::Media: |
| case Ring::ObjectType::Certificate: |
| case Ring::ObjectType::ContactRequest: |
| // nothing to do |
| case Ring::ObjectType::COUNT__: |
| break; |
| } |
| |
| /* get rectangle to know where to draw the add to contact popup */ |
| GdkRectangle rect; |
| auto path = gtk_tree_model_get_path(model, &iter); |
| auto column = gtk_tree_view_get_column(priv->treeview, 0); |
| gtk_tree_view_get_cell_area(priv->treeview, path, column, &rect); |
| gtk_tree_view_convert_bin_window_to_widget_coords(priv->treeview, rect.x, rect.y, &rect.x, &rect.y); |
| gtk_tree_path_free(path); |
| |
| /* add to contact - only offer to add CMs which are not already associated with a Person */ |
| switch (type.value<Ring::ObjectType>()) { |
| case Ring::ObjectType::Person: |
| // already a contact |
| break; |
| case Ring::ObjectType::ContactMethod: |
| { |
| auto cm = object.value<ContactMethod *>(); |
| if (!cm->contact()) { |
| gtk_widget_set_sensitive(GTK_WIDGET(add_to_contact_item), TRUE); |
| menu_item_add_to_contact(GTK_MENU_ITEM(add_to_contact_item), cm, GTK_WIDGET(priv->treeview), &rect); |
| } |
| } |
| break; |
| case Ring::ObjectType::Call: |
| { |
| auto cm = object.value<Call *>()->peerContactMethod(); |
| if (!cm->contact()) { |
| gtk_widget_set_sensitive(GTK_WIDGET(add_to_contact_item), TRUE); |
| menu_item_add_to_contact(GTK_MENU_ITEM(add_to_contact_item), cm, GTK_WIDGET(priv->treeview), &rect); |
| } |
| } |
| break; |
| case Ring::ObjectType::Media: |
| case Ring::ObjectType::Certificate: |
| case Ring::ObjectType::ContactRequest: |
| // nothing to do |
| case Ring::ObjectType::COUNT__: |
| break; |
| } |
| |
| /* remove contact */ |
| if (type.value<Ring::ObjectType>() == Ring::ObjectType::Person) { |
| gtk_widget_set_sensitive(GTK_WIDGET(remove_contact_item), TRUE); |
| auto person = object.value<Person *>(); |
| g_signal_connect(remove_contact_item, |
| "activate", |
| G_CALLBACK(remove_contact), |
| person); |
| } |
| |
| /* show all items */ |
| gtk_widget_show_all(GTK_WIDGET(self)); |
| } |
| |
| static void |
| contact_popup_menu_init(G_GNUC_UNUSED ContactPopupMenu *self) |
| { |
| // nothing to do |
| } |
| |
| static void |
| contact_popup_menu_dispose(GObject *object) |
| { |
| G_OBJECT_CLASS(contact_popup_menu_parent_class)->dispose(object); |
| } |
| |
| static void |
| contact_popup_menu_finalize(GObject *object) |
| { |
| G_OBJECT_CLASS(contact_popup_menu_parent_class)->finalize(object); |
| } |
| |
| static void |
| contact_popup_menu_class_init(ContactPopupMenuClass *klass) |
| { |
| G_OBJECT_CLASS(klass)->finalize = contact_popup_menu_finalize; |
| G_OBJECT_CLASS(klass)->dispose = contact_popup_menu_dispose; |
| } |
| |
| GtkWidget * |
| contact_popup_menu_new(GtkTreeView *treeview) |
| { |
| gpointer self = g_object_new(CONTACT_POPUP_MENU_TYPE, NULL); |
| ContactPopupMenuPrivate *priv = CONTACT_POPUP_MENU_GET_PRIVATE(self); |
| |
| priv->treeview = treeview; |
| GtkTreeSelection *selection = gtk_tree_view_get_selection(priv->treeview); |
| g_signal_connect(selection, "changed", G_CALLBACK(update), self); |
| |
| // build the menu for the first time |
| update(selection, CONTACT_POPUP_MENU(self)); |
| |
| return (GtkWidget *)self; |
| } |
| |
| gboolean |
| contact_popup_menu_show(ContactPopupMenu *self, GdkEventButton *event) |
| { |
| /* check for right click */ |
| if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_SECONDARY ) { |
| /* the menu will automatically get updated when the selection changes */ |
| gtk_menu_popup(GTK_MENU(self), NULL, NULL, NULL, NULL, event->button, event->time); |
| } |
| |
| return GDK_EVENT_PROPAGATE; /* so that the item selection changes */ |
| } |