1 /***************************************************************************
2 qgslocatormodel.cpp
3 --------------------
4 begin : May 2017
5 copyright : (C) 2017 by Nyall Dawson
6 email : nyall dot dawson at gmail dot com
7 ***************************************************************************/
8
9 /***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17
18 #include <QFont>
19
20 #include "qgslocatormodel.h"
21 #include "qgslocator.h"
22 #include "qgsapplication.h"
23 #include "qgslogger.h"
24
25
26 //
27 // QgsLocatorModel
28 //
29
QgsLocatorModel(QObject * parent)30 QgsLocatorModel::QgsLocatorModel( QObject *parent )
31 : QAbstractTableModel( parent )
32 {
33 mDeferredClearTimer.setInterval( 100 );
34 mDeferredClearTimer.setSingleShot( true );
35 connect( &mDeferredClearTimer, &QTimer::timeout, this, &QgsLocatorModel::clear );
36 }
37
clear()38 void QgsLocatorModel::clear()
39 {
40 mDeferredClearTimer.stop();
41 mDeferredClear = false;
42
43 beginResetModel();
44 mResults.clear();
45 mFoundResultsFromFilterNames.clear();
46 mFoundResultsFilterGroups.clear();
47 endResetModel();
48 }
49
deferredClear()50 void QgsLocatorModel::deferredClear()
51 {
52 mDeferredClear = true;
53 mDeferredClearTimer.start();
54 }
55
rowCount(const QModelIndex &) const56 int QgsLocatorModel::rowCount( const QModelIndex & ) const
57 {
58 return mResults.size();
59 }
60
columnCount(const QModelIndex &) const61 int QgsLocatorModel::columnCount( const QModelIndex & ) const
62 {
63 return 2;
64 }
65
data(const QModelIndex & index,int role) const66 QVariant QgsLocatorModel::data( const QModelIndex &index, int role ) const
67 {
68 if ( !index.isValid() || index.row() < 0 || index.column() < 0 ||
69 index.row() >= rowCount( QModelIndex() ) || index.column() >= columnCount( QModelIndex() ) )
70 return QVariant();
71
72 switch ( role )
73 {
74 case Qt::DisplayRole:
75 case Qt::EditRole:
76 {
77 switch ( index.column() )
78 {
79 case Name:
80 if ( !mResults.at( index.row() ).filter )
81 return mResults.at( index.row() ).result.displayString;
82 else if ( mResults.at( index.row() ).filter && mResults.at( index.row() ).groupSorting == 0 )
83 return mResults.at( index.row() ).filterTitle;
84 else
85 {
86 QString groupTitle = mResults.at( index.row() ).groupTitle;
87 groupTitle.prepend( " " );
88 return groupTitle;
89 }
90 case Description:
91 if ( !mResults.at( index.row() ).filter )
92 return mResults.at( index.row() ).result.description;
93 else
94 return QVariant();
95 }
96 break;
97 }
98
99 case Qt::FontRole:
100 if ( index.column() == Name && !mResults.at( index.row() ).groupTitle.isEmpty() )
101 {
102 QFont font;
103 font.setItalic( true );
104 return font;
105 }
106 else
107 {
108 return QVariant();
109 }
110 break;
111
112 case Qt::DecorationRole:
113 switch ( index.column() )
114 {
115 case Name:
116 if ( !mResults.at( index.row() ).filter )
117 {
118 QIcon icon = mResults.at( index.row() ).result.icon;
119 if ( !icon.isNull() )
120 return icon;
121 return QgsApplication::getThemeIcon( QStringLiteral( "/search.svg" ) );
122 }
123 else
124 return QVariant();
125 case Description:
126 return QVariant();
127 }
128 break;
129
130 case ResultDataRole:
131 if ( !mResults.at( index.row() ).filter )
132 return QVariant::fromValue( mResults.at( index.row() ).result );
133 else
134 return QVariant();
135
136 case ResultTypeRole:
137 // 0 for filter title, the group otherwise, 9999 if no group
138 return mResults.at( index.row() ).groupSorting;
139
140 case ResultScoreRole:
141 if ( mResults.at( index.row() ).filter )
142 return 0;
143 else
144 return ( mResults.at( index.row() ).result.score );
145
146 case ResultFilterPriorityRole:
147 if ( !mResults.at( index.row() ).filter )
148 return mResults.at( index.row() ).result.filter->priority();
149 else
150 return mResults.at( index.row() ).filter->priority();
151
152 case ResultFilterNameRole:
153 if ( !mResults.at( index.row() ).filter )
154 return mResults.at( index.row() ).result.filter->displayName();
155 else
156 return mResults.at( index.row() ).filterTitle;
157
158 case ResultFilterGroupSortingRole:
159 if ( mResults.at( index.row() ).groupTitle.isEmpty() )
160 return 1;
161 else
162 return 0;
163
164 case ResultActionsRole:
165 return QVariant::fromValue( mResults.at( index.row() ).result.actions );
166 }
167
168 return QVariant();
169 }
170
flags(const QModelIndex & index) const171 Qt::ItemFlags QgsLocatorModel::flags( const QModelIndex &index ) const
172 {
173 if ( !index.isValid() || index.row() < 0 || index.column() < 0 ||
174 index.row() >= rowCount( QModelIndex() ) || index.column() >= columnCount( QModelIndex() ) )
175 return QAbstractTableModel::flags( index );
176
177 Qt::ItemFlags flags = QAbstractTableModel::flags( index );
178 if ( mResults.at( index.row() ).filter )
179 {
180 flags = flags & ~( Qt::ItemIsSelectable | Qt::ItemIsEnabled );
181 }
182 return flags;
183 }
184
roleNames() const185 QHash<int, QByteArray> QgsLocatorModel::roleNames() const
186 {
187 QHash<int, QByteArray> roles;
188 roles[ResultDataRole] = "ResultData";
189 roles[ResultTypeRole] = "ResultType";
190 roles[ResultFilterPriorityRole] = "ResultFilterPriority";
191 roles[ResultScoreRole] = "ResultScore";
192 roles[ResultFilterNameRole] = "ResultFilterName";
193 roles[ResultFilterGroupSortingRole] = "ResultFilterGroupSorting";
194 roles[ResultActionsRole] = "ResultContextMenuActions";
195 roles[Qt::DisplayRole] = "Text";
196 return roles;
197 }
198
addResult(const QgsLocatorResult & result)199 void QgsLocatorModel::addResult( const QgsLocatorResult &result )
200 {
201 mDeferredClearTimer.stop();
202 if ( mDeferredClear )
203 {
204 mFoundResultsFromFilterNames.clear();
205 mFoundResultsFilterGroups.clear();
206 }
207
208 int pos = mResults.size();
209 bool addingFilter = !result.filter->displayName().isEmpty() && !mFoundResultsFromFilterNames.contains( result.filter->name() );
210 if ( addingFilter )
211 mFoundResultsFromFilterNames << result.filter->name();
212
213 bool addingGroup = !result.group.isEmpty() && ( !mFoundResultsFilterGroups.contains( result.filter )
214 || !mFoundResultsFilterGroups.value( result.filter ).contains( result.group ) );
215 if ( addingGroup )
216 {
217 if ( !mFoundResultsFilterGroups.contains( result.filter ) )
218 mFoundResultsFilterGroups[result.filter] = QStringList();
219 mFoundResultsFilterGroups[result.filter] << result.group ;
220 }
221 if ( mDeferredClear )
222 {
223 beginResetModel();
224 mResults.clear();
225 }
226 else
227 beginInsertRows( QModelIndex(), pos, pos + ( static_cast<int>( addingFilter ) + static_cast<int>( addingGroup ) ) );
228
229 if ( addingFilter )
230 {
231 Entry entry;
232 entry.filterTitle = result.filter->displayName();
233 entry.filter = result.filter;
234 mResults << entry;
235 }
236 if ( addingGroup )
237 {
238 Entry entry;
239 entry.filterTitle = result.filter->displayName();
240 entry.groupTitle = result.group;
241 // the sorting of groups will be achieved by order of adding groups
242 // this could be customized by adding the extra info to QgsLocatorResult
243 entry.groupSorting = mFoundResultsFilterGroups[result.filter].count();
244 entry.filter = result.filter;
245 mResults << entry;
246 }
247 Entry entry;
248 entry.result = result;
249 // keep the group title empty to allow differecing group title from results
250 entry.groupSorting = result.group.isEmpty() ? NoGroup : mFoundResultsFilterGroups[result.filter].indexOf( result.group ) + 1;
251 mResults << entry;
252
253 if ( mDeferredClear )
254 endResetModel();
255 else
256 endInsertRows();
257
258 mDeferredClear = false;
259 }
260
261
262 //
263 // QgsLocatorAutomaticModel
264 //
265
QgsLocatorAutomaticModel(QgsLocator * locator)266 QgsLocatorAutomaticModel::QgsLocatorAutomaticModel( QgsLocator *locator )
267 : QgsLocatorModel( locator )
268 , mLocator( locator )
269 {
270 Q_ASSERT( mLocator );
271 connect( mLocator, &QgsLocator::foundResult, this, &QgsLocatorAutomaticModel::addResult );
272 connect( mLocator, &QgsLocator::finished, this, &QgsLocatorAutomaticModel::searchFinished );
273 }
274
locator()275 QgsLocator *QgsLocatorAutomaticModel::locator()
276 {
277 return mLocator;
278 }
279
search(const QString & string)280 void QgsLocatorAutomaticModel::search( const QString &string )
281 {
282 if ( mLocator->isRunning() )
283 {
284 // can't do anything while a query is running, and can't block
285 // here waiting for the current query to cancel
286 // so we queue up this string until cancel has happened
287 mLocator->cancelWithoutBlocking();
288 mNextRequestedString = string;
289 mHasQueuedRequest = true;
290 return;
291 }
292 else
293 {
294 deferredClear();
295 mLocator->fetchResults( string, createContext() );
296 }
297 }
298
createContext()299 QgsLocatorContext QgsLocatorAutomaticModel::createContext()
300 {
301 return QgsLocatorContext();
302 }
303
searchFinished()304 void QgsLocatorAutomaticModel::searchFinished()
305 {
306 if ( mHasQueuedRequest )
307 {
308 // a queued request was waiting for this - run the queued search now
309 QString nextSearch = mNextRequestedString;
310 mNextRequestedString.clear();
311 mHasQueuedRequest = false;
312 search( nextSearch );
313 }
314 }
315
316
317
318
319
320 //
321 // QgsLocatorProxyModel
322 //
323
QgsLocatorProxyModel(QObject * parent)324 QgsLocatorProxyModel::QgsLocatorProxyModel( QObject *parent )
325 : QSortFilterProxyModel( parent )
326 {
327 setDynamicSortFilter( true );
328 setSortLocaleAware( true );
329 setFilterCaseSensitivity( Qt::CaseInsensitive );
330 sort( 0 );
331 }
332
lessThan(const QModelIndex & left,const QModelIndex & right) const333 bool QgsLocatorProxyModel::lessThan( const QModelIndex &left, const QModelIndex &right ) const
334 {
335 // first go by filter priority
336 int leftFilterPriority = sourceModel()->data( left, QgsLocatorModel::ResultFilterPriorityRole ).toInt();
337 int rightFilterPriority = sourceModel()->data( right, QgsLocatorModel::ResultFilterPriorityRole ).toInt();
338 if ( leftFilterPriority != rightFilterPriority )
339 return leftFilterPriority < rightFilterPriority;
340
341 // then filter name
342 QString leftFilter = sourceModel()->data( left, QgsLocatorModel::ResultFilterNameRole ).toString();
343 QString rightFilter = sourceModel()->data( right, QgsLocatorModel::ResultFilterNameRole ).toString();
344 if ( leftFilter != rightFilter )
345 return QString::localeAwareCompare( leftFilter, rightFilter ) < 0;
346
347 // then make sure filter title or group appears before filter's results
348 int leftTypeRole = sourceModel()->data( left, QgsLocatorModel::ResultTypeRole ).toInt();
349 int rightTypeRole = sourceModel()->data( right, QgsLocatorModel::ResultTypeRole ).toInt();
350 if ( leftTypeRole != rightTypeRole )
351 return leftTypeRole < rightTypeRole;
352
353 // make sure group title are above
354 int leftGroupRole = sourceModel()->data( left, QgsLocatorModel::ResultFilterGroupSortingRole ).toInt();
355 int rightGroupRole = sourceModel()->data( right, QgsLocatorModel::ResultFilterGroupSortingRole ).toInt();
356 if ( leftGroupRole != rightGroupRole )
357 return leftGroupRole < rightGroupRole;
358
359 // sort filter's results by score
360 double leftScore = sourceModel()->data( left, QgsLocatorModel::ResultScoreRole ).toDouble();
361 double rightScore = sourceModel()->data( right, QgsLocatorModel::ResultScoreRole ).toDouble();
362 if ( !qgsDoubleNear( leftScore, rightScore ) )
363 return leftScore > rightScore;
364
365 // lastly sort filter's results by string
366 leftFilter = sourceModel()->data( left, Qt::DisplayRole ).toString();
367 rightFilter = sourceModel()->data( right, Qt::DisplayRole ).toString();
368 return QString::localeAwareCompare( leftFilter, rightFilter ) < 0;
369 }
370
371
372