blob: 6b5fb11d817a14aee0988ff93b9c90155bdacf56 [file] [log] [blame]
/*
* Copyright (C) 2015-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 "recentcontactsview.h"
#include <gtk/gtk.h>
#include <glib/gi18n.h>
#include "models/gtkqtreemodel.h"
#include "utils/calling.h"
#include <memory>
#include <globalinstances.h>
#include "native/pixbufmanipulator.h"
#include <contactmethod.h>
#include "defines.h"
#include "utils/models.h"
#include <recentmodel.h>
#include <call.h>
#include "utils/menus.h"
#include <itemdataroles.h>
#include <callmodel.h>
#include <QtCore/QItemSelectionModel>
#include <historytimecategorymodel.h>
#include <QtCore/QDateTime>
#include <QtCore/QMimeData>
#include "utils/drawing.h"
#include <numbercategory.h>
#include "contactpopupmenu.h"
static constexpr const char* CALL_TARGET = "CALL_TARGET";
static constexpr int CALL_TARGET_ID = 0;
struct _RecentContactsView
{
GtkTreeView parent;
};
struct _RecentContactsViewClass
{
GtkTreeViewClass parent_class;
};
typedef struct _RecentContactsViewPrivate RecentContactsViewPrivate;
struct _RecentContactsViewPrivate
{
GtkWidget *popup_menu;
QMetaObject::Connection selection_updated;
QMetaObject::Connection layout_changed;
};
G_DEFINE_TYPE_WITH_PRIVATE(RecentContactsView, recent_contacts_view, GTK_TYPE_TREE_VIEW);
#define RECENT_CONTACTS_VIEW_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), RECENT_CONTACTS_VIEW_TYPE, RecentContactsViewPrivate))
static void
update_selection(GtkTreeSelection *selection)
{
if (gtk_q_tree_model_ignore_selection_change(selection)) return;
auto current_proxy = get_index_from_selection(selection);
auto current = RecentModel::instance().peopleProxy()->mapToSource(current_proxy);
RecentModel::instance().selectionModel()->setCurrentIndex(current, QItemSelectionModel::ClearAndSelect);
// update the CallModel selection since we rely on the UserActionModel
if (auto call_to_select = RecentModel::instance().getActiveCall(current)) {
CallModel::instance().selectCall(call_to_select);
} else {
CallModel::instance().selectionModel()->clearCurrentIndex();
}
}
static void
render_contact_photo(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
GtkCellRenderer *cell,
GtkTreeModel *model,
GtkTreeIter *iter,
G_GNUC_UNUSED gpointer data)
{
QModelIndex idx = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(model), iter);
std::shared_ptr<GdkPixbuf> image;
/* we only want to render a photo for the top nodes: Person, ContactMethod (, later Conference) */
QVariant object = idx.data(static_cast<int>(Ring::Role::Object));
if (idx.isValid() && object.isValid()) {
QVariant var_photo;
if (auto person = object.value<Person *>()) {
var_photo = GlobalInstances::pixmapManipulator().contactPhoto(person, QSize(50, 50), true);
} else if (auto cm = object.value<ContactMethod *>()) {
/* get photo, note that this should in all cases be the fallback avatar, since there
* shouldn't be a person associated with this contact method */
var_photo = GlobalInstances::pixmapManipulator().callPhoto(cm, QSize(50, 50), true);
} else if (auto call = object.value<Call *>()) {
if (call->type() == Call::Type::CONFERENCE) {
var_photo = GlobalInstances::pixmapManipulator().callPhoto(call, QSize(50, 50), true);
}
}
image = var_photo.value<std::shared_ptr<GdkPixbuf>>();
}
// set the width of the cell rendered to the width of the photo
// so that the other renderers are shifted to the right
g_object_set(G_OBJECT(cell), "width", 50, NULL);
g_object_set(G_OBJECT(cell), "pixbuf", image.get(), NULL);
}
/**
* This is the 2nd column in the treeview; for Person and ContactMethod items we want to DisplayRole
* the name in the first row and the number (Ring registered name or URI) in the second row. for
* Conferences we simply display the name, and for calls we simply display the Call status (note:
* that if the item is a Call, it is not a top level item)
*/
static void
render_name_and_number(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
GtkCellRenderer *cell,
GtkTreeModel *model,
GtkTreeIter *iter,
GtkTreeView *treeview)
{
gchar *text = nullptr;
QModelIndex idx = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(model), iter);
// check if this iter is selected
gboolean is_selected = FALSE;
if (GTK_IS_TREE_VIEW(treeview)) {
auto selection = gtk_tree_view_get_selection(treeview);
is_selected = gtk_tree_selection_iter_is_selected(selection, iter);
}
auto type = idx.data(static_cast<int>(Ring::Role::ObjectType));
if (idx.isValid() && type.isValid()) {
switch (type.value<Ring::ObjectType>()) {
case Ring::ObjectType::Person:
case Ring::ObjectType::ContactMethod:
{
auto name = idx.data(static_cast<int>(Ring::Role::Name)).toString();
auto number = idx.data(static_cast<int>(Ring::Role::Number)).toString();
/* we want the color of the status text to be the default color if this iter is
* selected so that the treeview is able to invert it against the selection color */
if (is_selected) {
text = g_markup_printf_escaped(
"<span font_weight=\"bold\">%s</span>\n<span size=\"smaller\">%s</span>",
// "%s\n<span size=\"smaller\">%s</span>",
name.toUtf8().constData(),
number.toUtf8().constData()
);
} else {
text = g_markup_printf_escaped(
"<span font_weight=\"bold\">%s</span>\n<span color=\"gray\" size=\"smaller\">%s</span>",
// "%s\n<span color=\"gray\" size=\"smaller\">%s</span>",
name.toUtf8().constData(),
number.toUtf8().constData()
);
}
}
break;
case Ring::ObjectType::Call:
{
// check if it is a conference
auto idx_source = RecentModel::instance().peopleProxy()->mapToSource(idx);
auto is_conference = RecentModel::instance().isConference(idx_source);
if (is_conference) {
auto var_name = idx.data(static_cast<int>(Ring::Role::Name));
text = g_markup_escape_text(var_name.value<QString>().toUtf8().constData(), -1);
} else {
auto parent_source = RecentModel::instance().peopleProxy()->mapToSource(idx.parent());
if (RecentModel::instance().isConference(parent_source)) {
// part of conference, simply display the name
auto var_name = idx.data(static_cast<int>(Ring::Role::Name));
/* we want the color of the name text to be the default color if this iter is
* selected so that the treeview is able to invert it against the selection color */
if (is_selected) {
text = g_markup_printf_escaped("<span size=\"smaller\">%s</span>",
var_name.value<QString>().toUtf8().constData());
} else {
text = g_markup_printf_escaped("<span size=\"smaller\" color=\"gray\">%s</span>",
var_name.value<QString>().toUtf8().constData());
}
} else {
// just a call, so display the state
auto var_status = idx.data(static_cast<int>(Ring::Role::FormattedState));
QString status;
if (var_status.isValid())
status += var_status.value<QString>();
/* we want the color of the status text to be the default color if this iter is
* selected so that the treeview is able to invert it against the selection color */
if (is_selected) {
text = g_markup_printf_escaped("<span size=\"smaller\">%s</span>",
status.toUtf8().constData());
} else {
text = g_markup_printf_escaped("<span size=\"smaller\" color=\"gray\">%s</span>",
status.toUtf8().constData());
}
}
}
}
break;
case Ring::ObjectType::Media:
case Ring::ObjectType::Certificate:
case Ring::ObjectType::ContactRequest:
// nothing to do for now
case Ring::ObjectType::COUNT__:
break;
}
}
g_object_set(G_OBJECT(cell), "markup", text, NULL);
g_free(text);
}
/**
* This is the 3rd column in the treeview. For Person and ContactMethod items we want to display
* in the first row the call status or else the last used date. In the second row we want to display
* either the call duration or else the number of unread messages. In the case of a Call item we
* simply display the call duration.
*/
static void
render_info(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
GtkCellRenderer *cell,
GtkTreeModel *model,
GtkTreeIter *iter,
GtkTreeView *treeview)
{
gchar *text = nullptr;
QModelIndex idx = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(model), iter);
// check if this iter is selected
gboolean is_selected = FALSE;
if (GTK_IS_TREE_VIEW(treeview)) {
auto selection = gtk_tree_view_get_selection(treeview);
is_selected = gtk_tree_selection_iter_is_selected(selection, iter);
}
auto type = idx.data(static_cast<int>(Ring::Role::ObjectType));
if (idx.isValid() && type.isValid()) {
switch (type.value<Ring::ObjectType>()) {
case Ring::ObjectType::Person:
case Ring::ObjectType::ContactMethod:
{
gchar *row0 = nullptr; // either call status or last used
gchar *row1 = nullptr; // either call duration or unread msg count
QString status;
auto var_status = idx.data(static_cast<int>(Ring::Role::FormattedState));
auto var_lastused = idx.data(static_cast<int>(Ring::Role::LastUsed));
// show the status if there is a call, otherwise the last used date/time
if (var_status.isValid()) {
status = var_status.value<QString>();
} else if (var_lastused.isValid()) {
auto date_time = var_lastused.value<QDateTime>();
auto category = HistoryTimeCategoryModel::timeToHistoryConst(date_time.toTime_t());
/* If it is 'today', then we show the time; otherwise we will show date category
* (the day or long ago it was). The day and the time together take up too much
* space */
if (category == HistoryTimeCategoryModel::HistoryConst::Today) {
status = QLocale::system().toString(date_time.time(), QLocale::ShortFormat);
} else {
status = HistoryTimeCategoryModel::timeToHistoryCategory(date_time.toTime_t());
}
}
if (is_selected) {
row0 = g_markup_printf_escaped(
"<span size=\"smaller\">%s</span>",
status.toUtf8().constData()
);
} else {
row0 = g_markup_printf_escaped(
"<span size=\"smaller\" color=\"gray\">%s</span>",
status.toUtf8().constData()
);
}
// check if there are any children (calls); we need to convert to source model in
// case there is only one
auto idx_source = RecentModel::instance().peopleProxy()->mapToSource(idx);
auto duration = idx.data(static_cast<int>(Ring::Role::Length));
if (idx_source.isValid()
&& (idx_source.model()->rowCount(idx_source) == 1)
&& duration.isValid())
{
row1 = g_markup_printf_escaped("%s", duration.toString().toUtf8().constData());
}
else
{
auto unread = idx.data(static_cast<int>(Ring::Role::UnreadTextMessageCount)).toInt();
if (unread > 0) {
if (is_selected) {
row1 = g_markup_printf_escaped(
"<span font_weight=\"bold\">%d</span>",
unread
);
} else {
row1 = g_markup_printf_escaped(
"<span color=\"red\" font_weight=\"bold\">%d</span>",
unread
);
}
}
}
text = g_strconcat(row0, "\n", row1, nullptr);
g_free(row0);
g_free(row1);
}
break;
case Ring::ObjectType::Call:
{
// do not display the duration if the call is part of a conference
auto parent_source = RecentModel::instance().peopleProxy()->mapToSource(idx.parent());
auto in_conference = RecentModel::instance().isConference(parent_source);
if (!in_conference) {
auto duration = idx.data(static_cast<int>(Ring::Role::Length));
if (duration.isValid())
text = g_markup_escape_text(duration.value<QString>().toUtf8().constData(), -1);
}
}
break;
case Ring::ObjectType::Media:
case Ring::ObjectType::Certificate:
case Ring::ObjectType::ContactRequest:
// nothing to do for now
case Ring::ObjectType::COUNT__:
break;
}
}
g_object_set(G_OBJECT(cell), "markup", text, NULL);
g_free(text);
}
static void
activate_item(GtkTreeView *tree_view,
GtkTreePath *path,
G_GNUC_UNUSED GtkTreeViewColumn *column,
G_GNUC_UNUSED gpointer user_data)
{
auto model = gtk_tree_view_get_model(tree_view);
GtkTreeIter iter;
if (gtk_tree_model_get_iter(model, &iter, path)) {
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));
if (idx.isValid() && type.isValid()) {
switch (type.value<Ring::ObjectType>()) {
case Ring::ObjectType::Person:
{
// call the last used contact method
// TODO: if no contact methods have been used, offer a popup to Choose
auto p_var = idx.data(static_cast<int>(Ring::Role::Object));
if (p_var.isValid()) {
auto person = p_var.value<Person *>();
auto cms = person->phoneNumbers();
if (!cms.isEmpty()) {
auto last_used_cm = cms.at(0);
for (int i = 1; i < cms.size(); ++i) {
auto new_cm = cms.at(i);
if (difftime(new_cm->lastUsed(), last_used_cm->lastUsed()) > 0)
last_used_cm = new_cm;
}
place_new_call(last_used_cm);
}
}
}
break;
case Ring::ObjectType::ContactMethod:
{
// call the contact method
auto cm = idx.data(static_cast<int>(Ring::Role::Object));
if (cm.isValid())
place_new_call(cm.value<ContactMethod *>());
}
break;
case Ring::ObjectType::Call:
case Ring::ObjectType::Media:
case Ring::ObjectType::Certificate:
case Ring::ObjectType::ContactRequest:
// nothing to do for now
case Ring::ObjectType::COUNT__:
break;
}
}
}
}
static void
expand_if_child(G_GNUC_UNUSED GtkTreeModel *tree_model,
GtkTreePath *path,
G_GNUC_UNUSED GtkTreeIter *iter,
GtkTreeView *treeview)
{
if (gtk_tree_path_get_depth(path) > 1)
gtk_tree_view_expand_to_path(treeview, path);
}
static void
scroll_to_selection(GtkTreeSelection *selection)
{
auto treeview = gtk_tree_selection_get_tree_view(selection);
auto model = gtk_tree_view_get_model(treeview);
if (gtk_q_tree_model_is_layout_changing(GTK_Q_TREE_MODEL(model))) {
/* during a layout change, the GtkTreeModel items will be removed, so the GTK selection will
* be empty, we want to ignore this */
return;
}
GtkTreeIter iter;
if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
auto path = gtk_tree_model_get_path(model, &iter);
auto treeview = gtk_tree_selection_get_tree_view(selection);
gtk_tree_view_scroll_to_cell(treeview, path, nullptr, FALSE, 0.0, 0.0);
}
}
static void
on_drag_data_get(GtkWidget *treeview,
G_GNUC_UNUSED GdkDragContext *context,
GtkSelectionData *data,
G_GNUC_UNUSED guint info,
G_GNUC_UNUSED guint time,
G_GNUC_UNUSED gpointer user_data)
{
g_return_if_fail(IS_RECENT_CONTACTS_VIEW(treeview));
/* we always drag the selected row */
auto selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(treeview));
GtkTreeModel *model = NULL;
GtkTreeIter iter;
if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
auto path_str = gtk_tree_model_get_string_from_iter(model, &iter);
gtk_selection_data_set(data,
gdk_atom_intern_static_string(CALL_TARGET),
8, /* bytes */
(guchar *)path_str,
strlen(path_str) + 1);
g_free(path_str);
} else {
g_warning("drag selection not valid");
}
}
static gboolean
on_drag_drop(GtkWidget *treeview,
GdkDragContext *context,
gint x,
gint y,
guint time,
G_GNUC_UNUSED gpointer user_data)
{
g_return_val_if_fail(IS_RECENT_CONTACTS_VIEW(treeview), FALSE);
GtkTreePath *path = NULL;
GtkTreeViewDropPosition drop_pos;
if (gtk_tree_view_get_dest_row_at_pos(GTK_TREE_VIEW(treeview),
x, y, &path, &drop_pos)) {
GdkAtom target_type = gtk_drag_dest_find_target(treeview, context, NULL);
if (target_type != GDK_NONE) {
g_debug("can drop");
gtk_drag_get_data(treeview, context, target_type, time);
return TRUE;
}
gtk_tree_path_free(path);
}
return FALSE;
}
static gboolean
on_drag_motion(GtkWidget *treeview,
GdkDragContext *context,
gint x,
gint y,
guint time,
G_GNUC_UNUSED gpointer user_data)
{
g_return_val_if_fail(IS_RECENT_CONTACTS_VIEW(treeview), FALSE);
GtkTreePath *path = NULL;
GtkTreeViewDropPosition drop_pos;
if (gtk_tree_view_get_dest_row_at_pos(GTK_TREE_VIEW(treeview),
x, y, &path, &drop_pos)) {
// we only want to drop on a row, not before or after
if (drop_pos == GTK_TREE_VIEW_DROP_BEFORE) {
gtk_tree_view_set_drag_dest_row(GTK_TREE_VIEW(treeview), path, GTK_TREE_VIEW_DROP_INTO_OR_BEFORE);
} else if (drop_pos == GTK_TREE_VIEW_DROP_AFTER) {
gtk_tree_view_set_drag_dest_row(GTK_TREE_VIEW(treeview), path, GTK_TREE_VIEW_DROP_INTO_OR_AFTER);
}
gdk_drag_status(context, gdk_drag_context_get_suggested_action(context), time);
return TRUE;
} else {
// not a row in the treeview, so we cannot drop
return FALSE;
}
}
static void
on_drag_data_received(GtkWidget *treeview,
GdkDragContext *context,
gint x,
gint y,
GtkSelectionData *data,
G_GNUC_UNUSED guint info,
guint time,
G_GNUC_UNUSED gpointer user_data)
{
g_return_if_fail(IS_RECENT_CONTACTS_VIEW(treeview));
gboolean success = FALSE;
/* get the source and destination calls */
auto path_str_source = (gchar *)gtk_selection_data_get_data(data);
auto type = gtk_selection_data_get_data_type(data);
g_debug("data type: %s", gdk_atom_name(type));
if (path_str_source && strlen(path_str_source) > 0) {
g_debug("source path: %s", path_str_source);
/* get the destination path */
GtkTreePath *dest_path = NULL;
if (gtk_tree_view_get_dest_row_at_pos(GTK_TREE_VIEW(treeview), x, y, &dest_path, NULL)) {
auto model = gtk_tree_view_get_model(GTK_TREE_VIEW(treeview));
GtkTreeIter source;
gtk_tree_model_get_iter_from_string(model, &source, path_str_source);
auto idx_source_proxy = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(model), &source);
GtkTreeIter dest;
gtk_tree_model_get_iter(model, &dest, dest_path);
auto idx_dest_proxy = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(model), &dest);
// get call objects and indeces from RecentModel indeces being drag and dropped
auto idx_source = RecentModel::instance().peopleProxy()->mapToSource(idx_source_proxy);
auto idx_dest = RecentModel::instance().peopleProxy()->mapToSource(idx_dest_proxy);
auto call_source = RecentModel::instance().getActiveCall(idx_source);
auto call_dest = RecentModel::instance().getActiveCall(idx_dest);
auto idx_call_source = CallModel::instance().getIndex(call_source);
auto idx_call_dest = CallModel::instance().getIndex(call_dest);
if (idx_call_source.isValid() && idx_call_dest.isValid()) {
QModelIndexList source_list;
source_list << idx_call_source;
auto mimeData = CallModel::instance().mimeData(source_list);
auto action = Call::DropAction::Conference;
mimeData->setProperty("dropAction", action);
if (CallModel::instance().dropMimeData(mimeData, Qt::MoveAction, idx_call_dest.row(), idx_call_dest.column(), idx_call_dest.parent())) {
success = TRUE;
} else {
g_warning("could not drop mime data");
}
} else {
g_warning("source or dest call not valid");
}
gtk_tree_path_free(dest_path);
}
}
gtk_drag_finish(context, success, FALSE, time);
}
static gboolean
synchronize_selection(RecentContactsView *self)
{
auto idx = RecentModel::instance().selectionModel()->currentIndex();
auto selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(self));
auto model = gtk_tree_view_get_model(GTK_TREE_VIEW(self));
if (gtk_q_tree_model_is_layout_changing(GTK_Q_TREE_MODEL(model))) {
/* during a layout change, the GtkTreeModel items will be removed, so the GTK selection will
* be empty, we want to avoid trying to sync during this time, and reschedule it to try
* again later */
return G_SOURCE_CONTINUE;
}
auto idx_proxy = RecentModel::instance().peopleProxy()->mapFromSource(idx);
if (idx_proxy.isValid()) {
/* select the current */
GtkTreeIter iter;
if (gtk_q_tree_model_source_index_to_iter(GTK_Q_TREE_MODEL(model), idx_proxy, &iter)) {
gtk_tree_selection_select_iter(selection, &iter);
}
} else {
gtk_tree_selection_unselect_all(selection);
}
return G_SOURCE_REMOVE;
}
static void
recent_contacts_view_init(RecentContactsView *self)
{
RecentContactsViewPrivate *priv = RECENT_CONTACTS_VIEW_GET_PRIVATE(self);
gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(self), FALSE);
/* no need to show the expander since it will always be expanded */
gtk_tree_view_set_show_expanders(GTK_TREE_VIEW(self), FALSE);
/* disable default search, we will handle it ourselves via LRC;
* otherwise the search steals input focus on key presses */
gtk_tree_view_set_enable_search(GTK_TREE_VIEW(self), FALSE);
GtkQTreeModel *recent_model = gtk_q_tree_model_new(
RecentModel::instance().peopleProxy(),
1,
0, Qt::DisplayRole, G_TYPE_STRING);
gtk_tree_view_set_model(GTK_TREE_VIEW(self),
GTK_TREE_MODEL(recent_model));
/* photo and name/contact method column */
GtkCellArea *area = gtk_cell_area_box_new();
GtkTreeViewColumn *column = gtk_tree_view_column_new_with_area(area);
/* photo renderer */
GtkCellRenderer *renderer = gtk_cell_renderer_pixbuf_new();
gtk_cell_area_box_pack_start(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
/* get the photo */
gtk_tree_view_column_set_cell_data_func(
column,
renderer,
(GtkTreeCellDataFunc)render_contact_photo,
NULL,
NULL);
/* name/cm and status renderer */
renderer = gtk_cell_renderer_text_new();
g_object_set(G_OBJECT(renderer), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
gtk_cell_area_box_pack_start(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
gtk_tree_view_column_set_cell_data_func(
column,
renderer,
(GtkTreeCellDataFunc)render_name_and_number,
self,
NULL);
/* call duration or unread messages */
renderer = gtk_cell_renderer_text_new();
g_object_set(G_OBJECT(renderer), "xalign", 1.0, NULL);
gtk_cell_area_box_pack_end(GTK_CELL_AREA_BOX(area), renderer, FALSE, FALSE, FALSE);
gtk_tree_view_column_set_cell_data_func(
column,
renderer,
(GtkTreeCellDataFunc)render_info,
self,
NULL);
gtk_tree_view_append_column(GTK_TREE_VIEW(self), column);
gtk_tree_view_column_set_resizable(column, TRUE);
gtk_tree_view_column_set_expand(column, TRUE);
gtk_tree_view_expand_all(GTK_TREE_VIEW(self));
g_signal_connect(self, "row-activated", G_CALLBACK(activate_item), NULL);
g_signal_connect(recent_model, "row-inserted", G_CALLBACK(expand_if_child), self);
GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(self));
g_signal_connect(selection, "changed", G_CALLBACK(update_selection), NULL);
g_signal_connect(selection, "changed", G_CALLBACK(scroll_to_selection), NULL);
g_signal_connect_swapped(recent_model, "rows-reordered", G_CALLBACK(scroll_to_selection), selection);
/* sync initial selection */
g_idle_add((GSourceFunc)synchronize_selection, self);
auto synchronize_selection_idle = [self] () { g_idle_add((GSourceFunc)synchronize_selection, self); };
/* update the selection based on the RecentModel */
priv->selection_updated = QObject::connect(
RecentModel::instance().selectionModel(),
&QItemSelectionModel::currentChanged,
synchronize_selection_idle
);
/* we may need to update the selection when the layout changes */
priv->layout_changed = QObject::connect(
RecentModel::instance().peopleProxy(),
&QAbstractItemModel::layoutChanged,
synchronize_selection_idle
);
/* drag and drop */
static GtkTargetEntry targetentries[] = {
{ (gchar *)CALL_TARGET, GTK_TARGET_SAME_WIDGET, CALL_TARGET_ID },
};
gtk_tree_view_enable_model_drag_source(GTK_TREE_VIEW(self),
GDK_BUTTON1_MASK, targetentries, 1, (GdkDragAction)(GDK_ACTION_DEFAULT | GDK_ACTION_MOVE));
gtk_tree_view_enable_model_drag_dest(GTK_TREE_VIEW(self),
targetentries, 1, GDK_ACTION_DEFAULT);
g_signal_connect(self, "drag-data-get", G_CALLBACK(on_drag_data_get), nullptr);
g_signal_connect(self, "drag-drop", G_CALLBACK(on_drag_drop), nullptr);
g_signal_connect(self, "drag-motion", G_CALLBACK(on_drag_motion), nullptr);
g_signal_connect(self, "drag_data_received", G_CALLBACK(on_drag_data_received), nullptr);
/* init popup menu */
priv->popup_menu = contact_popup_menu_new(GTK_TREE_VIEW(self));
g_signal_connect_swapped(self, "button-press-event", G_CALLBACK(contact_popup_menu_show), priv->popup_menu);
gtk_widget_show_all(GTK_WIDGET(self));
}
static void
recent_contacts_view_dispose(GObject *object)
{
RecentContactsView *self = RECENT_CONTACTS_VIEW(object);
RecentContactsViewPrivate *priv = RECENT_CONTACTS_VIEW_GET_PRIVATE(self);
QObject::disconnect(priv->selection_updated);
QObject::disconnect(priv->layout_changed);
gtk_widget_destroy(priv->popup_menu);
G_OBJECT_CLASS(recent_contacts_view_parent_class)->dispose(object);
}
static void
recent_contacts_view_finalize(GObject *object)
{
G_OBJECT_CLASS(recent_contacts_view_parent_class)->finalize(object);
}
static void
recent_contacts_view_class_init(RecentContactsViewClass *klass)
{
G_OBJECT_CLASS(klass)->finalize = recent_contacts_view_finalize;
G_OBJECT_CLASS(klass)->dispose = recent_contacts_view_dispose;
}
GtkWidget *
recent_contacts_view_new()
{
gpointer self = g_object_new(RECENT_CONTACTS_VIEW_TYPE, NULL);
return (GtkWidget *)self;
}