From a6561ccb194cb54a373b600f4dc25ad4b6f67bbd Mon Sep 17 00:00:00 2001 From: Julien Wadel Date: Fri, 15 Nov 2024 11:16:55 +0100 Subject: [PATCH] Fix reentrency issues with magic search : - store search parameters into Core. - add search limitation to avoid 300 useless items. - retrieve old parameters on proxy when changing list. - store parent proxy to avoid MOC warnings. Fix contacts search views: - add a loading state for buzy indicators. - limit results on suggestions. - avoid to create MagicSearchProxy if not needed. - add a status to know if friend is stored or not. - propagate invalidateFilter. - delay search while typing. Fix margins and participants selection. Do not search contacts when contact panel is not shown. Avoid search on empty magicbar. Avoid repeating section on object that disappeared from cache. Focus on new contact after creation. Avoid changing maxresult if not needed. Redirect only if friend is not ldap Fix empty display name on making favorite a ldap contact. Fix focus and positions on favorites. --- Linphone/core/call/CallCore.cpp | 5 +- Linphone/core/friend/FriendCore.cpp | 29 +- Linphone/core/friend/FriendCore.hpp | 11 +- Linphone/core/friend/FriendGui.cpp | 6 + Linphone/core/friend/FriendGui.hpp | 7 + Linphone/core/proxy/SortFilterProxy.cpp | 4 + Linphone/core/proxy/SortFilterProxy.hpp | 1 + Linphone/core/search/MagicSearchList.cpp | 79 ++- Linphone/core/search/MagicSearchList.hpp | 15 +- Linphone/core/search/MagicSearchProxy.cpp | 105 ++- Linphone/core/search/MagicSearchProxy.hpp | 24 +- Linphone/model/search/MagicSearchModel.cpp | 45 +- Linphone/model/search/MagicSearchModel.hpp | 11 +- Linphone/view/CMakeLists.txt | 1 + .../Display/Contact/ContactListItem.qml | 264 +++++++ .../Display/Contact/ContactListView.qml | 671 ++++++++++-------- Linphone/view/Control/Input/SearchBar.qml | 13 +- Linphone/view/Page/Form/Call/NewCallForm.qml | 184 ++--- .../Page/Form/Meeting/AddParticipantsForm.qml | 107 +-- Linphone/view/Page/Layout/Main/MainLayout.qml | 237 ++++--- Linphone/view/Page/Main/Call/CallPage.qml | 2 +- .../view/Page/Main/Contact/ContactPage.qml | 220 ++---- 22 files changed, 1158 insertions(+), 883 deletions(-) create mode 100644 Linphone/view/Control/Display/Contact/ContactListItem.qml 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) } } }