diff --git a/src/plugins/CMakeLists.txt b/src/plugins/CMakeLists.txt
index 5d07d99608f..92eef226d81 100644
--- a/src/plugins/CMakeLists.txt
+++ b/src/plugins/CMakeLists.txt
@@ -4,6 +4,7 @@ add_subdirectory(coreplugin)
# Level 1: (only depends of Level 0)
add_subdirectory(texteditor)
add_subdirectory(serialterminal)
+add_subdirectory(extensionmanager)
add_subdirectory(helloworld)
add_subdirectory(imageviewer)
add_subdirectory(marketplace)
diff --git a/src/plugins/extensionmanager/CMakeLists.txt b/src/plugins/extensionmanager/CMakeLists.txt
new file mode 100644
index 00000000000..1afb628ff70
--- /dev/null
+++ b/src/plugins/extensionmanager/CMakeLists.txt
@@ -0,0 +1,12 @@
+add_qtc_plugin(ExtensionManager
+ PLUGIN_DEPENDS Core
+ SOURCES
+ extensionmanager.qrc
+ extensionmanagerconstants.h
+ extensionmanagerplugin.cpp
+ extensionmanagertr.h
+ extensionmanagerwidget.cpp
+ extensionmanagerwidget.h
+ extensionsbrowser.cpp
+ extensionsbrowser.h
+)
diff --git a/src/plugins/extensionmanager/ExtensionManager.json.in b/src/plugins/extensionmanager/ExtensionManager.json.in
new file mode 100644
index 00000000000..1dcc10805be
--- /dev/null
+++ b/src/plugins/extensionmanager/ExtensionManager.json.in
@@ -0,0 +1,20 @@
+{
+ "Name" : "ExtensionManager",
+ "Version" : "${IDE_VERSION}",
+ "CompatVersion" : "${IDE_VERSION_COMPAT}",
+ "Vendor" : "The Qt Company Ltd",
+ "Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd",
+ "License" : [ "Commercial Usage",
+ "",
+ "Licensees holding valid Qt Commercial licenses may use this plugin in accordance with the Qt Commercial License Agreement provided with the Software or, alternatively, in accordance with the terms contained in a written agreement between you and The Qt Company.",
+ "",
+ "GNU General Public License Usage",
+ "",
+ "Alternatively, this plugin may be used under the terms of the GNU General Public License version 3 as published by the Free Software Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT included in the packaging of this plugin. Please review the following information to ensure the GNU General Public License requirements will be met: https://www.gnu.org/licenses/gpl-3.0.html."
+ ],
+ "Category" : "Core",
+ "Description" : "Extension Manager",
+ "Experimental": true,
+ "Url" : "http://www.qt.io",
+ ${IDE_PLUGIN_DEPENDENCIES}
+}
diff --git a/src/plugins/extensionmanager/extensionmanager.qbs b/src/plugins/extensionmanager/extensionmanager.qbs
new file mode 100644
index 00000000000..062bdfc6526
--- /dev/null
+++ b/src/plugins/extensionmanager/extensionmanager.qbs
@@ -0,0 +1,18 @@
+import qbs 1.0
+
+QtcPlugin {
+ name: "ExtensionManager"
+
+ Depends { name: "Core" }
+
+ files: [
+ "extensionmanager.qrc",
+ "extensionmanagerconstants.h",
+ "extensionmanagerplugin.cpp",
+ "extensionmanagertr.h",
+ "extensionmanagerwidget.cpp",
+ "extensionmanagerwidget.h",
+ "extensionsbrowser.cpp",
+ "extensionsbrowser.h",
+ ]
+}
diff --git a/src/plugins/extensionmanager/extensionmanager.qrc b/src/plugins/extensionmanager/extensionmanager.qrc
new file mode 100644
index 00000000000..cff16c156ff
--- /dev/null
+++ b/src/plugins/extensionmanager/extensionmanager.qrc
@@ -0,0 +1,10 @@
+
+
+ images/extensionsmall.png
+ images/extensionsmall@2x.png
+ images/mode_extensionmanager_mask.png
+ images/mode_extensionmanager_mask@2x.png
+ images/packsmall.png
+ images/packsmall@2x.png
+
+
diff --git a/src/plugins/extensionmanager/extensionmanagerconstants.h b/src/plugins/extensionmanager/extensionmanagerconstants.h
new file mode 100644
index 00000000000..8605cae6c1e
--- /dev/null
+++ b/src/plugins/extensionmanager/extensionmanagerconstants.h
@@ -0,0 +1,11 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#pragma once
+
+namespace ExtensionManager::Constants {
+
+const char MODE_EXTENSIONMANAGER[] = "ExtensionManager";
+const char C_EXTENSIONMANAGER[] = "ExtensionManager";
+
+} // ExtensionManager::Constant
diff --git a/src/plugins/extensionmanager/extensionmanagerplugin.cpp b/src/plugins/extensionmanager/extensionmanagerplugin.cpp
new file mode 100644
index 00000000000..1e7eecff16b
--- /dev/null
+++ b/src/plugins/extensionmanager/extensionmanagerplugin.cpp
@@ -0,0 +1,91 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#include "extensionmanagertr.h"
+
+#include "extensionmanagerconstants.h"
+#include "extensionmanagerwidget.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include // TODO: Remove!
+
+#include
+#include
+
+#include
+#include
+#include
+
+#include
+#include
+
+using namespace ExtensionSystem;
+using namespace Core;
+using namespace Utils;
+
+namespace ExtensionManager::Internal {
+
+class ExtensionManagerMode final : public IMode
+{
+public:
+ ExtensionManagerMode()
+ {
+ setObjectName("ExtensionManagerMode");
+ setId(Constants::C_EXTENSIONMANAGER);
+ setContext(Context(Constants::MODE_EXTENSIONMANAGER));
+ setDisplayName(Tr::tr("Extensions"));
+ const Icon FLAT({{":/extensionmanager/images/mode_extensionmanager_mask.png",
+ Theme::IconsBaseColor}});
+ const Icon FLAT_ACTIVE({{":/extensionmanager/images/mode_extensionmanager_mask.png",
+ Theme::IconsModeWelcomeActiveColor}});
+ setIcon(Utils::Icon::modeIcon(FLAT, FLAT, FLAT_ACTIVE));
+ setPriority(72);
+
+ using namespace Layouting;
+ auto widget = Column {
+ new StyledBar,
+ new ExtensionManagerWidget,
+ noMargin, spacing(0),
+ }.emerge();
+
+ setWidget(widget);
+ }
+
+ ~ExtensionManagerMode() { delete widget(); }
+};
+
+class ExtensionManagerPlugin final : public ExtensionSystem::IPlugin
+{
+ Q_OBJECT
+ Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QtCreatorPlugin" FILE "ExtensionManager.json")
+
+public:
+ ~ExtensionManagerPlugin() final
+ {
+ delete m_mode;
+ }
+
+ void initialize() final
+ {
+ m_mode = new ExtensionManagerMode;
+ }
+
+ bool delayedInitialize() final
+ {
+ ModeManager::activateMode(m_mode->id()); // TODO: Remove!
+ return true;
+ }
+
+private:
+ ExtensionManagerMode *m_mode = nullptr;
+};
+
+} // ExtensionManager::Internal
+
+#include "extensionmanagerplugin.moc"
diff --git a/src/plugins/extensionmanager/extensionmanagertr.h b/src/plugins/extensionmanager/extensionmanagertr.h
new file mode 100644
index 00000000000..c4221e8f6e8
--- /dev/null
+++ b/src/plugins/extensionmanager/extensionmanagertr.h
@@ -0,0 +1,15 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#pragma once
+
+#include
+
+namespace ExtensionManager {
+
+struct Tr
+{
+ Q_DECLARE_TR_FUNCTIONS(QtC::ExtensionManager)
+};
+
+} // ExtensionManager
diff --git a/src/plugins/extensionmanager/extensionmanagerwidget.cpp b/src/plugins/extensionmanager/extensionmanagerwidget.cpp
new file mode 100644
index 00000000000..4eb6850c91c
--- /dev/null
+++ b/src/plugins/extensionmanager/extensionmanagerwidget.cpp
@@ -0,0 +1,305 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#include "extensionmanagertr.h"
+
+#include "extensionmanagerconstants.h"
+#include "extensionmanagerwidget.h"
+#include "extensionsbrowser.h"
+#include "utils/algorithm.h"
+
+#include
+#include
+#include
+#include
+#include // TODO: Remove!
+#include
+
+#include
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+using namespace Core;
+using namespace Utils;
+
+namespace ExtensionManager::Internal {
+
+static QWidget *createVr(QWidget *parent = nullptr)
+{
+ auto vr = new QWidget(parent);
+ vr->setFixedWidth(1);
+ setBackgroundColor(vr, Theme::Token_Stroke_Subtle);
+ return vr;
+}
+
+class CollapsingWidget : public QWidget
+{
+public:
+ explicit CollapsingWidget(QWidget *parent = nullptr)
+ : QWidget(parent)
+ {
+ setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
+ }
+
+ void setWidth(int width)
+ {
+ m_width = width;
+ setVisible(width > 0);
+ updateGeometry();
+ }
+
+ QSize sizeHint() const override
+ {
+ return {m_width, 0};
+ }
+
+private:
+ int m_width = 100;
+};
+
+ExtensionManagerWidget::ExtensionManagerWidget()
+{
+ m_leftColumn = new ExtensionsBrowser;
+
+ auto descriptionColumns = new QWidget;
+
+ m_secondarDescriptionWidget = new CollapsingWidget;
+
+ m_primaryDescription = new QTextBrowser;
+ m_primaryDescription->setOpenExternalLinks(true);
+ m_primaryDescription->setFrameStyle(QFrame::NoFrame);
+
+ m_secondaryDescription = new QTextBrowser;
+ m_secondaryDescription->setFrameStyle(QFrame::NoFrame);
+
+ using namespace Layouting;
+ Row {
+ createVr(),
+ m_secondaryDescription,
+ noMargin(), spacing(0),
+ }.attachTo(m_secondarDescriptionWidget);
+
+ Row {
+ createVr(),
+ Row {
+ m_primaryDescription,
+ noMargin(),
+ },
+ m_secondarDescriptionWidget,
+ noMargin(), spacing(0),
+ }.attachTo(descriptionColumns);
+
+ Row {
+ Space(WelcomePageHelpers::HSpacing),
+ m_leftColumn,
+ descriptionColumns,
+ noMargin(), spacing(0),
+ }.attachTo(this);
+
+ setBackgroundColor(this, Theme::Token_Background_Default);
+
+ connect(m_leftColumn, &ExtensionsBrowser::itemSelected,
+ this, &ExtensionManagerWidget::updateView);
+ connect(this, &ResizeSignallingWidget::resized, this,
+ [this] (const QSize &size, const QSize &oldSize) {
+ const int intendedLeftColumnWidth = size.width() - 580;
+ m_leftColumn->adjustToWidth(intendedLeftColumnWidth);
+ const bool secondaryDescriptionVisible = size.width() > 970;
+ const int secondaryDescriptionWidth = secondaryDescriptionVisible ? 264 : 0;
+ m_secondarDescriptionWidget->setWidth(secondaryDescriptionWidth);
+ });
+ updateView({}, {});
+}
+
+void ExtensionManagerWidget::updateView(const QModelIndex ¤t,
+ [[maybe_unused]] const QModelIndex &previous)
+{
+ const QString h5Css =
+ StyleHelper::fontToCssProperties(StyleHelper::uiFont(StyleHelper::UiElementH5))
+ + "; margin-top: 28px;";
+ const QString h6Css =
+ StyleHelper::fontToCssProperties(StyleHelper::uiFont(StyleHelper::UiElementH6))
+ + "; margin-top: 28px;";
+ const QString h6CapitalCss =
+ StyleHelper::fontToCssProperties(StyleHelper::uiFont(StyleHelper::UiElementH6Capital))
+ + QString::fromLatin1("; color: %1;")
+ .arg(creatorTheme()->color(Theme::Token_Text_Muted).name());
+ const QString bodyStyle = QString::fromLatin1("color: %1; background-color: %2;"
+ "margin-left: %3px; margin-right: %3px;")
+ .arg(creatorTheme()->color(Theme::Token_Text_Default).name())
+ .arg(creatorTheme()->color(Theme::Token_Background_Muted).name())
+ .arg(WelcomePageHelpers::HSpacing);
+ const QString htmlStart = QString(R"(
+
+
+ )").arg(bodyStyle);
+ const QString htmlEnd = QString(R"(
+
+ )");
+
+ if (!current.isValid()) {
+ const QString emptyHtml = htmlStart + htmlEnd;
+ m_primaryDescription->setText(emptyHtml);
+ m_secondaryDescription->setText(emptyHtml);
+ return;
+ }
+
+ const ItemData data = itemData(current);
+ const bool isPack = data.type == ItemTypePack;
+ const ExtensionSystem::PluginSpec *extension = data.plugins.first();
+
+ {
+ const QString shortDescription =
+ isPack ? QLatin1String("Short description for pack ") + data.name
+ : extension->description();
+ QString longDescription =
+ isPack ? QLatin1String("Some longer text that describes the purpose and functionality "
+ "of the extensions that are part of pack ") + data.name
+ : extension->longDescription();
+ longDescription.replace("\n", "
");
+ const QString location = isPack ? extension->location() : extension->filePath();
+
+ QString description = htmlStart;
+
+ description.append(QString(R"(
+
%2
+ %3
+ )").arg(h5Css)
+ .arg(shortDescription)
+ .arg(longDescription));
+
+ description.append(QString(R"(
+ %2
+ %3
+ )").arg(h6Css)
+ .arg(Tr::tr("Get started"))
+ .arg(Tr::tr("Install the extension from above. Installation starts automatically. "
+ "You can always uninstall the extension afterwards.")));
+
+ description.append(QString(R"(
+ %2
+
+ %4 >
+
+ %6 >
+
+ )").arg(h6Css)
+ .arg(Tr::tr("More information"))
+ .arg(Tr::tr("Online Documentation"))
+ .arg("https://doc.qt.io/qtcreator/")
+ .arg(Tr::tr("Tutorials"))
+ .arg("https://doc.qt.io/qtcreator/creator-tutorials.html"));
+
+ const QString examplesBoxCss =
+ QString::fromLatin1("height: 168px; background-color: %1; ")
+ .arg(creatorTheme()->color(Theme::Token_Background_Default).name());
+ description.append(QString(R"(
+ %2
+
+
+
+ )").arg(h6CapitalCss)
+ .arg(Tr::tr("Examples"))
+ .arg(examplesBoxCss));
+
+ const QString captionStrongCss = StyleHelper::fontToCssProperties(
+ StyleHelper::uiFont(StyleHelper::UiElementCaptionStrong));
+ description.append(QString(R"(
+ %2
+
+
+
+ )").arg(h6Css)
+ .arg(Tr::tr("Extension library details"))
+ .arg(captionStrongCss)
+ .arg(Tr::tr("Size"))
+ .arg("547 MB")
+ .arg(Tr::tr("Version"))
+ .arg(extension->version())
+ .arg(Tr::tr("Location"))
+ .arg(location));
+
+ description.append(htmlEnd);
+ m_primaryDescription->setText(description);
+ }
+
+ {
+ QString description = htmlStart;
+
+ description.append(QString(R"(
+
%2
+ )").arg(h6CapitalCss)
+ .arg(Tr::tr("Extension details")));
+
+ description.append(QString(R"(
+ %2
+ %3
+ )").arg(h6Css)
+ .arg(Tr::tr("Released"))
+ .arg("23.5.2023"));
+
+ const QString tagTemplate = QString(R"(
+ %2 |
+ )").arg(creatorTheme()->color(Theme::Token_Stroke_Subtle).name());
+ const QStringList tags = Utils::transform(data.tags,
+ [&tagTemplate] (const QString &tag) {
+ return tagTemplate.arg(tag);
+ });
+ description.append(QString(R"(
+ %2
+ %3
+ )").arg(h6Css)
+ .arg(Tr::tr("Related tags"))
+ .arg(tags.join(" ")));
+
+ description.append(QString(R"(
+ %2
+
+ macOS
+ Windows
+ Linux
+
+ )").arg(h6Css)
+ .arg(Tr::tr("Platforms")));
+
+ QStringList dependencies;
+ for (const ExtensionSystem::PluginSpec *spec : data.plugins) {
+ dependencies.append(Utils::transform(spec->dependencies(),
+ &ExtensionSystem::PluginDependency::toString));
+ }
+ dependencies.removeDuplicates();
+ dependencies.sort();
+ description.append(QString(R"(
+ %2
+ %3
+ )").arg(h6Css)
+ .arg(Tr::tr("Dependencies"))
+ .arg(dependencies.isEmpty() ? "-" : dependencies.join("
")));
+
+ if (isPack) {
+ const QStringList extensions = Utils::transform(data.plugins,
+ &ExtensionSystem::PluginSpec::name);
+ description.append(QString(R"(
+ %2
+ %3
+ )").arg(h6Css)
+ .arg(Tr::tr("Exentsions in pack"))
+ .arg(extensions.join("
")));
+ }
+
+ description.append(htmlEnd);
+ m_secondaryDescription->setText(description);
+ }
+}
+
+} // ExtensionManager::Internal
diff --git a/src/plugins/extensionmanager/extensionmanagerwidget.h b/src/plugins/extensionmanager/extensionmanagerwidget.h
new file mode 100644
index 00000000000..e99eb9898eb
--- /dev/null
+++ b/src/plugins/extensionmanager/extensionmanagerwidget.h
@@ -0,0 +1,33 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#include "extensionmanagertr.h"
+
+#include "extensionmanagerconstants.h"
+
+#include
+
+QT_BEGIN_NAMESPACE
+class QTextBrowser;
+QT_END_NAMESPACE
+
+namespace ExtensionManager::Internal {
+
+class CollapsingWidget;
+class ExtensionsBrowser;
+
+class ExtensionManagerWidget final : public Core::ResizeSignallingWidget
+{
+public:
+ explicit ExtensionManagerWidget();
+
+private:
+ void updateView(const QModelIndex ¤t, [[maybe_unused]] const QModelIndex &previous);
+
+ ExtensionsBrowser *m_leftColumn;
+ CollapsingWidget *m_secondarDescriptionWidget;
+ QTextBrowser *m_primaryDescription;
+ QTextBrowser *m_secondaryDescription;
+};
+
+} // ExtensionManager::Internal
diff --git a/src/plugins/extensionmanager/extensionsbrowser.cpp b/src/plugins/extensionmanager/extensionsbrowser.cpp
new file mode 100644
index 00000000000..fe48f48b833
--- /dev/null
+++ b/src/plugins/extensionmanager/extensionsbrowser.cpp
@@ -0,0 +1,524 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#include "extensionmanagertr.h"
+
+#include "extensionsbrowser.h"
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+using namespace ExtensionSystem;
+using namespace Core;
+using namespace Utils;
+
+namespace ExtensionManager::Internal {
+
+Q_LOGGING_CATEGORY(browserLog, "qtc.extensionmanager.browser", QtWarningMsg)
+
+using PluginSpecList = QList;
+using Tags = QStringList;
+
+constexpr QSize itemSize = {330, 86};
+constexpr int gapSize = 2 * WelcomePageHelpers::GridItemGap;
+constexpr QSize cellSize = {itemSize.width() + gapSize, itemSize.height() + gapSize};
+
+enum Role {
+ RoleName = Qt::UserRole,
+ RoleItemType,
+ RoleTags,
+ RolePluginSpecs,
+ RoleSearchText,
+};
+
+ItemData itemData(const QModelIndex &index)
+{
+ return {
+ index.data(RoleName).toString(),
+ index.data(RoleItemType).value(),
+ index.data(RoleTags).toStringList(),
+ index.data(RolePluginSpecs).value(),
+ };
+}
+
+void setBackgroundColor(QWidget *widget, Theme::Color colorRole)
+{
+ QPalette palette = creatorTheme()->palette();
+ palette.setColor(QPalette::Window,
+ creatorTheme()->color(colorRole));
+ widget->setPalette(palette);
+ widget->setBackgroundRole(QPalette::Window);
+ widget->setAutoFillBackground(true);
+}
+
+static QColor colorForExtensionName(const QString &name)
+{
+ const size_t hash = qHash(name);
+ return QColor::fromHsv(hash % 360, 180, 110);
+}
+
+static QStandardItemModel *extensionsModel()
+{
+ // The new extensions concept renames plugins to extensions and adds "packs" which are
+ // groups of extensions.
+ //
+ // TODO: The "meta data" here which is injected into the model is only a place holder that
+ // helps exploring the upcoming extensions concept.
+ //
+ // Before this loses the WIP prefix, we should at least have a concrete idea of how the data
+ // is structured and where it lives. Ideally, it continues to reside exclusively in the
+ // extension meta data.
+ //
+ // The grouping of extensions into packs could be done via extension tag. Extensions and will
+ // receive tags and if possible screen shots.
+ // Packs will also have a complete set of meta data. That could be an accumulation of the data
+ // of the contained extensions. Or simply the data from the "first" extension in a pack.
+
+ static const char tagBuildTools[] = "Build Tools";
+ static const char tagCodeAnalyzing[] = "Code Analyzing";
+ static const char tagConnectivity[] = "Connectivity";
+ static const char tagCore[] = "Core";
+ static const char tagCpp[] = "C++";
+ static const char tagEditorConvenience[] = "Editor Convenience";
+ static const char tagEditor[] = "Editor";
+ static const char tagEssentials[] = "Essentials";
+ static const char tagGlsl[] = "GLSL";
+ static const char tagPackageManager[] = "Package Manager";
+ static const char tagPlatformSupport[] = "Platform Support";
+ static const char tagProgrammingLanguage[] = "Programming Language";
+ static const char tagPython[] = "Python";
+ static const char tagQml[] = "QML";
+ static const char tagQuick[] = "Quick";
+ static const char tagService[] = "Service";
+ static const char tagTestAutomation[] = "Test Automation";
+ static const char tagUiEditor[] = "Visual UI Editor" ;
+ static const char tagVersionControl[] = "Version Control";
+ static const char tagVisualEditor[] = "Visual editor";
+ static const char tagWidgets[] = "Widgets";
+
+ static const char tagTagUndefined[] = "Tag undefined";
+
+ static const struct {
+ const QString name;
+ const QStringList extensions;
+ const Tags tags;
+ } packs[] = {
+ {"Core",
+ {"Core", "Help", "ProjectExplorer", "TextEditor", "Welcome", "GenericProjectManager",
+ "QtSupport"},
+ {tagCore}
+ },
+ {"Core (from Installer)",
+ {"LicenseChecker", "Marketplace", "UpdateInfo"},
+ {tagCore}
+ },
+ {"Essentials",
+ {"Bookmarks", "BinEditor", "Debugger", "DiffEditor", "ImageViewer", "Macros",
+ "LanguageClient", "ResourceEditor"},
+ {tagEssentials}
+ },
+ {"C++ Language support",
+ {"ClangCodeModel", "ClangFormat", "ClassView", "CppEditor"},
+ {tagProgrammingLanguage, tagCpp}
+ },
+ {"QML Language Support (Qt Quick libraries)",
+ {"QmlJSEditor", "QmlJSTools", "QmlPreview", "QmlProfiler", "QmlProjectManager"},
+ {tagProgrammingLanguage, tagQml}
+ },
+ {"Visual QML UI Editor",
+ {"QmlDesigner", "QmlDesignerBase"},
+ {tagUiEditor, tagQml, tagQuick}
+ },
+ {"Visual C++ Widgets UI Editor",
+ {"Designer"},
+ {tagUiEditor, tagCpp, tagWidgets}
+ },
+ };
+
+ static const struct {
+ const QString name;
+ const Tags tags;
+ } extensions[] = {
+ {"GLSLEditor", {tagProgrammingLanguage, tagGlsl}},
+ {"Nim", {tagProgrammingLanguage}},
+ {"Python", {tagProgrammingLanguage, tagPython}},
+ {"Haskell", {tagProgrammingLanguage}},
+
+ {"ModelEditor", {tagVisualEditor}},
+ {"ScxmlEditor", {tagVisualEditor}},
+
+ {"Bazaar", {tagVersionControl}},
+ {"CVS", {tagVersionControl}},
+ {"ClearCase", {tagVersionControl}},
+ {"Fossil", {tagVersionControl}},
+ {"Git", {tagVersionControl}},
+ {"Mercurial", {tagVersionControl}},
+ {"Perforce", {tagVersionControl}},
+ {"Subversion", {tagVersionControl}},
+ {"VcsBase", {tagVersionControl}},
+ {"GitLab", {tagVersionControl, tagService}},
+
+ {"AutoTest", {tagTestAutomation}},
+ {"Squish", {tagTestAutomation}},
+ {"Coco", {tagTestAutomation}},
+
+ {"Vcpkg", {tagPackageManager}},
+ {"Conan", {tagPackageManager}},
+
+ {"Copilot", {tagEditorConvenience}},
+ {"EmacsKeys", {tagEditorConvenience}},
+ {"FakeVim", {tagEditorConvenience}},
+ {"Terminal", {tagEditorConvenience}},
+ {"Todo", {tagEditorConvenience}},
+ {"CodePaster", {tagEditorConvenience}},
+ {"Beautifier", {tagEditorConvenience}},
+
+ {"SerialTerminal", {tagConnectivity}},
+
+ {"SilverSearcher", {tagEditor}},
+
+ {"AutotoolsProjectManager", {tagBuildTools}},
+ {"CMakeProjectManager", {tagBuildTools}},
+ {"CompilationDatabaseProjectManager", {tagBuildTools}},
+ {"IncrediBuild", {tagBuildTools}},
+ {"MesonProjectManager", {tagBuildTools}},
+ {"QbsProjectManager", {tagBuildTools}},
+ {"QmakeProjectManager", {tagBuildTools}},
+
+ {"Axivion", {tagCodeAnalyzing}},
+ {"ClangTools", {tagCodeAnalyzing}},
+ {"Cppcheck", {tagCodeAnalyzing}},
+ {"CtfVisualizer", {tagCodeAnalyzing}},
+ {"PerfProfiler", {tagCodeAnalyzing}},
+ {"Valgrind", {tagCodeAnalyzing}},
+
+ {"Android", {tagPlatformSupport}},
+ {"BareMetal", {tagPlatformSupport}},
+ {"Boot2Qt", {tagPlatformSupport}},
+ {"Ios", {tagPlatformSupport}},
+ {"McuSupport", {tagPlatformSupport}},
+ {"Qnx", {tagPlatformSupport}},
+ {"RemoteLinux", {tagPlatformSupport}},
+ {"SafeRenderer", {tagPlatformSupport}},
+ {"VxWorks", {tagPlatformSupport}},
+ {"WebAssembly", {tagPlatformSupport}},
+ {"Docker", {tagPlatformSupport}},
+
+ // Missing in Kimmo's excel sheet:
+ {"CompilerExplorer", {tagTagUndefined}},
+ {"ExtensionManager", {tagTagUndefined}},
+ {"ScreenRecorder", {tagTagUndefined}},
+ };
+
+ QList items;
+ QStringList expectedExtensions;
+ QStringList unexpectedExtensions;
+ QHash installedPlugins;
+ for (const PluginSpec *ps : PluginManager::plugins()) {
+ installedPlugins.insert(ps->name(), ps);
+ unexpectedExtensions.append(ps->name());
+ }
+
+ const auto handleExtension = [&] (const ItemData &extension, bool addToBrowser) {
+ if (!installedPlugins.contains(extension.name)) {
+ expectedExtensions.append(extension.name);
+ return false;
+ }
+ unexpectedExtensions.removeOne(extension.name);
+
+ if (addToBrowser) {
+ QStandardItem *item = new QStandardItem;
+ const PluginSpecList pluginSpecs = {installedPlugins.value(extension.name)};
+ item->setData(ItemTypeExtension, RoleItemType);
+ item->setData(QVariant::fromValue(extension.tags), RoleTags);
+ item->setData(QVariant::fromValue(pluginSpecs), RolePluginSpecs);
+ item->setData(extension.name, RoleName);
+ items.append(item);
+ }
+
+ return true;
+ };
+
+ const bool addPackedExtensionsToBrowser = true; // TODO: Determine how we want this. As setting?
+ for (const auto &pack : packs) {
+ PluginSpecList pluginSpecs;
+ for (const QString &extension : pack.extensions) {
+ const ItemData extensionData = {extension, {}, pack.tags, {}};
+ if (!handleExtension(extensionData, addPackedExtensionsToBrowser))
+ continue;
+ pluginSpecs.append(installedPlugins.value(extension));
+ }
+ if (pluginSpecs.isEmpty())
+ continue;
+
+ QStandardItem *item = new QStandardItem;
+ item->setData(ItemTypePack, RoleItemType);
+ item->setData(QVariant::fromValue(pack.tags), RoleTags);
+ item->setData(QVariant::fromValue(pluginSpecs), RolePluginSpecs);
+ item->setData(pack.name, RoleName);
+ items.append(item);
+ }
+
+ for (const auto &extension : extensions) {
+ const ItemData extensionData = {extension.name, {}, extension.tags, {}};
+ handleExtension(extensionData, true);
+ }
+
+ QStandardItemModel *result = new QStandardItemModel;
+ for (auto item : items) {
+ QStringList searchTexts;
+ searchTexts.append(item->data(RoleName).toString());
+ searchTexts.append(item->data(RoleTags).toStringList());
+ const PluginSpecList pluginSpecs = item->data(RolePluginSpecs).value();
+ for (auto pluginSpec : pluginSpecs) {
+ searchTexts.append(pluginSpec->name());
+ searchTexts.append(pluginSpec->description());
+ searchTexts.append(pluginSpec->longDescription());
+ searchTexts.append(pluginSpec->category());
+ searchTexts.append(pluginSpec->copyright());
+ }
+ searchTexts.removeDuplicates();
+ item->setData(searchTexts.join(" "), RoleSearchText);
+
+ item->setDragEnabled(false);
+ item->setEditable(false);
+
+ result->appendRow(item);
+ }
+
+ if (browserLog().isDebugEnabled()) {
+ if (!expectedExtensions.isEmpty())
+ qCDebug(browserLog) << "Expected extensions/plugins are not installed:"
+ << expectedExtensions.join(", ");
+ if (!unexpectedExtensions.isEmpty())
+ qCDebug(browserLog) << "Unexpected extensions/plugins are installed:"
+ << unexpectedExtensions.join(", ");
+ }
+
+ return result;
+}
+
+class ExtensionItemDelegate : public QItemDelegate
+{
+public:
+ explicit ExtensionItemDelegate(QObject *parent = nullptr)
+ : QItemDelegate(parent)
+ {
+ }
+
+ void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index)
+ const override
+ {
+ painter->save();
+ painter->setRenderHint(QPainter::Antialiasing);
+
+ const ItemData data = itemData(index);
+ const bool isPack = data.type == ItemTypePack;
+ const QRectF itemRect(option.rect.topLeft(), itemSize);
+ {
+ const bool selected = option.state & QStyle::State_Selected;
+ const bool hovered = option.state & QStyle::State_MouseOver;
+ constexpr qreal strokeWidth = 1;
+ constexpr qreal shrink = strokeWidth / 2;
+ const QRectF itemRectAdjusted = itemRect.adjusted(shrink, shrink, -shrink, -shrink);
+ constexpr qreal rounding = 4.5;
+ QPainterPath itemOutlinePath;
+ itemOutlinePath.addRoundedRect(itemRectAdjusted, rounding, rounding);
+ const QColor fillColor = creatorTheme()->color(hovered ? Theme::Token_Background_Hover
+ : Theme::Token_Background_Muted);
+ const QColor strokeColor = creatorTheme()->color(selected ? Theme::Token_Stroke_Strong
+ : Theme::Token_Stroke_Subtle);
+ painter->setBrush(fillColor);
+ painter->setPen(strokeColor);
+ painter->drawPath(itemOutlinePath);
+ }
+ {
+ constexpr QRectF bigCircle(16, 16, 48, 48);
+ constexpr double gradientMargin = 0.14645;
+ const QRectF bigCircleLocal = bigCircle.translated(itemRect.topLeft());
+ QPainterPath bigCirclePath;
+ bigCirclePath.addEllipse(bigCircleLocal);
+ QLinearGradient gradient(bigCircleLocal.topLeft(), bigCircleLocal.bottomRight());
+ const QColor startColor = isPack ? qRgb(0x1e, 0x99, 0x6e)
+ : colorForExtensionName(data.name);
+ const QColor endColor = isPack ? qRgb(0x07, 0x6b, 0x6d) : startColor.lighter(150);
+ gradient.setColorAt(gradientMargin, startColor);
+ gradient.setColorAt(1 - gradientMargin, endColor);
+ painter->fillPath(bigCirclePath, gradient);
+
+ static const QIcon packIcon =
+ Icon({{":/extensionmanager/images/packsmall.png",
+ Theme::Token_Text_Default}}, Icon::Tint).icon();
+ static const QIcon extensionIcon =
+ Icon({{":/extensionmanager/images/extensionsmall.png",
+ Theme::Token_Text_Default}}, Icon::Tint).icon();
+ QRectF iconRect(0, 0, 32, 32);
+ iconRect.moveCenter(bigCircleLocal.center());
+ (isPack ? packIcon : extensionIcon).paint(painter, iconRect.toRect());
+ }
+ if (isPack) {
+ constexpr QRectF smallCircle(47, 50, 18, 18);
+ constexpr qreal strokeWidth = 1;
+ constexpr qreal shrink = strokeWidth / 2;
+ constexpr QRectF smallCircleAdjusted = smallCircle.adjusted(shrink, shrink,
+ -shrink, -shrink);
+ const QRectF smallCircleLocal = smallCircleAdjusted.translated(itemRect.topLeft());
+ const QColor fillColor = creatorTheme()->color(Theme::Token_Background_Hover);
+ const QColor strokeColor = creatorTheme()->color(Theme::Token_Stroke_Subtle);
+ painter->setBrush(fillColor);
+ painter->setPen(strokeColor);
+ painter->drawEllipse(smallCircleLocal);
+
+ painter->setFont(StyleHelper::uiFont(StyleHelper::UiElementCaptionStrong));
+ const QColor textColor = creatorTheme()->color(Theme::Token_Text_Default);
+ painter->setPen(textColor);
+ painter->drawText(smallCircleLocal, QString::number(data.plugins.count()),
+ QTextOption(Qt::AlignCenter));
+ }
+ {
+ constexpr int textX = 80;
+ constexpr int rightMargin = 2 * WelcomePageHelpers::ItemGap;
+ constexpr int maxTextWidth = itemSize.width() - textX - rightMargin;
+ constexpr Qt::TextElideMode elideMode = Qt::ElideRight;
+
+ constexpr int titleY = 30;
+ const QPointF titleOrigin(itemRect.topLeft() + QPointF(textX, titleY));
+ painter->setPen(creatorTheme()->color(Theme::Token_Text_Default));
+ painter->setFont(StyleHelper::uiFont(StyleHelper::UiElementH6));
+ const QString titleElided = painter->fontMetrics().elidedText(
+ data.name, elideMode, maxTextWidth);
+ painter->drawText(titleOrigin, titleElided);
+
+ constexpr int copyrightY = 52;
+ const QPointF copyrightOrigin(itemRect.topLeft() + QPointF(textX, copyrightY));
+ painter->setPen(creatorTheme()->color(Theme::Token_Text_Muted));
+ painter->setFont(StyleHelper::uiFont(StyleHelper::UiElementCaptionStrong));
+ const QString copyrightElided = painter->fontMetrics().elidedText(
+ data.plugins.first()->copyright(), elideMode, maxTextWidth);
+ painter->drawText(copyrightOrigin, copyrightElided);
+
+ constexpr int tagsY = 70;
+ const QPointF tagsOrigin(itemRect.topLeft() + QPointF(textX, tagsY));
+ const QString tags = data.tags.join(", ");
+ painter->setPen(creatorTheme()->color(Theme::Token_Text_Default));
+ painter->setFont(StyleHelper::uiFont(StyleHelper::UiElementCaption));
+ const QString tagsElided = painter->fontMetrics().elidedText(
+ tags, elideMode, maxTextWidth);
+ painter->drawText(tagsOrigin, tagsElided);
+ }
+
+ painter->restore();
+ }
+
+ QSize sizeHint([[maybe_unused]] const QStyleOptionViewItem &option,
+ [[maybe_unused]] const QModelIndex &index) const override
+ {
+ return cellSize;
+ }
+};
+
+ExtensionsBrowser::ExtensionsBrowser()
+{
+ setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
+
+ auto manageLabel = new QLabel(Tr::tr("Manage Extensions"));
+ manageLabel->setFont(StyleHelper::uiFont(StyleHelper::UiElementH1));
+
+ m_searchBox = new Core::SearchBox;
+ m_searchBox->setFixedWidth(itemSize.width());
+
+ m_updateButton = new WelcomePageButton;
+ m_updateButton->setText(Tr::tr("Install..."));
+
+ m_filterProxyModel = new QSortFilterProxyModel(this);
+ m_filterProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
+ m_filterProxyModel->setFilterRole(RoleSearchText);
+ m_filterProxyModel->setSortRole(RoleItemType);
+
+ m_extensionsView = new QListView;
+ m_extensionsView->setFrameStyle(QFrame::NoFrame);
+ m_extensionsView->setItemDelegate(new ExtensionItemDelegate(this));
+ m_extensionsView->setResizeMode(QListView::Adjust);
+ m_extensionsView->setSelectionMode(QListView::SingleSelection);
+ m_extensionsView->setUniformItemSizes(true);
+ m_extensionsView->setViewMode(QListView::IconMode);
+ m_extensionsView->setModel(m_filterProxyModel);
+ m_extensionsView->setMouseTracking(true);
+
+ using namespace Layouting;
+ Column {
+ Space(15),
+ manageLabel,
+ Space(15),
+ Row { m_searchBox, st, m_updateButton, Space(extraListViewWidth() + gapSize) },
+ Space(gapSize),
+ m_extensionsView,
+ noMargin(), spacing(0),
+ }.attachTo(this);
+
+ setBackgroundColor(this, Theme::Token_Background_Default);
+ setBackgroundColor(m_extensionsView, Theme::Token_Background_Default);
+ setBackgroundColor(m_extensionsView->viewport(), Theme::Token_Background_Default);
+
+ auto updateModel = [this] {
+ m_model.reset(extensionsModel());
+ m_filterProxyModel->setSourceModel(m_model.data());
+ m_filterProxyModel->sort(0);
+
+ if (m_selectionModel == nullptr) {
+ m_selectionModel = new QItemSelectionModel(m_filterProxyModel, m_extensionsView);
+ m_extensionsView->setSelectionModel(m_selectionModel);
+ connect(m_extensionsView->selectionModel(), &QItemSelectionModel::currentChanged,
+ this, &ExtensionsBrowser::itemSelected);
+ }
+ };
+
+ connect(ExtensionSystem::PluginManager::instance(),
+ &ExtensionSystem::PluginManager::pluginsChanged, this, updateModel);
+ connect(m_searchBox->m_lineEdit, &Utils::FancyLineEdit::textChanged,
+ m_filterProxyModel, &QSortFilterProxyModel::setFilterWildcard);
+}
+
+void ExtensionsBrowser::adjustToWidth(const int width)
+{
+ const int widthForItems = width - extraListViewWidth();
+ m_columnsCount = qMax(1, qFloor(widthForItems / cellSize.width()));
+ m_updateButton->setVisible(m_columnsCount > 1);
+ updateGeometry();
+}
+
+QSize ExtensionsBrowser::sizeHint() const
+{
+ const int columsWidth = m_columnsCount * cellSize.width();
+ return { columsWidth + extraListViewWidth(), 0};
+}
+
+int ExtensionsBrowser::extraListViewWidth() const
+{
+ // TODO: Investigate "transient" scrollbar, just for this list view.
+ return m_extensionsView->style()->pixelMetric(QStyle::PM_ScrollBarExtent)
+ + 1; // Needed
+}
+
+} // ExtensionManager::Internal
diff --git a/src/plugins/extensionmanager/extensionsbrowser.h b/src/plugins/extensionmanager/extensionsbrowser.h
new file mode 100644
index 00000000000..7a41becf0e6
--- /dev/null
+++ b/src/plugins/extensionmanager/extensionsbrowser.h
@@ -0,0 +1,72 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#pragma once
+
+#include
+
+#include
+#include
+
+QT_BEGIN_NAMESPACE
+class QItemSelectionModel;
+class QListView;
+class QSortFilterProxyModel;
+QT_END_NAMESPACE
+
+namespace ExtensionSystem
+{
+class PluginSpec;
+}
+
+namespace Core {
+class SearchBox;
+class WelcomePageButton;
+}
+
+namespace ExtensionManager::Internal {
+
+using PluginSpecList = QList;
+using Tags = QStringList;
+
+enum ItemType {
+ ItemTypePack,
+ ItemTypeExtension,
+};
+
+struct ItemData {
+ const QString name;
+ const ItemType type = ItemTypeExtension;
+ const Tags tags;
+ const PluginSpecList plugins;
+};
+
+ItemData itemData(const QModelIndex &index);
+void setBackgroundColor(QWidget *widget, Utils::Theme::Color colorRole);
+
+class ExtensionsBrowser final : public QWidget
+{
+ Q_OBJECT
+
+public:
+ ExtensionsBrowser();
+
+ void adjustToWidth(const int width);
+ QSize sizeHint() const override;
+
+signals:
+ void itemSelected(const QModelIndex ¤t, const QModelIndex &previous);
+
+private:
+ int extraListViewWidth() const; // Space for scrollbar, etc.
+
+ QScopedPointer m_model;
+ Core::SearchBox *m_searchBox;
+ Core::WelcomePageButton *m_updateButton;
+ QListView *m_extensionsView;
+ QItemSelectionModel *m_selectionModel = nullptr;
+ QSortFilterProxyModel *m_filterProxyModel;
+ int m_columnsCount = 2;
+};
+
+} // ExtensionManager::Internal
diff --git a/src/plugins/extensionmanager/images/extensionsmall.png b/src/plugins/extensionmanager/images/extensionsmall.png
new file mode 100644
index 00000000000..6cdb8df12c9
Binary files /dev/null and b/src/plugins/extensionmanager/images/extensionsmall.png differ
diff --git a/src/plugins/extensionmanager/images/extensionsmall@2x.png b/src/plugins/extensionmanager/images/extensionsmall@2x.png
new file mode 100644
index 00000000000..f788b5565a8
Binary files /dev/null and b/src/plugins/extensionmanager/images/extensionsmall@2x.png differ
diff --git a/src/plugins/extensionmanager/images/mode_extensionmanager_mask.png b/src/plugins/extensionmanager/images/mode_extensionmanager_mask.png
new file mode 100644
index 00000000000..8caaf5ab8cc
Binary files /dev/null and b/src/plugins/extensionmanager/images/mode_extensionmanager_mask.png differ
diff --git a/src/plugins/extensionmanager/images/mode_extensionmanager_mask@2x.png b/src/plugins/extensionmanager/images/mode_extensionmanager_mask@2x.png
new file mode 100644
index 00000000000..539aa5eb858
Binary files /dev/null and b/src/plugins/extensionmanager/images/mode_extensionmanager_mask@2x.png differ
diff --git a/src/plugins/extensionmanager/images/packsmall.png b/src/plugins/extensionmanager/images/packsmall.png
new file mode 100644
index 00000000000..7a1c5e09bc7
Binary files /dev/null and b/src/plugins/extensionmanager/images/packsmall.png differ
diff --git a/src/plugins/extensionmanager/images/packsmall@2x.png b/src/plugins/extensionmanager/images/packsmall@2x.png
new file mode 100644
index 00000000000..a58632c756c
Binary files /dev/null and b/src/plugins/extensionmanager/images/packsmall@2x.png differ
diff --git a/src/plugins/plugins.qbs b/src/plugins/plugins.qbs
index 6adaa25ae7e..7277e1afbb1 100644
--- a/src/plugins/plugins.qbs
+++ b/src/plugins/plugins.qbs
@@ -37,6 +37,7 @@ Project {
"designer/designer.qbs",
"diffeditor/diffeditor.qbs",
"docker/docker.qbs",
+ "extensionmanager/extensionmanager.qbs",
"fakevim/fakevim.qbs",
"fossil/fossil.qbs",
"emacskeys/emacskeys.qbs",
diff --git a/src/tools/icons/qtcreatoricons.svg b/src/tools/icons/qtcreatoricons.svg
index 60accd46bb6..a6052a92002 100644
--- a/src/tools/icons/qtcreatoricons.svg
+++ b/src/tools/icons/qtcreatoricons.svg
@@ -645,6 +645,14 @@
width="16"
id="backgroundRect"
style="display:inline;fill:#ffffff" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+