1/*
2 * Copyright (C) 2020-2022 Roy Qu (royqh1979@gmail.com)
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 */
17#include "searchresultview.h"
18#include <QApplication>
19#include <QPainter>
20#include <QStyledItemDelegate>
21#include "mainwindow.h"
22
23PSearchResults SearchResultModel::addSearchResults(const QString &keyword, QSynedit::SearchOptions options, SearchFileScope scope)
24{
25 int index=-1;
26 for (int i=0;i<mSearchResults.size();i++) {
27 PSearchResults results = mSearchResults[i];
28 if (results->keyword == keyword && results->scope == scope
29 && results->searchType == SearchType::Search) {
30 index=i;
31 break;
32 }
33 }
34 if (index>=0) {
35 mSearchResults.removeAt(index);
36 }
37 if (mSearchResults.size()>=MAX_SEARCH_RESULTS) {
38 mSearchResults.pop_back();
39 }
40 PSearchResults results = std::make_shared<SearchResults>();
41 results->keyword = keyword;
42 results->options = options;
43 results->scope = scope;
44 results->searchType = SearchType::Search;
45 mSearchResults.push_front(results);
46 mCurrentIndex = 0;
47 return results;
48}
49
50PSearchResults SearchResultModel::addSearchResults(
51 const QString& keyword,
52 const QString& symbolFullname,
53 SearchFileScope scope)
54{
55 int index=-1;
56 for (int i=0;i<mSearchResults.size();i++) {
57 PSearchResults results = mSearchResults[i];
58 if (results->searchType == SearchType::FindOccurences
59 && results->scope == scope
60 && results->statementFullname == symbolFullname
61 ) {
62 index=i;
63 break;
64 }
65 }
66 if (index>=0) {
67 mSearchResults.removeAt(index);
68 }
69 if (mSearchResults.size()>=MAX_SEARCH_RESULTS) {
70 mSearchResults.pop_back();
71 }
72 PSearchResults results = std::make_shared<SearchResults>();
73 results->keyword = keyword;
74 results->statementFullname = symbolFullname;
75 results->filename = "";
76 results->searchType = SearchType::FindOccurences;
77 results->scope = scope;
78 mSearchResults.push_front(results);
79 mCurrentIndex = 0;
80 return results;
81}
82
83PSearchResults SearchResultModel::results(int index)
84{
85 if (index<0 || index>=mSearchResults.size()) {
86 return PSearchResults();
87 }
88 return mSearchResults[index];
89}
90
91void SearchResultModel::notifySearchResultsUpdated()
92{
93 emit modelChanged();
94}
95
96SearchResultModel::SearchResultModel(QObject* parent):
97 QObject(parent),
98 mCurrentIndex(-1)
99{
100
101}
102
103int SearchResultModel::currentIndex() const
104{
105 return mCurrentIndex;
106}
107
108int SearchResultModel::resultsCount() const
109{
110 return mSearchResults.count();
111}
112
113PSearchResults SearchResultModel::currentResults()
114{
115 return results(mCurrentIndex);
116}
117
118void SearchResultModel::setCurrentIndex(int index)
119{
120 if (index!=mCurrentIndex &&
121 index>=0 && index<mSearchResults.size()) {
122 mCurrentIndex = index;
123 emit currentChanged(mCurrentIndex);
124 }
125}
126
127void SearchResultModel::clear()
128{
129 mCurrentIndex = -1;
130 mSearchResults.clear();
131 emit modelChanged();
132}
133
134void SearchResultModel::removeSearchResults(int index)
135{
136 mSearchResults.removeAt(index);
137 emit modelChanged();
138}
139
140SearchResultTreeModel::SearchResultTreeModel(SearchResultModel *model, QObject *parent):
141 QAbstractItemModel(parent),
142 mSearchResultModel(model),
143 mSelectable(false)
144{
145 connect(mSearchResultModel,&SearchResultModel::currentChanged,
146 this,&SearchResultTreeModel::onResultModelChanged);
147 connect(mSearchResultModel,&SearchResultModel::modelChanged,
148 this,&SearchResultTreeModel::onResultModelChanged);
149}
150
151QModelIndex SearchResultTreeModel::index(int row, int column, const QModelIndex &parent) const
152{
153 if (!hasIndex(row,column,parent))
154 return QModelIndex();
155
156 PSearchResults results = mSearchResultModel->currentResults();
157 if (!results)
158 return QModelIndex();
159 SearchResultTreeItem *parentItem=nullptr;
160 PSearchResultTreeItem childItem;
161 if (!parent.isValid()) {
162 parentItem = nullptr;
163 childItem = results->results[row];
164 } else {
165 parentItem = static_cast<SearchResultTreeItem *>(parent.internalPointer());
166 childItem = parentItem->results[row];
167 }
168 if (childItem)
169 return createIndex(row,column,childItem.get());
170 return QModelIndex();
171}
172
173QModelIndex SearchResultTreeModel::parent(const QModelIndex &child) const
174{
175 if (!child.isValid())
176 return QModelIndex();
177 SearchResultTreeItem* item = static_cast<SearchResultTreeItem *>(child.internalPointer());
178 if (!item) {
179 return QModelIndex();
180 } else {
181 if (item->parent==nullptr)
182 return QModelIndex();
183 SearchResultTreeItem* parent = item->parent;
184 int row = -1;
185 for (int i=0;i<parent->results.count();i++) {
186 if (parent->results[i].get()==item) {
187 row = i;
188 break;
189 }
190 }
191 return createIndex(row,0,parent);
192 }
193}
194
195int SearchResultTreeModel::rowCount(const QModelIndex &parent) const
196{
197 if (!parent.isValid()){ //root
198 PSearchResults searchResults = mSearchResultModel->currentResults();
199 if (!searchResults)
200 return 0;
201 return searchResults->results.count();
202 }
203 SearchResultTreeItem* item = static_cast<SearchResultTreeItem *>(parent.internalPointer()); if (!item)
204 return 0;
205 return item->results.count();
206}
207
208int SearchResultTreeModel::columnCount(const QModelIndex &) const
209{
210 return 1;
211}
212
213QVariant SearchResultTreeModel::data(const QModelIndex &index, int role) const
214{
215 if (!index.isValid()){
216 return QVariant();
217 }
218 SearchResultTreeItem *item = static_cast<SearchResultTreeItem *>(index.internalPointer());
219 if (!item)
220 return QVariant();
221 if (role == Qt::DisplayRole) {
222
223 PSearchResults results = mSearchResultModel->currentResults();
224
225 if (!results || !index.isValid() ) {
226 // This is nothing this function is supposed to handle
227 return QVariant();
228 }
229
230 if (item->parent==nullptr) { //is filename
231 return QString("%1(%2)").arg(item->filename)
232 .arg(item->results.count());
233 } else {
234 return QString("%1 %2: %3").arg(tr("Line")).arg(item->line)
235 .arg(item->text);
236 }
237 }
238 if (role == Qt::CheckStateRole && mSelectable) {
239
240 PSearchResults results = mSearchResultModel->currentResults();
241
242 if (!results || !index.isValid() ) {
243 // This is nothing this function is supposed to handle
244 return QVariant();
245 }
246
247 if (item->parent==nullptr) { //is filename
248 return QVariant();
249 } else {
250 return (item->selected)?Qt::Checked:Qt::Unchecked;
251 }
252 }
253 return QVariant();
254
255}
256
257SearchResultModel *SearchResultTreeModel::searchResultModel() const
258{
259 return mSearchResultModel;
260}
261
262bool SearchResultTreeModel::getItemFileAndLineChar(const QModelIndex &index, QString &filename, int &line, int &startChar)
263{
264 if (!index.isValid()){
265 return false;
266 }
267 SearchResultTreeItem *item = static_cast<SearchResultTreeItem *>(index.internalPointer());
268 if (!item)
269 return false;
270
271 PSearchResults results = mSearchResultModel->currentResults();
272
273 if (!results ) {
274 // This is nothing this function is supposed to handle
275 return false;
276 }
277
278 SearchResultTreeItem *parent = item->parent;
279 if (parent==nullptr) { //is filename
280 return false;
281 } else {
282 filename = parent->filename;
283 line = item->line;
284 startChar = item->start;
285 return true;
286 }
287 return false;
288}
289
290void SearchResultTreeModel::onResultModelChanged()
291{
292 beginResetModel();
293 endResetModel();
294}
295
296Qt::ItemFlags SearchResultTreeModel::flags(const QModelIndex &) const
297{
298 Qt::ItemFlags flags=Qt::ItemIsEnabled | Qt::ItemIsSelectable;
299 if (mSelectable) {
300 flags.setFlag(Qt::ItemIsUserCheckable);
301 }
302 return flags;
303}
304
305bool SearchResultTreeModel::setData(const QModelIndex &index, const QVariant &value, int role)
306{
307 if (!index.isValid()){
308 return false;
309 }
310 SearchResultTreeItem *item = static_cast<SearchResultTreeItem *>(index.internalPointer());
311 if (!item)
312 return false;
313 if (role == Qt::CheckStateRole && mSelectable) {
314 PSearchResults results = mSearchResultModel->currentResults();
315
316 if (!results || !index.isValid() ) {
317 // This is nothing this function is supposed to handle
318 return false;
319 }
320
321 if (item->parent==nullptr) { //is filename
322 return false;
323 } else {
324 item->selected = value.toBool();
325 return true;
326 }
327 }
328 return false;
329
330}
331
332bool SearchResultTreeModel::selectable() const
333{
334 return mSelectable;
335}
336
337void SearchResultTreeModel::setSelectable(bool newSelectable)
338{
339 if (newSelectable!=mSelectable) {
340 mSelectable = newSelectable;
341 }
342 beginResetModel();
343 if (mSelectable) {
344 //select all items by default
345 PSearchResults results = mSearchResultModel->currentResults();
346 if (results) {
347 foreach (const PSearchResultTreeItem& file, results->results) {
348 file->selected = false;
349 foreach (const PSearchResultTreeItem& item, file->results) {
350 item->selected = true;
351 }
352 }
353 }
354 }
355 endResetModel();
356}
357
358SearchResultListModel::SearchResultListModel(SearchResultModel *model, QObject *parent):
359 QAbstractListModel(parent),
360 mSearchResultModel(model)
361{
362 connect(mSearchResultModel, &SearchResultModel::modelChanged,
363 this, &SearchResultListModel::onResultModelChanged);
364}
365
366int SearchResultListModel::rowCount(const QModelIndex &) const
367{
368 return mSearchResultModel->resultsCount();
369}
370
371QVariant SearchResultListModel::data(const QModelIndex &index, int role) const
372{
373 if (!index.isValid())
374 return QVariant();
375 if (role == Qt::DisplayRole) {
376 PSearchResults results = mSearchResultModel->results(index.row());
377 if (!results)
378 return QVariant();
379 if (results->searchType == SearchType::Search) {
380 switch (results->scope) {
381 case SearchFileScope::currentFile:
382 return tr("Current File:") + QString(" \"%1\"").arg(results->keyword);
383 case SearchFileScope::wholeProject:
384 return tr("Files In Project:") + QString(" \"%1\"").arg(results->keyword);
385 case SearchFileScope::openedFiles:
386 return tr("Open Files:") + QString(" \"%1\"").arg(results->keyword);
387 }
388 } else if (results->searchType == SearchType::FindOccurences) {
389 if (results->scope == SearchFileScope::currentFile) {
390 return tr("Find Usages in Current File: '%1'")
391 .arg(results->keyword);
392 } else {
393 return tr("Find Usages in Project: '%1'")
394 .arg(results->keyword);
395 }
396 }
397 }
398 return QVariant();
399}
400
401void SearchResultListModel::onResultModelChanged()
402{
403 beginResetModel();
404 endResetModel();
405}
406
407/**
408 *
409 * see https://stackoverflow.com/questions/1956542/how-to-make-item-view-render-rich-html-text-in-qt/66412883#66412883
410 */
411SearchResultTreeViewDelegate::SearchResultTreeViewDelegate(PSearchResultTreeModel model, QObject *parent):
412 QStyledItemDelegate(parent),
413 mModel(model)
414{
415
416}
417
418void SearchResultTreeViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optIn, const QModelIndex &index) const
419{
420 QStyleOptionViewItem option = optIn;
421 initStyleOption(&option,index);
422 PSearchResults results = mModel->searchResultModel()->currentResults();
423
424 if (!results || !index.isValid() ) {
425 // This is nothing this function is supposed to handle
426 return;
427 }
428
429 QStyle *style = option.widget ? option.widget->style() : QApplication::style();
430
431 // Painting item without text (this takes care of painting e.g. the highlighted for selected
432 // or hovered over items in an ItemView)
433 option.text = QString();
434 style->drawControl(QStyle::CE_ItemViewItem, &option, painter, option.widget);
435 SearchResultTreeItem* item = static_cast<SearchResultTreeItem *>(index.internalPointer());
436
437 QString fullText;
438 if (item->parent==nullptr) { //is filename
439 fullText = QString("%1(%2)").arg(item->filename)
440 .arg(item->results.count());
441 } else {
442 fullText = QString("%1 %2: %3").arg(tr("Line")).arg(item->line)
443 .arg(item->text);
444 }
445 // Figure out where to render the text in order to follow the requested alignment
446 option.text = fullText;
447 QRect textRect = style->subElementRect(QStyle::SE_ItemViewItemText, &option);
448
449 QFontMetrics metrics = option.fontMetrics;
450 int x=textRect.left();
451 int y=textRect.top() + metrics.ascent();
452 if (item->parent==nullptr) { //is filename
453 painter->drawText(x,y,fullText);
454 } else {
455 QString s = item->text.mid(0,item->start-1);
456 QString text = QString("%1 %2: %3").arg(tr("Line")).arg(item->line)
457 .arg(s);
458 painter->drawText(x,y,text);
459 x+=metrics.horizontalAdvance(text);
460 QFont font = option.font;
461 font.setBold(true);
462 text=item->text.mid(item->start-1,item->len);
463 metrics = QFontMetrics(font);
464 int width = metrics.horizontalAdvance(text);
465 QFont oldFont = painter->font();
466 QPen oldPen = painter->pen();
467 painter->setPen(qApp->palette().color(QPalette::ColorRole::HighlightedText));
468 painter->setFont(font);
469 QRect rect = textRect;
470 rect.setLeft(x);
471 rect.setWidth(width);
472 painter->fillRect(rect,qApp->palette().color(QPalette::ColorRole::Highlight));
473 painter->drawText(x,y,text);
474 metrics = QFontMetrics(font);
475 x+=width;
476 painter->setFont(oldFont);
477 painter->setPen(oldPen);
478
479 text = item->text.mid(item->start-1+item->len);
480 painter->drawText(x,y,text);
481 }
482
483
484}
485