blob: 7dc4a7c7588c017b5019feda074cb5fa61cbb38e [file] [log] [blame]
/*
* Copyright (C) 2016-2017 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"
#include "utils/accounts.h"
// GTK+ related
#include <glib/gi18n.h>
// LRC
#include <contactmethod.h>
#include <person.h>
#include <personmodel.h>
#include <numbercategory.h>
#include <call.h>
#include <account.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
add_daemon_contact(G_GNUC_UNUSED GtkWidget *item, ContactMethod *cm)
{
// get the choosen account
auto account = cm->account();
if (not account) {
account = get_active_ring_account();
if (not account) {
g_warning("invalid account, cannot send invitation!");
return;
}
}
// Send a request to the new contact
if (not account->sendContactRequest(cm))
g_warning("contact request not forwarded, cannot send invitation!");
}
static void
add_contact(G_GNUC_UNUSED GtkWidget *item, ContactMethod *cm)
{
const auto& collections = PersonModel::instance().enabledCollections();
CollectionInterface* collection = nullptr;
for (const auto c : collections) {
if (c->id() == QByteArray("ppc")) {
collection = c;
}
}
if (collection) {
// Create a new Person in PeerProfileCollection
auto person = new Person(collection);
person->setFormattedName(cm->bestName());
Person::ContactMethods numbers;
numbers << cm;
person->setContactMethods(numbers);
PersonModel::instance().addPeerProfile(person);
person->save();
} else {
g_warning("Can't find PeerProfileCollection");
}
// And add it in Daemon Contacts
add_daemon_contact(item, cm);
}
static void
rm_daemon_contact(G_GNUC_UNUSED GtkWidget *item, ContactMethod *cm)
{
// get the choosen account
auto account = cm->account();
if (not account) {
account = get_active_ring_account();
if (not account) {
g_warning("invalid account, cannot send invitation!");
return;
}
}
if (not account->removeContact(cm))
g_warning("contact request not forwarded, cannot send invitation!");
}
static void
remove_contact(GtkWidget *item, Person *person)
{
// Remove the Person from PeerProfileCollection
person->remove();
// And the cm from Daemon Contacts
for ( const auto cm : person->phoneNumbers() ) {
rm_daemon_contact(item, cm);
}
}
/**
* 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);
// Get the current cm
ContactMethod* cm = nullptr;
switch (type.value<Ring::ObjectType>()) {
case Ring::ObjectType::Person:
{
auto cms = object.value<Person *>()->phoneNumbers();
if (cms.size() > 0)
cm = cms.at(0);
}
break;
case Ring::ObjectType::ContactMethod:
cm = object.value<ContactMethod *>();
break;
case Ring::ObjectType::Call:
cm = object.value<Call *>()->peerContactMethod();
break;
case Ring::ObjectType::Media:
case Ring::ObjectType::Certificate:
case Ring::ObjectType::ContactRequest:
case Ring::ObjectType::COUNT__:
// nothing to do
break;
}
auto account = cm->account();
if (not account) {
account = get_active_ring_account();
}
switch (type.value<Ring::ObjectType>()) {
case Ring::ObjectType::Person:
{
auto person = object.value<Person *>();
gtk_widget_set_sensitive(GTK_WIDGET(remove_contact_item), true);
// Remove to PeerProfileCollection and Daemon Contacts
g_signal_connect(remove_contact_item,
"activate",
G_CALLBACK(remove_contact),
person);
}
break;
case Ring::ObjectType::ContactMethod:
case Ring::ObjectType::Call:
{
// Check if it's a daemon contact
auto isADaemonContact = false;
if (account) {
auto contacts = account->getContacts();
isADaemonContact = contacts.indexOf(cm) != -1;
}
gtk_widget_set_sensitive(GTK_WIDGET(add_to_contact_item), !isADaemonContact);
gtk_widget_set_sensitive(GTK_WIDGET(remove_contact_item), isADaemonContact);
// Add to PeerProfileCollection and Daemon Contacts
g_signal_connect(add_to_contact_item,
"activate",
G_CALLBACK(add_contact),
cm);
// Remove from Daemon Contacts (it's not in PeerProfileCollection)
g_signal_connect(remove_contact_item,
"activate",
G_CALLBACK(rm_daemon_contact),
cm);
}
break;
case Ring::ObjectType::Media:
case Ring::ObjectType::Certificate:
case Ring::ObjectType::ContactRequest:
case Ring::ObjectType::COUNT__:
{
// No CM to add or remove
gtk_widget_set_sensitive(GTK_WIDGET(add_to_contact_item), false);
gtk_widget_set_sensitive(GTK_WIDGET(remove_contact_item), false);
}
break;
}
/* 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 */
}