diff --git a/kdevplatform/interfaces/iprojectcontroller.h b/kdevplatform/interfaces/iprojectcontroller.h index a740b7ef72..d82ca974ed 100644 --- a/kdevplatform/interfaces/iprojectcontroller.h +++ b/kdevplatform/interfaces/iprojectcontroller.h @@ -1,204 +1,208 @@ /* This file is part of KDevelop Copyright 2006 Adam Treat Copyright 2007 Andreas Pakulat This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KDEVPLATFORM_IPROJECTCONTROLLER_H #define KDEVPLATFORM_IPROJECTCONTROLLER_H #include #include #include #include "interfacesexport.h" namespace KDevelop { class IProject; class ProjectBuildSetModel; class ProjectModel; class ProjectBaseItem; class ProjectChangesModel; /** * @class IProjectController */ class KDEVPLATFORMINTERFACES_EXPORT IProjectController : public QObject { Q_OBJECT public: explicit IProjectController( QObject *parent = nullptr ); ~IProjectController() override; Q_INVOKABLE virtual KDevelop::IProject* projectAt( int ) const = 0; Q_INVOKABLE virtual int projectCount() const = 0; Q_INVOKABLE virtual QList projects() const = 0; /** * Provides access to the model representing the open projects * @returns the model containing the projects and their items */ virtual ProjectModel* projectModel() = 0; /** * @returns an instance to the model that keeps track of the state * of the files per project. */ virtual ProjectChangesModel* changesModel() = 0; virtual ProjectBuildSetModel* buildSetModel() = 0; /** * Find an open project using the name of the project * @param name the name of the project to be found * @returns the project or null if no project with that name is open */ virtual KDevelop::IProject* findProjectByName( const QString& name ) = 0; /** * Finding an open project for a given file or folder in the project * @param url the url of a file/folder belonging to an open project * @returns the first open project containing the url or null if no such * project can be found */ virtual IProject* findProjectForUrl( const QUrl& url ) const = 0; /** * Checks whether the given project name is used already or not. The project * controller supports only 1 project with a given name to be open at any time * @param name the name of the project to be opened or created * @returns whether the name is already used for an open project */ virtual bool isProjectNameUsed( const QString& name ) const = 0; virtual QUrl projectsBaseDirectory() const = 0; enum FormattingOptions { FormatHtml, FormatPlain }; /** * Returns a pretty short representation of the base path of the url, considering the currently loaded projects: * When the file is part of a currently loaded project, that projects name is shown as prefix instead of the * the full file path. * The returned path always has a training slash. * @param format formatting used for the string */ virtual QString prettyFilePath(const QUrl& url, FormattingOptions format = FormatHtml) const = 0; /** * Returns a pretty short representation of the given url, considering the currently loaded projects: * When the file is part of a currently loaded project, that projects name is shown as prefix instead of the * the full file path. * @param format formatting used for the string */ virtual QString prettyFileName(const QUrl& url, FormattingOptions format = FormatHtml) const = 0; /** * @returns whether project files should be parsed or not */ static bool parseAllProjectSources(); public Q_SLOTS: /** * Tries finding a project-file for the given source-url and opens it. * If no .kdev4 project file is found, the user is asked to import a project. */ virtual void openProjectForUrl( const QUrl &sourceUrl ) = 0; /** * open the project from the given kdev4 project file. This only reads * the file and starts creating the project model from it. The opening process * is finished once @ref projectOpened signal is emitted. * @param url a kdev4 project file top open */ virtual void openProject( const QUrl & url = QUrl() ) = 0; /** * close the given project. Closing the project is done in steps and * the @ref projectClosing and @ref projectClosed signals are emitted. Only when * the latter signal is emitted it is guaranteed that the project has been closed. * The @ref IProject object will be deleted after the closing has finished. */ virtual void closeProject( IProject* ) = 0; /** * Close all projects * * This usually calls closeProject() on all controlled projects * @sa closeProject() */ virtual void closeAllProjects() = 0; virtual void configureProject( IProject* ) = 0; - /// Schedules all files of the @p project for reparsing by @see BackgroundParser - virtual void reparseProject( IProject* project, bool ForceUpdate = false ) = 0; + /** + * Schedules all files of the @p project for reparsing by @see BackgroundParser + * The @p forceAll argument is for triggering a full reparse of the entire project + * after the initial import. + */ + virtual void reparseProject( IProject* project, bool forceUpdate = false, bool forceAll = false ) = 0; // virtual void changeCurrentProject( KDevelop::ProjectBaseItem* ) = 0; Q_SIGNALS: /** * Emitted right before a project is started to be loaded. * * At this point all sanity checks have been done, so the project * is really going to be loaded. Will be followed by @ref projectOpened signal * when loading completes or by @ref projectOpeningAborted if there are errors during loading * or it is aborted. * * @param project the project that is about to be opened. */ void projectAboutToBeOpened( KDevelop::IProject* project ); /** * emitted after a project is completely opened and the project model * has been populated. * @param project the project that has been opened. */ void projectOpened( KDevelop::IProject* project ); /** * emitted when starting to close a project that has been completely loaded before * (the @ref projectOpened signal has been emitted). * @param project the project that is going to be closed. */ void projectClosing( KDevelop::IProject* project ); /** * emitted when a project has been closed completely. * The project object is still valid, the deletion will be done * delayed during the next run of the event loop. * @param project the project that has been closed. */ void projectClosed( KDevelop::IProject* project ); /** * emitted when a project could not be loaded correctly or loading was aborted. * @p project contents may not be initialized properly. * @param project the project which loading has been aborted. */ void projectOpeningAborted( KDevelop::IProject* project ); /** * emitted whenever the project configuration dialog accepted * changes * @param project the project whose configuration has changed */ void projectConfigurationChanged( KDevelop::IProject* project ); }; } #endif diff --git a/kdevplatform/language/backgroundparser/parseprojectjob.cpp b/kdevplatform/language/backgroundparser/parseprojectjob.cpp index ed834e48c6..359fdbbe54 100644 --- a/kdevplatform/language/backgroundparser/parseprojectjob.cpp +++ b/kdevplatform/language/backgroundparser/parseprojectjob.cpp @@ -1,186 +1,188 @@ /* Copyright 2009 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "parseprojectjob.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDevelop; class KDevelop::ParseProjectJobPrivate { public: - ParseProjectJobPrivate(IProject* project, bool forceUpdate) + ParseProjectJobPrivate(IProject* project, bool forceUpdate, bool forceAll) : forceUpdate(forceUpdate) + , forceAll(forceAll) , project(project) { } int updated = 0; bool forceUpdate; + bool forceAll; KDevelop::IProject* project; QSet filesToParse; }; bool ParseProjectJob::doKill() { qCDebug(LANGUAGE) << "stopping project parse job"; deleteLater(); return true; } ParseProjectJob::~ParseProjectJob() { ICore::self()->languageController()->backgroundParser()->revertAllRequests(this); if (ICore::self()->runController()->currentJobs().contains(this)) ICore::self()->runController()->unregisterJob(this); } -ParseProjectJob::ParseProjectJob(IProject* project, bool forceUpdate) - : d(new ParseProjectJobPrivate(project, forceUpdate)) +ParseProjectJob::ParseProjectJob(IProject* project, bool forceUpdate, bool forceAll) + : d(new ParseProjectJobPrivate(project, forceUpdate, forceAll)) { connect(project, &IProject::destroyed, this, &ParseProjectJob::deleteNow); - if (!ICore::self()->projectController()->parseAllProjectSources()) { + if (forceAll || ICore::self()->projectController()->parseAllProjectSources()) { + d->filesToParse = project->fileSet(); + } else { // In case we don't want to parse the whole project, still add all currently open files that belong to the project to the background-parser foreach (auto document, ICore::self()->documentController()->openDocuments()) { const auto path = IndexedString(document->url()); if (project->fileSet().contains(path)) { d->filesToParse.insert(path); } } - } else { - d->filesToParse = project->fileSet(); } setCapabilities(Killable); setObjectName(i18np("Process 1 file in %2", "Process %1 files in %2", d->filesToParse.size(), d->project->name())); } void ParseProjectJob::deleteNow() { delete this; } void ParseProjectJob::updateProgress() { } void ParseProjectJob::updateReady(const IndexedString& url, const ReferencedTopDUContext& topContext) { Q_UNUSED(url); Q_UNUSED(topContext); ++d->updated; if (d->updated % ((d->filesToParse.size() / 100) + 1) == 0) updateProgress(); if (d->updated >= d->filesToParse.size()) deleteLater(); } void ParseProjectJob::start() { if (ICore::self()->shuttingDown()) { return; } if (d->filesToParse.isEmpty()) { deleteLater(); return; } qCDebug(LANGUAGE) << "starting project parse job"; TopDUContext::Features processingLevel = d->filesToParse.size() < ICore::self()->languageController()->completionSettings()-> minFilesForSimplifiedParsing() ? TopDUContext::VisibleDeclarationsAndContexts : TopDUContext:: SimplifiedVisibleDeclarationsAndContexts; if (d->forceUpdate) { if (processingLevel & TopDUContext::VisibleDeclarationsAndContexts) { processingLevel = TopDUContext::AllDeclarationsContextsAndUses; } processingLevel = ( TopDUContext::Features )(TopDUContext::ForceUpdate | processingLevel); } if (auto currentDocument = ICore::self()->documentController()->activeDocument()) { const auto path = IndexedString(currentDocument->url()); auto fileIt = d->filesToParse.find(path); if (fileIt != d->filesToParse.end()) { ICore::self()->languageController()->backgroundParser()->addDocument(path, TopDUContext::AllDeclarationsContextsAndUses, BackgroundParser::BestPriority, this); d->filesToParse.erase(fileIt); } } // Add all currently open files that belong to the project to the background-parser, so that they'll be parsed first of all foreach (auto document, ICore::self()->documentController()->openDocuments()) { const auto path = IndexedString(document->url()); auto fileIt = d->filesToParse.find(path); if (fileIt != d->filesToParse.end()) { ICore::self()->languageController()->backgroundParser()->addDocument(path, TopDUContext::AllDeclarationsContextsAndUses, 10, this); d->filesToParse.erase(fileIt); } } - if (!ICore::self()->projectController()->parseAllProjectSources()) { + if (!d->forceAll && !ICore::self()->projectController()->parseAllProjectSources()) { return; } // prevent UI-lockup by processing events after some files // esp. noticeable when dealing with huge projects const int processAfter = 1000; int processed = 0; // guard against reentrancy issues, see also bug 345480 auto crashGuard = QPointer {this}; foreach (const IndexedString& url, d->filesToParse) { ICore::self()->languageController()->backgroundParser()->addDocument(url, processingLevel, BackgroundParser::InitialParsePriority, this); ++processed; if (processed == processAfter) { QApplication::processEvents(); if (!crashGuard) { return; } processed = 0; } } } diff --git a/kdevplatform/language/backgroundparser/parseprojectjob.h b/kdevplatform/language/backgroundparser/parseprojectjob.h index 183b7ad5d5..91e3d0fdd2 100644 --- a/kdevplatform/language/backgroundparser/parseprojectjob.h +++ b/kdevplatform/language/backgroundparser/parseprojectjob.h @@ -1,56 +1,62 @@ /* Copyright 2009 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KDEVPLATFORM_PARSEPROJECTJOB_H #define KDEVPLATFORM_PARSEPROJECTJOB_H #include #include #include namespace KDevelop { class ReferencedTopDUContext; class IProject; -///A job that parses all project-files in the given project -///Deletes itself as soon as the project is deleted +///A job that parses all project-files in the given project either +///when KDevelop is configured to parse all files at project import +///(see ProjectController:parseAllProjectSources()) or when the +///forceAll argument is true. That forceAll argument allows to +///trigger a full project reparse after the initial import, e.g. +///via the project manager's context menu. +///ParseProjectJob instances delete themselves as soon as the project +///is deleted or when a new job is started. class KDEVPLATFORMLANGUAGE_EXPORT ParseProjectJob : public KJob { Q_OBJECT public: - explicit ParseProjectJob(KDevelop::IProject* project, bool forceUpdate = false); + explicit ParseProjectJob(KDevelop::IProject* project, bool forceUpdate = false, bool forceAll = false); ~ParseProjectJob() override; void start() override; bool doKill() override; private Q_SLOTS: void deleteNow(); void updateReady(const KDevelop::IndexedString& url, const KDevelop::ReferencedTopDUContext& topContext); private: void updateProgress(); private: const QScopedPointer d; }; } #endif // KDEVPLATFORM_PARSEPROJECTJOB_H diff --git a/kdevplatform/shell/projectcontroller.cpp b/kdevplatform/shell/projectcontroller.cpp index 0a192d9009..1c233a4685 100644 --- a/kdevplatform/shell/projectcontroller.cpp +++ b/kdevplatform/shell/projectcontroller.cpp @@ -1,1298 +1,1307 @@ /* This file is part of KDevelop Copyright 2006 Adam Treat Copyright 2007 Andreas Pakulat This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "projectcontroller.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "core.h" // TODO: Should get rid off this include (should depend on IProject only) #include "project.h" #include "mainwindow.h" #include "shellextension.h" #include "plugincontroller.h" #include "configdialog.h" #include "uicontroller.h" #include "documentcontroller.h" #include "openprojectdialog.h" #include "sessioncontroller.h" #include "session.h" #include "debug.h" namespace KDevelop { class ProjectControllerPrivate { public: QList m_projects; QMap< IProject*, QList > m_projectPlugins; QPointer m_recentProjectsAction; Core* const m_core; // IProject* m_currentProject; ProjectModel* const model; QPointer m_openProject; QPointer m_fetchProject; QPointer m_closeProject; QPointer m_openConfig; IProjectDialogProvider* dialog; QList m_currentlyOpening; // project-file urls that are being opened ProjectController* const q; ProjectBuildSetModel* buildset; bool m_foundProjectFile; //Temporary flag used while searching the hierarchy for a project file bool m_cleaningUp; //Temporary flag enabled while destroying the project-controller ProjectChangesModel* m_changesModel = nullptr; QHash< IProject*, QPointer > m_parseJobs; // parse jobs that add files from the project to the background parser. ProjectControllerPrivate(Core* core, ProjectController* p) : m_core(core) , model(new ProjectModel()) , dialog(nullptr) , q(p) , buildset(nullptr) , m_foundProjectFile(false) , m_cleaningUp(false) { } void unloadAllProjectPlugins() { if( m_projects.isEmpty() ) m_core->pluginControllerInternal()->unloadProjectPlugins(); } void projectConfig( QObject * obj ) { if( !obj ) return; auto* proj = qobject_cast(obj); if( !proj ) return; auto cfgDlg = new KDevelop::ConfigDialog(m_core->uiController()->activeMainWindow()); cfgDlg->setAttribute(Qt::WA_DeleteOnClose); cfgDlg->setModal(true); QVector configPages; ProjectConfigOptions options; options.developerFile = proj->developerFile(); options.developerTempFile = proj->developerTempFile(); options.projectTempFile = proj->projectTempFile(); options.project = proj; foreach (IPlugin* plugin, findPluginsForProject(proj)) { const int perProjectConfigPagesCount = plugin->perProjectConfigPages(); configPages.reserve(configPages.size() + perProjectConfigPagesCount); for (int i = 0; i < perProjectConfigPagesCount; ++i) { configPages.append(plugin->perProjectConfigPage(i, options, cfgDlg)); } } std::sort(configPages.begin(), configPages.end(), [](const ConfigPage* a, const ConfigPage* b) { return a->name() < b->name(); }); for (auto page : configPages) { cfgDlg->appendConfigPage(page); } QObject::connect(cfgDlg, &ConfigDialog::configSaved, cfgDlg, [this, proj](ConfigPage* page) { Q_UNUSED(page) Q_ASSERT_X(proj, Q_FUNC_INFO, "ConfigDialog signalled project config change, but no project set for configuring!"); emit q->projectConfigurationChanged(proj); }); cfgDlg->setWindowTitle(i18n("Configure Project %1", proj->name())); QObject::connect(cfgDlg, &KDevelop::ConfigDialog::finished, proj, [proj]() { proj->projectConfiguration()->sync(); }); cfgDlg->show(); } void saveListOfOpenedProjects() { auto activeSession = Core::self()->activeSession(); if (!activeSession) { return; } QList openProjects; openProjects.reserve( m_projects.size() ); foreach( IProject* project, m_projects ) { openProjects.append(project->projectFile().toUrl()); } activeSession->setContainedProjects( openProjects ); } // Recursively collects builder dependencies for a project. static void collectBuilders( QList< IProjectBuilder* >& destination, IProjectBuilder* topBuilder, IProject* project ) { const QList auxBuilders = topBuilder->additionalBuilderPlugins(project); destination.append( auxBuilders ); for (IProjectBuilder* auxBuilder : auxBuilders ) { collectBuilders( destination, auxBuilder, project ); } } QVector findPluginsForProject( IProject* project ) const { const QList plugins = m_core->pluginController()->loadedPlugins(); const IBuildSystemManager* const buildSystemManager = project->buildSystemManager(); QVector projectPlugins; QList buildersForKcm; // Important to also include the "top" builder for the project, so // projects with only one such builder are kept working. Otherwise the project config // dialog is empty for such cases. if (buildSystemManager) { buildersForKcm << buildSystemManager->builder(); collectBuilders( buildersForKcm, buildSystemManager->builder(), project ); } for (auto plugin : plugins) { auto info = m_core->pluginController()->pluginInfo(plugin); auto* manager = plugin->extension(); if( manager && manager != project->projectFileManager() ) { // current plugin is a manager but does not apply to given project, skip continue; } auto* builder = plugin->extension(); if ( builder && !buildersForKcm.contains( builder ) ) { continue; } // Do not show config pages for analyzer tools which need a buildSystemManager // TODO: turn into generic feature to disable plugin config pages which do not apply for a project if (!buildSystemManager) { const auto required = KPluginMetaData::readStringList(info.rawData(), QStringLiteral("X-KDevelop-IRequired")); if (required.contains(QLatin1String("org.kdevelop.IBuildSystemManager"))) { continue; } } qCDebug(SHELL) << "Using plugin" << info.pluginId() << "for project" << project->name(); projectPlugins << plugin; } return projectPlugins; } void updateActionStates() { // if only one project loaded, this is always our target int itemCount = (m_projects.size() == 1) ? 1 : 0; if (itemCount == 0) { // otherwise base on selection auto* itemContext = dynamic_cast(ICore::self()->selectionController()->currentSelection()); if (itemContext) { itemCount = itemContext->items().count(); } } m_openConfig->setEnabled(itemCount == 1); m_closeProject->setEnabled(itemCount > 0); } - void openProjectConfig() - { - // if only one project loaded, this is our target - IProject *project = (m_projects.count() == 1) ? m_projects.at(0) : nullptr; - - // otherwise base on selection - if (!project) { - auto* ctx = dynamic_cast(ICore::self()->selectionController()->currentSelection()); - if (ctx && ctx->items().count() == 1) { - project = ctx->items().at(0)->project(); - } - } - - if (project) { - q->configureProject(project); - } - } - - void closeSelectedProjects() + QSet selectedProjects() { QSet projects; // if only one project loaded, this is our target if (m_projects.count() == 1) { projects.insert(m_projects.at(0)); } else { // otherwise base on selection - auto* ctx = dynamic_cast(ICore::self()->selectionController()->currentSelection()); + auto* ctx = dynamic_cast(ICore::self()->selectionController()->currentSelection()); if (ctx) { foreach (ProjectBaseItem* item, ctx->items()) { projects.insert(item->project()); } } } + return projects; + } + + void openProjectConfig() + { + auto projects = selectedProjects(); + + if (projects.count() == 1) { + q->configureProject(*projects.constBegin()); + } + } - foreach (IProject* project, projects) { + void closeSelectedProjects() + { + foreach (IProject* project, selectedProjects()) { q->closeProject(project); } } void importProject(const QUrl& url_) { QUrl url(url_); if (url.isLocalFile()) { const QString path = QFileInfo(url.toLocalFile()).canonicalFilePath(); if (!path.isEmpty()) { url = QUrl::fromLocalFile(path); } } if ( !url.isValid() ) { KMessageBox::error(Core::self()->uiControllerInternal()->activeMainWindow(), i18n("Invalid Location: %1", url.toDisplayString(QUrl::PreferLocalFile))); return; } if ( m_currentlyOpening.contains(url)) { qCDebug(SHELL) << "Already opening " << url << ". Aborting."; KPassivePopup::message( i18n( "Project already being opened"), i18n( "Already opening %1, not opening again", url.toDisplayString(QUrl::PreferLocalFile) ), m_core->uiController()->activeMainWindow() ); return; } foreach( IProject* project, m_projects ) { if ( url == project->projectFile().toUrl() ) { if ( dialog->userWantsReopen() ) { // close first, then open again by falling through q->closeProject(project); } else { // abort return; } } } m_currentlyOpening += url; m_core->pluginControllerInternal()->loadProjectPlugins(); auto* project = new Project(); QObject::connect(project, &Project::aboutToOpen, q, &ProjectController::projectAboutToBeOpened); if ( !project->open( Path(url) ) ) { m_currentlyOpening.removeAll(url); q->abortOpeningProject(project); project->deleteLater(); } } void areaChanged(Sublime::Area* area) { KActionCollection* ac = m_core->uiControllerInternal()->defaultMainWindow()->actionCollection(); ac->action(QStringLiteral("commit_current_project"))->setEnabled(area->objectName() == QLatin1String("code")); ac->action(QStringLiteral("commit_current_project"))->setVisible(area->objectName() == QLatin1String("code")); } }; IProjectDialogProvider::IProjectDialogProvider() {} IProjectDialogProvider::~IProjectDialogProvider() {} ProjectDialogProvider::ProjectDialogProvider(ProjectControllerPrivate* p) : d(p) {} ProjectDialogProvider::~ProjectDialogProvider() {} bool writeNewProjectFile( const QString& localConfigFile, const QString& name, const QString& createdFrom, const QString& manager ) { KSharedConfigPtr cfg = KSharedConfig::openConfig( localConfigFile, KConfig::SimpleConfig ); if (!cfg->isConfigWritable(true)) { qCDebug(SHELL) << "can't write to configfile"; return false; } KConfigGroup grp = cfg->group( "Project" ); grp.writeEntry( "Name", name ); grp.writeEntry( "CreatedFrom", createdFrom ); grp.writeEntry( "Manager", manager ); cfg->sync(); return true; } bool writeProjectSettingsToConfigFile(const QUrl& projectFileUrl, OpenProjectDialog* dlg) { if ( !projectFileUrl.isLocalFile() ) { QTemporaryFile tmp; if ( !tmp.open() ) { return false; } if ( !writeNewProjectFile( tmp.fileName(), dlg->projectName(), dlg->selectedUrl().fileName(), dlg->projectManager() ) ) { return false; } // explicitly close file before uploading it, see also: https://bugs.kde.org/show_bug.cgi?id=254519 tmp.close(); auto uploadJob = KIO::file_copy(QUrl::fromLocalFile(tmp.fileName()), projectFileUrl); KJobWidgets::setWindow(uploadJob, Core::self()->uiControllerInternal()->defaultMainWindow()); return uploadJob->exec(); } // Here and above we take .filename() part of the selectedUrl() to make it relative to the project root, // thus keeping .kdev file relocatable return writeNewProjectFile( projectFileUrl.toLocalFile(), dlg->projectName(), dlg->selectedUrl().fileName(), dlg->projectManager() ); } bool projectFileExists( const QUrl& u ) { if( u.isLocalFile() ) { return QFileInfo::exists( u.toLocalFile() ); } else { auto statJob = KIO::stat(u, KIO::StatJob::DestinationSide, 0, KIO::HideProgressInfo); KJobWidgets::setWindow(statJob, Core::self()->uiControllerInternal()->activeMainWindow()); return statJob->exec(); } } bool equalProjectFile( const QString& configPath, OpenProjectDialog* dlg ) { KSharedConfigPtr cfg = KSharedConfig::openConfig( configPath, KConfig::SimpleConfig ); KConfigGroup grp = cfg->group( "Project" ); QString defaultName = dlg->projectFileUrl().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).fileName(); return (grp.readEntry( "Name", QString() ) == dlg->projectName() || dlg->projectName() == defaultName) && grp.readEntry( "Manager", QString() ) == dlg->projectManager(); } QUrl ProjectDialogProvider::askProjectConfigLocation(bool fetch, const QUrl& startUrl, const QUrl& repoUrl, IPlugin* vcsOrProviderPlugin) { Q_ASSERT(d); ScopedDialog dlg(fetch, startUrl, repoUrl, vcsOrProviderPlugin, Core::self()->uiController()->activeMainWindow()); if(dlg->exec() == QDialog::Rejected) { return QUrl(); } QUrl projectFileUrl = dlg->projectFileUrl(); qCDebug(SHELL) << "selected project:" << projectFileUrl << dlg->projectName() << dlg->projectManager(); if ( dlg->projectManager() == QLatin1String("") ) { return projectFileUrl; } // controls if existing project file should be saved bool writeProjectConfigToFile = true; if( projectFileExists( projectFileUrl ) ) { // check whether config is equal bool shouldAsk = true; if( projectFileUrl == dlg->selectedUrl() ) { if( projectFileUrl.isLocalFile() ) { shouldAsk = !equalProjectFile( projectFileUrl.toLocalFile(), dlg ); } else { shouldAsk = false; QTemporaryFile tmpFile; if (tmpFile.open()) { auto downloadJob = KIO::file_copy(projectFileUrl, QUrl::fromLocalFile(tmpFile.fileName())); KJobWidgets::setWindow(downloadJob, qApp->activeWindow()); if (downloadJob->exec()) { shouldAsk = !equalProjectFile(tmpFile.fileName(), dlg); } } } } if ( shouldAsk ) { KGuiItem yes = KStandardGuiItem::yes(); yes.setText(i18n("Override")); yes.setToolTip(i18nc("@info:tooltip", "Continue to open the project and use the just provided project configuration.")); yes.setIcon(QIcon()); KGuiItem no = KStandardGuiItem::no(); no.setText(i18n("Open Existing File")); no.setToolTip(i18nc("@info:tooltip", "Continue to open the project but use the existing project configuration.")); no.setIcon(QIcon()); KGuiItem cancel = KStandardGuiItem::cancel(); cancel.setToolTip(i18nc("@info:tooltip", "Cancel and do not open the project.")); int ret = KMessageBox::questionYesNoCancel(qApp->activeWindow(), i18n("There already exists a project configuration file at %1.\n" "Do you want to override it or open the existing file?", projectFileUrl.toDisplayString(QUrl::PreferLocalFile)), i18n("Override existing project configuration"), yes, no, cancel ); if ( ret == KMessageBox::No ) { writeProjectConfigToFile = false; } else if ( ret == KMessageBox::Cancel ) { return QUrl(); } // else fall through and write new file } else { writeProjectConfigToFile = false; } } if (writeProjectConfigToFile) { Path projectConfigDir(projectFileUrl); projectConfigDir.setLastPathSegment(QStringLiteral(".kdev4")); auto delJob = KIO::del(projectConfigDir.toUrl()); delJob->exec(); if (!writeProjectSettingsToConfigFile(projectFileUrl, dlg)) { KMessageBox::error(d->m_core->uiControllerInternal()->defaultMainWindow(), i18n("Unable to create configuration file %1", projectFileUrl.url())); return QUrl(); } } return projectFileUrl; } bool ProjectDialogProvider::userWantsReopen() { Q_ASSERT(d); return (KMessageBox::questionYesNo( d->m_core->uiControllerInternal()->defaultMainWindow(), i18n( "Reopen the current project?" ) ) == KMessageBox::No) ? false : true; } void ProjectController::setDialogProvider(IProjectDialogProvider* dialog) { Q_ASSERT(d->dialog); delete d->dialog; d->dialog = dialog; } ProjectController::ProjectController( Core* core ) : IProjectController(core) , d(new ProjectControllerPrivate(core, this)) { qRegisterMetaType>(); setObjectName(QStringLiteral("ProjectController")); //NOTE: this is required to be called here, such that the // actions are available when the UI controller gets // initialized *before* the project controller if (Core::self()->setupFlags() != Core::NoUi) { setupActions(); } } void ProjectController::setupActions() { KActionCollection * ac = d->m_core->uiControllerInternal()->defaultMainWindow()->actionCollection(); QAction*action; d->m_openProject = action = ac->addAction( QStringLiteral("project_open") ); action->setText(i18nc( "@action", "Open / Import Project..." ) ); action->setToolTip( i18nc( "@info:tooltip", "Open or import project" ) ); action->setWhatsThis( i18nc( "@info:whatsthis", "Open an existing KDevelop 4 project or import " "an existing Project into KDevelop 4. This entry " "allows one to select a KDevelop4 project file " "or an existing directory to open it in KDevelop. " "When opening an existing directory that does " "not yet have a KDevelop4 project file, the file " "will be created." ) ); action->setIcon(QIcon::fromTheme(QStringLiteral("project-open"))); connect(action, &QAction::triggered, this, [&] { openProject(); }); d->m_fetchProject = action = ac->addAction( QStringLiteral("project_fetch") ); action->setText(i18nc( "@action", "Fetch Project..." ) ); action->setIcon( QIcon::fromTheme( QStringLiteral("edit-download") ) ); action->setToolTip( i18nc( "@info:tooltip", "Fetch project" ) ); action->setWhatsThis( i18nc( "@info:whatsthis", "Guides the user through the project fetch " "and then imports it into KDevelop 4." ) ); // action->setIcon(QIcon::fromTheme("project-open")); connect( action, &QAction::triggered, this, &ProjectController::fetchProject ); // action = ac->addAction( "project_close" ); // action->setText( i18n( "C&lose Project" ) ); // connect( action, SIGNAL(triggered(bool)), SLOT(closeProject()) ); // action->setToolTip( i18n( "Close project" ) ); // action->setWhatsThis( i18n( "Closes the current project." ) ); // action->setEnabled( false ); d->m_closeProject = action = ac->addAction( QStringLiteral("project_close") ); connect( action, &QAction::triggered, this, [&] { d->closeSelectedProjects(); } ); action->setText( i18nc( "@action", "Close Project(s)" ) ); action->setIcon( QIcon::fromTheme( QStringLiteral("project-development-close") ) ); action->setToolTip( i18nc( "@info:tooltip", "Closes all currently selected projects" ) ); action->setEnabled( false ); d->m_openConfig = action = ac->addAction( QStringLiteral("project_open_config") ); connect( action, &QAction::triggered, this, [&] { d->openProjectConfig(); } ); action->setText( i18n( "Open Configuration..." ) ); action->setIcon( QIcon::fromTheme(QStringLiteral("configure")) ); action->setEnabled( false ); action = ac->addAction( QStringLiteral("commit_current_project") ); connect( action, &QAction::triggered, this, &ProjectController::commitCurrentProject ); action->setText( i18n( "Commit Current Project..." ) ); action->setIconText( i18n( "Commit..." ) ); action->setIcon( QIcon::fromTheme(QStringLiteral("svn-commit")) ); connect(d->m_core->uiControllerInternal()->defaultMainWindow(), &MainWindow::areaChanged, this, [&] (Sublime::Area* area) { d->areaChanged(area); }); d->m_core->uiControllerInternal()->area(0, QStringLiteral("code"))->addAction(action); KSharedConfig * config = KSharedConfig::openConfig().data(); // KConfigGroup group = config->group( "General Options" ); d->m_recentProjectsAction = KStandardAction::openRecent(this, SLOT(openProject(QUrl)), this); ac->addAction( QStringLiteral("project_open_recent"), d->m_recentProjectsAction ); d->m_recentProjectsAction->setText( i18n( "Open Recent Project" ) ); d->m_recentProjectsAction->setWhatsThis( i18nc( "@info:whatsthis", "Opens recently opened project." ) ); d->m_recentProjectsAction->loadEntries( KConfigGroup(config, "RecentProjects") ); auto* openProjectForFileAction = new QAction( this ); ac->addAction(QStringLiteral("project_open_for_file"), openProjectForFileAction); openProjectForFileAction->setText(i18n("Open Project for Current File")); openProjectForFileAction->setIcon(QIcon::fromTheme(QStringLiteral("project-open"))); connect( openProjectForFileAction, &QAction::triggered, this, &ProjectController::openProjectForUrlSlot); } ProjectController::~ProjectController() { delete d->model; delete d->dialog; } void ProjectController::cleanup() { if ( d->m_currentlyOpening.isEmpty() ) { d->saveListOfOpenedProjects(); } saveRecentProjectsActionEntries(); d->m_cleaningUp = true; if( buildSetModel() ) { buildSetModel()->storeToSession( Core::self()->activeSession() ); } closeAllProjects(); } void ProjectController::saveRecentProjectsActionEntries() { if (!d->m_recentProjectsAction) return; auto config = KSharedConfig::openConfig(); KConfigGroup recentGroup = config->group("RecentProjects"); d->m_recentProjectsAction->saveEntries( recentGroup ); config->sync(); } void ProjectController::initialize() { d->buildset = new ProjectBuildSetModel( this ); buildSetModel()->loadFromSession( Core::self()->activeSession() ); connect( this, &ProjectController::projectOpened, d->buildset, &ProjectBuildSetModel::loadFromProject ); connect( this, &ProjectController::projectClosing, d->buildset, &ProjectBuildSetModel::saveToProject ); connect( this, &ProjectController::projectClosed, d->buildset, &ProjectBuildSetModel::projectClosed ); d->m_changesModel = new ProjectChangesModel(this); loadSettings(false); d->dialog = new ProjectDialogProvider(d.data()); QDBusConnection::sessionBus().registerObject( QStringLiteral("/org/kdevelop/ProjectController"), this, QDBusConnection::ExportScriptableSlots ); KSharedConfigPtr config = Core::self()->activeSession()->config(); KConfigGroup group = config->group( "General Options" ); const auto projects = group.readEntry( "Open Projects", QList() ); connect( Core::self()->selectionController(), &ISelectionController::selectionChanged, this, [&] () { d->updateActionStates(); } ); connect(this, &ProjectController::projectOpened, this, [&] () { d->updateActionStates(); }); connect(this, &ProjectController::projectClosing, this, [&] () { d->updateActionStates(); }); QTimer::singleShot(0, this, [this, projects](){ openProjects(projects); emit initialized(); }); } void ProjectController::openProjects(const QList& projects) { for (const QUrl& url : projects) { openProject(url); } } void ProjectController::loadSettings( bool projectIsLoaded ) { Q_UNUSED(projectIsLoaded) } void ProjectController::saveSettings( bool projectIsLoaded ) { Q_UNUSED( projectIsLoaded ); } int ProjectController::projectCount() const { return d->m_projects.count(); } IProject* ProjectController::projectAt( int num ) const { if( !d->m_projects.isEmpty() && num >= 0 && num < d->m_projects.count() ) return d->m_projects.at( num ); return nullptr; } QList ProjectController::projects() const { return d->m_projects; } void ProjectController::eventuallyOpenProjectFile(KIO::Job* _job, const KIO::UDSEntryList& entries) { auto* job(dynamic_cast(_job)); Q_ASSERT(job); for (const KIO::UDSEntry& entry : entries) { if(d->m_foundProjectFile) break; if(!entry.isDir()) { QString name = entry.stringValue( KIO::UDSEntry::UDS_NAME ); if(name.endsWith(QLatin1String(".kdev4"))) { //We have found a project-file, open it openProject(Path(Path(job->url()), name).toUrl()); d->m_foundProjectFile = true; } } } } void ProjectController::openProjectForUrlSlot(bool) { if(ICore::self()->documentController()->activeDocument()) { QUrl url = ICore::self()->documentController()->activeDocument()->url(); IProject* project = ICore::self()->projectController()->findProjectForUrl(url); if(!project) { openProjectForUrl(url); }else{ KMessageBox::error(Core::self()->uiController()->activeMainWindow(), i18n("Project already open: %1", project->name())); } }else{ KMessageBox::error(Core::self()->uiController()->activeMainWindow(), i18n("No active document")); } } void ProjectController::openProjectForUrl(const QUrl& sourceUrl) { Q_ASSERT(!sourceUrl.isRelative()); QUrl dirUrl = sourceUrl; if (sourceUrl.isLocalFile() && !QFileInfo(sourceUrl.toLocalFile()).isDir()) { dirUrl = dirUrl.adjusted(QUrl::RemoveFilename); } QUrl testAt = dirUrl; d->m_foundProjectFile = false; while(!testAt.path().isEmpty()) { KIO::ListJob* job = KIO::listDir(testAt); connect(job, &KIO::ListJob::entries, this, &ProjectController::eventuallyOpenProjectFile); KJobWidgets::setWindow(job, ICore::self()->uiController()->activeMainWindow()); job->exec(); if(d->m_foundProjectFile) { //Fine! We have directly opened the project d->m_foundProjectFile = false; return; } QUrl oldTest = testAt.adjusted(QUrl::RemoveFilename); if(oldTest == testAt) break; } QUrl askForOpen = d->dialog->askProjectConfigLocation(false, dirUrl); if(askForOpen.isValid()) openProject(askForOpen); } void ProjectController::openProject( const QUrl &projectFile ) { QUrl url = projectFile; if ( url.isEmpty() ) { url = d->dialog->askProjectConfigLocation(false); if ( url.isEmpty() ) { return; } } Q_ASSERT(!url.isRelative()); QList existingSessions; if(!Core::self()->sessionController()->activeSession()->containedProjects().contains(url)) { foreach( const Session* session, Core::self()->sessionController()->sessions()) { if(session->containedProjects().contains(url)) { existingSessions << session; #if 0 ///@todo Think about this! Problem: The session might already contain files, the debugger might be active, etc. //If this session is empty, close it if(Core::self()->sessionController()->activeSession()->description().isEmpty()) { //Terminate this instance of kdevelop if the user agrees foreach(Sublime::MainWindow* window, Core::self()->uiController()->controller()->mainWindows()) window->close(); } #endif } } } if ( ! existingSessions.isEmpty() ) { ScopedDialog dialog(Core::self()->uiControllerInternal()->activeMainWindow()); dialog->setWindowTitle(i18n("Project Already Open")); auto mainLayout = new QVBoxLayout(dialog); mainLayout->addWidget(new QLabel(i18n("The project you're trying to open is already open in at least one " "other session.
What do you want to do?"))); QGroupBox sessions; sessions.setLayout(new QVBoxLayout); QRadioButton* newSession = new QRadioButton(i18n("Add project to current session")); sessions.layout()->addWidget(newSession); newSession->setChecked(true); foreach ( const Session* session, existingSessions ) { QRadioButton* button = new QRadioButton(i18n("Open session %1", session->description())); button->setProperty("sessionid", session->id().toString()); sessions.layout()->addWidget(button); } sessions.layout()->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Expanding)); mainLayout->addWidget(&sessions); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Abort); auto okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, dialog.data(), &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, dialog.data(), &QDialog::reject); mainLayout->addWidget(buttonBox); if (!dialog->exec()) return; foreach ( const QObject* obj, sessions.children() ) { if ( const auto* button = qobject_cast(obj) ) { QString sessionid = button->property("sessionid").toString(); if ( button->isChecked() && ! sessionid.isEmpty() ) { Core::self()->sessionController()->loadSession(sessionid); return; } } } } if ( url.isEmpty() ) { url = d->dialog->askProjectConfigLocation(false); } if ( !url.isEmpty() ) { d->importProject(url); } } bool ProjectController::fetchProjectFromUrl(const QUrl& repoUrl) { IPlugin* vcsOrProviderPlugin = nullptr; // TODO: query also projectprovider plugins, and that before plain vcs plugins // e.g. KDE provider plugin could catch URLs from mirror or pickup kde:repo things auto* pluginController = d->m_core->pluginController(); const auto& vcsPlugins = pluginController->allPluginsForExtension(QStringLiteral("org.kdevelop.IBasicVersionControl")); for (auto* plugin : vcsPlugins) { auto* iface = plugin->extension(); if (iface->isValidRemoteRepositoryUrl(repoUrl)) { vcsOrProviderPlugin = plugin; break; } } if (!vcsOrProviderPlugin) { KMessageBox::error(Core::self()->uiController()->activeMainWindow(), i18n("No enabled plugin supports this repository URL: %1", repoUrl.toDisplayString())); return false; } const QUrl url = d->dialog->askProjectConfigLocation(true, QUrl(), repoUrl, vcsOrProviderPlugin); if (!url.isEmpty()) { d->importProject(url); } return true; } void ProjectController::fetchProject() { QUrl url = d->dialog->askProjectConfigLocation(true); if ( !url.isEmpty() ) { d->importProject(url); } } void ProjectController::projectImportingFinished( IProject* project ) { if( !project ) { qCWarning(SHELL) << "OOOPS: 0-pointer project"; return; } IPlugin *managerPlugin = project->managerPlugin(); QList pluglist; pluglist.append( managerPlugin ); d->m_projectPlugins.insert( project, pluglist ); d->m_projects.append( project ); if ( d->m_currentlyOpening.isEmpty() ) { d->saveListOfOpenedProjects(); } if (Core::self()->setupFlags() != Core::NoUi) { d->m_recentProjectsAction->addUrl( project->projectFile().toUrl() ); saveRecentProjectsActionEntries(); } Q_ASSERT(d->m_currentlyOpening.contains(project->projectFile().toUrl())); d->m_currentlyOpening.removeAll(project->projectFile().toUrl()); emit projectOpened( project ); reparseProject(project); } // helper method for closeProject() void ProjectController::unloadUnusedProjectPlugins(IProject* proj) { QList pluginsForProj = d->m_projectPlugins.value( proj ); d->m_projectPlugins.remove( proj ); QList otherProjectPlugins; otherProjectPlugins.reserve(d->m_projectPlugins.size()); Q_FOREACH( const QList& _list, d->m_projectPlugins ) { otherProjectPlugins << _list; } QSet pluginsForProjSet = QSet::fromList( pluginsForProj ); QSet otherPrjPluginsSet = QSet::fromList( otherProjectPlugins ); // loaded - target = tobe unloaded. const QSet tobeRemoved = pluginsForProjSet.subtract( otherPrjPluginsSet ); for (IPlugin* _plugin : tobeRemoved) { KPluginMetaData _plugInfo = Core::self()->pluginController()->pluginInfo( _plugin ); if( _plugInfo.isValid() ) { QString _plugName = _plugInfo.pluginId(); qCDebug(SHELL) << "about to unloading :" << _plugName; Core::self()->pluginController()->unloadPlugin( _plugName ); } } } // helper method for closeProject() void ProjectController::closeAllOpenedFiles(IProject* proj) { foreach(IDocument* doc, Core::self()->documentController()->openDocuments()) { if (proj->inProject(IndexedString(doc->url()))) { doc->close(); } } } // helper method for closeProject() void ProjectController::initializePluginCleanup(IProject* proj) { // Unloading (and thus deleting) these plugins is not a good idea just yet // as we're being called by the view part and it gets deleted when we unload the plugin(s) // TODO: find a better place to unload connect(proj, &IProject::destroyed, this, [&] { d->unloadAllProjectPlugins(); }); } void ProjectController::takeProject(IProject* proj) { if (!proj) { return; } // loading might have failed d->m_currentlyOpening.removeAll(proj->projectFile().toUrl()); d->m_projects.removeAll(proj); emit projectClosing(proj); //Core::self()->saveSettings(); // The project file is being closed. // Now we can save settings for all of the Core // objects including this one!! unloadUnusedProjectPlugins(proj); closeAllOpenedFiles(proj); proj->close(); if (d->m_projects.isEmpty()) { initializePluginCleanup(proj); } if(!d->m_cleaningUp) d->saveListOfOpenedProjects(); emit projectClosed(proj); } void ProjectController::closeProject(IProject* proj) { takeProject(proj); proj->deleteLater(); // be safe when deleting } void ProjectController::closeAllProjects() { foreach (auto project, d->m_projects) { closeProject(project); } } void ProjectController::abortOpeningProject(IProject* proj) { d->m_currentlyOpening.removeAll(proj->projectFile().toUrl()); emit projectOpeningAborted(proj); } ProjectModel* ProjectController::projectModel() { return d->model; } IProject* ProjectController::findProjectForUrl( const QUrl& url ) const { if (d->m_projects.isEmpty()) { return nullptr; } ProjectBaseItem* item = d->model->itemForPath(IndexedString(url)); if (item) { return item->project(); } return nullptr; } IProject* ProjectController::findProjectByName( const QString& name ) { Q_FOREACH( IProject* proj, d->m_projects ) { if( proj->name() == name ) { return proj; } } return nullptr; } void ProjectController::configureProject( IProject* project ) { d->projectConfig( project ); } void ProjectController::addProject(IProject* project) { Q_ASSERT(project); if (d->m_projects.contains(project)) { qCWarning(SHELL) << "Project already tracked by this project controller:" << project; return; } // fake-emit signals so listeners are aware of a new project being added emit projectAboutToBeOpened(project); project->setParent(this); d->m_projects.append(project); emit projectOpened(project); } bool ProjectController::isProjectNameUsed( const QString& name ) const { foreach( IProject* p, projects() ) { if( p->name() == name ) { return true; } } return false; } QUrl ProjectController::projectsBaseDirectory() const { KConfigGroup group = ICore::self()->activeSession()->config()->group( "Project Manager" ); return group.readEntry("Projects Base Directory", QUrl::fromLocalFile(QDir::homePath() + QLatin1String("/projects"))); } QString ProjectController::prettyFilePath(const QUrl& url, FormattingOptions format) const { IProject* project = Core::self()->projectController()->findProjectForUrl(url); if(!project) { // Find a project with the correct base directory at least foreach(IProject* candidateProject, Core::self()->projectController()->projects()) { if(candidateProject->path().toUrl().isParentOf(url)) { project = candidateProject; break; } } } Path parent = Path(url).parent(); QString prefixText; if (project) { if (format == FormatHtml) { prefixText = QLatin1String("") + project->name() + QLatin1String("/"); } else { prefixText = project->name() + QLatin1Char(':'); } QString relativePath = project->path().relativePath(parent); if(relativePath.startsWith(QLatin1String("./"))) { relativePath.remove(0, 2); } if (!relativePath.isEmpty()) { prefixText += relativePath + QLatin1Char('/'); } } else { prefixText = parent.pathOrUrl() + QLatin1Char('/'); } return prefixText; } QString ProjectController::prettyFileName(const QUrl& url, FormattingOptions format) const { IProject* project = Core::self()->projectController()->findProjectForUrl(url); if(project && project->path() == Path(url)) { if (format == FormatHtml) { return QLatin1String("") + project->name() + QLatin1String(""); } else { return project->name(); } } QString prefixText = prettyFilePath( url, format ); if (format == FormatHtml) { return prefixText + QLatin1String("") + url.fileName() + QLatin1String(""); } else { return prefixText + url.fileName(); } } ContextMenuExtension ProjectController::contextMenuExtension(Context* ctx, QWidget* parent) { Q_UNUSED(parent); ContextMenuExtension ext; - if ( ctx->type() != Context::ProjectItemContext || !static_cast(ctx)->items().isEmpty() ) { + if ( ctx->type() != Context::ProjectItemContext) { + return ext; + } + if (!static_cast(ctx)->items().isEmpty() ) { + + auto* action = new QAction(i18n("Reparse the Entire Project"), this); + connect(action, &QAction::triggered, this, [&] { + foreach (auto project, d->selectedProjects()) { + reparseProject(project, true, true); + } + }); + + ext.addAction(ContextMenuExtension::ProjectGroup, action); return ext; } + ext.addAction(ContextMenuExtension::ProjectGroup, d->m_openProject); ext.addAction(ContextMenuExtension::ProjectGroup, d->m_fetchProject); ext.addAction(ContextMenuExtension::ProjectGroup, d->m_recentProjectsAction); + return ext; } ProjectBuildSetModel* ProjectController::buildSetModel() { return d->buildset; } ProjectChangesModel* ProjectController::changesModel() { return d->m_changesModel; } void ProjectController::commitCurrentProject() { IDocument* doc=ICore::self()->documentController()->activeDocument(); if(!doc) return; QUrl url=doc->url(); IProject* project = ICore::self()->projectController()->findProjectForUrl(url); if(project && project->versionControlPlugin()) { IPlugin* plugin = project->versionControlPlugin(); auto* vcs=plugin->extension(); if(vcs) { ICore::self()->documentController()->saveAllDocuments(KDevelop::IDocument::Silent); const Path basePath = project->path(); VCSCommitDiffPatchSource* patchSource = new VCSCommitDiffPatchSource(new VCSStandardDiffUpdater(vcs, basePath.toUrl())); bool ret = showVcsDiff(patchSource); if(!ret) { ScopedDialog commitDialog(patchSource); commitDialog->setCommitCandidates(patchSource->infos()); commitDialog->exec(); } } } } QString ProjectController::mapSourceBuild( const QString& path_, bool reverse, bool fallbackRoot ) const { Path path(path_); IProject* sourceDirProject = nullptr, *buildDirProject = nullptr; Q_FOREACH(IProject* proj, d->m_projects) { if(proj->path().isParentOf(path) || proj->path() == path) sourceDirProject = proj; if(proj->buildSystemManager()) { Path buildDir = proj->buildSystemManager()->buildDirectory(proj->projectItem()); if(buildDir.isValid() && (buildDir.isParentOf(path) || buildDir == path)) buildDirProject = proj; } } if(!reverse) { // Map-target is the build directory if(sourceDirProject && sourceDirProject->buildSystemManager()) { // We're in the source, map into the build directory QString relativePath = sourceDirProject->path().relativePath(path); Path build = sourceDirProject->buildSystemManager()->buildDirectory(sourceDirProject->projectItem()); build.addPath(relativePath); while(!QFile::exists(build.path())) build = build.parent(); return build.pathOrUrl(); }else if(buildDirProject && fallbackRoot) { // We're in the build directory, map to the build directory root return buildDirProject->buildSystemManager()->buildDirectory(buildDirProject->projectItem()).pathOrUrl(); } }else{ // Map-target is the source directory if(buildDirProject) { Path build = buildDirProject->buildSystemManager()->buildDirectory(buildDirProject->projectItem()); // We're in the source, map into the build directory QString relativePath = build.relativePath(path); Path source = buildDirProject->path(); source.addPath(relativePath); while(!QFile::exists(source.path())) source = source.parent(); return source.pathOrUrl(); }else if(sourceDirProject && fallbackRoot) { // We're in the source directory, map to the root return sourceDirProject->path().pathOrUrl(); } } return QString(); } -void ProjectController::reparseProject( IProject* project, bool forceUpdate ) -{ + void KDevelop::ProjectController::reparseProject(IProject *project, bool forceUpdate, bool forceAll) + { if (auto job = d->m_parseJobs.value(project)) { job->kill(); } - d->m_parseJobs[project] = new KDevelop::ParseProjectJob(project, forceUpdate); + d->m_parseJobs[project] = new KDevelop::ParseProjectJob(project, forceUpdate, forceAll); ICore::self()->runController()->registerJob(d->m_parseJobs[project]); } } diff --git a/kdevplatform/shell/projectcontroller.h b/kdevplatform/shell/projectcontroller.h index 5177dfd776..0912b56f14 100644 --- a/kdevplatform/shell/projectcontroller.h +++ b/kdevplatform/shell/projectcontroller.h @@ -1,191 +1,191 @@ /* This file is part of KDevelop Copyright 2006 Adam Treat Copyright 2007 Andreas Pakulat This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KDEVPLATFORM_PROJECTCONTROLLER_H #define KDEVPLATFORM_PROJECTCONTROLLER_H #include #include #include #include "shellexport.h" namespace Sublime { class Area; } namespace KIO { class Job; } namespace KDevelop { class IProject; class Core; class Context; class ContextMenuExtension; class IPlugin; class KDEVPLATFORMSHELL_EXPORT IProjectDialogProvider : public QObject { Q_OBJECT public: IProjectDialogProvider(); ~IProjectDialogProvider() override; public Q_SLOTS: /** * Displays some UI to ask the user for the project location. * * @param fetch will tell the UI that the user might want to fetch the project first * @param startUrl tells where to look first */ virtual QUrl askProjectConfigLocation(bool fetch, const QUrl& startUrl = QUrl(), const QUrl& repoUrl = QUrl(), IPlugin* plugin = nullptr) = 0; virtual bool userWantsReopen() = 0; }; class KDEVPLATFORMSHELL_EXPORT ProjectController : public IProjectController { Q_OBJECT Q_CLASSINFO( "D-Bus Interface", "org.kdevelop.ProjectController" ) friend class Core; friend class CorePrivate; friend class ProjectPreferences; public: explicit ProjectController( Core* core ); ~ProjectController() override; IProject* projectAt( int ) const override; int projectCount() const override; QList projects() const override; ProjectBuildSetModel* buildSetModel() override; ProjectModel* projectModel() override; ProjectChangesModel* changesModel() override; IProject* findProjectByName( const QString& name ) override; IProject* findProjectForUrl( const QUrl& ) const override; bool isProjectNameUsed( const QString& name ) const override; void setDialogProvider(IProjectDialogProvider*); QUrl projectsBaseDirectory() const override; QString prettyFileName(const QUrl& url, FormattingOptions format = FormatHtml) const override; QString prettyFilePath(const QUrl& url, FormattingOptions format = FormatHtml) const override; ContextMenuExtension contextMenuExtension(KDevelop::Context* ctx, QWidget* parent); /** * @param repoUrl url identifying the repo * @returns @c true if a plugin was found to handle the repo (also if user cancelled), @c false otherwise */ bool fetchProjectFromUrl(const QUrl& repoUrl); public Q_SLOTS: Q_SCRIPTABLE void openProjectForUrl( const QString &sourceUrl ) { openProjectForUrl(QUrl(sourceUrl)); } void openProjectForUrl( const QUrl &sourceUrl ) override; void fetchProject(); void openProject( const QUrl &KDev4ProjectFile = QUrl() ) override; void abortOpeningProject( IProject* ); void projectImportingFinished( IProject* ); void closeProject( IProject* ) override; void closeAllProjects() override; void configureProject( IProject* ) override; - void reparseProject( IProject* project, bool forceUpdate = false ) override; + void reparseProject( IProject* project, bool forceUpdate = false, bool forceAll = false ) override; void eventuallyOpenProjectFile(KIO::Job* job, const KIO::UDSEntryList& entries); void openProjectForUrlSlot(bool); // void changeCurrentProject( ProjectBaseItem* ); void openProjects(const QList& projects); void commitCurrentProject(); // Maps the given path from the source to the equivalent path within the build directory // of the corresponding project. If the path is already in the build directory and fallbackRoot is true, // then it is mapped to the root of the build directory. // This is used through DBus from within kdevplatform_shell_environment.sh // If reverse is true, maps the opposite direction, from build to source. [ Used in kdevplatform_shell_environment.sh ] Q_SCRIPTABLE QString mapSourceBuild( const QString& path, bool reverse = false, bool fallbackRoot = true ) const; protected: /** * Add the existing project @p project to the controller * * @note Method is used for testing objectives, consider using openProject() instead! * @note takes ownership over the project * * @sa openProject() */ void addProject(IProject* proj); /** * Remove the project @p project from the controller * * @note Ownership is passed on to the caller */ void takeProject(IProject* proj); virtual void loadSettings( bool projectIsLoaded ); virtual void saveSettings( bool projectIsLoaded ); virtual void initialize(); Q_SIGNALS: void initialized(); private: //FIXME Do not load all of this just for the project being opened... //void legacyLoading(); void setupActions(); void cleanup(); void saveRecentProjectsActionEntries(); // helper methods for closeProject() void unloadUnusedProjectPlugins(IProject* proj); void disableProjectCloseAction(); void closeAllOpenedFiles(IProject* proj); void initializePluginCleanup(IProject* proj); private: const QScopedPointer d; friend class ProjectControllerPrivate; }; class ProjectDialogProvider : public IProjectDialogProvider { Q_OBJECT public: explicit ProjectDialogProvider(ProjectControllerPrivate* p); ~ProjectDialogProvider() override; ProjectControllerPrivate* const d; public Q_SLOTS: QUrl askProjectConfigLocation(bool fetch, const QUrl& startUrl, const QUrl& repoUrl, IPlugin* plugin) override; bool userWantsReopen() override; }; } #endif