1 /* ============================================================
2  *
3  * This file is a part of digiKam project
4  * https://www.digikam.org
5  *
6  * Date        : 2010-06-01
7  * Description : A widget to search for places.
8  *
9  * Copyright (C) 2010-2021 by Gilles Caulier <caulier dot gilles at gmail dot com>
10  * Copyright (C) 2010-2011 by Michael G. Hansen <mike at mghansen dot de>
11  *
12  * This program is free software; you can redistribute it
13  * and/or modify it under the terms of the GNU General
14  * Public License as published by the Free Software Foundation;
15  * either version 2, or (at your option)
16  * any later version.
17  *
18  * This program is distributed in the hope that it will be useful,
19  * but WITHOUT ANY WARRANTY; without even the implied warranty of
20  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21  * GNU General Public License for more details.
22  *
23  * ============================================================ */
24 
25 #include "searchresultmodel.h"
26 
27 // Qt includes
28 
29 #include <QContextMenuEvent>
30 #include <QPainter>
31 #include <QAction>
32 #include <QStandardPaths>
33 
34 // local includes
35 
36 #include "searchbackend.h"
37 #include "gpscommon.h"
38 #include "gpsundocommand.h"
39 #include "gpsitemmodel.h"
40 
41 namespace DigikamGenericGeolocationEditPlugin
42 {
43 
RowRangeLessThan(const QPair<int,int> & a,const QPair<int,int> & b)44 static bool RowRangeLessThan(const QPair<int, int>& a, const QPair<int, int>& b)
45 {
46     return (a.first < b.first);
47 }
48 
49 class Q_DECL_HIDDEN SearchResultModel::Private
50 {
51 public:
52 
Private()53     explicit Private()
54     {
55         markerNormalUrl   = QUrl::fromLocalFile(QStandardPaths::locate(QStandardPaths::GenericDataLocation,
56                                                 QLatin1String("digikam/geolocationedit/searchmarker-normal.png")));
57         markerNormal      = QPixmap(markerNormalUrl.toLocalFile());
58         markerSelectedUrl = QUrl::fromLocalFile(QStandardPaths::locate(QStandardPaths::GenericDataLocation,
59                                                 QLatin1String("digikam/geolocationedit/searchmarker-selected.png")));
60         markerSelected    = QPixmap(markerSelectedUrl.toLocalFile());
61         selectionModel    = nullptr;
62     }
63 
64     QList<SearchResultModel::SearchResultItem> searchResults;
65     QUrl                                       markerNormalUrl;
66     QUrl                                       markerSelectedUrl;
67     QPixmap                                    markerNormal;
68     QPixmap                                    markerSelected;
69     QItemSelectionModel*                       selectionModel;
70 };
71 
SearchResultModel(QObject * const parent)72 SearchResultModel::SearchResultModel(QObject* const parent)
73     : QAbstractItemModel(parent),
74       d                 (new Private())
75 {
76 }
77 
~SearchResultModel()78 SearchResultModel::~SearchResultModel()
79 {
80     delete d;
81 }
82 
columnCount(const QModelIndex & parent) const83 int SearchResultModel::columnCount(const QModelIndex& parent) const
84 {
85     Q_UNUSED(parent)
86 
87     return 1;
88 }
89 
setData(const QModelIndex & index,const QVariant & value,int role)90 bool SearchResultModel::setData(const QModelIndex& index, const QVariant& value, int role)
91 {
92     Q_UNUSED(index)
93     Q_UNUSED(value)
94     Q_UNUSED(role)
95 
96     return false;
97 }
98 
data(const QModelIndex & index,int role) const99 QVariant SearchResultModel::data(const QModelIndex& index, int role) const
100 {
101     const int rowNumber = index.row();
102 
103     if ((rowNumber < 0) || (rowNumber >= d->searchResults.count()))
104     {
105         return QVariant();
106     }
107 
108     const int columnNumber = index.column();
109 
110     if (columnNumber == 0)
111     {
112         switch (role)
113         {
114             case Qt::DisplayRole:
115             {
116                 return d->searchResults.at(rowNumber).result.name;
117             }
118 
119             case Qt::DecorationRole:
120             {
121                 QPixmap markerIcon;
122                 getMarkerIcon(index, nullptr, nullptr, &markerIcon, nullptr);
123                 return markerIcon;
124             }
125 
126             default:
127             {
128                 return QVariant();
129             }
130         }
131     }
132 
133     return QVariant();
134 }
135 
index(int row,int column,const QModelIndex & parent) const136 QModelIndex SearchResultModel::index(int row, int column, const QModelIndex& parent) const
137 {
138     if (parent.isValid())
139     {
140         // there are no child items, only top level items
141 
142         return QModelIndex();
143     }
144 
145     if ((column < 0) || (column >= 1) || (row < 0) || (row >= d->searchResults.count()))
146     {
147         return QModelIndex();
148     }
149 
150     return createIndex(row, column, (void*)nullptr);
151 }
152 
parent(const QModelIndex & index) const153 QModelIndex SearchResultModel::parent(const QModelIndex& index) const
154 {
155     Q_UNUSED(index)
156 
157     // we have only top level items
158 
159     return QModelIndex();
160 }
161 
rowCount(const QModelIndex & parent) const162 int SearchResultModel::rowCount(const QModelIndex& parent) const
163 {
164     if (parent.isValid())
165     {
166         return 0;
167     }
168 
169     return d->searchResults.count();
170 }
171 
setHeaderData(int section,Qt::Orientation orientation,const QVariant & value,int role)172 bool SearchResultModel::setHeaderData(int section, Qt::Orientation orientation, const QVariant& value, int role)
173 {
174     Q_UNUSED(section)
175     Q_UNUSED(orientation)
176     Q_UNUSED(value)
177     Q_UNUSED(role)
178 
179     return false;
180 }
181 
headerData(int section,Qt::Orientation orientation,int role) const182 QVariant SearchResultModel::headerData(int section, Qt::Orientation orientation, int role) const
183 {
184     Q_UNUSED(role)
185 
186     if ((section >= 1) || (orientation != Qt::Horizontal))
187     {
188         return false;
189     }
190 
191     return QVariant(QLatin1String("Name"));
192 }
193 
flags(const QModelIndex & index) const194 Qt::ItemFlags SearchResultModel::flags(const QModelIndex& index) const
195 {
196     return QAbstractItemModel::flags(index);
197 }
198 
addResults(const SearchBackend::SearchResult::List & results)199 void SearchResultModel::addResults(const SearchBackend::SearchResult::List& results)
200 {
201     // first check which items are not duplicates
202 
203     QList<int> nonDuplicates;
204 
205     for (int i = 0 ; i < results.count() ; ++i)
206     {
207         const SearchBackend::SearchResult& currentResult = results.at(i);
208         bool isDuplicate                                 = false;
209 
210         for (int j = 0 ; j < d->searchResults.count() ; ++j)
211         {
212             if (currentResult.internalId == d->searchResults.at(j).result.internalId)
213             {
214                 isDuplicate = true;
215                 break;
216             }
217         }
218 
219         if (!isDuplicate)
220         {
221             nonDuplicates << i;
222         }
223     }
224 
225     if (nonDuplicates.isEmpty())
226     {
227         return;
228     }
229 
230     beginInsertRows(QModelIndex(), d->searchResults.count(), d->searchResults.count()+nonDuplicates.count()-1);
231 
232     for (int i = 0 ; i < nonDuplicates.count() ; ++i)
233     {
234         SearchResultItem item;
235         item.result = results.at(nonDuplicates.at(i));
236         d->searchResults << item;
237     }
238 
239     endInsertRows();
240 }
241 
resultItem(const QModelIndex & index) const242 SearchResultModel::SearchResultItem SearchResultModel::resultItem(const QModelIndex& index) const
243 {
244     if (!index.isValid())
245     {
246         return SearchResultItem();
247     }
248 
249     return d->searchResults.at(index.row());
250 }
251 
getMarkerIcon(const QModelIndex & index,QPoint * const offset,QSize * const size,QPixmap * const pixmap,QUrl * const url) const252 bool SearchResultModel::getMarkerIcon(const QModelIndex& index, QPoint* const offset, QSize* const size, QPixmap* const pixmap, QUrl* const url) const
253 {
254     // determine the id of the marker
255 
256     const int markerNumber    = index.row();
257     const bool itemIsSelected = d->selectionModel ? d->selectionModel->isSelected(index) : false;
258     QPixmap markerPixmap      = itemIsSelected    ? d->markerSelected                    : d->markerNormal;
259 
260     // if the caller requests a URL and the marker will not get
261     // a special label, return a URL. Otherwise, return a pixmap.
262 
263     const bool returnViaUrl   = url && (markerNumber > 26);
264 
265     if (returnViaUrl)
266     {
267         *url = itemIsSelected ? d->markerSelectedUrl : d->markerNormalUrl;
268 
269         if (size)
270         {
271             *size = markerPixmap.size();
272         }
273     }
274     else
275     {
276         if (markerNumber <= 26)
277         {
278             const QString markerId = QChar('A' + markerNumber);
279             QPainter painter(&markerPixmap);
280             painter.setRenderHint(QPainter::Antialiasing);
281             painter.setPen(Qt::black);
282             QRect textRect(0, 2, markerPixmap.width(), markerPixmap.height());
283             painter.drawText(textRect, Qt::AlignHCenter, markerId);
284         }
285 
286         *pixmap = markerPixmap;
287     }
288 
289     if (offset)
290     {
291         *offset = QPoint(markerPixmap.width()/2, markerPixmap.height()-1);
292     }
293 
294     return true;
295 }
296 
setSelectionModel(QItemSelectionModel * const selectionModel)297 void SearchResultModel::setSelectionModel(QItemSelectionModel* const selectionModel)
298 {
299     d->selectionModel = selectionModel;
300 }
301 
clearResults()302 void SearchResultModel::clearResults()
303 {
304     beginResetModel();
305     d->searchResults.clear();
306     endResetModel();
307 }
308 
removeRowsByIndexes(const QModelIndexList & rowsList)309 void SearchResultModel::removeRowsByIndexes(const QModelIndexList& rowsList)
310 {
311     // extract the row numbers first:
312 
313     QList<int> rowNumbers;
314 
315     foreach (const QModelIndex& index, rowsList)
316     {
317         if (index.isValid())
318         {
319             rowNumbers << index.row();
320         }
321     }
322 
323     if (rowNumbers.isEmpty())
324     {
325         return;
326     }
327 
328     std::sort(rowNumbers.begin(), rowNumbers.end());
329 
330     // now delete the rows, starting with the last row:
331 
332     for (int i = rowNumbers.count()-1 ; i >= 0 ; --i)
333     {
334         const int rowNumber = rowNumbers.at(i);
335 
336         /// @todo This is very slow for several indexes, because the views update after every removal
337 
338         beginRemoveRows(QModelIndex(), rowNumber, rowNumber);
339         d->searchResults.removeAt(rowNumber);
340         endRemoveRows();
341     }
342 }
343 
removeRowsBySelection(const QItemSelection & selectionList)344 void SearchResultModel::removeRowsBySelection(const QItemSelection& selectionList)
345 {
346     // extract the row numbers first:
347 
348     QList<QPair<int, int> > rowRanges;
349 
350     foreach (const QItemSelectionRange& range, selectionList)
351     {
352         rowRanges << QPair<int, int>(range.top(), range.bottom());
353     }
354 
355     // we expect the ranges to be sorted here
356 
357     std::sort(rowRanges.begin(), rowRanges.end(), RowRangeLessThan);
358 
359     // now delete the rows, starting with the last row:
360 
361     for (int i = rowRanges.count()-1 ; i >= 0 ; --i)
362     {
363         const QPair<int, int> currentRange = rowRanges.at(i);
364 
365         /// @todo This is very slow for several indexes, because the views update after every removal
366 
367         beginRemoveRows(QModelIndex(), currentRange.first, currentRange.second);
368 
369         for (int j = currentRange.second ; j >= currentRange.first ; --j)
370         {
371             d->searchResults.removeAt(j);
372         }
373 
374         endRemoveRows();
375     }
376 }
377 
378 } // namespace DigikamGenericGeolocationEditPlugin
379