File indexing completed on 2024-04-28 04:38:54
0001 /* 0002 SPDX-FileCopyrightText: 2010 Silvère Lestang <silvere.lestang@gmail.com> 0003 SPDX-FileCopyrightText: 2010 Julien Desgats <julien.desgats@gmail.com> 0004 0005 SPDX-License-Identifier: GPL-2.0-or-later 0006 */ 0007 0008 #include "grepoutputview.h" 0009 #include "grepoutputmodel.h" 0010 #include "grepoutputdelegate.h" 0011 #include "ui_grepoutputview.h" 0012 #include "grepviewplugin.h" 0013 #include "grepdialog.h" 0014 #include "greputil.h" 0015 #include "grepjob.h" 0016 #include "debug.h" 0017 0018 #include <interfaces/icore.h> 0019 #include <interfaces/isession.h> 0020 0021 #include <KConfigGroup> 0022 #include <KMessageBox> 0023 #include <KMessageBox_KDevCompat> 0024 #include <KColorScheme> 0025 #include <KLocalizedString> 0026 0027 #include <QAction> 0028 #include <QMenu> 0029 #include <QWidgetAction> 0030 0031 using namespace KDevelop; 0032 0033 GrepOutputViewFactory::GrepOutputViewFactory(GrepViewPlugin* plugin) 0034 : m_plugin(plugin) 0035 {} 0036 0037 QWidget* GrepOutputViewFactory::create(QWidget* parent) 0038 { 0039 return new GrepOutputView(parent, m_plugin); 0040 } 0041 0042 Qt::DockWidgetArea GrepOutputViewFactory::defaultPosition() const 0043 { 0044 return Qt::BottomDockWidgetArea; 0045 } 0046 0047 QString GrepOutputViewFactory::id() const 0048 { 0049 return QStringLiteral("org.kdevelop.GrepOutputView"); 0050 } 0051 0052 0053 const int GrepOutputView::HISTORY_SIZE = 5; 0054 0055 namespace { 0056 enum { GrepSettingsStorageItemCount = 10 }; 0057 } 0058 0059 GrepOutputView::GrepOutputView(QWidget* parent, GrepViewPlugin* plugin) 0060 : QWidget(parent) 0061 , m_next(nullptr) 0062 , m_prev(nullptr) 0063 , m_collapseAll(nullptr) 0064 , m_expandAll(nullptr) 0065 , m_refresh(nullptr) 0066 , m_clearSearchHistory(nullptr) 0067 , m_statusLabel(nullptr) 0068 , m_plugin(plugin) 0069 { 0070 Ui::GrepOutputView::setupUi(this); 0071 0072 setWindowTitle(i18nc("@title:window", "Find/Replace Output View")); 0073 setWindowIcon(QIcon::fromTheme(QStringLiteral("edit-find"), windowIcon())); 0074 0075 m_prev = new QAction(QIcon::fromTheme(QStringLiteral("go-previous")), i18nc("@action", "&Previous Item"), this); 0076 m_next = new QAction(QIcon::fromTheme(QStringLiteral("go-next")), i18nc("@action", "&Next Item"), this); 0077 /* Expand-all and collapse-all icons were added to breeze with version 5.57. We use a fallback 0078 * icon here because we support older frameworks versions and oxygen doesn't have such an icon 0079 */ 0080 m_collapseAll = new QAction(QIcon::fromTheme(QStringLiteral("collapse-all"), 0081 QIcon::fromTheme(QStringLiteral("arrow-left-double"))), i18nc("@action", "C&ollapse All"), this); 0082 m_expandAll = new QAction(QIcon::fromTheme(QStringLiteral("expand-all"), 0083 QIcon::fromTheme(QStringLiteral("arrow-right-double"))), i18nc("@action", "&Expand All"), this); 0084 updateButtonState(false); 0085 auto *separator = new QAction(this); 0086 separator->setSeparator(true); 0087 auto* newSearchAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-find")), i18nc("@action", "New &Search"), this); 0088 m_refresh = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18nc("@action", "Refresh"), this); 0089 m_refresh->setEnabled(false); 0090 m_clearSearchHistory = new QAction(QIcon::fromTheme(QStringLiteral("edit-clear-list")), i18nc("@action", "Clear Search History"), this); 0091 m_clearSearchHistory->setEnabled(false); 0092 0093 addAction(m_prev); 0094 addAction(m_next); 0095 addAction(m_collapseAll); 0096 addAction(m_expandAll); 0097 addAction(separator); 0098 addAction(newSearchAction); 0099 addAction(m_refresh); 0100 addAction(m_clearSearchHistory); 0101 0102 separator = new QAction(this); 0103 separator->setSeparator(true); 0104 addAction(separator); 0105 0106 auto *statusWidget = new QWidgetAction(this); 0107 m_statusLabel = new QLabel(this); 0108 statusWidget->setDefaultWidget(m_statusLabel); 0109 addAction(statusWidget); 0110 0111 modelSelector->setEditable(false); 0112 modelSelector->setContextMenuPolicy(Qt::CustomContextMenu); 0113 connect(modelSelector, &KComboBox::customContextMenuRequested, 0114 this, &GrepOutputView::modelSelectorContextMenu); 0115 connect(modelSelector, QOverload<int>::of(&KComboBox::currentIndexChanged), 0116 this, &GrepOutputView::changeModel); 0117 0118 resultsTreeView->setItemDelegate(GrepOutputDelegate::self()); 0119 resultsTreeView->setRootIsDecorated(false); 0120 resultsTreeView->setHeaderHidden(true); 0121 resultsTreeView->setUniformRowHeights(false); 0122 resultsTreeView->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); 0123 0124 connect(m_prev, &QAction::triggered, this, &GrepOutputView::selectPreviousItem); 0125 connect(m_next, &QAction::triggered, this, &GrepOutputView::selectNextItem); 0126 connect(m_collapseAll, &QAction::triggered, this, &GrepOutputView::collapseAllItems); 0127 connect(m_expandAll, &QAction::triggered, this, &GrepOutputView::expandAllItems); 0128 connect(applyButton, &QPushButton::clicked, this, &GrepOutputView::onApply); 0129 connect(m_refresh, &QAction::triggered, this, &GrepOutputView::refresh); 0130 connect(m_clearSearchHistory, &QAction::triggered, this, &GrepOutputView::clearSearchHistory); 0131 KConfigGroup cg = ICore::self()->activeSession()->config()->group( "GrepDialog" ); 0132 replacementCombo->addItems( cg.readEntry("LastReplacementItems", QStringList()) ); 0133 replacementCombo->setInsertPolicy(QComboBox::InsertAtTop); 0134 applyButton->setIcon(QIcon::fromTheme(QStringLiteral("dialog-ok-apply"))); 0135 0136 connect(replacementCombo, &KComboBox::editTextChanged, this, &GrepOutputView::replacementTextChanged); 0137 connect(replacementCombo, QOverload<const QString&>::of(&KComboBox::returnPressed), this, &GrepOutputView::onApply); 0138 0139 connect(newSearchAction, &QAction::triggered, this, &GrepOutputView::showDialog); 0140 0141 resultsTreeView->header()->setStretchLastSection(true); 0142 0143 resultsTreeView->header()->setStretchLastSection(true); 0144 0145 // read Find/Replace settings history 0146 const QStringList s = cg.readEntry("LastSettings", QStringList()); 0147 if (s.size() % GrepSettingsStorageItemCount != 0) { 0148 qCWarning(PLUGIN_GREPVIEW) << "Stored settings history has unexpected size:" << s; 0149 } else { 0150 m_settingsHistory.reserve(s.size() / GrepSettingsStorageItemCount); 0151 auto it = s.begin(); 0152 while (it != s.end()) { 0153 GrepJobSettings settings; 0154 settings.projectFilesOnly = ((it++)->toUInt() != 0); 0155 settings.caseSensitive = ((it++)->toUInt() != 0); 0156 settings.regexp = ((it++)->toUInt() != 0); 0157 settings.depth = (it++)->toInt(); 0158 settings.pattern = *(it++); 0159 settings.searchTemplate = *(it++); 0160 settings.replacementTemplate = *(it++); 0161 settings.files = *(it++); 0162 settings.exclude = *(it++); 0163 settings.searchPaths = *(it++); 0164 0165 settings.fromHistory = true; 0166 m_settingsHistory << settings; 0167 } 0168 } 0169 0170 // Restore the grep jobs with settings from the history without performing a search. 0171 auto* const dlg = new GrepDialog(m_plugin, this, this, false); 0172 dlg->historySearch(m_settingsHistory); 0173 0174 updateCheckable(); 0175 } 0176 0177 void GrepOutputView::replacementTextChanged() 0178 { 0179 updateCheckable(); 0180 0181 if (model()) { 0182 // see https://bugs.kde.org/show_bug.cgi?id=274902 - renewModel can trigger a call here without an active model 0183 updateApplyState(model()->index(0, 0), model()->index(0, 0)); 0184 } 0185 } 0186 0187 GrepOutputView::~GrepOutputView() 0188 { 0189 KConfigGroup cg = ICore::self()->activeSession()->config()->group( "GrepDialog" ); 0190 cg.writeEntry("LastReplacementItems", qCombo2StringList(replacementCombo, true)); 0191 QStringList settingsStrings; 0192 settingsStrings.reserve(m_settingsHistory.size() * GrepSettingsStorageItemCount); 0193 for (const GrepJobSettings& s : qAsConst(m_settingsHistory)) { 0194 settingsStrings 0195 << QString::number(s.projectFilesOnly ? 1 : 0) 0196 << QString::number(s.caseSensitive ? 1 : 0) 0197 << QString::number(s.regexp ? 1 : 0) 0198 << QString::number(s.depth) 0199 << s.pattern 0200 << s.searchTemplate 0201 << s.replacementTemplate 0202 << s.files 0203 << s.exclude 0204 << s.searchPaths; 0205 } 0206 cg.writeEntry("LastSettings", settingsStrings); 0207 emit outputViewIsClosed(); 0208 } 0209 0210 GrepOutputModel* GrepOutputView::renewModel(const GrepJobSettings& settings, const QString& description) 0211 { 0212 // clear oldest model 0213 while(modelSelector->count() >= GrepOutputView::HISTORY_SIZE) { 0214 QVariant var = modelSelector->itemData(GrepOutputView::HISTORY_SIZE - 1); 0215 qvariant_cast<QObject*>(var)->deleteLater(); 0216 modelSelector->removeItem(GrepOutputView::HISTORY_SIZE - 1); 0217 } 0218 0219 while(m_settingsHistory.count() >= GrepOutputView::HISTORY_SIZE) { 0220 m_settingsHistory.removeFirst(); 0221 } 0222 0223 replacementCombo->clearEditText(); 0224 0225 auto* newModel = new GrepOutputModel(resultsTreeView); 0226 applyButton->setEnabled(false); 0227 // text may be already present 0228 newModel->setReplacement(replacementCombo->currentText()); 0229 connect(newModel, &GrepOutputModel::rowsRemoved, 0230 this, &GrepOutputView::rowsRemoved); 0231 connect(resultsTreeView, &QTreeView::activated, newModel, &GrepOutputModel::activate); 0232 connect(replacementCombo, &KComboBox::editTextChanged, newModel, &GrepOutputModel::setReplacement); 0233 connect(newModel, &GrepOutputModel::rowsInserted, this, &GrepOutputView::expandElements); 0234 connect(newModel, &GrepOutputModel::showErrorMessage, this, &GrepOutputView::showErrorMessage); 0235 connect(m_plugin, &GrepViewPlugin::grepJobFinished, this, &GrepOutputView::updateScrollArea); 0236 0237 // appends new model to history 0238 modelSelector->insertItem(0, description, QVariant::fromValue<QObject*>(newModel)); 0239 modelSelector->setCurrentIndex(0); 0240 0241 m_settingsHistory.append(settings); 0242 0243 updateCheckable(); 0244 0245 return newModel; 0246 } 0247 0248 0249 GrepOutputModel* GrepOutputView::model() 0250 { 0251 return static_cast<GrepOutputModel*>(resultsTreeView->model()); 0252 } 0253 0254 void GrepOutputView::changeModel(int index) 0255 { 0256 if (model()) { 0257 disconnect(model(), &GrepOutputModel::showMessage, 0258 this, &GrepOutputView::showMessage); 0259 disconnect(model(), &GrepOutputModel::dataChanged, 0260 this, &GrepOutputView::updateApplyState); 0261 } 0262 0263 replacementCombo->clearEditText(); 0264 0265 //after deleting the whole search history, index is -1 0266 if(index >= 0) 0267 { 0268 QVariant var = modelSelector->itemData(index); 0269 auto *resultModel = static_cast<GrepOutputModel *>(qvariant_cast<QObject*>(var)); 0270 resultsTreeView->setModel(resultModel); 0271 resultsTreeView->expandAll(); 0272 0273 connect(model(), &GrepOutputModel::showMessage, 0274 this, &GrepOutputView::showMessage); 0275 connect(model(), &GrepOutputModel::dataChanged, 0276 this, &GrepOutputView::updateApplyState); 0277 model()->showMessageEmit(); 0278 applyButton->setEnabled(model()->hasResults() && 0279 model()->getRootItem() && 0280 model()->getRootItem()->checkState() != Qt::Unchecked && 0281 !replacementCombo->currentText().isEmpty()); 0282 if(model()->hasResults()) 0283 expandElements(QModelIndex()); 0284 else { 0285 updateButtonState(false); 0286 } 0287 } 0288 0289 updateCheckable(); 0290 updateApplyState(model()->index(0, 0), model()->index(0, 0)); 0291 m_refresh->setEnabled(true); 0292 m_clearSearchHistory->setEnabled(true); 0293 } 0294 0295 void GrepOutputView::setMessage(const QString& msg, MessageType type) 0296 { 0297 if (type == Error) { 0298 QPalette palette = m_statusLabel->palette(); 0299 KColorScheme::adjustForeground(palette, KColorScheme::NegativeText, QPalette::WindowText); 0300 m_statusLabel->setPalette(palette); 0301 } else { 0302 m_statusLabel->setPalette(QPalette()); 0303 } 0304 m_statusLabel->setText(msg); 0305 } 0306 0307 void GrepOutputView::showErrorMessage( const QString& errorMessage ) 0308 { 0309 setMessage(errorMessage, Error); 0310 } 0311 0312 void GrepOutputView::showMessage( KDevelop::IStatus* , const QString& message ) 0313 { 0314 setMessage(message, Information); 0315 } 0316 0317 void GrepOutputView::onApply() 0318 { 0319 if(model()) 0320 { 0321 Q_ASSERT(model()->rowCount()); 0322 // ask a confirmation before an empty string replacement 0323 if (replacementCombo->currentText().length() == 0 0324 && KMessageBox::questionTwoActions( 0325 this, i18n("Do you want to replace with an empty string?"), 0326 i18nc("@title:window", "Start Replacement"), 0327 KGuiItem(i18nc("@action:button", "Replace"), QStringLiteral("dialog-ok-apply")), 0328 KStandardGuiItem::cancel()) 0329 == KMessageBox::SecondaryAction) { 0330 return; 0331 } 0332 0333 setEnabled(false); 0334 model()->doReplacements(); 0335 setEnabled(true); 0336 } 0337 } 0338 0339 void GrepOutputView::showDialog() 0340 { 0341 m_plugin->showDialog(true); 0342 } 0343 0344 void GrepOutputView::refresh() 0345 { 0346 int index = modelSelector->currentIndex(); 0347 if (index >= 0) { 0348 QVariant var = modelSelector->currentData(); 0349 qvariant_cast<QObject*>(var)->deleteLater(); 0350 modelSelector->removeItem(index); 0351 0352 QVector<GrepJobSettings> refresh_history({ 0353 m_settingsHistory.takeAt(m_settingsHistory.count() - 1 - index) 0354 }); 0355 refresh_history.first().fromHistory = false; 0356 0357 auto* const dlg = new GrepDialog(m_plugin, this, this, false); 0358 dlg->historySearch(refresh_history); 0359 } 0360 } 0361 0362 void GrepOutputView::expandElements(const QModelIndex& index) 0363 { 0364 updateButtonState(true); 0365 0366 resultsTreeView->expand(index); 0367 } 0368 0369 void GrepOutputView::updateButtonState(bool enable) 0370 { 0371 m_prev->setEnabled(enable); 0372 m_next->setEnabled(enable); 0373 m_collapseAll->setEnabled(enable); 0374 m_expandAll->setEnabled(enable); 0375 } 0376 0377 void GrepOutputView::selectPreviousItem() 0378 { 0379 if (!model()) { 0380 return; 0381 } 0382 0383 QModelIndex prev_idx = model()->previousItemIndex(resultsTreeView->currentIndex()); 0384 if (prev_idx.isValid()) { 0385 resultsTreeView->setCurrentIndex(prev_idx); 0386 model()->activate(prev_idx); 0387 } 0388 } 0389 0390 void GrepOutputView::selectNextItem() 0391 { 0392 if (!model()) { 0393 return; 0394 } 0395 0396 QModelIndex next_idx = model()->nextItemIndex(resultsTreeView->currentIndex()); 0397 if (next_idx.isValid()) { 0398 resultsTreeView->setCurrentIndex(next_idx); 0399 model()->activate(next_idx); 0400 } 0401 } 0402 0403 0404 void GrepOutputView::collapseAllItems() 0405 { 0406 // Collapse everything 0407 resultsTreeView->collapseAll(); 0408 0409 if (resultsTreeView->model()) { 0410 // Now reopen the first children, which correspond to the files. 0411 resultsTreeView->expand(resultsTreeView->model()->index(0, 0)); 0412 } 0413 } 0414 0415 void GrepOutputView::expandAllItems() 0416 { 0417 resultsTreeView->expandAll(); 0418 } 0419 0420 void GrepOutputView::rowsRemoved() 0421 { 0422 Q_ASSERT(model()); 0423 0424 updateButtonState(model()->rowCount() > 0); 0425 } 0426 0427 void GrepOutputView::updateApplyState(const QModelIndex& topLeft, const QModelIndex& bottomRight) 0428 { 0429 Q_UNUSED(bottomRight); 0430 0431 if (!model() || !model()->hasResults()) { 0432 applyButton->setEnabled(false); 0433 return; 0434 } 0435 0436 // we only care about the root item 0437 if(!topLeft.parent().isValid()) 0438 { 0439 applyButton->setEnabled(topLeft.data(Qt::CheckStateRole) != Qt::Unchecked && model()->itemsCheckable()); 0440 } 0441 } 0442 0443 void GrepOutputView::updateCheckable() 0444 { 0445 if(model()) 0446 model()->makeItemsCheckable(!replacementCombo->currentText().isEmpty() || model()->itemsCheckable()); 0447 } 0448 0449 void GrepOutputView::clearSearchHistory() 0450 { 0451 GrepJob *runningJob = m_plugin->grepJob(); 0452 if(runningJob) 0453 { 0454 connect(runningJob, &GrepJob::finished, this, [=]() {updateButtonState(false);}); 0455 runningJob->kill(); 0456 } 0457 while(modelSelector->count() > 0) 0458 { 0459 QVariant var = modelSelector->itemData(0); 0460 qvariant_cast<QObject*>(var)->deleteLater(); 0461 modelSelector->removeItem(0); 0462 } 0463 0464 m_settingsHistory.clear(); 0465 0466 applyButton->setEnabled(false); 0467 0468 updateButtonState(false); 0469 m_refresh->setEnabled(false); 0470 m_clearSearchHistory->setEnabled(false); 0471 m_statusLabel->setText(QString()); 0472 } 0473 0474 void GrepOutputView::modelSelectorContextMenu(const QPoint& pos) 0475 { 0476 QPoint globalPos = modelSelector->mapToGlobal(pos); 0477 QMenu myMenu(this); 0478 myMenu.addAction(m_clearSearchHistory); 0479 myMenu.exec(globalPos); 0480 } 0481 0482 void GrepOutputView::updateScrollArea() 0483 { 0484 if (!model()) { 0485 return; 0486 } 0487 0488 for (int col = 0; col < model()->columnCount(); ++col) 0489 resultsTreeView->resizeColumnToContents(col); 0490 } 0491 0492 #include "moc_grepoutputview.cpp"