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"