mirror of
https://gitlab.linphone.org/BC/public/linphone-desktop.git
synced 2026-01-22 06:08:07 +00:00
feat(app): smart search can display contacts & unregistered contacts (unstable)
This commit is contained in:
parent
bed7cddf06
commit
05b99bf111
14 changed files with 259 additions and 45 deletions
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<SmartSearchBarModel>(
|
||||
"Linphone", 1, 0, "SmartSearchBarModel",
|
||||
[](QQmlEngine *, QJSEngine *) -> QObject *{
|
||||
return new SmartSearchBarModel();
|
||||
}
|
||||
);
|
||||
qmlRegisterType<Camera>("Linphone", 1, 0, "Camera");
|
||||
qmlRegisterType<ContactsListProxyModel>("Linphone", 1, 0, "ContactsListProxyModel");
|
||||
qmlRegisterType<ChatModel>("Linphone", 1, 0, "ChatModel");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ public:
|
|||
public slots:
|
||||
void setFilter (const QString &pattern) {
|
||||
setFilterFixedString(pattern);
|
||||
invalidateFilter();
|
||||
invalidate();
|
||||
}
|
||||
|
||||
protected:
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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_
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
#include "SmartSearchBarModel.hpp"
|
||||
|
||||
// =============================================================================
|
||||
|
||||
int SmartSearchBarModel::rowCount (const QModelIndex &) const {
|
||||
return m_contacts.rowCount() + m_sip_addresses.rowCount();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> SmartSearchBarModel::roleNames () const {
|
||||
QHash<int, QByteArray> 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();
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
#ifndef SMART_SEARCH_BAR_MODEL_H_
|
||||
#define SMART_SEARCH_BAR_MODEL_H_
|
||||
|
||||
#include <QAbstractListModel>
|
||||
|
||||
#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<int, QByteArray> 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_
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
#include <QString>
|
||||
|
||||
// ===================================================================
|
||||
// =============================================================================
|
||||
|
||||
namespace Utils {
|
||||
inline QString linphoneStringToQString (const std::string &string) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,8 +89,7 @@ ApplicationWindow {
|
|||
maxMenuHeight: MainWindowStyle.searchBox.maxHeight
|
||||
placeholderText: qsTr('mainSearchBarPlaceholder')
|
||||
|
||||
contactsModel: ContactsListProxyModel {}
|
||||
othersSipAddresses: UnregisteredSipAddressesProxyModel {}
|
||||
model: SmartSearchBarModel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue