Akonadi Contacts

contactsfilterproxymodel.cpp
1 /*
2  This file is part of Akonadi Contact.
3 
4  SPDX-FileCopyrightText: 2009 Tobias Koenig <[email protected]>
5 
6  SPDX-License-Identifier: LGPL-2.0-or-later
7 */
8 
9 #include "contactsfilterproxymodel.h"
10 
11 #include "contactstreemodel.h"
12 
13 #include <Akonadi/EntityTreeModel>
14 #include <KContacts/Addressee>
15 #include <KContacts/ContactGroup>
16 
17 using namespace Akonadi;
18 
19 static bool contactMatchesFilter(const KContacts::Addressee &contact, const QString &filterString, ContactsFilterProxyModel::MatchFilterContactFlag flag);
20 static bool contactGroupMatchesFilter(const KContacts::ContactGroup &group, const QString &filterString);
21 
22 static QString normalize(QStringView str)
23 {
24  QString out;
25  out.reserve(str.size());
26  for (const auto c : str) {
27  // case folding
28  const auto n = c.toCaseFolded();
29 
30  // if the character has a canonical decomposition use that and skip the
31  // combining diacritic markers following it
32  // see https://en.wikipedia.org/wiki/Unicode_equivalence
33  // see https://en.wikipedia.org/wiki/Combining_character
34  if (n.decompositionTag() == QChar::Canonical) {
35  out.push_back(n.decomposition().at(0));
36  }
37  // handle compatibility compositions such as ligatures
38  // see https://en.wikipedia.org/wiki/Unicode_compatibility_characters
39  else if (n.decompositionTag() == QChar::Compat && n.isLetter() && n.script() == QChar::Script_Latin) {
40  out.append(n.decomposition());
41  } else {
42  out.push_back(n);
43  }
44  }
45  return out;
46 }
47 
48 class Akonadi::ContactsFilterProxyModelPrivate
49 {
50 public:
51  ContactsFilterProxyModelPrivate()
52  : flags({})
53  {
54  }
55 
56  QString mFilter;
58  ContactsFilterProxyModel::MatchFilterContactFlag matchFilterFlag = ContactsFilterProxyModel::MatchFilterContactFlag::All;
59  bool mExcludeVirtualCollections = false;
60 };
61 
63  : QSortFilterProxyModel(parent)
64  , d(new ContactsFilterProxyModelPrivate)
65 {
66  // contact names should be sorted correctly
67  setSortLocaleAware(true);
69 }
70 
72 
74 {
75  d->mFilter = filter;
77 }
78 
79 bool ContactsFilterProxyModel::filterAcceptsRow(int row, const QModelIndex &parent) const
80 {
81  const QModelIndex index = sourceModel()->index(row, 0, parent);
82  if (d->mExcludeVirtualCollections) {
84  if (collection.isValid() && collection.isVirtual()) {
85  return false;
86  }
87  }
88 
89  if ((d->mFilter.isEmpty()) && (!(d->flags & ContactsFilterProxyModel::HasEmail))) {
90  return true;
91  }
92  const QString filterStr = normalize(d->mFilter);
94 
95  if (item.hasPayload<KContacts::Addressee>()) {
96  const auto contact = item.payload<KContacts::Addressee>();
97  if (d->flags & ContactsFilterProxyModel::HasEmail) {
98  if (contact.emails().isEmpty()) {
99  return false;
100  }
101  }
102  if (!d->mFilter.isEmpty()) {
103  return contactMatchesFilter(contact, filterStr, d->matchFilterFlag);
104  }
105  } else {
106  if (!d->mFilter.isEmpty()) {
107  if (item.hasPayload<KContacts::ContactGroup>()) {
108  const auto group = item.payload<KContacts::ContactGroup>();
109  return contactGroupMatchesFilter(group, filterStr);
110  }
111  }
112  }
113 
114  return true;
115 }
116 
117 bool ContactsFilterProxyModel::lessThan(const QModelIndex &leftIndex, const QModelIndex &rightIndex) const
118 {
119  const QDate leftDate = leftIndex.data(ContactsTreeModel::DateRole).toDate();
120  const QDate rightDate = rightIndex.data(ContactsTreeModel::DateRole).toDate();
121  if (leftDate.isValid() && rightDate.isValid()) {
122  if (leftDate.month() < rightDate.month()) {
123  return true;
124  } else if (leftDate.month() == rightDate.month()) {
125  if (leftDate.day() < rightDate.day()) {
126  return true;
127  }
128  } else {
129  return false;
130  }
131  }
132 
133  return QSortFilterProxyModel::lessThan(leftIndex, rightIndex);
134 }
135 
136 void ContactsFilterProxyModel::setMatchFilterContactFlag(ContactsFilterProxyModel::MatchFilterContactFlag flag)
137 {
138  d->matchFilterFlag = flag;
139 }
140 
142 {
143  d->flags = flags;
144 }
145 
147 {
148  if (exclude != d->mExcludeVirtualCollections) {
149  d->mExcludeVirtualCollections = exclude;
151  }
152 }
153 
154 Qt::ItemFlags ContactsFilterProxyModel::flags(const QModelIndex &index) const
155 {
156  if (!index.isValid()) {
157  // Don't crash
158  return Qt::NoItemFlags;
159  }
161  if (collection.isValid()) {
163  }
165 }
166 
167 static bool addressMatchesFilter(const KContacts::Address &address, const QString &filterString)
168 {
169  if (address.street().contains(filterString, Qt::CaseInsensitive)) {
170  return true;
171  }
172 
173  if (address.locality().contains(filterString, Qt::CaseInsensitive)) {
174  return true;
175  }
176 
177  if (address.region().contains(filterString, Qt::CaseInsensitive)) {
178  return true;
179  }
180 
181  if (address.postalCode().contains(filterString, Qt::CaseInsensitive)) {
182  return true;
183  }
184 
185  if (address.country().contains(filterString, Qt::CaseInsensitive)) {
186  return true;
187  }
188 
189  if (address.label().contains(filterString, Qt::CaseInsensitive)) {
190  return true;
191  }
192 
193  if (address.postOfficeBox().contains(filterString, Qt::CaseInsensitive)) {
194  return true;
195  }
196 
197  return false;
198 }
199 
200 static bool contactMatchesFilter(const KContacts::Addressee &contact, const QString &filterString, ContactsFilterProxyModel::MatchFilterContactFlag flag)
201 {
202  if (normalize(contact.assembledName()).contains(filterString, Qt::CaseInsensitive)) {
203  return true;
204  }
205 
206  if (normalize(contact.formattedName()).contains(filterString, Qt::CaseInsensitive)) {
207  return true;
208  }
209 
210  if (normalize(contact.nickName()).contains(filterString, Qt::CaseInsensitive)) {
211  return true;
212  }
213 
214  int count = 0;
215  if (flag == ContactsFilterProxyModel::MatchFilterContactFlag::All) {
216  if (contact.birthday().toString().contains(filterString, Qt::CaseInsensitive)) {
217  return true;
218  }
219  const KContacts::Address::List addresses = contact.addresses();
220  count = addresses.count();
221  for (int i = 0; i < count; ++i) {
222  if (addressMatchesFilter(addresses.at(i), filterString)) {
223  return true;
224  }
225  }
226 
227  const KContacts::PhoneNumber::List phoneNumbers = contact.phoneNumbers();
228  count = phoneNumbers.count();
229  for (int i = 0; i < count; ++i) {
230  if (phoneNumbers.at(i).number().contains(filterString, Qt::CaseInsensitive)) {
231  return true;
232  }
233  }
234  }
235 
236  const QStringList emails = contact.emails();
237  count = emails.count();
238  for (int i = 0; i < count; ++i) {
239  if (normalize(emails.at(i)).contains(filterString, Qt::CaseInsensitive)) {
240  return true;
241  }
242  }
243 
244  if (flag == ContactsFilterProxyModel::MatchFilterContactFlag::All) {
245  const QStringList categories = contact.categories();
246  count = categories.count();
247  for (int i = 0; i < count; ++i) {
248  if (categories.at(i).contains(filterString, Qt::CaseInsensitive)) {
249  return true;
250  }
251  }
252  if (contact.mailer().contains(filterString, Qt::CaseInsensitive)) {
253  return true;
254  }
255 
256  if (normalize(contact.title()).contains(filterString, Qt::CaseInsensitive)) {
257  return true;
258  }
259 
260  if (contact.role().contains(filterString, Qt::CaseInsensitive)) {
261  return true;
262  }
263 
264  if (normalize(contact.organization()).contains(filterString, Qt::CaseInsensitive)) {
265  return true;
266  }
267 
268  if (normalize(contact.department()).contains(filterString, Qt::CaseInsensitive)) {
269  return true;
270  }
271 
272  if (normalize(contact.note()).contains(filterString, Qt::CaseInsensitive)) {
273  return true;
274  }
275 
276  if (contact.url().url().url().contains(filterString, Qt::CaseInsensitive)) {
277  return true;
278  }
279 
280  const QStringList customs = contact.customs();
281  count = customs.count();
282  for (int i = 0; i < count; ++i) {
283  if (customs.at(i).contains(filterString, Qt::CaseInsensitive)) {
284  return true;
285  }
286  }
287  }
288 
289  return false;
290 }
291 
292 bool contactGroupMatchesFilter(const KContacts::ContactGroup &group, const QString &filterString)
293 {
294  if (normalize(group.name()).contains(filterString, Qt::CaseInsensitive)) {
295  return true;
296  }
297 
298  const int count = group.dataCount();
299  for (int i = 0; i < count; ++i) {
300  if (normalize(group.data(i).name()).contains(filterString, Qt::CaseInsensitive)) {
301  return true;
302  }
303  if (normalize(group.data(i).email()).contains(filterString, Qt::CaseInsensitive)) {
304  return true;
305  }
306  }
307 
308  return false;
309 }
QString assembledName() const
QString title() const
int month() const const
QString mailer() const
CaseInsensitive
QString organization() const
int count(const T &value) const const
T value() const const
QString formattedName() const
void reserve(int size)
QString normalize(QStringView str)
QString note() const
PhoneNumber::List phoneNumbers() const
@ DateRole
The QDate object for the current index.
QDate toDate() const const
qsizetype size() const const
void setFilterString(const QString &filter)
Sets the filter that is used to filter for matching contacts and contact groups.
QVariant data(int role) const const
typedef ItemFlags
ResourceLocatorUrl url() const
QDateTime birthday() const
~ContactsFilterProxyModel() override
Destroys the contacts filter proxy model.
virtual Qt::ItemFlags flags(const QModelIndex &index) const const override
QString nickName() const
void setMatchFilterContactFlag(ContactsFilterProxyModel::MatchFilterContactFlag flag)
setMatchFilterContactFlag
const T & at(int i) const const
const T & at(int i) const const
void push_back(QChar ch)
QString role() const
bool isValid() const const
void setDynamicSortFilter(bool enable)
QStringList categories() const
PostalAddress address(const QVariant &location)
Data & data(int index)
void setSortLocaleAware(bool on)
bool isValid() const const
virtual bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const const
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const const override
ContactsFilterProxyModel(QObject *parent=nullptr)
Creates a new contacts filter proxy model.
void setExcludeVirtualCollections(bool exclude)
Sets whether we want virtual collections to be filtered or not.
QString name() const
int count(const T &value) const const
Address::List addresses() const
virtual bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const const
QStringList customs() const
void setFilterFlags(ContactsFilterProxyModel::FilterFlags flags)
Sets the filter flags.
T payload() const
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
QString toString(Qt::DateFormat format) const const
QStringList emails() const
QString department() const
QObject * parent() const const
QString & append(QChar ch)
int day() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Sat Apr 1 2023 04:09:04 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.