File indexing completed on 2024-05-19 12:24:21
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"