File indexing completed on 2024-04-28 04:37:23

0001 /*
0002     SPDX-FileCopyrightText: 2006 Adam Treat <treat@kde.org>
0003     SPDX-FileCopyrightText: 2007 Andreas Pakulat <apaku@gmx.de>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "projectcontroller.h"
0009 
0010 #include <QAction>
0011 #include <QApplication>
0012 #include <QDBusConnection>
0013 #include <QDir>
0014 #include <QGroupBox>
0015 #include <QLabel>
0016 #include <QList>
0017 #include <QMap>
0018 #include <QPointer>
0019 #include <QPushButton>
0020 #include <QRadioButton>
0021 #include <QSet>
0022 #include <QTemporaryFile>
0023 #include <QVBoxLayout>
0024 #include <QTimer>
0025 
0026 #include <KActionCollection>
0027 #include <KConfigGroup>
0028 #include <KIO/DeleteJob>
0029 #include <KIO/FileCopyJob>
0030 #include <KIO/ListJob>
0031 #include <KIO/StatJob>
0032 #include <KJobWidgets>
0033 #include <KLocalizedString>
0034 #include <KMessageBox>
0035 #include <KMessageBox_KDevCompat>
0036 #include <KRecentFilesAction>
0037 #include <KSharedConfig>
0038 #include <KStandardAction>
0039 
0040 #include <sublime/area.h>
0041 #include <sublime/message.h>
0042 #include <interfaces/iplugin.h>
0043 #include <interfaces/isession.h>
0044 #include <interfaces/context.h>
0045 #include <interfaces/contextmenuextension.h>
0046 #include <interfaces/iselectioncontroller.h>
0047 #include <project/interfaces/iprojectfilemanager.h>
0048 #include <project/interfaces/ibuildsystemmanager.h>
0049 #include <project/interfaces/iprojectbuilder.h>
0050 #include <project/projectchangesmodel.h>
0051 #include <project/projectmodel.h>
0052 #include <project/projectbuildsetmodel.h>
0053 #include <projectconfigpage.h>
0054 #include <language/backgroundparser/parseprojectjob.h>
0055 #include <interfaces/iruncontroller.h>
0056 #include <util/scopeddialog.h>
0057 #include <vcs/widgets/vcsdiffpatchsources.h>
0058 #include <vcs/widgets/vcscommitdialog.h>
0059 
0060 #include "core.h"
0061 // TODO: Should get rid off this include (should depend on IProject only)
0062 #include "project.h"
0063 #include "mainwindow.h"
0064 #include "shellextension.h"
0065 #include "plugincontroller.h"
0066 #include "configdialog.h"
0067 #include "uicontroller.h"
0068 #include "documentcontroller.h"
0069 #include "openprojectdialog.h"
0070 #include "sessioncontroller.h"
0071 #include "session.h"
0072 #include "debug.h"
0073 
0074 namespace KDevelop
0075 {
0076 
0077 class ProjectControllerPrivate
0078 {
0079 public:
0080     QList<IProject*> m_projects;
0081     QMap< IProject*, QList<IPlugin*> > m_projectPlugins;
0082     QPointer<KRecentFilesAction> m_recentProjectsAction;
0083     Core* const m_core;
0084 //     IProject* m_currentProject;
0085     ProjectModel* const model;
0086     QPointer<QAction> m_openProject;
0087     QPointer<QAction> m_fetchProject;
0088     QPointer<QAction> m_closeProject;
0089     QPointer<QAction> m_openConfig;
0090     IProjectDialogProvider* dialog;
0091     QList<QUrl> m_currentlyOpening; // project-file urls that are being opened
0092     ProjectController* const q;
0093     ProjectBuildSetModel* buildset;
0094     bool m_foundProjectFile; //Temporary flag used while searching the hierarchy for a project file
0095     bool m_cleaningUp; //Temporary flag enabled while destroying the project-controller
0096     ProjectChangesModel* m_changesModel = nullptr;
0097     QHash<IProject*, KJob*> m_parseJobs; // parse jobs that add files from the project to the background parser.
0098 
0099     ProjectControllerPrivate(Core* core, ProjectController* p)
0100         : m_core(core)
0101         , model(new ProjectModel())
0102         , dialog(nullptr)
0103         , q(p)
0104         , buildset(nullptr)
0105         , m_foundProjectFile(false)
0106         , m_cleaningUp(false)
0107     {
0108     }
0109 
0110     void unloadAllProjectPlugins()
0111     {
0112         if( m_projects.isEmpty() )
0113             m_core->pluginControllerInternal()->unloadProjectPlugins();
0114     }
0115 
0116     void projectConfig( QObject * obj )
0117     {
0118         if( !obj )
0119             return;
0120         auto* proj = qobject_cast<Project*>(obj);
0121         if( !proj )
0122             return;
0123 
0124         auto cfgDlg = new KDevelop::ConfigDialog(m_core->uiController()->activeMainWindow());
0125         cfgDlg->setAttribute(Qt::WA_DeleteOnClose);
0126         cfgDlg->setModal(true);
0127 
0128         QVector<KDevelop::ConfigPage*> configPages;
0129 
0130         ProjectConfigOptions options;
0131         options.developerFile = proj->developerFile();
0132         options.developerTempFile = proj->developerTempFile();
0133         options.projectTempFile = proj->projectTempFile();
0134         options.project = proj;
0135 
0136         const auto plugins = findPluginsForProject(proj);
0137         for (IPlugin* plugin : plugins) {
0138             const int perProjectConfigPagesCount = plugin->perProjectConfigPages();
0139             configPages.reserve(configPages.size() + perProjectConfigPagesCount);
0140             for (int i = 0; i < perProjectConfigPagesCount; ++i) {
0141                 configPages.append(plugin->perProjectConfigPage(i, options, cfgDlg));
0142             }
0143         }
0144 
0145         std::sort(configPages.begin(), configPages.end(),
0146                   [](const ConfigPage* a, const ConfigPage* b) {
0147             return a->name() < b->name();
0148         });
0149 
0150         for (auto page : configPages) {
0151             cfgDlg->appendConfigPage(page);
0152         }
0153 
0154         QObject::connect(cfgDlg, &ConfigDialog::configSaved, cfgDlg, [this, proj](ConfigPage* page) {
0155             Q_UNUSED(page)
0156             Q_ASSERT_X(proj, Q_FUNC_INFO,
0157                     "ConfigDialog signalled project config change, but no project set for configuring!");
0158             emit q->projectConfigurationChanged(proj);
0159         });
0160         cfgDlg->setWindowTitle(i18nc("@title:window", "Configure Project %1", proj->name()));
0161         QObject::connect(cfgDlg, &KDevelop::ConfigDialog::finished, proj, [proj]() {
0162             proj->projectConfiguration()->sync();
0163         });
0164         cfgDlg->show();
0165     }
0166 
0167     void saveListOfOpenedProjects()
0168     {
0169         auto activeSession = Core::self()->activeSession();
0170         if (!activeSession) {
0171             return;
0172         }
0173 
0174         QList<QUrl> openProjects;
0175         openProjects.reserve( m_projects.size() );
0176 
0177         for (IProject* project : qAsConst(m_projects)) {
0178             openProjects.append(project->projectFile().toUrl());
0179         }
0180 
0181         activeSession->setContainedProjects( openProjects );
0182     }
0183 
0184     // Recursively collects builder dependencies for a project.
0185     static void collectBuilders( QList< IProjectBuilder* >& destination, IProjectBuilder* topBuilder, IProject* project )
0186     {
0187         const QList<IProjectBuilder*> auxBuilders = topBuilder->additionalBuilderPlugins(project);
0188         destination.append( auxBuilders );
0189         for (IProjectBuilder* auxBuilder : auxBuilders ) {
0190             collectBuilders( destination, auxBuilder, project );
0191         }
0192     }
0193 
0194     QVector<IPlugin*> findPluginsForProject( IProject* project ) const
0195     {
0196         const QList<IPlugin*> plugins = m_core->pluginController()->loadedPlugins();
0197         const IBuildSystemManager* const buildSystemManager = project->buildSystemManager();
0198         QVector<IPlugin*> projectPlugins;
0199         QList<IProjectBuilder*> buildersForKcm;
0200         // Important to also include the "top" builder for the project, so
0201         // projects with only one such builder are kept working. Otherwise the project config
0202         // dialog is empty for such cases.
0203         if (buildSystemManager) {
0204             buildersForKcm << buildSystemManager->builder();
0205             collectBuilders( buildersForKcm, buildSystemManager->builder(), project );
0206         }
0207 
0208         for (auto plugin : plugins) {
0209             auto info = m_core->pluginController()->pluginInfo(plugin);
0210             auto* manager = plugin->extension<KDevelop::IProjectFileManager>();
0211             if( manager && manager != project->projectFileManager() )
0212             {
0213                 // current plugin is a manager but does not apply to given project, skip
0214                 continue;
0215             }
0216             auto* builder = plugin->extension<KDevelop::IProjectBuilder>();
0217             if ( builder && !buildersForKcm.contains( builder ) )
0218             {
0219                 continue;
0220             }
0221             // Do not show config pages for analyzer tools which need a buildSystemManager
0222             // TODO: turn into generic feature to disable plugin config pages which do not apply for a project
0223             if (!buildSystemManager) {
0224                 const auto required = info.value(QStringLiteral("X-KDevelop-IRequired"), QStringList());
0225                 if (required.contains(QLatin1String("org.kdevelop.IBuildSystemManager"))) {
0226                     continue;
0227                 }
0228             }
0229 
0230             qCDebug(SHELL) << "Using plugin" << info.pluginId() << "for project" << project->name();
0231             projectPlugins << plugin;
0232         }
0233 
0234         return projectPlugins;
0235     }
0236 
0237     void updateActionStates()
0238     {
0239         // if only one project loaded, this is always our target
0240         int itemCount = (m_projects.size() == 1) ? 1 : 0;
0241 
0242         if (itemCount == 0) {
0243             // otherwise base on selection
0244             auto* itemContext = dynamic_cast<ProjectItemContext*>(ICore::self()->selectionController()->currentSelection());
0245             if (itemContext) {
0246                 itemCount = itemContext->items().count();
0247             }
0248         }
0249 
0250         m_openConfig->setEnabled(itemCount == 1);
0251         m_closeProject->setEnabled(itemCount > 0);
0252     }
0253 
0254     QSet<IProject*> selectedProjects()
0255     {
0256         QSet<IProject*> projects;
0257 
0258         // if only one project loaded, this is our target
0259         if (m_projects.count() == 1) {
0260             projects.insert(m_projects.at(0));
0261         } else {
0262             // otherwise base on selection
0263             auto* ctx = dynamic_cast<ProjectItemContext*>(ICore::self()->selectionController()->currentSelection());
0264             if (ctx) {
0265                 const auto items = ctx->items();
0266                 for (ProjectBaseItem* item : items) {
0267                     projects.insert(item->project());
0268                 }
0269             }
0270         }
0271         return projects;
0272     }
0273 
0274     void openProjectConfig()
0275     {
0276         auto projects = selectedProjects();
0277 
0278         if (projects.count() == 1) {
0279             q->configureProject(*projects.constBegin());
0280         }
0281     }
0282 
0283     void closeSelectedProjects()
0284     {
0285         const auto projects = selectedProjects();
0286         for (IProject* project : projects) {
0287             q->closeProject(project);
0288         }
0289     }
0290 
0291     void importProject(const QUrl& url_)
0292     {
0293         QUrl url(url_);
0294         if (url.isLocalFile()) {
0295             const QString path = QFileInfo(url.toLocalFile()).canonicalFilePath();
0296             if (!path.isEmpty()) {
0297                 url = QUrl::fromLocalFile(path);
0298             }
0299         }
0300 
0301         if ( !url.isValid() )
0302         {
0303             const QString messageText =  i18n("Invalid Location: %1", url.toDisplayString(QUrl::PreferLocalFile));
0304             auto* message = new Sublime::Message(messageText, Sublime::Message::Error);
0305             ICore::self()->uiController()->postMessage(message);
0306             return;
0307         }
0308 
0309         if ( m_currentlyOpening.contains(url))
0310         {
0311             qCDebug(SHELL) << "Already opening " << url << ". Aborting.";
0312             const QString messageText =
0313                 i18n("Already opening %1, not opening again", url.toDisplayString(QUrl::PreferLocalFile));
0314             auto* message = new Sublime::Message(messageText, Sublime::Message::Information);
0315             message->setAutoHide(0);
0316             ICore::self()->uiController()->postMessage(message);
0317             return;
0318         }
0319 
0320         const auto projects = m_projects;
0321         for (IProject* project : projects) {
0322             if ( url == project->projectFile().toUrl() )
0323             {
0324                 if ( dialog->userWantsReopen() )
0325                 { // close first, then open again by falling through
0326                     q->closeProject(project);
0327                 } else { // abort
0328                     return;
0329                 }
0330             }
0331         }
0332 
0333         m_currentlyOpening += url;
0334 
0335         m_core->pluginControllerInternal()->loadProjectPlugins();
0336 
0337         auto* project = new Project();
0338         QObject::connect(project, &Project::aboutToOpen,
0339                          q, &ProjectController::projectAboutToBeOpened);
0340         if ( !project->open( Path(url) ) )
0341         {
0342             m_currentlyOpening.removeAll(url);
0343             q->abortOpeningProject(project);
0344             project->deleteLater();
0345         }
0346     }
0347 
0348     void areaChanged(Sublime::Area* area) {
0349         KActionCollection* ac = m_core->uiControllerInternal()->defaultMainWindow()->actionCollection();
0350         ac->action(QStringLiteral("commit_current_project"))->setEnabled(area->objectName() == QLatin1String("code"));
0351         ac->action(QStringLiteral("commit_current_project"))->setVisible(area->objectName() == QLatin1String("code"));
0352     }
0353 };
0354 
0355 IProjectDialogProvider::IProjectDialogProvider()
0356 {}
0357 
0358 IProjectDialogProvider::~IProjectDialogProvider()
0359 {}
0360 
0361 ProjectDialogProvider::ProjectDialogProvider(ProjectControllerPrivate* p) : d(p)
0362 {}
0363 
0364 ProjectDialogProvider::~ProjectDialogProvider()
0365 {}
0366 
0367 bool writeNewProjectFile( const QString& localConfigFile, const QString& name, const QString& createdFrom, const QString& manager )
0368 {
0369     KSharedConfigPtr cfg = KSharedConfig::openConfig( localConfigFile, KConfig::SimpleConfig );
0370     if (!cfg->isConfigWritable(true)) {
0371         qCDebug(SHELL) << "can't write to configfile";
0372         return false;
0373     }
0374     KConfigGroup grp = cfg->group( "Project" );
0375     grp.writeEntry( "Name", name );
0376     grp.writeEntry( "CreatedFrom", createdFrom );
0377     grp.writeEntry( "Manager", manager );
0378     cfg->sync();
0379     return true;
0380 }
0381 
0382 bool writeProjectSettingsToConfigFile(const QUrl& projectFileUrl, OpenProjectDialog* dlg)
0383 {
0384     if ( !projectFileUrl.isLocalFile() ) {
0385         QTemporaryFile tmp;
0386         if ( !tmp.open() ) {
0387             return false;
0388         }
0389         if ( !writeNewProjectFile( tmp.fileName(), dlg->projectName(), dlg->selectedUrl().fileName(), dlg->projectManager() ) ) {
0390             return false;
0391         }
0392         // explicitly close file before uploading it, see also: https://bugs.kde.org/show_bug.cgi?id=254519
0393         tmp.close();
0394 
0395         auto uploadJob = KIO::file_copy(QUrl::fromLocalFile(tmp.fileName()), projectFileUrl);
0396         KJobWidgets::setWindow(uploadJob, Core::self()->uiControllerInternal()->defaultMainWindow());
0397         return uploadJob->exec();
0398     }
0399     // Here and above we take .filename() part of the selectedUrl() to make it relative to the project root,
0400     // thus keeping .kdev file relocatable
0401     return writeNewProjectFile( projectFileUrl.toLocalFile(), dlg->projectName(), dlg->selectedUrl().fileName(), dlg->projectManager() );
0402 }
0403 
0404 
0405 bool projectFileExists( const QUrl& u )
0406 {
0407     if( u.isLocalFile() )
0408     {
0409         return QFileInfo::exists( u.toLocalFile() );
0410     } else
0411     {
0412         auto statJob = KIO::statDetails(u, KIO::StatJob::DestinationSide, KIO::StatNoDetails, KIO::HideProgressInfo);
0413         KJobWidgets::setWindow(statJob, Core::self()->uiControllerInternal()->activeMainWindow());
0414         return statJob->exec();
0415     }
0416 }
0417 
0418 bool equalProjectFile( const QString& configPath, OpenProjectDialog* dlg )
0419 {
0420     KSharedConfigPtr cfg = KSharedConfig::openConfig( configPath, KConfig::SimpleConfig );
0421     KConfigGroup grp = cfg->group( "Project" );
0422     QString defaultName = dlg->projectFileUrl().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).fileName();
0423     return (grp.readEntry( "Name", QString() ) == dlg->projectName() || dlg->projectName() == defaultName) &&
0424            grp.readEntry( "Manager", QString() ) == dlg->projectManager();
0425 }
0426 
0427 QUrl ProjectDialogProvider::askProjectConfigLocation(bool fetch, const QUrl& startUrl,
0428                                                      const QUrl& repoUrl, IPlugin* vcsOrProviderPlugin)
0429 {
0430     Q_ASSERT(d);
0431     ScopedDialog<OpenProjectDialog> dlg(fetch, startUrl, repoUrl, vcsOrProviderPlugin,
0432                                          Core::self()->uiController()->activeMainWindow());
0433     if(dlg->exec() == QDialog::Rejected) {
0434         return QUrl();
0435     }
0436 
0437     QUrl projectFileUrl = dlg->projectFileUrl();
0438     qCDebug(SHELL) << "selected project:" << projectFileUrl << dlg->projectName() << dlg->projectManager();
0439     if ( dlg->projectManager() == QLatin1String("<built-in>") ) {
0440         return projectFileUrl;
0441     }
0442 
0443     // controls if existing project file should be saved
0444     bool writeProjectConfigToFile = true;
0445     if( projectFileExists( projectFileUrl ) )
0446     {
0447         // check whether config is equal
0448         bool shouldAsk = true;
0449         if( projectFileUrl == dlg->selectedUrl() )
0450         {
0451             if( projectFileUrl.isLocalFile() )
0452             {
0453                 shouldAsk = !equalProjectFile( projectFileUrl.toLocalFile(), dlg );
0454             } else {
0455                 shouldAsk = false;
0456 
0457                 QTemporaryFile tmpFile;
0458                 if (tmpFile.open()) {
0459                     auto downloadJob = KIO::file_copy(projectFileUrl, QUrl::fromLocalFile(tmpFile.fileName()));
0460                     KJobWidgets::setWindow(downloadJob, qApp->activeWindow());
0461                     if (downloadJob->exec()) {
0462                         shouldAsk = !equalProjectFile(tmpFile.fileName(), dlg);
0463                     }
0464                 }
0465             }
0466         }
0467 
0468         if ( shouldAsk )
0469         {
0470             KGuiItem yes(i18nc("@action:button", "Override"));
0471             yes.setToolTip(i18nc("@info:tooltip", "Continue to open the project and use the just provided project configuration"));
0472             KGuiItem no(i18nc("@action:button", "Open Existing File"));
0473             no.setToolTip(i18nc("@info:tooltip", "Continue to open the project but use the existing project configuration"));
0474             KGuiItem cancel = KStandardGuiItem::cancel();
0475             cancel.setToolTip(i18nc("@info:tooltip", "Cancel and do not open the project"));
0476             int ret = KMessageBox::questionTwoActionsCancel(
0477                 qApp->activeWindow(),
0478                 i18n("There already exists a project configuration file at %1.\n"
0479                      "Do you want to override it or open the existing file?",
0480                      projectFileUrl.toDisplayString(QUrl::PreferLocalFile)),
0481                 i18nc("@title:window", "Override Existing Project Configuration"), yes, no, cancel);
0482             if (ret == KMessageBox::SecondaryAction) {
0483                 writeProjectConfigToFile = false;
0484             } else if (ret == KMessageBox::Cancel) {
0485                 return QUrl();
0486             } // else fall through and write new file
0487         } else {
0488             writeProjectConfigToFile = false;
0489         }
0490     }
0491 
0492     if (writeProjectConfigToFile) {
0493         Path projectConfigDir(projectFileUrl);
0494         projectConfigDir.setLastPathSegment(QStringLiteral(".kdev4"));
0495         auto delJob = KIO::del(projectConfigDir.toUrl());
0496         delJob->exec();
0497 
0498         if (!writeProjectSettingsToConfigFile(projectFileUrl, dlg)) {
0499             const QString messageText = i18n("Unable to create configuration file %1", projectFileUrl.url());
0500             auto* message = new Sublime::Message(messageText, Sublime::Message::Error);
0501             ICore::self()->uiController()->postMessage(message);
0502             return QUrl();
0503         }
0504     }
0505 
0506     return projectFileUrl;
0507 }
0508 
0509 bool ProjectDialogProvider::userWantsReopen()
0510 {
0511     Q_ASSERT(d);
0512     return (KMessageBox::questionTwoActions(
0513                 d->m_core->uiControllerInternal()->defaultMainWindow(), i18n("Reopen the current project?"), {},
0514                 KGuiItem(i18nc("@action:button", "Reopen"), QStringLiteral("view-refresh")), KStandardGuiItem::cancel())
0515             == KMessageBox::SecondaryAction)
0516         ? false
0517         : true;
0518 }
0519 
0520 void ProjectController::setDialogProvider(IProjectDialogProvider* dialog)
0521 {
0522     Q_D(ProjectController);
0523 
0524     Q_ASSERT(d->dialog);
0525     delete d->dialog;
0526     d->dialog = dialog;
0527 }
0528 
0529 ProjectController::ProjectController( Core* core )
0530     : IProjectController(core)
0531     , d_ptr(new ProjectControllerPrivate(core, this))
0532 {
0533     qRegisterMetaType<QList<QUrl>>();
0534 
0535     setObjectName(QStringLiteral("ProjectController"));
0536 
0537     //NOTE: this is required to be called here, such that the
0538     //      actions are available when the UI controller gets
0539     //      initialized *before* the project controller
0540     if (Core::self()->setupFlags() != Core::NoUi) {
0541         setupActions();
0542     }
0543 }
0544 
0545 void ProjectController::setupActions()
0546 {
0547     Q_D(ProjectController);
0548 
0549     KActionCollection * ac =
0550         d->m_core->uiControllerInternal()->defaultMainWindow()->actionCollection();
0551 
0552     QAction*action;
0553 
0554     d->m_openProject = action = ac->addAction( QStringLiteral("project_open") );
0555     action->setText(i18nc( "@action", "Open / Import Project..." ) );
0556     action->setToolTip( i18nc( "@info:tooltip", "Open or import project" ) );
0557     action->setWhatsThis( i18nc( "@info:whatsthis", "Open an existing KDevelop project or import "
0558                                                     "an existing Project into KDevelop. This entry "
0559                                                     "allows one to select a KDevelop project file "
0560                                                     "or an existing directory to open it in KDevelop. "
0561                                                     "When opening an existing directory that does "
0562                                                     "not yet have a KDevelop project file, the file "
0563                                                     "will be created." ) );
0564     action->setIcon(QIcon::fromTheme(QStringLiteral("project-open")));
0565     connect(action, &QAction::triggered, this, [&] { openProject(); });
0566 
0567     d->m_fetchProject = action = ac->addAction( QStringLiteral("project_fetch") );
0568     action->setText(i18nc( "@action", "Fetch Project..." ) );
0569     action->setIcon( QIcon::fromTheme( QStringLiteral("edit-download") ) );
0570     action->setToolTip( i18nc( "@info:tooltip", "Fetch project" ) );
0571     action->setWhatsThis( i18nc( "@info:whatsthis", "Guides the user through the project fetch "
0572                                                     "and then imports it into KDevelop." ) );
0573 //     action->setIcon(QIcon::fromTheme("project-open"));
0574     connect( action, &QAction::triggered, this, &ProjectController::fetchProject );
0575 
0576 //    action = ac->addAction( "project_close" );
0577 //    action->setText( i18n( "C&lose Project" ) );
0578 //    connect( action, SIGNAL(triggered(bool)), SLOT(closeProject()) );
0579 //    action->setToolTip( i18n( "Close project" ) );
0580 //    action->setWhatsThis( i18n( "Closes the current project." ) );
0581 //    action->setEnabled( false );
0582 
0583     d->m_closeProject = action = ac->addAction( QStringLiteral("project_close") );
0584     connect(action, &QAction::triggered,
0585             this, [this] { Q_D(ProjectController); d->closeSelectedProjects(); } );
0586     action->setText( i18nc( "@action", "Close Project(s)" ) );
0587     action->setIcon( QIcon::fromTheme( QStringLiteral("project-development-close") ) );
0588     action->setToolTip( i18nc( "@info:tooltip", "Closes all currently selected projects" ) );
0589     action->setEnabled( false );
0590 
0591     d->m_openConfig = action = ac->addAction( QStringLiteral("project_open_config") );
0592     connect(action, &QAction::triggered,
0593             this, [this] { Q_D(ProjectController); d->openProjectConfig(); } );
0594     action->setText( i18nc("@action", "Open Configuration..." ) );
0595     action->setIcon( QIcon::fromTheme(QStringLiteral("configure")) );
0596     action->setEnabled( false );
0597 
0598     action = ac->addAction( QStringLiteral("commit_current_project") );
0599     connect( action, &QAction::triggered, this, &ProjectController::commitCurrentProject );
0600     action->setText( i18nc("@action", "Commit Current Project..." ) );
0601     action->setIconText( i18nc("@action", "Commit..." ) );
0602     action->setIcon( QIcon::fromTheme(QStringLiteral("svn-commit")) );
0603     connect(d->m_core->uiControllerInternal()->defaultMainWindow(), &MainWindow::areaChanged,
0604             this, [this] (Sublime::Area* area) { Q_D(ProjectController); d->areaChanged(area); });
0605     d->m_core->uiControllerInternal()->area(0, QStringLiteral("code"))->addAction(action);
0606 
0607     KSharedConfig * config = KSharedConfig::openConfig().data();
0608 //     KConfigGroup group = config->group( "General Options" );
0609 
0610     d->m_recentProjectsAction = KStandardAction::openRecent(this, SLOT(openProject(QUrl)), this);
0611     ac->addAction( QStringLiteral("project_open_recent"), d->m_recentProjectsAction );
0612     d->m_recentProjectsAction->setText( i18nc("@action", "Open Recent Project" ) );
0613     d->m_recentProjectsAction->setWhatsThis( i18nc( "@info:whatsthis", "Opens recently opened project." ) );
0614     d->m_recentProjectsAction->loadEntries( KConfigGroup(config, "RecentProjects") );
0615 
0616     auto* openProjectForFileAction = new QAction( this );
0617     ac->addAction(QStringLiteral("project_open_for_file"), openProjectForFileAction);
0618     openProjectForFileAction->setText(i18nc("@action", "Open Project for Current File"));
0619     openProjectForFileAction->setIcon(QIcon::fromTheme(QStringLiteral("project-open")));
0620     connect( openProjectForFileAction, &QAction::triggered, this, &ProjectController::openProjectForUrlSlot);
0621 }
0622 
0623 ProjectController::~ProjectController()
0624 {
0625     Q_D(ProjectController);
0626 
0627     delete d->model;
0628     delete d->dialog;
0629 }
0630 
0631 void ProjectController::cleanup()
0632 {
0633     Q_D(ProjectController);
0634 
0635     if ( d->m_currentlyOpening.isEmpty() ) {
0636         d->saveListOfOpenedProjects();
0637     }
0638 
0639     saveRecentProjectsActionEntries();
0640 
0641     d->m_cleaningUp = true;
0642     if( buildSetModel() ) {
0643         buildSetModel()->storeToSession( Core::self()->activeSession() );
0644     }
0645 
0646     closeAllProjects();
0647 }
0648 
0649 void ProjectController::saveRecentProjectsActionEntries()
0650 {
0651     Q_D(ProjectController);
0652 
0653     if (!d->m_recentProjectsAction)
0654         return;
0655 
0656     auto config = KSharedConfig::openConfig();
0657     KConfigGroup recentGroup = config->group("RecentProjects");
0658     d->m_recentProjectsAction->saveEntries( recentGroup );
0659     config->sync();
0660 }
0661 
0662 void ProjectController::initialize()
0663 {
0664     Q_D(ProjectController);
0665 
0666     d->buildset = new ProjectBuildSetModel( this );
0667     buildSetModel()->loadFromSession( Core::self()->activeSession() );
0668     connect( this, &ProjectController::projectOpened,
0669              d->buildset, &ProjectBuildSetModel::loadFromProject );
0670     connect( this, &ProjectController::projectClosing,
0671              d->buildset, &ProjectBuildSetModel::saveToProject );
0672     connect( this, &ProjectController::projectClosed,
0673              d->buildset, &ProjectBuildSetModel::projectClosed );
0674 
0675     d->m_changesModel = new ProjectChangesModel(this);
0676 
0677     loadSettings(false);
0678     d->dialog = new ProjectDialogProvider(d);
0679 
0680     QDBusConnection::sessionBus().registerObject( QStringLiteral("/org/kdevelop/ProjectController"),
0681         this, QDBusConnection::ExportScriptableSlots );
0682 
0683     KSharedConfigPtr config = Core::self()->activeSession()->config();
0684     KConfigGroup group = config->group( "General Options" );
0685     const auto projects = group.readEntry( "Open Projects", QList<QUrl>() );
0686 
0687     connect( Core::self()->selectionController(), &ISelectionController::selectionChanged,
0688              this, [this]() { Q_D(ProjectController); d->updateActionStates(); } );
0689     connect(this, &ProjectController::projectOpened,
0690             this, [this]() { Q_D(ProjectController); d->updateActionStates(); });
0691     connect(this, &ProjectController::projectClosing,
0692             this, [this]() { Q_D(ProjectController); d->updateActionStates(); });
0693 
0694     QTimer::singleShot(0, this, [this, projects](){
0695         openProjects(projects);
0696         emit initialized();
0697     });
0698 }
0699 
0700 void ProjectController::openProjects(const QList<QUrl>& projects)
0701 {
0702     for (const QUrl& url : projects) {
0703         openProject(url);
0704     }
0705 }
0706 
0707 void ProjectController::loadSettings( bool projectIsLoaded )
0708 {
0709     Q_UNUSED(projectIsLoaded)
0710 }
0711 
0712 void ProjectController::saveSettings( bool projectIsLoaded )
0713 {
0714     Q_UNUSED( projectIsLoaded );
0715 }
0716 
0717 
0718 int ProjectController::projectCount() const
0719 {
0720     Q_D(const ProjectController);
0721 
0722     return d->m_projects.count();
0723 }
0724 
0725 IProject* ProjectController::projectAt( int num ) const
0726 {
0727     Q_D(const ProjectController);
0728 
0729     if( !d->m_projects.isEmpty() && num >= 0 && num < d->m_projects.count() )
0730         return d->m_projects.at( num );
0731     return nullptr;
0732 }
0733 
0734 QList<IProject*> ProjectController::projects() const
0735 {
0736     Q_D(const ProjectController);
0737 
0738     return d->m_projects;
0739 }
0740 
0741 void ProjectController::eventuallyOpenProjectFile(KIO::Job* _job, const KIO::UDSEntryList& entries)
0742 {
0743     Q_D(ProjectController);
0744 
0745     auto* job = qobject_cast<KIO::SimpleJob*>(_job);
0746     Q_ASSERT(job);
0747     for (const KIO::UDSEntry& entry : entries) {
0748         if(d->m_foundProjectFile)
0749             break;
0750         if(!entry.isDir()) {
0751             QString name = entry.stringValue( KIO::UDSEntry::UDS_NAME );
0752 
0753             if(name.endsWith(QLatin1String(".kdev4"))) {
0754                 //We have found a project-file, open it
0755                 openProject(Path(Path(job->url()), name).toUrl());
0756                 d->m_foundProjectFile = true;
0757             }
0758         }
0759     }
0760 }
0761 
0762 void ProjectController::openProjectForUrlSlot(bool) {
0763     if(ICore::self()->documentController()->activeDocument()) {
0764         QUrl url = ICore::self()->documentController()->activeDocument()->url();
0765         IProject* project = ICore::self()->projectController()->findProjectForUrl(url);
0766         if(!project) {
0767             openProjectForUrl(url);
0768         }else{
0769             auto* message = new Sublime::Message(i18n("Project already open: %1", project->name()), Sublime::Message::Error);
0770             Core::self()->uiController()->postMessage(message);
0771         }
0772     }else{
0773         auto* message = new Sublime::Message(i18n("No active document"), Sublime::Message::Error);
0774         Core::self()->uiController()->postMessage(message);
0775     }
0776 }
0777 
0778 
0779 void ProjectController::openProjectForUrl(const QUrl& sourceUrl) {
0780     Q_D(ProjectController);
0781 
0782     Q_ASSERT(!sourceUrl.isRelative());
0783     QUrl dirUrl = sourceUrl;
0784     if (sourceUrl.isLocalFile() && !QFileInfo(sourceUrl.toLocalFile()).isDir()) {
0785         dirUrl = dirUrl.adjusted(QUrl::RemoveFilename);
0786     }
0787     QUrl testAt = dirUrl;
0788 
0789     d->m_foundProjectFile = false;
0790 
0791     while(!testAt.path().isEmpty()) {
0792         KIO::ListJob* job = KIO::listDir(testAt);
0793 
0794         connect(job, &KIO::ListJob::entries, this, &ProjectController::eventuallyOpenProjectFile);
0795         KJobWidgets::setWindow(job, ICore::self()->uiController()->activeMainWindow());
0796         job->exec();
0797         if(d->m_foundProjectFile) {
0798             //Fine! We have directly opened the project
0799             d->m_foundProjectFile = false;
0800             return;
0801         }
0802         QUrl oldTest = testAt.adjusted(QUrl::RemoveFilename);
0803         if(oldTest == testAt)
0804             break;
0805     }
0806 
0807     QUrl askForOpen = d->dialog->askProjectConfigLocation(false, dirUrl);
0808 
0809     if(askForOpen.isValid())
0810         openProject(askForOpen);
0811 }
0812 
0813 void ProjectController::openProject( const QUrl &projectFile )
0814 {
0815     Q_D(ProjectController);
0816 
0817     QUrl url = projectFile;
0818 
0819     if ( url.isEmpty() ) {
0820         url = d->dialog->askProjectConfigLocation(false);
0821         if ( url.isEmpty() ) {
0822             return;
0823         }
0824     }
0825 
0826     Q_ASSERT(!url.isRelative());
0827 
0828     QList<const Session*> existingSessions;
0829     if(!Core::self()->sessionController()->activeSession()->containedProjects().contains(url))
0830     {
0831         const auto sessions = Core::self()->sessionController()->sessions();
0832         for (const Session* session : sessions) {
0833             if(session->containedProjects().contains(url))
0834             {
0835                 existingSessions << session;
0836 #if 0
0837                     ///@todo Think about this! Problem: The session might already contain files, the debugger might be active, etc.
0838                     //If this session is empty, close it
0839                     if(Core::self()->sessionController()->activeSession()->description().isEmpty())
0840                     {
0841                         //Terminate this instance of kdevelop if the user agrees
0842                         const auto windows = Core::self()->uiController()->controller()->mainWindows();
0843                         for (Sublime::MainWindow* window : windows) {
0844                             window->close();
0845                         }
0846                     }
0847 #endif
0848             }
0849         }
0850     }
0851 
0852     if ( ! existingSessions.isEmpty() ) {
0853         ScopedDialog<QDialog> dialog(Core::self()->uiControllerInternal()->activeMainWindow());
0854         dialog->setWindowTitle(i18nc("@title:window", "Project Already Open"));
0855 
0856         auto mainLayout = new QVBoxLayout(dialog);
0857         mainLayout->addWidget(new QLabel(i18n("The project you're trying to open is already open in at least one "
0858                                                      "other session.<br>What do you want to do?")));
0859         QGroupBox sessions;
0860         sessions.setLayout(new QVBoxLayout);
0861         auto* newSession = new QRadioButton(i18nc("@option:radio", "Add project to current session"));
0862         sessions.layout()->addWidget(newSession);
0863         newSession->setChecked(true);
0864         for (const Session* session : qAsConst(existingSessions)) {
0865             auto* button = new QRadioButton(i18nc("@option:radio", "Open session %1", session->description()));
0866             button->setProperty("sessionid", session->id().toString());
0867             sessions.layout()->addWidget(button);
0868         }
0869         sessions.layout()->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Expanding));
0870         mainLayout->addWidget(&sessions);
0871 
0872         auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Abort);
0873         auto okButton = buttonBox->button(QDialogButtonBox::Ok);
0874         okButton->setDefault(true);
0875         okButton->setShortcut(Qt::CTRL | Qt::Key_Return);
0876         connect(buttonBox, &QDialogButtonBox::accepted, dialog.data(), &QDialog::accept);
0877         connect(buttonBox, &QDialogButtonBox::rejected, dialog.data(), &QDialog::reject);
0878         mainLayout->addWidget(buttonBox);
0879 
0880         if (!dialog->exec())
0881             return;
0882 
0883         for (const QObject* obj : sessions.children()) {
0884             if ( const auto* button = qobject_cast<const QRadioButton*>(obj) ) {
0885                 QString sessionid = button->property("sessionid").toString();
0886                 if ( button->isChecked() && ! sessionid.isEmpty() ) {
0887                     Core::self()->sessionController()->loadSession(sessionid);
0888                     return;
0889                 }
0890             }
0891         }
0892     }
0893 
0894     if ( url.isEmpty() )
0895     {
0896         url = d->dialog->askProjectConfigLocation(false);
0897     }
0898 
0899     if ( !url.isEmpty() )
0900     {
0901         d->importProject(url);
0902     }
0903 }
0904 
0905 bool ProjectController::fetchProjectFromUrl(const QUrl& repoUrl, FetchFlags fetchFlags)
0906 {
0907     Q_D(ProjectController);
0908 
0909     IPlugin* vcsOrProviderPlugin = nullptr;
0910 
0911     // TODO: query also projectprovider plugins, and that before plain vcs plugins
0912     // e.g. KDE provider plugin could catch URLs from mirror or pickup kde:repo things
0913     auto* pluginController = d->m_core->pluginController();
0914     const auto& vcsPlugins = pluginController->allPluginsForExtension(QStringLiteral("org.kdevelop.IBasicVersionControl"));
0915 
0916     for (auto* plugin : vcsPlugins) {
0917         auto* iface = plugin->extension<IBasicVersionControl>();
0918         if (iface->isValidRemoteRepositoryUrl(repoUrl)) {
0919             vcsOrProviderPlugin = plugin;
0920             break;
0921         }
0922     }
0923     if (!vcsOrProviderPlugin) {
0924         if (fetchFlags.testFlag(FetchShowErrorIfNotSupported)) {
0925             const QString messageText =
0926                 i18n("No enabled plugin supports this repository URL: %1", repoUrl.toDisplayString());
0927             auto* message = new Sublime::Message(messageText, Sublime::Message::Error);
0928             ICore::self()->uiController()->postMessage(message);
0929         }
0930         return false;
0931     }
0932 
0933     const QUrl url = d->dialog->askProjectConfigLocation(true, QUrl(), repoUrl, vcsOrProviderPlugin);
0934 
0935     if (!url.isEmpty()) {
0936         d->importProject(url);
0937     }
0938 
0939     return true;
0940 }
0941 
0942 void ProjectController::fetchProject()
0943 {
0944     Q_D(ProjectController);
0945 
0946     QUrl url = d->dialog->askProjectConfigLocation(true);
0947 
0948     if ( !url.isEmpty() )
0949     {
0950         d->importProject(url);
0951     }
0952 }
0953 
0954 void ProjectController::projectImportingFinished( IProject* project )
0955 {
0956     Q_D(ProjectController);
0957 
0958     if( !project )
0959     {
0960         qCWarning(SHELL) << "OOOPS: 0-pointer project";
0961         return;
0962     }
0963     IPlugin *managerPlugin = project->managerPlugin();
0964     QList<IPlugin*> pluglist;
0965     pluglist.append( managerPlugin );
0966     d->m_projectPlugins.insert( project, pluglist );
0967     d->m_projects.append( project );
0968 
0969     if ( d->m_currentlyOpening.isEmpty() ) {
0970         d->saveListOfOpenedProjects();
0971     }
0972 
0973     if (Core::self()->setupFlags() != Core::NoUi)
0974     {
0975         d->m_recentProjectsAction->addUrl( project->projectFile().toUrl() );
0976         saveRecentProjectsActionEntries();
0977     }
0978 
0979     Q_ASSERT(d->m_currentlyOpening.contains(project->projectFile().toUrl()));
0980     d->m_currentlyOpening.removeAll(project->projectFile().toUrl());
0981     emit projectOpened( project );
0982 
0983     reparseProject(project);
0984 }
0985 
0986 // helper method for closeProject()
0987 void ProjectController::unloadUnusedProjectPlugins(IProject* proj)
0988 {
0989     Q_D(ProjectController);
0990 
0991     const QList<IPlugin*> pluginsForProj = d->m_projectPlugins.value( proj );
0992     d->m_projectPlugins.remove( proj );
0993 
0994     QList<IPlugin*> otherProjectPlugins;
0995     for (const QList<IPlugin*>& _list : qAsConst(d->m_projectPlugins)) {
0996         otherProjectPlugins << _list;
0997     }
0998 
0999     QSet<IPlugin*> pluginsForProjSet(pluginsForProj.begin(), pluginsForProj.end());
1000     QSet<IPlugin*> otherPrjPluginsSet(otherProjectPlugins.constBegin(), otherProjectPlugins.constEnd());
1001     // loaded - target = tobe unloaded.
1002     const QSet<IPlugin*> tobeRemoved = pluginsForProjSet.subtract( otherPrjPluginsSet );
1003     for (IPlugin* _plugin : tobeRemoved) {
1004         KPluginMetaData _plugInfo = Core::self()->pluginController()->pluginInfo( _plugin );
1005         if( _plugInfo.isValid() )
1006         {
1007             QString _plugName = _plugInfo.pluginId();
1008             qCDebug(SHELL) << "about to unloading :" << _plugName;
1009             Core::self()->pluginController()->unloadPlugin( _plugName );
1010         }
1011     }
1012 }
1013 
1014 // helper method for closeProject()
1015 void ProjectController::closeAllOpenedFiles(IProject* proj)
1016 {
1017     const auto documents = Core::self()->documentController()->openDocuments();
1018     for (IDocument* doc : documents) {
1019         if (proj->inProject(IndexedString(doc->url()))) {
1020             doc->close();
1021         }
1022     }
1023 }
1024 
1025 // helper method for closeProject()
1026 void ProjectController::initializePluginCleanup(IProject* proj)
1027 {
1028     // Unloading (and thus deleting) these plugins is not a good idea just yet
1029     // as we're being called by the view part and it gets deleted when we unload the plugin(s)
1030     // TODO: find a better place to unload
1031     connect(proj, &IProject::destroyed,
1032             this, [this] { Q_D(ProjectController); d->unloadAllProjectPlugins(); });
1033 }
1034 
1035 void ProjectController::takeProject(IProject* proj)
1036 {
1037     Q_D(ProjectController);
1038 
1039     if (!proj) {
1040         return;
1041     }
1042 
1043     // loading might have failed
1044     d->m_currentlyOpening.removeAll(proj->projectFile().toUrl());
1045     d->m_projects.removeAll(proj);
1046     if (auto* job = d->m_parseJobs.value(proj)) {
1047         job->kill(); // Removes job from m_parseJobs.
1048     }
1049     emit projectClosing(proj);
1050     //Core::self()->saveSettings();     // The project file is being closed.
1051                                         // Now we can save settings for all of the Core
1052                                         // objects including this one!!
1053     unloadUnusedProjectPlugins(proj);
1054     closeAllOpenedFiles(proj);
1055     proj->close();
1056     if (d->m_projects.isEmpty())
1057     {
1058         initializePluginCleanup(proj);
1059     }
1060 
1061     if(!d->m_cleaningUp)
1062         d->saveListOfOpenedProjects();
1063 
1064     emit projectClosed(proj);
1065 }
1066 
1067 void ProjectController::closeProject(IProject* proj)
1068 {
1069     takeProject(proj);
1070     proj->deleteLater(); // be safe when deleting
1071 }
1072 
1073 void ProjectController::closeAllProjects()
1074 {
1075     Q_D(ProjectController);
1076 
1077     const auto projects = d->m_projects;
1078     for (auto* project : projects) {
1079         closeProject(project);
1080     }
1081 }
1082 
1083 void ProjectController::abortOpeningProject(IProject* proj)
1084 {
1085     Q_D(ProjectController);
1086 
1087     d->m_currentlyOpening.removeAll(proj->projectFile().toUrl());
1088     emit projectOpeningAborted(proj);
1089 }
1090 
1091 ProjectModel* ProjectController::projectModel()
1092 {
1093     Q_D(ProjectController);
1094 
1095     return d->model;
1096 }
1097 
1098 IProject* ProjectController::findProjectForUrl( const QUrl& url ) const
1099 {
1100     Q_D(const ProjectController);
1101 
1102     if (d->m_projects.isEmpty()) {
1103         return nullptr;
1104     }
1105 
1106     ProjectBaseItem* item = d->model->itemForPath(IndexedString(url));
1107     if (item) {
1108         return item->project();
1109     }
1110     return nullptr;
1111 }
1112 
1113 IProject* ProjectController::findProjectByName( const QString& name )
1114 {
1115     Q_D(ProjectController);
1116 
1117     auto it = std::find_if(d->m_projects.constBegin(), d->m_projects.constEnd(), [&](IProject* proj) {
1118         return (proj->name() == name);
1119     });
1120     return (it != d->m_projects.constEnd()) ? *it : nullptr;
1121 }
1122 
1123 
1124 void ProjectController::configureProject( IProject* project )
1125 {
1126     Q_D(ProjectController);
1127 
1128     d->projectConfig( project );
1129 }
1130 
1131 void ProjectController::addProject(IProject* project)
1132 {
1133     Q_D(ProjectController);
1134 
1135     Q_ASSERT(project);
1136     if (d->m_projects.contains(project)) {
1137         qCWarning(SHELL) << "Project already tracked by this project controller:" << project;
1138         return;
1139     }
1140 
1141     // fake-emit signals so listeners are aware of a new project being added
1142     emit projectAboutToBeOpened(project);
1143     project->setParent(this);
1144     d->m_projects.append(project);
1145     emit projectOpened(project);
1146 }
1147 
1148 bool ProjectController::isProjectNameUsed( const QString& name ) const
1149 {
1150     const auto projects = this->projects();
1151     return std::any_of(projects.begin(), projects.end(), [&](IProject* p) {
1152         return (p->name() == name);
1153     });
1154 }
1155 
1156 QUrl ProjectController::projectsBaseDirectory() const
1157 {
1158     KConfigGroup group = ICore::self()->activeSession()->config()->group( "Project Manager" );
1159     return group.readEntry("Projects Base Directory", QUrl::fromLocalFile(QDir::homePath() + QLatin1String("/projects")));
1160 }
1161 
1162 QString ProjectController::prettyFilePath(const QUrl& url, FormattingOptions format) const
1163 {
1164     IProject* project = Core::self()->projectController()->findProjectForUrl(url);
1165 
1166     if(!project)
1167     {
1168         // Find a project with the correct base directory at least
1169         const auto projects = Core::self()->projectController()->projects();
1170         auto it = std::find_if(projects.begin(), projects.end(), [&](IProject* candidateProject) {
1171             return (candidateProject->path().toUrl().isParentOf(url));
1172         });
1173         if (it != projects.end()) {
1174             project = *it;
1175         }
1176     }
1177 
1178     Path parent = Path(url).parent();
1179     QString prefixText;
1180     if (project) {
1181         if (format == FormatHtml) {
1182             prefixText = QLatin1String("<i>") +  project->name() + QLatin1String("</i>/");
1183         } else {
1184             prefixText = project->name() + QLatin1Char(':');
1185         }
1186         QString relativePath = project->path().relativePath(parent);
1187         if(relativePath.startsWith(QLatin1String("./"))) {
1188             relativePath.remove(0, 2);
1189         }
1190         if (!relativePath.isEmpty()) {
1191             prefixText += relativePath + QLatin1Char('/');
1192         }
1193     } else {
1194         prefixText = parent.pathOrUrl() + QLatin1Char('/');
1195     }
1196     return prefixText;
1197 }
1198 
1199 QString ProjectController::prettyFileName(const QUrl& url, FormattingOptions format) const
1200 {
1201     IProject* project = Core::self()->projectController()->findProjectForUrl(url);
1202     if(project && project->path() == Path(url))
1203     {
1204         if (format == FormatHtml) {
1205             return QLatin1String("<i>") +  project->name() + QLatin1String("</i>");
1206         } else {
1207             return project->name();
1208         }
1209     }
1210 
1211     QString prefixText = prettyFilePath( url, format );
1212     if (format == FormatHtml) {
1213         return prefixText + QLatin1String("<b>") + url.fileName() + QLatin1String("</b>");
1214     } else {
1215         return prefixText + url.fileName();
1216     }
1217 }
1218 
1219 ContextMenuExtension ProjectController::contextMenuExtension(Context* ctx, QWidget* parent)
1220 {
1221     Q_D(ProjectController);
1222 
1223     Q_UNUSED(parent);
1224     ContextMenuExtension ext;
1225     if ( ctx->type() != Context::ProjectItemContext) {
1226         return ext;
1227     }
1228     if (!static_cast<ProjectItemContext*>(ctx)->items().isEmpty() ) {
1229 
1230         auto* action = new QAction(i18nc("@action", "Reparse the Entire Project"), this);
1231         connect(action, &QAction::triggered, this, [this] {
1232             Q_D(ProjectController);
1233             const auto projects = d->selectedProjects();
1234             for (auto* project : projects) {
1235                 reparseProject(project, true, true);
1236             }
1237         });
1238 
1239         ext.addAction(ContextMenuExtension::ProjectGroup, action);
1240         return ext;
1241     }
1242 
1243     ext.addAction(ContextMenuExtension::ProjectGroup, d->m_openProject);
1244     ext.addAction(ContextMenuExtension::ProjectGroup, d->m_fetchProject);
1245     ext.addAction(ContextMenuExtension::ProjectGroup, d->m_recentProjectsAction);
1246 
1247     return ext;
1248 }
1249 
1250 ProjectBuildSetModel* ProjectController::buildSetModel()
1251 {
1252     Q_D(ProjectController);
1253 
1254     return d->buildset;
1255 }
1256 
1257 ProjectChangesModel* ProjectController::changesModel()
1258 {
1259     Q_D(ProjectController);
1260 
1261     return d->m_changesModel;
1262 }
1263 
1264 void ProjectController::commitCurrentProject()
1265 {
1266     IDocument* doc=ICore::self()->documentController()->activeDocument();
1267     if(!doc)
1268         return;
1269 
1270     QUrl url=doc->url();
1271     IProject* project = ICore::self()->projectController()->findProjectForUrl(url);
1272 
1273     if(project && project->versionControlPlugin()) {
1274         IPlugin* plugin = project->versionControlPlugin();
1275         auto* vcs=plugin->extension<IBasicVersionControl>();
1276 
1277         if(vcs) {
1278             ICore::self()->documentController()->saveAllDocuments(KDevelop::IDocument::Silent);
1279 
1280             const Path basePath = project->path();
1281             auto* patchSource = new VCSCommitDiffPatchSource(new VCSStandardDiffUpdater(vcs, basePath.toUrl()));
1282 
1283             bool ret = showVcsDiff(patchSource);
1284 
1285             if(!ret) {
1286                 ScopedDialog<VcsCommitDialog> commitDialog(patchSource);
1287                 commitDialog->setCommitCandidates(patchSource->infos());
1288                 commitDialog->exec();
1289             }
1290         }
1291     }
1292 }
1293 
1294 QString ProjectController::mapSourceBuild( const QString& path_, bool reverse, bool fallbackRoot ) const
1295 {
1296     Q_D(const ProjectController);
1297 
1298     Path path(path_);
1299     IProject* sourceDirProject = nullptr, *buildDirProject = nullptr;
1300     for (IProject* proj : qAsConst(d->m_projects)) {
1301         if(proj->path().isParentOf(path) || proj->path() == path)
1302             sourceDirProject = proj;
1303         if(proj->buildSystemManager())
1304         {
1305             Path buildDir = proj->buildSystemManager()->buildDirectory(proj->projectItem());
1306             if(buildDir.isValid() && (buildDir.isParentOf(path) || buildDir == path))
1307                 buildDirProject = proj;
1308         }
1309     }
1310 
1311     if(!reverse)
1312     {
1313         // Map-target is the build directory
1314         if(sourceDirProject && sourceDirProject->buildSystemManager())
1315         {
1316             // We're in the source, map into the build directory
1317             QString relativePath = sourceDirProject->path().relativePath(path);
1318 
1319             Path build = sourceDirProject->buildSystemManager()->buildDirectory(sourceDirProject->projectItem());
1320             build.addPath(relativePath);
1321             while(!QFile::exists(build.path()))
1322                 build = build.parent();
1323             return build.pathOrUrl();
1324         }else if(buildDirProject && fallbackRoot)
1325         {
1326             // We're in the build directory, map to the build directory root
1327             return buildDirProject->buildSystemManager()->buildDirectory(buildDirProject->projectItem()).pathOrUrl();
1328         }
1329     }else{
1330         // Map-target is the source directory
1331         if(buildDirProject)
1332         {
1333             Path build = buildDirProject->buildSystemManager()->buildDirectory(buildDirProject->projectItem());
1334             // We're in the source, map into the build directory
1335             QString relativePath = build.relativePath(path);
1336 
1337             Path source = buildDirProject->path();
1338             source.addPath(relativePath);
1339             while(!QFile::exists(source.path()))
1340                 source = source.parent();
1341             return source.pathOrUrl();
1342         }else if(sourceDirProject && fallbackRoot)
1343         {
1344             // We're in the source directory, map to the root
1345             return sourceDirProject->path().pathOrUrl();
1346         }
1347     }
1348     return QString();
1349 }
1350 
1351 void ProjectController::reparseProject(IProject* project, bool forceUpdate, bool forceAll)
1352 {
1353     Q_D(ProjectController);
1354 
1355     if (auto* oldJob = d->m_parseJobs.value(project)) {
1356         oldJob->kill(); // Removes oldJob from m_parseJobs.
1357     }
1358 
1359     auto& job = d->m_parseJobs[project];
1360     job = new ParseProjectJob(project, forceUpdate, forceAll || parseAllProjectSources());
1361     connect(job, &KJob::finished, this, [d, project](KJob* job) {
1362         const auto it = d->m_parseJobs.constFind(project);
1363         if (it != d->m_parseJobs.cend() && *it == job) {
1364             d->m_parseJobs.erase(it);
1365         }
1366     });
1367     ICore::self()->runController()->registerJob(job);
1368 }
1369 
1370 }
1371 
1372 #include "moc_projectcontroller.cpp"