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 | |
23 | PSearchResults 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 | |
50 | PSearchResults 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 | |
83 | PSearchResults SearchResultModel::results(int index) |
84 | { |
85 | if (index<0 || index>=mSearchResults.size()) { |
86 | return PSearchResults(); |
87 | } |
88 | return mSearchResults[index]; |
89 | } |
90 | |
91 | void SearchResultModel::notifySearchResultsUpdated() |
92 | { |
93 | emit modelChanged(); |
94 | } |
95 | |
96 | SearchResultModel::SearchResultModel(QObject* parent): |
97 | QObject(parent), |
98 | mCurrentIndex(-1) |
99 | { |
100 | |
101 | } |
102 | |
103 | int SearchResultModel::currentIndex() const |
104 | { |
105 | return mCurrentIndex; |
106 | } |
107 | |
108 | int SearchResultModel::resultsCount() const |
109 | { |
110 | return mSearchResults.count(); |
111 | } |
112 | |
113 | PSearchResults SearchResultModel::currentResults() |
114 | { |
115 | return results(mCurrentIndex); |
116 | } |
117 | |
118 | void 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 | |
127 | void SearchResultModel::clear() |
128 | { |
129 | mCurrentIndex = -1; |
130 | mSearchResults.clear(); |
131 | emit modelChanged(); |
132 | } |
133 | |
134 | void SearchResultModel::removeSearchResults(int index) |
135 | { |
136 | mSearchResults.removeAt(index); |
137 | emit modelChanged(); |
138 | } |
139 | |
140 | SearchResultTreeModel::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 | |
151 | QModelIndex 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 | |
173 | QModelIndex 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 | |
195 | int 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 | |
208 | int SearchResultTreeModel::columnCount(const QModelIndex &) const |
209 | { |
210 | return 1; |
211 | } |
212 | |
213 | QVariant 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 | |
257 | SearchResultModel *SearchResultTreeModel::searchResultModel() const |
258 | { |
259 | return mSearchResultModel; |
260 | } |
261 | |
262 | bool 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 | |
290 | void SearchResultTreeModel::onResultModelChanged() |
291 | { |
292 | beginResetModel(); |
293 | endResetModel(); |
294 | } |
295 | |
296 | Qt::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 | |
305 | bool 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 | |
332 | bool SearchResultTreeModel::selectable() const |
333 | { |
334 | return mSelectable; |
335 | } |
336 | |
337 | void 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 | |
358 | SearchResultListModel::SearchResultListModel(SearchResultModel *model, QObject *parent): |
359 | QAbstractListModel(parent), |
360 | mSearchResultModel(model) |
361 | { |
362 | connect(mSearchResultModel, &SearchResultModel::modelChanged, |
363 | this, &SearchResultListModel::onResultModelChanged); |
364 | } |
365 | |
366 | int SearchResultListModel::rowCount(const QModelIndex &) const |
367 | { |
368 | return mSearchResultModel->resultsCount(); |
369 | } |
370 | |
371 | QVariant 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 | |
401 | void 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 | */ |
411 | SearchResultTreeViewDelegate::SearchResultTreeViewDelegate(PSearchResultTreeModel model, QObject *parent): |
412 | QStyledItemDelegate(parent), |
413 | mModel(model) |
414 | { |
415 | |
416 | } |
417 | |
418 | void 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 | |