[plasma/union] src: Introduce StylePackage as a class that represents a packaged style
Arjen Hiemstra <[email protected]> Wed, 5 Aug 2026 10:33:23 +0000 (UTC)
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit a372f5dd062b2bf08dcb9dd5b89aab4c1ba993d2 by Arjen Hiemstra.
Committed on 05/08/2026 at 10:24.
Pushed by ahiemstra into branch 'master'.
Introduce StylePackage as a class that represents a packaged style
This encapsulates functionality that handles a style as a package of
assets with certain required files. It also exposes metadata about the
package, that can be used for KCMs and other UIs that want to show
information about a style.
M +2 -0 src/CMakeLists.txt
M +10 -1 src/InputPlugin.h
A +297 -0 src/StylePackage.cpp [License: LGPL(3+eV) LGPL(v3.0) LGPL(v2.1)]
A +132 -0 src/StylePackage.h [License: LGPL(3+eV) LGPL(v3.0) LGPL(v2.1)]
M +11 -0 src/input/css/CssPlugin.cpp
M +2 -0 src/input/css/CssPlugin.h
https://invent.kde.org/plasma/union/-/commit/a372f5dd062b2bf08dcb9dd5b89aab4c1ba993d2
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 663d3022..b672e987 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -17,6 +17,7 @@ target_sources(Union PRIVATE
Color.cpp
PlatformPlugin.cpp
StyleCache.cpp
+ StylePackage.cpp
)
target_sources(Union PUBLIC
@@ -37,6 +38,7 @@ target_sources(Union PUBLIC
Color.h
PluginRegistry.h
PlatformPlugin.h
+ StylePackage.h
)
ecm_qt_declare_logging_category(Union
diff --git a/src/InputPlugin.h b/src/InputPlugin.h
index 91ecf7cd..e3fe7296 100644
--- a/src/InputPlugin.h
+++ b/src/InputPlugin.h
@@ -4,6 +4,7 @@
#pragma once
#include "PluginRegistry.h"
+#include "StylePackage.h"
#include "union_export.h"
@@ -30,7 +31,15 @@ public:
InputPlugin(QObject *parent = nullptr);
/*!
- * Create an instance of a style by name.
+ * Validate if a given package is a valid package for this input.
+ *
+ * This should be reimplemented by subclasses and check whether the given
+ * package has all files required for createStyle() to work correctly.
+ */
+ virtual StylePackage::Error validatePackage(const StylePackage &package) = 0;
+
+ /*!
+ * Create an instance of a style from a package.
*
* This should be reimplemented by subclasses and create a new instance of
* Style using an appropriate StyleLoader and other input specific data.
diff --git a/src/StylePackage.cpp b/src/StylePackage.cpp
new file mode 100644
index 00000000..2db46c7b
--- /dev/null
+++ b/src/StylePackage.cpp
@@ -0,0 +1,297 @@
+// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+// SPDX-FileCopyrightText: 2026 Arjen Hiemstra <[email protected]>
+
+#include "StylePackage.h"
+
+#include <QFile>
+#include <QJsonArray>
+
+#include "InputPlugin.h"
+#include "Style.h"
+
+#include "union_logging.h"
+
+using namespace Union;
+using namespace Qt::StringLiterals;
+
+namespace fs = std::filesystem;
+
+struct StyleMetaData {
+ QString name;
+ QString description;
+ QString version;
+ QString license;
+ QUrl url;
+ QStringList authors;
+};
+
+class UNION_NO_EXPORT StylePackage::Private
+{
+public:
+ Error error = Error::None;
+
+ fs::path path;
+ QString inputType;
+
+ std::optional<StyleMetaData> metaData;
+};
+
+StylePackage::StylePackage()
+ : StylePackage(fs::path())
+{
+}
+
+StylePackage::StylePackage(const std::filesystem::path &path)
+ : d(std::make_unique<Private>())
+{
+ d->path = path;
+
+ if (!fs::exists(d->path)) {
+ qCDebug(UNION_GENERAL) << "Package" << path.string() << "was not found";
+ d->error = Error::NotFound;
+ } else if (!fs::exists(d->path / "metadata.json")) {
+ qCDebug(UNION_GENERAL) << "Could not find metadata.json in package" << path.string();
+ d->error = Error::MissingFiles;
+ } else if (!fs::exists(d->path / "contents")) {
+ qCDebug(UNION_GENERAL) << "Could not find contents directory in package" << path.string();
+ d->error = Error::MissingFiles;
+ }
+}
+
+StylePackage::StylePackage(StylePackage &&other)
+ : d(std::move(other.d))
+{
+}
+
+Union::StylePackage::StylePackage(const StylePackage &other)
+ : d(std::make_unique<Private>())
+{
+ *this = other;
+}
+
+StylePackage::~StylePackage() = default;
+
+StylePackage &Union::StylePackage::operator=(StylePackage &&other)
+{
+ std::swap(d, other.d);
+ return *this;
+}
+
+StylePackage &Union::StylePackage::operator=(const StylePackage &other)
+{
+ if (this != &other) {
+ d->error = other.d->error;
+ d->path = other.d->path;
+ d->inputType = other.d->inputType;
+ d->metaData = other.d->metaData;
+ }
+ return *this;
+}
+
+fs::path StylePackage::path() const
+{
+ return d->path;
+}
+
+QString StylePackage::id() const
+{
+ return QString::fromStdString(d->path.filename());
+}
+
+QString StylePackage::inputType() const
+{
+ if (!isValid()) {
+ return {};
+ }
+
+ return d->inputType;
+}
+
+QString StylePackage::name() const
+{
+ loadMetadata();
+
+ if (d->metaData) {
+ return d->metaData->name;
+ }
+
+ return {};
+}
+
+QString StylePackage::description() const
+{
+ loadMetadata();
+
+ if (d->metaData) {
+ return d->metaData->description;
+ }
+
+ return {};
+}
+
+QString StylePackage::version() const
+{
+ loadMetadata();
+
+ if (d->metaData) {
+ return d->metaData->version;
+ }
+
+ return {};
+}
+
+QString StylePackage::license() const
+{
+ loadMetadata();
+
+ if (d->metaData) {
+ return d->metaData->license;
+ }
+
+ return {};
+}
+
+QUrl StylePackage::url() const
+{
+ loadMetadata();
+
+ if (d->metaData) {
+ return d->metaData->url;
+ }
+
+ return {};
+}
+
+QStringList StylePackage::authors() const
+{
+ loadMetadata();
+
+ if (d->metaData) {
+ return d->metaData->authors;
+ }
+
+ return {};
+}
+
+bool StylePackage::isValid() const
+{
+ if (d->error != Error::None) {
+ return false;
+ }
+
+ if (d->inputType.isEmpty()) {
+ validate();
+ }
+
+ return d->error == Error::None;
+}
+
+StylePackage::Error StylePackage::error() const
+{
+ return d->error;
+}
+
+Style::Ptr StylePackage::load()
+{
+ if (!isValid()) {
+ return nullptr;
+ }
+
+ loadMetadata();
+
+ auto inputPlugin = InputPlugin::inputPlugin(d->inputType);
+ if (!inputPlugin) {
+ return nullptr;
+ }
+
+ return inputPlugin->createStyle(*this);
+}
+
+void StylePackage::validate() const
+{
+ if (d->error != Error::None) {
+ return;
+ }
+
+ QFile metaDataFile(d->path / "metadata.json");
+ if (!metaDataFile.open(QIODevice::ReadOnly)) {
+ qCDebug(UNION_GENERAL) << "Could not open metadata.json of package" << d->path.string() << "for reading";
+ d->error = Error::InvalidMetaData;
+ return;
+ }
+
+ QJsonParseError parseError;
+ auto json = QJsonDocument::fromJson(metaDataFile.readAll(), &parseError).object();
+ if (parseError.error != QJsonParseError::NoError) {
+ qCDebug(UNION_GENERAL) << "Could not parse metadata for package" << d->path.string() << parseError.errorString();
+ d->error = Error::InvalidMetaData;
+ return;
+ }
+
+ if (json.isEmpty()) {
+ qCDebug(UNION_GENERAL) << "Metadata for package" << d->path.string() << "is not an object";
+ d->error = Error::InvalidMetaData;
+ return;
+ }
+
+ d->inputType = json.value(u"input-type").toString();
+
+ auto inputPlugin = InputPlugin::inputPlugin(d->inputType);
+ if (!inputPlugin) {
+ qCDebug(UNION_GENERAL) << "Could not find input type" << d->inputType << "for package" << d->path.string();
+ d->error = Error::UnknownInputType;
+ return;
+ }
+
+ if (auto result = inputPlugin->validatePackage(*this); result != Error::None) {
+ qCDebug(UNION_GENERAL) << "Input plugin" << d->inputType << "considers package" << d->path.string() << "invalid";
+ d->error = result;
+ return;
+ }
+}
+
+void StylePackage::loadMetadata() const
+{
+ if (d->metaData) {
+ return;
+ }
+
+ if (!fs::exists(d->path / "metadata.json")) {
+ return;
+ }
+
+ QFile metaDataFile(d->path / "metadata.json");
+ if (!metaDataFile.open(QIODevice::ReadOnly)) {
+ return;
+ }
+
+ auto json = QJsonDocument::fromJson(metaDataFile.readAll()).object();
+
+ auto metaData = StyleMetaData{};
+ metaData.name = json.value(u"name").toString();
+ metaData.description = json.value(u"description").toString();
+ metaData.version = json.value(u"version").toString();
+ metaData.license = json.value(u"license").toString();
+ metaData.url = QUrl{json.value(u"url").toString()};
+
+ const auto authors = json.value(u"authors").toArray();
+ std::ranges::transform(authors, std::back_inserter(metaData.authors), [](const QJsonValue &value) {
+ return value.toString();
+ });
+
+ d->metaData = metaData;
+}
+
+QDebug operator<<(QDebug debug, const StylePackage &package)
+{
+ QDebugStateSaver saver(debug);
+ debug.nospace() << "StylePackage(";
+ debug << package.path().string();
+ if (package.isValid()) {
+ debug.nospace() << ", valid, inputType: " << package.inputType();
+ } else {
+ debug.nospace() << ", invalid: " << int(package.error());
+ }
+ debug << ")";
+ return debug;
+}
diff --git a/src/StylePackage.h b/src/StylePackage.h
new file mode 100644
index 00000000..6555f19e
--- /dev/null
+++ b/src/StylePackage.h
@@ -0,0 +1,132 @@
+// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+// SPDX-FileCopyrightText: 2026 Arjen Hiemstra <[email protected]>
+
+#pragma once
+
+#include <filesystem>
+
+#include <QObject>
+
+#include "union_export.h"
+
+namespace Union
+{
+class InputPlugin;
+class Style;
+
+/*!
+ * A bundle of assets that represent a style.
+ *
+ * The StylePackage class is a representation of a package containing style
+ * assets, along with metadata about that style.
+ *
+ * Packages are effectively directories with certain expected contents. At the
+ * very least, a package is expected to contain a "metadata.json" file and a
+ * "contents" directory. Other files may be required depending on the input
+ * format used for the package.
+ */
+class UNION_EXPORT StylePackage
+{
+public:
+ /*!
+ *
+ */
+ enum class Error {
+ None,
+ NotFound,
+ MissingFiles,
+ InvalidMetaData,
+ UnknownInputType,
+ };
+
+ /*!
+ * Default constructor.
+ *
+ * Creates an empty and invalid package.
+ */
+ StylePackage();
+ /*!
+ * Creates a package pointing at a specific directories.
+ */
+ explicit StylePackage(const std::filesystem::path &path);
+ StylePackage(StylePackage &&other);
+ StylePackage(const StylePackage &other);
+ ~StylePackage();
+
+ StylePackage &operator=(StylePackage &&other);
+ StylePackage &operator=(const StylePackage &other);
+
+ /*!
+ * Return the path of this package.
+ */
+ std::filesystem::path path() const;
+ /*!
+ * Returns the ID of this package.
+ *
+ * This is effectively the filename of the path.
+ */
+ QString id() const;
+ /*!
+ * Return the input type that is required for this package.
+ */
+ QString inputType() const;
+ /*!
+ * Returns the name of this package.
+ */
+ QString name() const;
+ /*!
+ * Returns the description of this package.
+ */
+ QString description() const;
+ /*!
+ * Returns the version of this package.
+ *
+ * Note that there are no constraints on the format of the version.
+ */
+ QString version() const;
+ /*!
+ * Returns the license of this package.
+ */
+ QString license() const;
+ /*!
+ * Returns the URL of this package.
+ */
+ QUrl url() const;
+ /*!
+ * Returns the list of authors of this package.
+ *
+ * This is just a list of author names.
+ */
+ QStringList authors() const;
+
+ /*!
+ * Returns if the package is valid.
+ *
+ * This will load the package's metadata file to determine the input type
+ * for this package, then ask the input plugin to validate the package, if
+ * that was not done before.
+ *
+ * You can use the error() method to check what kind of problem an invalid
+ * package has.
+ */
+ bool isValid() const;
+ /*!
+ * Returns the error that caused the package to be invalid.
+ */
+ Error error() const;
+ /*!
+ * Load the package fully and use it to create a new Style instance.
+ */
+ std::shared_ptr<Style> load();
+
+private:
+ void validate() const;
+ void loadMetadata() const;
+
+ class Private;
+ std::unique_ptr<Private> d;
+};
+
+}
+
+UNION_EXPORT QDebug operator<<(QDebug debug, const Union::StylePackage &package);
diff --git a/src/input/css/CssPlugin.cpp b/src/input/css/CssPlugin.cpp
index b7168e5a..467451bb 100644
--- a/src/input/css/CssPlugin.cpp
+++ b/src/input/css/CssPlugin.cpp
@@ -9,11 +9,22 @@
using namespace Qt::StringLiterals;
+namespace fs = std::filesystem;
+
CssPlugin::CssPlugin(QObject *parent)
: Union::InputPlugin(parent)
{
}
+Union::StylePackage::Error CssPlugin::validatePackage(const Union::StylePackage &package)
+{
+ if (!fs::exists(package.path() / "contents" / "css" / "style.css")) {
+ return StylePackage::Error::MissingFiles;
+ }
+
+ return StylePackage::Error::None;
+}
+
std::shared_ptr<Union::Style> CssPlugin::createStyle(const QString &styleName) const
{
return Union::Style::create(name(), styleName, std::make_unique<CssLoader>());
diff --git a/src/input/css/CssPlugin.h b/src/input/css/CssPlugin.h
index f8381a58..256ac2c6 100644
--- a/src/input/css/CssPlugin.h
+++ b/src/input/css/CssPlugin.h
@@ -18,5 +18,7 @@ class CssPlugin : public Union::InputPlugin
public:
CssPlugin(QObject *parent = nullptr);
+ Union::StylePackage::Error validatePackage(const Union::StylePackage &package) override;
+
std::shared_ptr<Union::Style> createStyle(const QString &styleName) const override;
};