diff --git a/Linphone/core/call/CallCore.cpp b/Linphone/core/call/CallCore.cpp index 5831f51e2..ec07b72e4 100644 --- a/Linphone/core/call/CallCore.cpp +++ b/Linphone/core/call/CallCore.cpp @@ -824,8 +824,6 @@ void CallCore::findRemoteLdapFriend(QSharedPointer me) { auto linphoneSearch = CoreModel::getInstance()->getCore()->createMagicSearch(); linphoneSearch->setLimitedSearch(true); mLdapMagicSearchModel = Utils::makeQObject_ptr(linphoneSearch); - mLdapMagicSearchModel->setSourceFlags((int)LinphoneEnums::MagicSearchSource::LdapServers); - mLdapMagicSearchModel->setAggregationFlag(LinphoneEnums::MagicSearchAggregation::Friend); mLdapMagicSearchModel->setSelf(mLdapMagicSearchModel); mLdapMagicSearchModelConnection = QSharedPointer>( new SafeConnection(me, mLdapMagicSearchModel), &QObject::deleteLater); @@ -845,5 +843,6 @@ void CallCore::findRemoteLdapFriend(QSharedPointer me) { }); } }); - mLdapMagicSearchModel->search(mRemoteUsername); + mLdapMagicSearchModel->search(mRemoteAddress, (int)LinphoneEnums::MagicSearchSource::LdapServers, + LinphoneEnums::MagicSearchAggregation::Friend, -1); } diff --git a/Linphone/core/friend/FriendCore.cpp b/Linphone/core/friend/FriendCore.cpp index dd201f6d5..470142acb 100644 --- a/Linphone/core/friend/FriendCore.cpp +++ b/Linphone/core/friend/FriendCore.cpp @@ -44,14 +44,14 @@ QVariant createFriendDevice(const QString &name, const QString &address, Linphon return map; } -QSharedPointer FriendCore::create(const std::shared_ptr &contact) { - auto sharedPointer = QSharedPointer(new FriendCore(contact), &QObject::deleteLater); +QSharedPointer FriendCore::create(const std::shared_ptr &contact, bool isStored) { + auto sharedPointer = QSharedPointer(new FriendCore(contact, isStored), &QObject::deleteLater); sharedPointer->setSelf(sharedPointer); sharedPointer->moveToThread(App::getInstance()->thread()); return sharedPointer; } -FriendCore::FriendCore(const std::shared_ptr &contact) : QObject(nullptr) { +FriendCore::FriendCore(const std::shared_ptr &contact, bool isStored) : QObject(nullptr) { App::getInstance()->mEngine->setObjectOwnership(this, QQmlEngine::CppOwnership); if (contact) { mustBeInLinphoneThread(getClassName()); @@ -60,6 +60,7 @@ FriendCore::FriendCore(const std::shared_ptr &contact) : QObje mConsolidatedPresence = LinphoneEnums::fromLinphone(contact->getConsolidatedPresence()); mPresenceTimestamp = mFriendModel->getPresenceTimestamp(); mPictureUri = Utils::coreStringToAppString(contact->getPhoto()); + auto defaultAddress = contact->getAddress(); auto vcard = contact->getVcard(); if (vcard) { mOrganization = Utils::coreStringToAppString(vcard->getOrganization()); @@ -69,13 +70,16 @@ FriendCore::FriendCore(const std::shared_ptr &contact) : QObje mFullName = Utils::coreStringToAppString(vcard->getFullName()); mVCardString = Utils::coreStringToAppString(vcard->asVcard4String()); } + if (defaultAddress) { + if (mGivenName.isEmpty()) mGivenName = Utils::coreStringToAppString(defaultAddress->getUsername()); + } auto addresses = contact->getAddresses(); for (auto &address : addresses) { mAddressList.append( createFriendAddressVariant(_addressLabel, Utils::coreStringToAppString(address->asStringUriOnly()))); } mDefaultAddress = - contact->getAddress() ? Utils::coreStringToAppString(contact->getAddress()->asStringUriOnly()) : QString(); + defaultAddress ? Utils::coreStringToAppString(contact->getAddress()->asStringUriOnly()) : QString(); auto phoneNumbers = contact->getPhoneNumbersWithLabel(); for (auto &phoneNumber : phoneNumbers) { mPhoneNumberList.append( @@ -94,9 +98,11 @@ FriendCore::FriendCore(const std::shared_ptr &contact) : QObje mStarred = contact->getStarred(); mIsSaved = true; + mIsStored = isStored; } else { mIsSaved = false; mStarred = false; + mIsStored = false; } mIsLdap = ToolModel::friendIsInFriendList(ToolModel::getLdapFriendList(), contact); @@ -502,15 +508,28 @@ bool FriendCore::getIsSaved() const { void FriendCore::setIsSaved(bool data) { if (mIsSaved != data) { mIsSaved = data; + if (mIsSaved) setIsStored(true); emit isSavedChanged(mIsSaved); } } +bool FriendCore::getIsStored() const { + return mIsStored; +} +void FriendCore::setIsStored(bool data) { + if (mIsStored != data) { + mIsStored = data; + emit isStoredChanged(); + } +} + void FriendCore::writeIntoModel(std::shared_ptr model) const { mustBeInLinphoneThread(QString("[") + gClassName + "] " + Q_FUNC_INFO); model->getFriend()->edit(); // needed to create the vcard if not created yet - model->setName(mGivenName + (mFamilyName.isEmpty() || mGivenName.isEmpty() ? "" : " ") + mFamilyName); + model->setName(mFullName.isEmpty() + ? mGivenName + (mFamilyName.isEmpty() || mGivenName.isEmpty() ? "" : " ") + mFamilyName + : mFullName); auto core = CoreModel::getInstance()->getCore(); std::list> addresses; diff --git a/Linphone/core/friend/FriendCore.hpp b/Linphone/core/friend/FriendCore.hpp index 1630e1fbf..4c2a33e14 100644 --- a/Linphone/core/friend/FriendCore.hpp +++ b/Linphone/core/friend/FriendCore.hpp @@ -65,6 +65,7 @@ class FriendCore : public QObject, public AbstractObject { Q_PROPERTY(LinphoneEnums::ConsolidatedPresence consolidatedPresence READ getConsolidatedPresence NOTIFY consolidatedPresenceChanged) Q_PROPERTY(bool isSaved READ getIsSaved NOTIFY isSavedChanged) + Q_PROPERTY(bool isStored READ getIsStored NOTIFY isStoredChanged) Q_PROPERTY(QString pictureUri READ getPictureUri WRITE setPictureUri NOTIFY pictureUriChanged) Q_PROPERTY(bool starred READ getStarred WRITE lSetStarred NOTIFY starredChanged) Q_PROPERTY(bool readOnly READ getReadOnly CONSTANT) @@ -72,8 +73,8 @@ class FriendCore : public QObject, public AbstractObject { public: // Should be call from model Thread. Will be automatically in App thread after initialization - static QSharedPointer create(const std::shared_ptr &contact); - FriendCore(const std::shared_ptr &contact); + static QSharedPointer create(const std::shared_ptr &contact, bool isStored = true); + FriendCore(const std::shared_ptr &contact, bool isStored = true); FriendCore(const FriendCore &friendCore); ~FriendCore(); void setSelf(QSharedPointer me); @@ -81,7 +82,6 @@ public: void reset(const FriendCore &contact); QString getDisplayName() const; - void setDisplayName(const QString &name); QString getFamilyName() const; void setFamilyName(const QString &name); @@ -131,6 +131,9 @@ public: bool getIsSaved() const; void setIsSaved(bool isSaved); + bool getIsStored() const; // Exist in DB + void setIsStored(bool isStored); + QString getPictureUri() const; void setPictureUri(const QString &uri); void onPictureUriChanged(QString uri); @@ -163,6 +166,7 @@ signals: void pictureUriChanged(); void saved(); void isSavedChanged(bool isSaved); + void isStoredChanged(); void removed(FriendCore *contact); void defaultAddressChanged(); void allAddressesChanged(); @@ -189,6 +193,7 @@ protected: QString mDefaultAddress; QString mPictureUri; bool mIsSaved; + bool mIsStored; QString mVCardString; bool mIsLdap; std::shared_ptr mFriendModel; diff --git a/Linphone/core/friend/FriendGui.cpp b/Linphone/core/friend/FriendGui.cpp index b54d7485c..1efebb631 100644 --- a/Linphone/core/friend/FriendGui.cpp +++ b/Linphone/core/friend/FriendGui.cpp @@ -26,11 +26,13 @@ DEFINE_ABSTRACT_OBJECT(FriendGui) FriendGui::FriendGui(QObject *parent) : QObject(parent) { mustBeInMainThread(getClassName()); mCore = FriendCore::create(nullptr); + connect(mCore.get(), &FriendCore::isStoredChanged, this, &FriendGui::isStoredChanged); } FriendGui::FriendGui(QSharedPointer core) { App::getInstance()->mEngine->setObjectOwnership(this, QQmlEngine::JavaScriptOwnership); mCore = core; if (isInLinphoneThread()) moveToThread(App::getInstance()->thread()); + connect(mCore.get(), &FriendCore::isStoredChanged, this, &FriendGui::isStoredChanged); } FriendGui::~FriendGui() { @@ -43,3 +45,7 @@ void FriendGui::createContact(const QString &address) { FriendCore *FriendGui::getCore() const { return mCore.get(); } + +bool FriendGui::getIsStored() { + return mCore ? mCore->getIsStored() : false; +} diff --git a/Linphone/core/friend/FriendGui.hpp b/Linphone/core/friend/FriendGui.hpp index 6b2a182de..3470fac76 100644 --- a/Linphone/core/friend/FriendGui.hpp +++ b/Linphone/core/friend/FriendGui.hpp @@ -29,6 +29,7 @@ class FriendGui : public QObject, public AbstractObject { Q_OBJECT Q_PROPERTY(FriendCore *core READ getCore CONSTANT) + Q_PROPERTY(bool isStored READ getIsStored NOTIFY isStoredChanged) public: FriendGui(QObject *parent = nullptr); @@ -37,6 +38,12 @@ public: FriendCore *getCore() const; Q_INVOKABLE void createContact(const QString &address); QSharedPointer mCore; + + bool getIsStored(); +signals: + void isStoredChanged(); + +public: DECLARE_ABSTRACT_OBJECT }; diff --git a/Linphone/core/proxy/SortFilterProxy.cpp b/Linphone/core/proxy/SortFilterProxy.cpp index c063e5212..cfeff8291 100644 --- a/Linphone/core/proxy/SortFilterProxy.cpp +++ b/Linphone/core/proxy/SortFilterProxy.cpp @@ -88,3 +88,7 @@ void SortFilterProxy::setSortOrder(const Qt::SortOrder &order) { void SortFilterProxy::remove(int index, int count) { QSortFilterProxyModel::removeRows(index, count); } + +void SortFilterProxy::invalidateFilter() { + QSortFilterProxyModel::invalidateFilter(); +} diff --git a/Linphone/core/proxy/SortFilterProxy.hpp b/Linphone/core/proxy/SortFilterProxy.hpp index 16a3d3369..8caeeada5 100644 --- a/Linphone/core/proxy/SortFilterProxy.hpp +++ b/Linphone/core/proxy/SortFilterProxy.hpp @@ -69,6 +69,7 @@ public: void setFilterText(const QString &filter); Q_INVOKABLE void remove(int index, int count = 1); + void invalidateFilter(); signals: void countChanged(); diff --git a/Linphone/core/search/MagicSearchList.cpp b/Linphone/core/search/MagicSearchList.cpp index 60716791d..4cf57207e 100644 --- a/Linphone/core/search/MagicSearchList.cpp +++ b/Linphone/core/search/MagicSearchList.cpp @@ -21,7 +21,6 @@ #include "MagicSearchList.hpp" #include "core/App.hpp" #include "core/friend/FriendCore.hpp" -#include "core/friend/FriendGui.hpp" #include "tool/Utils.hpp" #include #include @@ -62,7 +61,7 @@ void MagicSearchList::setSelf(QSharedPointer me) { if (haveContact == mList.end()) { connect(friendCore.get(), &FriendCore::removed, this, qOverload(&MagicSearchList::remove)); add(friendCore); - emit friendCreated(getCount() - 1); + emit friendCreated(getCount() - 1, new FriendGui(friendCore)); } }); mCoreModelConnection->invokeToModel([this] { @@ -71,34 +70,17 @@ void MagicSearchList::setSelf(QSharedPointer me) { auto magicSearch = Utils::makeQObject_ptr(linphoneSearch); mCoreModelConnection->invokeToCore([this, magicSearch] { mMagicSearch = magicSearch; - mMagicSearch->mSourceFlags = mSourceFlags; - mMagicSearch->mAggregationFlag = mAggregationFlag; mMagicSearch->setSelf(mMagicSearch); mModelConnection = QSharedPointer>( new SafeConnection(mCoreModelConnection->mCore.mQData, mMagicSearch), &QObject::deleteLater); - mModelConnection->makeConnectToCore(&MagicSearchList::lSearch, [this](QString filter) { - mModelConnection->invokeToModel([this, filter]() { mMagicSearch->search(filter); }); - }); - mModelConnection->makeConnectToCore(&MagicSearchList::lSetSourceFlags, [this](int flags) { - mModelConnection->invokeToModel([this, flags]() { mMagicSearch->setSourceFlags(flags); }); - }); - mModelConnection->makeConnectToCore(&MagicSearchList::lSetAggregationFlag, - [this](LinphoneEnums::MagicSearchAggregation aggregation) { - mModelConnection->invokeToModel([this, aggregation]() { - mMagicSearch->setAggregationFlag(aggregation); - }); - }); mModelConnection->makeConnectToCore( - &MagicSearchList::lSetAggregationFlag, [this](LinphoneEnums::MagicSearchAggregation flag) { - mModelConnection->invokeToModel([this, flag]() { mMagicSearch->setAggregationFlag(flag); }); - }); - mModelConnection->makeConnectToModel(&MagicSearchModel::sourceFlagsChanged, [this](int flags) { - mModelConnection->invokeToCore([this, flags]() { setSourceFlags(flags); }); - }); - mModelConnection->makeConnectToModel( - &MagicSearchModel::aggregationFlagChanged, [this](LinphoneEnums::MagicSearchAggregation flag) { - mModelConnection->invokeToCore([this, flag]() { setAggregationFlag(flag); }); + &MagicSearchList::lSearch, + [this](QString filter, int sourceFlags, LinphoneEnums::MagicSearchAggregation aggregationFlag, + int maxResults) { + mModelConnection->invokeToModel([this, filter, sourceFlags, aggregationFlag, maxResults]() { + mMagicSearch->search(filter, sourceFlags, aggregationFlag, maxResults); + }); }); mModelConnection->makeConnectToModel( &MagicSearchModel::searchResultsReceived, @@ -106,26 +88,30 @@ void MagicSearchList::setSelf(QSharedPointer me) { auto *contacts = new QList>(); for (auto it : results) { QSharedPointer contact; - if (it->getFriend()) { - contact = FriendCore::create(it->getFriend()); + auto linphoneFriend = it->getFriend(); + // Considered LDAP results as stored. + bool isStored = (it->getSourceFlags() & (int)linphone::MagicSearch::Source::LdapServers) > 0; + if (linphoneFriend) { + contact = FriendCore::create(linphoneFriend); contacts->append(contact); } else if (auto address = it->getAddress()) { auto linphoneFriend = CoreModel::getInstance()->getCore()->createFriend(); - contact = FriendCore::create(linphoneFriend); - auto displayname = Utils::coreStringToAppString(address->getDisplayName()); - auto splitted = displayname.split(" "); - if (splitted.size() > 0) { + linphoneFriend->setAddress(address); + contact = FriendCore::create(linphoneFriend, isStored); + auto displayName = Utils::coreStringToAppString(address->getDisplayName()); + auto splitted = displayName.split(" "); + if (!displayName.isEmpty() && splitted.size() > 0) { contact->setGivenName(splitted[0]); splitted.removeFirst(); contact->setFamilyName(splitted.join(" ")); } else { contact->setGivenName(Utils::coreStringToAppString(address->getUsername())); } - contact->appendAddress(Utils::coreStringToAppString(address->asStringUriOnly())); contacts->append(contact); } else if (!it->getPhoneNumber().empty()) { - auto linphoneFriend = CoreModel::getInstance()->getCore()->createFriend(); - contact = FriendCore::create(linphoneFriend); + linphoneFriend = CoreModel::getInstance()->getCore()->createFriend(); + linphoneFriend->setAddress(address); + contact = FriendCore::create(linphoneFriend, isStored); contact->setGivenName(Utils::coreStringToAppString(it->getPhoneNumber())); contact->appendPhoneNumber(tr("Phone"), Utils::coreStringToAppString(it->getPhoneNumber())); contacts->append(contact); @@ -148,10 +134,11 @@ void MagicSearchList::setResults(const QList> &contac if (!isFriendCore) continue; disconnect(isFriendCore.get()); } + qDebug() << log().arg("SetResults: %1").arg(contacts.size()); resetData(contacts); for (auto it : contacts) { connect(it.get(), &FriendCore::removed, this, qOverload(&MagicSearchList::remove)); - connect(it.get(), &FriendCore::starredChanged, this, [this] { lSearch(mSearchFilter); }); + connect(it.get(), &FriendCore::starredChanged, this, &MagicSearchList::friendStarredChanged); } } @@ -161,7 +148,7 @@ void MagicSearchList::addResult(const QSharedPointer &contact) { void MagicSearchList::setSearch(const QString &search) { mSearchFilter = search; if (!search.isEmpty()) { - lSearch(search); + emit lSearch(search, mSourceFlags, mAggregationFlag, mMaxResults); } else { beginResetModel(); mList.clear(); @@ -180,6 +167,17 @@ void MagicSearchList::setSourceFlags(int flags) { } } +int MagicSearchList::getMaxResults() const { + return mMaxResults; +} + +void MagicSearchList::setMaxResults(int maxResults) { + if (mMaxResults != maxResults) { + mMaxResults = maxResults; + emit maxResultsChanged(mMaxResults); + } +} + LinphoneEnums::MagicSearchAggregation MagicSearchList::getAggregationFlag() const { return mAggregationFlag; } @@ -191,11 +189,20 @@ void MagicSearchList::setAggregationFlag(LinphoneEnums::MagicSearchAggregation f } } +QHash MagicSearchList::roleNames() const { + QHash roles; + roles[Qt::DisplayRole] = "$modelData"; + roles[Qt::DisplayRole + 1] = "isStored"; + return roles; +} + QVariant MagicSearchList::data(const QModelIndex &index, int role) const { int row = index.row(); if (!index.isValid() || row < 0 || row >= mList.count()) return QVariant(); if (role == Qt::DisplayRole) { return QVariant::fromValue(new FriendGui(mList[row].objectCast())); + } else if (role == Qt::DisplayRole + 1) { + return mList[row].objectCast()->getIsStored(); } return QVariant(); } diff --git a/Linphone/core/search/MagicSearchList.hpp b/Linphone/core/search/MagicSearchList.hpp index 783a39174..f3b820333 100644 --- a/Linphone/core/search/MagicSearchList.hpp +++ b/Linphone/core/search/MagicSearchList.hpp @@ -22,6 +22,7 @@ #define MAGIC_SEARCH_LIST_H_ #include "../proxy/ListProxy.hpp" +#include "core/friend/FriendGui.hpp" #include "model/search/MagicSearchModel.hpp" #include "tool/AbstractObject.hpp" #include "tool/thread/SafeConnection.hpp" @@ -50,19 +51,24 @@ public: LinphoneEnums::MagicSearchAggregation getAggregationFlag() const; void setAggregationFlag(LinphoneEnums::MagicSearchAggregation flag); + int getMaxResults() const; + void setMaxResults(int maxResults); + + virtual QHash roleNames() const override; virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; int findFriendIndexByAddress(const QString &address); signals: - void lSearch(QString filter); - void lSetSourceFlags(int sourceFlags); - void lSetAggregationFlag(LinphoneEnums::MagicSearchAggregation aggregationFlag); + void + lSearch(QString filter, int sourceFlags, LinphoneEnums::MagicSearchAggregation aggregationFlag, int maxResults); void sourceFlagsChanged(int sourceFlags); void aggregationFlagChanged(LinphoneEnums::MagicSearchAggregation flag); + void maxResultsChanged(int maxResults); - void friendCreated(int index); + void friendCreated(int index, FriendGui *data); + void friendStarredChanged(); void initialized(); @@ -70,6 +76,7 @@ private: int mSourceFlags; LinphoneEnums::MagicSearchAggregation mAggregationFlag; QString mSearchFilter; + int mMaxResults = -1; std::shared_ptr mMagicSearch; QSharedPointer> mModelConnection; diff --git a/Linphone/core/search/MagicSearchProxy.cpp b/Linphone/core/search/MagicSearchProxy.cpp index 254eb25de..376c402e2 100644 --- a/Linphone/core/search/MagicSearchProxy.cpp +++ b/Linphone/core/search/MagicSearchProxy.cpp @@ -20,16 +20,14 @@ #include "MagicSearchProxy.hpp" #include "MagicSearchList.hpp" + #include "core/App.hpp" -#include "core/friend/FriendGui.hpp" +#include "core/friend/FriendCore.hpp" MagicSearchProxy::MagicSearchProxy(QObject *parent) : LimitProxy(parent) { - mSourceFlags = (int)LinphoneEnums::MagicSearchSource::Friends | (int)LinphoneEnums::MagicSearchSource::LdapServers; - mAggregationFlag = LinphoneEnums::MagicSearchAggregation::Friend; setList(MagicSearchList::create()); - sort(0); connect(this, &MagicSearchProxy::forceUpdate, [this] { - if (mList) emit mList->lSearch(mSearchText); + if (mList) emit mList->lSearch(mSearchText, getSourceFlags(), getAggregationFlag(), getMaxResults()); }); connect(App::getInstance(), &App::currentDateChanged, this, &MagicSearchProxy::forceUpdate); } @@ -50,28 +48,28 @@ void MagicSearchProxy::setList(QSharedPointer newList) { Qt::QueuedConnection); connect(mList.get(), &MagicSearchList::aggregationFlagChanged, this, &MagicSearchProxy::aggregationFlagChanged, Qt::QueuedConnection); + connect( mList.get(), &MagicSearchList::friendCreated, this, - [this](int index) { - auto proxyIndex = - dynamic_cast(sourceModel())->mapFromSource(mList->index(index, 0)).row(); - // auto proxyIndex = mapFromSource(sourceModel()->index(index, 0)); // OLD (keep for checking new proxy - // behavior) - emit friendCreated(proxyIndex); + [this](int index, FriendGui *data) { + auto sortModel = dynamic_cast(sourceModel()); + sortModel->invalidate(); + if (!data->mCore->isLdap()) { + auto proxyIndex = sortModel->mapFromSource(mList->index(index, 0)).row(); + // auto proxyIndex = mapFromSource(sourceModel()->index(index, 0)); // OLD (keep for checking new + // proxy behavior) + emit localFriendCreated(proxyIndex); + } }, Qt::QueuedConnection); connect( - mList.get(), &MagicSearchList::initialized, this, - [this, newList = mList.get()] { - emit newList->lSetSourceFlags(mSourceFlags); - emit newList->lSetAggregationFlag(mAggregationFlag); - emit initialized(); - }, + mList.get(), &MagicSearchList::initialized, this, [this, newList = mList.get()] { emit initialized(); }, Qt::QueuedConnection); } auto sortFilterList = new SortFilterList(mList.get(), Qt::AscendingOrder); if (oldModel) { sortFilterList->mShowFavoritesOnly = oldModel->mShowFavoritesOnly; + sortFilterList->mHideSuggestions = oldModel->mHideSuggestions; sortFilterList->mShowLdapContacts = oldModel->mShowLdapContacts; sortFilterList->mHideListProxy = oldModel->mHideListProxy; if (sortFilterList->mHideListProxy) { @@ -81,7 +79,14 @@ void MagicSearchProxy::setList(QSharedPointer newList) { [this, sortFilterList]() { sortFilterList->invalidate(); }); } } + connect( + mList.get(), &MagicSearchList::friendStarredChanged, this, + [this, sortFilterList]() { + if (showFavoritesOnly()) sortFilterList->invalidate(); + }, + Qt::QueuedConnection); setSourceModels(sortFilterList); + sort(0); } int MagicSearchProxy::findFriendIndexByAddress(const QString &address) { @@ -105,14 +110,19 @@ void MagicSearchProxy::setSearchText(const QString &search) { } int MagicSearchProxy::getSourceFlags() const { - return mSourceFlags; + return mList->getSourceFlags(); } void MagicSearchProxy::setSourceFlags(int flags) { - if (flags != mSourceFlags) { - mSourceFlags = flags; - emit mList->lSetSourceFlags(flags); - } + mList->setSourceFlags(flags); +} + +int MagicSearchProxy::getMaxResults() const { + return mList->getMaxResults(); +} + +void MagicSearchProxy::setMaxResults(int flags) { + mList->setMaxResults(flags); } bool MagicSearchProxy::showFavoritesOnly() const { @@ -128,9 +138,28 @@ void MagicSearchProxy::setShowFavoritesOnly(bool show) { } } -void MagicSearchProxy::setParentProxy(MagicSearchProxy *proxy) { - setList(proxy->mList); - emit parentProxyChanged(); +bool MagicSearchProxy::getHideSuggestions() const { + return dynamic_cast(sourceModel())->mHideSuggestions; +} +void MagicSearchProxy::setHideSuggestions(bool data) { + auto list = dynamic_cast(sourceModel()); + if (list->mHideSuggestions != data) { + list->mHideSuggestions = data; + list->invalidate(); + emit hideSuggestionsChanged(); + } +} + +MagicSearchProxy *MagicSearchProxy::getParentProxy() const { + return mParentProxy; +} + +void MagicSearchProxy::setParentProxy(MagicSearchProxy *parentProxy) { + if (parentProxy && parentProxy->mList) { + mParentProxy = parentProxy; + setList(parentProxy->mList); + emit parentProxyChanged(); + } } MagicSearchProxy *MagicSearchProxy::getHideListProxy() const { @@ -138,36 +167,34 @@ MagicSearchProxy *MagicSearchProxy::getHideListProxy() const { return list ? list->mHideListProxy : nullptr; } -void MagicSearchProxy::setHideListProxy(MagicSearchProxy *proxy) { +void MagicSearchProxy::setHideListProxy(MagicSearchProxy *hideListProxy) { auto list = dynamic_cast(sourceModel()); - if (list && list->mHideListProxy != proxy) { + if (list && list->mHideListProxy != hideListProxy) { if (list->mHideListProxy) list->disconnect(list->mHideListProxy); - list->mHideListProxy = proxy; + list->mHideListProxy = hideListProxy; list->invalidate(); - if (proxy) { - connect(proxy, &MagicSearchProxy::countChanged, list, [this, list]() { list->invalidate(); }); - connect(proxy, &MagicSearchProxy::modelReset, list, [this, list]() { list->invalidate(); }); + if (hideListProxy) { + connect(hideListProxy, &MagicSearchProxy::countChanged, list, [this, list]() { list->invalidateFilter(); }); + connect(hideListProxy, &MagicSearchProxy::modelReset, list, [this, list]() { list->invalidateFilter(); }); } emit hideListProxyChanged(); } } LinphoneEnums::MagicSearchAggregation MagicSearchProxy::getAggregationFlag() const { - return mAggregationFlag; + return mList->getAggregationFlag(); } void MagicSearchProxy::setAggregationFlag(LinphoneEnums::MagicSearchAggregation flag) { - if (flag != mAggregationFlag) { - mAggregationFlag = flag; - emit mList->lSetAggregationFlag(flag); - } + mList->setAggregationFlag(flag); } bool MagicSearchProxy::SortFilterList::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { auto friendCore = getItemAtSource(sourceRow); auto toShow = false; if (friendCore) { - toShow = (!mShowFavoritesOnly || friendCore->getStarred()) && (mShowLdapContacts || !friendCore->isLdap()); + toShow = (!mHideSuggestions || friendCore->getIsStored()) && + (!mShowFavoritesOnly || friendCore->getStarred()) && (mShowLdapContacts || !friendCore->isLdap()); if (toShow && mHideListProxy) { for (auto &friendAddress : friendCore->getAllAddresses()) { toShow = mHideListProxy->findFriendIndexByAddress(friendAddress.toMap()["address"].toString()) == -1; @@ -184,6 +211,10 @@ bool MagicSearchProxy::SortFilterList::lessThan(const QModelIndex &sourceLeft, c auto r = getItemAtSource(sourceRight.row()); if (l && r) { + bool lIsStored = l->getIsStored(); + bool rIsStored = r->getIsStored(); + if (lIsStored && !rIsStored) return true; + else if (!lIsStored && rIsStored) return false; auto lName = l->getDisplayName().toLower(); auto rName = r->getDisplayName().toLower(); return lName < rName; diff --git a/Linphone/core/search/MagicSearchProxy.hpp b/Linphone/core/search/MagicSearchProxy.hpp index 4ffecd00b..90a76db19 100644 --- a/Linphone/core/search/MagicSearchProxy.hpp +++ b/Linphone/core/search/MagicSearchProxy.hpp @@ -22,6 +22,7 @@ #define MAGIC_SEARCH_PROXY_H_ #include "../proxy/LimitProxy.hpp" +#include "core/friend/FriendGui.hpp" #include "core/search/MagicSearchList.hpp" #include "tool/LinphoneEnums.hpp" @@ -32,18 +33,21 @@ class MagicSearchProxy : public LimitProxy { Q_PROPERTY(QString searchText READ getSearchText WRITE setSearchText NOTIFY searchTextChanged) Q_PROPERTY(int sourceFlags READ getSourceFlags WRITE setSourceFlags NOTIFY sourceFlagsChanged) + + Q_PROPERTY(int maxResults READ getMaxResults WRITE setMaxResults NOTIFY maxResultsChanged) Q_PROPERTY(LinphoneEnums::MagicSearchAggregation aggregationFlag READ getAggregationFlag WRITE setAggregationFlag NOTIFY aggregationFlagChanged) Q_PROPERTY(bool showFavoritesOnly READ showFavoritesOnly WRITE setShowFavoritesOnly NOTIFY showFavoriteOnlyChanged) - Q_PROPERTY(MagicSearchProxy *parentProxy WRITE setParentProxy NOTIFY parentProxyChanged) + Q_PROPERTY(MagicSearchProxy *parentProxy READ getParentProxy WRITE setParentProxy NOTIFY parentProxyChanged) Q_PROPERTY(MagicSearchProxy *hideListProxy READ getHideListProxy WRITE setHideListProxy NOTIFY hideListProxyChanged) Q_PROPERTY(bool showLdapContacts READ showLdapContacts WRITE setShowLdapContacts CONSTANT) + Q_PROPERTY(bool hideSuggestions READ getHideSuggestions WRITE setHideSuggestions NOTIFY hideSuggestionsChanged) public: - DECLARE_SORTFILTER_CLASS(bool mShowFavoritesOnly = false; bool mShowLdapContacts = false; + DECLARE_SORTFILTER_CLASS(bool mShowFavoritesOnly = false; bool mShowLdapContacts = true; + bool mHideSuggestions = false; MagicSearchProxy *mHideListProxy = nullptr;) - MagicSearchProxy(QObject *parent = Q_NULLPTR); ~MagicSearchProxy(); @@ -56,12 +60,19 @@ public: LinphoneEnums::MagicSearchAggregation getAggregationFlag() const; void setAggregationFlag(LinphoneEnums::MagicSearchAggregation flag); + int getMaxResults() const; + void setMaxResults(int maxResults); + bool showFavoritesOnly() const; void setShowFavoritesOnly(bool show); bool showLdapContacts() const; void setShowLdapContacts(bool show); + bool getHideSuggestions() const; + void setHideSuggestions(bool data); + + MagicSearchProxy *getParentProxy() const; void setList(QSharedPointer list); Q_INVOKABLE void setParentProxy(MagicSearchProxy *proxy); @@ -75,17 +86,18 @@ signals: void searchTextChanged(); void sourceFlagsChanged(int sourceFlags); void aggregationFlagChanged(LinphoneEnums::MagicSearchAggregation aggregationFlag); + void maxResultsChanged(int maxResults); void forceUpdate(); - void friendCreated(int index); + void localFriendCreated(int index); void showFavoriteOnlyChanged(); + void hideSuggestionsChanged(); void parentProxyChanged(); void hideListProxyChanged(); void initialized(); protected: + MagicSearchProxy *mParentProxy = nullptr; QString mSearchText; - int mSourceFlags; - LinphoneEnums::MagicSearchAggregation mAggregationFlag; QSharedPointer mList; }; diff --git a/Linphone/model/search/MagicSearchModel.cpp b/Linphone/model/search/MagicSearchModel.cpp index 13fbcf646..0d46a3d09 100644 --- a/Linphone/model/search/MagicSearchModel.cpp +++ b/Linphone/model/search/MagicSearchModel.cpp @@ -23,6 +23,7 @@ #include #include "model/core/CoreModel.hpp" +#include "model/setting/SettingsModel.hpp" #include "model/tool/ToolModel.hpp" #include "tool/Utils.hpp" @@ -37,35 +38,43 @@ MagicSearchModel::~MagicSearchModel() { mustBeInLinphoneThread("~" + getClassName()); } -void MagicSearchModel::search(QString filter) { - mustBeInLinphoneThread(log().arg(Q_FUNC_INFO)); +void MagicSearchModel::search(QString filter, + int sourceFlags, + LinphoneEnums::MagicSearchAggregation aggregation, + int maxResults) { mLastSearch = filter; - mMonitor->getContactsListAsync(filter != "*" ? Utils::appStringToCoreString(filter) : "", "", mSourceFlags, - LinphoneEnums::toLinphone(mAggregationFlag)); -} - -void MagicSearchModel::setSourceFlags(int flags) { - mustBeInLinphoneThread(log().arg(Q_FUNC_INFO)); - if (mSourceFlags != flags) { - mSourceFlags = flags; - emit sourceFlagsChanged(mSourceFlags); + setMaxResults(maxResults); + if ((filter == "" || filter == "*") && ((sourceFlags & (int)LinphoneEnums::MagicSearchSource::LdapServers) > 0) && + !SettingsModel::getInstance()->getSyncLdapContacts()) { + sourceFlags &= ~(int)LinphoneEnums::MagicSearchSource::LdapServers; } + qInfo() << log().arg("Searching ") << filter << " from " << sourceFlags << " with limit " << maxResults; + mMonitor->getContactsListAsync(filter != "*" ? Utils::appStringToCoreString(filter) : "", "", sourceFlags, + LinphoneEnums::toLinphone(aggregation)); } -void MagicSearchModel::setAggregationFlag(LinphoneEnums::MagicSearchAggregation flag) { - mustBeInLinphoneThread(log().arg(Q_FUNC_INFO)); - if (mAggregationFlag != flag) { - mAggregationFlag = flag; - emit aggregationFlagChanged(mAggregationFlag); +int MagicSearchModel::getMaxResults() const { + if (!mMonitor->getLimitedSearch()) return -1; + else return mMonitor->getSearchLimit(); +} + +void MagicSearchModel::setMaxResults(int maxResults) { + if (maxResults <= 0 && mMonitor->getLimitedSearch() || + maxResults > 0 && (!mMonitor->getLimitedSearch() || maxResults != mMonitor->getSearchLimit())) { + mMonitor->setLimitedSearch(maxResults > 0); + if (maxResults > 0) mMonitor->setSearchLimit(maxResults); + emit maxResultsChanged(maxResults); } } void MagicSearchModel::onSearchResultsReceived(const std::shared_ptr &magicSearch) { - for (auto it : magicSearch->getLastSearch()) { + qDebug() << log().arg("SDK send callback: onSearchResultsReceived"); + auto results = magicSearch->getLastSearch(); + for (auto it : results) { bool isLdap = (it->getSourceFlags() & (int)LinphoneEnums::MagicSearchSource::LdapServers) != 0; if (isLdap && it->getFriend()) updateLdapFriendListWithFriend(it->getFriend()); } - emit searchResultsReceived(magicSearch->getLastSearch()); + emit searchResultsReceived(results); } void MagicSearchModel::onLdapHaveMoreResults(const std::shared_ptr &magicSearch, diff --git a/Linphone/model/search/MagicSearchModel.hpp b/Linphone/model/search/MagicSearchModel.hpp index 3ab53f82c..c0878fbba 100644 --- a/Linphone/model/search/MagicSearchModel.hpp +++ b/Linphone/model/search/MagicSearchModel.hpp @@ -37,17 +37,14 @@ public: MagicSearchModel(const std::shared_ptr &data, QObject *parent = nullptr); ~MagicSearchModel(); - void search(QString filter); - void setSourceFlags(int flags); - void setAggregationFlag(LinphoneEnums::MagicSearchAggregation flag); + void search(QString filter, int sourceFlags, LinphoneEnums::MagicSearchAggregation aggregation, int maxResults); - int mSourceFlags = (int)linphone::MagicSearch::Source::All; - LinphoneEnums::MagicSearchAggregation mAggregationFlag = LinphoneEnums::MagicSearchAggregation::None; + int getMaxResults() const; + void setMaxResults(int maxResults); QString mLastSearch; signals: - void sourceFlagsChanged(int sourceFlags); - void aggregationFlagChanged(LinphoneEnums::MagicSearchAggregation aggregationFlag); + void maxResultsChanged(int maxResults); private: DECLARE_ABSTRACT_OBJECT diff --git a/Linphone/view/CMakeLists.txt b/Linphone/view/CMakeLists.txt index b0686a11a..d7c3498a4 100644 --- a/Linphone/view/CMakeLists.txt +++ b/Linphone/view/CMakeLists.txt @@ -47,6 +47,7 @@ list(APPEND _LINPHONEAPP_QML_FILES view/Control/Display/Call/CallStatistics.qml view/Control/Display/Contact/Avatar.qml view/Control/Display/Contact/Contact.qml + view/Control/Display/Contact/ContactListItem.qml view/Control/Display/Contact/ContactListView.qml view/Control/Display/Contact/Voicemail.qml view/Control/Display/Meeting/MeetingListView.qml diff --git a/Linphone/view/Control/Display/Contact/ContactListItem.qml b/Linphone/view/Control/Display/Contact/ContactListItem.qml new file mode 100644 index 000000000..cb34c84a2 --- /dev/null +++ b/Linphone/view/Control/Display/Contact/ContactListItem.qml @@ -0,0 +1,264 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls.Basic as Control + +import Linphone +import UtilsCpp 1.0 +import ConstantsCpp 1.0 +import SettingsCpp +FocusScope { + id: mainItem + implicitHeight: 56 * DefaultStyle.dp + property var searchResultItem + property bool showInitials: true // Display Initials of Display name. + property bool showDefaultAddress: true // Display address below display name. + property bool showActions: false // Display actions layout (call buttons) + property bool showContactMenu: true // Display the dot menu for contacts. + property string highlightText // Bold characters in Display name. + + property bool displayNameCapitalization: true // Capitalize display name. + + property bool selectionEnabled: true // Contact can be selected + property bool multiSelectionEnabled: false //Multiple items can be selected. + property list selectedContacts // List of default address on selected contacts. + property int selectedContactCount: selectedContacts.length + property bool isSelected: false // selected in list => currentIndex == index + + + property var previousInitial // Use directly previous initial + property int itemsRightMargin: 39 * DefaultStyle.dp + + property var displayName: searchResultItem.core.displayName + property string initial: displayName ? displayName[0].toLocaleLowerCase(ConstantsCpp.DefaultLocale) : '' + + signal clicked(var mouse) + signal contactStarredChanged() + signal contactDeletionRequested(FriendGui contact) + + Connections { + enabled: searchResultItem.core + target: searchResultItem.core + function onStarredChanged() { mainItem.contactStarredChanged()} + } + + Text { + id: initial + anchors.left: parent.left + visible: mainItem.showInitials + anchors.verticalCenter: parent.verticalCenter + anchors.rightMargin: 15 * DefaultStyle.dp + verticalAlignment: Text.AlignVCenter + width: 20 * DefaultStyle.dp + opacity: previousInitial != mainItem.initial ? 1 : 0 + text: mainItem.initial + color: DefaultStyle.main2_400 + font { + pixelSize: 20 * DefaultStyle.dp + weight: 500 * DefaultStyle.dp + capitalization: Font.AllUppercase + } + } + RowLayout { + id: contactDelegate + anchors.left: initial.visible ? initial.right : parent.left + anchors.right: parent.right + anchors.rightMargin: mainItem.itemsRightMargin + anchors.verticalCenter: parent.verticalCenter + spacing: 16 * DefaultStyle.dp + z: 1 + Avatar { + Layout.preferredWidth: 45 * DefaultStyle.dp + Layout.preferredHeight: 45 * DefaultStyle.dp + Layout.leftMargin: 5 * DefaultStyle.dp + contact: searchResultItem + } + ColumnLayout { + spacing: 0 + Text { + text: UtilsCpp.boldTextPart(mainItem.displayName, mainItem.highlightText) + font{ + pixelSize: mainItem.showDefaultAddress ? 16 * DefaultStyle.dp : 14 * DefaultStyle.dp + capitalization: mainItem.displayNameCapitalization ? Font.Capitalize : Font.MixedCase + weight: mainItem.showDefaultAddress ? 800 * DefaultStyle.dp : 400 * DefaultStyle.dp + } + maximumLineCount: 1 + Layout.fillWidth: true + } + Text { + Layout.topMargin: 2 * DefaultStyle.dp + Layout.fillWidth: true + visible: mainItem.showDefaultAddress + text: SettingsCpp.onlyDisplaySipUriUsername ? UtilsCpp.getUsername(searchResultItem.core.defaultAddress) : searchResultItem.core.defaultAddress + maximumLineCount: 1 + elide: Text.ElideRight + font { + weight: 300 * DefaultStyle.dp + pixelSize: 12 * DefaultStyle.dp + } + } + } + Item{Layout.fillWidth: true} + RowLayout { + id: actionsRow + z: 1 + visible: actionButtons || friendPopup.visible || mainItem.multiSelectionEnabled + spacing: visible ? 16 * DefaultStyle.dp : 0 + EffectImage { + id: isSelectedCheck + // visible: mainItem.multiSelectionEnabled && (mainItem.confInfoGui.core.getParticipantIndex(searchResultItem.core.defaultAddress) != -1) + visible: mainItem.multiSelectionEnabled && (mainItem.selectedContacts.indexOf(searchResultItem.core.defaultAddress) != -1) + Layout.preferredWidth: 24 * DefaultStyle.dp + Layout.preferredHeight: 24 * DefaultStyle.dp + imageSource: AppIcons.check + colorizationColor: DefaultStyle.main1_500_main + Connections { + target: mainItem + // onParticipantsChanged: isSelectedCheck.visible = mainItem.confInfoGui.core.getParticipantIndex(searchResultItem.core.defaultAddress) != -1 + function onSelectedContactCountChanged(){ + isSelectedCheck.visible = (mainItem.selectedContacts.indexOf(searchResultItem.core.defaultAddress) != -1) + } + } + } + RowLayout{ + id: actionButtons + Layout.rightMargin: 10 * DefaultStyle.dp + visible: mainItem.showActions + spacing: visible ? 10 * DefaultStyle.dp : 0 + Button { + id: callButton + Layout.preferredWidth: 45 * DefaultStyle.dp + Layout.preferredHeight: 45 * DefaultStyle.dp + icon.width: 24 * DefaultStyle.dp + icon.height: 24 * DefaultStyle.dp + icon.source: AppIcons.phone + focus: visible + contentImageColor: DefaultStyle.main2_500main + background: Rectangle { + anchors.fill: parent + radius: 40 * DefaultStyle.dp + color: DefaultStyle.main2_200 + } + onClicked: UtilsCpp.createCall(searchResultItem.core.defaultAddress) + KeyNavigation.right: chatButton + KeyNavigation.left: chatButton + } + Button { + id: chatButton + visible: actionButtons.visible && !SettingsCpp.disableChatFeature + Layout.preferredWidth: 45 * DefaultStyle.dp + Layout.preferredHeight: 45 * DefaultStyle.dp + icon.width: 24 * DefaultStyle.dp + icon.height: 24 * DefaultStyle.dp + icon.source: AppIcons.chatTeardropText + focus: visible && !callButton.visible + contentImageColor: DefaultStyle.main2_500main + background: Rectangle { + anchors.fill: parent + radius: 40 * DefaultStyle.dp + color: DefaultStyle.main2_200 + } + KeyNavigation.right: callButton + KeyNavigation.left: callButton + } + } + PopupButton { + id: friendPopup + z: 1 + // Layout.rightMargin: 13 * DefaultStyle.dp + Layout.alignment: Qt.AlignVCenter + Layout.rightMargin: 8 * DefaultStyle.dp + popup.x: 0 + popup.padding: 10 * DefaultStyle.dp + visible: mainItem.showContactMenu && (contactArea.containsMouse || hovered || popup.opened) + + popup.contentItem: ColumnLayout { + Button { + visible: searchResultItem.core.isStored + text: searchResultItem.core.starred ? qsTr("Enlever des favoris") : qsTr("Mettre en favori") + icon.source: searchResultItem.core.starred ? AppIcons.heartFill : AppIcons.heart + icon.width: 24 * DefaultStyle.dp + icon.height: 24 * DefaultStyle.dp + spacing: 10 * DefaultStyle.dp + textSize: 14 * DefaultStyle.dp + textWeight: 400 * DefaultStyle.dp + textColor: DefaultStyle.main2_500main + contentImageColor: searchResultItem.core.starred ? DefaultStyle.danger_500main : DefaultStyle.main2_600 + onClicked: { + searchResultItem.core.lSetStarred(!searchResultItem.core.starred) + friendPopup.close() + } + background: Item{} + } + Button { + text: qsTr("Partager") + icon.source: AppIcons.shareNetwork + icon.width: 24 * DefaultStyle.dp + icon.height: 24 * DefaultStyle.dp + spacing: 10 * DefaultStyle.dp + textSize: 14 * DefaultStyle.dp + textWeight: 400 * DefaultStyle.dp + textColor: DefaultStyle.main2_500main + onClicked: { + var vcard = searchResultItem.core.getVCard() + var username = searchResultItem.core.givenName + searchResultItem.core.familyName + var filepath = UtilsCpp.createVCardFile(username, vcard) + if (filepath == "") UtilsCpp.showInformationPopup(qsTr("Erreur"), qsTr("La création du fichier vcard a échoué"), false) + else mainWindow.showInformationPopup(qsTr("VCard créée"), qsTr("VCard du contact enregistrée dans %1").arg(filepath)) + UtilsCpp.shareByEmail(qsTr("Partage de contact"), vcard, filepath) + } + background: Item{} + } + Button { + text: qsTr("Supprimer") + icon.source: AppIcons.trashCan + icon.width: 24 * DefaultStyle.dp + icon.height: 24 * DefaultStyle.dp + spacing: 10 * DefaultStyle.dp + textSize: 14 * DefaultStyle.dp + textWeight: 400 * DefaultStyle.dp + textColor: DefaultStyle.danger_500main + contentImageColor: DefaultStyle.danger_500main + visible: !searchResultItem.core.readOnly + onClicked: { + mainItem.contactDeletionRequested(searchResultItem) + friendPopup.close() + } + background: Item{} + } + } + } + } + } + + MouseArea { + id: contactArea + enabled: mainItem.selectionEnabled + anchors.fill: contactDelegate + //height: mainItem.height + hoverEnabled: true + acceptedButtons: Qt.AllButtons + z: -1 + focus: !actionButtons.visible + Rectangle { + anchors.fill: contactArea + radius: 8 * DefaultStyle.dp + opacity: 0.7 + color: mainItem.isSelected ? DefaultStyle.main2_200 : DefaultStyle.main2_100 + visible: contactArea.containsMouse || friendPopup.hovered || mainItem.isSelected || friendPopup.visible + } + Keys.onPressed: (event)=> { + if (event.key == Qt.Key_Space || event.key == Qt.Key_Enter || event.key == Qt.Key_Return) { + contactArea.clicked(undefined) + event.accepted = true; + } + } + onClicked: (mouse) => { + forceActiveFocus() + if (mouse && mouse.button == Qt.RightButton && mainItem.showContactMenu) { + friendPopup.open() + } else { + mainItem.clicked(mouse) + } + } + } +} diff --git a/Linphone/view/Control/Display/Contact/ContactListView.qml b/Linphone/view/Control/Display/Contact/ContactListView.qml index 9b47ff313..3ef7193cb 100644 --- a/Linphone/view/Control/Display/Contact/ContactListView.qml +++ b/Linphone/view/Control/Display/Contact/ContactListView.qml @@ -7,52 +7,56 @@ import UtilsCpp 1.0 import ConstantsCpp 1.0 import SettingsCpp + ListView { id: mainItem - height: contentHeight - visible: contentHeight > 0 - clip: true - currentIndex: -1 - //keyNavigationWraps: true - // rightMargin: 5 * DefaultStyle.dp - property bool selectionEnabled: true - property bool hoverEnabled: true - // dots popup menu - property bool contactMenuVisible: true - // call, video call etc menu - property bool actionLayoutVisible: false - property bool initialHeadersVisible: true - property bool displayNameCapitalization: true - property bool showFavoritesOnly: false - property bool showDefaultAddress: false - property bool showLdapContacts: false + property bool showInitials: true // Display Initials of Display name. + property bool showDefaultAddress: true // Display address below display name. + property bool showActions: false // Display actions layout (call buttons) + property bool showContactMenu: true // Display the dot menu for contacts. + property bool showFavorites: true // Display the favorites in the header + property bool hideSuggestions: false // Hide not stored contacts (not suggestions) + property string highlightText // Bold characters in Display name. + property var sourceFlags: LinphoneEnums.MagicSearchSource.All + + property bool displayNameCapitalization: true // Capitalize display name. + + property bool selectionEnabled: true // Contact can be selected + property bool multiSelectionEnabled: false //Multiple items can be selected. + property list selectedContacts // List of default address on selected contacts. + property FriendGui selectedContact//: model.getAt(currentIndex) || null + property bool searchOnInitialization: false - - property var listProxy: MagicSearchProxy{} - property alias hideListProxy: magicSearchProxy.hideListProxy + property bool loading: false + property bool pauseSearch: false // true = don't search on text change // Model properties // set searchBarText without specifying a model to bold // matching names property string searchBarText - property string searchText: searchBarText.length === 0 ? "*" : searchBarText - property var aggregationFlag: LinphoneEnums.MagicSearchAggregation.Friend - property var sourceFlags: LinphoneEnums.MagicSearchSource.Friends | ((searchText.length > 0 && searchText != "*") || SettingsCpp.syncLdapContacts ? LinphoneEnums.MagicSearchSource.LdapServers : 0) - + property string searchText// Binding is done on searchBarTextChanged property ConferenceInfoGui confInfoGui - - property bool multiSelectionEnabled: false - property list selectedContacts - property int selectedContactCount: selectedContacts.length - - property FriendGui selectedContact: model.getAt(currentIndex) || null - + + property bool haveFavorites: false + property int sectionsPixelSize: 16 * DefaultStyle.dp + property int sectionsWeight: 800 * DefaultStyle.dp + property int sectionsSpacing: 18 * DefaultStyle.dp + + property int itemsRightMargin: 39 * DefaultStyle.dp + + signal resultsReceived() signal contactStarredChanged() signal contactDeletionRequested(FriendGui contact) signal contactAddedToSelection(string address) signal contactRemovedFromSelection(string address) signal contactClicked(FriendGui contact) + + clip: true + highlightFollowsCurrentItem: true + cacheBuffer: 400 + // Binding loop hack + onContentHeightChanged: Qt.callLater(function(){cacheBuffer = Math.max(0,contentHeight)}) function selectContact(address) { var index = magicSearchProxy.findFriendIndexByAddress(address) @@ -89,9 +93,75 @@ ListView { return index != -1 } - onCurrentIndexChanged: selectedContact = model.getAt(currentIndex) || null - onCountChanged: selectedContact = model.getAt(currentIndex) || null - + onResultsReceived: { + loading = false + mainItem.positionViewAtBeginning() + } + onSearchBarTextChanged: { + loading = true + if(!pauseSearch) { + searchText = searchBarText.length === 0 ? "*" : searchBarText + } + } + onPauseSearchChanged: { + if(!pauseSearch){ + searchText = searchBarText.length === 0 ? "*" : searchBarText + } + } + onAtYEndChanged: if(atYEnd) magicSearchProxy.displayMore() + keyNavigationEnabled: false + Keys.onPressed: (event)=> { + if(header.activeFocus) return; + if(event.key == Qt.Key_Up || event.key == Qt.Key_Down){ + if (currentIndex == 0 && event.key == Qt.Key_Up) { + if( headerItem.list.count > 0) { + mainItem.highlightFollowsCurrentItem = false + currentIndex = -1 + headerItem.list.currentIndex = headerItem.list.count -1 + var item = headerItem.list.itemAtIndex(headerItem.list.currentIndex) + mainItem.selectedContact = item.searchResultItem + item.forceActiveFocus() + headerItem.updatePosition() + event.accepted = true; + }else{ + mainItem.currentIndex = mainItem.count - 1 + var item = itemAtIndex(mainItem.currentIndex) + mainItem.selectedContact = item.searchResultItem + item.forceActiveFocus() + event.accepted = true; + } + }else if(currentIndex >= mainItem.count -1 && event.key == Qt.Key_Down){ + if( headerItem.list.count > 0) { + mainItem.highlightFollowsCurrentItem = false + mainItem.currentIndex = -1 + headerItem.list.currentIndex = 0 + var item = headerItem.list.itemAtIndex(headerItem.list.currentIndex) + mainItem.selectedContact = item.searchResultItem + item.forceActiveFocus() + headerItem.updatePosition() + event.accepted = true; + }else{ + mainItem.currentIndex = 0 + var item = itemAtIndex(mainItem.currentIndex) + mainItem.selectedContact = item.searchResultItem + item.forceActiveFocus() + event.accepted = true; + } + }else if(event.key == Qt.Key_Up){ + mainItem.highlightFollowsCurrentItem = true + var item = itemAtIndex(--mainItem.currentIndex) + mainItem.selectedContact = item.searchResultItem + item.forceActiveFocus() + event.accepted = true; + }else if(event.key == Qt.Key_Down){ + mainItem.highlightFollowsCurrentItem = true + var item = itemAtIndex(++mainItem.currentIndex) + mainItem.selectedContact = item.searchResultItem + item.forceActiveFocus() + event.accepted = true; + } + } + } Component.onCompleted: { if (confInfoGui) { for(var i = 0; i < confInfoGui.core.participants.length; ++i) { @@ -99,33 +169,7 @@ ListView { } } } - - // strange behaviour with this lines - // When a popup opens after clicking on a contact, the selected contact - // changes because we lose focus on the list - // onActiveFocusChanged: if(activeFocus && (!footerItem || !footerItem.activeFocus)) { - // currentIndex = 0 - // } - - model: MagicSearchProxy { - id: magicSearchProxy - searchText: mainItem.searchText - // This property is needed instead of playing on the delegate visibility - // considering its starred status. Otherwise, the row in the list still - // exists even if its delegate is not visible, and creates navigation issues - showFavoritesOnly: mainItem.showFavoritesOnly - aggregationFlag: mainItem.aggregationFlag - parentProxy: mainItem.listProxy - showLdapContacts: mainItem.showLdapContacts - sourceFlags: mainItem.sourceFlags - onFriendCreated: (index) => { - mainItem.currentIndex = index - } - onInitialized: { - if(mainItem.searchOnInitialization) magicSearchProxy.forceUpdate() - } - } - + Connections { target: SettingsCpp onLdapConfigChanged: { @@ -136,255 +180,302 @@ ListView { Control.ScrollBar.vertical: ScrollBar { id: scrollbar + rightPadding: 8 * DefaultStyle.dp + topPadding: mainItem.haveFavorites ? 24 * DefaultStyle.dp : 0 // Avoid to be on top of collapse button + active: true interactive: true - // anchors.top: parent.top - // anchors.bottom: parent.bottom - // anchors.right: parent.right + policy: mainItem.contentHeight > mainItem.height ? Control.ScrollBar.AlwaysOn : Control.ScrollBar.AlwaysOff } - Keys.onPressed: (event)=>{ - if(event.key == Qt.Key_Tab && !mainItem.itemAtIndex(mainItem.currentIndex).activeFocus){ - mainItem.itemAtIndex(mainItem.currentIndex).forceActiveFocus() + + model: MagicSearchProxy { + id: magicSearchProxy + searchText: mainItem.searchText + aggregationFlag: LinphoneEnums.MagicSearchAggregation.Friend + sourceFlags: mainItem.sourceFlags + + hideSuggestions: mainItem.hideSuggestions + initialDisplayItems: 20 + onLocalFriendCreated: (index) => { + var item = itemAtIndex(index) + if(item){ + mainItem.currentIndex = index + mainItem.selectedContact = item.searchResultItem + item.forceActiveFocus() + } } + onInitialized: { + mainItem.loading = true + magicSearchProxy.forceUpdate() + } + onModelReset: mainItem.resultsReceived() } - delegate: FocusScope { - id: itemDelegate - height: 56 * DefaultStyle.dp - width: mainItem.width - property var previousItem : mainItem.model.count > 0 && index > 0 ? mainItem.model.getAt(index-1) : null - property var previousDisplayName: previousItem ? previousItem.core.displayName : "" - property var displayName: modelData.core.displayName - - Connections { - enabled: modelData.core - target: modelData.core - function onStarredChanged() { mainItem.contactStarredChanged()} + + section.property: "isStored" + //section.criteria: ViewSection.FirstCharacter + section.delegate: Item{ + width: mainItem.width + height: textItem.implicitHeight + sectionsSpacing * 2 + required property bool section + Text { + id: textItem + anchors.fill: parent + text: section ? qsTr("Contacts") : qsTr("Suggestions") + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + font { + pixelSize: sectionsPixelSize + weight: sectionsWeight + } + } + } - Text { - id: initial - anchors.left: parent.left - visible: mainItem.initialHeadersVisible && mainItem.model.sourceFlags != LinphoneEnums.MagicSearchSource.All - anchors.verticalCenter: parent.verticalCenter - anchors.rightMargin: 15 * DefaultStyle.dp - verticalAlignment: Text.AlignVCenter - width: 20 * DefaultStyle.dp - opacity: (!previousItem || !previousDisplayName.toLocaleLowerCase(ConstantsCpp.DefaultLocale).startsWith(displayName[0].toLocaleLowerCase(ConstantsCpp.DefaultLocale))) ? 1 : 0 - text: displayName[0] - color: DefaultStyle.main2_400 - font { - pixelSize: 20 * DefaultStyle.dp - weight: 500 * DefaultStyle.dp - capitalization: Font.AllUppercase - } - } - RowLayout { - id: contactDelegate - anchors.left: initial.visible ? initial.right : parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.bottom: parent.bottom - spacing: 16 * DefaultStyle.dp - z: 1 - Avatar { - Layout.leftMargin: 5 * DefaultStyle.dp - Layout.preferredWidth: 45 * DefaultStyle.dp - Layout.preferredHeight: 45 * DefaultStyle.dp - contact: modelData - } - ColumnLayout { - spacing: 0 - Text { - text: UtilsCpp.boldTextPart(itemDelegate.displayName, mainItem.searchBarText) - font{ - pixelSize: mainItem.showDefaultAddress ? 16 * DefaultStyle.dp : 14 * DefaultStyle.dp - capitalization: mainItem.displayNameCapitalization ? Font.Capitalize : Font.MixedCase - weight: mainItem.showDefaultAddress ? 800 * DefaultStyle.dp : 400 * DefaultStyle.dp - } - maximumLineCount: 1 - Layout.fillWidth: true - } - Text { - maximumLineCount: 1 - visible: mainItem.showDefaultAddress - text: SettingsCpp.onlyDisplaySipUriUsername ? UtilsCpp.getUsername(modelData.core.defaultAddress) : modelData.core.defaultAddress - Layout.fillWidth: true - Layout.topMargin: 2 * DefaultStyle.dp - font { - weight: 300 * DefaultStyle.dp - pixelSize: 12 * DefaultStyle.dp + header: FocusScope{ + id: headerItem + width: mainItem.width + height: favoritesContents.implicitHeight + property alias list: favoriteList + + // Hack because changing currentindex change focus. + Timer{ + id: focusDelay + interval: 10 + onTriggered: { + mainItem.highlightFollowsCurrentItem = !headerItem.activeFocus } } - } - Item{Layout.fillWidth: true} - RowLayout { - id: actionsRow - z: 1 - visible: actionButtons || friendPopup.visible || mainItem.multiSelectionEnabled - spacing: visible ? 16 * DefaultStyle.dp : 0 - EffectImage { - id: isSelectedCheck - // visible: mainItem.multiSelectionEnabled && (mainItem.confInfoGui.core.getParticipantIndex(modelData.core.defaultAddress) != -1) - visible: mainItem.multiSelectionEnabled && (mainItem.selectedContacts.indexOf(modelData.core.defaultAddress) != -1) - Layout.preferredWidth: 24 * DefaultStyle.dp - Layout.preferredHeight: 24 * DefaultStyle.dp - imageSource: AppIcons.check - colorizationColor: DefaultStyle.main1_500_main - Connections { - target: mainItem - // onParticipantsChanged: isSelectedCheck.visible = mainItem.confInfoGui.core.getParticipantIndex(modelData.core.defaultAddress) != -1 - function onSelectedContactCountChanged(){ isSelectedCheck.visible = (mainItem.selectedContacts.indexOf(modelData.core.defaultAddress) != -1)} + onActiveFocusChanged:focusDelay.restart() + //--------------------------------------------------- + + function updatePosition(){ + var item = favoriteList.itemAtIndex(favoriteList.currentIndex) + if( item){ + // For debugging just in case + //var listPosition = item.mapToItem(favoriteList, item.x, item.y) + //var newPosition = favoriteList.mapToItem(mainItem, listPosition.x, listPosition.y) + //console.log("item pos: " +item.x + " / " +item.y) + //console.log("fav pos: " +favoriteList.x + " / " +favoriteList.y) + //console.log("fav content: " +favoriteList.contentX + " / " +favoriteList.contentY) + //console.log("main pos: " +mainItem.x + " / " +mainItem.y) + //console.log("main content: " +mainItem.contentX + " / " +mainItem.contentY) + //console.log("list pos: " +listPosition.x + " / " +listPosition.y) + //console.log("new pos: " +newPosition.x + " / " +newPosition.y) + //console.log("header pos: " +headerItem.x + " / " +headerItem.y) + //console.log("Moving to " + (headerItem.y+item.y)) + mainItem.contentY = headerItem.y+item.y } - } - RowLayout{ - id: actionButtons - visible: mainItem.actionLayoutVisible - spacing: visible ? 10 * DefaultStyle.dp : 0 - Button { - id: callButton - Layout.preferredWidth: 45 * DefaultStyle.dp - Layout.preferredHeight: 45 * DefaultStyle.dp - icon.width: 24 * DefaultStyle.dp - icon.height: 24 * DefaultStyle.dp - icon.source: AppIcons.phone - contentImageColor: DefaultStyle.main2_600 - focus: visible - background: Rectangle { - anchors.fill: parent - radius: 40 * DefaultStyle.dp - color: DefaultStyle.main2_200 - } - onClicked: UtilsCpp.createCall(modelData.core.defaultAddress) - KeyNavigation.right: chatButton - KeyNavigation.left: chatButton - } - Button { - id: chatButton - visible: actionButtons.visible && !SettingsCpp.disableChatFeature - Layout.preferredWidth: 45 * DefaultStyle.dp - Layout.preferredHeight: 45 * DefaultStyle.dp - icon.width: 24 * DefaultStyle.dp - icon.height: 24 * DefaultStyle.dp - icon.source: AppIcons.chatTeardropText - focus: visible && !callButton.visible - contentImageColor: DefaultStyle.main2_500main - background: Rectangle { - anchors.fill: parent - radius: 40 * DefaultStyle.dp - color: DefaultStyle.main2_200 - } - KeyNavigation.right: callButton - KeyNavigation.left: callButton - } - } - PopupButton { - id: friendPopup - z: 1 - // Layout.rightMargin: 13 * DefaultStyle.dp - Layout.alignment: Qt.AlignVCenter - Layout.rightMargin: 8 * DefaultStyle.dp - popup.x: 0 - popup.padding: 10 * DefaultStyle.dp - hoverEnabled: mainItem.hoverEnabled - visible: mainItem.contactMenuVisible && (contactArea.containsMouse || hovered || popup.opened) && (!mainItem.delegateButtons || mainItem.delegateButtons.length === 0) - popup.contentItem: ColumnLayout { - Button { - text: $modelData.core.starred ? qsTr("Enlever des favoris") : qsTr("Mettre en favori") - background: Item{} - icon.source: modelData.core.starred ? AppIcons.heartFill : AppIcons.heart - icon.width: 24 * DefaultStyle.dp - icon.height: 24 * DefaultStyle.dp - spacing: 10 * DefaultStyle.dp - textSize: 14 * DefaultStyle.dp - textWeight: 400 * DefaultStyle.dp - textColor: DefaultStyle.main2_500main - contentImageColor: modelData.core.starred ? DefaultStyle.danger_500main : DefaultStyle.main2_600 - onClicked: { - modelData.core.lSetStarred(!modelData.core.starred) - friendPopup.close() + } + + ColumnLayout { + id: favoritesContents + width: parent.width + spacing: mainItem.haveFavorites ? sectionsSpacing : 0 + BusyIndicator { + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight: visible ? 60 * DefaultStyle.dp : 0 + Layout.preferredWidth: 60 * DefaultStyle.dp + indicatorHeight: 60 * DefaultStyle.dp + indicatorWidth: 60 * DefaultStyle.dp + visible: mainItem.loading + indicatorColor: DefaultStyle.main1_500_main + + } + Item{// Do not use directly RowLayout : there is an issue where the layout doesn't update on visible + Layout.fillWidth: true + Layout.preferredHeight: mainItem.haveFavorites ? favoriteTitle.implicitHeight : 0 + RowLayout { + id: favoriteTitle + anchors.fill: parent + spacing: 0 + + // Need this because it can stay at 0 on display without manual relayouting (moving position, resize) + visible: mainItem.haveFavorites + onVisibleChanged: if(visible) { + Qt.callLater(mainItem.positionViewAtBeginning)// If not later, the view will not move to favoris at startup + } + Text { + //Layout.fillHeight: true + text: qsTr("Favoris") + font { + pixelSize: sectionsPixelSize + weight: sectionsWeight + } + } + Item { + Layout.fillWidth: true + } + Button { + id: favoriteExpandButton + background: Item{} + icon.source: favoriteList.visible ? AppIcons.upArrow : AppIcons.downArrow + Layout.fillHeight: true + Layout.preferredWidth: height + //Layout.preferredWidth: 24 * DefaultStyle.dp + //Layout.preferredHeight: 24 * DefaultStyle.dp + Layout.rightMargin: 23 * DefaultStyle.dp + icon.width: 24 * DefaultStyle.dp + icon.height: 24 * DefaultStyle.dp + focus: true + onClicked: favoriteList.visible = !favoriteList.visible + KeyNavigation.down: favoriteList } } - Button { - text: qsTr("Partager") - background: Item{} - icon.source: AppIcons.shareNetwork - icon.width: 24 * DefaultStyle.dp - icon.height: 24 * DefaultStyle.dp - spacing: 10 * DefaultStyle.dp - textSize: 14 * DefaultStyle.dp - textWeight: 400 * DefaultStyle.dp - textColor: DefaultStyle.main2_500main - onClicked: { - var vcard = modelData.core.getVCard() - var username = modelData.core.givenName + modelData.core.familyName - var filepath = UtilsCpp.createVCardFile(username, vcard) - if (filepath == "") UtilsCpp.showInformationPopup(qsTr("Erreur"), qsTr("La création du fichier vcard a échoué"), false) - else mainWindow.showInformationPopup(qsTr("VCard créée"), qsTr("VCard du contact enregistrée dans %1").arg(filepath)) - UtilsCpp.shareByEmail(qsTr("Partage de contact"), vcard, filepath) + } + ListView{ + id: favoriteList + Layout.fillWidth: true + Layout.preferredHeight: count > 0 ? contentHeight : 0// Show full and avoid scrolling + + + + onCountChanged: mainItem.haveFavorites = count > 0 + Keys.onPressed: (event)=> { + if(event.key == Qt.Key_Up || event.key == Qt.Key_Down) { + if (favoriteList.currentIndex == 0 && event.key == Qt.Key_Up) { + if( mainItem.count > 0) { + mainItem.highlightFollowsCurrentItem = true + favoriteList.currentIndex = -1 + mainItem.currentIndex = mainItem.count-1 + var item = mainItem.itemAtIndex(mainItem.currentIndex) + mainItem.selectedContact = item.searchResultItem + item.forceActiveFocus() + event.accepted = true; + }else{ + favoriteList.currentIndex = favoriteList.count - 1 + var item = itemAtIndex(favoriteList.currentIndex) + mainItem.selectedContact = item.searchResultItem + item.forceActiveFocus() + event.accepted = true; + } + }else if(currentIndex >= favoriteList.count -1 && event.key == Qt.Key_Down) { + if( mainItem.count > 0) { + mainItem.highlightFollowsCurrentItem = true + favoriteList.currentIndex = -1 + mainItem.currentIndex = 0 + var item = mainItem.itemAtIndex(mainItem.currentIndex) + mainItem.selectedContact = item.searchResultItem + item.forceActiveFocus() + event.accepted = true; + }else{ + favoriteList.currentIndex = 0 + var item = itemAtIndex(favoriteList.currentIndex) + mainItem.selectedContact = item.searchResultItem + item.forceActiveFocus() + event.accepted = true; + } + }else if(event.key == Qt.Key_Up){ + mainItem.highlightFollowsCurrentItem = false + var item = itemAtIndex(--favoriteList.currentIndex) + mainItem.selectedContact = item.searchResultItem + item.forceActiveFocus() + headerItem.updatePosition() + event.accepted = true; + }else if(event.key == Qt.Key_Down){ + mainItem.highlightFollowsCurrentItem = false + var item = itemAtIndex(++favoriteList.currentIndex) + mainItem.selectedContact = item.searchResultItem + item.forceActiveFocus() + headerItem.updatePosition() + event.accepted = true; + } } } - Button { - text: qsTr("Supprimer") - background: Item{} - icon.source: AppIcons.trashCan - icon.width: 24 * DefaultStyle.dp - icon.height: 24 * DefaultStyle.dp - spacing: 10 * DefaultStyle.dp - textSize: 14 * DefaultStyle.dp - textWeight: 400 * DefaultStyle.dp - textColor: DefaultStyle.danger_500main - contentImageColor: DefaultStyle.danger_500main - visible: !modelData.core.readOnly - onClicked: { - mainItem.contactDeletionRequested(modelData) - friendPopup.close() + property MagicSearchProxy proxy: MagicSearchProxy{ + parentProxy: mainItem.model + showFavoritesOnly: true + hideSuggestions: mainItem.hideSuggestions + } + model : showFavorites && mainItem.searchBarText == '' ? proxy : [] + delegate: ContactListItem{ + width: favoriteList.width + focus: true + + searchResultItem: $modelData + showInitials: mainItem.showInitials + showDefaultAddress: mainItem.showDefaultAddress + showActions: mainItem.showActions + showContactMenu: mainItem.showContactMenu + highlightText: mainItem.highlightText + + displayNameCapitalization: mainItem.displayNameCapitalization + itemsRightMargin: mainItem.itemsRightMargin + selectionEnabled: mainItem.selectionEnabled + multiSelectionEnabled: mainItem.multiSelectionEnabled + selectedContacts: mainItem.selectedContacts + isSelected: mainItem.selectedContact && mainItem.selectedContact.core == searchResultItem.core + previousInitial: ''//favoriteList.count > 0 ? favoriteList.itemAtIndex(index-1)?.initial : '' // Binding on count + initial: '' // Hide initials but keep space + + onIsSelectedChanged: if(isSelected) favoriteList.currentIndex = index + onContactStarredChanged: mainItem.contactStarredChanged() + onContactDeletionRequested: (contact) => mainItem.contactDeletionRequested(contact) + onClicked: (mouse) => { + mainItem.highlightFollowsCurrentItem = false + favoriteList.currentIndex = index + mainItem.selectedContact = searchResultItem + forceActiveFocus() + headerItem.updatePosition() + if (mainItem.multiSelectionEnabled) { + var indexInSelection = mainItem.selectedContacts.indexOf(searchResultItem.core.defaultAddress) + if (indexInSelection == -1) { + mainItem.addContactToSelection(searchResultItem.core.defaultAddress) + } + else { + mainItem.removeContactFromSelection(indexInSelection, 1) + } + } + mainItem.contactClicked(searchResultItem) } } } } } - } + + delegate: ContactListItem{ + id: contactItem + width: mainItem.width + focus: true - MouseArea { - id: contactArea - enabled: mainItem.selectionEnabled - hoverEnabled: mainItem.hoverEnabled - anchors.fill: contactDelegate - height: mainItem.height - acceptedButtons: Qt.AllButtons - z: -1 - focus: !actionButtons.visible - Rectangle { - anchors.fill: contactArea - opacity: 0.7 - radius: 8 * DefaultStyle.dp - color: mainItem.currentIndex === index ? DefaultStyle.main2_200 : DefaultStyle.main2_100 - visible: contactArea.containsMouse || friendPopup.hovered || mainItem.currentIndex === index - } - Keys.onPressed: (event)=> { - if (event.key == Qt.Key_Space || event.key == Qt.Key_Enter || event.key == Qt.Key_Return) { - contactArea.clicked(undefined) - event.accepted = true; - } - } - onClicked: (mouse) => { - if (mouse && mouse.button == Qt.RightButton) { - friendPopup.open() - } else { - mainItem.forceActiveFocus() - mainItem.currentIndex = index - if (mainItem.multiSelectionEnabled) { - var indexInSelection = mainItem.selectedContacts.indexOf(modelData.core.defaultAddress) - if (indexInSelection == -1) { - mainItem.addContactToSelection(modelData.core.defaultAddress) - } - else { - mainItem.removeContactFromSelection(indexInSelection, 1) - } - } - mainItem.contactClicked(modelData) - } - } + searchResultItem: $modelData + showInitials: mainItem.showInitials && searchResultItem.core.isStored + showDefaultAddress: mainItem.showDefaultAddress + showActions: mainItem.showActions + showContactMenu: searchResultItem.core.isStored + highlightText: mainItem.highlightText + + displayNameCapitalization: mainItem.displayNameCapitalization + itemsRightMargin: mainItem.itemsRightMargin + + selectionEnabled: mainItem.selectionEnabled + multiSelectionEnabled: mainItem.multiSelectionEnabled + selectedContacts: mainItem.selectedContacts + isSelected: mainItem.selectedContact && mainItem.selectedContact.core == searchResultItem.core + previousInitial: mainItem.itemAtIndex(index-1)?.initial + + onIsSelectedChanged: if(isSelected) mainItem.currentIndex = index + onContactStarredChanged: mainItem.contactStarredChanged() + onContactDeletionRequested: (contact) => mainItem.contactDeletionRequested(contact) + onClicked: (mouse) => { + mainItem.highlightFollowsCurrentItem = true + if (mouse && mouse.button == Qt.RightButton) { + friendPopup.open() + } else { + forceActiveFocus() + if(mainItem.selectedContact && mainItem.selectedContact.core != contactItem.searchResultItem.core) + headerItem.list.currentIndex = -1 + mainItem.selectedContact = contactItem.searchResultItem + if (mainItem.multiSelectionEnabled) { + var indexInSelection = mainItem.selectedContacts.indexOf(searchResultItem.core.defaultAddress) + if (indexInSelection == -1) { + mainItem.addContactToSelection(searchResultItem.core.defaultAddress) + } + else { + mainItem.removeContactFromSelection(indexInSelection, 1) + } + } + mainItem.contactClicked(searchResultItem) + } } } } diff --git a/Linphone/view/Control/Input/SearchBar.qml b/Linphone/view/Control/Input/SearchBar.qml index 04f82bc3e..7817adcf7 100644 --- a/Linphone/view/Control/Input/SearchBar.qml +++ b/Linphone/view/Control/Input/SearchBar.qml @@ -11,13 +11,14 @@ FocusScope { property int textInputWidth: 350 * DefaultStyle.dp property color borderColor: "transparent" property color focusedBorderColor: DefaultStyle.main2_500main - property string text: textField.text + property string text: textField.searchText property bool magnifierVisible: true property var validator: RegularExpressionValidator{} property Control.Popup numericPadPopup property alias numericPadButton: dialerButton readonly property bool hasActiveFocus: textField.activeFocus property alias color: backgroundItem.color + property bool delaySearch: true // Wait some idle time after typing to start searching signal openNumericPadRequested()// Useful for redirection before displaying numeric pad. @@ -62,6 +63,9 @@ FocusScope { anchors.leftMargin: magnifier.visible ? 0 : 10 * DefaultStyle.dp anchors.right: clearTextButton.left anchors.verticalCenter: parent.verticalCenter + + property string searchText + focus: true placeholderText: mainItem.placeholderText placeholderTextColor: mainItem.placeholderTextColor @@ -75,6 +79,7 @@ FocusScope { color: DefaultStyle.main2_600 selectByMouse: true validator: mainItem.validator + onTextChanged: mainItem.delaySearch ? delayTimer.restart() : searchText = text background: Item { opacity: 0. } @@ -83,6 +88,12 @@ FocusScope { color: DefaultStyle.main2_500main width: 1 * DefaultStyle.dp } + Timer{ + id: delayTimer + interval: 300 + repeat: false + onTriggered: textField.searchText = textField.text + } } Button { id: dialerButton diff --git a/Linphone/view/Page/Form/Call/NewCallForm.qml b/Linphone/view/Page/Form/Call/NewCallForm.qml index 000b957ac..efa1609c2 100644 --- a/Linphone/view/Page/Form/Call/NewCallForm.qml +++ b/Linphone/view/Page/Form/Call/NewCallForm.qml @@ -20,7 +20,6 @@ FocusScope { signal transferCallToAnotherRequested(CallGui dest) signal contactClicked(FriendGui contact) clip: true - onVisibleChanged: if (numPadPopup.opened) numPadPopup.close() ColumnLayout { anchors.fill: parent @@ -71,7 +70,7 @@ FocusScope { Layout.alignment: Qt.AlignTop Layout.fillWidth: true Layout.rightMargin: 39 * DefaultStyle.dp - Layout.maximumWidth: mainItem.width + //Layout.maximumWidth: mainItem.width focus: true color: mainItem.searchBarColor borderColor: mainItem.searchBarBorderColor @@ -79,127 +78,72 @@ FocusScope { numericPadPopup: mainItem.numPadPopup KeyNavigation.down: grouCallButton } - Flickable { - Layout.fillWidth: true - Layout.fillHeight: true - Layout.topMargin: 25 * DefaultStyle.dp - contentWidth: width - contentHeight: content.height - clip: true - Control.ScrollBar.vertical: ScrollBar { - active: true - interactive: true - policy: Control.ScrollBar.AsNeeded - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.right: parent.right - anchors.rightMargin: 8 * DefaultStyle.dp + ColumnLayout { + id: content + spacing: 32 * DefaultStyle.dp + Button { + id: grouCallButton + visible: mainItem.groupCallVisible && !SettingsCpp.disableMeetingsFeature + Layout.preferredWidth: 320 * DefaultStyle.dp + Layout.preferredHeight: 44 * DefaultStyle.dp + padding: 0 + KeyNavigation.up: searchBar + KeyNavigation.down: contactLoader.item + onClicked: mainItem.groupCallCreationRequested() + background: Rectangle { + anchors.fill: parent + radius: 50 * DefaultStyle.dp + gradient: Gradient { + orientation: Gradient.Horizontal + GradientStop { position: 0.0; color: DefaultStyle.main2_100} + GradientStop { position: 1.0; color: DefaultStyle.grey_0} + } + } + contentItem: RowLayout { + spacing: 16 * DefaultStyle.dp + anchors.verticalCenter: parent.verticalCenter + Image { + source: AppIcons.groupCall + Layout.preferredWidth: 44 * DefaultStyle.dp + sourceSize.width: 44 * DefaultStyle.dp + fillMode: Image.PreserveAspectFit + } + Text { + text: "Appel de groupe" + color: DefaultStyle.grey_1000 + font { + pixelSize: 16 * DefaultStyle.dp + weight: 800 * DefaultStyle.dp + underline: grouCallButton.shadowEnabled + } + } + Item { + Layout.fillWidth: true + } + Image { + source: AppIcons.rightArrow + Layout.preferredWidth: 24 * DefaultStyle.dp + Layout.preferredHeight: 24 * DefaultStyle.dp + } + } } - - ColumnLayout { - id: content - spacing: 32 * DefaultStyle.dp - anchors.left: parent.left - anchors.right: parent.right - anchors.rightMargin: 39 * DefaultStyle.dp - Button { - id: grouCallButton - visible: mainItem.groupCallVisible && !SettingsCpp.disableMeetingsFeature - Layout.preferredWidth: 320 * DefaultStyle.dp - Layout.preferredHeight: 44 * DefaultStyle.dp - padding: 0 - KeyNavigation.up: searchBar - KeyNavigation.down: contactList.count >0 ? contactList : searchList - onClicked: mainItem.groupCallCreationRequested() - background: Rectangle { - anchors.fill: parent - radius: 50 * DefaultStyle.dp - gradient: Gradient { - orientation: Gradient.Horizontal - GradientStop { position: 0.0; color: DefaultStyle.main2_100} - GradientStop { position: 1.0; color: DefaultStyle.grey_0} - } - } - contentItem: RowLayout { - spacing: 16 * DefaultStyle.dp - anchors.verticalCenter: parent.verticalCenter - Image { - source: AppIcons.groupCall - Layout.preferredWidth: 44 * DefaultStyle.dp - sourceSize.width: 44 * DefaultStyle.dp - fillMode: Image.PreserveAspectFit - } - Text { - text: "Appel de groupe" - color: DefaultStyle.grey_1000 - font { - pixelSize: 16 * DefaultStyle.dp - weight: 800 * DefaultStyle.dp - underline: grouCallButton.shadowEnabled - } - } - Item { - Layout.fillWidth: true - } - Image { - source: AppIcons.rightArrow - Layout.preferredWidth: 24 * DefaultStyle.dp - Layout.preferredHeight: 24 * DefaultStyle.dp - } - } + Loader{ + // This is a hack for an incomprehensible behavior on sections title where they doesn't match with their delegate and can be unordered after resetting models. + id: contactLoader + Layout.fillWidth: true + Layout.fillHeight: true + property string t: searchBar.text + onTChanged: { + contactLoader.active = false + Qt.callLater(function(){contactLoader.active=true}) } - ColumnLayout { - spacing: 18 * DefaultStyle.dp - visible: contactList.contentHeight > 0 - Text { - text: qsTr("Contacts") - font { - pixelSize: 16 * DefaultStyle.dp - weight: 800 * DefaultStyle.dp - } + //------------------------------------------------------------- + sourceComponent: ContactListView{ + id: contactList + searchBarText: searchBar.text + onContactClicked: (contact) => { + mainItem.contactClicked(contact) } - ContactListView{ - id: contactList - Layout.fillWidth: true - Layout.preferredHeight: contentHeight - Control.ScrollBar.vertical.visible: false - contactMenuVisible: false - searchOnInitialization: true - searchBarText: searchBar.text - onContactClicked: (contact) => { - mainItem.contactClicked(contact) - } - } - } - ColumnLayout { - spacing: 18 * DefaultStyle.dp - visible: searchList.count > 0 - Text { - text: qsTr("Suggestions") - font { - pixelSize: 16 * DefaultStyle.dp - weight: 800 * DefaultStyle.dp - } - } - ContactListView{ - id: searchList - Layout.fillWidth: true - Layout.fillHeight: true - Layout.preferredHeight: contentHeight - contactMenuVisible: false - Control.ScrollBar.vertical.visible: false - initialHeadersVisible: false - displayNameCapitalization: false - searchBarText: searchBar.text - sourceFlags: LinphoneEnums.MagicSearchSource.All - hideListProxy: contactList.model - onContactClicked: (contact) => { - mainItem.contactClicked(contact) - } - } - } - Item { - Layout.fillHeight: true } } } diff --git a/Linphone/view/Page/Form/Meeting/AddParticipantsForm.qml b/Linphone/view/Page/Form/Meeting/AddParticipantsForm.qml index 3bccac611..d9739aeda 100644 --- a/Linphone/view/Page/Form/Meeting/AddParticipantsForm.qml +++ b/Linphone/view/Page/Form/Meeting/AddParticipantsForm.qml @@ -10,7 +10,7 @@ FocusScope{ id: mainItem property string placeHolderText: qsTr("Rechercher des contacts") - property list selectedParticipants: suggestionList.selectedContacts + property list selectedParticipants//: contactLoader.item ? contactLoader.item.selectedContacts property int selectedParticipantsCount: selectedParticipants.length property ConferenceInfoGui conferenceInfoGui property color searchBarColor: DefaultStyle.grey_100 @@ -30,7 +30,7 @@ FocusScope{ Layout.preferredHeight: contentHeight Layout.maximumHeight: mainItem.height / 3 width: mainItem.width - model: suggestionList.selectedContacts + model: mainItem.selectedParticipants clip: true focus: participantList.count > 0 Keys.onPressed: (event) => { @@ -40,7 +40,7 @@ FocusScope{ } delegate: FocusScope { height: 56 * DefaultStyle.dp - width: participantList.width - scrollbar.implicitWidth - 12 * DefaultStyle.dp + width: participantList.width - scrollbar.implicitWidth - 28 * DefaultStyle.dp RowLayout { anchors.fill: parent spacing: 10 * DefaultStyle.dp @@ -67,7 +67,7 @@ FocusScope{ icon.height: 24 * DefaultStyle.dp focus: true contentImageColor: DefaultStyle.main1_500_main - onClicked: suggestionList.removeSelectedContactByAddress(modelData) + onClicked: if(contactLoader.item) contactLoader.item.removeSelectedContactByAddress(modelData) } } } @@ -83,7 +83,7 @@ FocusScope{ } } SearchBar { - id: searchbar + id: searchBar Layout.fillWidth: true Layout.topMargin: 6 * DefaultStyle.dp Layout.rightMargin: 28 * DefaultStyle.dp @@ -95,86 +95,49 @@ FocusScope{ KeyNavigation.up: participantList.count > 0 ? participantList : nextItemInFocusChain(false) - KeyNavigation.down: contactList + KeyNavigation.down: contactLoader.item } - Flickable { - Layout.fillWidth: true - Layout.fillHeight: true - contentWidth: width - contentHeight: content.height - clip: true - Control.ScrollBar.vertical: ScrollBar { - id: contactsScrollBar - active: true - interactive: true - policy: Control.ScrollBar.AsNeeded - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.right: parent.right - anchors.rightMargin: 8 * DefaultStyle.dp + ColumnLayout { + id: content + spacing: 15 * DefaultStyle.dp + Text { + visible: !contactLoader.item?.loading && contactLoader.item?.count === 0 + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: 137 * DefaultStyle.dp + text: qsTr("Aucun contact%1").arg(searchBar.text.length !== 0 ? " correspondant" : "") + font { + pixelSize: 16 * DefaultStyle.dp + weight: 800 * DefaultStyle.dp + } } - ColumnLayout { - id: content - anchors.left: parent.left - anchors.right: parent.right - anchors.rightMargin: contactsScrollBar.implicitWidth + 12 * DefaultStyle.dp - Text { - Layout.topMargin: 6 * DefaultStyle.dp - text: qsTr("Contacts") - font { - pixelSize: 16 * DefaultStyle.dp - weight: 800 * DefaultStyle.dp - } + Loader{ + // This is a hack for an incomprehensible behavior on sections title where they doesn't match with their delegate and can be unordered after resetting models. + id: contactLoader + Layout.fillWidth: true + Layout.fillHeight: true + property string t: searchBar.text + onTChanged: { + contactLoader.active = false + Qt.callLater(function(){contactLoader.active=true}) } - ContactListView { + //------------------------------------------------------------- + sourceComponent: ContactListView{ id: contactList - visible: contentHeight > 0 || searchbar.text.length > 0 - Layout.fillWidth: true - // Layout.fillHeight: true - Layout.topMargin: 8 * DefaultStyle.dp - Layout.preferredHeight: contentHeight - multiSelectionEnabled: true - contactMenuVisible: false - confInfoGui: mainItem.conferenceInfoGui - searchBarText: searchbar.text - searchOnInitialization: true - onContactAddedToSelection: (address) => { - suggestionList.addContactToSelection(address) - } - onContactRemovedFromSelection: (address) => suggestionList.removeSelectedContactByAddress(address) - Control.ScrollBar.vertical.visible: false - } - Text { - Layout.topMargin: 6 * DefaultStyle.dp - text: qsTr("Suggestions") - font { - pixelSize: 16 * DefaultStyle.dp - weight: 800 * DefaultStyle.dp - } - } - ContactListView { - id: suggestionList Layout.fillWidth: true Layout.fillHeight: true - Layout.preferredHeight: contentHeight - Control.ScrollBar.vertical.visible: false - contactMenuVisible: false - searchBarText: searchbar.text - sourceFlags: LinphoneEnums.MagicSearchSource.All + itemsRightMargin: 28 * DefaultStyle.dp multiSelectionEnabled: true - displayNameCapitalization: false - hideListProxy: contactList.model + showContactMenu: false + confInfoGui: mainItem.conferenceInfoGui + selectedContacts: mainItem.selectedParticipants + onSelectedContactsChanged: Qt.callLater(function(){mainItem.selectedParticipants = selectedContacts}) + searchBarText: searchBar.text onContactAddedToSelection: (address) => { contactList.addContactToSelection(address) - participantList.positionViewAtEnd() } onContactRemovedFromSelection: (address) => contactList.removeSelectedContactByAddress(address) } } } - - // Item { - // Layout.fillHeight: true - // } } } diff --git a/Linphone/view/Page/Layout/Main/MainLayout.qml b/Linphone/view/Page/Layout/Main/MainLayout.qml index 3ef785e40..d998e3ed7 100644 --- a/Linphone/view/Page/Layout/Main/MainLayout.qml +++ b/Linphone/view/Page/Layout/Main/MainLayout.qml @@ -171,8 +171,8 @@ Item { if (text.length != 0) listPopup.open() else listPopup.close() } - KeyNavigation.down: contactList.count > 0 ? contactList : contactList.footerItem - KeyNavigation.up: contactList.footerItem + KeyNavigation.down: contactLoader.item?.count > 0 || !contactLoader.item?.footerItem? contactLoader.item : contactLoader.item?.footerItem + KeyNavigation.up: contactLoader.item?.footerItem ? contactLoader.item?.footerItem : contactLoader.item component MagicSearchButton: Button { id: button @@ -196,13 +196,13 @@ Item { id: listPopup width: magicSearchBar.width property int maxHeight: 400 * DefaultStyle.dp - property bool displayScrollbar: contactList.contentHeight + topPadding + bottomPadding> maxHeight - height: Math.min(contactList.contentHeight + topPadding + bottomPadding, maxHeight) + property bool displayScrollbar: contactLoader.item?.contentHeight + topPadding + bottomPadding> maxHeight + height: Math.min(contactLoader.item?.contentHeight + topPadding + bottomPadding, maxHeight) y: magicSearchBar.height // closePolicy: Popup.NoAutoClose topPadding: 20 * DefaultStyle.dp bottomPadding: 20 * DefaultStyle.dp - rightPadding: 20 * DefaultStyle.dp + rightPadding: 10 * DefaultStyle.dp leftPadding: 20 * DefaultStyle.dp background: Item { @@ -213,7 +213,7 @@ Item { color: DefaultStyle.grey_0 anchors.fill: parent border.color: DefaultStyle.main1_500_main - border.width: contactList.activeFocus ? 2 : 0 + border.width: contactLoader.item?.activeFocus ? 2 : 0 } MultiEffect { @@ -237,125 +237,138 @@ Item { } } - contentItem: ContactListView { - id: contactList - visible: magicSearchBar.text.length != 0 - Layout.preferredHeight: contentHeight + contentItem: Loader{ + // This is a hack for an incomprehensible behavior on sections title where they doesn't match with their delegate and can be unordered after resetting models. + id: contactLoader Layout.fillWidth: true - Layout.rightMargin: 5 * DefaultStyle.dp - initialHeadersVisible: false - contactMenuVisible: false - actionLayoutVisible: true - selectionEnabled: false - showDefaultAddress: true - showLdapContacts: true - Control.ScrollBar.vertical: scrollbar - searchText: magicSearchBar.text - - Keys.onPressed: (event) => { - if(event.key == Qt.Key_Down){ - if(contactList.currentIndex == contactList.count -1) { - contactList.currentIndex = -1 - contactList.footerItem.forceActiveFocus() - event.accepted = true - } - } else if(event.key == Qt.Key_Up){ - if(contactList.currentIndex <= 0) { - contactList.currentIndex = -1 - contactList.footerItem.forceActiveFocus() - event.accepted = true - } - } + Layout.fillHeight: true + property bool deactivate: false + active: !deactivate && magicSearchBar.text != '' + property string t: magicSearchBar.text + onTChanged: { + contactLoader.deactivate = true + Qt.callLater(function(){contactLoader.deactivate=false}) } - header: Text { - visible: contactList.count > 0 - text: qsTr("Contact") - color: DefaultStyle.main2_500main - font { - pixelSize: 13 * DefaultStyle.dp - weight: 700 * DefaultStyle.dp - } - } - footer: FocusScope{ - id: suggestionFocusScope - width: contactList.width - height: visible ? content.implicitHeight : 0 - onActiveFocusChanged: if(activeFocus) contactList.positionViewAtEnd() - visible: !contactList.haveAddress(suggestionText.text) - Rectangle{ - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - height: suggestionRow.implicitHeight - color: suggestionFocusScope.activeFocus ? DefaultStyle.numericPadPressedButtonColor : 'transparent' - } - ColumnLayout { - id: content - anchors.fill: parent - anchors.rightMargin: 5 * DefaultStyle.dp - - spacing: 10 * DefaultStyle.dp - Text { - text: qsTr("Suggestion") - color: DefaultStyle.main2_500main - font { - pixelSize: 13 * DefaultStyle.dp - weight: 700 * DefaultStyle.dp - } - } - - Keys.onPressed: (event) => { - if(contactList.count <= 0) return; - if(event.key == Qt.Key_Down){ - contactList.currentIndex = 0 + //------------------------------------------------------------- + sourceComponent: ContactListView { + id: contactList + visible: magicSearchBar.text.length != 0 + Layout.preferredHeight: item?.contentHeight + Layout.fillWidth: true + itemsRightMargin: 5 * DefaultStyle.dp //(Actions have already 10 of margin) + showInitials: false + showContactMenu: false + showActions: true + showFavorites: false + selectionEnabled: false + showDefaultAddress: true + hideSuggestions: true + + sectionsPixelSize: 13 * DefaultStyle.dp + sectionsWeight: 700 * DefaultStyle.dp + sectionsSpacing: 5 * DefaultStyle.dp + + Control.ScrollBar.vertical: scrollbar + searchBarText: magicSearchBar.text + + Keys.onPressed: (event) => { + if(event.key == Qt.Key_Down){ + if(contactList.currentIndex == contactList.count -1) { + contactList.currentIndex = -1 + contactList.footerItem.forceActiveFocus() event.accepted = true - } else if(event.key == Qt.Key_Up){ - contactList.currentIndex = contactList.count - 1 + } + } else if(event.key == Qt.Key_Up){ + if(contactList.currentIndex <= 0) { + contactList.currentIndex = -1 + contactList.footerItem.forceActiveFocus() event.accepted = true } } - RowLayout { - id: suggestionRow - spacing: 10 * DefaultStyle.dp + } + + footer: FocusScope{ + id: suggestionFocusScope + width: contactList.width + height: visible ? content.implicitHeight : 0 + onActiveFocusChanged: if(activeFocus) contactList.positionViewAtEnd() + visible: !contactList.haveAddress(suggestionText.text) + Rectangle{ + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: suggestionRow.implicitHeight + color: suggestionFocusScope.activeFocus ? DefaultStyle.numericPadPressedButtonColor : 'transparent' + } + ColumnLayout { + id: content + anchors.fill: parent + anchors.leftMargin: 5 * DefaultStyle.dp + anchors.rightMargin: 15 * DefaultStyle.dp - Avatar { - Layout.preferredWidth: 45 * DefaultStyle.dp - Layout.preferredHeight: 45 * DefaultStyle.dp - _address: magicSearchBar.text - } + spacing: 10 * DefaultStyle.dp Text { - id: suggestionText - property var urlObj: UtilsCpp.interpretUrl(magicSearchBar.text) - text: SettingsCpp.onlyDisplaySipUriUsername ? UtilsCpp.getUsername(urlObj?.value) : urlObj?.value + text: qsTr("Suggestion") + color: DefaultStyle.main2_500main font { - pixelSize: 12 * DefaultStyle.dp - weight: 300 * DefaultStyle.dp + pixelSize: 13 * DefaultStyle.dp + weight: 700 * DefaultStyle.dp } } - Item { - Layout.fillWidth: true - } - MagicSearchButton { - id: callButton - Layout.preferredWidth: 45 * DefaultStyle.dp - Layout.preferredHeight: 45 * DefaultStyle.dp - icon.source: AppIcons.phone - focus: true - onClicked: { - UtilsCpp.createCall(magicSearchBar.text) - magicSearchBar.clearText() + + Keys.onPressed: (event) => { + if(contactList.count <= 0) return; + if(event.key == Qt.Key_Down){ + contactList.currentIndex = 0 + event.accepted = true + } else if(event.key == Qt.Key_Up){ + contactList.currentIndex = contactList.count - 1 + event.accepted = true } - KeyNavigation.right: chatButton - KeyNavigation.left: chatButton } - MagicSearchButton { - id: chatButton - visible: !SettingsCpp.disableChatFeature - Layout.preferredWidth: 45 * DefaultStyle.dp - Layout.preferredHeight: 45 * DefaultStyle.dp - icon.source: AppIcons.chatTeardropText - KeyNavigation.right: callButton - KeyNavigation.left: callButton + RowLayout { + id: suggestionRow + spacing: 10 * DefaultStyle.dp + + Avatar { + Layout.preferredWidth: 45 * DefaultStyle.dp + Layout.preferredHeight: 45 * DefaultStyle.dp + _address: magicSearchBar.text + } + Text { + id: suggestionText + property var urlObj: UtilsCpp.interpretUrl(magicSearchBar.text) + text: SettingsCpp.onlyDisplaySipUriUsername ? UtilsCpp.getUsername(urlObj?.value) : urlObj?.value + font { + pixelSize: 12 * DefaultStyle.dp + weight: 300 * DefaultStyle.dp + } + } + Item { + Layout.fillWidth: true + } + MagicSearchButton { + id: callButton + Layout.preferredWidth: 45 * DefaultStyle.dp + Layout.preferredHeight: 45 * DefaultStyle.dp + icon.source: AppIcons.phone + focus: true + onClicked: { + UtilsCpp.createCall(magicSearchBar.text) + magicSearchBar.clearText() + } + KeyNavigation.right: chatButton + KeyNavigation.left: chatButton + } + MagicSearchButton { + id: chatButton + visible: !SettingsCpp.disableChatFeature + Layout.preferredWidth: 45 * DefaultStyle.dp + Layout.preferredHeight: 45 * DefaultStyle.dp + icon.source: AppIcons.chatTeardropText + KeyNavigation.right: callButton + KeyNavigation.left: callButton + } } } } diff --git a/Linphone/view/Page/Main/Call/CallPage.qml b/Linphone/view/Page/Main/Call/CallPage.qml index 2c1fcc5cd..a8138eac4 100644 --- a/Linphone/view/Page/Main/Call/CallPage.qml +++ b/Linphone/view/Page/Main/Call/CallPage.qml @@ -55,7 +55,7 @@ AbstractMainPage { onNoItemButtonPressed: goToNewCall() - showDefaultItem: listStackView.currentItem && listStackView.currentItem.objectName == "historyListItem" && listStackView.currentItem.listView.count === 0 + showDefaultItem: listStackView.currentItem && listStackView.currentItem.objectName == "historyListItem" && listStackView.currentItem.listView.count === 0 || false function goToNewCall() { if (listStackView.currentItem && listStackView.currentItem.objectName != "newCallItem") listStackView.push(newCallItem) diff --git a/Linphone/view/Page/Main/Contact/ContactPage.qml b/Linphone/view/Page/Main/Contact/ContactPage.qml index 490a8cc03..6cba677dd 100644 --- a/Linphone/view/Page/Main/Contact/ContactPage.qml +++ b/Linphone/view/Page/Main/Contact/ContactPage.qml @@ -20,8 +20,7 @@ AbstractMainPage { onVisibleChanged: if (!visible) { rightPanelStackView.clear() - contactList.currentIndex = -1 - favoriteList.currentIndex = -1 + if(contactLoader.item) contactLoader.item.currentIndex = -1 } onSelectedContactChanged: { @@ -52,13 +51,8 @@ AbstractMainPage { // rightPanelStackView.initialItem: contactDetail - showDefaultItem: rightPanelStackView.depth == 0 && leftPanelNoItemText.visible && searchBar.text.length === 0 + showDefaultItem: rightPanelStackView.depth == 0 && contactLoader.item?.count === 0 && searchBar.text.length === 0 - MagicSearchProxy { - id: allFriends - showLdapContacts: SettingsCpp.syncLdapContacts - } - function deleteContact(contact) { if (!contact) return var mainWin = UtilsCpp.getMainWindow() @@ -211,170 +205,60 @@ AbstractMainPage { spacing: 38 * DefaultStyle.dp SearchBar { id: searchBar - visible: contactList.count != 0 || text.length !== 0 + visible: !contactLoader.item || contactLoader.item.loading || contactLoader.item.count != 0 || text.length !== 0 Layout.leftMargin: leftPanel.leftMargin Layout.rightMargin: leftPanel.rightMargin Layout.topMargin: 18 * DefaultStyle.dp Layout.fillWidth: true placeholderText: qsTr("Rechercher un contact") KeyNavigation.up: createContactButton - KeyNavigation.down: favoriteList.contentHeight > 0 ? favoriteExpandButton : contactExpandButton + KeyNavigation.down: contactLoader.item } - - RowLayout { - Flickable { - id: listLayout - contentWidth: width - contentHeight: content.height - clip: true - Control.ScrollBar.vertical: contactsScrollbar + ColumnLayout { + id: content + spacing: 15 * DefaultStyle.dp + Text { + visible: contactLoader.item && !contactLoader.item.loading && contactLoader.item.count === 0 + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: 137 * DefaultStyle.dp + text: qsTr("Aucun contact%1").arg(searchBar.text.length !== 0 ? " correspondant" : "") + font { + pixelSize: 16 * DefaultStyle.dp + weight: 800 * DefaultStyle.dp + } + } + Loader{ + // This is a hack for an incomprehensible behavior on sections title where they doesn't match with their delegate and can be unordered after resetting models. + id: contactLoader Layout.fillWidth: true Layout.fillHeight: true - - ColumnLayout { - id: content - width: parent.width - spacing: 15 * DefaultStyle.dp - Text { - id: leftPanelNoItemText - visible: contactList.count === 0 - Layout.alignment: Qt.AlignHCenter - Layout.topMargin: 137 * DefaultStyle.dp - text: qsTr("Aucun contact%1").arg(searchBar.text.length !== 0 ? " correspondant" : "") - font { - pixelSize: 16 * DefaultStyle.dp - weight: 800 * DefaultStyle.dp - } + Layout.leftMargin: 45 * DefaultStyle.dp + property string t: searchBar.text + active: leftPanel.visible + onTChanged: { + contactLoader.active = false + Qt.callLater(function(){contactLoader.active=true}) + } + //------------------------------------------------------------- + sourceComponent: ContactListView{ + id: contactList + searchBarText: searchBar.text + hideSuggestions: true + sourceFlags: LinphoneEnums.MagicSearchSource.Friends | LinphoneEnums.MagicSearchSource.FavoriteFriends | LinphoneEnums.MagicSearchSource.LdapServers + + onSelectedContactChanged: { + mainItem.selectedContact = selectedContact } - ColumnLayout { - visible: favoriteList.contentHeight > 0 - onVisibleChanged: if (visible && !favoriteList.visible) favoriteList.visible = true - Layout.leftMargin: leftPanel.leftMargin - Layout.rightMargin: leftPanel.rightMargin - spacing: 18 * DefaultStyle.dp - RowLayout { - spacing: 0 - Text { - text: qsTr("Favoris") - font { - pixelSize: 16 * DefaultStyle.dp - weight: 800 * DefaultStyle.dp - } - } - Item { - Layout.fillWidth: true - } - Button { - id: favoriteExpandButton - background: Item{} - icon.source: favoriteList.visible ? AppIcons.upArrow : AppIcons.downArrow - Layout.preferredWidth: 24 * DefaultStyle.dp - Layout.preferredHeight: 24 * DefaultStyle.dp - icon.width: 24 * DefaultStyle.dp - icon.height: 24 * DefaultStyle.dp - onClicked: favoriteList.visible = !favoriteList.visible - KeyNavigation.up: searchBar - KeyNavigation.down: favoriteList - } - } - ContactListView{ - id: favoriteList - hoverEnabled: mainItem.leftPanelEnabled - highlightFollowsCurrentItem: true - Layout.fillWidth: true - Layout.preferredHeight: contentHeight - Control.ScrollBar.vertical.visible: false - showFavoritesOnly: true - searchOnInitialization: true - contactMenuVisible: true - searchBarText: searchBar.text - listProxy: allFriends - onSelectedContactChanged: { - if (selectedContact) { - contactList.currentIndex = -1 - } - mainItem.selectedContact = selectedContact - } - onContactDeletionRequested: (contact) => { - mainItem.deleteContact(contact) - } - } + onContactDeletionRequested: (contact) => { + mainItem.deleteContact(contact) } - ColumnLayout { - visible: contactList.contentHeight > 0 - onVisibleChanged: if (visible && !contactList.visible) contactList.visible = true - Layout.leftMargin: leftPanel.leftMargin - Layout.rightMargin: leftPanel.rightMargin - spacing: 16 * DefaultStyle.dp - RowLayout { - spacing: 0 - Text { - text: qsTr("Contacts") - font { - pixelSize: 16 * DefaultStyle.dp - weight: 800 * DefaultStyle.dp - } - } - Item { - Layout.fillWidth: true - } - Button { - id: contactExpandButton - background: Item{} - icon.source: contactList.visible ? AppIcons.upArrow : AppIcons.downArrow - Layout.preferredWidth: 24 * DefaultStyle.dp - Layout.preferredHeight: 24 * DefaultStyle.dp - icon.width: 24 * DefaultStyle.dp - icon.height: 24 * DefaultStyle.dp - onClicked: contactList.visible = !contactList.visible - KeyNavigation.up: favoriteList.visible ? favoriteList : searchBar - KeyNavigation.down: contactList - } - } - ContactListView{ - id: contactList - Layout.fillWidth: true - Layout.preferredHeight: contentHeight - Control.ScrollBar.vertical.visible: false - hoverEnabled: mainItem.leftPanelEnabled - contactMenuVisible: true - searchOnInitialization: true - highlightFollowsCurrentItem: true - searchBarText: searchBar.text - listProxy: allFriends - onCountChanged: { - if (initialFriendToDisplay.length !== 0) { - if (selectContact(initialFriendToDisplay) != -1) initialFriendToDisplay = "" - } - } - onSelectedContactChanged: { - if (selectedContact) { - favoriteList.currentIndex = -1 - } - mainItem.selectedContact = selectedContact - } - onContactDeletionRequested: (contact) => { - mainItem.deleteContact(contact) - } - Connections { - target: contactList.model - function onFriendCreated(index) { - contactList.currentIndex = index - } - } + onCountChanged: { + if (initialFriendToDisplay.length !== 0) { + if (selectContact(initialFriendToDisplay) != -1) initialFriendToDisplay = "" } } } } - ScrollBar { - id: contactsScrollbar - Layout.fillHeight: true - Layout.rightMargin: 8 * DefaultStyle.dp - height: listLayout.height - active: true - interactive: true - policy: Control.ScrollBar.AsNeeded - } } } } @@ -493,8 +377,6 @@ AbstractMainPage { spacing: 0 Text { text: contactDetail.contactName - Layout.fillWidth: true - maximumLineCount: 1 font { pixelSize: 29 * DefaultStyle.dp weight: 800 * DefaultStyle.dp @@ -579,6 +461,7 @@ AbstractMainPage { model: mainItem.selectedContact ? mainItem.selectedContact.core.allAddresses : [] } delegate: Item { + property var listViewModelData: modelData width: addrList.width height: 46 * DefaultStyle.dp @@ -595,7 +478,7 @@ AbstractMainPage { Layout.fillWidth: true Text { Layout.fillWidth: true - text: modelData.label + text: listViewModelData.label font { pixelSize: 13 * DefaultStyle.dp weight: 700 * DefaultStyle.dp @@ -603,7 +486,7 @@ AbstractMainPage { } Text { Layout.fillWidth: true - property string _text: modelData.address + property string _text: listViewModelData.address text: SettingsCpp.onlyDisplaySipUriUsername ? UtilsCpp.getUsername(_text) : _text font { pixelSize: 14 * DefaultStyle.dp @@ -624,7 +507,7 @@ AbstractMainPage { icon.width: 24 * DefaultStyle.dp icon.height: 24 * DefaultStyle.dp onClicked: { - UtilsCpp.createCall(modelData.address) + UtilsCpp.createCall(listViewModelData.address) } } } @@ -761,16 +644,17 @@ AbstractMainPage { id: deviceDelegate width: deviceList.width height: 30 * DefaultStyle.dp + property var listViewModelData: modelData property var callObj property CallGui deviceCall: callObj ? callObj.value : null - property string deviceName: modelData.name.length != 0 ? modelData.name : qsTr("Appareil sans nom") + property string deviceName: listViewModelData.name.length != 0 ? listViewModelData.name : qsTr("Appareil sans nom") Text { text: deviceDelegate.deviceName font.pixelSize: 14 * DefaultStyle.dp } Item{Layout.fillWidth: true} Image{ - visible: modelData.securityLevel === LinphoneEnums.SecurityLevel.EndToEndEncryptedAndVerified + visible: listViewModelData.securityLevel === LinphoneEnums.SecurityLevel.EndToEndEncryptedAndVerified source: AppIcons.trusted width: 22 * DefaultStyle.dp height: 22 * DefaultStyle.dp @@ -778,7 +662,7 @@ AbstractMainPage { Button { Layout.preferredHeight: 30 * DefaultStyle.dp - visible: modelData.securityLevel != LinphoneEnums.SecurityLevel.EndToEndEncryptedAndVerified + visible: listViewModelData.securityLevel != LinphoneEnums.SecurityLevel.EndToEndEncryptedAndVerified color: DefaultStyle.main1_100 icon.source: AppIcons.warningCircle icon.height: 14 * DefaultStyle.dp @@ -794,12 +678,12 @@ AbstractMainPage { onClicked: { if (SettingsCpp.getDisplayDeviceCheckConfirmation()) { verifyDevicePopup.deviceName = deviceDelegate.deviceName - verifyDevicePopup.deviceAddress = modelData.address + verifyDevicePopup.deviceAddress = listViewModelData.address verifyDevicePopup.open() } else { - UtilsCpp.createCall(modelData.address, {}, LinphoneEnums.MediaEncryption.Zrtp) - parent.callObj = UtilsCpp.getCallByAddress(modelData.address) + UtilsCpp.createCall(listViewModelData.address, {}, LinphoneEnums.MediaEncryption.Zrtp) + parent.callObj = UtilsCpp.getCallByAddress(listViewModelData.address) } } }