& problems, const Flags flags)
: m_problems(problems)
, m_flags(flags)
, m_widget(nullptr)
{
// Sort problems vector:
// 1) By severity
// 2) By sourceString, if severities are equals
std::sort(m_problems.begin(), m_problems.end(), [](const IProblem::Ptr& a, const IProblem::Ptr& b) {
if (a->severity() < b->severity())
return true;
if (a->severity() > b->severity())
return false;
if (a->sourceString() < b->sourceString())
return true;
return false;
});
}
ProblemNavigationContext::~ProblemNavigationContext()
{
delete m_widget;
}
QWidget* ProblemNavigationContext::widget() const
{
return m_widget;
}
bool ProblemNavigationContext::isWidgetMaximized() const
{
return false;
}
QString ProblemNavigationContext::name() const
{
return i18n("Problem");
}
QString ProblemNavigationContext::escapedHtml(const QString& text) const
{
const QString htmlStart = QStringLiteral("");
const QString htmlEnd = QStringLiteral("");
QString result = text.trimmed();
if (!result.startsWith(htmlStart))
return result.toHtmlEscaped();
result.remove(htmlStart);
result.remove(htmlEnd);
return result;
}
void ProblemNavigationContext::html(IProblem::Ptr problem)
{
auto iconPath = iconForSeverity(problem->severity());
modifyHtml() += QStringLiteral("");
modifyHtml() += QStringLiteral("%1 | ").arg(htmlImg(iconPath, KIconLoader::Panel));
// BEGIN: right column
modifyHtml() += QStringLiteral("");
modifyHtml() += i18n("Problem in %1", problem->sourceString());
modifyHtml() += QStringLiteral(" ");
if (m_flags & ShowLocation) {
modifyHtml() += labelHighlight(i18n("Location: "));
makeLink(QStringLiteral("%1 :%2")
.arg(problem->finalLocation().document.toUrl().fileName())
.arg(problem->finalLocation().start().line() + 1),
QString(),
NavigationAction(problem->finalLocation().document.toUrl(), problem->finalLocation().start())
);
modifyHtml() += QStringLiteral(" ");
}
QString description = escapedHtml(problem->description());
QString explanation = escapedHtml(problem->explanation());
modifyHtml() += description;
// Add only non-empty explanation which differs from the problem description.
// Skip this if we have more than one problem.
if (m_problems.size() == 1 && !explanation.isEmpty() && explanation != description)
modifyHtml() += QLatin1String("") + explanation +
QLatin1String(" ");
modifyHtml() += QStringLiteral(" | ");
// END: right column
modifyHtml() += QStringLiteral("
");
- auto diagnostics = problem->diagnostics();
+ const auto diagnostics = problem->diagnostics();
if (!diagnostics.isEmpty()) {
DUChainReadLocker lock;
for (auto diagnostic : diagnostics) {
modifyHtml() += QStringLiteral("");
modifyHtml() += labelHighlight(QStringLiteral("%1: ").arg(diagnostic->severityString()));
modifyHtml() += escapedHtml(diagnostic->description());
const DocumentRange range = diagnostic->finalLocation();
Declaration* declaration = DUChainUtils::itemUnderCursor(range.document.toUrl(), range.start()).declaration;
if (declaration) {
modifyHtml() += i18n("
See: ");
makeLink(declaration->toString(), DeclarationPointer(declaration),
NavigationAction::NavigateDeclaration);
modifyHtml() += i18n(" in ");
const auto url = declaration->url().toUrl();
makeLink(QStringLiteral("%1 :%2")
.arg(url.fileName())
.arg(declaration->rangeInCurrentRevision().start().line() + 1),
url.toDisplayString(QUrl::PreferLocalFile), NavigationAction(url, declaration->rangeInCurrentRevision().start()));
} else if (range.start().isValid()) {
modifyHtml() += i18n("
See: ");
const auto url = range.document.toUrl();
makeLink(QStringLiteral("%1 :%2")
.arg(url.fileName())
.arg(range.start().line() + 1),
url.toDisplayString(QUrl::PreferLocalFile), NavigationAction(url, range.start()));
}
modifyHtml() += QStringLiteral("
");
}
}
auto assistant = problem->solutionAssistant();
if (assistant && !assistant->actions().isEmpty()) {
modifyHtml() +=
QStringLiteral("").arg(QStringLiteral(
"#b3d4ff"));
modifyHtml() +=
QStringLiteral("%1 | ").arg(htmlImg(QStringLiteral(
"dialog-ok-apply"),
KIconLoader::Panel));
const int startIndex = m_assistantsActions.size();
int currentIndex = startIndex;
const auto assistantActions = assistant->actions();
for (auto& assistantAction : assistantActions) {
m_assistantsActions.append(assistantAction);
if (currentIndex != startIndex)
modifyHtml() += QStringLiteral(" ");
makeLink(i18n("Solution (%1)", currentIndex + 1), KEY_INVOKE_ACTION(currentIndex),
NavigationAction(KEY_INVOKE_ACTION(currentIndex)));
modifyHtml() += QLatin1String(": ") + assistantAction->description().toHtmlEscaped();
++currentIndex;
}
modifyHtml() += QStringLiteral(" |
");
modifyHtml() += QStringLiteral("
");
}
}
QString ProblemNavigationContext::html(bool shorten)
{
AbstractNavigationContext::html(shorten);
clear();
m_assistantsActions.clear();
int problemIndex = 0;
for (auto& problem : qAsConst(m_problems)) {
html(problem);
if (++problemIndex != m_problems.size())
modifyHtml() += QStringLiteral("
");
}
return currentHtml();
}
NavigationContextPointer ProblemNavigationContext::executeKeyAction(const QString& key)
{
if (key.startsWith(QLatin1String("invoke_action_"))) {
const int index = key.midRef(strlen("invoke_action_")).toInt();
executeAction(index);
}
return {};
}
void ProblemNavigationContext::executeAction(int index)
{
if (index < 0 || index >= m_assistantsActions.size())
return;
auto action = m_assistantsActions.at(index);
Q_ASSERT(action);
if (action) {
action->execute();
if (topContext())
DUChain::self()->updateContextForUrl(topContext()->url(), TopDUContext::ForceUpdate);
} else {
qCWarning(LANGUAGE()) << "No such action";
return;
}
}
diff --git a/kdevplatform/project/projectchangesmodel.cpp b/kdevplatform/project/projectchangesmodel.cpp
index ffe3e9fcbe..40d819f397 100644
--- a/kdevplatform/project/projectchangesmodel.cpp
+++ b/kdevplatform/project/projectchangesmodel.cpp
@@ -1,295 +1,300 @@
/* This file is part of KDevelop
Copyright 2010 Aleix Pol Gonzalez
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 "projectchangesmodel.h"
#include "debug.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
Q_DECLARE_METATYPE(KDevelop::IProject*)
using namespace KDevelop;
ProjectChangesModel::ProjectChangesModel(QObject* parent)
: VcsFileChangesModel(parent)
{
const auto projects = ICore::self()->projectController()->projects();
for (IProject* p : projects) {
addProject(p);
}
connect(ICore::self()->projectController(), &IProjectController::projectOpened,
this, &ProjectChangesModel::addProject);
connect(ICore::self()->projectController(), &IProjectController::projectClosing,
this, &ProjectChangesModel::removeProject);
connect(ICore::self()->documentController(), &IDocumentController::documentSaved,
this, &ProjectChangesModel::documentSaved);
connect(ICore::self()->projectController()->projectModel(), &ProjectModel::rowsInserted,
this, &ProjectChangesModel::itemsAdded);
connect(ICore::self()->runController(), &IRunController::jobUnregistered, this, &ProjectChangesModel::jobUnregistered);
}
ProjectChangesModel::~ProjectChangesModel()
{}
void ProjectChangesModel::addProject(IProject* p)
{
QStandardItem* it = new QStandardItem(p->name());
it->setData(p->name(), ProjectChangesModel::ProjectNameRole);
IPlugin* plugin = p->versionControlPlugin();
if(plugin) {
auto* vcs = plugin->extension();
auto info = ICore::self()->pluginController()->pluginInfo(plugin);
it->setIcon(QIcon::fromTheme(info.iconName()));
it->setToolTip(vcs->name());
auto* branchingExtension = plugin->extension();
if(branchingExtension) {
const auto pathUrl = p->path().toUrl();
branchingExtension->registerRepositoryForCurrentBranchChanges(pathUrl);
// can't use new signal slot syntax here, IBranchingVersionControl is not a QObject
connect(plugin, SIGNAL(repositoryBranchChanged(QUrl)), this, SLOT(repositoryBranchChanged(QUrl)));
repositoryBranchChanged(pathUrl);
} else
reload(QList() << p);
} else {
it->setEnabled(false);
}
appendRow(it);
}
void ProjectChangesModel::removeProject(IProject* p)
{
QStandardItem* it=projectItem(p);
if (!it) {
// when the project is closed before it was fully populated, we won't ever see a
// projectOpened signal - handle this gracefully by just ignoring the remove request
return;
}
removeRow(it->row());
}
QStandardItem* findItemChild(QStandardItem* parent, const QVariant& value, int role = Qt::DisplayRole)
{
for(int i=0; irowCount(); i++) {
QStandardItem* curr=parent->child(i);
if(curr->data(role) == value)
return curr;
}
return nullptr;
}
QStandardItem* ProjectChangesModel::projectItem(IProject* p) const
{
return findItemChild(invisibleRootItem(), p->name(), ProjectChangesModel::ProjectNameRole);
}
void ProjectChangesModel::updateState(IProject* p, const KDevelop::VcsStatusInfo& status)
{
QStandardItem* pItem = projectItem(p);
Q_ASSERT(pItem);
VcsFileChangesModel::updateState(pItem, status);
}
void ProjectChangesModel::changes(IProject* project, const QList& urls, IBasicVersionControl::RecursionMode mode)
{
IPlugin* vcsplugin=project->versionControlPlugin();
IBasicVersionControl* vcs = vcsplugin ? vcsplugin->extension() : nullptr;
if(vcs && vcs->isVersionControlled(urls.first())) { //TODO: filter?
VcsJob* job=vcs->status(urls, mode);
job->setProperty("urls", QVariant::fromValue>(urls));
job->setProperty("mode", QVariant::fromValue(mode));
job->setProperty("project", QVariant::fromValue(project));
connect(job, &VcsJob::finished, this, &ProjectChangesModel::statusReady);
ICore::self()->runController()->registerJob(job);
}
}
void ProjectChangesModel::statusReady(KJob* job)
{
auto* status=static_cast(job);
const QList states = status->fetchResults().toList();
auto* project = job->property("project").value();
if(!project)
return;
QSet foundUrls;
foundUrls.reserve(states.size());
for (const QVariant& state : states) {
const VcsStatusInfo st = state.value();
foundUrls += st.url();
updateState(project, st);
}
QStandardItem* itProject = projectItem(project);
if (!itProject) {
qCDebug(PROJECT) << "Project no longer listed in model:" << project->name() << "- skipping update";
return;
}
IBasicVersionControl::RecursionMode mode = IBasicVersionControl::RecursionMode(job->property("mode").toInt());
- const QSet uncertainUrls = urls(itProject).toSet().subtract(foundUrls);
+ const QList projectUrls = urls(itProject);
+#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
+ const QSet uncertainUrls = QSet(projectUrls.begin(), projectUrls.end()).subtract(foundUrls);
+#else
+ const QSet uncertainUrls = projectUrls.toSet().subtract(foundUrls);
+#endif
const QList sourceUrls = job->property("urls").value>();
for (const QUrl& url : sourceUrls) {
if(url.isLocalFile() && QDir(url.toLocalFile()).exists()) {
for (const QUrl& currentUrl : uncertainUrls) {
if((mode == IBasicVersionControl::NonRecursive && currentUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash) == url.adjusted(QUrl::StripTrailingSlash))
|| (mode == IBasicVersionControl::Recursive && url.isParentOf(currentUrl))
) {
removeUrl(currentUrl);
}
}
}
}
}
void ProjectChangesModel::documentSaved(KDevelop::IDocument* document)
{
reload({document->url()});
}
void ProjectChangesModel::itemsAdded(const QModelIndex& parent, int start, int end)
{
ProjectModel* model=ICore::self()->projectController()->projectModel();
ProjectBaseItem* item=model->itemFromIndex(parent);
if(!item)
return;
IProject* project=item->project();
if(!project)
return;
QList urls;
for(int i=start; iindex(i, 0, parent);
item=model->itemFromIndex(idx);
if(item->type()==ProjectBaseItem::File || item->type()==ProjectBaseItem::Folder || item->type()==ProjectBaseItem::BuildFolder)
urls += item->path().toUrl();
}
if(!urls.isEmpty())
changes(project, urls, KDevelop::IBasicVersionControl::NonRecursive);
}
void ProjectChangesModel::reload(const QList& projects)
{
for (IProject* project : projects) {
changes(project, {project->path().toUrl()}, KDevelop::IBasicVersionControl::Recursive);
}
}
void ProjectChangesModel::reload(const QList& urls)
{
for (const QUrl& url : urls) {
IProject* project=ICore::self()->projectController()->findProjectForUrl(url);
if (project) {
// FIXME: merge multiple urls of the same project
changes(project, {url}, KDevelop::IBasicVersionControl::NonRecursive);
}
}
}
void ProjectChangesModel::reloadAll()
{
QList< IProject* > projects = ICore::self()->projectController()->projects();
reload(projects);
}
void ProjectChangesModel::jobUnregistered(KJob* job)
{
static const std::array readOnly = {
KDevelop::VcsJob::Add,
KDevelop::VcsJob::Remove,
KDevelop::VcsJob::Pull,
KDevelop::VcsJob::Commit,
KDevelop::VcsJob::Move,
KDevelop::VcsJob::Copy,
KDevelop::VcsJob::Revert,
};
auto* vcsjob = qobject_cast(job);
if (vcsjob && std::find(readOnly.begin(), readOnly.end(), vcsjob->type()) != readOnly.end()) {
reloadAll();
}
}
void ProjectChangesModel::repositoryBranchChanged(const QUrl& url)
{
IProject* project = ICore::self()->projectController()->findProjectForUrl(url);
if(project) {
IPlugin* v = project->versionControlPlugin();
Q_ASSERT(v);
auto* branching = v->extension();
Q_ASSERT(branching);
VcsJob* job = branching->currentBranch(url);
connect(job, &VcsJob::resultsReady, this, &ProjectChangesModel::branchNameReady);
job->setProperty("project", QVariant::fromValue(project));
ICore::self()->runController()->registerJob(job);
}
}
void ProjectChangesModel::branchNameReady(VcsJob* job)
{
auto* project = qobject_cast(job->property("project").value());
if(job->status()==VcsJob::JobSucceeded) {
QString name = job->fetchResults().toString();
QString branchName = name.isEmpty() ? i18n("no branch") : name;
projectItem(project)->setText(i18nc("project name (branch name)", "%1 (%2)", project->name(), branchName));
} else {
projectItem(project)->setText(project->name());
}
reload(QList() << project);
}
diff --git a/kdevplatform/serialization/tests/bench_itemrepository.cpp b/kdevplatform/serialization/tests/bench_itemrepository.cpp
index 4151e9c53e..9e3f7bd432 100644
--- a/kdevplatform/serialization/tests/bench_itemrepository.cpp
+++ b/kdevplatform/serialization/tests/bench_itemrepository.cpp
@@ -1,224 +1,228 @@
/*
* This file is part of KDevelop
* Copyright 2012-2013 Milian Wolff
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#include "bench_itemrepository.h"
#include
#include
#include
#include
QTEST_GUILESS_MAIN(BenchItemRepository)
using namespace KDevelop;
struct TestData
{
uint length;
TestData& operator=(const TestData& rhs) = delete;
uint itemSize() const
{
return sizeof(TestData) + length;
}
uint hash() const
{
const char* str = (( const char* )this) + sizeof(TestData);
return IndexedString::hashString(str, length);
}
};
struct TestDataRepositoryItemRequest
{
//The text is supposed to be utf8 encoded
TestDataRepositoryItemRequest(const char* text, uint length)
: m_length(length)
, m_text(text)
, m_hash(IndexedString::hashString(text, length))
{
}
enum {
AverageSize = 10 //This should be the approximate average size of an Item
};
using HashType = uint;
//Should return the hash-value associated with this request(For example the hash of a string)
HashType hash() const
{
return m_hash;
}
//Should return the size of an item created with createItem
uint itemSize() const
{
return sizeof(TestData) + m_length;
}
//Should create an item where the information of the requested item is permanently stored. The pointer
//@param item equals an allocated range with the size of itemSize().
void createItem(TestData* item) const
{
item->length = m_length;
void* itemText = reinterpret_cast(item + 1);
memcpy(itemText, m_text, m_length);
}
static void destroy(TestData* item, AbstractItemRepository&)
{
Q_UNUSED(item);
//Nothing to do here (The object is not intelligent)
}
static bool persistent(const TestData* item)
{
Q_UNUSED(item);
return true;
}
//Should return whether the here requested item equals the given item
bool equals(const TestData* item) const
{
return item->length == m_length && (memcmp(++item, m_text, m_length) == 0);
}
unsigned short m_length;
const char* m_text;
unsigned int m_hash;
};
using TestDataRepository = ItemRepository;
void BenchItemRepository::initTestCase()
{
ItemRepositoryRegistry::initialize(m_repositoryPath);
}
void BenchItemRepository::cleanupTestCase()
{
ItemRepositoryRegistry::deleteRepositoryFromDisk(m_repositoryPath);
}
static QVector generateData()
{
QVector data;
static const int NUM_ITEMS = 100000;
data.resize(NUM_ITEMS);
for (int i = 0; i < NUM_ITEMS; ++i) {
data[i] = QStringLiteral("/foo/%1").arg(i);
}
return data;
}
static QVector insertData(const QVector& data, TestDataRepository& repo)
{
QVector indices;
indices.reserve(data.size());
for (const QString& item : data) {
const QByteArray byteArray = item.toUtf8();
indices << repo.index(TestDataRepositoryItemRequest(byteArray.constData(), byteArray.length()));
}
return indices;
}
void BenchItemRepository::insert()
{
TestDataRepository repo("TestDataRepositoryInsert");
const QVector data = generateData();
QVector indices;
QBENCHMARK_ONCE {
indices = insertData(data, repo);
repo.store();
}
Q_ASSERT(indices.size() == data.size());
QCOMPARE(repo.statistics().totalItems, uint(data.size()));
}
void BenchItemRepository::remove()
{
TestDataRepository repo("TestDataRepositoryRemove");
const QVector data = generateData();
const QVector indices = insertData(data, repo);
repo.store();
+#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
+ QVERIFY(indices.size() == QSet(indices.begin(), indices.end()).size());
+#else
QVERIFY(indices.size() == indices.toList().toSet().size());
+#endif
QVERIFY(indices.size() == data.size());
QBENCHMARK_ONCE {
for (uint index : indices) {
repo.deleteItem(index);
}
repo.store();
}
QCOMPARE(repo.statistics().totalItems, 0u);
}
void BenchItemRepository::removeDisk()
{
const QVector data = generateData();
QVector indices;
{
TestDataRepository repo("TestDataRepositoryRemoveDisk");
indices = insertData(data, repo);
repo.store();
}
TestDataRepository repo("TestDataRepositoryRemoveDisk");
QVERIFY(repo.statistics().totalItems == static_cast(data.size()));
QBENCHMARK_ONCE {
for (uint index : qAsConst(indices)) {
repo.deleteItem(index);
}
repo.store();
}
QCOMPARE(repo.statistics().totalItems, 0u);
}
void BenchItemRepository::lookupKey()
{
TestDataRepository repo("TestDataRepositoryLookupKey");
const QVector data = generateData();
QVector indices = insertData(data, repo);
srand(0);
std::random_shuffle(indices.begin(), indices.end());
QBENCHMARK {
for (uint index : qAsConst(indices)) {
repo.itemFromIndex(index);
}
}
}
void BenchItemRepository::lookupValue()
{
TestDataRepository repo("TestDataRepositoryLookupValue");
const QVector data = generateData();
QVector indices = insertData(data, repo);
srand(0);
std::random_shuffle(indices.begin(), indices.end());
QBENCHMARK {
for (const QString& item : data) {
const QByteArray byteArray = item.toUtf8();
repo.findIndex(TestDataRepositoryItemRequest(byteArray.constData(), byteArray.length()));
}
}
}
diff --git a/kdevplatform/shell/plugincontroller.cpp b/kdevplatform/shell/plugincontroller.cpp
index c7ac8aca4c..bbfeb73dff 100644
--- a/kdevplatform/shell/plugincontroller.cpp
+++ b/kdevplatform/shell/plugincontroller.cpp
@@ -1,868 +1,883 @@
/* This file is part of the KDE project
Copyright 2004, 2007 Alexander Dymo
Copyright 2006 Matt Rogers
Based on code from Kopete
Copyright (c) 2002-2003 Martijn Klingens
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 "plugincontroller.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "core.h"
#include "shellextension.h"
#include "runcontroller.h"
#include "debugcontroller.h"
#include "documentationcontroller.h"
#include "sourceformattercontroller.h"
#include "projectcontroller.h"
#include "ktexteditorpluginintegration.h"
#include "debug.h"
namespace {
inline QString KEY_Plugins() { return QStringLiteral("Plugins"); }
inline QString KEY_Suffix_Enabled() { return QStringLiteral("Enabled"); }
inline QString KEY_LoadMode() { return QStringLiteral("X-KDevelop-LoadMode"); }
inline QString KEY_Category() { return QStringLiteral("X-KDevelop-Category"); }
inline QString KEY_Mode() { return QStringLiteral("X-KDevelop-Mode"); }
inline QString KEY_Version() { return QStringLiteral("X-KDevelop-Version"); }
inline QString KEY_Interfaces() { return QStringLiteral("X-KDevelop-Interfaces"); }
inline QString KEY_Required() { return QStringLiteral("X-KDevelop-IRequired"); }
inline QString KEY_Optional() { return QStringLiteral("X-KDevelop-IOptional"); }
inline QString KEY_KPlugin() { return QStringLiteral("KPlugin"); }
inline QString KEY_EnabledByDefault() { return QStringLiteral("EnabledByDefault"); }
inline QString KEY_Global() { return QStringLiteral("Global"); }
inline QString KEY_Project() { return QStringLiteral("Project"); }
inline QString KEY_Gui() { return QStringLiteral("GUI"); }
inline QString KEY_AlwaysOn() { return QStringLiteral("AlwaysOn"); }
inline QString KEY_UserSelectable() { return QStringLiteral("UserSelectable"); }
bool isUserSelectable( const KPluginMetaData& info )
{
QString loadMode = info.value(KEY_LoadMode());
return loadMode.isEmpty() || loadMode == KEY_UserSelectable();
}
bool isGlobalPlugin( const KPluginMetaData& info )
{
return info.value(KEY_Category()) == KEY_Global();
}
bool hasMandatoryProperties( const KPluginMetaData& info )
{
QString mode = info.value(KEY_Mode());
if (mode.isEmpty()) {
return false;
}
// when the plugin is installed into the versioned plugin path, it's good to go
if (info.fileName().contains(QLatin1String("/kdevplatform/" QT_STRINGIFY(KDEVELOP_PLUGIN_VERSION) "/"))) {
return true;
}
// the version property is only required when the plugin is not installed into the right directory
QVariant version = info.rawData().value(KEY_Version()).toVariant();
if (version.isValid() && version.value() == KDEVELOP_PLUGIN_VERSION) {
return true;
}
return false;
}
+inline QSet stringSet(const QVariant& variant)
+{
+#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
+ const QStringList list = variant.toStringList();
+ return QSet(list.begin(), list.end());
+#else
+ return variant.toStringList().toSet();
+#endif
+}
+
bool constraintsMatch( const KPluginMetaData& info, const QVariantMap& constraints)
{
for (auto it = constraints.begin(); it != constraints.end(); ++it) {
const auto property = info.rawData().value(it.key()).toVariant();
if (!property.isValid()) {
return false;
} else if (property.canConvert()) {
- QSet values = property.toStringList().toSet();
- QSet expected = it.value().toStringList().toSet();
+ const QSet values = stringSet(property);
+ const QSet expected = stringSet(it.value());
if (!values.contains(expected)) {
return false;
}
} else if (it.value() != property) {
return false;
}
}
return true;
}
struct Dependency
{
explicit Dependency(const QString &dependency)
: interface(dependency)
{
if (dependency.contains(QLatin1Char('@'))) {
const auto list = dependency.split(QLatin1Char('@'), QString::SkipEmptyParts);
if (list.size() == 2) {
interface = list.at(0);
pluginName = list.at(1);
}
}
}
QString interface;
QString pluginName;
};
}
namespace KDevelop {
class PluginControllerPrivate
{
public:
explicit PluginControllerPrivate(Core *core)
: core(core)
{}
QVector plugins;
//map plugin infos to currently loaded plugins
using InfoToPluginMap = QHash;
InfoToPluginMap loadedPlugins;
// The plugin manager's mode. The mode is StartingUp until loadAllPlugins()
// has finished loading the plugins, after which it is set to Running.
// ShuttingDown and DoneShutdown are used during shutdown by the
// async unloading of plugins.
enum CleanupMode
{
Running /**< the plugin manager is running */,
CleaningUp /**< the plugin manager is cleaning up for shutdown */,
CleanupDone /**< the plugin manager has finished cleaning up */
};
CleanupMode cleanupMode;
bool canUnload(const KPluginMetaData& plugin)
{
qCDebug(SHELL) << "checking can unload for:" << plugin.name() << plugin.value(KEY_LoadMode());
if (plugin.value(KEY_LoadMode()) == KEY_AlwaysOn()) {
return false;
}
const QStringList interfaces = KPluginMetaData::readStringList(plugin.rawData(), KEY_Interfaces());
qCDebug(SHELL) << "checking dependencies:" << interfaces;
for (auto it = loadedPlugins.constBegin(), end = loadedPlugins.constEnd(); it != end; ++it) {
const KPluginMetaData& info = it.key();
if (info.pluginId() != plugin.pluginId()) {
const QStringList dependencies =
KPluginMetaData::readStringList(plugin.rawData(), KEY_Required()) +
KPluginMetaData::readStringList(plugin.rawData(), KEY_Optional());
for (const QString& dep : dependencies) {
Dependency dependency(dep);
if (!dependency.pluginName.isEmpty() && dependency.pluginName != plugin.pluginId()) {
continue;
}
if (interfaces.contains(dependency.interface) && !canUnload(info)) {
return false;
}
}
}
}
return true;
}
KPluginMetaData infoForId( const QString& id ) const
{
for (const KPluginMetaData& info : plugins) {
if (info.pluginId() == id) {
return info;
}
}
return KPluginMetaData();
}
/**
* Iterate over all cached plugin infos, and call the functor for every enabled plugin.
*
* If an extension and/or pluginName is given, the functor will only be called for
* those plugins matching this information.
*
* The functor should return false when the iteration can be stopped, and true if it
* should be continued.
*/
template
void foreachEnabledPlugin(F func, const QString &extension = {}, const QVariantMap& constraints = QVariantMap(), const QString &pluginName = {}) const
{
const auto currentPlugins = plugins;
for (const auto& info : currentPlugins) {
if ((pluginName.isEmpty() || info.pluginId() == pluginName)
&& (extension.isEmpty() || KPluginMetaData::readStringList(info.rawData(), KEY_Interfaces()).contains(extension))
&& constraintsMatch(info, constraints)
&& isEnabled(info))
{
if (!func(info)) {
break;
}
}
}
}
enum EnableState {
DisabledByEnv,
DisabledBySetting,
DisabledByUnknown,
FirstEnabledState,
EnabledBySetting = FirstEnabledState,
AlwaysEnabled
};
/**
* Estimate enabled state of a plugin
*/
EnableState enabledState(const KPluginMetaData& info) const
{
// first check black listing from environment
static const QStringList disabledPlugins = QString::fromLatin1(qgetenv("KDEV_DISABLE_PLUGINS")).split(QLatin1Char(';'));
if (disabledPlugins.contains(info.pluginId())) {
return DisabledByEnv;
}
if (!isUserSelectable( info ))
return AlwaysEnabled;
// read stored user preference
const KConfigGroup grp = Core::self()->activeSession()->config()->group( KEY_Plugins() );
const QString pluginEnabledKey = info.pluginId() + KEY_Suffix_Enabled();
if (grp.hasKey(pluginEnabledKey)) {
return grp.readEntry(pluginEnabledKey, true) ? EnabledBySetting : DisabledBySetting;
}
// should not happen
return DisabledByUnknown;
}
/**
* Decide whether a plugin is enabled
*/
bool isEnabled(const KPluginMetaData& info) const
{
return (enabledState(info) >= FirstEnabledState);
}
Core* const core;
};
PluginController::PluginController(Core *core)
: IPluginController()
, d_ptr(new PluginControllerPrivate(core))
{
Q_D(PluginController);
setObjectName(QStringLiteral("PluginController"));
QSet foundPlugins;
auto newPlugins = KPluginLoader::findPlugins(QStringLiteral("kdevplatform/" QT_STRINGIFY(KDEVELOP_PLUGIN_VERSION)), [&](const KPluginMetaData& meta) {
if (meta.serviceTypes().contains(QStringLiteral("KDevelop/Plugin"))) {
foundPlugins.insert(meta.pluginId());
return true;
} else {
qCWarning(SHELL) << "Plugin" << meta.fileName() << "is installed into the kdevplatform plugin directory, but does not have"
" \"KDevelop/Plugin\" set as the service type. This plugin will not be loaded.";
return false;
}
});
qCDebug(SHELL) << "Found" << newPlugins.size() << "plugins:" << foundPlugins;
if (newPlugins.isEmpty()) {
qCWarning(SHELL) << "Did not find any plugins, check your environment.";
qCWarning(SHELL) << " Note: QT_PLUGIN_PATH is set to:" << qgetenv("QT_PLUGIN_PATH");
}
d->plugins = newPlugins;
KTextEditorIntegration::initialize();
const QVector ktePlugins = KPluginLoader::findPlugins(QStringLiteral("ktexteditor"), [](const KPluginMetaData & md) {
return md.serviceTypes().contains(QStringLiteral("KTextEditor/Plugin"))
&& md.serviceTypes().contains(QStringLiteral("KDevelop/Plugin"));
});
foundPlugins.clear();
std::for_each(ktePlugins.cbegin(), ktePlugins.cend(), [&foundPlugins](const KPluginMetaData& data) {
foundPlugins << data.pluginId();
});
qCDebug(SHELL) << "Found" << ktePlugins.size() << " KTextEditor plugins:" << foundPlugins;
d->plugins.reserve(d->plugins.size() + ktePlugins.size());
for (const auto& info : ktePlugins) {
auto data = info.rawData();
// temporary workaround for Kate's ctags plugin being enabled by default
// see https://mail.kde.org/pipermail/kwrite-devel/2019-July/004821.html
if (info.pluginId() == QLatin1String("katectagsplugin")) {
auto kpluginData = data[KEY_KPlugin()].toObject();
kpluginData[KEY_EnabledByDefault()] = false;
data[KEY_KPlugin()] = kpluginData;
}
// add some KDevelop specific JSON data
data[KEY_Category()] = KEY_Global();
data[KEY_Mode()] = KEY_Gui();
data[KEY_Version()] = KDEVELOP_PLUGIN_VERSION;
d->plugins.append({data, info.fileName(), info.metaDataFileName()});
}
d->cleanupMode = PluginControllerPrivate::Running;
// Register the KDevelop::IPlugin* metatype so we can properly unload it
qRegisterMetaType( "KDevelop::IPlugin*" );
}
PluginController::~PluginController()
{
Q_D(PluginController);
if ( d->cleanupMode != PluginControllerPrivate::CleanupDone ) {
qCWarning(SHELL) << "Destructing plugin controller without going through the shutdown process!";
}
}
KPluginMetaData PluginController::pluginInfo( const IPlugin* plugin ) const
{
Q_D(const PluginController);
return d->loadedPlugins.key(const_cast(plugin));
}
void PluginController::cleanup()
{
Q_D(PluginController);
if(d->cleanupMode != PluginControllerPrivate::Running)
{
//qCDebug(SHELL) << "called when not running. state =" << d->cleanupMode;
return;
}
d->cleanupMode = PluginControllerPrivate::CleaningUp;
// Ask all plugins to unload
while ( !d->loadedPlugins.isEmpty() )
{
//Let the plugin do some stuff before unloading
unloadPlugin(d->loadedPlugins.begin().value(), Now);
}
d->cleanupMode = PluginControllerPrivate::CleanupDone;
}
IPlugin* PluginController::loadPlugin( const QString& pluginName )
{
return loadPluginInternal( pluginName );
}
void PluginController::initialize()
{
Q_D(PluginController);
QElapsedTimer timer;
timer.start();
QMap pluginMap;
if( ShellExtension::getInstance()->defaultPlugins().isEmpty() )
{
for (const KPluginMetaData& pi : qAsConst(d->plugins)) {
QJsonValue enabledByDefaultValue = pi.rawData()[KEY_KPlugin()].toObject()[KEY_EnabledByDefault()];
// plugins enabled until explicitly specified otherwise
const bool enabledByDefault = (enabledByDefaultValue.isNull() || enabledByDefaultValue.toBool());
pluginMap.insert(pi.pluginId(), enabledByDefault);
}
} else
{
// Get the default from the ShellExtension
const auto defaultPlugins = ShellExtension::getInstance()->defaultPlugins();
for (const QString& s : defaultPlugins) {
pluginMap.insert( s, true );
}
}
KConfigGroup grp = Core::self()->activeSession()->config()->group( KEY_Plugins() );
QMap entries = grp.entryMap();
QMap::Iterator it;
for ( it = entries.begin(); it != entries.end(); ++it )
{
const QString key = it.key();
if (key.endsWith(KEY_Suffix_Enabled())) {
const QString pluginid = key.left(key.length() - 7);
const bool defValue = pluginMap.value( pluginid, false );
const bool enabled = grp.readEntry(key, defValue);
pluginMap.insert( pluginid, enabled );
}
}
// store current known set of enabled plugins
for (const KPluginMetaData& pi : qAsConst(d->plugins)) {
if (isUserSelectable(pi)) {
auto it = pluginMap.constFind(pi.pluginId());
if (it != pluginMap.constEnd() && (it.value())) {
grp.writeEntry(pi.pluginId() + KEY_Suffix_Enabled(), true);
}
} else {
// Backward compat: Remove any now-obsolete entries
grp.deleteEntry(pi.pluginId() + QLatin1String("Disabled"));
}
}
// Synchronize so we're writing out to the file.
grp.sync();
// load global plugins
for (const KPluginMetaData& pi : qAsConst(d->plugins)) {
if (isGlobalPlugin(pi)) {
loadPluginInternal(pi.pluginId());
}
}
qCDebug(SHELL) << "Done loading plugins - took:" << timer.elapsed() << "ms";
}
QList PluginController::loadedPlugins() const
{
Q_D(const PluginController);
return d->loadedPlugins.values();
}
bool PluginController::unloadPlugin( const QString & pluginId )
{
Q_D(PluginController);
IPlugin *thePlugin = plugin( pluginId );
bool canUnload = d->canUnload( d->infoForId( pluginId ) );
qCDebug(SHELL) << "Unloading plugin:" << pluginId << "?" << thePlugin << canUnload;
if( thePlugin && canUnload )
{
return unloadPlugin(thePlugin, Later);
}
return (canUnload && thePlugin);
}
bool PluginController::unloadPlugin(IPlugin* plugin, PluginDeletion deletion)
{
Q_D(PluginController);
qCDebug(SHELL) << "unloading plugin:" << plugin << pluginInfo( plugin ).name();
emit unloadingPlugin(plugin);
plugin->unload();
emit pluginUnloaded(plugin);
//Remove the plugin from our list of plugins so we create a new
//instance when we're asked for it again.
//This is important to do right here, not later when the plugin really
//vanishes. For example project re-opening might try to reload the plugin
//and then would get the "old" pointer which will be deleted in the next
//event loop run and thus causing crashes.
for ( PluginControllerPrivate::InfoToPluginMap::Iterator it = d->loadedPlugins.begin();
it != d->loadedPlugins.end(); ++it )
{
if ( it.value() == plugin )
{
d->loadedPlugins.erase( it );
break;
}
}
if (deletion == Later)
plugin->deleteLater();
else
delete plugin;
return true;
}
KPluginMetaData PluginController::infoForPluginId( const QString &pluginId ) const
{
Q_D(const PluginController);
auto it = std::find_if(d->plugins.constBegin(), d->plugins.constEnd(), [&](const KPluginMetaData& info) {
return (info.pluginId() == pluginId);
});
return (it != d->plugins.constEnd()) ? *it : KPluginMetaData();
}
IPlugin *PluginController::loadPluginInternal( const QString &pluginId )
{
Q_D(PluginController);
QElapsedTimer timer;
timer.start();
KPluginMetaData info = infoForPluginId( pluginId );
if ( !info.isValid() ) {
qCWarning(SHELL) << "Unable to find a plugin named '" << pluginId << "'!" ;
return nullptr;
}
if ( IPlugin* plugin = d->loadedPlugins.value( info ) ) {
return plugin;
}
const auto enabledState = d->enabledState(info);
if (enabledState < PluginControllerPrivate::FirstEnabledState) {
// Do not load disabled plugins
qCDebug(SHELL) << "Not loading plugin named" << pluginId << (
(enabledState == PluginControllerPrivate::DisabledByEnv) ?
"because disabled by KDEV_DISABLE_PLUGINS." :
(enabledState == PluginControllerPrivate::DisabledBySetting) ?
"because disabled by setting." :
/* else, should not happen */
"because disabled for unknown reason.");
return nullptr;
}
if ( !hasMandatoryProperties( info ) ) {
qCWarning(SHELL) << "Unable to load plugin named" << pluginId << "because not all mandatory properties are set.";
return nullptr;
}
if ( info.value(KEY_Mode()) == KEY_Gui() && Core::self()->setupFlags() == Core::NoUi ) {
qCDebug(SHELL) << "Not loading plugin named" << pluginId
<< "- Running in No-Ui mode, but the plugin says it needs a GUI";
return nullptr;
}
qCDebug(SHELL) << "Attempting to load" << pluginId << "- name:" << info.name();
emit loadingPlugin( info.pluginId() );
// first, ensure all dependencies are available and not disabled
// this is unrelated to whether they are loaded already or not.
// when we depend on e.g. A and B, but B cannot be found, then we
// do not want to load A first and then fail on B and leave A loaded.
// this would happen if we'd skip this step here and directly loadDependencies.
QStringList missingInterfaces;
if ( !hasUnresolvedDependencies( info, missingInterfaces ) ) {
qCWarning(SHELL) << "Can't load plugin" << pluginId
<< "some of its required dependencies could not be fulfilled:"
<< missingInterfaces.join(QLatin1Char(','));
return nullptr;
}
// now ensure all dependencies are loaded
QString failedDependency;
if( !loadDependencies( info, failedDependency ) ) {
qCWarning(SHELL) << "Can't load plugin" << pluginId
<< "because a required dependency could not be loaded:" << failedDependency;
return nullptr;
}
// same for optional dependencies, but don't error out if anything fails
loadOptionalDependencies( info );
// now we can finally load the plugin itself
KPluginLoader loader(info.fileName());
auto factory = loader.factory();
if (!factory) {
qCWarning(SHELL) << "Can't load plugin" << pluginId
<< "because a factory to load the plugin could not be obtained:" << loader.errorString();
return nullptr;
}
// now create it
auto plugin = factory->create(d->core);
if (!plugin) {
if (auto katePlugin = factory->create(d->core, QVariantList() << info.pluginId())) {
plugin = new KTextEditorIntegration::Plugin(katePlugin, d->core);
} else {
qCWarning(SHELL) << "Creating plugin" << pluginId << "failed.";
return nullptr;
}
}
KConfigGroup group = Core::self()->activeSession()->config()->group(KEY_Plugins());
// runtime errors such as missing executables on the system or such get checked now
if (plugin->hasError()) {
qCWarning(SHELL) << "Could not load plugin" << pluginId << ", it reported the error:" << plugin->errorDescription()
<< "Disabling the plugin now.";
group.writeEntry(info.pluginId() + KEY_Suffix_Enabled(), false); // do the same as KPluginInfo did
group.sync();
unloadPlugin(pluginId);
return nullptr;
}
// yay, it all worked - the plugin is loaded
d->loadedPlugins.insert(info, plugin);
group.writeEntry(info.pluginId() + KEY_Suffix_Enabled(), true); // do the same as KPluginInfo did
group.sync();
qCDebug(SHELL) << "Successfully loaded plugin" << pluginId << "from" << loader.fileName() << "- took:" << timer.elapsed() << "ms";
emit pluginLoaded( plugin );
return plugin;
}
IPlugin* PluginController::plugin(const QString& pluginId) const
{
Q_D(const PluginController);
KPluginMetaData info = infoForPluginId( pluginId );
if ( !info.isValid() )
return nullptr;
return d->loadedPlugins.value( info );
}
bool PluginController::hasUnresolvedDependencies( const KPluginMetaData& info, QStringList& missing ) const
{
Q_D(const PluginController);
- QSet required = KPluginMetaData::readStringList(info.rawData(), KEY_Required()).toSet();
+ const QStringList requiredList = KPluginMetaData::readStringList(info.rawData(), KEY_Required());
+#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
+ QSet required(requiredList.begin(), requiredList.end());
+#else
+ QSet required = requiredList.toSet();
+#endif
if (!required.isEmpty()) {
d->foreachEnabledPlugin([&required] (const KPluginMetaData& plugin) -> bool {
const auto interfaces = KPluginMetaData::readStringList(plugin.rawData(), KEY_Interfaces());
for (const QString& iface : interfaces) {
required.remove(iface);
required.remove(iface + QLatin1Char('@') + plugin.pluginId());
}
return !required.isEmpty();
});
}
// if we found all dependencies required should be empty now
if (!required.isEmpty()) {
missing = required.values();
return false;
}
return true;
}
void PluginController::loadOptionalDependencies( const KPluginMetaData& info )
{
const QStringList dependencies = KPluginMetaData::readStringList(info.rawData(), KEY_Optional());
for (const QString& dep : dependencies) {
Dependency dependency(dep);
if (!pluginForExtension(dependency.interface, dependency.pluginName)) {
qCDebug(SHELL) << "Couldn't load optional dependency:" << dep << info.pluginId();
}
}
}
bool PluginController::loadDependencies( const KPluginMetaData& info, QString& failedDependency )
{
const QStringList dependencies = KPluginMetaData::readStringList(info.rawData(), KEY_Required());
for (const QString& value : dependencies) {
Dependency dependency(value);
if (!pluginForExtension(dependency.interface, dependency.pluginName)) {
failedDependency = value;
return false;
}
}
return true;
}
IPlugin *PluginController::pluginForExtension(const QString &extension, const QString &pluginName, const QVariantMap& constraints)
{
Q_D(PluginController);
IPlugin* plugin = nullptr;
d->foreachEnabledPlugin([this, &plugin] (const KPluginMetaData& info) -> bool {
Q_D(PluginController);
plugin = d->loadedPlugins.value( info );
if( !plugin ) {
plugin = loadPluginInternal( info.pluginId() );
}
return !plugin;
}, extension, constraints, pluginName);
return plugin;
}
QList PluginController::allPluginsForExtension(const QString &extension, const QVariantMap& constraints)
{
Q_D(PluginController);
//qCDebug(SHELL) << "Finding all Plugins for Extension:" << extension << "|" << constraints;
QList plugins;
d->foreachEnabledPlugin([this, &plugins] (const KPluginMetaData& info) -> bool {
Q_D(PluginController);
IPlugin* plugin = d->loadedPlugins.value( info );
if( !plugin) {
plugin = loadPluginInternal( info.pluginId() );
}
if (plugin && !plugins.contains(plugin)) {
plugins << plugin;
}
return true;
}, extension, constraints);
return plugins;
}
QVector PluginController::queryExtensionPlugins(const QString& extension, const QVariantMap& constraints) const
{
Q_D(const PluginController);
QVector plugins;
d->foreachEnabledPlugin([&plugins] (const KPluginMetaData& info) -> bool {
plugins << info;
return true;
}, extension, constraints);
return plugins;
}
QStringList PluginController::allPluginNames() const
{
Q_D(const PluginController);
QStringList names;
names.reserve(d->plugins.size());
for (const KPluginMetaData& info : qAsConst(d->plugins)) {
names << info.pluginId();
}
return names;
}
QList PluginController::queryPluginsForContextMenuExtensions(KDevelop::Context* context, QWidget* parent) const
{
Q_D(const PluginController);
// This fixes random order of extension menu items between different runs of KDevelop.
// Without sorting we have random reordering of "Analyze With" submenu for example:
// 1) "Cppcheck" actions, "Vera++" actions - first run
// 2) "Vera++" actions, "Cppcheck" actions - some other run.
QMultiMap sortedPlugins;
for (auto it = d->loadedPlugins.constBegin(); it != d->loadedPlugins.constEnd(); ++it) {
sortedPlugins.insert(it.key().name(), it.value());
}
QList exts;
exts.reserve(sortedPlugins.size());
for (IPlugin* plugin : qAsConst(sortedPlugins)) {
exts << plugin->contextMenuExtension(context, parent);
}
exts << Core::self()->debugControllerInternal()->contextMenuExtension(context, parent);
exts << Core::self()->documentationControllerInternal()->contextMenuExtension(context, parent);
exts << Core::self()->sourceFormatterControllerInternal()->contextMenuExtension(context, parent);
exts << Core::self()->runControllerInternal()->contextMenuExtension(context, parent);
exts << Core::self()->projectControllerInternal()->contextMenuExtension(context, parent);
return exts;
}
QStringList PluginController::projectPlugins() const
{
Q_D(const PluginController);
QStringList names;
for (const KPluginMetaData& info : qAsConst(d->plugins)) {
if (info.value(KEY_Category()) == KEY_Project()) {
names << info.pluginId();
}
}
return names;
}
void PluginController::loadProjectPlugins()
{
const auto pluginNames = projectPlugins();
for (const QString& name : pluginNames) {
loadPluginInternal( name );
}
}
void PluginController::unloadProjectPlugins()
{
const auto pluginNames = projectPlugins();
for (const QString& name : pluginNames) {
unloadPlugin( name );
}
}
QVector PluginController::allPluginInfos() const
{
Q_D(const PluginController);
return d->plugins;
}
void PluginController::updateLoadedPlugins()
{
Q_D(PluginController);
QStringList defaultPlugins = ShellExtension::getInstance()->defaultPlugins();
KConfigGroup grp = Core::self()->activeSession()->config()->group( KEY_Plugins() );
for (const KPluginMetaData& info : qAsConst(d->plugins)) {
if( isGlobalPlugin( info ) )
{
bool enabled = grp.readEntry(info.pluginId() + KEY_Suffix_Enabled(), ( defaultPlugins.isEmpty() || defaultPlugins.contains( info.pluginId() ) ) ) || !isUserSelectable( info );
bool loaded = d->loadedPlugins.contains( info );
if( loaded && !enabled )
{
qCDebug(SHELL) << "unloading" << info.pluginId();
if( !unloadPlugin( info.pluginId() ) )
{
grp.writeEntry( info.pluginId() + KEY_Suffix_Enabled(), false );
}
} else if( !loaded && enabled )
{
loadPluginInternal( info.pluginId() );
}
}
// TODO: what about project plugins? what about dependency plugins?
}
}
void PluginController::resetToDefaults()
{
Q_D(PluginController);
KSharedConfigPtr cfg = Core::self()->activeSession()->config();
cfg->deleteGroup( KEY_Plugins() );
cfg->sync();
KConfigGroup grp = cfg->group( KEY_Plugins() );
QStringList plugins = ShellExtension::getInstance()->defaultPlugins();
if( plugins.isEmpty() )
{
for (const KPluginMetaData& info : qAsConst(d->plugins)) {
if (!isUserSelectable(info)) {
continue;
}
QJsonValue enabledByDefault = info.rawData()[KEY_KPlugin()].toObject()[KEY_EnabledByDefault()];
// plugins enabled until explicitly specified otherwise
if (enabledByDefault.isNull() || enabledByDefault.toBool()) {
plugins << info.pluginId();
}
}
}
for (const QString& s : qAsConst(plugins)) {
grp.writeEntry(s + KEY_Suffix_Enabled(), true);
}
grp.sync();
}
}
diff --git a/kdevplatform/shell/projectcontroller.cpp b/kdevplatform/shell/projectcontroller.cpp
index 91435623a2..0911cc1700 100644
--- a/kdevplatform/shell/projectcontroller.cpp
+++ b/kdevplatform/shell/projectcontroller.cpp
@@ -1,1375 +1,1380 @@
/* 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
#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;
const auto plugins = findPluginsForProject(proj);
for (IPlugin* plugin : plugins) {
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() );
for (IProject* project : qAsConst(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);
}
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());
if (ctx) {
const auto items = ctx->items();
for (ProjectBaseItem* item : items) {
projects.insert(item->project());
}
}
}
return projects;
}
void openProjectConfig()
{
auto projects = selectedProjects();
if (projects.count() == 1) {
q->configureProject(*projects.constBegin());
}
}
void closeSelectedProjects()
{
const auto projects = selectedProjects();
for (IProject* project : projects) {
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() )
{
const QString messageText = i18n("Invalid Location: %1", url.toDisplayString(QUrl::PreferLocalFile));
auto* message = new Sublime::Message(messageText, Sublime::Message::Error);
ICore::self()->uiController()->postMessage(message);
return;
}
if ( m_currentlyOpening.contains(url))
{
qCDebug(SHELL) << "Already opening " << url << ". Aborting.";
const QString messageText =
i18n("Already opening %1, not opening again", url.toDisplayString(QUrl::PreferLocalFile));
auto* message = new Sublime::Message(messageText, Sublime::Message::Information);
message->setAutoHide(0);
ICore::self()->uiController()->postMessage(message);
return;
}
const auto projects = m_projects;
for (IProject* project : 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)) {
const QString messageText = i18n("Unable to create configuration file %1", projectFileUrl.url());
auto* message = new Sublime::Message(messageText, Sublime::Message::Error);
ICore::self()->uiController()->postMessage(message);
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_D(ProjectController);
Q_ASSERT(d->dialog);
delete d->dialog;
d->dialog = dialog;
}
ProjectController::ProjectController( Core* core )
: IProjectController(core)
, d_ptr(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()
{
Q_D(ProjectController);
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, [this] { Q_D(ProjectController); 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, [this] { Q_D(ProjectController); 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, [this] (Sublime::Area* area) { Q_D(ProjectController); 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()
{
Q_D(ProjectController);
delete d->model;
delete d->dialog;
}
void ProjectController::cleanup()
{
Q_D(ProjectController);
if ( d->m_currentlyOpening.isEmpty() ) {
d->saveListOfOpenedProjects();
}
saveRecentProjectsActionEntries();
d->m_cleaningUp = true;
if( buildSetModel() ) {
buildSetModel()->storeToSession( Core::self()->activeSession() );
}
closeAllProjects();
}
void ProjectController::saveRecentProjectsActionEntries()
{
Q_D(ProjectController);
if (!d->m_recentProjectsAction)
return;
auto config = KSharedConfig::openConfig();
KConfigGroup recentGroup = config->group("RecentProjects");
d->m_recentProjectsAction->saveEntries( recentGroup );
config->sync();
}
void ProjectController::initialize()
{
Q_D(ProjectController);
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);
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, [this]() { Q_D(ProjectController); d->updateActionStates(); } );
connect(this, &ProjectController::projectOpened,
this, [this]() { Q_D(ProjectController); d->updateActionStates(); });
connect(this, &ProjectController::projectClosing,
this, [this]() { Q_D(ProjectController); 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
{
Q_D(const ProjectController);
return d->m_projects.count();
}
IProject* ProjectController::projectAt( int num ) const
{
Q_D(const ProjectController);
if( !d->m_projects.isEmpty() && num >= 0 && num < d->m_projects.count() )
return d->m_projects.at( num );
return nullptr;
}
QList ProjectController::projects() const
{
Q_D(const ProjectController);
return d->m_projects;
}
void ProjectController::eventuallyOpenProjectFile(KIO::Job* _job, const KIO::UDSEntryList& entries)
{
Q_D(ProjectController);
auto* job = qobject_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{
auto* message = new Sublime::Message(i18n("Project already open: %1", project->name()), Sublime::Message::Error);
Core::self()->uiController()->postMessage(message);
}
}else{
auto* message = new Sublime::Message(i18n("No active document"), Sublime::Message::Error);
Core::self()->uiController()->postMessage(message);
}
}
void ProjectController::openProjectForUrl(const QUrl& sourceUrl) {
Q_D(ProjectController);
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 )
{
Q_D(ProjectController);
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))
{
const auto sessions = Core::self()->sessionController()->sessions();
for (const Session* session : 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
const auto windows = Core::self()->uiController()->controller()->mainWindows();
for (Sublime::MainWindow* window : windows) {
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);
for (const Session* session : qAsConst(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;
for (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, FetchFlags fetchFlags)
{
Q_D(ProjectController);
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) {
if (fetchFlags.testFlag(FetchShowErrorIfNotSupported)) {
const QString messageText =
i18n("No enabled plugin supports this repository URL: %1", repoUrl.toDisplayString());
auto* message = new Sublime::Message(messageText, Sublime::Message::Error);
ICore::self()->uiController()->postMessage(message);
}
return false;
}
const QUrl url = d->dialog->askProjectConfigLocation(true, QUrl(), repoUrl, vcsOrProviderPlugin);
if (!url.isEmpty()) {
d->importProject(url);
}
return true;
}
void ProjectController::fetchProject()
{
Q_D(ProjectController);
QUrl url = d->dialog->askProjectConfigLocation(true);
if ( !url.isEmpty() )
{
d->importProject(url);
}
}
void ProjectController::projectImportingFinished( IProject* project )
{
Q_D(ProjectController);
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)
{
Q_D(ProjectController);
- QList pluginsForProj = d->m_projectPlugins.value( proj );
+ const QList pluginsForProj = d->m_projectPlugins.value( proj );
d->m_projectPlugins.remove( proj );
QList otherProjectPlugins;
for (const QList& _list : qAsConst(d->m_projectPlugins)) {
otherProjectPlugins << _list;
}
+#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
+ QSet pluginsForProjSet(pluginsForProj.begin(), pluginsForProj.end());
+ QSet otherPrjPluginsSet(otherProjectPlugins.constBegin(), otherProjectPlugins.constEnd());
+#else
QSet pluginsForProjSet = QSet::fromList( pluginsForProj );
QSet otherPrjPluginsSet = QSet::fromList( otherProjectPlugins );
+#endif
// 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)
{
const auto documents = Core::self()->documentController()->openDocuments();
for (IDocument* doc : documents) {
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, [this] { Q_D(ProjectController); d->unloadAllProjectPlugins(); });
}
void ProjectController::takeProject(IProject* proj)
{
Q_D(ProjectController);
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()
{
Q_D(ProjectController);
const auto projects = d->m_projects;
for (auto* project : projects) {
closeProject(project);
}
}
void ProjectController::abortOpeningProject(IProject* proj)
{
Q_D(ProjectController);
d->m_currentlyOpening.removeAll(proj->projectFile().toUrl());
emit projectOpeningAborted(proj);
}
ProjectModel* ProjectController::projectModel()
{
Q_D(ProjectController);
return d->model;
}
IProject* ProjectController::findProjectForUrl( const QUrl& url ) const
{
Q_D(const ProjectController);
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_D(ProjectController);
auto it = std::find_if(d->m_projects.constBegin(), d->m_projects.constEnd(), [&](IProject* proj) {
return (proj->name() == name);
});
return (it != d->m_projects.constEnd()) ? *it : nullptr;
}
void ProjectController::configureProject( IProject* project )
{
Q_D(ProjectController);
d->projectConfig( project );
}
void ProjectController::addProject(IProject* project)
{
Q_D(ProjectController);
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
{
const auto projects = this->projects();
return std::any_of(projects.begin(), projects.end(), [&](IProject* p) {
return (p->name() == name);
});
}
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
const auto projects = Core::self()->projectController()->projects();
auto it = std::find_if(projects.begin(), projects.end(), [&](IProject* candidateProject) {
return (candidateProject->path().toUrl().isParentOf(url));
});
if (it != projects.end()) {
project = *it;
}
}
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_D(ProjectController);
Q_UNUSED(parent);
ContextMenuExtension ext;
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, [this] {
Q_D(ProjectController);
const auto projects = d->selectedProjects();
for (auto* project : projects) {
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()
{
Q_D(ProjectController);
return d->buildset;
}
ProjectChangesModel* ProjectController::changesModel()
{
Q_D(ProjectController);
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
{
Q_D(const ProjectController);
Path path(path_);
IProject* sourceDirProject = nullptr, *buildDirProject = nullptr;
for (IProject* proj : qAsConst(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 KDevelop::ProjectController::reparseProject(IProject *project, bool forceUpdate, bool forceAll)
{
Q_D(ProjectController);
if (auto job = d->m_parseJobs.value(project)) {
job->kill();
}
d->m_parseJobs[project] = new KDevelop::ParseProjectJob(project, forceUpdate, forceAll);
ICore::self()->runController()->registerJob(d->m_parseJobs[project]);
}
}
diff --git a/kdevplatform/shell/workingsets/workingset.cpp b/kdevplatform/shell/workingsets/workingset.cpp
index db736e3034..b5b00b2924 100644
--- a/kdevplatform/shell/workingsets/workingset.cpp
+++ b/kdevplatform/shell/workingsets/workingset.cpp
@@ -1,585 +1,595 @@
/*
Copyright David Nolden
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program 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 General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "workingset.h"
#include "debug.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SYNC_OFTEN
using namespace KDevelop;
bool WorkingSet::m_loading = false;
namespace {
QIcon generateIcon(const WorkingSetIconParameters& params)
{
QImage pixmap(16, 16, QImage::Format_ARGB32);
// fill the background with a transparent color
pixmap.fill(QColor::fromRgba(qRgba(0, 0, 0, 0)));
const uint coloredCount = params.coloredCount;
// coordinates of the rectangles to draw, for 16x16 icons specifically
QList rects{
{1, 1, 5, 5},
{1, 9, 5, 5},
{9, 1, 5, 5},
{9, 9, 5, 5},
};
if ( params.swapDiagonal ) {
#if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0))
rects.swapItemsAt(1, 2);
#else
rects.swap(1, 2);
#endif
}
QPainter painter(&pixmap);
// color for non-colored squares, paint them brighter if the working set is the active one
const int inact = 40;
QColor darkColor = QColor::fromRgb(inact, inact, inact);
// color for colored squares
// this code is not fragile, you can just tune the magic formulas at random and see what looks good.
// just make sure to keep it within the 0-360 / 0-255 / 0-255 space of the HSV model
QColor brightColor = QColor::fromHsv(params.hue, qMin(255, 215 + (params.setId*5) % 150),
205 + (params.setId*11) % 50);
// Y'UV "Y" value, the approximate "lightness" of the color
// If it is above 0.6, then making the color darker a bit is okay,
// if it is below 0.35, then the color should be a bit brighter.
float brightY = 0.299 * brightColor.redF() + 0.587 * brightColor.greenF() + 0.114 * brightColor.blueF();
if ( brightY > 0.6 ) {
if ( params.setId % 7 < 2 ) {
// 2/7 chance to make the color significantly darker
brightColor = brightColor.darker(120 + (params.setId*7) % 35);
}
else if ( params.setId % 5 == 0 ) {
// 1/5 chance to make it a bit darker
brightColor = brightColor.darker(110 + (params.setId*3) % 10);
}
}
if ( brightY < 0.35 ) {
// always make the color brighter to avoid very dark colors (like rgb(0, 0, 255))
brightColor = brightColor.lighter(120 + (params.setId*13) % 55);
}
int at = 0;
for (const QRect& rect : qAsConst(rects)) {
QColor currentColor;
// pick the colored squares; you can get different patterns by re-ordering the "rects" list
if ( (at + params.setId*7) % 4 < coloredCount ) {
currentColor = brightColor;
}
else {
currentColor = darkColor;
}
// draw the filling of the square
painter.setPen(QColor(currentColor));
painter.setBrush(QBrush(currentColor));
painter.drawRect(rect);
// draw a slight set-in shadow for the square -- it's barely recognizeable,
// but it looks way better than without
painter.setBrush(Qt::NoBrush);
painter.setPen(QColor(0, 0, 0, 50));
painter.drawRect(rect);
painter.setPen(QColor(0, 0, 0, 25));
painter.drawRect(rect.x() + 1, rect.y() + 1, rect.width() - 2, rect.height() - 2);
at += 1;
}
return QIcon(QPixmap::fromImage(pixmap));
}
}
WorkingSet::WorkingSet(const QString& id)
: QObject()
, m_id(id)
, m_icon(generateIcon(WorkingSetIconParameters(id)))
{
}
void WorkingSet::saveFromArea( Sublime::Area* a, Sublime::AreaIndex * area, KConfigGroup setGroup, KConfigGroup areaGroup )
{
if (area->isSplit()) {
setGroup.writeEntry("Orientation", area->orientation() == Qt::Horizontal ? "Horizontal" : "Vertical");
if (area->first()) {
saveFromArea(a, area->first(), KConfigGroup(&setGroup, "0"), KConfigGroup(&areaGroup, "0"));
}
if (area->second()) {
saveFromArea(a, area->second(), KConfigGroup(&setGroup, "1"), KConfigGroup(&areaGroup, "1"));
}
} else {
setGroup.writeEntry("View Count", area->viewCount());
areaGroup.writeEntry("View Count", area->viewCount());
int index = 0;
const auto views = area->views();
for (Sublime::View* view : views) {
//The working set config gets an updated list of files
QString docSpec = view->document()->documentSpecifier();
//only save the documents from protocols KIO understands
//otherwise we try to load kdev:// too early
if (!KProtocolInfo::isKnownProtocol(QUrl(docSpec))) {
continue;
}
setGroup.writeEntry(QStringLiteral("View %1").arg(index), docSpec);
//The area specific config stores the working set documents in order along with their state
areaGroup.writeEntry(QStringLiteral("View %1").arg(index), docSpec);
KConfigGroup viewGroup(&areaGroup, QStringLiteral("View %1 Config").arg(index));
view->writeSessionConfig(viewGroup);
++index;
}
}
}
bool WorkingSet::isEmpty() const
{
KConfigGroup setConfig(Core::self()->activeSession()->config(), "Working File Sets");
KConfigGroup group = setConfig.group(m_id);
return !group.hasKey("Orientation") && group.readEntry("View Count", 0) == 0;
}
struct DisableMainWindowUpdatesFromArea
{
explicit DisableMainWindowUpdatesFromArea(Sublime::Area* area) : m_area(area) {
if(area) {
const auto windows = Core::self()->uiControllerInternal()->mainWindows();
for (Sublime::MainWindow* window : windows) {
if(window->area() == area) {
if(window->updatesEnabled()) {
wasUpdatesEnabled.insert(window);
window->setUpdatesEnabled(false);
}
}
}
}
}
~DisableMainWindowUpdatesFromArea() {
if(m_area) {
for (Sublime::MainWindow* window : qAsConst(wasUpdatesEnabled)) {
window->setUpdatesEnabled(wasUpdatesEnabled.contains(window));
}
}
}
private:
Q_DISABLE_COPY(DisableMainWindowUpdatesFromArea)
Sublime::Area* m_area;
QSet wasUpdatesEnabled;
};
void loadFileList(QStringList& ret, const KConfigGroup& group)
{
if (group.hasKey("Orientation")) {
QStringList subgroups = group.groupList();
if (subgroups.contains(QStringLiteral("0"))) {
{
KConfigGroup subgroup(&group, "0");
loadFileList(ret, subgroup);
}
if (subgroups.contains(QStringLiteral("1"))) {
KConfigGroup subgroup(&group, "1");
loadFileList(ret, subgroup);
}
}
} else {
int viewCount = group.readEntry("View Count", 0);
ret.reserve(ret.size() + viewCount);
for (int i = 0; i < viewCount; ++i) {
QString specifier = group.readEntry(QStringLiteral("View %1").arg(i), QString());
ret << specifier;
}
}
}
QStringList WorkingSet::fileList() const
{
QStringList ret;
KConfigGroup setConfig(Core::self()->activeSession()->config(), "Working File Sets");
KConfigGroup group = setConfig.group(m_id);
loadFileList(ret, group);
return ret;
}
+QSet WorkingSet::fileSet() const
+{
+ const QStringList fileList = this->fileList();
+#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
+ return QSet(fileList.begin(), fileList.end());
+#else
+ return fileList.toSet();
+#endif
+}
+
void WorkingSet::loadToArea(Sublime::Area* area, Sublime::AreaIndex* areaIndex) {
PushValue enableLoading(m_loading, true);
/// We cannot disable the updates here, because (probably) due to a bug in Qt,
/// which causes the updates to stay disabled forever after some complex operations
/// on the sub-views. This could be reproduced by creating two working-sets with complex
/// split-view configurations and switching between them. Re-enabling the updates doesn't help.
// DisableMainWindowUpdatesFromArea updatesDisabler(area);
qCDebug(SHELL) << "loading working-set" << m_id << "into area" << area;
QMultiMap recycle;
const auto viewsBefore = area->views();
for (Sublime::View* view : viewsBefore) {
recycle.insert( view->document()->documentSpecifier(), area->removeView(view) );
}
qCDebug(SHELL) << "recycling" << recycle.size() << "old views";
Q_ASSERT( area->views().empty() );
KConfigGroup setConfig(Core::self()->activeSession()->config(), "Working File Sets");
KConfigGroup setGroup = setConfig.group(m_id);
KConfigGroup areaGroup = setConfig.group(m_id + QLatin1Char('|') + area->title());
loadToArea(area, areaIndex, setGroup, areaGroup, recycle);
// Delete views which were not recycled
qCDebug(SHELL) << "deleting " << recycle.size() << " old views";
qDeleteAll( recycle );
area->setActiveView(nullptr);
//activate view in the working set
/// @todo correctly select one out of multiple equal views
QString activeView = areaGroup.readEntry("Active View", QString());
const auto viewsAfter = area->views();
for (Sublime::View* v : viewsAfter) {
if (v->document()->documentSpecifier() == activeView) {
area->setActiveView(v);
break;
}
}
if( !area->activeView() && !area->views().isEmpty() )
area->setActiveView( area->views().at(0) );
if( area->activeView() ) {
const auto windows = Core::self()->uiControllerInternal()->mainWindows();
for (Sublime::MainWindow* window : windows) {
if(window->area() == area) {
window->activateView( area->activeView() );
}
}
}
}
void WorkingSet::loadToArea(Sublime::Area* area, Sublime::AreaIndex* areaIndex, const KConfigGroup& setGroup, const KConfigGroup& areaGroup, QMultiMap& recycle)
{
Q_ASSERT( !areaIndex->isSplit() );
if (setGroup.hasKey("Orientation")) {
QStringList subgroups = setGroup.groupList();
/// @todo also save and restore the ratio
if (subgroups.contains(QStringLiteral("0")) && subgroups.contains(QStringLiteral("1"))) {
// qCDebug(SHELL) << "has zero, split:" << split;
Qt::Orientation orientation = setGroup.readEntry("Orientation", "Horizontal") == QLatin1String("Vertical") ? Qt::Vertical : Qt::Horizontal;
if(!areaIndex->isSplit()){
areaIndex->split(orientation);
}else{
areaIndex->setOrientation(orientation);
}
loadToArea(area, areaIndex->first(), KConfigGroup(&setGroup, "0"), KConfigGroup(&areaGroup, "0"), recycle);
loadToArea(area, areaIndex->second(), KConfigGroup(&setGroup, "1"), KConfigGroup(&areaGroup, "1"), recycle);
if( areaIndex->first()->viewCount() == 0 )
areaIndex->unsplit(areaIndex->first());
else if( areaIndex->second()->viewCount() == 0 )
areaIndex->unsplit(areaIndex->second());
}
} else {
//Load all documents from the workingset into this areaIndex
int viewCount = setGroup.readEntry("View Count", 0);
QMap createdViews;
for (int i = 0; i < viewCount; ++i) {
QString specifier = setGroup.readEntry(QStringLiteral("View %1").arg(i), QString());
if (specifier.isEmpty()) {
continue;
}
Sublime::View* previousView = area->views().empty() ? nullptr : area->views().at(area->views().size() - 1);
QMultiMap::iterator it = recycle.find( specifier );
if( it != recycle.end() )
{
area->addView( *it, areaIndex, previousView );
recycle.erase( it );
continue;
}
IDocument* doc = Core::self()->documentControllerInternal()->openDocument(QUrl::fromUserInput(specifier),
KTextEditor::Cursor::invalid(), IDocumentController::DoNotActivate | IDocumentController::DoNotCreateView);
auto *document = dynamic_cast(doc);
if (document) {
Sublime::View* view = document->createView();
area->addView(view, areaIndex, previousView);
createdViews[i] = view;
} else {
qCWarning(SHELL) << "Unable to create view" << specifier;
}
}
//Load state
for (int i = 0; i < viewCount; ++i)
{
KConfigGroup viewGroup(&areaGroup, QStringLiteral("View %1 Config").arg(i));
if (viewGroup.exists() && createdViews.contains(i))
createdViews[i]->readSessionConfig(viewGroup);
}
}
}
void deleteGroupRecursive(KConfigGroup group) {
// qCDebug(SHELL) << "deleting" << group.name();
const auto entryMap = group.entryMap();
for (auto it = entryMap.begin(), end = entryMap.end(); it != end; ++it) {
group.deleteEntry(it.key());
}
Q_ASSERT(group.entryMap().isEmpty());
const auto groupList = group.groupList();
for (const QString& subGroup : groupList) {
deleteGroupRecursive(group.group(subGroup));
group.deleteGroup(subGroup);
}
//Why doesn't this work?
// Q_ASSERT(group.groupList().isEmpty());
group.deleteGroup();
}
void WorkingSet::deleteSet(bool force, bool silent)
{
if(m_areas.isEmpty() || force) {
emit aboutToRemove(this);
KConfigGroup setConfig(Core::self()->activeSession()->config(), "Working File Sets");
KConfigGroup group = setConfig.group(m_id);
deleteGroupRecursive(group);
#ifdef SYNC_OFTEN
setConfig.sync();
#endif
if(!silent)
emit setChangedSignificantly();
}
}
void WorkingSet::saveFromArea(Sublime::Area* area, Sublime::AreaIndex* areaIndex)
{
qCDebug(SHELL) << "saving" << m_id << "from area";
bool wasPersistent = isPersistent();
KConfigGroup setConfig(Core::self()->activeSession()->config(), "Working File Sets");
KConfigGroup setGroup = setConfig.group(m_id);
deleteGroupRecursive(setGroup);
KConfigGroup areaGroup = setConfig.group(m_id + QLatin1Char('|') + area->title());
QString lastActiveView = areaGroup.readEntry("Active View", "");
deleteGroupRecursive(areaGroup);
if (area->activeView() && area->activeView()->document())
areaGroup.writeEntry("Active View", area->activeView()->document()->documentSpecifier());
else
areaGroup.writeEntry("Active View", lastActiveView);
saveFromArea(area, areaIndex, setGroup, areaGroup);
if(isEmpty())
{
deleteGroupRecursive(setGroup);
deleteGroupRecursive(areaGroup);
}
setPersistent(wasPersistent);
#ifdef SYNC_OFTEN
setConfig.sync();
#endif
emit setChangedSignificantly();
}
void WorkingSet::areaViewAdded(Sublime::AreaIndex*, Sublime::View*) {
auto* area = qobject_cast