diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 538b0257a..30ecfa53d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -68,7 +68,8 @@ set(SOURCES src/components/settings/SettingsModel.cpp src/components/sip-addresses/SipAddressesModel.cpp src/components/sip-addresses/UnregisteredSipAddressesModel.cpp - src/components/sip-addresses/UnregisteredSipAddressesProxyModel.hpp + src/components/sip-addresses/UnregisteredSipAddressesProxyModel.cpp + src/components/smart-search-bar/SmartSearchBarModel.cpp src/components/timeline/TimelineModel.cpp src/main.cpp ) @@ -94,6 +95,7 @@ set(HEADERS src/components/sip-addresses/SipAddressesModel.hpp src/components/sip-addresses/UnregisteredSipAddressesModel.hpp src/components/sip-addresses/UnregisteredSipAddressesProxyModel.hpp + src/components/smart-search-bar/SmartSearchBarModel.hpp src/components/timeline/TimelineModel.hpp src/utils.hpp ) diff --git a/tests/src/app/App.cpp b/tests/src/app/App.cpp index d9dded5f9..2c63bc0c9 100644 --- a/tests/src/app/App.cpp +++ b/tests/src/app/App.cpp @@ -6,14 +6,13 @@ #include "../components/camera/Camera.hpp" #include "../components/chat/ChatProxyModel.hpp" -#include "../components/contacts/ContactsListModel.hpp" #include "../components/contacts/ContactsListProxyModel.hpp" #include "../components/core/CoreManager.hpp" #include "../components/notifier/Notifier.hpp" #include "../components/settings/AccountSettingsModel.hpp" -#include "../components/sip-addresses/SipAddressesModel.hpp" #include "../components/sip-addresses/UnregisteredSipAddressesProxyModel.hpp" #include "../components/timeline/TimelineModel.hpp" +#include "../components/smart-search-bar/SmartSearchBarModel.hpp" #include "App.hpp" @@ -140,6 +139,12 @@ void App::registerTypes () { } ); + qmlRegisterSingletonType( + "Linphone", 1, 0, "SmartSearchBarModel", + [](QQmlEngine *, QJSEngine *) -> QObject *{ + return new SmartSearchBarModel(); + } + ); qmlRegisterType("Linphone", 1, 0, "Camera"); qmlRegisterType("Linphone", 1, 0, "ContactsListProxyModel"); qmlRegisterType("Linphone", 1, 0, "ChatModel"); diff --git a/tests/src/components/contacts/ContactsListProxyModel.cpp b/tests/src/components/contacts/ContactsListProxyModel.cpp index ac24a6538..fdf107f98 100644 --- a/tests/src/components/contacts/ContactsListProxyModel.cpp +++ b/tests/src/components/contacts/ContactsListProxyModel.cpp @@ -35,7 +35,6 @@ ContactsListProxyModel::ContactsListProxyModel (QObject *parent) : QSortFilterPr m_list = CoreManager::getInstance()->getContactsListModel(); setSourceModel(m_list); - setFilterCaseSensitivity(Qt::CaseInsensitive); setDynamicSortFilter(false); for (const ContactModel *contact : m_list->m_list) @@ -88,12 +87,8 @@ float ContactsListProxyModel::computeStringWeight (const QString &string, float if ((offset = tmp_offset) == 0) break; } - // No weight. - if (offset == -1) - return 0; - - // Weight & offset. switch (offset) { + case -1: return 0; case 0: return percentage * FACTOR_POS_0; case 1: return percentage * FACTOR_POS_1; case 2: return percentage * FACTOR_POS_2; diff --git a/tests/src/components/contacts/ContactsListProxyModel.hpp b/tests/src/components/contacts/ContactsListProxyModel.hpp index 9d257ffad..2129cd152 100644 --- a/tests/src/components/contacts/ContactsListProxyModel.hpp +++ b/tests/src/components/contacts/ContactsListProxyModel.hpp @@ -25,7 +25,7 @@ public: public slots: void setFilter (const QString &pattern) { setFilterFixedString(pattern); - invalidateFilter(); + invalidate(); } protected: diff --git a/tests/src/components/sip-addresses/UnregisteredSipAddressesModel.cpp b/tests/src/components/sip-addresses/UnregisteredSipAddressesModel.cpp index 610407d48..8776b0e17 100644 --- a/tests/src/components/sip-addresses/UnregisteredSipAddressesModel.cpp +++ b/tests/src/components/sip-addresses/UnregisteredSipAddressesModel.cpp @@ -10,5 +10,5 @@ UnregisteredSipAddressesModel::UnregisteredSipAddressesModel (QObject *parent) : bool UnregisteredSipAddressesModel::filterAcceptsRow (int source_row, const QModelIndex &source_parent) const { QModelIndex index = sourceModel()->index(source_row, 0, source_parent); - return index.data().toMap().contains("contact"); + return !index.data().toMap().contains("contact"); } diff --git a/tests/src/components/sip-addresses/UnregisteredSipAddressesProxyModel.cpp b/tests/src/components/sip-addresses/UnregisteredSipAddressesProxyModel.cpp index b7896163d..84924a761 100644 --- a/tests/src/components/sip-addresses/UnregisteredSipAddressesProxyModel.cpp +++ b/tests/src/components/sip-addresses/UnregisteredSipAddressesProxyModel.cpp @@ -1,3 +1,57 @@ +#include "../core/CoreManager.hpp" + #include "UnregisteredSipAddressesProxyModel.hpp" +#define WEIGHT_POS_0 5 +#define WEIGHT_POS_1 4 +#define WEIGHT_POS_2 3 +#define WEIGHT_POS_3 2 +#define WEIGHT_POS_OTHER 1 + // ============================================================================= + +const QRegExp UnregisteredSipAddressesProxyModel::m_search_separators("^[^_.-;@ ][_.-;@ ]"); + +// ----------------------------------------------------------------------------- + +UnregisteredSipAddressesProxyModel::UnregisteredSipAddressesProxyModel (QObject *parent) : + QSortFilterProxyModel(parent) { + setSourceModel(CoreManager::getInstance()->getUnregisteredSipAddressesModel()); + setDynamicSortFilter(false); + sort(0); +} + +bool UnregisteredSipAddressesProxyModel::filterAcceptsRow (int source_row, const QModelIndex &source_parent) const { + QModelIndex index = sourceModel()->index(source_row, 0, source_parent); + return computeStringWeight(index.data().toMap()["sipAddress"].toString()) > 0; +} + +bool UnregisteredSipAddressesProxyModel::lessThan (const QModelIndex &left, const QModelIndex &right) const { + return computeStringWeight( + sourceModel()->data(left).toMap()["sipAddress"].toString() + ) > computeStringWeight( + sourceModel()->data(right).toMap()["sipAddress"].toString() + ); +} + +int UnregisteredSipAddressesProxyModel::computeStringWeight (const QString &string) const { + int index = -1; + int offset = -1; + + while ((index = filterRegExp().indexIn(string, index + 1)) != -1) { + int tmp_offset = index - string.lastIndexOf(m_search_separators, index) - 1; + if ((tmp_offset != -1 && tmp_offset < offset) || offset == -1) + if ((offset = tmp_offset) == 0) break; + } + + switch (offset) { + case -1: return 0; + case 0: return WEIGHT_POS_0; + case 1: return WEIGHT_POS_1; + case 2: return WEIGHT_POS_2; + case 3: return WEIGHT_POS_3; + default: break; + } + + return WEIGHT_POS_OTHER; +} diff --git a/tests/src/components/sip-addresses/UnregisteredSipAddressesProxyModel.hpp b/tests/src/components/sip-addresses/UnregisteredSipAddressesProxyModel.hpp index edd521c51..1173ecc33 100644 --- a/tests/src/components/sip-addresses/UnregisteredSipAddressesProxyModel.hpp +++ b/tests/src/components/sip-addresses/UnregisteredSipAddressesProxyModel.hpp @@ -7,6 +7,25 @@ class UnregisteredSipAddressesProxyModel : public QSortFilterProxyModel { Q_OBJECT; + +public: + UnregisteredSipAddressesProxyModel (QObject *parent = Q_NULLPTR); + ~UnregisteredSipAddressesProxyModel () = default; + +public slots: + void setFilter (const QString &pattern) { + setFilterFixedString(pattern); + invalidate(); + } + +protected: + bool filterAcceptsRow (int source_row, const QModelIndex &source_parent) const override; + bool lessThan (const QModelIndex &left, const QModelIndex &right) const override; + +private: + int computeStringWeight (const QString &string) const; + + static const QRegExp m_search_separators; }; #endif // UNREGISTERED_SIP_ADDRESSES_PROXY_MODEL_H_ diff --git a/tests/src/components/smart-search-bar/SmartSearchBarModel.cpp b/tests/src/components/smart-search-bar/SmartSearchBarModel.cpp new file mode 100644 index 000000000..b117868f6 --- /dev/null +++ b/tests/src/components/smart-search-bar/SmartSearchBarModel.cpp @@ -0,0 +1,31 @@ +#include "SmartSearchBarModel.hpp" + +// ============================================================================= + +int SmartSearchBarModel::rowCount (const QModelIndex &) const { + return m_contacts.rowCount() + m_sip_addresses.rowCount(); +} + +QHash SmartSearchBarModel::roleNames () const { + QHash roles; + roles[Qt::DisplayRole] = "$entry"; + return roles; +} + +QVariant SmartSearchBarModel::data (const QModelIndex &index, int role) const { + int row = index.row(); + int n_contacts = m_contacts.rowCount(); + int n_sip_addresses = m_sip_addresses.rowCount(); + + if (row < 0 || row >= n_contacts + n_sip_addresses) + return QVariant(); + + if (role == Qt::DisplayRole) { + if (row < n_contacts) + return QVariant::fromValue(m_contacts.data(m_contacts.index(row, 0), role)); + + return QVariant::fromValue(m_sip_addresses.data(m_sip_addresses.index(row - n_contacts, 0), role)); + } + + return QVariant(); +} diff --git a/tests/src/components/smart-search-bar/SmartSearchBarModel.hpp b/tests/src/components/smart-search-bar/SmartSearchBarModel.hpp new file mode 100644 index 000000000..c0659a53d --- /dev/null +++ b/tests/src/components/smart-search-bar/SmartSearchBarModel.hpp @@ -0,0 +1,29 @@ +#ifndef SMART_SEARCH_BAR_MODEL_H_ +#define SMART_SEARCH_BAR_MODEL_H_ + +#include + +#include "../contacts/ContactsListProxyModel.hpp" +#include "../sip-addresses/UnregisteredSipAddressesProxyModel.hpp" + +// ============================================================================= + +class SmartSearchBarModel : public QAbstractListModel { + Q_OBJECT; + +public: + SmartSearchBarModel (QObject *parent = Q_NULLPTR) : QAbstractListModel(parent) {} + + ~SmartSearchBarModel () = default; + + int rowCount (const QModelIndex &index = QModelIndex()) const override; + + QHash roleNames () const override; + QVariant data (const QModelIndex &index, int role) const override; + +private: + ContactsListProxyModel m_contacts; + UnregisteredSipAddressesProxyModel m_sip_addresses; +}; + +#endif // SMART_SEARCH_BAR_MODEL_H_ diff --git a/tests/src/utils.hpp b/tests/src/utils.hpp index 65b93705a..ec41849b0 100644 --- a/tests/src/utils.hpp +++ b/tests/src/utils.hpp @@ -3,7 +3,7 @@ #include -// =================================================================== +// ============================================================================= namespace Utils { inline QString linphoneStringToQString (const std::string &string) { diff --git a/tests/ui/modules/Common/Popup/DropDownDynamicMenu.qml b/tests/ui/modules/Common/Popup/DropDownDynamicMenu.qml index 5e5a51263..1b90b241c 100644 --- a/tests/ui/modules/Common/Popup/DropDownDynamicMenu.qml +++ b/tests/ui/modules/Common/Popup/DropDownDynamicMenu.qml @@ -1,8 +1,8 @@ import Utils 1.0 -// =================================================================== +// ============================================================================= // Menu which supports `ListView`. -// =================================================================== +// ============================================================================= AbstractDropDownMenu { // Can be computed, but for performance usage, it must be given @@ -14,10 +14,7 @@ AbstractDropDownMenu { var list = _content[0] Utils.assert(list != null, 'No list found.') - Utils.assert( - Utils.qmlTypeof(list, 'QQuickListView'), - 'No list view parameter.' - ) + Utils.assert(Utils.qmlTypeof(list, 'QQuickListView'), 'No list view parameter.') var height = list.count * entryHeight diff --git a/tests/ui/modules/Common/SearchBox.qml b/tests/ui/modules/Common/SearchBox.qml index 54778523a..e69fad060 100644 --- a/tests/ui/modules/Common/SearchBox.qml +++ b/tests/ui/modules/Common/SearchBox.qml @@ -4,10 +4,10 @@ import Common 1.0 import Common.Styles 1.0 import Utils 1.0 -// =================================================================== +// ============================================================================= // A reusable search input which display a entries model in a menu. // Each entry can be filtered with the search input. -// =================================================================== +// ============================================================================= Item { id: searchBox @@ -16,8 +16,7 @@ Item { property alias entryHeight: menu.entryHeight property alias maxMenuHeight: menu.maxMenuHeight - // This property must implement `setFilterFixedString` and/or - // `invalidate` functions. + // This property must implement `setFilter` function. property alias model: list.model property alias placeholderText: searchField.placeholderText @@ -27,7 +26,7 @@ Item { signal menuClosed signal menuOpened - // ----------------------------------------------------------------- + // --------------------------------------------------------------------------- function hideMenu () { if (!_isOpen) { @@ -47,17 +46,14 @@ Item { function _filter (text) { Utils.assert( - model.setFilterFixedString != null, - '`model.setFilterFixedString` must be defined.' + model.setFilter != null, + '`model.setFilter` must be defined.' ) - model.setFilterFixedString(text) - if (model.invalidate) { - model.invalidate() - } + model.setFilter(text) } - // ----------------------------------------------------------------- + // --------------------------------------------------------------------------- implicitHeight: searchField.height @@ -112,7 +108,7 @@ Item { } } - // ----------------------------------------------------------------- + // --------------------------------------------------------------------------- states: State { name: 'opened' diff --git a/tests/ui/modules/Linphone/SmartSearchBar.qml b/tests/ui/modules/Linphone/SmartSearchBar.qml index 74e16f5be..c327a3a59 100644 --- a/tests/ui/modules/Linphone/SmartSearchBar.qml +++ b/tests/ui/modules/Linphone/SmartSearchBar.qml @@ -1,25 +1,112 @@ +import QtQuick 2.7 +import QtQuick.Layouts 1.3 + import Common 1.0 import Linphone 1.0 +import LinphoneUtils 1.0 -// =================================================================== +// ============================================================================= SearchBox { id: searchBox - delegate: Contact { - // contact: $contact + delegate: Rectangle { + id: searchBoxEntry + width: parent.width + height: searchBox.entryHeight + color: '#FFFFFF' - actions: [ - ActionButton { - icon: 'call' - onClicked: CallsWindow.show() - }, + Rectangle { + id: indicator - ActionButton { - icon: 'video_call' - onClicked: CallsWindow.show() + anchors.left: parent.left + color: 'transparent' + height: parent.height + width: 5 + } + + MouseArea { + id: mouseArea + + anchors.fill: parent + hoverEnabled: true + + RowLayout { + anchors { + fill: parent + leftMargin: 22 + rightMargin: 10 + } + spacing: 15 + + // --------------------------------------------------------------------- + // Contact or address info + // --------------------------------------------------------------------- + + Avatar { + id: avatar + Layout.preferredHeight: 30 + Layout.preferredWidth: 30 + image: $entry.vcard && $entry.vcard.avatar + presenceLevel: $entry.presenceLevel != null ? $entry.presenceLevel : -1 + username: LinphoneUtils.getContactUsername($entry.sipAddress || $entry) + } + + Text { + Layout.fillWidth: true + color: '#4B5964' + elide: Text.ElideRight + + font { + bold: true + pointSize: 9 + } + + text: $entry.vcard ? $entry.vcard.username : $entry.sipAddress + } + + // --------------------------------------------------------------------- + // Actions + // --------------------------------------------------------------------- + + ActionBar { + iconSize: 36 + + ActionButton { + icon: 'video_call' + onClicked: CallsWindow.show() + } + + ActionButton { + icon: 'call' + onClicked: CallsWindow.show() + } + + ActionButton { + icon: 'chat' + onClicked: window.setView('Conversation', { + sipAddress: $entry.sipAddress || $entry.vcard.sipAddresses[0] // FIXME: Display menu if many addresses. + }) + } + } } - ] + } + + // ------------------------------------------------------------------------- + + states: State { + when: mouseArea.containsMouse + + PropertyChanges { + color: '#D0D8DE' + target: searchBoxEntry + } + + PropertyChanges { + color: '#FF5E00' + target: indicator + } + } } } diff --git a/tests/ui/views/App/MainWindow/MainWindow.qml b/tests/ui/views/App/MainWindow/MainWindow.qml index 54b697294..0504e89b7 100644 --- a/tests/ui/views/App/MainWindow/MainWindow.qml +++ b/tests/ui/views/App/MainWindow/MainWindow.qml @@ -89,8 +89,7 @@ ApplicationWindow { maxMenuHeight: MainWindowStyle.searchBox.maxHeight placeholderText: qsTr('mainSearchBarPlaceholder') - contactsModel: ContactsListProxyModel {} - othersSipAddresses: UnregisteredSipAddressesProxyModel {} + model: SmartSearchBarModel } } }