| 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 | |
| 24 | 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 | |
| 40 | HeaderCompletionPopup::() |
| 41 | { |
| 42 | delete mModel; |
| 43 | } |
| 44 | |
| 45 | void HeaderCompletionPopup::(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 | |
| 55 | bool HeaderCompletionPopup::(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 | |
| 98 | void HeaderCompletionPopup::(const KeyPressedCallback &newKeypressedCallback) |
| 99 | { |
| 100 | mListView->setKeypressedCallback(newKeypressedCallback); |
| 101 | } |
| 102 | |
| 103 | void HeaderCompletionPopup::(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 | |
| 114 | QString HeaderCompletionPopup::(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 | |
| 137 | static bool (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 | |
| 148 | void HeaderCompletionPopup::(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 | |
| 167 | void HeaderCompletionPopup::(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 | |
| 203 | void HeaderCompletionPopup::(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 | |
| 222 | void HeaderCompletionPopup::(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 | |
| 238 | void HeaderCompletionPopup::(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 | |
| 245 | bool HeaderCompletionPopup::() const |
| 246 | { |
| 247 | return mSearchLocal; |
| 248 | } |
| 249 | |
| 250 | void HeaderCompletionPopup::(bool newSearchLocal) |
| 251 | { |
| 252 | mSearchLocal = newSearchLocal; |
| 253 | } |
| 254 | |
| 255 | bool HeaderCompletionPopup::() const |
| 256 | { |
| 257 | return mIgnoreCase; |
| 258 | } |
| 259 | |
| 260 | void HeaderCompletionPopup::(bool newIgnoreCase) |
| 261 | { |
| 262 | mIgnoreCase = newIgnoreCase; |
| 263 | } |
| 264 | |
| 265 | const QString &HeaderCompletionPopup::() const |
| 266 | { |
| 267 | return mPhrase; |
| 268 | } |
| 269 | |
| 270 | void HeaderCompletionPopup::(const PCppParser &newParser) |
| 271 | { |
| 272 | mParser = newParser; |
| 273 | } |
| 274 | |
| 275 | void HeaderCompletionPopup::(QShowEvent *) |
| 276 | { |
| 277 | mListView->setFocus(); |
| 278 | } |
| 279 | |
| 280 | void HeaderCompletionPopup::(QHideEvent *) |
| 281 | { |
| 282 | mCompletionList.clear(); |
| 283 | mFullCompletionList.clear(); |
| 284 | mParser = nullptr; |
| 285 | } |
| 286 | |
| 287 | bool HeaderCompletionPopup::(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 | |
| 300 | HeaderCompletionListModel::(const QList<PHeaderCompletionListItem> *files, QObject *parent): |
| 301 | QAbstractListModel(parent), |
| 302 | mFiles(files) |
| 303 | { |
| 304 | |
| 305 | } |
| 306 | |
| 307 | int HeaderCompletionListModel::(const QModelIndex &) const |
| 308 | { |
| 309 | return mFiles->count(); |
| 310 | } |
| 311 | |
| 312 | QVariant HeaderCompletionListModel::(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 | |
| 341 | void HeaderCompletionListModel::() |
| 342 | { |
| 343 | beginResetModel(); |
| 344 | endResetModel(); |
| 345 | } |
| 346 | |
| 347 | void HeaderCompletionListModel::(const QColor &newColor) |
| 348 | { |
| 349 | mSystemColor = newColor; |
| 350 | } |
| 351 | |
| 352 | void HeaderCompletionListModel::(const QColor &newColor) |
| 353 | { |
| 354 | mProjectColor = newColor; |
| 355 | } |
| 356 | |
| 357 | void HeaderCompletionListModel::(const QColor &newFolderColor) |
| 358 | { |
| 359 | mFolderColor = newFolderColor; |
| 360 | } |
| 361 | |
| 362 | void HeaderCompletionListModel::(const QColor &newColor) |
| 363 | { |
| 364 | mLocalColor = newColor; |
| 365 | } |
| 366 | |