Project 'savoirfairelinux/ring-daemon' was moved to 'savoirfairelinux/jami-daemon'. Please update any links and bookmarks that may still have the old path.
Select Git revision
recentmodel.cpp
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
recentmodel.cpp 43.98 KiB
/************************************************************************************
* Copyright (C) 2015-2016 by Savoir-faire Linux *
* Author : Emmanuel Lepage Vallee <emmanuel.lepage@savoirfairelinux.com> *
* Alexandre Lision <alexandre.lision@savoirfairelinux.com> *
* Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com> *
* *
* This library is free software; you can redistribute it and/or *
* modify it under the terms of the GNU Lesser General Public *
* License as published by the Free Software Foundation; either *
* version 2.1 of the License, or (at your option) any later version. *
* *
* This library 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 *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *
***********************************************************************************/
#include "recentmodel.h"
//std
#include <algorithm>
//Qt
#include <QtCore/QCoreApplication>
#include <QtCore/QSortFilterProxyModel>
//Ring
#include <call.h>
#include <person.h>
#include <personmodel.h>
#include <contactmethod.h>
#include <phonedirectorymodel.h>
#include <callmodel.h>
#include <categorizedhistorymodel.h>
#include <media/recordingmodel.h>
#include <media/textrecording.h>
#include "accountmodel.h"
#include "contactrequest.h"
#include "certificate.h"
#include "availableaccountmodel.h"
struct CallGroup
{
QVector<Call*> m_lCalls ;
Call::Direction m_Direction;
bool m_Missed ;
time_t m_LastUsed ;
};
struct RecentViewNode
{
//Types
enum class Type {
PERSON ,
CONTACT_METHOD ,
CALL ,
CALL_GROUP ,
CONFERENCE ,
TEXT_MESSAGE ,
TEXT_MESSAGE_GROUP,
};
//Constructor
explicit RecentViewNode();
RecentViewNode(Call* c, RecentModelPrivate* model);
RecentViewNode(const Person *p, RecentModelPrivate* model);
RecentViewNode(ContactMethod *cm, RecentModelPrivate* model);
virtual ~RecentViewNode();
//Attributes
RecentModelPrivate* m_pModel ;
long int m_Index ;
Type m_Type ;
RecentViewNode* m_pParent ;
QList<RecentViewNode*> m_lChildren;
QMetaObject::Connection m_ConnectionChanged;
union {
const Person* m_pPerson ;
ContactMethod* m_pContactMethod;
Call* m_pCall ;
CallGroup* m_pCallGroup ;
//ImConversationIterator; //TODO
//ImConversationIterator;
} m_uContent;
//Helpers
inline time_t lastUsed ( ) const;
RecentViewNode* childNode(Call *call) const;
void slotChanged( );
};
class PeopleProxy : public QSortFilterProxyModel
{
Q_OBJECT
public:
PeopleProxy(RecentModel* source_model);
virtual QVariant data(const QModelIndex& index, int role) const override;
protected:
virtual bool filterAcceptsRow ( int source_row, const QModelIndex & source_parent ) const override;
};
class RecentModelPrivate : public QObject
{
Q_OBJECT
public:
RecentModelPrivate(RecentModel* p);
/*
* m_lTopLevelReverted hold the elements in the reverse order of
* QAbstractItemModel::index. This cause most of the energy to be
* in the bottom half of the vector, preventing std::move every time
* someone is contacted
*/
QList<RecentViewNode*> m_lTopLevelReverted;
QHash<const Person*,RecentViewNode*> m_hPersonsToNodes ;
QHash<ContactMethod*,RecentViewNode*> m_hCMsToNodes ;
QHash<Call*,RecentViewNode*> m_hCallsToNodes ;
QHash<Call*,RecentViewNode*> m_hConfToNodes ;
QItemSelectionModel* m_pSelectionModel ;
//Helper
void insertNode (RecentViewNode* n, time_t t, bool isNew);
void removeNode (RecentViewNode* n );
RecentViewNode* parentNode (Call *call ) const;
void insertCallNode(RecentViewNode* parent, RecentViewNode* callNode);
void moveCallNode (RecentViewNode* destination, RecentViewNode* callNode);
void removeCall (RecentViewNode* callNode );
void selectNode (RecentViewNode* node ) const;
private:
RecentModel* q_ptr;
public Q_SLOTS:
void slotLastUsedTimeChanged(const Person* p , time_t t );
void slotPersonAdded (const Person* p );
void slotLastUsedChanged (ContactMethod* cm, time_t t );
void slotContactChanged (ContactMethod* cm, Person* np, Person* op);
void slotCallAdded (Call* call , Call* parent );
void slotChanged (RecentViewNode* node );
void slotCallStateChanged (Call* call , Call::State previousState);
void slotConferenceAdded (Call* conf );
void slotConferenceRemoved (Call* conf );
void slotConferenceChanged (Call* conf );
void slotCurrentCallChanged (const QModelIndex ¤t, const QModelIndex &previous);
};
RecentModelPrivate::RecentModelPrivate(RecentModel* p) : q_ptr(p)
{
m_pSelectionModel = nullptr;
}
QItemSelectionModel* RecentModel::selectionModel() const
{
if (!d_ptr->m_pSelectionModel) {
d_ptr->m_pSelectionModel = new QItemSelectionModel(const_cast<RecentModel*>(this));
}
return d_ptr->m_pSelectionModel;
}
QStringList
RecentModel::getParticipantName(Call* call) const
{
if (not call || call->type() != Call::Type::CONFERENCE) {
qWarning() << "Invalid use of getParticipantName";
return QStringList();
}
if (auto node = d_ptr->m_hConfToNodes[call]) {
QStringList participants;
Q_FOREACH(auto child, node->m_lChildren) {
participants << data(getIndex(child->m_uContent.m_pCall), static_cast<int>(Ring::Role::Name)).toString();
}
return participants;
}
return QStringList();
}
int
RecentModel::getParticipantNumber(Call *call) const
{
if (not call || call->type() != Call::Type::CONFERENCE) {
qWarning() << "Invalid use of getParticipantNumber";
return 0;
}
if (auto node = d_ptr->m_hConfToNodes[call]) {
return node->m_lChildren.size();
}
return 0;
}
void RecentModelPrivate::selectNode(RecentViewNode* node) const
{
const auto idx = q_ptr->createIndex(node->m_Index, 0, node);
q_ptr->selectionModel()->setCurrentIndex(idx, QItemSelectionModel::ClearAndSelect);
}
RecentModel::RecentModel(QObject* parent) : QAbstractItemModel(parent), d_ptr(new RecentModelPrivate(this))
{
connect(&PersonModel::instance() , &PersonModel::lastUsedTimeChanged , d_ptr, &RecentModelPrivate::slotLastUsedTimeChanged);
connect(&PersonModel::instance() , &PersonModel::newPersonAdded , d_ptr, &RecentModelPrivate::slotPersonAdded );
connect(&PhoneDirectoryModel::instance(), &PhoneDirectoryModel::lastUsedChanged, d_ptr, &RecentModelPrivate::slotLastUsedChanged );
connect(&PhoneDirectoryModel::instance(), &PhoneDirectoryModel::contactChanged , d_ptr, &RecentModelPrivate::slotContactChanged );
connect(&CallModel::instance() , &CallModel::callAdded , d_ptr, &RecentModelPrivate::slotCallAdded );
connect(&CallModel::instance() , &CallModel::callStateChanged , d_ptr, &RecentModelPrivate::slotCallStateChanged );
connect(&CallModel::instance() , &CallModel::conferenceCreated , d_ptr, &RecentModelPrivate::slotConferenceAdded );
connect(&CallModel::instance() , &CallModel::conferenceRemoved , d_ptr, &RecentModelPrivate::slotConferenceRemoved );
connect(&CallModel::instance() , &CallModel::conferenceChanged , d_ptr, &RecentModelPrivate::slotConferenceChanged );
connect(CallModel::instance().selectionModel(), &QItemSelectionModel::currentChanged, d_ptr, &RecentModelPrivate::slotCurrentCallChanged);
// Fill contacts from daemon source
for (int i = 0; i < AccountModel::instance().rowCount(); i++) {
auto account = qvariant_cast<Account*>(AccountModel::instance().data(AccountModel::instance().index(i,0),
static_cast<int>(Account::Role::Object)));
for (auto cm : account->getContacts())
d_ptr->slotLastUsedChanged(cm, cm->lastUsed());
}
// Called when a contact was added to the daemon list (e.g when the user as accepted a request)
connect(&AccountModel::instance(), &AccountModel::accountContactAdded, [this] (Account* a, const ContactRequest* r) {
auto cm = r->certificate()->contactMethod();
if (!cm)
cm = PhoneDirectoryModel::instance().getNumber(r->certificate()->remoteId(), a);
d_ptr->slotLastUsedChanged(cm, cm->lastUsed());
});
//Fill the contacts
for (int i=0; i < PersonModel::instance().rowCount(); i++) {
auto person = qvariant_cast<Person*>(PersonModel::instance().data(
PersonModel::instance().index(i,0),
static_cast<int>(Person::Role::Object)
));
if (person && person->lastUsedTime())
d_ptr->slotLastUsedTimeChanged(person, person->lastUsedTime());
}
//Fill the "orphan" contact methods
for (int i = 0; i < PhoneDirectoryModel::instance().rowCount(); i++) {
auto cm = qvariant_cast<ContactMethod*>(PhoneDirectoryModel::instance().data(
PhoneDirectoryModel::instance().index(i,0),
static_cast<int>(PhoneDirectoryModel::Role::Object)
));
if (cm && cm->lastUsed() && (!cm->contact() || cm->contact()->isPlaceHolder()))
d_ptr->slotLastUsedChanged(cm, cm->lastUsed());
}
//Get any ongoing calls (except conferences)
auto callList = CallModel::instance().getActiveCalls();
for (int i = 0; i < callList.size(); ++i) {
auto call = callList.at(i);
if (call->type() != Call::Type::CONFERENCE)
d_ptr->slotCallAdded(call, nullptr);
}
//Add any ongoing conferences
auto confList = CallModel::instance().getActiveConferences();
for (int i = 0; i < confList.size(); ++i) {
auto conf = confList.at(i);
d_ptr->slotConferenceAdded(conf);
}
}
RecentModel::~RecentModel()
{
for (RecentViewNode* n : d_ptr->m_lTopLevelReverted)
delete n;
delete d_ptr;
}
RecentViewNode::RecentViewNode()
{
}
RecentViewNode::RecentViewNode(Call* c, RecentModelPrivate *model)
{
m_pModel = model ;
m_Type = c->type() == Call::Type::CONFERENCE ? RecentViewNode::Type::CONFERENCE : RecentViewNode::Type::CALL;
m_uContent.m_pCall = c ;
m_pParent = nullptr ;
m_Index = 0 ;
m_ConnectionChanged = QObject::connect(c, &Call::changed, [this](){this->slotChanged();});
}
RecentViewNode::RecentViewNode(const Person* p, RecentModelPrivate *model)
{
m_pModel = model ;
m_Type = RecentViewNode::Type::PERSON;
m_uContent.m_pPerson = p ;
m_pParent = nullptr ;
m_Index = 0 ;
m_ConnectionChanged = QObject::connect(p, &Person::changed, [this](){this->slotChanged();});
}
RecentViewNode::RecentViewNode(ContactMethod *cm, RecentModelPrivate *model)
{
m_pModel = model ;
m_Type = RecentViewNode::Type::CONTACT_METHOD;
m_uContent.m_pContactMethod = cm ;
m_pParent = nullptr ;
m_Index = 0 ;
m_ConnectionChanged = QObject::connect(cm, &ContactMethod::changed, [this](){this->slotChanged();});
}
RecentViewNode::~RecentViewNode()
{
QObject::disconnect(m_ConnectionChanged);
for (RecentViewNode* n : m_lChildren) {
delete n;
}
}
time_t RecentViewNode::lastUsed() const
{
switch(m_Type) {
case Type::PERSON :
return m_uContent.m_pPerson->lastUsedTime();
case Type::CONTACT_METHOD :
return m_uContent.m_pContactMethod->lastUsed();
case Type::CALL :
case Type::CONFERENCE:
return m_uContent.m_pCall->stopTimeStamp();
case Type::CALL_GROUP :
return m_uContent.m_pCallGroup->m_LastUsed;
case Type::TEXT_MESSAGE :
case Type::TEXT_MESSAGE_GROUP:
//TODO
break;
}
return {};
}
// returns the child node containing the given call, if it exists
RecentViewNode*
RecentViewNode::childNode(Call *call) const
{
if (!call)
return {};
// only Person and CM nodes contains calls as children for now, so no need to check other types
if ( !(m_Type == RecentViewNode::Type::PERSON || m_Type == RecentViewNode::Type::CONTACT_METHOD) )
return {};
auto itEnd = m_lChildren.cend();
auto it = std::find_if (m_lChildren.cbegin(),
itEnd, [call] (RecentViewNode* child) {
return child->m_uContent.m_pCall == call;
});
if (it == itEnd)
return {};
return *it;
}
void
RecentViewNode::slotChanged()
{
m_pModel->slotChanged(this);
}
RecentModel& RecentModel::instance()
{
static auto instance = new RecentModel(QCoreApplication::instance());
return *instance;
}
/**
* Tries to find the given call in the RecentModel and return
* the corresponding index
*/
QModelIndex
RecentModel::getIndex(Call *call) const
{
// first check if it is a conference
if (auto confNode = d_ptr->m_hConfToNodes.value(call))
return index(confNode->m_Index, 0);
if (auto callNode = d_ptr->m_hCallsToNodes.value(call)) {
if (callNode->m_pParent)
return index(callNode->m_Index, 0, index(callNode->m_pParent->m_Index, 0));
}
return {};
}
/**
* Tries to find the given Person in the RecentModel and return
* the corresponding index
*/
QModelIndex
RecentModel::getIndex(Person *p) const
{
if (d_ptr->m_hPersonsToNodes.contains(p)) {
if (auto node = d_ptr->m_hPersonsToNodes.value(p))
return index(node->m_Index, 0);
}
return {};
}
/**
* Tries to find the given CM in the RecentModel and return
* the corresponding index
*/
QModelIndex
RecentModel::getIndex(ContactMethod *cm) const
{
// check if the CM is an item the RecentModel
if (d_ptr->m_hCMsToNodes.contains(cm)) {
if (auto node = d_ptr->m_hCMsToNodes.value(cm))
return index(node->m_Index, 0);
}
// otherwise, its possible the CM is contained within a Person item
if (auto person = cm->contact())
return getIndex(person);
return {};
}
/**
* Returns if the given index corresponds to an item in the RecentModel which is a conference
*/
bool
RecentModel::isConference(const QModelIndex& idx) const
{
if (idx.isValid()) {
auto object = idx.data(static_cast<int>(Ring::Role::Object));
if (auto call = object.value<Call *>())
return call->type() == Call::Type::CONFERENCE;
}
return false;
}
/**
* Check if given index has an ongoing call
* returns true if one of its child is also in the CallModel
*/
bool RecentModel::hasActiveCall(const QModelIndex &idx)
{
if (not idx.isValid())
return false;
auto node = static_cast<RecentViewNode*>(idx.internalPointer());
QListIterator<RecentViewNode*> lIterator(node->m_lChildren);
lIterator.toBack();
while (lIterator.hasPrevious()) {
auto child = lIterator.previous();
if (child->m_Type == RecentViewNode::Type::CALL) {
return CallModel::instance().getIndex(child->m_uContent.m_pCall).isValid();
}
}
return false;
}
/**
* Helper function to extract all ContactMethod from a given index
* The index must be of type ContactMethod, Person or Call
*/
QVector<ContactMethod*>
RecentModel::getContactMethods(const QModelIndex& idx) const
{
auto result = QVector<ContactMethod*>();
RecentViewNode* node = static_cast<RecentViewNode*>(idx.internalPointer());
if (!idx.isValid() || !node) {
return result;
}
switch(node->m_Type) {
case RecentViewNode::Type::PERSON :
result << node->m_uContent.m_pPerson->phoneNumbers();
return result;
case RecentViewNode::Type::CONTACT_METHOD :
result << node->m_uContent.m_pContactMethod;
return result;
case RecentViewNode::Type::CALL :
result << node->m_uContent.m_pCall->peerContactMethod();
return result;
case RecentViewNode::Type::CALL_GROUP :
case RecentViewNode::Type::TEXT_MESSAGE :
case RecentViewNode::Type::TEXT_MESSAGE_GROUP:
case RecentViewNode::Type::CONFERENCE :
break;
}
return result;
}
/**
* Return the first found ongoing call of the given index
* Index can be the call itself or the associated parent
*/
Call* RecentModel::getActiveCall(const QModelIndex &idx)
{
if (not idx.isValid())
return nullptr;
RecentViewNode* node = static_cast<RecentViewNode*>(idx.internalPointer());
if (node->m_Type == RecentViewNode::Type::CALL
|| node->m_Type == RecentViewNode::Type::CONFERENCE ) {
return node->m_uContent.m_pCall;
}
QListIterator<RecentViewNode*> lIterator(node->m_lChildren);
lIterator.toBack();
while (lIterator.hasPrevious()) {
auto child = lIterator.previous();
if (child->m_Type == RecentViewNode::Type::CALL) {
return child->m_uContent.m_pCall;
}
}
return nullptr;
}
QHash<int,QByteArray> RecentModel::roleNames() const
{
static QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
/*static bool initRoles = false;
if (!initRoles) {
initRoles = true;
}*/
return roles;
}
bool RecentModel::setData( const QModelIndex& index, const QVariant &value, int role)
{
Q_UNUSED(index)
Q_UNUSED(value)
Q_UNUSED(role)
return false;
}
QVariant RecentModel::data( const QModelIndex& index, int role ) const
{
if (!index.isValid())
return QVariant();
RecentViewNode* node = static_cast<RecentViewNode*>(index.internalPointer());
switch(node->m_Type) {
case RecentViewNode::Type::PERSON :
return node->m_uContent.m_pPerson->roleData(role);
case RecentViewNode::Type::CONTACT_METHOD :
return node->m_uContent.m_pContactMethod->roleData(role);
case RecentViewNode::Type::CALL :
case RecentViewNode::Type::CONFERENCE :
return node->m_uContent.m_pCall->roleData(role);
case RecentViewNode::Type::CALL_GROUP :
return node->m_uContent.m_pCallGroup->m_lCalls[0]->roleData(role);
case RecentViewNode::Type::TEXT_MESSAGE :
case RecentViewNode::Type::TEXT_MESSAGE_GROUP:
//TODO
break;
}
return QVariant();
}
int RecentModel::rowCount( const QModelIndex& parent ) const
{
if (!parent.isValid())
return d_ptr->m_lTopLevelReverted.size();
RecentViewNode* node = static_cast<RecentViewNode*>(parent.internalPointer());
return node->m_lChildren.size();
}
Qt::ItemFlags RecentModel::flags( const QModelIndex& index ) const
{
return index.isValid() ? Qt::ItemIsEnabled | Qt::ItemIsSelectable : Qt::NoItemFlags;
}
int RecentModel::columnCount( const QModelIndex& parent ) const
{
Q_UNUSED(parent)
return 1;
}
QModelIndex RecentModel::parent( const QModelIndex& index ) const
{
if (!index.isValid())
return QModelIndex();
RecentViewNode* node = static_cast<RecentViewNode*>(index.internalPointer());
if (!node->m_pParent)
return QModelIndex();
return createIndex(node->m_pParent->m_Index, 0, node->m_pParent);
}
QModelIndex RecentModel::index( int row, int column, const QModelIndex& parent) const
{
if (!parent.isValid() && row >= 0 && row < d_ptr->m_lTopLevelReverted.size() && !column)
return createIndex(row, 0, d_ptr->m_lTopLevelReverted[d_ptr->m_lTopLevelReverted.size() - 1 - row]);
if (!parent.isValid())
return QModelIndex();
RecentViewNode* node = static_cast<RecentViewNode*>(parent.internalPointer());
if (row >= 0 && row < node->m_lChildren.size())
return createIndex(row, 0, node->m_lChildren[row]);
return QModelIndex();
}
QVariant RecentModel::headerData( int section, Qt::Orientation orientation, int role) const
{
if (!section && role == Qt::DisplayRole && orientation == Qt::Horizontal)
return tr("Recent persons");
return QVariant();
}
/*
* Move rows around to keep the person/contactmethods ordered
*
* Further optimization:
* * Invert m_Index to avoid the O(N) loop when adding an item
*/
void RecentModelPrivate::insertNode(RecentViewNode* n, time_t t, bool isNew)
{
//Don't bother with the sorted insertion and indexes housekeeping
if (m_lTopLevelReverted.isEmpty()) {
q_ptr->beginInsertRows(QModelIndex(),0,0);
m_lTopLevelReverted << n;
q_ptr->endInsertRows();
return;
}
//Compute the bounds, this is needed to use beginMoveRows
int newPos = 0;
if (m_lTopLevelReverted.last()->lastUsed() > t) {
//NOTE std::lower_bound need the "value" argument to be the same type as the iterator
//this use the m_Index field to hold the time_t
static RecentViewNode fake;
fake.m_Index = static_cast<long int>(t);
//NOTE Using std::lower_bound is officially supported on QList on all platforms
auto lower = std::lower_bound(m_lTopLevelReverted.begin(), m_lTopLevelReverted.end(), &fake,
[t](const RecentViewNode* a, const RecentViewNode* t2) -> bool {
return a->lastUsed() < t2->m_Index;
});
// the value pointed by the iterator returned by this function may also
// be equivalent to val, and not only greater
if (!isNew && n->m_Index == (*lower)->m_Index) {
newPos = (*lower)->m_Index;
} else
newPos = (*lower)->m_Index+1;
}
//Begin the transaction
if (!isNew) {
if (newPos == n->m_Index)
return; //Nothing to do
if (not q_ptr->beginMoveRows(QModelIndex(), n->m_Index, n->m_Index, QModelIndex(), newPos)) {
qWarning() << "RecentModel: Invalid move detected index : " << n->m_Index
<< "newPos: " << newPos << "size: " << m_lTopLevelReverted.size();
return;
}
m_lTopLevelReverted.removeAt(m_lTopLevelReverted.size()-1-n->m_Index);
}
else
q_ptr->beginInsertRows(QModelIndex(),newPos,newPos);
//Apply the transaction
m_lTopLevelReverted.insert(m_lTopLevelReverted.size() - newPos,n);
for (int i = 0 ; i < m_lTopLevelReverted.size(); ++i) {
m_lTopLevelReverted[i]->m_Index = m_lTopLevelReverted.size() - 1 - i;
}
//Notify that the transaction is complete
if (!isNew)
q_ptr->endMoveRows();
else
q_ptr->endInsertRows();
#if 0
//Uncomment if there is issues
qDebug() << "\n\nList:" << m_lTopLevelReverted.size() << isNew;
for (int i = 0; i<m_lTopLevelReverted.size();i++) {
qDebug() << "|||" << m_lTopLevelReverted[i]->lastUsed() << m_lTopLevelReverted[i]->m_Index << q_ptr->data(q_ptr->index(m_lTopLevelReverted.size()-1-i,0),Qt::DisplayRole);
for (auto child : m_lTopLevelReverted[i]->m_lChildren) {
qDebug() << "|||" << "|||" << child << child->m_uContent.m_pCall->formattedName();
}
}
#endif
}
void RecentModelPrivate::removeNode(RecentViewNode* n)
{
const int idx = n->m_Index;
q_ptr->beginRemoveRows(QModelIndex(), idx, idx);
m_lTopLevelReverted.removeOne(n);
delete n;
// update all indices after this one
for (int i = m_lTopLevelReverted.size() - 1 - idx; i >= 0; --i) {
--m_lTopLevelReverted[i]->m_Index;
}
q_ptr->endRemoveRows();
}
void RecentModelPrivate::slotPersonAdded(const Person* p)
{
if (p) {
// prevent person duplication by checking if the contact method
// is already present in m_hCMsToNodes
for ( const auto cmToRm : p->phoneNumbers() )
if ( auto cmNode = m_hCMsToNodes.take(cmToRm) )
removeNode(cmNode);
slotLastUsedTimeChanged(p, p->lastUsedTime());
}
}
void RecentModelPrivate::slotLastUsedTimeChanged(const Person* p, time_t t)
{
RecentViewNode* n = m_hPersonsToNodes.value(p);
const bool isNew = !n;
if (isNew) {
n = new RecentViewNode(p, this);
n->m_pParent = nullptr;
m_hPersonsToNodes[p] = n;
}
insertNode(n, t, isNew);
}
void RecentModelPrivate::slotLastUsedChanged(ContactMethod* cm, time_t t)
{
//ContactMethod with a Person are handled elsewhere
if (!cm->contact() || cm->contact()->isPlaceHolder()) {
RecentViewNode* n = m_hCMsToNodes.value(cm);
const bool isNew = !n;
if (isNew) {
n = new RecentViewNode(cm, this);
m_hCMsToNodes[cm] = n;
}
insertNode(n, t, isNew);
}
}
///Remove the contact method once they are associated with a contact
void RecentModelPrivate::slotContactChanged(ContactMethod* cm, Person* np, Person* op)
{
Q_UNUSED(op)
// m_hCMsToNodes contains RecentViewNode pointers, take will return a default
// constructed ptr (e.g nullptr) if key is not in the QHash
if (auto n = m_hCMsToNodes.take(cm)) {
// remove its child calls from the list first, they will be destroyed when the call is over
auto newParentNode = np != nullptr ? m_hPersonsToNodes[np] : nullptr;
Q_FOREACH(auto cmNode, n->m_lChildren) {
if (newParentNode) {
cmNode->m_pParent = newParentNode;
newParentNode->m_lChildren.append(cmNode);
} else
cmNode->m_pParent = nullptr;
}
n->m_lChildren.clear();
removeNode(n);
if (newParentNode && newParentNode->m_lChildren.size()) {
selectNode(newParentNode);
}
}
}
void
RecentModelPrivate::insertCallNode(RecentViewNode *parent, RecentViewNode* callNode)
{
if (!parent) {
qWarning() << "parent node is null while trying to insert a call node";
return;
}
if (!callNode) {
qWarning() << "call node is null when trying to insert a call node";
return;
}
// make sure the parent is a Person or CM
// TODO: allow conference parent
if ( ! (parent->m_Type == RecentViewNode::Type::PERSON
|| parent->m_Type == RecentViewNode::Type::CONTACT_METHOD) ) {
qWarning() << "parent of Call Node must be a Person or Contact Method";
return;
}
callNode->m_pParent = parent;
callNode->m_Index = parent->m_lChildren.size();
auto parentIdx = q_ptr->index(parent->m_Index,0);
q_ptr->beginInsertRows(parentIdx, callNode->m_Index, callNode->m_Index);
parent->m_lChildren.append(callNode);
q_ptr->endInsertRows();
// emit dataChanged on parent, since number of children has changed
emit q_ptr->dataChanged(parentIdx, parentIdx);
if (parent->m_lChildren.size() > 1) {
// emit a dataChanged on the first call so that the PeopleProxy
// now shows the first call (otherwise it will only show the 2nd +)
auto firstChild = q_ptr->index(0, 0, parentIdx);
emit q_ptr->dataChanged(firstChild, firstChild);
}
/* in the case of a conference, we select the call;
* in case the parent only has one call, we select the parent;
* in case the parent has multiple calls, we select the call;
*/
auto callIdx = q_ptr->index(callNode->m_Index, 0, parentIdx);
if (q_ptr->isConference(callIdx) || q_ptr->isConference(parentIdx) || q_ptr->rowCount(parentIdx) > 1)
q_ptr->selectionModel()->setCurrentIndex(callIdx, QItemSelectionModel::ClearAndSelect);
else
q_ptr->selectionModel()->setCurrentIndex(callIdx.parent(), QItemSelectionModel::ClearAndSelect);
}
void
RecentModelPrivate::moveCallNode(RecentViewNode* destination, RecentViewNode* callNode)
{
if (not callNode->m_pParent) {
qWarning() << "Trying to move call node with invalid parent";
return;
}
if (callNode->m_Type != RecentViewNode::Type::CALL) {
qWarning() << "cannot move node which is not of type call" << callNode;
return;
}
auto parentNode = callNode->m_pParent;
auto parent = q_ptr->index(parentNode->m_Index, 0);
const auto removedIndex = callNode->m_Index;
q_ptr->beginRemoveRows(parent, removedIndex, removedIndex);
parentNode->m_lChildren.removeAt(removedIndex);
// update the indices of the remaining children
for (int i = removedIndex; i < parentNode->m_lChildren.size(); ++i) {
if (auto child = parentNode->m_lChildren.at(i))
--child->m_Index;
}
q_ptr->endRemoveRows();
callNode->m_pParent = destination;
callNode->m_Index = destination->m_lChildren.size();
auto destIdx = q_ptr->index(destination->m_Index, 0);
q_ptr->beginInsertRows(destIdx, callNode->m_Index, callNode->m_Index);
destination->m_lChildren.append(callNode);
q_ptr->endInsertRows();
if (parentNode->m_lChildren.size() == 1) {
// there is now only one call, emit dataChanged on it so it becomes hidden in the PeopleProxy
auto firstChild = q_ptr->index(0, 0, parent);
emit q_ptr->dataChanged(firstChild, firstChild);
}
// emit dataChanged on the parent since the number of children has changed
emit q_ptr->dataChanged(parent, parent);
}
void
RecentModelPrivate::removeCall(RecentViewNode* callNode)
{
if (callNode->m_Type != RecentViewNode::Type::CALL) {
qWarning() << "cannot remove node which is not of type call" << callNode;
return;
}
m_hCallsToNodes.remove(callNode->m_uContent.m_pCall);
// if it was in the RecentModel, then we need to emit rowsRemoved
if (auto parentNode = callNode->m_pParent) {
auto parent = q_ptr->index(parentNode->m_Index, 0);
const auto removedIndex = callNode->m_Index;
q_ptr->beginRemoveRows(parent, removedIndex, removedIndex);
parentNode->m_lChildren.removeAt(removedIndex);
// update the indices of the remaining children
for (int i = removedIndex; i < parentNode->m_lChildren.size(); ++i) {
if (auto child = parentNode->m_lChildren.at(i))
--child->m_Index;
}
q_ptr->endRemoveRows();
if (parentNode->m_lChildren.size() == 1) {
// there is now only one call, emit dataChanged on it so it becomes hidden in the PeopleProxy
auto firstChild = q_ptr->index(0, 0, parent);
emit q_ptr->dataChanged(firstChild, firstChild);
}
// emit dataChanted on the parent since the number of children has changed
emit q_ptr->dataChanged(parent, parent);
}
delete callNode;
}
void
RecentModelPrivate::slotCallAdded(Call* call, Call* parent)
{
Q_UNUSED(parent)
if(!call) {
qWarning() << "callAdded on nullptr";
return;
}
// new call, create Call node and try to find its parent
// if we find a parent, insert the Call node into the model, otherwise it will be done in slotChanged
auto callNode = new RecentViewNode(call, this);
m_hCallsToNodes[call] = callNode;
// check if call is associated with a Person or CM yet
if (auto parent = parentNode(call)) {
insertCallNode(parent, callNode);
}
}
// helper method to find parent node of a call, if it exists
RecentViewNode*
RecentModelPrivate::parentNode(Call *call) const
{
if (!call)
return {};
if (auto p = call->peerContactMethod()->contact()) {
// if we don't find the Person in our list of nodes, then we want to check the list of CMs
if (m_hPersonsToNodes.contains(p))
return m_hPersonsToNodes.value(p);
}
return m_hCMsToNodes.value(call->peerContactMethod());
}
void
RecentModelPrivate::slotChanged(RecentViewNode *node)
{
if(!node) {
qWarning() << "changed called on null RecentViewNode";
return;
}
switch (node->m_Type) {
case RecentViewNode::Type::PERSON:
case RecentViewNode::Type::CONTACT_METHOD:
case RecentViewNode::Type::CONFERENCE:
{
auto idx = q_ptr->index(node->m_Index, 0);
emit q_ptr->dataChanged(idx, idx);
}
break;
case RecentViewNode::Type::CALL:
{
// make sure the Call has a parent, else try to find one
if (node->m_pParent) {
auto parent = q_ptr->index(node->m_pParent->m_Index, 0);
auto idx = q_ptr->index(node->m_Index, 0, parent);
emit q_ptr->dataChanged(parent, parent);
emit q_ptr->dataChanged(idx, idx);
} else {
if (auto parent = parentNode(node->m_uContent.m_pCall)) {
insertCallNode(parent, node);
}
}
}
break;
case RecentViewNode::Type::CALL_GROUP:
case RecentViewNode::Type::TEXT_MESSAGE:
case RecentViewNode::Type::TEXT_MESSAGE_GROUP:
// nothing to do for now
break;
}
}
void
RecentModelPrivate::slotCallStateChanged(Call* call, Call::State previousState)
{
Q_UNUSED(previousState)
if (auto callNode = m_hCallsToNodes.value(call)) {
switch(call->state()) {
case Call::State::COUNT__:
case Call::State::ERROR:
case Call::State::ABORTED:
case Call::State::OVER:
removeCall(callNode);
break;
case Call::State::TRANSFERRED:
case Call::State::INCOMING:
case Call::State::RINGING:
case Call::State::INITIALIZATION:
case Call::State::CONNECTED:
case Call::State::CURRENT:
case Call::State::DIALING:
case Call::State::NEW:
case Call::State::HOLD:
case Call::State::FAILURE:
case Call::State::BUSY:
case Call::State::TRANSF_HOLD:
case Call::State::CONFERENCE:
case Call::State::CONFERENCE_HOLD:
break;
};
}
}
void
RecentModelPrivate::slotConferenceRemoved(Call* conf)
{
RecentViewNode* n = m_hConfToNodes[conf];
if (n) {
foreach (RecentViewNode* node, n->m_lChildren) {
if (node->m_uContent.m_pCall->lifeCycleState() != Call::LifeCycleState::PROGRESS)
removeCall(node);
else {
moveCallNode(parentNode(node->m_uContent.m_pCall), node);
}
}
removeNode(n);
m_hConfToNodes.remove(conf);
}
}
void
RecentModelPrivate::slotConferenceAdded(Call* conf)
{
RecentViewNode* n = m_hConfToNodes[conf];
const bool isNew = !n;
if (isNew) {
n = new RecentViewNode(conf, this);
m_hConfToNodes[conf] = n;
}
insertNode(n, conf->startTimeStamp(), isNew);
// move the participants after inserting the node
auto pList = CallModel::instance().getConferenceParticipants(conf);
foreach (Call* p, pList) {
moveCallNode(n, m_hCallsToNodes.value(p));
}
// Select the newly created conference
q_ptr->selectionModel()->setCurrentIndex(q_ptr->getIndex(conf), QItemSelectionModel::ClearAndSelect);
}
void RecentModelPrivate::slotConferenceChanged(Call* conf)
{
if (auto confNode = m_hConfToNodes.value(conf)) {
auto pSet = QSet<Call*>::fromList(CallModel::instance().getConferenceParticipants(conf));
if (pSet.isEmpty()) {
slotConferenceRemoved(conf);
return;
}
if (confNode->m_lChildren.size() == pSet.size())
return;
QSet<Call*> confPSet;
foreach(const RecentViewNode* node, confNode->m_lChildren) {
confPSet.insert(node->m_uContent.m_pCall);
}
if (confPSet.size() > pSet.size()) {
confPSet = confPSet.subtract(pSet);
foreach(Call* call, confPSet) {
removeCall(m_hCallsToNodes.value(call));
}
} else {
pSet = pSet.subtract(confPSet);
foreach(Call* call, pSet) {
moveCallNode(confNode, m_hCallsToNodes.value(call));
}
// Reselect the conference
q_ptr->selectionModel()->setCurrentIndex(q_ptr->getIndex(conf), QItemSelectionModel::ClearAndSelect);
}
}
}
void
RecentModelPrivate::slotCurrentCallChanged(const QModelIndex ¤t, const QModelIndex &previous)
{
Q_UNUSED(previous)
auto callIdx = q_ptr->getIndex(CallModel::instance().getCall(current));
if (callIdx.isValid()) {
/* in the case of a conference, we select the call;
* in case the parent only has one call, we select the parent;
* in case the parent has multiple calls, we select the call;
*/
auto parentIdx = callIdx.parent();
if (q_ptr->isConference(callIdx) || q_ptr->isConference(parentIdx) || q_ptr->rowCount(parentIdx) > 1)
q_ptr->selectionModel()->setCurrentIndex(callIdx, QItemSelectionModel::ClearAndSelect);
else
q_ptr->selectionModel()->setCurrentIndex(callIdx.parent(), QItemSelectionModel::ClearAndSelect);
} else {
/* nothing is selected in the CallModel; however, if we still have a call selected in the
* RecentModel, we need to select it in the CallModel, or else all the actions of the call
* will be invalid, since the UserActionModel is based on the selection of the CallModel */
auto recentIdx = q_ptr->selectionModel()->currentIndex();
auto recentCall = q_ptr->getActiveCall(recentIdx);
if (recentIdx.isValid() && recentCall) {
CallModel::instance().selectCall(recentCall);
}
/* otherwise do not update the selection in the RecentModel, eg: if a Person was selected
* and the Call is over, we still want the Person to be selected */
}
}
///Filter out every data relevant to a person
QSortFilterProxyModel*
RecentModel::peopleProxy() const
{
static PeopleProxy* p = new PeopleProxy(const_cast<RecentModel*>(this));
return p;
}
PeopleProxy::PeopleProxy(RecentModel* sourceModel)
{
setSourceModel(sourceModel);
}
bool
PeopleProxy::filterAcceptsRow(int source_row, const QModelIndex & source_parent) const
{
//we filter only on top nodes
if (!source_parent.isValid() && filterRegExp().isEmpty()) {
// get the user chosen account
auto index_chosen_account = AvailableAccountModel::instance().selectionModel()->currentIndex();
auto chosen_account = index_chosen_account.data(static_cast<int>(Account::Role::Object)).value<Account*>();
// if there is no account selected, show the item.
if (not chosen_account)
return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent);
auto idx = sourceModel()->index(source_row, 0);
if (not idx.isValid()) // for example, manages rowCount() calls
return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent);
auto type = idx.data(static_cast<int>(Ring::Role::ObjectType)).value<Ring::ObjectType>();
auto object = idx.data(static_cast<int>(Ring::Role::Object));
if (type == Ring::ObjectType::ContactMethod) {
// checks if the associated account is the same that the one selected
auto cm = object.value<ContactMethod *>();
// LRC can create cm without account (typically if the cm was createad but never called)
// in this case the cm will be shown for any account.
if (not cm->account())
return cm;
return cm->account() == chosen_account;
} else if (type == Ring::ObjectType::Person) {
const auto person_numbers = object.value<Person *>()->phoneNumbers();
// checks if the Person contains any ContactMethod wich has the same account than the one selected
if (chosen_account and \
std::any_of(std::begin(person_numbers), std::end(person_numbers),
[&](const ContactMethod* cm) { return cm->account() == chosen_account; })) {
return true;
}
// return false if any ContactMethod has a valid account (but none are the selected_account)
if (std::any_of(std::begin(person_numbers), std::end(person_numbers),
[&](const ContactMethod* cm) { return cm->account() != nullptr; })) {
return false;
}
}
// anything else without ContactMethod does not require to be filtered
return true;
}else if (!source_parent.isValid()) {
auto idx = sourceModel()->index(source_row, 0);
//we want to filter on name and number; note that Person object may have many numbers
if (idx.data(static_cast<int>(Ring::Role::Name)).toString().contains(filterRegExp())) {
return true;
} else {
auto type = idx.data(static_cast<int>(Ring::Role::ObjectType)).value<Ring::ObjectType>();
auto object = idx.data(static_cast<int>(Ring::Role::Object));
switch (type) {
case Ring::ObjectType::Person:
{
auto p = object.value<Person *>();
for (auto cm : p->phoneNumbers()) {
if (cm->uri().full().contains(filterRegExp()))
return true;
}
return false;
}
break;
case Ring::ObjectType::ContactMethod:
{
auto cm = object.value<ContactMethod *>();
return cm->uri().full().contains(filterRegExp());
}
break;
// top nodes are only of type Person or ContactMethod
case Ring::ObjectType::Call:
case Ring::ObjectType::Media:
case Ring::ObjectType::Certificate:
case Ring::ObjectType::ContactRequest:
case Ring::ObjectType::COUNT__:
break;
}
}
return false; // no matches
}
//in the case of children, only show if there is more than one unless it is a conference
if (static_cast<RecentModel *>(sourceModel())->isConference(source_parent)
|| sourceModel()->rowCount(source_parent) > 1 )
return true;
return false;
}
QVariant
PeopleProxy::data(const QModelIndex& index, int role) const
{
auto indexSource = this->mapToSource(index);
if (!indexSource.isValid())
return QVariant();
//This proxy model filters out single calls, so in this case we want to forward certain data
//from the call to its parent
RecentViewNode* node = static_cast<RecentViewNode*>(indexSource.internalPointer());
bool topNode = node->m_Type == RecentViewNode::Type::PERSON ||
node->m_Type == RecentViewNode::Type::CONTACT_METHOD;
bool forwardRole = role == static_cast<int>(Ring::Role::State) ||
role == static_cast<int>(Ring::Role::FormattedState) ||
role == static_cast<int>(Ring::Role::Length);
if ( topNode && forwardRole ) {
if (sourceModel()->rowCount(indexSource) == 1) {
auto child = sourceModel()->index(0, 0, indexSource);
return sourceModel()->data(child, role);
}
}
return sourceModel()->data(indexSource, role);
}
#include <recentmodel.moc>