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