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 "headercompletionpopup.h"
18
19#include <QCoreApplication>
20#include <QFileInfo>
21#include <QVBoxLayout>
22#include <QDebug>
23
24HeaderCompletionPopup::HeaderCompletionPopup(QWidget* parent):QWidget(parent)
25{
26 setWindowFlags(Qt::Popup);
27 mListView = new CodeCompletionListView(this);
28 mModel=new HeaderCompletionListModel(&mCompletionList);
29 mListView->setModel(mModel);
30 setLayout(new QVBoxLayout());
31 layout()->addWidget(mListView);
32 layout()->setMargin(0);
33
34 mSearchLocal = false;
35 mCurrentFile = "";
36 mPhrase = "";
37 mIgnoreCase = false;
38}
39
40HeaderCompletionPopup::~HeaderCompletionPopup()
41{
42 delete mModel;
43}
44
45void HeaderCompletionPopup::prepareSearch(const QString &phrase, const QString &fileName)
46{
47 QCursor oldCursor = cursor();
48 setCursor(Qt::WaitCursor);
49 mCurrentFile = fileName;
50 mPhrase = phrase;
51 getCompletionFor(phrase);
52 setCursor(oldCursor);
53}
54
55bool HeaderCompletionPopup::search(const QString &phrase, bool autoHideOnSingleResult)
56{
57 mPhrase = phrase;
58 if (mPhrase.isEmpty()) {
59 hide();
60 return false;
61 }
62 if(!isEnabled())
63 return false;
64
65 QCursor oldCursor = cursor();
66 setCursor(Qt::WaitCursor);
67
68 int i = mPhrase.lastIndexOf('\\');
69 if (i<0) {
70 i = mPhrase.lastIndexOf('/');
71 }
72 QString symbol = mPhrase;
73 if (i>=0) {
74 symbol = mPhrase.mid(i+1);
75 }
76
77 // filter fFullCompletionList to fCompletionList
78 filterList(symbol);
79 mModel->notifyUpdated();
80 setCursor(oldCursor);
81
82 if (!mCompletionList.isEmpty()) {
83 mListView->setCurrentIndex(mModel->index(0,0));
84 if (mCompletionList.count() == 1) {
85 // if only one suggestion and auto hide , don't show the frame
86 if (autoHideOnSingleResult)
87 return true;
88 // if only one suggestion, and is exactly the symbol to search, hide the frame (the search is over)
89 if (symbol == mCompletionList[0]->filename)
90 return true;
91 }
92 } else {
93 hide();
94 }
95 return false;
96}
97
98void HeaderCompletionPopup::setKeypressedCallback(const KeyPressedCallback &newKeypressedCallback)
99{
100 mListView->setKeypressedCallback(newKeypressedCallback);
101}
102
103void HeaderCompletionPopup::setSuggestionColor(const QColor& localColor,
104 const QColor& projectColor,
105 const QColor& systemColor,
106 const QColor& folderColor)
107{
108 mModel->setLocalColor(localColor);
109 mModel->setProjectColor(projectColor);
110 mModel->setSystemColor(systemColor);
111 mModel->setFolderColor(folderColor);
112}
113
114QString HeaderCompletionPopup::selectedFilename(bool updateUsageCount)
115{
116 if (!isEnabled())
117 return "";
118 int index = mListView->currentIndex().row();
119 PHeaderCompletionListItem item;
120 if (index>=0 && index<mCompletionList.count())
121 item=mCompletionList[index];
122 else if (mCompletionList.count()>0) {
123 item=mCompletionList.front();
124 }
125 if (item) {
126 if (updateUsageCount) {
127 item->usageCount++;
128 mHeaderUsageCounts.insert(item->fullpath,item->usageCount);
129 }
130 if (item->isFolder)
131 return item->filename+"/";
132 return item->filename;
133 }
134 return "";
135}
136
137static bool sortByUsage(const PHeaderCompletionListItem& item1,const PHeaderCompletionListItem& item2){
138 if (item1->usageCount != item2->usageCount)
139 return item1->usageCount > item2->usageCount;
140
141 if (item1->itemType != item2->itemType)
142 return item1->itemType<item2->itemType;
143
144 return item1->filename < item2->filename;
145}
146
147
148void HeaderCompletionPopup::filterList(const QString &member)
149{
150 Qt::CaseSensitivity caseSensitivity=mIgnoreCase?Qt::CaseInsensitive:Qt::CaseSensitive;
151 mCompletionList.clear();
152 if (member.isEmpty()) {
153 foreach (const PHeaderCompletionListItem& item,mFullCompletionList.values()) {
154 mCompletionList.append(item);
155 }
156 } else {
157 foreach (const PHeaderCompletionListItem& item,mFullCompletionList.values()) {
158 if (item->filename.startsWith(member, caseSensitivity)) {
159 mCompletionList.append(item);
160 }
161 }
162 }
163 std::sort(mCompletionList.begin(),mCompletionList.end(), sortByUsage);
164}
165
166
167void HeaderCompletionPopup::getCompletionFor(const QString &phrase)
168{
169 int idx = phrase.lastIndexOf('\\');
170 if (idx<0) {
171 idx = phrase.lastIndexOf('/');
172 }
173 mFullCompletionList.clear();
174 if (idx < 0) { // dont have basedir
175 if (mSearchLocal) {
176 QFileInfo fileInfo(mCurrentFile);
177 addFilesInPath(fileInfo.absolutePath(), HeaderCompletionListItemType::LocalHeader);
178 };
179
180 for (const QString& path: mParser->includePaths()) {
181 addFilesInPath(path, HeaderCompletionListItemType::ProjectHeader);
182 }
183
184 for (const QString& path: mParser->projectIncludePaths()) {
185 addFilesInPath(path, HeaderCompletionListItemType::SystemHeader);
186 }
187 } else {
188 QString current = phrase.mid(0,idx);
189 if (mSearchLocal) {
190 QFileInfo fileInfo(mCurrentFile);
191 addFilesInSubDir(fileInfo.absolutePath(),current, HeaderCompletionListItemType::LocalHeader);
192 }
193 for (const QString& path: mParser->includePaths()) {
194 addFilesInSubDir(path,current, HeaderCompletionListItemType::ProjectHeader);
195 }
196
197 for (const QString& path: mParser->projectIncludePaths()) {
198 addFilesInSubDir(path,current, HeaderCompletionListItemType::SystemHeader);
199 }
200 }
201}
202
203void HeaderCompletionPopup::addFilesInPath(const QString &path, HeaderCompletionListItemType type)
204{
205 QDir dir(path);
206 if (!dir.exists())
207 return;
208 foreach (const QFileInfo& fileInfo, dir.entryInfoList()) {
209 if (fileInfo.fileName().startsWith("."))
210 continue;
211 if (fileInfo.isDir()) {
212 addFile(dir, fileInfo, type);
213 continue;
214 }
215 QString suffix = fileInfo.suffix().toLower();
216 if (suffix == "h" || suffix == "hpp" || suffix == "") {
217 addFile(dir, fileInfo, type);
218 }
219 }
220}
221
222void HeaderCompletionPopup::addFile(const QDir& dir, const QFileInfo& fileInfo, HeaderCompletionListItemType type)
223{
224 QString fileName = fileInfo.fileName();
225 if (fileName.isEmpty())
226 return;
227 if (fileName.startsWith('.'))
228 return;
229 PHeaderCompletionListItem item = std::make_shared<HeaderCompletionListItem>();
230 item->filename = fileName;
231 item->itemType = type;
232 item->fullpath = dir.absoluteFilePath(fileName);
233 item->usageCount = mHeaderUsageCounts.value(item->fullpath,0);
234 item->isFolder = fileInfo.isDir();
235 mFullCompletionList.insert(fileName,item);
236}
237
238void HeaderCompletionPopup::addFilesInSubDir(const QString &baseDirPath, const QString &subDirName, HeaderCompletionListItemType type)
239{
240 QDir baseDir(baseDirPath);
241 QString subDirPath = baseDir.filePath(subDirName);
242 addFilesInPath(subDirPath, type);
243}
244
245bool HeaderCompletionPopup::searchLocal() const
246{
247 return mSearchLocal;
248}
249
250void HeaderCompletionPopup::setSearchLocal(bool newSearchLocal)
251{
252 mSearchLocal = newSearchLocal;
253}
254
255bool HeaderCompletionPopup::ignoreCase() const
256{
257 return mIgnoreCase;
258}
259
260void HeaderCompletionPopup::setIgnoreCase(bool newIgnoreCase)
261{
262 mIgnoreCase = newIgnoreCase;
263}
264
265const QString &HeaderCompletionPopup::phrase() const
266{
267 return mPhrase;
268}
269
270void HeaderCompletionPopup::setParser(const PCppParser &newParser)
271{
272 mParser = newParser;
273}
274
275void HeaderCompletionPopup::showEvent(QShowEvent *)
276{
277 mListView->setFocus();
278}
279
280void HeaderCompletionPopup::hideEvent(QHideEvent *)
281{
282 mCompletionList.clear();
283 mFullCompletionList.clear();
284 mParser = nullptr;
285}
286
287bool HeaderCompletionPopup::event(QEvent *event)
288{
289 bool result = QWidget::event(event);
290 switch (event->type()) {
291 case QEvent::FontChange:
292 mListView->setFont(font());
293 break;
294 default:
295 break;
296 }
297 return result;
298}
299
300HeaderCompletionListModel::HeaderCompletionListModel(const QList<PHeaderCompletionListItem> *files, QObject *parent):
301 QAbstractListModel(parent),
302 mFiles(files)
303{
304
305}
306
307int HeaderCompletionListModel::rowCount(const QModelIndex &) const
308{
309 return mFiles->count();
310}
311
312QVariant HeaderCompletionListModel::data(const QModelIndex &index, int role) const
313{
314 if (!index.isValid())
315 return QVariant();
316 if (index.row()>=mFiles->count())
317 return QVariant();
318
319 switch(role) {
320 case Qt::DisplayRole: {
321 return mFiles->at(index.row())->filename;
322 }
323 case Qt::ForegroundRole: {
324 PHeaderCompletionListItem item=mFiles->at(index.row());
325 if (item->isFolder)
326 return mFolderColor;
327 switch(item->itemType) {
328 case HeaderCompletionListItemType::LocalHeader:
329 return mLocalColor;
330 case HeaderCompletionListItemType::ProjectHeader:
331 return mProjectColor;
332 case HeaderCompletionListItemType::SystemHeader:
333 return mSystemColor;
334 }
335 }
336 break;
337 }
338 return QVariant();
339}
340
341void HeaderCompletionListModel::notifyUpdated()
342{
343 beginResetModel();
344 endResetModel();
345}
346
347void HeaderCompletionListModel::setSystemColor(const QColor &newColor)
348{
349 mSystemColor = newColor;
350}
351
352void HeaderCompletionListModel::setProjectColor(const QColor &newColor)
353{
354 mProjectColor = newColor;
355}
356
357void HeaderCompletionListModel::setFolderColor(const QColor &newFolderColor)
358{
359 mFolderColor = newFolderColor;
360}
361
362void HeaderCompletionListModel::setLocalColor(const QColor &newColor)
363{
364 mLocalColor = newColor;
365}
366