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"