File indexing completed on 2024-05-05 04:40:12

0001 /*
0002     SPDX-FileCopyrightText: 2006-2009 David Nolden <david.nolden.kdevelop@art-master.de>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "patchreviewtoolview.h"
0008 #include "localpatchsource.h"
0009 #include "patchreview.h"
0010 #include "debug.h"
0011 
0012 #ifdef WITH_KOMPAREDIFF2_5_4_OR_NEWER
0013 #include <KompareDiff2/DiffModelList>
0014 #include <KompareDiff2/KompareModelList>
0015 #else
0016 #include <libkomparediff2/diffmodellist.h>
0017 #include <libkomparediff2/komparemodellist.h>
0018 #endif
0019 
0020 #include <interfaces/icore.h>
0021 #include <interfaces/idocumentcontroller.h>
0022 #include <vcs/models/vcsfilechangesmodel.h>
0023 #include <interfaces/ipatchsource.h>
0024 #include <interfaces/iplugincontroller.h>
0025 #include <interfaces/itestcontroller.h>
0026 #include <interfaces/itestsuite.h>
0027 #include <interfaces/iruncontroller.h>
0028 #include <interfaces/context.h>
0029 #include <interfaces/contextmenuextension.h>
0030 #include <interfaces/iprojectcontroller.h>
0031 #include <interfaces/iuicontroller.h>
0032 #include <util/projecttestjob.h>
0033 #include <sublime/area.h>
0034 #include <sublime/view.h>
0035 #include <sublime/document.h>
0036 #include <sublime/mainwindow.h>
0037 #include <sublime/message.h>
0038 
0039 #include <QFileInfo>
0040 #include <QMenu>
0041 #include <QJsonObject>
0042 #include <QJsonArray>
0043 
0044 #include <KLocalizedString>
0045 #include <KTextEditor/Document>
0046 #include <KTextEditor/View>
0047 
0048 #ifdef WITH_PURPOSE
0049 #include <Purpose/AlternativesModel>
0050 #include <purpose_version.h>
0051 #if PURPOSE_VERSION >= QT_VERSION_CHECK(5, 104, 0)
0052 #include <Purpose/Menu>
0053 #else
0054 #include <PurposeWidgets/Menu>
0055 #endif
0056 #endif
0057 
0058 using namespace KDevelop;
0059 
0060 class PatchFilesModel : public VcsFileChangesModel
0061 {
0062     Q_OBJECT
0063 public:
0064     PatchFilesModel( QObject *parent, bool allowSelection ) : VcsFileChangesModel( parent, allowSelection ) { };
0065     enum ItemRoles { HunksNumberRole = LastItemRole+1 };
0066 
0067 public Q_SLOTS:
0068     void updateState( const KDevelop::VcsStatusInfo &status, unsigned hunksNum ) {
0069         int row = VcsFileChangesModel::updateState( invisibleRootItem(), status );
0070         if ( row == -1 )
0071             return;
0072 
0073         QStandardItem *item = invisibleRootItem()->child( row, 0 );
0074         setFileInfo( item, hunksNum );
0075         item->setData( QVariant( hunksNum ), HunksNumberRole );
0076     }
0077 
0078     void updateState( const KDevelop::VcsStatusInfo &status ) {
0079         int row = VcsFileChangesModel::updateState( invisibleRootItem(), status );
0080         if ( row == -1 )
0081             return;
0082 
0083         QStandardItem *item = invisibleRootItem()->child( row, 0 );
0084         setFileInfo( invisibleRootItem()->child( row, 0 ), item->data( HunksNumberRole ).toUInt() );
0085     }
0086 
0087 private:
0088     void setFileInfo( QStandardItem *item, unsigned int hunksNum ) {
0089         const auto url = item->index().data(VcsFileChangesModel::UrlRole).toUrl();
0090         const QString path = ICore::self()->projectController()->prettyFileName(url, KDevelop::IProjectController::FormatPlain);
0091         const QString newText = i18ncp( "%1: number of changed hunks, %2: file name",
0092             "%2 (1 hunk)", "%2 (%1 hunks)", hunksNum,
0093             path);
0094         item->setText( newText );
0095     }
0096 };
0097 
0098 PatchReviewToolView::PatchReviewToolView( QWidget* parent, PatchReviewPlugin* plugin )
0099     : QWidget( parent ),
0100     m_resetCheckedUrls( true ),
0101     m_plugin( plugin )
0102 {
0103     setWindowIcon(QIcon::fromTheme(QStringLiteral("text-x-patch"), windowIcon()));
0104 
0105     connect( m_plugin->finishReviewAction(), &QAction::triggered, this, &PatchReviewToolView::finishReview );
0106 
0107     connect( plugin, &PatchReviewPlugin::patchChanged, this, &PatchReviewToolView::patchChanged );
0108     connect( plugin, &PatchReviewPlugin::startingNewReview, this, &PatchReviewToolView::startingNewReview );
0109     connect( ICore::self()->documentController(), &IDocumentController::documentActivated, this, &PatchReviewToolView::documentActivated );
0110 
0111     auto* w = qobject_cast<Sublime::MainWindow*>(ICore::self()->uiController()->activeMainWindow());
0112     connect(w, &Sublime::MainWindow::areaChanged, m_plugin, &PatchReviewPlugin::areaChanged);
0113 
0114     showEditDialog();
0115     patchChanged();
0116 }
0117 
0118 void PatchReviewToolView::resizeEvent(QResizeEvent* ev)
0119 {
0120     bool vertical = (width() < height());
0121     m_editPatch.buttonsLayout->setDirection(vertical ? QBoxLayout::TopToBottom : QBoxLayout::LeftToRight);
0122     m_editPatch.contentLayout->setDirection(vertical ? QBoxLayout::TopToBottom : QBoxLayout::LeftToRight);
0123     m_editPatch.buttonsSpacer->changeSize(vertical ? 0 : 40, 0, QSizePolicy::Fixed, QSizePolicy::Fixed);
0124     QWidget::resizeEvent(ev);
0125     if(m_customWidget) {
0126         m_editPatch.contentLayout->removeWidget( m_customWidget );
0127         m_editPatch.contentLayout->insertWidget(0, m_customWidget );
0128     }
0129 }
0130 
0131 void PatchReviewToolView::startingNewReview()
0132 {
0133     m_resetCheckedUrls = true;
0134 }
0135 
0136 void PatchReviewToolView::patchChanged() {
0137     fillEditFromPatch();
0138     kompareModelChanged();
0139 
0140 #ifdef WITH_PURPOSE
0141     IPatchSource::Ptr p = m_plugin->patch();
0142     if (p) {
0143         m_exportMenu->model()->setInputData(QJsonObject {
0144             { QStringLiteral("urls"), QJsonArray { p->file().toString() } },
0145             { QStringLiteral("mimeType"), { QStringLiteral("text/x-patch") } },
0146             { QStringLiteral("localBaseDir"), { p->baseDir().toString() } },
0147             { QStringLiteral("updateComment"), { QStringLiteral("Patch updated through KDevelop's Patch Review plugin") } }
0148         });
0149     }
0150 #endif
0151 }
0152 
0153 PatchReviewToolView::~PatchReviewToolView()
0154 {
0155 }
0156 
0157 LocalPatchSource* PatchReviewToolView::GetLocalPatchSource() {
0158     IPatchSource::Ptr ips = m_plugin->patch();
0159 
0160     if ( !ips )
0161         return nullptr;
0162     return qobject_cast<LocalPatchSource*>(ips.data());
0163 }
0164 
0165 void PatchReviewToolView::fillEditFromPatch() {
0166     IPatchSource::Ptr ipatch = m_plugin->patch();
0167     if ( !ipatch )
0168         return;
0169 
0170     m_editPatch.cancelReview->setVisible( ipatch->canCancel() );
0171 
0172     m_fileModel->setIsCheckbable( m_plugin->patch()->canSelectFiles() );
0173 
0174     if( m_customWidget ) {
0175         qCDebug(PLUGIN_PATCHREVIEW) << "removing custom widget";
0176         m_customWidget->hide();
0177         m_editPatch.contentLayout->removeWidget( m_customWidget );
0178     }
0179 
0180     m_customWidget = ipatch->customWidget();
0181     if( m_customWidget ) {
0182         m_editPatch.contentLayout->insertWidget( 0, m_customWidget );
0183         m_customWidget->show();
0184         qCDebug(PLUGIN_PATCHREVIEW) << "got custom widget";
0185     }
0186 
0187     bool showTests = false;
0188     QMap<QUrl, VcsStatusInfo::State> files = ipatch->additionalSelectableFiles();
0189     QMap<QUrl, VcsStatusInfo::State>::const_iterator it = files.constBegin();
0190 
0191     for (; it != files.constEnd(); ++it) {
0192         auto project = ICore::self()->projectController()->findProjectForUrl(it.key());
0193         if (project && !ICore::self()->testController()->testSuitesForProject(project).isEmpty()) {
0194             showTests = true;
0195             break;
0196         }
0197     }
0198 
0199     m_editPatch.testsButton->setVisible(showTests);
0200     m_editPatch.testProgressBar->hide();
0201 }
0202 
0203 void PatchReviewToolView::slotAppliedChanged( int newState ) {
0204     if ( LocalPatchSource* lpatch = GetLocalPatchSource() ) {
0205         lpatch->setAlreadyApplied( newState == Qt::Checked );
0206         m_plugin->notifyPatchChanged();
0207     }
0208 }
0209 
0210 void PatchReviewToolView::showEditDialog() {
0211     m_editPatch.setupUi( this );
0212 
0213     bool allowSelection = m_plugin->patch() && m_plugin->patch()->canSelectFiles();
0214     m_fileModel = new PatchFilesModel( this, allowSelection );
0215     m_fileSortProxyModel = new VcsFileChangesSortProxyModel(this);
0216     m_fileSortProxyModel->setSourceModel(m_fileModel);
0217     m_fileSortProxyModel->sort(1);
0218     m_fileSortProxyModel->setDynamicSortFilter(true);
0219     m_editPatch.filesList->setModel( m_fileSortProxyModel );
0220     m_editPatch.filesList->header()->hide();
0221     m_editPatch.filesList->setRootIsDecorated( false );
0222     m_editPatch.filesList->setContextMenuPolicy(Qt::CustomContextMenu);
0223     connect(m_editPatch.filesList, &QTreeView::customContextMenuRequested, this, &PatchReviewToolView::customContextMenuRequested);
0224     connect(m_fileModel, &PatchFilesModel::itemChanged, this, &PatchReviewToolView::fileItemChanged);
0225     m_editPatch.finishReview->setDefaultAction(m_plugin->finishReviewAction());
0226 
0227 #ifdef WITH_PURPOSE
0228     m_exportMenu = new Purpose::Menu(this);
0229     connect(m_exportMenu, &Purpose::Menu::finished, this, [](const QJsonObject &output, int error, const QString &errorMessage) {
0230         Sublime::Message* message;
0231         if (error==0) {
0232             const QString messageText = i18n("<qt>You can find the new request at:<br /><a href='%1'>%1</a> </qt>", output[QLatin1String("url")].toString());
0233             message = new Sublime::Message(messageText, Sublime::Message::Information);
0234         } else {
0235             const QString messageText = i18n("Couldn't export the patch.\n%1", errorMessage);
0236             message = new Sublime::Message(messageText, Sublime::Message::Error);
0237         }
0238         ICore::self()->uiController()->postMessage(message);
0239     });
0240     // set the model input parameters to avoid terminal warnings
0241     m_exportMenu->model()->setInputData(QJsonObject {
0242         { QStringLiteral("urls"), QJsonArray { QString() } },
0243         { QStringLiteral("mimeType"), { QStringLiteral("text/x-patch") } }
0244     });
0245     m_exportMenu->model()->setPluginType(QStringLiteral("Export"));
0246     m_editPatch.exportReview->setMenu( m_exportMenu );
0247 #else
0248     m_editPatch.exportReview->setEnabled(false);
0249 #endif
0250 
0251     connect( m_editPatch.previousHunk, &QToolButton::clicked, this, &PatchReviewToolView::prevHunk );
0252     connect( m_editPatch.nextHunk, &QToolButton::clicked, this, &PatchReviewToolView::nextHunk );
0253     connect( m_editPatch.previousFile, &QToolButton::clicked, this, &PatchReviewToolView::prevFile );
0254     connect( m_editPatch.nextFile, &QToolButton::clicked, this, &PatchReviewToolView::nextFile );
0255     connect( m_editPatch.filesList, &QTreeView::activated , this, &PatchReviewToolView::fileDoubleClicked );
0256 
0257     connect( m_editPatch.cancelReview, &QToolButton::clicked, m_plugin, &PatchReviewPlugin::cancelReview );
0258     //connect( m_editPatch.cancelButton, SIGNAL(pressed()), this, SLOT(slotEditCancel()) );
0259 
0260     //connect( this, SIGNAL(finished(int)), this, SLOT(slotEditDialogFinished(int)) );
0261 
0262     connect( m_editPatch.updateButton, &QToolButton::clicked, m_plugin, &PatchReviewPlugin::forceUpdate );
0263 
0264     connect( m_editPatch.testsButton, &QToolButton::clicked, this, &PatchReviewToolView::runTests );
0265 
0266     m_selectAllAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-select-all")), i18nc("@action", "Select All"), this );
0267     connect( m_selectAllAction, &QAction::triggered, this, &PatchReviewToolView::selectAll );
0268     m_deselectAllAction = new QAction( i18nc("@action", "Deselect All"), this );
0269     connect( m_deselectAllAction, &QAction::triggered, this, &PatchReviewToolView::deselectAll );
0270 }
0271 
0272 void PatchReviewToolView::customContextMenuRequested(const QPoint& pos)
0273 {
0274     QList<QUrl> urls;
0275     const QModelIndexList selectionIdxs = m_editPatch.filesList->selectionModel()->selectedIndexes();
0276     urls.reserve(selectionIdxs.size());
0277     for (const QModelIndex& idx : selectionIdxs) {
0278         urls += idx.data(KDevelop::VcsFileChangesModel::UrlRole).toUrl();
0279     }
0280 
0281     QPointer<QMenu> menu = new QMenu(m_editPatch.filesList);
0282     QList<ContextMenuExtension> extensions;
0283     if(!urls.isEmpty()) {
0284         KDevelop::FileContext context(urls);
0285         extensions = ICore::self()->pluginController()->queryPluginsForContextMenuExtensions(&context, menu);
0286     }
0287 
0288     QList<QAction*> vcsActions;
0289     for (const ContextMenuExtension& ext : qAsConst(extensions)) {
0290         vcsActions += ext.actions(ContextMenuExtension::VcsGroup);
0291     }
0292 
0293     menu->addAction(m_selectAllAction);
0294     menu->addAction(m_deselectAllAction);
0295     menu->addActions(vcsActions);
0296     menu->exec(m_editPatch.filesList->viewport()->mapToGlobal(pos));
0297 
0298     delete menu;
0299 }
0300 
0301 void PatchReviewToolView::nextHunk()
0302 {
0303     IDocument* current = ICore::self()->documentController()->activeDocument();
0304     if(current && current->textDocument())
0305         m_plugin->seekHunk( true, current->textDocument()->url() );
0306 }
0307 
0308 void PatchReviewToolView::prevHunk()
0309 {
0310     IDocument* current = ICore::self()->documentController()->activeDocument();
0311     if(current && current->textDocument())
0312         m_plugin->seekHunk( false, current->textDocument()->url() );
0313 }
0314 
0315 void PatchReviewToolView::seekFile(bool forwards)
0316 {
0317     if(!m_plugin->patch())
0318         return;
0319     const QList<QUrl> checkedUrls = m_fileModel->checkedUrls();
0320     QList<QUrl> allUrls = m_fileModel->urls();
0321     IDocument* current = ICore::self()->documentController()->activeDocument();
0322     if(!current || checkedUrls.empty())
0323         return;
0324     qCDebug(PLUGIN_PATCHREVIEW) << "seeking direction" << forwards;
0325     int currentIndex = allUrls.indexOf(current->url());
0326     QUrl newUrl;
0327     if((forwards && current->url() == checkedUrls.back()) ||
0328             (!forwards && current->url() == checkedUrls[0]))
0329     {
0330         newUrl = m_plugin->patch()->file();
0331         qCDebug(PLUGIN_PATCHREVIEW) << "jumping to patch";
0332     }
0333     else if(current->url() == m_plugin->patch()->file() || currentIndex == -1)
0334     {
0335         if(forwards)
0336             newUrl = checkedUrls[0];
0337         else
0338             newUrl = checkedUrls.back();
0339         qCDebug(PLUGIN_PATCHREVIEW) << "jumping from patch";
0340     } else {
0341         QSet<QUrl> checkedUrlsSet(checkedUrls.begin(), checkedUrls.end());
0342         for(int offset = 1; offset < allUrls.size(); ++offset)
0343         {
0344             int pos;
0345             if(forwards) {
0346                 pos = (currentIndex + offset) % allUrls.size();
0347             }else{
0348                 pos = currentIndex - offset;
0349                 if(pos < 0)
0350                     pos += allUrls.size();
0351             }
0352             if(checkedUrlsSet.contains(allUrls[pos]))
0353             {
0354                 newUrl = allUrls[pos];
0355                 break;
0356             }
0357         }
0358     }
0359 
0360     if(newUrl.isValid())
0361     {
0362         open( newUrl, true );
0363     }else{
0364         qCDebug(PLUGIN_PATCHREVIEW) << "found no valid target url";
0365     }
0366 }
0367 
0368 void PatchReviewToolView::open( const QUrl& url, bool activate ) const
0369 {
0370     qCDebug(PLUGIN_PATCHREVIEW) << "activating url" << url;
0371     // If the document is already open in this area, just re-activate it
0372     if(KDevelop::IDocument* doc = ICore::self()->documentController()->documentForUrl(url)) {
0373         const auto views = ICore::self()->uiController()->activeArea()->views();
0374         for (Sublime::View* view : views) {
0375             if(view->document() == dynamic_cast<Sublime::Document*>(doc))
0376             {
0377                 if (activate) {
0378                     // use openDocument() for the activation so that the document is added to File/Open Recent.
0379                     ICore::self()->documentController()->openDocument(doc->url(), KTextEditor::Range::invalid());
0380                 }
0381                 return;
0382             }
0383         }
0384     }
0385 
0386     QStandardItem* item = m_fileModel->itemForUrl( url );
0387 
0388     IDocument* buddyDoc = nullptr;
0389 
0390     if (m_plugin->patch() && item) {
0391         for (int preRow = item->row() - 1; preRow >= 0; --preRow) {
0392             QStandardItem* preItem = m_fileModel->item(preRow);
0393             if (!m_fileModel->isCheckable() || preItem->checkState() == Qt::Checked) {
0394                 // found valid predecessor, take it as buddy
0395                 buddyDoc = ICore::self()->documentController()->documentForUrl(preItem->index().data(VcsFileChangesModel::UrlRole).toUrl());
0396                 if (buddyDoc) {
0397                     break;
0398                 }
0399             }
0400         }
0401         if (!buddyDoc) {
0402             buddyDoc = ICore::self()->documentController()->documentForUrl(m_plugin->patch()->file());
0403         }
0404     }
0405 
0406     // we simplify and assume that documents to be opened without activating them also need not be
0407     // added to the Files/Open Recent menu.
0408     IDocument* newDoc = ICore::self()->documentController()->openDocument(url, KTextEditor::Range::invalid(),
0409         activate ? IDocumentController::DefaultMode : IDocumentController::DoNotActivate|IDocumentController::DoNotAddToRecentOpen,
0410         QString(), buddyDoc);
0411 
0412     KTextEditor::View* view = nullptr;
0413     if(newDoc)
0414         view = newDoc->activeTextView();
0415 
0416     if(view && view->cursorPosition().line() == 0)
0417         m_plugin->seekHunk( true, url );
0418 }
0419 
0420 void PatchReviewToolView::fileItemChanged( QStandardItem* item )
0421 {
0422     if (item->column() != 0 || !m_plugin->patch())
0423         return;
0424 
0425     QUrl url = item->index().data(VcsFileChangesModel::UrlRole).toUrl();
0426     if (url.isEmpty())
0427         return;
0428 
0429     KDevelop::IDocument* doc = ICore::self()->documentController()->documentForUrl(url);
0430     if(m_fileModel->isCheckable() && item->checkState() != Qt::Checked)
0431     {   // The file was deselected, so eventually close it
0432         if(doc && doc->state() == IDocument::Clean)
0433         {
0434             const auto views = ICore::self()->uiController()->activeArea()->views();
0435             for (Sublime::View* view : views) {
0436                 if(view->document() == dynamic_cast<Sublime::Document*>(doc))
0437                 {
0438                     ICore::self()->uiController()->activeArea()->closeView(view);
0439                     return;
0440                 }
0441             }
0442         }
0443     } else if (!doc) {
0444         // Maybe the file was unchecked before, or it  was just loaded.
0445         open( url, false );
0446     }
0447 }
0448 
0449 void PatchReviewToolView::nextFile()
0450 {
0451     seekFile(true);
0452 }
0453 
0454 void PatchReviewToolView::prevFile()
0455 {
0456     seekFile(false);
0457 }
0458 
0459 void PatchReviewToolView::deselectAll()
0460 {
0461     m_fileModel->setAllChecked(false);
0462 }
0463 
0464 void PatchReviewToolView::selectAll()
0465 {
0466     m_fileModel->setAllChecked(true);
0467 }
0468 
0469 void PatchReviewToolView::finishReview() {
0470     QList<QUrl> selectedUrls = m_fileModel->checkedUrls();
0471     qCDebug(PLUGIN_PATCHREVIEW) << "finishing review with" << selectedUrls;
0472     m_plugin->finishReview( selectedUrls );
0473 }
0474 
0475 void PatchReviewToolView::fileDoubleClicked( const QModelIndex& idx )
0476 {
0477     const QUrl file = idx.data(VcsFileChangesModel::UrlRole).toUrl();
0478     open( file, true );
0479 }
0480 
0481 void PatchReviewToolView::kompareModelChanged() {
0482 
0483     QList<QUrl> oldCheckedUrls = m_fileModel->checkedUrls();
0484 
0485     m_fileModel->clear();
0486 
0487     if ( !m_plugin->modelList() )
0488         return;
0489 
0490     QMap<QUrl, KDevelop::VcsStatusInfo::State> additionalUrls = m_plugin->patch()->additionalSelectableFiles();
0491 
0492     const Diff2::DiffModelList* models = m_plugin->modelList()->models();
0493     if( models )
0494     {
0495         for (auto* model : *models) {
0496             const Diff2::DifferenceList* diffs = model->differences();
0497             int cnt = 0;
0498             if ( diffs )
0499                 cnt = diffs->count();
0500 
0501             const QUrl file = m_plugin->urlForFileModel(model);
0502             if( file.isLocalFile() && !QFileInfo( file.toLocalFile() ).isReadable() )
0503                 continue;
0504 
0505             VcsStatusInfo status;
0506             status.setUrl( file );
0507             status.setState( cnt>0 ? VcsStatusInfo::ItemModified : VcsStatusInfo::ItemUpToDate );
0508 
0509             m_fileModel->updateState( status, cnt );
0510         }
0511     }
0512 
0513     for( QMap<QUrl, KDevelop::VcsStatusInfo::State>::const_iterator it = additionalUrls.constBegin(); it != additionalUrls.constEnd(); ++it ) {
0514         VcsStatusInfo status;
0515         status.setUrl( it.key() );
0516         status.setState( it.value() );
0517         m_fileModel->updateState( status );
0518     }
0519 
0520     if(!m_resetCheckedUrls)
0521         m_fileModel->setCheckedUrls(oldCheckedUrls);
0522     else
0523         m_resetCheckedUrls = false;
0524 
0525     m_editPatch.filesList->resizeColumnToContents( 0 );
0526 
0527     // Eventually select the active document
0528     documentActivated( ICore::self()->documentController()->activeDocument() );
0529 }
0530 
0531 void PatchReviewToolView::documentActivated( IDocument* doc ) {
0532     if( !doc )
0533         return;
0534 
0535     if ( !m_plugin->modelList() )
0536         return;
0537 
0538     const auto matches = m_fileSortProxyModel->match(
0539         m_fileSortProxyModel->index(0, 0), VcsFileChangesModel::UrlRole,
0540         doc->url(), 1, Qt::MatchExactly);
0541     m_editPatch.filesList->setCurrentIndex(matches.value(0));
0542 }
0543 
0544 void PatchReviewToolView::runTests()
0545 {
0546     IPatchSource::Ptr ipatch = m_plugin->patch();
0547     if ( !ipatch ) {
0548         return;
0549     }
0550 
0551     IProject* project = nullptr;
0552     QMap<QUrl, VcsStatusInfo::State> files = ipatch->additionalSelectableFiles();
0553     QMap<QUrl, VcsStatusInfo::State>::const_iterator it = files.constBegin();
0554 
0555     for (; it != files.constEnd(); ++it) {
0556         project = ICore::self()->projectController()->findProjectForUrl(it.key());
0557         if (project) {
0558            break;
0559         }
0560     }
0561 
0562     if (!project) {
0563         return;
0564     }
0565 
0566     m_editPatch.testProgressBar->setFormat(i18n("Running tests: %p%"));
0567     m_editPatch.testProgressBar->setValue(0);
0568     m_editPatch.testProgressBar->show();
0569 
0570     auto* job = new ProjectTestJob(project, this);
0571     connect(job, &ProjectTestJob::finished, this, &PatchReviewToolView::testJobResult);
0572     connect(job, &KJob::percentChanged, this, &PatchReviewToolView::testJobPercent);
0573     ICore::self()->runController()->registerJob(job);
0574 }
0575 
0576 void PatchReviewToolView::testJobPercent(KJob* job, unsigned long percent)
0577 {
0578     Q_UNUSED(job);
0579     m_editPatch.testProgressBar->setValue(percent);
0580 }
0581 
0582 void PatchReviewToolView::testJobResult(KJob* job)
0583 {
0584     auto* testJob = qobject_cast<ProjectTestJob*>(job);
0585     if (!testJob) {
0586         return;
0587     }
0588 
0589     ProjectTestResult result = testJob->testResult();
0590 
0591     QString format;
0592     if (result.passed > 0 && result.failed == 0 && result.error == 0)
0593     {
0594         format = i18np("Test passed", "All %1 tests passed", result.passed);
0595     }
0596     else
0597     {
0598         format = i18n("Test results: %1 passed, %2 failed, %3 errors", result.passed, result.failed, result.error);
0599     }
0600     m_editPatch.testProgressBar->setFormat(format);
0601 
0602     // Needed because some test jobs may raise their own output views
0603     ICore::self()->uiController()->raiseToolView(this);
0604 }
0605 
0606 #include "patchreviewtoolview.moc"
0607 #include "moc_patchreviewtoolview.cpp"