diff options
Diffstat (limited to 'src/assets')
172 files changed, 5962 insertions, 0 deletions
diff --git a/src/assets/CMakeLists.txt b/src/assets/CMakeLists.txt new file mode 100644 index 0000000000..da9c0c14ee --- /dev/null +++ b/src/assets/CMakeLists.txt @@ -0,0 +1,7 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause +add_subdirectory(icons) + +if (NOT INTEGRITY AND TARGET Qt6::Network AND TARGET Qt6::Concurrent) + add_subdirectory(downloader) +endif() diff --git a/src/assets/downloader/CMakeLists.txt b/src/assets/downloader/CMakeLists.txt new file mode 100644 index 0000000000..872932ad2a --- /dev/null +++ b/src/assets/downloader/CMakeLists.txt @@ -0,0 +1,26 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +qt_internal_add_module(ExamplesAssetDownloaderPrivate + CONFIG_MODULE_NAME examples_asset_downloader + STATIC + INTERNAL_MODULE + SOURCES + assetdownloader.cpp assetdownloader.h + tasking/barrier.cpp tasking/barrier.h + tasking/concurrentcall.h + tasking/networkquery.cpp tasking/networkquery.h + tasking/qprocesstask.cpp tasking/qprocesstask.h + tasking/tasking_global.h + tasking/tasktree.cpp tasking/tasktree.h + tasking/tasktreerunner.cpp tasking/tasktreerunner.h + DEFINES + QT_NO_CAST_FROM_ASCII + PUBLIC_LIBRARIES + Qt6::Concurrent + Qt6::Core + Qt6::CorePrivate + Qt6::Network + NO_GENERATE_CPP_EXPORTS +) + diff --git a/src/assets/downloader/assetdownloader.cpp b/src/assets/downloader/assetdownloader.cpp new file mode 100644 index 0000000000..47caf58bf7 --- /dev/null +++ b/src/assets/downloader/assetdownloader.cpp @@ -0,0 +1,557 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "assetdownloader.h" + +#include "tasking/concurrentcall.h" +#include "tasking/networkquery.h" +#include "tasking/tasktreerunner.h" + +#include <QtCore/private/qzipreader_p.h> + +#include <QtCore/QDir> +#include <QtCore/QFile> +#include <QtCore/QJsonArray> +#include <QtCore/QJsonDocument> +#include <QtCore/QJsonObject> +#include <QtCore/QStandardPaths> +#include <QtCore/QTemporaryDir> +#include <QtCore/QTemporaryFile> + +using namespace Tasking; + +QT_BEGIN_NAMESPACE + +namespace Assets::Downloader { + +struct DownloadableAssets +{ + QUrl remoteUrl; + QList<QUrl> files; +}; + +class AssetDownloaderPrivate +{ +public: + AssetDownloaderPrivate(AssetDownloader *q) : m_q(q) {} + AssetDownloader *m_q = nullptr; + + std::unique_ptr<QNetworkAccessManager> m_manager; + std::unique_ptr<QTemporaryDir> m_temporaryDir; + TaskTreeRunner m_taskTreeRunner; + QString m_lastProgressText; + QDir m_localDownloadDir; + + QString m_jsonFileName; + QString m_zipFileName; + QDir m_preferredLocalDownloadDir = + QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation); + QUrl m_offlineAssetsFilePath; + QUrl m_downloadBase; + + void setLocalDownloadDir(const QDir &dir) + { + if (m_localDownloadDir != dir) { + m_localDownloadDir = dir; + emit m_q->localDownloadDirChanged(QUrl::fromLocalFile(m_localDownloadDir.absolutePath())); + } + } + void setProgress(int progressValue, int progressMaximum, const QString &progressText) + { + m_lastProgressText = progressText; + emit m_q->progressChanged(progressValue, progressMaximum, progressText); + } + void updateProgress(int progressValue, int progressMaximum) + { + setProgress(progressValue, progressMaximum, m_lastProgressText); + } + void clearProgress(const QString &progressText) + { + setProgress(0, 0, progressText); + } + + void setupDownload(NetworkQuery *query, const QString &progressText) + { + query->setNetworkAccessManager(m_manager.get()); + clearProgress(progressText); + QObject::connect(query, &NetworkQuery::started, query, [this, query] { + QNetworkReply *reply = query->reply(); + QObject::connect(reply, &QNetworkReply::downloadProgress, + query, [this](qint64 bytesReceived, qint64 totalBytes) { + updateProgress((totalBytes > 0) ? 100.0 * bytesReceived / totalBytes : 0, 100); + }); + }); + } +}; + +static bool isWritableDir(const QDir &dir) +{ + if (dir.exists()) { + QTemporaryFile file(dir.filePath(QString::fromLatin1("tmp"))); + return file.open(); + } + return false; +} + +static bool sameFileContent(const QFileInfo &first, const QFileInfo &second) +{ + if (first.exists() ^ second.exists()) + return false; + + if (first.size() != second.size()) + return false; + + QFile firstFile(first.absoluteFilePath()); + QFile secondFile(second.absoluteFilePath()); + + if (firstFile.open(QFile::ReadOnly) && secondFile.open(QFile::ReadOnly)) { + char char1; + char char2; + int readBytes1 = 0; + int readBytes2 = 0; + while (!firstFile.atEnd()) { + readBytes1 = firstFile.read(&char1, 1); + readBytes2 = secondFile.read(&char2, 1); + if (readBytes1 != readBytes2 || readBytes1 != 1) + return false; + if (char1 != char2) + return false; + } + return true; + } + + return false; +} + +static bool createDirectory(const QDir &dir) +{ + if (dir.exists()) + return true; + + if (!createDirectory(dir.absoluteFilePath(QString::fromUtf8("..")))) + return false; + + return dir.mkpath(QString::fromUtf8(".")); +} + +static bool canBeALocalBaseDir(const QDir &dir) +{ + if (dir.exists()) + return !dir.isEmpty() || isWritableDir(dir); + return createDirectory(dir) && isWritableDir(dir); +} + +static QDir baseLocalDir(const QDir &preferredLocalDir) +{ + if (canBeALocalBaseDir(preferredLocalDir)) + return preferredLocalDir; + + qWarning().noquote() << "AssetDownloader: Cannot set \"" << preferredLocalDir + << "\" as a local download directory!"; + return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation); +} + +static QString pathFromUrl(const QUrl &url) +{ + return url.isLocalFile() ? url.toLocalFile() : url.toString(); +} + +static QList<QUrl> filterDownloadableAssets(const QList<QUrl> &assetFiles, const QDir &expectedDir) +{ + QList<QUrl> downloadList; + std::copy_if(assetFiles.begin(), assetFiles.end(), std::back_inserter(downloadList), + [&](const QUrl &assetPath) { + return !QFileInfo::exists(expectedDir.absoluteFilePath(assetPath.toString())); + }); + return downloadList; +} + +static bool allAssetsPresent(const QList<QUrl> &assetFiles, const QDir &expectedDir) +{ + return std::all_of(assetFiles.begin(), assetFiles.end(), [&](const QUrl &assetPath) { + return QFileInfo::exists(expectedDir.absoluteFilePath(assetPath.toString())); + }); +} + +AssetDownloader::AssetDownloader(QObject *parent) + : QObject(parent) + , d(new AssetDownloaderPrivate(this)) +{} + +AssetDownloader::~AssetDownloader() = default; + +QUrl AssetDownloader::downloadBase() const +{ + return d->m_downloadBase; +} + +void AssetDownloader::setDownloadBase(const QUrl &downloadBase) +{ + if (d->m_downloadBase != downloadBase) { + d->m_downloadBase = downloadBase; + emit downloadBaseChanged(d->m_downloadBase); + } +} + +QUrl AssetDownloader::preferredLocalDownloadDir() const +{ + return QUrl::fromLocalFile(d->m_preferredLocalDownloadDir.absolutePath()); +} + +void AssetDownloader::setPreferredLocalDownloadDir(const QUrl &localDir) +{ + if (!localDir.isLocalFile()) + qWarning() << "preferredLocalDownloadDir Should be a local directory"; + + const QString path = pathFromUrl(localDir); + if (d->m_preferredLocalDownloadDir != path) { + d->m_preferredLocalDownloadDir.setPath(path); + emit preferredLocalDownloadDirChanged(preferredLocalDownloadDir()); + } +} + +QUrl AssetDownloader::offlineAssetsFilePath() const +{ + return d->m_offlineAssetsFilePath; +} + +void AssetDownloader::setOfflineAssetsFilePath(const QUrl &offlineAssetsFilePath) +{ + if (d->m_offlineAssetsFilePath != offlineAssetsFilePath) { + d->m_offlineAssetsFilePath = offlineAssetsFilePath; + emit offlineAssetsFilePathChanged(d->m_offlineAssetsFilePath); + } +} + +QString AssetDownloader::jsonFileName() const +{ + return d->m_jsonFileName; +} + +void AssetDownloader::setJsonFileName(const QString &jsonFileName) +{ + if (d->m_jsonFileName != jsonFileName) { + d->m_jsonFileName = jsonFileName; + emit jsonFileNameChanged(d->m_jsonFileName); + } +} + +QString AssetDownloader::zipFileName() const +{ + return d->m_zipFileName; +} + +void AssetDownloader::setZipFileName(const QString &zipFileName) +{ + if (d->m_zipFileName != zipFileName) { + d->m_zipFileName = zipFileName; + emit zipFileNameChanged(d->m_zipFileName); + } +} + +QUrl AssetDownloader::localDownloadDir() const +{ + return QUrl::fromLocalFile(d->m_localDownloadDir.absolutePath()); +} + +static void precheckLocalFile(const QUrl &url) +{ + if (url.isEmpty()) + return; + QFile file(pathFromUrl(url)); + if (!file.open(QIODevice::ReadOnly)) + qWarning() << "Cannot open local file" << url; +} + +static void readAssetsFileContent(QPromise<DownloadableAssets> &promise, const QByteArray &content) +{ + const QJsonObject json = QJsonDocument::fromJson(content).object(); + const QJsonArray assetsArray = json[u"assets"].toArray(); + DownloadableAssets result; + result.remoteUrl = json[u"url"].toString(); + for (const QJsonValue &asset : assetsArray) { + if (promise.isCanceled()) + return; + result.files.append(asset.toString()); + } + + if (result.files.isEmpty() || result.remoteUrl.isEmpty()) + promise.future().cancel(); + else + promise.addResult(result); +} + +static void unzip(QPromise<void> &promise, const QByteArray &content, const QDir &directory, + const QString &fileName) +{ + const QString zipFilePath = directory.absoluteFilePath(fileName); + QFile zipFile(zipFilePath); + if (!zipFile.open(QIODevice::WriteOnly)) { + promise.future().cancel(); + return; + } + zipFile.write(content); + zipFile.close(); + + if (promise.isCanceled()) + return; + + QZipReader reader(zipFilePath); + const bool extracted = reader.extractAll(directory.absolutePath()); + reader.close(); + if (extracted) + QFile::remove(zipFilePath); + else + promise.future().cancel(); +} + +static void writeAsset(QPromise<void> &promise, const QByteArray &content, const QString &filePath) +{ + const QFileInfo fileInfo(filePath); + QFile file(fileInfo.absoluteFilePath()); + if (!createDirectory(fileInfo.dir()) || !file.open(QFile::WriteOnly)) { + promise.future().cancel(); + return; + } + + if (promise.isCanceled()) + return; + + file.write(content); + file.close(); +} + +static void copyAndCheck(QPromise<void> &promise, const QString &sourcePath, const QString &destPath) +{ + QFile sourceFile(sourcePath); + QFile destFile(destPath); + const QFileInfo sourceFileInfo(sourceFile.fileName()); + const QFileInfo destFileInfo(destFile.fileName()); + + if (destFile.exists() && !destFile.remove()) { + qWarning().noquote() << QString::fromLatin1("Unable to remove file \"%1\".") + .arg(QFileInfo(destFile.fileName()).absoluteFilePath()); + promise.future().cancel(); + return; + } + + if (!createDirectory(destFileInfo.absolutePath())) { + qWarning().noquote() << QString::fromLatin1("Cannot create directory \"%1\".") + .arg(destFileInfo.absolutePath()); + promise.future().cancel(); + return; + } + + if (promise.isCanceled()) + return; + + if (!sourceFile.copy(destFile.fileName()) && !sameFileContent(sourceFileInfo, destFileInfo)) + promise.future().cancel(); +} + +void AssetDownloader::start() +{ + if (d->m_taskTreeRunner.isRunning()) + return; + + struct StorageData + { + QDir tempDir; + QByteArray jsonContent; + DownloadableAssets assets; + QList<QUrl> assetsToDownload; + QByteArray zipContent; + int doneCount = 0; + }; + + const Storage<StorageData> storage; + + const auto onSetup = [this, storage] { + if (!d->m_manager) + d->m_manager = std::make_unique<QNetworkAccessManager>(); + if (!d->m_temporaryDir) + d->m_temporaryDir = std::make_unique<QTemporaryDir>(); + if (!d->m_temporaryDir->isValid()) { + qWarning() << "Cannot create a temporary directory."; + return SetupResult::StopWithError; + } + storage->tempDir = d->m_temporaryDir->path(); + d->setLocalDownloadDir(baseLocalDir(d->m_preferredLocalDownloadDir)); + precheckLocalFile(d->m_offlineAssetsFilePath); + return SetupResult::Continue; + }; + + const auto onJsonDownloadSetup = [this](NetworkQuery &query) { + query.setRequest(QNetworkRequest(d->m_downloadBase.resolved(d->m_jsonFileName))); + d->setupDownload(&query, tr("Downloading JSON file...")); + }; + const auto onJsonDownloadDone = [this, storage](const NetworkQuery &query, DoneWith result) { + if (result == DoneWith::Success) { + storage->jsonContent = query.reply()->readAll(); + return DoneResult::Success; + } + qWarning() << "Cannot download" << d->m_downloadBase.resolved(d->m_jsonFileName) + << query.reply()->errorString(); + if (d->m_offlineAssetsFilePath.isEmpty()) { + qWarning() << "Also there is no local file as a replacement"; + return DoneResult::Error; + } + + QFile file(pathFromUrl(d->m_offlineAssetsFilePath)); + if (!file.open(QIODevice::ReadOnly)) { + qWarning() << "Also failed to open" << d->m_offlineAssetsFilePath; + return DoneResult::Error; + } + + storage->jsonContent = file.readAll(); + return DoneResult::Success; + }; + + const auto onReadAssetsFileSetup = [storage](ConcurrentCall<DownloadableAssets> &async) { + async.setConcurrentCallData(readAssetsFileContent, storage->jsonContent); + }; + const auto onReadAssetsFileDone = [storage](const ConcurrentCall<DownloadableAssets> &async) { + storage->assets = async.result(); + storage->assetsToDownload = storage->assets.files; + }; + + const auto onSkipIfAllAssetsPresent = [this, storage] { + return allAssetsPresent(storage->assets.files, d->m_localDownloadDir) + ? SetupResult::StopWithSuccess : SetupResult::Continue; + }; + + const auto onZipDownloadSetup = [this, storage](NetworkQuery &query) { + if (d->m_zipFileName.isEmpty()) + return SetupResult::StopWithSuccess; + + query.setRequest(QNetworkRequest(d->m_downloadBase.resolved(d->m_zipFileName))); + d->setupDownload(&query, tr("Downloading zip file...")); + return SetupResult::Continue; + }; + const auto onZipDownloadDone = [storage](const NetworkQuery &query, DoneWith result) { + if (result == DoneWith::Success) + storage->zipContent = query.reply()->readAll(); + return DoneResult::Success; // Ignore zip download failure + }; + + const auto onUnzipSetup = [this, storage](ConcurrentCall<void> &async) { + if (storage->zipContent.isEmpty()) + return SetupResult::StopWithSuccess; + + async.setConcurrentCallData(unzip, storage->zipContent, storage->tempDir, d->m_zipFileName); + d->clearProgress(tr("Unzipping...")); + return SetupResult::Continue; + }; + const auto onUnzipDone = [storage](DoneWith result) { + if (result == DoneWith::Success) { + // Avoid downloading assets that are present in unzipped tree + StorageData &storageData = *storage; + storageData.assetsToDownload = + filterDownloadableAssets(storageData.assets.files, storageData.tempDir); + } else { + qWarning() << "ZipFile failed"; + } + return DoneResult::Success; // Ignore unzip failure + }; + + const LoopUntil downloadIterator([storage](int iteration) { + return iteration < storage->assetsToDownload.count(); + }); + + const Storage<QByteArray> assetStorage; + + const auto onAssetsDownloadGroupSetup = [this, storage] { + d->setProgress(0, storage->assetsToDownload.size(), tr("Downloading assets...")); + }; + + const auto onAssetDownloadSetup = [this, storage, downloadIterator](NetworkQuery &query) { + query.setNetworkAccessManager(d->m_manager.get()); + query.setRequest(QNetworkRequest(storage->assets.remoteUrl.resolved( + storage->assetsToDownload.at(downloadIterator.iteration())))); + }; + const auto onAssetDownloadDone = [assetStorage](const NetworkQuery &query, DoneWith result) { + if (result == DoneWith::Success) + *assetStorage = query.reply()->readAll(); + }; + + const auto onAssetWriteSetup = [storage, downloadIterator, assetStorage]( + ConcurrentCall<void> &async) { + const QString filePath = storage->tempDir.absoluteFilePath( + storage->assetsToDownload.at(downloadIterator.iteration()).toString()); + async.setConcurrentCallData(writeAsset, *assetStorage, filePath); + }; + const auto onAssetWriteDone = [this, storage](DoneWith result) { + if (result != DoneWith::Success) { + qWarning() << "Asset write failed"; + return; + } + StorageData &storageData = *storage; + ++storageData.doneCount; + d->updateProgress(storageData.doneCount, storageData.assetsToDownload.size()); + }; + + const LoopUntil copyIterator([storage](int iteration) { + return iteration < storage->assets.files.count(); + }); + + const auto onAssetsCopyGroupSetup = [this, storage] { + storage->doneCount = 0; + d->setProgress(0, storage->assets.files.size(), tr("Copying assets...")); + }; + + const auto onAssetCopySetup = [this, storage, copyIterator](ConcurrentCall<void> &async) { + const QString fileName = storage->assets.files.at(copyIterator.iteration()).toString(); + const QString sourcePath = storage->tempDir.absoluteFilePath(fileName); + const QString destPath = d->m_localDownloadDir.absoluteFilePath(fileName); + async.setConcurrentCallData(copyAndCheck, sourcePath, destPath); + }; + const auto onAssetCopyDone = [this, storage] { + StorageData &storageData = *storage; + ++storageData.doneCount; + d->updateProgress(storageData.doneCount, storageData.assets.files.size()); + }; + + const auto onAssetsCopyGroupDone = [this, storage](DoneWith result) { + if (result != DoneWith::Success) { + d->setLocalDownloadDir(storage->tempDir); + qWarning() << "Asset copy failed"; + return; + } + d->m_temporaryDir.reset(); + }; + + const Group recipe { + storage, + onGroupSetup(onSetup), + NetworkQueryTask(onJsonDownloadSetup, onJsonDownloadDone), + ConcurrentCallTask<DownloadableAssets>(onReadAssetsFileSetup, onReadAssetsFileDone, CallDoneIf::Success), + Group { + onGroupSetup(onSkipIfAllAssetsPresent), + NetworkQueryTask(onZipDownloadSetup, onZipDownloadDone), + ConcurrentCallTask<void>(onUnzipSetup, onUnzipDone), + Group { + parallelIdealThreadCountLimit, + downloadIterator, + onGroupSetup(onAssetsDownloadGroupSetup), + Group { + assetStorage, + NetworkQueryTask(onAssetDownloadSetup, onAssetDownloadDone), + ConcurrentCallTask<void>(onAssetWriteSetup, onAssetWriteDone) + } + }, + Group { + parallelIdealThreadCountLimit, + copyIterator, + onGroupSetup(onAssetsCopyGroupSetup), + ConcurrentCallTask<void>(onAssetCopySetup, onAssetCopyDone, CallDoneIf::Success), + onGroupDone(onAssetsCopyGroupDone) + } + } + }; + d->m_taskTreeRunner.start(recipe, [this](TaskTree *) { emit started(); }, + [this](DoneWith result) { emit finished(result == DoneWith::Success); }); +} + +} // namespace Assets::Downloader + +QT_END_NAMESPACE diff --git a/src/assets/downloader/assetdownloader.h b/src/assets/downloader/assetdownloader.h new file mode 100644 index 0000000000..e000122b41 --- /dev/null +++ b/src/assets/downloader/assetdownloader.h @@ -0,0 +1,112 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef ASSETDOWNLOADER_H +#define ASSETDOWNLOADER_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/QObject> +#include <QtCore/QUrl> + +#include <memory> + +QT_BEGIN_NAMESPACE + +namespace Assets::Downloader { + +class AssetDownloaderPrivate; + +class AssetDownloader : public QObject +{ + Q_OBJECT + + Q_PROPERTY( + QUrl downloadBase + READ downloadBase + WRITE setDownloadBase + NOTIFY downloadBaseChanged) + + Q_PROPERTY( + QUrl preferredLocalDownloadDir + READ preferredLocalDownloadDir + WRITE setPreferredLocalDownloadDir + NOTIFY preferredLocalDownloadDirChanged) + + Q_PROPERTY( + QUrl offlineAssetsFilePath + READ offlineAssetsFilePath + WRITE setOfflineAssetsFilePath + NOTIFY offlineAssetsFilePathChanged) + + Q_PROPERTY( + QString jsonFileName + READ jsonFileName + WRITE setJsonFileName + NOTIFY jsonFileNameChanged) + + Q_PROPERTY( + QString zipFileName + READ zipFileName + WRITE setZipFileName + NOTIFY zipFileNameChanged) + + Q_PROPERTY( + QUrl localDownloadDir + READ localDownloadDir + NOTIFY localDownloadDirChanged) + +public: + AssetDownloader(QObject *parent = nullptr); + ~AssetDownloader(); + + QUrl downloadBase() const; + void setDownloadBase(const QUrl &downloadBase); + + QUrl preferredLocalDownloadDir() const; + void setPreferredLocalDownloadDir(const QUrl &localDir); + + QUrl offlineAssetsFilePath() const; + void setOfflineAssetsFilePath(const QUrl &offlineAssetsFilePath); + + QString jsonFileName() const; + void setJsonFileName(const QString &jsonFileName); + + QString zipFileName() const; + void setZipFileName(const QString &zipFileName); + + QUrl localDownloadDir() const; + +public Q_SLOTS: + void start(); + +Q_SIGNALS: + void started(); + void finished(bool success); + void progressChanged(int progressValue, int progressMaximum, const QString &progressText); + void localDownloadDirChanged(const QUrl &url); + + void downloadBaseChanged(const QUrl &); + void preferredLocalDownloadDirChanged(const QUrl &url); + void offlineAssetsFilePathChanged(const QUrl &); + void jsonFileNameChanged(const QString &); + void zipFileNameChanged(const QString &); + +private: + std::unique_ptr<AssetDownloaderPrivate> d; +}; + +} // namespace Assets::Downloader + +QT_END_NAMESPACE + +#endif // ASSETDOWNLOADER_H diff --git a/src/assets/downloader/tasking/barrier.cpp b/src/assets/downloader/tasking/barrier.cpp new file mode 100644 index 0000000000..c9e5992bc7 --- /dev/null +++ b/src/assets/downloader/tasking/barrier.cpp @@ -0,0 +1,54 @@ +// Copyright (C) 2024 Jarek Kobus +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "barrier.h" + +QT_BEGIN_NAMESPACE + +namespace Tasking { + +// That's cut down qtcassert.{c,h} to avoid the dependency. +#define QT_STRING(cond) qDebug("SOFT ASSERT: \"%s\" in %s: %s", cond, __FILE__, QT_STRINGIFY(__LINE__)) +#define QT_ASSERT(cond, action) if (Q_LIKELY(cond)) {} else { QT_STRING(#cond); action; } do {} while (0) + +void Barrier::setLimit(int value) +{ + QT_ASSERT(!isRunning(), return); + QT_ASSERT(value > 0, return); + + m_limit = value; +} + +void Barrier::start() +{ + QT_ASSERT(!isRunning(), return); + m_current = 0; + m_result.reset(); +} + +void Barrier::advance() +{ + // Calling advance on finished is OK + QT_ASSERT(isRunning() || m_result, return); + if (!isRunning()) // no-op + return; + ++m_current; + if (m_current == m_limit) + stopWithResult(DoneResult::Success); +} + +void Barrier::stopWithResult(DoneResult result) +{ + // Calling stopWithResult on finished is OK when the same success is passed + QT_ASSERT(isRunning() || (m_result && *m_result == result), return); + if (!isRunning()) // no-op + return; + m_current = -1; + m_result = result; + emit done(result); +} + +} // namespace Tasking + +QT_END_NAMESPACE diff --git a/src/assets/downloader/tasking/barrier.h b/src/assets/downloader/tasking/barrier.h new file mode 100644 index 0000000000..f99fdae60d --- /dev/null +++ b/src/assets/downloader/tasking/barrier.h @@ -0,0 +1,112 @@ +// Copyright (C) 2024 Jarek Kobus +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef TASKING_BARRIER_H +#define TASKING_BARRIER_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "tasking_global.h" + +#include "tasktree.h" + +QT_BEGIN_NAMESPACE + +namespace Tasking { + +class TASKING_EXPORT Barrier final : public QObject +{ + Q_OBJECT + +public: + void setLimit(int value); + int limit() const { return m_limit; } + + void start(); + void advance(); // If limit reached, stops with true + void stopWithResult(DoneResult result); // Ignores limit + + bool isRunning() const { return m_current >= 0; } + int current() const { return m_current; } + std::optional<DoneResult> result() const { return m_result; } + +Q_SIGNALS: + void done(DoneResult success); + +private: + std::optional<DoneResult> m_result = {}; + int m_limit = 1; + int m_current = -1; +}; + +class TASKING_EXPORT BarrierTaskAdapter : public TaskAdapter<Barrier> +{ +public: + BarrierTaskAdapter() { connect(task(), &Barrier::done, this, &TaskInterface::done); } + void start() final { task()->start(); } +}; + +using BarrierTask = CustomTask<BarrierTaskAdapter>; + +template <int Limit = 1> +class SharedBarrier +{ +public: + static_assert(Limit > 0, "SharedBarrier's limit should be 1 or more."); + SharedBarrier() : m_barrier(new Barrier) { + m_barrier->setLimit(Limit); + m_barrier->start(); + } + Barrier *barrier() const { return m_barrier.get(); } + +private: + std::shared_ptr<Barrier> m_barrier; +}; + +template <int Limit = 1> +using MultiBarrier = Storage<SharedBarrier<Limit>>; + +// Can't write: "MultiBarrier barrier;". Only "MultiBarrier<> barrier;" would work. +// Can't have one alias with default type in C++17, getting the following error: +// alias template deduction only available with C++20. +using SingleBarrier = MultiBarrier<1>; + +template <int Limit> +GroupItem waitForBarrierTask(const MultiBarrier<Limit> &sharedBarrier) +{ + return BarrierTask([sharedBarrier](Barrier &barrier) { + SharedBarrier<Limit> *activeBarrier = sharedBarrier.activeStorage(); + if (!activeBarrier) { + qWarning("The barrier referenced from WaitForBarrier element " + "is not reachable in the running tree. " + "It is possible that no barrier was added to the tree, " + "or the storage is not reachable from where it is referenced. " + "The WaitForBarrier task finishes with an error. "); + return SetupResult::StopWithError; + } + Barrier *activeSharedBarrier = activeBarrier->barrier(); + const std::optional<DoneResult> result = activeSharedBarrier->result(); + if (result.has_value()) { + return result.value() == DoneResult::Success ? SetupResult::StopWithSuccess + : SetupResult::StopWithError; + } + QObject::connect(activeSharedBarrier, &Barrier::done, &barrier, &Barrier::stopWithResult); + return SetupResult::Continue; + }); +} + +} // namespace Tasking + +QT_END_NAMESPACE + +#endif // TASKING_BARRIER_H diff --git a/src/assets/downloader/tasking/concurrentcall.h b/src/assets/downloader/tasking/concurrentcall.h new file mode 100644 index 0000000000..a43d77be15 --- /dev/null +++ b/src/assets/downloader/tasking/concurrentcall.h @@ -0,0 +1,119 @@ +// Copyright (C) 2024 Jarek Kobus +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef TASKING_CONCURRENTCALL_H +#define TASKING_CONCURRENTCALL_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "tasktree.h" + +#include <QtConcurrent/QtConcurrent> + +QT_BEGIN_NAMESPACE + +namespace Tasking { + +// This class introduces the dependency to Qt::Concurrent, otherwise Tasking namespace +// is independent on Qt::Concurrent. +// Possibly, it could be placed inside Qt::Concurrent library, as a wrapper around +// QtConcurrent::run() call. + +template <typename ResultType> +class ConcurrentCall +{ + Q_DISABLE_COPY_MOVE(ConcurrentCall) + +public: + ConcurrentCall() = default; + template <typename Function, typename ...Args> + void setConcurrentCallData(Function &&function, Args &&...args) + { + return wrapConcurrent(std::forward<Function>(function), std::forward<Args>(args)...); + } + void setThreadPool(QThreadPool *pool) { m_threadPool = pool; } + ResultType result() const + { + return m_future.resultCount() ? m_future.result() : ResultType(); + } + QList<ResultType> results() const + { + return m_future.results(); + } + QFuture<ResultType> future() const { return m_future; } + +private: + template <typename Function, typename ...Args> + void wrapConcurrent(Function &&function, Args &&...args) + { + m_startHandler = [this, function = std::forward<Function>(function), args...] { + QThreadPool *threadPool = m_threadPool ? m_threadPool : QThreadPool::globalInstance(); + return QtConcurrent::run(threadPool, function, args...); + }; + } + + template <typename Function, typename ...Args> + void wrapConcurrent(std::reference_wrapper<const Function> &&wrapper, Args &&...args) + { + m_startHandler = [this, wrapper = std::forward<std::reference_wrapper<const Function>>(wrapper), args...] { + QThreadPool *threadPool = m_threadPool ? m_threadPool : QThreadPool::globalInstance(); + return QtConcurrent::run(threadPool, std::forward<const Function>(wrapper.get()), + args...); + }; + } + + template <typename T> + friend class ConcurrentCallTaskAdapter; + + std::function<QFuture<ResultType>()> m_startHandler; + QThreadPool *m_threadPool = nullptr; + QFuture<ResultType> m_future; +}; + +template <typename ResultType> +class ConcurrentCallTaskAdapter : public TaskAdapter<ConcurrentCall<ResultType>> +{ +public: + ~ConcurrentCallTaskAdapter() { + if (m_watcher) { + m_watcher->cancel(); + m_watcher->waitForFinished(); + } + } + + void start() final { + if (!this->task()->m_startHandler) { + emit this->done(DoneResult::Error); // TODO: Add runtime assert + return; + } + m_watcher.reset(new QFutureWatcher<ResultType>); + this->connect(m_watcher.get(), &QFutureWatcherBase::finished, this, [this] { + emit this->done(toDoneResult(!m_watcher->isCanceled())); + m_watcher.release()->deleteLater(); + }); + this->task()->m_future = this->task()->m_startHandler(); + m_watcher->setFuture(this->task()->m_future); + } + +private: + std::unique_ptr<QFutureWatcher<ResultType>> m_watcher; +}; + +template <typename T> +using ConcurrentCallTask = CustomTask<ConcurrentCallTaskAdapter<T>>; + +} // namespace Tasking + +QT_END_NAMESPACE + +#endif // TASKING_CONCURRENTCALL_H diff --git a/src/assets/downloader/tasking/networkquery.cpp b/src/assets/downloader/tasking/networkquery.cpp new file mode 100644 index 0000000000..1003e0c30a --- /dev/null +++ b/src/assets/downloader/tasking/networkquery.cpp @@ -0,0 +1,58 @@ +// Copyright (C) 2024 Jarek Kobus +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "networkquery.h" + +#include <QtNetwork/QNetworkAccessManager> + +QT_BEGIN_NAMESPACE + +namespace Tasking { + +void NetworkQuery::start() +{ + if (m_reply) { + qWarning("The NetworkQuery is already running. Ignoring the call to start()."); + return; + } + if (!m_manager) { + qWarning("Can't start the NetworkQuery without the QNetworkAccessManager. " + "Stopping with an error."); + emit done(DoneResult::Error); + return; + } + switch (m_operation) { + case NetworkOperation::Get: + m_reply.reset(m_manager->get(m_request)); + break; + case NetworkOperation::Put: + m_reply.reset(m_manager->put(m_request, m_writeData)); + break; + case NetworkOperation::Post: + m_reply.reset(m_manager->post(m_request, m_writeData)); + break; + case NetworkOperation::Delete: + m_reply.reset(m_manager->deleteResource(m_request)); + break; + } + connect(m_reply.get(), &QNetworkReply::finished, this, [this] { + disconnect(m_reply.get(), &QNetworkReply::finished, this, nullptr); + emit done(toDoneResult(m_reply->error() == QNetworkReply::NoError)); + m_reply.release()->deleteLater(); + }); + if (m_reply->isRunning()) + emit started(); +} + +NetworkQuery::~NetworkQuery() +{ + if (m_reply) { + disconnect(m_reply.get(), &QNetworkReply::finished, this, nullptr); + m_reply->abort(); + } +} + +} // namespace Tasking + +QT_END_NAMESPACE diff --git a/src/assets/downloader/tasking/networkquery.h b/src/assets/downloader/tasking/networkquery.h new file mode 100644 index 0000000000..5deb1a4e7c --- /dev/null +++ b/src/assets/downloader/tasking/networkquery.h @@ -0,0 +1,77 @@ +// Copyright (C) 2024 Jarek Kobus +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef TASKING_NETWORKQUERY_H +#define TASKING_NETWORKQUERY_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "tasking_global.h" + +#include "tasktree.h" + +#include <QtNetwork/QNetworkReply> +#include <QtNetwork/QNetworkRequest> + +#include <memory> + +QT_BEGIN_NAMESPACE +class QNetworkAccessManager; + +namespace Tasking { + +// This class introduces the dependency to Qt::Network, otherwise Tasking namespace +// is independent on Qt::Network. +// Possibly, it could be placed inside Qt::Network library, as a wrapper around QNetworkReply. + +enum class NetworkOperation { Get, Put, Post, Delete }; + +class TASKING_EXPORT NetworkQuery final : public QObject +{ + Q_OBJECT + +public: + ~NetworkQuery(); + void setRequest(const QNetworkRequest &request) { m_request = request; } + void setOperation(NetworkOperation operation) { m_operation = operation; } + void setWriteData(const QByteArray &data) { m_writeData = data; } + void setNetworkAccessManager(QNetworkAccessManager *manager) { m_manager = manager; } + QNetworkReply *reply() const { return m_reply.get(); } + void start(); + +Q_SIGNALS: + void started(); + void done(DoneResult result); + +private: + QNetworkRequest m_request; + NetworkOperation m_operation = NetworkOperation::Get; + QByteArray m_writeData; // Used by Put and Post + QNetworkAccessManager *m_manager = nullptr; + std::unique_ptr<QNetworkReply> m_reply; +}; + +class TASKING_EXPORT NetworkQueryTaskAdapter : public TaskAdapter<NetworkQuery> +{ +public: + NetworkQueryTaskAdapter() { connect(task(), &NetworkQuery::done, this, &TaskInterface::done); } + void start() final { task()->start(); } +}; + +using NetworkQueryTask = CustomTask<NetworkQueryTaskAdapter>; + +} // namespace Tasking + +QT_END_NAMESPACE + +#endif // TASKING_NETWORKQUERY_H diff --git a/src/assets/downloader/tasking/qprocesstask.cpp b/src/assets/downloader/tasking/qprocesstask.cpp new file mode 100644 index 0000000000..ef10876c00 --- /dev/null +++ b/src/assets/downloader/tasking/qprocesstask.cpp @@ -0,0 +1,279 @@ +// Copyright (C) 2024 Jarek Kobus +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qprocesstask.h" + +#include <QtCore/QCoreApplication> +#include <QtCore/QDebug> +#include <QtCore/QMutex> +#include <QtCore/QThread> +#include <QtCore/QTimer> +#include <QtCore/QWaitCondition> + +QT_BEGIN_NAMESPACE + +#if QT_CONFIG(process) + +namespace Tasking { + +class ProcessReaperPrivate; + +class ProcessReaper final +{ +public: + static void reap(QProcess *process, int timeoutMs = 500); + ProcessReaper(); + ~ProcessReaper(); + +private: + static ProcessReaper *instance(); + + QThread m_thread; + ProcessReaperPrivate *m_private; +}; + +static const int s_timeoutThreshold = 10000; // 10 seconds + +static QString execWithArguments(QProcess *process) +{ + QStringList commandLine; + commandLine.append(process->program()); + commandLine.append(process->arguments()); + return commandLine.join(QChar::Space); +} + +struct ReaperSetup +{ + QProcess *m_process = nullptr; + int m_timeoutMs; +}; + +class Reaper : public QObject +{ + Q_OBJECT + +public: + Reaper(const ReaperSetup &reaperSetup) : m_reaperSetup(reaperSetup) {} + + void reap() + { + m_timer.start(); + connect(m_reaperSetup.m_process, &QProcess::finished, this, &Reaper::handleFinished); + if (emitFinished()) + return; + terminate(); + } + +Q_SIGNALS: + void finished(); + +private: + void terminate() + { + m_reaperSetup.m_process->terminate(); + QTimer::singleShot(m_reaperSetup.m_timeoutMs, this, &Reaper::handleTerminateTimeout); + } + + void kill() { m_reaperSetup.m_process->kill(); } + + bool emitFinished() + { + if (m_reaperSetup.m_process->state() != QProcess::NotRunning) + return false; + + if (!m_finished) { + const int timeout = m_timer.elapsed(); + if (timeout > s_timeoutThreshold) { + qWarning() << "Finished parallel reaping of" << execWithArguments(m_reaperSetup.m_process) + << "in" << (timeout / 1000.0) << "seconds."; + } + + m_finished = true; + emit finished(); + } + return true; + } + + void handleFinished() + { + if (emitFinished()) + return; + qWarning() << "Finished process still running..."; + // In case the process is still running - wait until it has finished + QTimer::singleShot(m_reaperSetup.m_timeoutMs, this, &Reaper::handleFinished); + } + + void handleTerminateTimeout() + { + if (emitFinished()) + return; + kill(); + } + + bool m_finished = false; + QElapsedTimer m_timer; + const ReaperSetup m_reaperSetup; +}; + +class ProcessReaperPrivate : public QObject +{ + Q_OBJECT + +public: + // Called from non-reaper's thread + void scheduleReap(const ReaperSetup &reaperSetup) + { + if (QThread::currentThread() == thread()) + qWarning() << "Can't schedule reap from the reaper internal thread."; + + QMutexLocker locker(&m_mutex); + m_reaperSetupList.append(reaperSetup); + QMetaObject::invokeMethod(this, &ProcessReaperPrivate::flush); + } + + // Called from non-reaper's thread + void waitForFinished() + { + if (QThread::currentThread() == thread()) + qWarning() << "Can't wait for finished from the reaper internal thread."; + + QMetaObject::invokeMethod(this, &ProcessReaperPrivate::flush, + Qt::BlockingQueuedConnection); + QMutexLocker locker(&m_mutex); + if (m_reaperList.isEmpty()) + return; + + m_waitCondition.wait(&m_mutex); + } + +private: + // All the private methods are called from the reaper's thread + QList<ReaperSetup> takeReaperSetupList() + { + QMutexLocker locker(&m_mutex); + return std::exchange(m_reaperSetupList, {}); + } + + void flush() + { + while (true) { + const QList<ReaperSetup> reaperSetupList = takeReaperSetupList(); + if (reaperSetupList.isEmpty()) + return; + for (const ReaperSetup &reaperSetup : reaperSetupList) + reap(reaperSetup); + } + } + + void reap(const ReaperSetup &reaperSetup) + { + Reaper *reaper = new Reaper(reaperSetup); + connect(reaper, &Reaper::finished, this, [this, reaper, process = reaperSetup.m_process] { + QMutexLocker locker(&m_mutex); + const bool isRemoved = m_reaperList.removeOne(reaper); + if (!isRemoved) + qWarning() << "Reaper list doesn't contain the finished process."; + + delete reaper; + delete process; + if (m_reaperList.isEmpty()) + m_waitCondition.wakeOne(); + }, Qt::QueuedConnection); + + { + QMutexLocker locker(&m_mutex); + m_reaperList.append(reaper); + } + + reaper->reap(); + } + + QMutex m_mutex; + QWaitCondition m_waitCondition; + QList<ReaperSetup> m_reaperSetupList; + QList<Reaper *> m_reaperList; +}; + +static ProcessReaper *s_instance = nullptr; +static QMutex s_instanceMutex; + +// Call me with s_instanceMutex locked. +ProcessReaper *ProcessReaper::instance() +{ + if (!s_instance) + s_instance = new ProcessReaper; + return s_instance; +} + +ProcessReaper::ProcessReaper() + : m_private(new ProcessReaperPrivate) +{ + m_private->moveToThread(&m_thread); + QObject::connect(&m_thread, &QThread::finished, m_private, &QObject::deleteLater); + m_thread.start(); + m_thread.moveToThread(qApp->thread()); +} + +ProcessReaper::~ProcessReaper() +{ + if (QThread::currentThread() != qApp->thread()) + qWarning() << "Destructing process reaper from non-main thread."; + + instance()->m_private->waitForFinished(); + m_thread.quit(); + m_thread.wait(); +} + +void ProcessReaper::reap(QProcess *process, int timeoutMs) +{ + if (!process) + return; + + if (QThread::currentThread() != process->thread()) { + qWarning() << "Can't reap process from non-process's thread."; + return; + } + + process->disconnect(); + if (process->state() == QProcess::NotRunning) { + delete process; + return; + } + + // Neither can move object with a parent into a different thread + // nor reaping the process with a parent makes any sense. + process->setParent(nullptr); + + QMutexLocker locker(&s_instanceMutex); + ProcessReaperPrivate *priv = instance()->m_private; + + process->moveToThread(priv->thread()); + ReaperSetup reaperSetup {process, timeoutMs}; + priv->scheduleReap(reaperSetup); +} + +void QProcessDeleter::deleteAll() +{ + QMutexLocker locker(&s_instanceMutex); + delete s_instance; + s_instance = nullptr; +} + +void QProcessDeleter::operator()(QProcess *process) +{ + ProcessReaper::reap(process); +} + +} // namespace Tasking + +#endif // QT_CONFIG(process) + +QT_END_NAMESPACE + +#if QT_CONFIG(process) + +#include "qprocesstask.moc" + +#endif // QT_CONFIG(process) + diff --git a/src/assets/downloader/tasking/qprocesstask.h b/src/assets/downloader/tasking/qprocesstask.h new file mode 100644 index 0000000000..6f2ca4a18e --- /dev/null +++ b/src/assets/downloader/tasking/qprocesstask.h @@ -0,0 +1,89 @@ +// Copyright (C) 2024 Jarek Kobus +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef TASKING_QPROCESSTASK_H +#define TASKING_QPROCESSTASK_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "tasking_global.h" + +#include "tasktree.h" + +#include <QtCore/QProcess> + +QT_BEGIN_NAMESPACE + +#if QT_CONFIG(process) + +namespace Tasking { + +// Deleting a running QProcess may block the caller thread up to 30 seconds and issue warnings. +// To avoid these issues we move the running QProcess into a separate thread +// managed by the internal ProcessReaper, instead of deleting it immediately. +// Inside the ProcessReaper's thread we try to finish the process in a most gentle way: +// we call QProcess::terminate() with 500 ms timeout, and if the process is still running +// after this timeout passed, we call QProcess::kill() and wait for the process to finish. +// All these handlings are done is a separate thread, so the main thread doesn't block at all +// when the QProcessTask is destructed. +// Finally, on application quit, QProcessDeleter::deleteAll() should be called in order +// to synchronize all the processes being still potentially reaped in a separate thread. +// The call to QProcessDeleter::deleteAll() is blocking in case some processes +// are still being reaped. +// This strategy seems most sensible, since when passing the running QProcess into the +// ProcessReaper we don't block immediately, but postpone the possible (not certain) block +// until the end of an application. +// In this way we terminate the running processes in the most safe way and keep the main thread +// responsive. That's a common case when the running application wants to terminate the QProcess +// immediately (e.g. on Cancel button pressed), without keeping and managing the handle +// to the still running QProcess. + +// The implementation of the internal reaper is inspired by the Utils::ProcessReaper taken +// from the QtCreator codebase. + +class TASKING_EXPORT QProcessDeleter +{ +public: + // Blocking, should be called after all QProcessAdapter instances are deleted. + static void deleteAll(); + void operator()(QProcess *process); +}; + +class TASKING_EXPORT QProcessAdapter : public TaskAdapter<QProcess, QProcessDeleter> +{ +private: + void start() final { + connect(task(), &QProcess::finished, this, [this] { + const bool success = task()->exitStatus() == QProcess::NormalExit + && task()->error() == QProcess::UnknownError + && task()->exitCode() == 0; + Q_EMIT done(toDoneResult(success)); + }); + connect(task(), &QProcess::errorOccurred, this, [this](QProcess::ProcessError error) { + if (error != QProcess::FailedToStart) + return; + Q_EMIT done(DoneResult::Error); + }); + task()->start(); + } +}; + +using QProcessTask = CustomTask<QProcessAdapter>; + +} // namespace Tasking + +#endif // QT_CONFIG(process) + +QT_END_NAMESPACE + +#endif // TASKING_QPROCESSTASK_H diff --git a/src/assets/downloader/tasking/tasking_global.h b/src/assets/downloader/tasking/tasking_global.h new file mode 100644 index 0000000000..57f0b7fefe --- /dev/null +++ b/src/assets/downloader/tasking/tasking_global.h @@ -0,0 +1,25 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef TASKING_GLOBAL_H +#define TASKING_GLOBAL_H + +#include <QtCore/qglobal.h> + +QT_BEGIN_NAMESPACE + +// #if defined(QT_SHARED) || !defined(QT_STATIC) +// # if defined(TASKING_LIBRARY) +// # define TASKING_EXPORT Q_DECL_EXPORT +// # else +// # define TASKING_EXPORT Q_DECL_IMPORT +// # endif +// #else +// # define TASKING_EXPORT +// #endif + +#define TASKING_EXPORT + +QT_END_NAMESPACE + +#endif // TASKING_GLOBAL_H diff --git a/src/assets/downloader/tasking/tasktree.cpp b/src/assets/downloader/tasking/tasktree.cpp new file mode 100644 index 0000000000..f1b4325e15 --- /dev/null +++ b/src/assets/downloader/tasking/tasktree.cpp @@ -0,0 +1,3431 @@ +// Copyright (C) 2024 Jarek Kobus +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "tasktree.h" + +#include "barrier.h" + +#include <QtCore/QDebug> +#include <QtCore/QEventLoop> +#include <QtCore/QFutureWatcher> +#include <QtCore/QHash> +#include <QtCore/QMetaEnum> +#include <QtCore/QMutex> +#include <QtCore/QPointer> +#include <QtCore/QPromise> +#include <QtCore/QSet> +#include <QtCore/QTime> +#include <QtCore/QTimer> + +using namespace std::chrono; + +QT_BEGIN_NAMESPACE + +namespace Tasking { + +// That's cut down qtcassert.{c,h} to avoid the dependency. +#define QT_STRING(cond) qDebug("SOFT ASSERT: \"%s\" in %s: %s", cond, __FILE__, QT_STRINGIFY(__LINE__)) +#define QT_ASSERT(cond, action) if (Q_LIKELY(cond)) {} else { QT_STRING(#cond); action; } do {} while (0) +#define QT_CHECK(cond) if (cond) {} else { QT_STRING(#cond); } do {} while (0) + +class Guard +{ + Q_DISABLE_COPY(Guard) +public: + Guard() = default; + ~Guard() { QT_CHECK(m_lockCount == 0); } + bool isLocked() const { return m_lockCount; } +private: + int m_lockCount = 0; + friend class GuardLocker; +}; + +class GuardLocker +{ + Q_DISABLE_COPY(GuardLocker) +public: + GuardLocker(Guard &guard) : m_guard(guard) { ++m_guard.m_lockCount; } + ~GuardLocker() { --m_guard.m_lockCount; } +private: + Guard &m_guard; +}; + +/*! + \module TaskingSolution + \title Tasking Solution + \ingroup solutions-modules + \brief Contains a general purpose Tasking solution. + + The Tasking solution depends on Qt only, and doesn't depend on any \QC specific code. +*/ + +/*! + \namespace Tasking + \inmodule TaskingSolution + \brief The Tasking namespace encloses all classes and global functions of the Tasking solution. +*/ + +/*! + \class Tasking::TaskInterface + \inheaderfile solutions/tasking/tasktree.h + \inmodule TaskingSolution + \brief TaskInterface is the abstract base class for implementing custom task adapters. + \reentrant + + To implement a custom task adapter, derive your adapter from the + \c TaskAdapter<Task> class template. TaskAdapter automatically creates and destroys + the custom task instance and associates the adapter with a given \c Task type. +*/ + +/*! + \fn virtual void TaskInterface::start() + + This method is called by the running TaskTree for starting the \c Task instance. + Reimplement this method in \c TaskAdapter<Task>'s subclass in order to start the + associated task. + + Use TaskAdapter::task() to access the associated \c Task instance. + + \sa done(), TaskAdapter::task() +*/ + +/*! + \fn void TaskInterface::done(DoneResult result) + + Emit this signal from the \c TaskAdapter<Task>'s subclass, when the \c Task is finished. + Pass DoneResult::Success as a \a result argument when the task finishes with success; + otherwise, when an error occurs, pass DoneResult::Error. +*/ + +/*! + \class Tasking::TaskAdapter + \inheaderfile solutions/tasking/tasktree.h + \inmodule TaskingSolution + \brief A class template for implementing custom task adapters. + \reentrant + + The TaskAdapter class template is responsible for creating a task of the \c Task type, + starting it, and reporting success or an error when the task is finished. + It also associates the adapter with a given \c Task type. + + Reimplement this class with the actual \c Task type to adapt the task's interface + into the general TaskTree's interface for managing the \c Task instances. + + Each subclass needs to provide a public default constructor, + implement the start() method, and emit the done() signal when the task is finished. + Use task() to access the associated \c Task instance. + + To use your task adapter inside the task tree, create an alias to the + Tasking::CustomTask template passing your task adapter as a template parameter: + \code + // Defines actual worker + class Worker {...}; + + // Adapts Worker's interface to work with task tree + class WorkerTaskAdapter : public TaskAdapter<Worker> {...}; + + // Defines WorkerTask as a new custom task type to be placed inside Group items + using WorkerTask = CustomTask<WorkerTaskAdapter>; + \endcode + + Optionally, you may pass a custom \c Deleter for the associated \c Task + as a second template parameter of your \c TaskAdapter subclass. + When the \c Deleter parameter is omitted, the \c std::default_delete<Task> is used by default. + The custom \c Deleter is useful when the destructor of the running \c Task + may potentially block the caller thread. Instead of blocking, the custom deleter may move + the running task into a separate thread and implement the blocking destruction there. + In this way, the fast destruction (seen from the caller thread) of the running task + with a blocking destructor may be achieved. + + For more information on implementing the custom task adapters, refer to \l {Task Adapters}. + + \sa start(), done(), task() +*/ + +/*! + \fn template <typename Task, typename Deleter = std::default_delete<Task>> TaskAdapter<Task, Deleter>::TaskAdapter<Task, Deleter>() + + Creates a task adapter for the given \c Task type. + + Internally, it creates an instance of \c Task, which is accessible via the task() method. + The optionally provided \c Deleter is used instead of the \c Task destructor. + When \c Deleter is omitted, the \c std::default_delete<Task> is used by default. + + \sa task() +*/ + +/*! + \fn template <typename Task, typename Deleter = std::default_delete<Task>> Task *TaskAdapter<Task, Deleter>::task() + + Returns the pointer to the associated \c Task instance. +*/ + +/*! + \fn template <typename Task, typename Deleter = std::default_delete<Task>> Task *TaskAdapter<Task, Deleter>::task() const + \overload + + Returns the \c const pointer to the associated \c Task instance. +*/ + +/*! + \class Tasking::Storage + \inheaderfile solutions/tasking/tasktree.h + \inmodule TaskingSolution + \brief A class template for custom data exchange in the running task tree. + \reentrant + + The Storage class template is responsible for dynamically creating and destructing objects + of the custom \c StorageStruct type. The creation and destruction are managed by the + running task tree. If a Storage object is placed inside a \l {Tasking::Group} {Group} element, + the running task tree creates the \c StorageStruct object when the group is started and before + the group's setup handler is called. Later, whenever any handler inside this group is called, + the task tree activates the previously created instance of the \c StorageStruct object. + This includes all tasks' and groups' setup and done handlers inside the group where the + Storage object was placed, also within the nested groups. + When a copy of the Storage object is passed to the handler via the lambda capture, + the handler may access the instance activated by the running task tree via the + \l {Tasking::Storage::operator->()} {operator->()}, + \l {Tasking::Storage::operator*()} {operator*()}, or activeStorage() method. + If two handlers capture the same Storage object, one of them may store a custom data there, + and the other may read it afterwards. + When the group is finished, the previously created instance of the \c StorageStruct + object is destroyed after the group's done handler is called. + + An example of data exchange between tasks: + + \code + const Storage<QString> storage; + + const auto onFirstDone = [storage](const Task &task) { + // Assings QString, taken from the first task result, to the active QString instance + // of the Storage object. + *storage = task.getResultAsString(); + }; + + const auto onSecondSetup = [storage](Task &task) { + // Reads QString from the active QString instance of the Storage object and use it to + // configure the second task before start. + task.configureWithString(*storage); + }; + + const Group root { + // The running task tree creates QString instance when root in entered + storage, + // The done handler of the first task stores the QString in the storage + TaskItem(..., onFirstDone), + // The setup handler of the second task reads the QString from the storage + TaskItem(onSecondSetup, ...) + }; + \endcode + + Since the root group executes its tasks sequentially, the \c onFirstDone handler + is always called before the \c onSecondSetup handler. This means that the QString data, + read from the \c storage inside the \c onSecondSetup handler's body, + has already been set by the \c onFirstDone handler. + You can always rely on it in \l {Tasking::sequential} {sequential} execution mode. + + The Storage internals are shared between all of its copies. That is why the copies of the + Storage object inside the handlers' lambda captures still refer to the same Storage instance. + You may place multiple Storage objects inside one \l {Tasking::Group} {Group} element, + provided that they do not include copies of the same Storage object. + Otherwise, an assert is triggered at runtime that includes an error message. + However, you can place copies of the same Storage object in different + \l {Tasking::Group} {Group} elements of the same recipe. In this case, the running task + tree will create multiple instances of the \c StorageStruct objects (one for each copy) + and storage shadowing will take place. Storage shadowing works in a similar way + to C++ variable shadowing inside the nested blocks of code: + + \code + Storage<QString> storage; + + const Group root { + storage, // Top copy, 1st instance of StorageStruct + onGroupSetup([storage] { ... }), // Top copy is active + Group { + storage, // Nested copy, 2nd instance of StorageStruct, + // shadows Top copy + onGroupSetup([storage] { ... }), // Nested copy is active + }, + Group { + onGroupSetup([storage] { ... }), // Top copy is active + } + }; + \endcode + + The Storage objects may also be used for passing the initial data to the executed task tree, + and for reading the final data out of the task tree before it finishes. + To do this, use \l {TaskTree::onStorageSetup()} {onStorageSetup()} or + \l {TaskTree::onStorageDone()} {onStorageDone()}, respectively. + + \note If you use an unreachable Storage object inside the handler, + because you forgot to place the storage in the recipe, + or placed it, but not in any handler's ancestor group, + you may expect a crash, preceded by the following message: + \e {The referenced storage is not reachable in the running tree. + A nullptr will be returned which might lead to a crash in the calling code. + It is possible that no storage was added to the tree, + or the storage is not reachable from where it is referenced.} +*/ + +/*! + \fn template <typename StorageStruct> Storage<StorageStruct>::Storage<StorageStruct>() + + Creates a storage for the given \c StorageStruct type. + + \note All copies of \c this object are considered to be the same Storage instance. +*/ + +/*! + \fn template <typename StorageStruct> StorageStruct &Storage<StorageStruct>::operator*() const noexcept + + Returns a \e reference to the active \c StorageStruct object, created by the running task tree. + Use this function only from inside the handler body of any GroupItem element placed + in the recipe, otherwise you may expect a crash. + Make sure that Storage is placed in any group ancestor of the handler's group item. + + \note The returned reference is valid as long as the group that created this instance + is still running. + + \sa activeStorage(), operator->() +*/ + +/*! + \fn template <typename StorageStruct> StorageStruct *Storage<StorageStruct>::operator->() const noexcept + + Returns a \e pointer to the active \c StorageStruct object, created by the running task tree. + Use this function only from inside the handler body of any GroupItem element placed + in the recipe, otherwise you may expect a crash. + Make sure that Storage is placed in any group ancestor of the handler's group item. + + \note The returned pointer is valid as long as the group that created this instance + is still running. + + \sa activeStorage(), operator*() +*/ + +/*! + \fn template <typename StorageStruct> StorageStruct *Storage<StorageStruct>::activeStorage() const + + Returns a \e pointer to the active \c StorageStruct object, created by the running task tree. + Use this function only from inside the handler body of any GroupItem element placed + in the recipe, otherwise you may expect a crash. + Make sure that Storage is placed in any group ancestor of the handler's group item. + + \note The returned pointer is valid as long as the group that created this instance + is still running. + + \sa operator->(), operator*() +*/ + +/*! + \class Tasking::GroupItem + \inheaderfile solutions/tasking/tasktree.h + \inmodule TaskingSolution + \brief GroupItem represents the basic element that may be a part of any Group. + \reentrant + + GroupItem is a basic element that may be a part of any \l {Tasking::Group} {Group}. + It encapsulates the functionality provided by any GroupItem's subclass. + It is a value type and it is safe to copy the GroupItem instance, + even when it is originally created via the subclass' constructor. + + There are four main kinds of GroupItem: + \table + \header + \li GroupItem Kind + \li Brief Description + \row + \li \l CustomTask + \li Defines asynchronous task type and task's start, done, and error handlers. + Aliased with a unique task name, such as, \c ConcurrentCallTask<ResultType> + or \c NetworkQueryTask. Asynchronous tasks are the main reason for using a task tree. + \row + \li \l {Tasking::Group} {Group} + \li A container for other group items. Since the group is of the GroupItem type, + it's possible to nest it inside another group. The group is seen by its parent + as a single asynchronous task. + \row + \li GroupItem containing \l {Tasking::Storage} {Storage} + \li Enables the child tasks of a group to exchange data. When GroupItem containing + \l {Tasking::Storage} {Storage} is placed inside a group, the task tree instantiates + the storage's data object just before the group is entered, + and destroys it just after the group is left. + \row + \li Other group control items + \li The items returned by \l {Tasking::parallelLimit()} {parallelLimit()} or + \l {Tasking::workflowPolicy()} {workflowPolicy()} influence the group's behavior. + The items returned by \l {Tasking::onGroupSetup()} {onGroupSetup()} or + \l {Tasking::onGroupDone()} {onGroupDone()} define custom handlers called when + the group starts or ends execution. + \endtable +*/ + +/*! + \fn template <typename StorageStruct> GroupItem::GroupItem(const Storage<StorageStruct> &storage) + + Constructs a \c GroupItem element holding the \a storage object. + + When the \l {Tasking::Group} {Group} element containing \e this GroupItem is entered + by the running task tree, an instance of the \c StorageStruct is created dynamically. + + When that group is about to be left after its execution, the previously instantiated + \c StorageStruct is deleted. + + The dynamically created instance of \c StorageStruct is accessible from inside any + handler body of the parent \l {Tasking::Group} {Group} element, + including nested groups and its tasks, via the + \l {Tasking::Storage::operator->()} {Storage::operator->()}, + \l {Tasking::Storage::operator*()} {Storage::operator*()}, or Storage::activeStorage() method. + + \sa {Tasking::Storage} {Storage} +*/ + +/*! + \fn GroupItem::GroupItem(const QList<GroupItem> &items) + + Constructs a \c GroupItem element with a given list of \a items. + + When this \c GroupItem element is parsed by the TaskTree, it is simply replaced with + its \a items. + + This constructor is useful when constructing a \l {Tasking::Group} {Group} element with + lists of \c GroupItem elements: + + \code + static QList<GroupItems> getItems(); + + ... + + const Group root { + parallel, + finishAllAndSuccess, + getItems(), // OK, getItems() list is wrapped into a single GroupItem element + onGroupSetup(...), + onGroupDone(...) + }; + \endcode + + If you want to create a subtree, use \l {Tasking::Group} {Group} instead. + + \note Don't confuse this \c GroupItem with the \l {Tasking::Group} {Group} element, as + \l {Tasking::Group} {Group} keeps its children nested + after being parsed by the task tree, while this \c GroupItem does not. + + \sa {Tasking::Group} {Group} +*/ + +/*! + \fn GroupItem::GroupItem(std::initializer_list<GroupItem> items) + \overload + \sa GroupItem(const QList<Tasking::GroupItem> &items) +*/ + +/*! + \class Tasking::Group + \inheaderfile solutions/tasking/tasktree.h + \inmodule TaskingSolution + \brief Group represents the basic element for composing declarative recipes describing + how to execute and handle a nested tree of asynchronous tasks. + \reentrant + + Group is a container for other group items. It encloses child tasks into one unit, + which is seen by the group's parent as a single, asynchronous task. + Since Group is of the GroupItem type, it may also be a child of Group. + + Insert child tasks into the group by using aliased custom task names, such as, + \c ConcurrentCallTask<ResultType> or \c NetworkQueryTask: + + \code + const Group group { + NetworkQueryTask(...), + ConcurrentCallTask<int>(...) + }; + \endcode + + The group's behavior may be customized by inserting the items returned by + \l {Tasking::parallelLimit()} {parallelLimit()} or + \l {Tasking::workflowPolicy()} {workflowPolicy()} functions: + + \code + const Group group { + parallel, + continueOnError, + NetworkQueryTask(...), + NetworkQueryTask(...) + }; + \endcode + + The group may contain nested groups: + + \code + const Group group { + finishAllAndSuccess, + NetworkQueryTask(...), + Group { + NetworkQueryTask(...), + Group { + parallel, + NetworkQueryTask(...), + NetworkQueryTask(...), + } + ConcurrentCallTask<QString>(...) + } + }; + \endcode + + The group may dynamically instantiate a custom storage structure, which may be used for + inter-task data exchange: + + \code + struct MyCustomStruct { QByteArray data; }; + + Storage<MyCustomStruct> storage; + + const auto onFirstSetup = [](NetworkQuery &task) { ... }; + const auto onFirstDone = [storage](const NetworkQuery &task) { + // storage-> gives a pointer to MyCustomStruct instance, + // created dynamically by the running task tree. + storage->data = task.reply()->readAll(); + }; + const auto onSecondSetup = [storage](ConcurrentCall<QImage> &task) { + // storage-> gives a pointer to MyCustomStruct. Since the group is sequential, + // the stored MyCustomStruct was already updated inside the onFirstDone handler. + const QByteArray storedData = storage->data; + }; + + const Group group { + // When the group is entered by a running task tree, it creates MyCustomStruct + // instance dynamically. It is later accessible from all handlers via + // the *storage or storage-> operators. + sequential, + storage, + NetworkQueryTask(onFirstSetup, onFirstDone, CallDoneIf::Success), + ConcurrentCallTask<QImage>(onSecondSetup) + }; + \endcode +*/ + +/*! + \fn Group::Group(const QList<GroupItem> &children) + + Constructs a group with a given list of \a children. + + This constructor is useful when the child items of the group are not known at compile time, + but later, at runtime: + + \code + const QStringList sourceList = ...; + + QList<GroupItem> groupItems { parallel }; + + for (const QString &source : sourceList) { + const NetworkQueryTask task(...); // use source for setup handler + groupItems << task; + } + + const Group group(groupItems); + \endcode +*/ + +/*! + \fn Group::Group(std::initializer_list<GroupItem> children) + + Constructs a group from \c std::initializer_list given by \a children. + + This constructor is useful when all child items of the group are known at compile time: + + \code + const Group group { + finishAllAndSuccess, + NetworkQueryTask(...), + Group { + NetworkQueryTask(...), + Group { + parallel, + NetworkQueryTask(...), + NetworkQueryTask(...), + } + ConcurrentCallTask<QString>(...) + } + }; + \endcode +*/ + +/*! + \class Tasking::Sync + \inheaderfile solutions/tasking/tasktree.h + \inmodule TaskingSolution + \brief Synchronously executes a custom handler between other tasks. + \reentrant + + \c Sync is useful when you want to execute an additional handler between other tasks. + \c Sync is seen by its parent \l {Tasking::Group} {Group} as any other task. + Avoid long-running execution of the \c Sync's handler body, since it is executed + synchronously from the caller thread. If that is unavoidable, consider using + \c ConcurrentCallTask instead. +*/ + +/*! + \fn template <typename Handler> Sync::Sync(Handler &&handler) + + Constructs an element that executes a passed \a handler synchronously. + The \c Handler is of the \c std::function<DoneResult()> type. + The DoneResult value, returned by the \a handler, is considered during parent group's + \l {workflowPolicy} {workflow policy} resolution. + Optionally, the shortened form of \c std::function<void()> is also accepted. + In this case, it's assumed that the return value is DoneResult::Success. + + The passed \a handler executes synchronously from the caller thread, so avoid a long-running + execution of the handler body. Otherwise, consider using \c ConcurrentCallTask. + + \note The \c Sync element is not counted as a task when reporting task tree progress, + and is not included in TaskTree::taskCount() or TaskTree::progressMaximum(). +*/ + +/*! + \class Tasking::CustomTask + \inheaderfile solutions/tasking/tasktree.h + \inmodule TaskingSolution + \brief A class template used for declaring custom task items and defining their setup + and done handlers. + \reentrant + + Describes custom task items within task tree recipes. + + Custom task names are aliased with unique names using the \c CustomTask template + with a given TaskAdapter subclass as a template parameter. + For example, \c ConcurrentCallTask<T> is an alias to the \c CustomTask that is defined + to work with \c ConcurrentCall<T> as an associated task class. + The following table contains example custom tasks and their associated task classes: + + \table + \header + \li Aliased Task Name (Tasking Namespace) + \li Associated Task Class + \li Brief Description + \row + \li ConcurrentCallTask<ReturnType> + \li ConcurrentCall<ReturnType> + \li Starts an asynchronous task. Runs in a separate thread. + \row + \li NetworkQueryTask + \li NetworkQuery + \li Sends a network query. + \row + \li TaskTreeTask + \li TaskTree + \li Starts a nested task tree. + \row + \li TimeoutTask + \li \c std::chrono::milliseconds + \li Starts a timer. + \row + \li WaitForBarrierTask + \li MultiBarrier<Limit> + \li Starts an asynchronous task waiting for the barrier to pass. + \endtable +*/ + +/*! + \typealias CustomTask::Task + + Type alias for the task type associated with the custom task's \c Adapter. +*/ + +/*! + \typealias CustomTask::Deleter + + Type alias for the task's type deleter associated with the custom task's \c Adapter. +*/ + +/*! + \typealias CustomTask::TaskSetupHandler + + Type alias for \c std::function<SetupResult(Task &)>. + + The \c TaskSetupHandler is an optional argument of a custom task element's constructor. + Any function with the above signature, when passed as a task setup handler, + will be called by the running task tree after the task is created and before it is started. + + Inside the body of the handler, you may configure the task according to your needs. + The additional parameters, including storages, may be passed to the handler + via the lambda capture. + You can decide dynamically whether the task should be started or skipped with + success or an error. + + \note Do not start the task inside the start handler by yourself. Leave it for TaskTree, + otherwise the behavior is undefined. + + The return value of the handler instructs the running task tree on how to proceed + after the handler's invocation is finished. The return value of SetupResult::Continue + instructs the task tree to continue running, that is, to execute the associated \c Task. + The return value of SetupResult::StopWithSuccess or SetupResult::StopWithError + instructs the task tree to skip the task's execution and finish it immediately with + success or an error, respectively. + + When the return type is either SetupResult::StopWithSuccess or SetupResult::StopWithError, + the task's done handler (if provided) isn't called afterwards. + + The constructor of a custom task accepts also functions in the shortened form of + \c std::function<void(Task &)>, that is, the return value is \c void. + In this case, it's assumed that the return value is SetupResult::Continue. + + \sa CustomTask(), TaskDoneHandler, GroupSetupHandler +*/ + +/*! + \typealias CustomTask::TaskDoneHandler + + Type alias for \c std::function<DoneResult(const Task &, DoneWith)>. + + The \c TaskDoneHandler is an optional argument of a custom task element's constructor. + Any function with the above signature, when passed as a task done handler, + will be called by the running task tree after the task execution finished and before + the final result of the execution is reported back to the parent group. + + Inside the body of the handler you may retrieve the final data from the finished task. + The additional parameters, including storages, may be passed to the handler + via the lambda capture. + It is also possible to decide dynamically whether the task should finish with its return + value, or the final result should be tweaked. + + The DoneWith argument is optional and your done handler may omit it. + When provided, it holds the info about the final result of a task that will be + reported to its parent. + + If you do not plan to read any data from the finished task, + you may omit the \c {const Task &} argument. + + The returned DoneResult value is optional and your handler may return \c void instead. + In this case, the final result of the task will be equal to the value indicated by + the DoneWith argument. When the handler returns the DoneResult value, + the task's final result may be tweaked inside the done handler's body by the returned value. + + \sa CustomTask(), TaskSetupHandler, GroupDoneHandler +*/ + +/*! + \fn template <typename Adapter> template <typename SetupHandler = TaskSetupHandler, typename DoneHandler = TaskDoneHandler> CustomTask<Adapter>::CustomTask(SetupHandler &&setup = TaskSetupHandler(), DoneHandler &&done = TaskDoneHandler(), CallDoneIf callDoneIf = CallDoneIf::SuccessOrError) + + Constructs a \c CustomTask instance and attaches the \a setup and \a done handlers to the task. + When the running task tree is about to start the task, + it instantiates the associated \l Task object, invokes \a setup handler with a \e reference + to the created task, and starts it. When the running task finishes, + the task tree invokes a \a done handler, with a \c const \e reference to the created task. + + The passed \a setup handler is of the \l TaskSetupHandler type. For example: + + \code + static void parseAndLog(const QString &input); + + ... + + const QString input = ...; + + const auto onFirstSetup = [input](ConcurrentCall<void> &task) { + if (input == "Skip") + return SetupResult::StopWithSuccess; // This task won't start, the next one will + if (input == "Error") + return SetupResult::StopWithError; // This task and the next one won't start + task.setConcurrentCallData(parseAndLog, input); + // This task will start, and the next one will start after this one finished with success + return SetupResult::Continue; + }; + + const auto onSecondSetup = [input](ConcurrentCall<void> &task) { + task.setConcurrentCallData(parseAndLog, input); + }; + + const Group group { + ConcurrentCallTask<void>(onFirstSetup), + ConcurrentCallTask<void>(onSecondSetup) + }; + \endcode + + The \a done handler is of the \l TaskDoneHandler type. + By default, the \a done handler is invoked whenever the task finishes. + Pass a non-default value for the \a callDoneIf argument when you want the handler to be called + only on a successful or failed execution. + + \sa TaskSetupHandler, TaskDoneHandler +*/ + +/*! + \enum Tasking::WorkflowPolicy + + This enum describes the possible behavior of the Group element when any group's child task + finishes its execution. It's also used when the running Group is canceled. + + \value StopOnError + Default. Corresponds to the stopOnError global element. + If any child task finishes with an error, the group stops and finishes with an error. + If all child tasks finished with success, the group finishes with success. + If a group is empty, it finishes with success. + \value ContinueOnError + Corresponds to the continueOnError global element. + Similar to stopOnError, but in case any child finishes with an error, + the execution continues until all tasks finish, and the group reports an error + afterwards, even when some other tasks in the group finished with success. + If all child tasks finish successfully, the group finishes with success. + If a group is empty, it finishes with success. + \value StopOnSuccess + Corresponds to the stopOnSuccess global element. + If any child task finishes with success, the group stops and finishes with success. + If all child tasks finished with an error, the group finishes with an error. + If a group is empty, it finishes with an error. + \value ContinueOnSuccess + Corresponds to the continueOnSuccess global element. + Similar to stopOnSuccess, but in case any child finishes successfully, + the execution continues until all tasks finish, and the group reports success + afterwards, even when some other tasks in the group finished with an error. + If all child tasks finish with an error, the group finishes with an error. + If a group is empty, it finishes with an error. + \value StopOnSuccessOrError + Corresponds to the stopOnSuccessOrError global element. + The group starts as many tasks as it can. When any task finishes, + the group stops and reports the task's result. + Useful only in parallel mode. + In sequential mode, only the first task is started, and when finished, + the group finishes too, so the other tasks are always skipped. + If a group is empty, it finishes with an error. + \value FinishAllAndSuccess + Corresponds to the finishAllAndSuccess global element. + The group executes all tasks and ignores their return results. When all + tasks finished, the group finishes with success. + If a group is empty, it finishes with success. + \value FinishAllAndError + Corresponds to the finishAllAndError global element. + The group executes all tasks and ignores their return results. When all + tasks finished, the group finishes with an error. + If a group is empty, it finishes with an error. + + Whenever a child task's result causes the Group to stop, that is, + in case of StopOnError, StopOnSuccess, or StopOnSuccessOrError policies, + the Group cancels the other running child tasks (if any - for example in parallel mode), + and skips executing tasks it has not started yet (for example, in the sequential mode - + those, that are placed after the failed task). Both canceling and skipping child tasks + may happen when parallelLimit() is used. + + The table below summarizes the differences between various workflow policies: + + \table + \header + \li \l WorkflowPolicy + \li Executes all child tasks + \li Result + \li Result when the group is empty + \row + \li StopOnError + \li Stops when any child task finished with an error and reports an error + \li An error when at least one child task failed, success otherwise + \li Success + \row + \li ContinueOnError + \li Yes + \li An error when at least one child task failed, success otherwise + \li Success + \row + \li StopOnSuccess + \li Stops when any child task finished with success and reports success + \li Success when at least one child task succeeded, an error otherwise + \li An error + \row + \li ContinueOnSuccess + \li Yes + \li Success when at least one child task succeeded, an error otherwise + \li An error + \row + \li StopOnSuccessOrError + \li Stops when any child task finished and reports child task's result + \li Success or an error, depending on the finished child task's result + \li An error + \row + \li FinishAllAndSuccess + \li Yes + \li Success + \li Success + \row + \li FinishAllAndError + \li Yes + \li An error + \li An error + \endtable + + If a child of a group is also a group, the child group runs its tasks according to its own + workflow policy. When a parent group stops the running child group because + of parent group's workflow policy, that is, when the StopOnError, StopOnSuccess, + or StopOnSuccessOrError policy was used for the parent, + the child group's result is reported according to the + \b Result column and to the \b {child group's workflow policy} row in the table above. +*/ + +/*! + \variable sequential + A convenient global group's element describing the sequential execution mode. + + This is the default execution mode of the Group element. + + When a Group has no execution mode, it runs in the sequential mode. + All the direct child tasks of a group are started in a chain, so that when one task finishes, + the next one starts. This enables you to pass the results from the previous task + as input to the next task before it starts. This mode guarantees that the next task + is started only after the previous task finishes. + + \sa parallel, parallelLimit() +*/ + +/*! + \variable parallel + A convenient global group's element describing the parallel execution mode. + + All the direct child tasks of a group are started after the group is started, + without waiting for the previous child tasks to finish. + In this mode, all child tasks run simultaneously. + + \sa sequential, parallelLimit() +*/ + +/*! + \variable parallelIdealThreadCountLimit + A convenient global group's element describing the parallel execution mode with a limited + number of tasks running simultanously. The limit is equal to the ideal number of threads + excluding the calling thread. + + This is a shortcut to: + \code + parallelLimit(qMax(QThread::idealThreadCount() - 1, 1)) + \endcode + + \sa parallel, parallelLimit() +*/ + +/*! + \variable stopOnError + A convenient global group's element describing the StopOnError workflow policy. + + This is the default workflow policy of the Group element. +*/ + +/*! + \variable continueOnError + A convenient global group's element describing the ContinueOnError workflow policy. +*/ + +/*! + \variable stopOnSuccess + A convenient global group's element describing the StopOnSuccess workflow policy. +*/ + +/*! + \variable continueOnSuccess + A convenient global group's element describing the ContinueOnSuccess workflow policy. +*/ + +/*! + \variable stopOnSuccessOrError + A convenient global group's element describing the StopOnSuccessOrError workflow policy. +*/ + +/*! + \variable finishAllAndSuccess + A convenient global group's element describing the FinishAllAndSuccess workflow policy. +*/ + +/*! + \variable finishAllAndError + A convenient global group's element describing the FinishAllAndError workflow policy. +*/ + +/*! + \enum Tasking::SetupResult + + This enum is optionally returned from the group's or task's setup handler function. + It instructs the running task tree on how to proceed after the setup handler's execution + finished. + \value Continue + Default. The group's or task's execution continues normally. + When a group's or task's setup handler returns void, it's assumed that + it returned Continue. + \value StopWithSuccess + The group's or task's execution stops immediately with success. + When returned from the group's setup handler, all child tasks are skipped, + and the group's onGroupDone() handler is invoked with DoneWith::Success. + The group reports success to its parent. The group's workflow policy is ignored. + When returned from the task's setup handler, the task isn't started, + its done handler isn't invoked, and the task reports success to its parent. + \value StopWithError + The group's or task's execution stops immediately with an error. + When returned from the group's setup handler, all child tasks are skipped, + and the group's onGroupDone() handler is invoked with DoneWith::Error. + The group reports an error to its parent. The group's workflow policy is ignored. + When returned from the task's setup handler, the task isn't started, + its error handler isn't invoked, and the task reports an error to its parent. +*/ + +/*! + \enum Tasking::DoneResult + + This enum is optionally returned from the group's or task's done handler function. + When the done handler doesn't return any value, that is, its return type is \c void, + its final return value is automatically deduced by the running task tree and reported + to its parent group. + + When the done handler returns the DoneResult, you can tweak the final return value + inside the handler. + + When the DoneResult is returned by the group's done handler, the group's workflow policy + is ignored. + + This enum is also used inside the TaskInterface::done() signal and it indicates whether + the task finished with success or an error. + + \value Success + The group's or task's execution ends with success. + \value Error + The group's or task's execution ends with an error. +*/ + +/*! + \enum Tasking::DoneWith + + This enum is an optional argument for the group's or task's done handler. + It indicates whether the group or task finished with success or an error, or it was canceled. + + It is also used as an argument inside the TaskTree::done() signal, + indicating the final result of the TaskTree execution. + + \value Success + The group's or task's execution ended with success. + \value Error + The group's or task's execution ended with an error. + \value Cancel + The group's or task's execution was canceled. This happens when the user calls + TaskTree::cancel() for the running task tree or when the group's workflow policy + results in canceling some of its running children. + Tweaking the done handler's final result by returning Tasking::DoneResult from + the handler is no-op when the group's or task's execution was canceled. +*/ + +/*! + \enum Tasking::CallDoneIf + + This enum is an optional argument for the \l onGroupDone() element or custom task's constructor. + It instructs the task tree on when the group's or task's done handler should be invoked. + + \value SuccessOrError + The done handler is always invoked. + \value Success + The done handler is invoked only after successful execution, + that is, when DoneWith::Success. + \value Error + The done handler is invoked only after failed execution, + that is, when DoneWith::Error or when DoneWith::Cancel. +*/ + +/*! + \typealias GroupItem::GroupSetupHandler + + Type alias for \c std::function<SetupResult()>. + + The \c GroupSetupHandler is an argument of the onGroupSetup() element. + Any function with the above signature, when passed as a group setup handler, + will be called by the running task tree when the group execution starts. + + The return value of the handler instructs the running group on how to proceed + after the handler's invocation is finished. The default return value of SetupResult::Continue + instructs the group to continue running, that is, to start executing its child tasks. + The return value of SetupResult::StopWithSuccess or SetupResult::StopWithError + instructs the group to skip the child tasks' execution and finish immediately with + success or an error, respectively. + + When the return type is either SetupResult::StopWithSuccess or SetupResult::StopWithError, + the group's done handler (if provided) is called synchronously immediately afterwards. + + \note Even if the group setup handler returns StopWithSuccess or StopWithError, + the group's done handler is invoked. This behavior differs from that of task done handler + and might change in the future. + + The onGroupSetup() element accepts also functions in the shortened form of + \c std::function<void()>, that is, the return value is \c void. + In this case, it's assumed that the return value is SetupResult::Continue. + + \sa onGroupSetup(), GroupDoneHandler, CustomTask::TaskSetupHandler +*/ + +/*! + \typealias GroupItem::GroupDoneHandler + + Type alias for \c std::function<DoneResult(DoneWith)>. + + The \c GroupDoneHandler is an argument of the onGroupDone() element. + Any function with the above signature, when passed as a group done handler, + will be called by the running task tree when the group execution ends. + + The DoneWith argument is optional and your done handler may omit it. + When provided, it holds the info about the final result of a group that will be + reported to its parent. + + The returned DoneResult value is optional and your handler may return \c void instead. + In this case, the final result of the group will be equal to the value indicated by + the DoneWith argument. When the handler returns the DoneResult value, + the group's final result may be tweaked inside the done handler's body by the returned value. + + \sa onGroupDone(), GroupSetupHandler, CustomTask::TaskDoneHandler +*/ + +/*! + \fn template <typename Handler> GroupItem onGroupSetup(Handler &&handler) + + Constructs a group's element holding the group setup handler. + The \a handler is invoked whenever the group starts. + + The passed \a handler is either of the \c std::function<SetupResult()> or the + \c std::function<void()> type. For more information on a possible handler type, refer to + \l {GroupItem::GroupSetupHandler}. + + When the \a handler is invoked, none of the group's child tasks are running yet. + + If a group contains the Storage elements, the \a handler is invoked + after the storages are constructed, so that the \a handler may already + perform some initial modifications to the active storages. + + \sa GroupItem::GroupSetupHandler, onGroupDone() +*/ + +/*! + \fn template <typename Handler> GroupItem onGroupDone(Handler &&handler, CallDoneIf callDoneIf = CallDoneIf::SuccessOrError) + + Constructs a group's element holding the group done handler. + By default, the \a handler is invoked whenever the group finishes. + Pass a non-default value for the \a callDoneIf argument when you want the handler to be called + only on a successful or failed execution. + Depending on the group's workflow policy, this handler may also be called + when the running group is canceled (e.g. when stopOnError element was used). + + The passed \a handler is of the \c std::function<DoneResult(DoneWith)> type. + Optionally, each of the return DoneResult type or the argument DoneWith type may be omitted + (that is, its return type may be \c void). For more information on a possible handler type, + refer to \l {GroupItem::GroupDoneHandler}. + + When the \a handler is invoked, all of the group's child tasks are already finished. + + If a group contains the Storage elements, the \a handler is invoked + before the storages are destructed, so that the \a handler may still + perform a last read of the active storages' data. + + \sa GroupItem::GroupDoneHandler, onGroupSetup() +*/ + +/*! + Constructs a group's element describing the \l{Execution Mode}{execution mode}. + + The execution mode element in a Group specifies how the direct child tasks of + the Group are started. + + For convenience, when appropriate, the \l sequential or \l parallel global elements + may be used instead. + + The \a limit defines the maximum number of direct child tasks running in parallel: + + \list + \li When \a limit equals to 0, there is no limit, and all direct child tasks are started + together, in the oder in which they appear in a group. This means the fully parallel + execution, and the \l parallel element may be used instead. + + \li When \a limit equals to 1, it means that only one child task may run at the time. + This means the sequential execution, and the \l sequential element may be used instead. + In this case, child tasks run in chain, so the next child task starts after + the previous child task has finished. + + \li When other positive number is passed as \a limit, the group's child tasks run + in parallel, but with a limited number of tasks running simultanously. + The \e limit defines the maximum number of tasks running in parallel in a group. + When the group is started, the first batch of tasks is started + (the number of tasks in a batch equals to the passed \a limit, at most), + while the others are kept waiting. When any running task finishes, + the group starts the next remaining one, so that the \e limit of simultaneously + running tasks inside a group isn't exceeded. This repeats on every child task's + finish until all child tasks are started. This enables you to limit the maximum + number of tasks that run simultaneously, for example if running too many processes might + block the machine for a long time. + \endlist + + In all execution modes, a group starts tasks in the oder in which they appear. + + If a child of a group is also a group, the child group runs its tasks according + to its own execution mode. + + \sa sequential, parallel +*/ +GroupItem parallelLimit(int limit) +{ + return Group::parallelLimit(qMax(limit, 0)); +} + +/*! + Constructs a group's \l {Workflow Policy} {workflow policy} element for a given \a policy. + + For convenience, global elements may be used instead. + + \sa stopOnError, continueOnError, stopOnSuccess, continueOnSuccess, stopOnSuccessOrError, + finishAllAndSuccess, finishAllAndError, WorkflowPolicy +*/ +GroupItem workflowPolicy(WorkflowPolicy policy) +{ + return Group::workflowPolicy(policy); +} + +const GroupItem nullItem = GroupItem({}); + +const GroupItem sequential = parallelLimit(1); +const GroupItem parallel = parallelLimit(0); +const GroupItem parallelIdealThreadCountLimit = parallelLimit(qMax(QThread::idealThreadCount() - 1, 1)); + +const GroupItem stopOnError = workflowPolicy(WorkflowPolicy::StopOnError); +const GroupItem continueOnError = workflowPolicy(WorkflowPolicy::ContinueOnError); +const GroupItem stopOnSuccess = workflowPolicy(WorkflowPolicy::StopOnSuccess); +const GroupItem continueOnSuccess = workflowPolicy(WorkflowPolicy::ContinueOnSuccess); +const GroupItem stopOnSuccessOrError = workflowPolicy(WorkflowPolicy::StopOnSuccessOrError); +const GroupItem finishAllAndSuccess = workflowPolicy(WorkflowPolicy::FinishAllAndSuccess); +const GroupItem finishAllAndError = workflowPolicy(WorkflowPolicy::FinishAllAndError); + +// Please note the thread_local keyword below guarantees a separate instance per thread. +// The s_activeTaskTrees is currently used internally only and is not exposed in the public API. +// It serves for withLog() implementation now. Add a note here when a new usage is introduced. +static thread_local QList<TaskTree *> s_activeTaskTrees = {}; + +static TaskTree *activeTaskTree() +{ + QT_ASSERT(s_activeTaskTrees.size(), return nullptr); + return s_activeTaskTrees.back(); +} + +DoneResult toDoneResult(bool success) +{ + return success ? DoneResult::Success : DoneResult::Error; +} + +static SetupResult toSetupResult(bool success) +{ + return success ? SetupResult::StopWithSuccess : SetupResult::StopWithError; +} + +static DoneResult toDoneResult(DoneWith doneWith) +{ + return doneWith == DoneWith::Success ? DoneResult::Success : DoneResult::Error; +} + +static DoneWith toDoneWith(DoneResult result) +{ + return result == DoneResult::Success ? DoneWith::Success : DoneWith::Error; +} + +class LoopThreadData +{ + Q_DISABLE_COPY_MOVE(LoopThreadData) + +public: + LoopThreadData() = default; + void pushIteration(int index) + { + m_activeLoopStack.push_back(index); + } + void popIteration() + { + QT_ASSERT(m_activeLoopStack.size(), return); + m_activeLoopStack.pop_back(); + } + int iteration() const + { + QT_ASSERT(m_activeLoopStack.size(), qWarning( + "The referenced loop is not reachable in the running tree. " + "A -1 will be returned which might lead to a crash in the calling code. " + "It is possible that no loop was added to the tree, " + "or the loop is not reachable from where it is referenced."); return -1); + return m_activeLoopStack.last(); + } + +private: + QList<int> m_activeLoopStack; +}; + +class LoopData +{ +public: + LoopThreadData &threadData() { + QMutexLocker lock(&m_threadDataMutex); + return m_threadDataMap.try_emplace(QThread::currentThread()).first->second; + } + + const std::optional<int> m_loopCount = {}; + const Loop::ValueGetter m_valueGetter = {}; + const Loop::Condition m_condition = {}; + QMutex m_threadDataMutex = {}; + // Use std::map on purpose, so that it doesn't invalidate references on modifications. + // Don't optimize it by using std::unordered_map. + std::map<QThread *, LoopThreadData> m_threadDataMap = {}; +}; + +Loop::Loop() + : m_loopData(new LoopData) +{} + +Loop::Loop(int count, const ValueGetter &valueGetter) + : m_loopData(new LoopData{count, valueGetter}) +{} + +Loop::Loop(const Condition &condition) + : m_loopData(new LoopData{{}, {}, condition}) +{} + +int Loop::iteration() const +{ + return m_loopData->threadData().iteration(); +} + +const void *Loop::valuePtr() const +{ + return m_loopData->m_valueGetter(iteration()); +} + +using StoragePtr = void *; + +static QString s_activeStorageWarning = QString::fromLatin1( + "The referenced storage is not reachable in the running tree. " + "A nullptr will be returned which might lead to a crash in the calling code. " + "It is possible that no storage was added to the tree, " + "or the storage is not reachable from where it is referenced."); + +class StorageThreadData +{ + Q_DISABLE_COPY_MOVE(StorageThreadData) + +public: + StorageThreadData() = default; + void pushStorage(StoragePtr storagePtr) + { + m_activeStorageStack.push_back({storagePtr, activeTaskTree()}); + } + void popStorage() + { + QT_ASSERT(m_activeStorageStack.size(), return); + m_activeStorageStack.pop_back(); + } + StoragePtr activeStorage() const + { + QT_ASSERT(m_activeStorageStack.size(), + qWarning().noquote() << s_activeStorageWarning; return nullptr); + const QPair<StoragePtr, TaskTree *> &top = m_activeStorageStack.last(); + QT_ASSERT(top.second == activeTaskTree(), + qWarning().noquote() << s_activeStorageWarning; return nullptr); + return top.first; + } + +private: + QList<QPair<StoragePtr, TaskTree *>> m_activeStorageStack; +}; + +class StorageData +{ +public: + StorageThreadData &threadData() { + QMutexLocker lock(&m_threadDataMutex); + return m_threadDataMap.try_emplace(QThread::currentThread()).first->second; + } + + const StorageBase::StorageConstructor m_constructor = {}; + const StorageBase::StorageDestructor m_destructor = {}; + QMutex m_threadDataMutex = {}; + // Use std::map on purpose, so that it doesn't invalidate references on modifications. + // Don't optimize it by using std::unordered_map. + std::map<QThread *, StorageThreadData> m_threadDataMap = {}; +}; + +StorageBase::StorageBase(const StorageConstructor &ctor, const StorageDestructor &dtor) + : m_storageData(new StorageData{ctor, dtor}) +{} + +void *StorageBase::activeStorageVoid() const +{ + return m_storageData->threadData().activeStorage(); +} + +void GroupItem::addChildren(const QList<GroupItem> &children) +{ + QT_ASSERT(m_type == Type::Group || m_type == Type::List, + qWarning("Only Group or List may have children, skipping..."); return); + if (m_type == Type::List) { + m_children.append(children); + return; + } + for (const GroupItem &child : children) { + switch (child.m_type) { + case Type::List: + addChildren(child.m_children); + break; + case Type::Group: + m_children.append(child); + break; + case Type::GroupData: + if (child.m_groupData.m_groupHandler.m_setupHandler) { + QT_ASSERT(!m_groupData.m_groupHandler.m_setupHandler, + qWarning("Group setup handler redefinition, overriding...")); + m_groupData.m_groupHandler.m_setupHandler + = child.m_groupData.m_groupHandler.m_setupHandler; + } + if (child.m_groupData.m_groupHandler.m_doneHandler) { + QT_ASSERT(!m_groupData.m_groupHandler.m_doneHandler, + qWarning("Group done handler redefinition, overriding...")); + m_groupData.m_groupHandler.m_doneHandler + = child.m_groupData.m_groupHandler.m_doneHandler; + m_groupData.m_groupHandler.m_callDoneIf + = child.m_groupData.m_groupHandler.m_callDoneIf; + } + if (child.m_groupData.m_parallelLimit) { + QT_ASSERT(!m_groupData.m_parallelLimit, + qWarning("Group execution mode redefinition, overriding...")); + m_groupData.m_parallelLimit = child.m_groupData.m_parallelLimit; + } + if (child.m_groupData.m_workflowPolicy) { + QT_ASSERT(!m_groupData.m_workflowPolicy, + qWarning("Group workflow policy redefinition, overriding...")); + m_groupData.m_workflowPolicy = child.m_groupData.m_workflowPolicy; + } + if (child.m_groupData.m_loop) { + QT_ASSERT(!m_groupData.m_loop, + qWarning("Group loop redefinition, overriding...")); + m_groupData.m_loop = child.m_groupData.m_loop; + } + break; + case Type::TaskHandler: + QT_ASSERT(child.m_taskHandler.m_createHandler, + qWarning("Task create handler can't be null, skipping..."); return); + m_children.append(child); + break; + case Type::Storage: + // Check for duplicates, as can't have the same storage twice on the same level. + for (const StorageBase &storage : child.m_storageList) { + if (m_storageList.contains(storage)) { + QT_ASSERT(false, qWarning("Can't add the same storage into one Group twice, " + "skipping...")); + continue; + } + m_storageList.append(storage); + } + break; + } + } +} + +/*! + \class Tasking::ExecutableItem + \inheaderfile solutions/tasking/tasktree.h + \inmodule TaskingSolution + \brief Base class for executable task items. + \reentrant + + \c ExecutableItem provides an additional interface for items containing executable tasks. + Use withTimeout() to attach a timeout to a task. + Use withLog() to include debugging information about the task startup and the execution result. +*/ + +/*! + Attaches \c TimeoutTask to a copy of \c this ExecutableItem, elapsing after \a timeout + in milliseconds, with an optionally provided timeout \a handler, and returns the coupled item. + + When the ExecutableItem finishes before \a timeout passes, the returned item finishes + immediately with the task's result. Otherwise, \a handler is invoked (if provided), + the task is canceled, and the returned item finishes with an error. +*/ +ExecutableItem ExecutableItem::withTimeout(milliseconds timeout, + const std::function<void()> &handler) const +{ + const auto onSetup = [timeout](milliseconds &timeoutData) { timeoutData = timeout; }; + return Group { + parallel, + stopOnSuccessOrError, + Group { + finishAllAndError, + handler ? TimeoutTask(onSetup, [handler] { handler(); }, CallDoneIf::Success) + : TimeoutTask(onSetup) + }, + *this + }; +} + +static QString currentTime() { return QTime::currentTime().toString(Qt::ISODateWithMs); } + +static QString logHeader(const QString &logName) +{ + return QString::fromLatin1("TASK TREE LOG [%1] \"%2\"").arg(currentTime(), logName); +}; + +/*! + Attaches a custom debug printout to a copy of \c this ExecutableItem, + issued on task startup and after the task is finished, and returns the coupled item. + + The debug printout includes a timestamp of the event (start or finish) + and \a logName to identify the specific task in the debug log. + + The finish printout contains the additional information whether the execution was + synchronous or asynchronous, its result (the value described by the DoneWith enum), + and the total execution time in milliseconds. +*/ +ExecutableItem ExecutableItem::withLog(const QString &logName) const +{ + struct LogStorage + { + time_point<system_clock, nanoseconds> start; + int asyncCount = 0; + }; + const Storage<LogStorage> storage; + return Group { + storage, + onGroupSetup([storage, logName] { + storage->start = system_clock::now(); + storage->asyncCount = activeTaskTree()->asyncCount(); + qDebug().noquote().nospace() << logHeader(logName) << " started."; + }), + *this, + onGroupDone([storage, logName](DoneWith result) { + const auto elapsed = duration_cast<milliseconds>(system_clock::now() - storage->start); + const int asyncCountDiff = activeTaskTree()->asyncCount() - storage->asyncCount; + QT_CHECK(asyncCountDiff >= 0); + const QMetaEnum doneWithEnum = QMetaEnum::fromType<DoneWith>(); + const QString syncType = asyncCountDiff ? QString::fromLatin1("asynchronously") + : QString::fromLatin1("synchronously"); + qDebug().noquote().nospace() << logHeader(logName) << " finished " << syncType + << " with " << doneWithEnum.valueToKey(int(result)) + << " within " << elapsed.count() << "ms."; + }) + }; +} + +ExecutableItem ExecutableItem::withCancelImpl( + const std::function<void(QObject *, const std::function<void()> &)> &connectWrapper) const +{ + const auto onSetup = [connectWrapper](Barrier &barrier) { + connectWrapper(&barrier, [barrierPtr = &barrier] { barrierPtr->advance(); }); + }; + return Group { + parallel, + stopOnSuccessOrError, + Group { + finishAllAndError, + BarrierTask(onSetup) + }, + *this + }; +} + +class TaskTreePrivate; +class TaskNode; +class RuntimeContainer; +class RuntimeIteration; +class RuntimeTask; + +class ExecutionContextActivator +{ +public: + ExecutionContextActivator(RuntimeIteration *iteration) { + activateTaskTree(iteration); + activateContext(iteration); + } + ExecutionContextActivator(RuntimeContainer *container) { + activateTaskTree(container); + activateContext(container); + } + ~ExecutionContextActivator() { + for (int i = m_activeStorages.size() - 1; i >= 0; --i) // iterate in reverse order + m_activeStorages[i].m_storageData->threadData().popStorage(); + for (int i = m_activeLoops.size() - 1; i >= 0; --i) // iterate in reverse order + m_activeLoops[i].m_loopData->threadData().popIteration(); + QT_ASSERT(s_activeTaskTrees.size(), return); + s_activeTaskTrees.pop_back(); + } + +private: + void activateTaskTree(RuntimeIteration *iteration); + void activateTaskTree(RuntimeContainer *container); + void activateContext(RuntimeIteration *iteration); + void activateContext(RuntimeContainer *container); + QList<Loop> m_activeLoops; + QList<StorageBase> m_activeStorages; +}; + +class ContainerNode +{ + Q_DISABLE_COPY(ContainerNode) + +public: + ContainerNode(ContainerNode &&other) = default; + ContainerNode(TaskTreePrivate *taskTreePrivate, const GroupItem &task); + + TaskTreePrivate *const m_taskTreePrivate = nullptr; + + const GroupItem::GroupHandler m_groupHandler; + const int m_parallelLimit = 1; + const WorkflowPolicy m_workflowPolicy = WorkflowPolicy::StopOnError; + const std::optional<Loop> m_loop; + const QList<StorageBase> m_storageList; + std::vector<TaskNode> m_children; + const int m_taskCount = 0; +}; + +class TaskNode +{ + Q_DISABLE_COPY(TaskNode) + +public: + TaskNode(TaskNode &&other) = default; + TaskNode(TaskTreePrivate *taskTreePrivate, const GroupItem &task) + : m_taskHandler(task.m_taskHandler) + , m_container(taskTreePrivate, task) + {} + + bool isTask() const { return bool(m_taskHandler.m_createHandler); } + int taskCount() const { return isTask() ? 1 : m_container.m_taskCount; } + + const GroupItem::TaskHandler m_taskHandler; + ContainerNode m_container; +}; + +class TaskTreePrivate +{ + Q_DISABLE_COPY_MOVE(TaskTreePrivate) + +public: + TaskTreePrivate(TaskTree *taskTree) + : q(taskTree) {} + + void start(); + void stop(); + void bumpAsyncCount(); + void advanceProgress(int byValue); + void emitDone(DoneWith result); + void callSetupHandler(StorageBase storage, StoragePtr storagePtr) { + callStorageHandler(storage, storagePtr, &StorageHandler::m_setupHandler); + } + void callDoneHandler(StorageBase storage, StoragePtr storagePtr) { + callStorageHandler(storage, storagePtr, &StorageHandler::m_doneHandler); + } + struct StorageHandler { + StorageBase::StorageHandler m_setupHandler = {}; + StorageBase::StorageHandler m_doneHandler = {}; + }; + typedef StorageBase::StorageHandler StorageHandler::*HandlerPtr; // ptr to class member + void callStorageHandler(StorageBase storage, StoragePtr storagePtr, HandlerPtr ptr) + { + const auto it = m_storageHandlers.constFind(storage); + if (it == m_storageHandlers.constEnd()) + return; + const StorageHandler storageHandler = *it; + if (storageHandler.*ptr) { + GuardLocker locker(m_guard); + (storageHandler.*ptr)(storagePtr); + } + } + + // Node related methods + + // If returned value != Continue, childDone() needs to be called in parent container (in caller) + // in order to unwind properly. + SetupResult start(RuntimeTask *node); + void stop(RuntimeTask *node); + bool invokeDoneHandler(RuntimeTask *node, DoneWith doneWith); + + // Container related methods + + SetupResult start(RuntimeContainer *container); + SetupResult continueStart(RuntimeContainer *container, SetupResult startAction); + SetupResult startChildren(RuntimeContainer *container); + SetupResult childDone(RuntimeIteration *iteration, bool success); + void stop(RuntimeContainer *container); + bool invokeDoneHandler(RuntimeContainer *container, DoneWith doneWith); + bool invokeLoopHandler(RuntimeContainer *container); + + template <typename Container, typename Handler, typename ...Args, + typename ReturnType = std::invoke_result_t<Handler, Args...>> + ReturnType invokeHandler(Container *container, Handler &&handler, Args &&...args) + { + ExecutionContextActivator activator(container); + GuardLocker locker(m_guard); + return std::invoke(std::forward<Handler>(handler), std::forward<Args>(args)...); + } + + static int effectiveLoopCount(const std::optional<Loop> &loop) + { + return loop && loop->m_loopData->m_loopCount ? *loop->m_loopData->m_loopCount : 1; + } + + TaskTree *q = nullptr; + Guard m_guard; + int m_progressValue = 0; + int m_asyncCount = 0; + QSet<StorageBase> m_storages; + QHash<StorageBase, StorageHandler> m_storageHandlers; + std::optional<TaskNode> m_root; + std::unique_ptr<RuntimeTask> m_runtimeRoot; // Keep me last in order to destruct first +}; + +static bool initialSuccessBit(WorkflowPolicy workflowPolicy) +{ + switch (workflowPolicy) { + case WorkflowPolicy::StopOnError: + case WorkflowPolicy::ContinueOnError: + case WorkflowPolicy::FinishAllAndSuccess: + return true; + case WorkflowPolicy::StopOnSuccess: + case WorkflowPolicy::ContinueOnSuccess: + case WorkflowPolicy::StopOnSuccessOrError: + case WorkflowPolicy::FinishAllAndError: + return false; + } + QT_CHECK(false); + return false; +} + +static bool isProgressive(RuntimeContainer *container); + +class RuntimeIteration +{ + Q_DISABLE_COPY(RuntimeIteration) + +public: + RuntimeIteration(int index, RuntimeContainer *container); + std::optional<Loop> loop() const; + void deleteChild(RuntimeTask *node); + + const int m_iterationIndex = 0; + const bool m_isProgressive = true; + RuntimeContainer *m_container = nullptr; + int m_doneCount = 0; + std::vector<std::unique_ptr<RuntimeTask>> m_children = {}; // Owning. +}; + +class RuntimeContainer +{ + Q_DISABLE_COPY(RuntimeContainer) + +public: + RuntimeContainer(const ContainerNode &taskContainer, RuntimeTask *parentTask) + : m_containerNode(taskContainer) + , m_parentTask(parentTask) + , m_storages(createStorages(taskContainer)) + , m_successBit(initialSuccessBit(taskContainer.m_workflowPolicy)) + , m_shouldIterate(taskContainer.m_loop) + {} + + ~RuntimeContainer() + { + for (int i = m_containerNode.m_storageList.size() - 1; i >= 0; --i) { // iterate in reverse order + const StorageBase storage = m_containerNode.m_storageList[i]; + StoragePtr storagePtr = m_storages.value(i); + if (m_callStorageDoneHandlersOnDestruction) + m_containerNode.m_taskTreePrivate->callDoneHandler(storage, storagePtr); + storage.m_storageData->m_destructor(storagePtr); + } + } + + static QList<StoragePtr> createStorages(const ContainerNode &container); + bool isStarting() const { return m_startGuard.isLocked(); } + RuntimeIteration *parentIteration() const; + bool updateSuccessBit(bool success); + void deleteFinishedIterations(); + int progressiveLoopCount() const + { + return m_containerNode.m_taskTreePrivate->effectiveLoopCount(m_containerNode.m_loop); + } + + const ContainerNode &m_containerNode; // Not owning. + RuntimeTask *m_parentTask = nullptr; // Not owning. + const QList<StoragePtr> m_storages; // Owning. + + bool m_successBit = true; + bool m_callStorageDoneHandlersOnDestruction = false; + Guard m_startGuard; + + int m_iterationCount = 0; + int m_nextToStart = 0; + int m_runningChildren = 0; + bool m_shouldIterate = true; + std::vector<std::unique_ptr<RuntimeIteration>> m_iterations; // Owning. +}; + +class RuntimeTask +{ +public: + ~RuntimeTask() + { + if (m_task) { + // Ensures the running task's d'tor doesn't emit done() signal. QTCREATORBUG-30204. + QObject::disconnect(m_task.get(), &TaskInterface::done, + m_taskNode.m_container.m_taskTreePrivate->q, nullptr); + } + } + + const TaskNode &m_taskNode; // Not owning. + RuntimeIteration *m_parentIteration = nullptr; // Not owning. + std::optional<RuntimeContainer> m_container = {}; // Owning. + std::unique_ptr<TaskInterface> m_task = {}; // Owning. +}; + +static bool isProgressive(RuntimeContainer *container) +{ + RuntimeIteration *iteration = container->m_parentTask->m_parentIteration; + return iteration ? iteration->m_isProgressive : true; +} + +void ExecutionContextActivator::activateTaskTree(RuntimeIteration *iteration) +{ + activateTaskTree(iteration->m_container); +} + +void ExecutionContextActivator::activateTaskTree(RuntimeContainer *container) +{ + s_activeTaskTrees.push_back(container->m_containerNode.m_taskTreePrivate->q); +} + +void ExecutionContextActivator::activateContext(RuntimeIteration *iteration) +{ + std::optional<Loop> loop = iteration->loop(); + if (loop) { + loop->m_loopData->threadData().pushIteration(iteration->m_iterationIndex); + m_activeLoops.append(*loop); + } + activateContext(iteration->m_container); +} + +void ExecutionContextActivator::activateContext(RuntimeContainer *container) +{ + const ContainerNode &containerNode = container->m_containerNode; + for (int i = 0; i < containerNode.m_storageList.size(); ++i) { + const StorageBase &storage = containerNode.m_storageList[i]; + if (m_activeStorages.contains(storage)) + continue; // Storage shadowing: The storage is already active, skipping it... + m_activeStorages.append(storage); + storage.m_storageData->threadData().pushStorage(container->m_storages.value(i)); + } + // Go to the parent after activating this storages so that storage shadowing works + // in the direction from child to parent root. + if (container->parentIteration()) + activateContext(container->parentIteration()); +} + +void TaskTreePrivate::start() +{ + QT_ASSERT(m_root, return); + QT_ASSERT(!m_runtimeRoot, return); + m_asyncCount = 0; + m_progressValue = 0; + { + GuardLocker locker(m_guard); + emit q->started(); + emit q->asyncCountChanged(m_asyncCount); + emit q->progressValueChanged(m_progressValue); + } + // TODO: check storage handlers for not existing storages in tree + for (auto it = m_storageHandlers.cbegin(); it != m_storageHandlers.cend(); ++it) { + QT_ASSERT(m_storages.contains(it.key()), qWarning("The registered storage doesn't " + "exist in task tree. Its handlers will never be called.")); + } + m_runtimeRoot.reset(new RuntimeTask{*m_root}); + start(m_runtimeRoot.get()); + bumpAsyncCount(); +} + +void TaskTreePrivate::stop() +{ + QT_ASSERT(m_root, return); + if (!m_runtimeRoot) + return; + stop(m_runtimeRoot.get()); + m_runtimeRoot.reset(); + emitDone(DoneWith::Cancel); +} + +void TaskTreePrivate::bumpAsyncCount() +{ + if (!m_runtimeRoot) + return; + ++m_asyncCount; + GuardLocker locker(m_guard); + emit q->asyncCountChanged(m_asyncCount); +} + +void TaskTreePrivate::advanceProgress(int byValue) +{ + if (byValue == 0) + return; + QT_CHECK(byValue > 0); + QT_CHECK(m_progressValue + byValue <= m_root->taskCount()); + m_progressValue += byValue; + GuardLocker locker(m_guard); + emit q->progressValueChanged(m_progressValue); +} + +void TaskTreePrivate::emitDone(DoneWith result) +{ + QT_CHECK(m_progressValue == m_root->taskCount()); + GuardLocker locker(m_guard); + emit q->done(result); +} + +RuntimeIteration::RuntimeIteration(int index, RuntimeContainer *container) + : m_iterationIndex(index) + , m_isProgressive(index < container->progressiveLoopCount() && isProgressive(container)) + , m_container(container) +{} + +std::optional<Loop> RuntimeIteration::loop() const +{ + return m_container->m_containerNode.m_loop; +} + +void RuntimeIteration::deleteChild(RuntimeTask *task) +{ + const auto it = std::find_if(m_children.cbegin(), m_children.cend(), [task](const auto &ptr) { + return ptr.get() == task; + }); + if (it != m_children.cend()) + m_children.erase(it); +} + +static std::vector<TaskNode> createChildren(TaskTreePrivate *taskTreePrivate, + const QList<GroupItem> &children) +{ + std::vector<TaskNode> result; + result.reserve(children.size()); + for (const GroupItem &child : children) + result.emplace_back(taskTreePrivate, child); + return result; +} + +ContainerNode::ContainerNode(TaskTreePrivate *taskTreePrivate, const GroupItem &task) + : m_taskTreePrivate(taskTreePrivate) + , m_groupHandler(task.m_groupData.m_groupHandler) + , m_parallelLimit(task.m_groupData.m_parallelLimit.value_or(1)) + , m_workflowPolicy(task.m_groupData.m_workflowPolicy.value_or(WorkflowPolicy::StopOnError)) + , m_loop(task.m_groupData.m_loop) + , m_storageList(task.m_storageList) + , m_children(createChildren(taskTreePrivate, task.m_children)) + , m_taskCount(std::accumulate(m_children.cbegin(), m_children.cend(), 0, + [](int r, const TaskNode &n) { return r + n.taskCount(); }) + * taskTreePrivate->effectiveLoopCount(m_loop)) +{ + for (const StorageBase &storage : m_storageList) + m_taskTreePrivate->m_storages << storage; +} + +QList<StoragePtr> RuntimeContainer::createStorages(const ContainerNode &container) +{ + QList<StoragePtr> storages; + for (const StorageBase &storage : container.m_storageList) { + StoragePtr storagePtr = storage.m_storageData->m_constructor(); + storages.append(storagePtr); + container.m_taskTreePrivate->callSetupHandler(storage, storagePtr); + } + return storages; +} + +RuntimeIteration *RuntimeContainer::parentIteration() const +{ + return m_parentTask->m_parentIteration; +} + +bool RuntimeContainer::updateSuccessBit(bool success) +{ + if (m_containerNode.m_workflowPolicy == WorkflowPolicy::FinishAllAndSuccess + || m_containerNode.m_workflowPolicy == WorkflowPolicy::FinishAllAndError + || m_containerNode.m_workflowPolicy == WorkflowPolicy::StopOnSuccessOrError) { + if (m_containerNode.m_workflowPolicy == WorkflowPolicy::StopOnSuccessOrError) + m_successBit = success; + return m_successBit; + } + + const bool donePolicy = m_containerNode.m_workflowPolicy == WorkflowPolicy::StopOnSuccess + || m_containerNode.m_workflowPolicy == WorkflowPolicy::ContinueOnSuccess; + m_successBit = donePolicy ? (m_successBit || success) : (m_successBit && success); + return m_successBit; +} + +void RuntimeContainer::deleteFinishedIterations() +{ + for (auto it = m_iterations.cbegin(); it != m_iterations.cend(); ) { + if (it->get()->m_doneCount == int(m_containerNode.m_children.size())) + it = m_iterations.erase(it); + else + ++it; + } +} + +SetupResult TaskTreePrivate::start(RuntimeContainer *container) +{ + const ContainerNode &containerNode = container->m_containerNode; + SetupResult startAction = SetupResult::Continue; + if (containerNode.m_groupHandler.m_setupHandler) { + startAction = invokeHandler(container, containerNode.m_groupHandler.m_setupHandler); + if (startAction != SetupResult::Continue) { + if (isProgressive(container)) + advanceProgress(containerNode.m_taskCount); + // Non-Continue SetupResult takes precedence over the workflow policy. + container->m_successBit = startAction == SetupResult::StopWithSuccess; + } + } + if (startAction == SetupResult::Continue + && (containerNode.m_children.empty() + || (containerNode.m_loop && !invokeLoopHandler(container)))) { + if (isProgressive(container)) + advanceProgress(containerNode.m_taskCount); + startAction = toSetupResult(container->m_successBit); + } + return continueStart(container, startAction); +} + +SetupResult TaskTreePrivate::continueStart(RuntimeContainer *container, SetupResult startAction) +{ + const SetupResult groupAction = startAction == SetupResult::Continue ? startChildren(container) + : startAction; + if (groupAction != SetupResult::Continue) { + const bool bit = container->updateSuccessBit(groupAction == SetupResult::StopWithSuccess); + RuntimeIteration *parentIteration = container->parentIteration(); + RuntimeTask *parentTask = container->m_parentTask; + QT_CHECK(parentTask); + const bool result = invokeDoneHandler(container, bit ? DoneWith::Success : DoneWith::Error); + if (parentIteration) { + parentIteration->deleteChild(parentTask); + if (!parentIteration->m_container->isStarting()) + childDone(parentIteration, result); + } else { + QT_CHECK(m_runtimeRoot.get() == parentTask); + m_runtimeRoot.reset(); + emitDone(result ? DoneWith::Success : DoneWith::Error); + } + } + return groupAction; +} + +SetupResult TaskTreePrivate::startChildren(RuntimeContainer *container) +{ + const ContainerNode &containerNode = container->m_containerNode; + const int childCount = int(containerNode.m_children.size()); + + if (container->m_iterationCount == 0) { + container->m_iterations.emplace_back( + std::make_unique<RuntimeIteration>(container->m_iterationCount, container)); + ++container->m_iterationCount; + } else if (containerNode.m_parallelLimit == 0) { + container->deleteFinishedIterations(); + if (container->m_iterations.empty()) + return toSetupResult(container->m_successBit); + return SetupResult::Continue; + } + + GuardLocker locker(container->m_startGuard); + + while (containerNode.m_parallelLimit == 0 + || container->m_runningChildren < containerNode.m_parallelLimit) { + container->deleteFinishedIterations(); + if (container->m_nextToStart == childCount) { + if (container->m_shouldIterate && invokeLoopHandler(container)) { + container->m_nextToStart = 0; + container->m_iterations.emplace_back( + std::make_unique<RuntimeIteration>(container->m_iterationCount, container)); + ++container->m_iterationCount; + } else { + if (container->m_iterations.empty()) + return toSetupResult(container->m_successBit); + return SetupResult::Continue; + } + } + RuntimeIteration *iteration = container->m_iterations.back().get(); + RuntimeTask *newTask = new RuntimeTask{containerNode.m_children.at(container->m_nextToStart), + iteration}; + iteration->m_children.emplace_back(newTask); + ++container->m_runningChildren; + ++container->m_nextToStart; + + const SetupResult startAction = start(newTask); + if (startAction == SetupResult::Continue) + continue; + + const SetupResult finalizeAction = childDone(iteration, + startAction == SetupResult::StopWithSuccess); + if (finalizeAction != SetupResult::Continue) + return finalizeAction; + } + return SetupResult::Continue; +} + +SetupResult TaskTreePrivate::childDone(RuntimeIteration *iteration, bool success) +{ + RuntimeContainer *container = iteration->m_container; + const WorkflowPolicy &workflowPolicy = container->m_containerNode.m_workflowPolicy; + const bool shouldStop = workflowPolicy == WorkflowPolicy::StopOnSuccessOrError + || (workflowPolicy == WorkflowPolicy::StopOnSuccess && success) + || (workflowPolicy == WorkflowPolicy::StopOnError && !success); + ++iteration->m_doneCount; + --container->m_runningChildren; + if (shouldStop) + stop(container); + + const bool updatedSuccess = container->updateSuccessBit(success); + const SetupResult startAction = shouldStop ? toSetupResult(updatedSuccess) + : SetupResult::Continue; + + if (container->isStarting()) + return startAction; + return continueStart(container, startAction); +} + +void TaskTreePrivate::stop(RuntimeContainer *container) +{ + const ContainerNode &containerNode = container->m_containerNode; + for (auto &iteration : container->m_iterations) { + for (auto &child : iteration->m_children) { + ++iteration->m_doneCount; + stop(child.get()); + } + + if (iteration->m_isProgressive) { + int skippedTaskCount = 0; + for (int i = iteration->m_doneCount; i < int(containerNode.m_children.size()); ++i) + skippedTaskCount += containerNode.m_children.at(i).taskCount(); + advanceProgress(skippedTaskCount); + } + } + const int skippedIterations = container->progressiveLoopCount() - container->m_iterationCount; + if (skippedIterations > 0) { + advanceProgress(container->m_containerNode.m_taskCount / container->progressiveLoopCount() + * skippedIterations); + } +} + +static bool shouldCall(CallDoneIf callDoneIf, DoneWith result) +{ + if (result == DoneWith::Success) + return callDoneIf != CallDoneIf::Error; + return callDoneIf != CallDoneIf::Success; +} + +bool TaskTreePrivate::invokeDoneHandler(RuntimeContainer *container, DoneWith doneWith) +{ + DoneResult result = toDoneResult(doneWith); + const GroupItem::GroupHandler &groupHandler = container->m_containerNode.m_groupHandler; + if (groupHandler.m_doneHandler && shouldCall(groupHandler.m_callDoneIf, doneWith)) + result = invokeHandler(container, groupHandler.m_doneHandler, doneWith); + container->m_callStorageDoneHandlersOnDestruction = true; + // TODO: is it needed? + container->m_parentTask->m_container.reset(); + return result == DoneResult::Success; +} + +bool TaskTreePrivate::invokeLoopHandler(RuntimeContainer *container) +{ + if (container->m_shouldIterate) { + const LoopData *loopData = container->m_containerNode.m_loop->m_loopData.get(); + if (loopData->m_loopCount) { + container->m_shouldIterate = container->m_iterationCount < loopData->m_loopCount; + } else if (loopData->m_condition) { + container->m_shouldIterate = invokeHandler(container, loopData->m_condition, + container->m_iterationCount); + } + } + return container->m_shouldIterate; +} + +SetupResult TaskTreePrivate::start(RuntimeTask *node) +{ + if (!node->m_taskNode.isTask()) { + node->m_container.emplace(node->m_taskNode.m_container, node); + return start(&*node->m_container); + } + + const GroupItem::TaskHandler &handler = node->m_taskNode.m_taskHandler; + node->m_task.reset(handler.m_createHandler()); + const SetupResult startAction = handler.m_setupHandler + ? invokeHandler(node->m_parentIteration, handler.m_setupHandler, *node->m_task.get()) + : SetupResult::Continue; + if (startAction != SetupResult::Continue) { + if (node->m_parentIteration->m_isProgressive) + advanceProgress(1); + node->m_parentIteration->deleteChild(node); + return startAction; + } + const std::shared_ptr<SetupResult> unwindAction + = std::make_shared<SetupResult>(SetupResult::Continue); + QObject::connect(node->m_task.get(), &TaskInterface::done, + q, [this, node, unwindAction](DoneResult doneResult) { + const bool result = invokeDoneHandler(node, toDoneWith(doneResult)); + QObject::disconnect(node->m_task.get(), &TaskInterface::done, q, nullptr); + node->m_task.release()->deleteLater(); + RuntimeIteration *parentIteration = node->m_parentIteration; + parentIteration->deleteChild(node); + if (parentIteration->m_container->isStarting()) { + *unwindAction = toSetupResult(result); + } else { + childDone(parentIteration, result); + bumpAsyncCount(); + } + }); + + node->m_task->start(); + return *unwindAction; +} + +void TaskTreePrivate::stop(RuntimeTask *node) +{ + if (!node->m_task) { + if (!node->m_container) + return; + stop(&*node->m_container); + node->m_container->updateSuccessBit(false); + invokeDoneHandler(&*node->m_container, DoneWith::Cancel); + return; + } + + invokeDoneHandler(node, DoneWith::Cancel); + node->m_task.reset(); +} + +bool TaskTreePrivate::invokeDoneHandler(RuntimeTask *node, DoneWith doneWith) +{ + DoneResult result = toDoneResult(doneWith); + const GroupItem::TaskHandler &handler = node->m_taskNode.m_taskHandler; + if (handler.m_doneHandler && shouldCall(handler.m_callDoneIf, doneWith)) { + result = invokeHandler(node->m_parentIteration, + handler.m_doneHandler, *node->m_task.get(), doneWith); + } + if (node->m_parentIteration->m_isProgressive) + advanceProgress(1); + return result == DoneResult::Success; +} + +/*! + \class Tasking::TaskTree + \inheaderfile solutions/tasking/tasktree.h + \inmodule TaskingSolution + \brief The TaskTree class runs an async task tree structure defined in a declarative way. + \reentrant + + Use the Tasking namespace to build extensible, declarative task tree + structures that contain possibly asynchronous tasks, such as QProcess, + NetworkQuery, or ConcurrentCall<ReturnType>. TaskTree structures enable you + to create a sophisticated mixture of a parallel or sequential flow of tasks + in the form of a tree and to run it any time later. + + \section1 Root Element and Tasks + + The TaskTree has a mandatory Group root element, which may contain + any number of tasks of various types, such as QProcessTask, NetworkQueryTask, + or ConcurrentCallTask<ReturnType>: + + \code + using namespace Tasking; + + const Group root { + QProcessTask(...), + NetworkQueryTask(...), + ConcurrentCallTask<int>(...) + }; + + TaskTree *taskTree = new TaskTree(root); + connect(taskTree, &TaskTree::done, ...); // finish handler + taskTree->start(); + \endcode + + The task tree above has a top level element of the Group type that contains + tasks of the QProcessTask, NetworkQueryTask, and ConcurrentCallTask<int> type. + After taskTree->start() is called, the tasks are run in a chain, starting + with QProcessTask. When the QProcessTask finishes successfully, the NetworkQueryTask + task is started. Finally, when the network task finishes successfully, the + ConcurrentCallTask<int> task is started. + + When the last running task finishes with success, the task tree is considered + to have run successfully and the done() signal is emitted with DoneWith::Success. + When a task finishes with an error, the execution of the task tree is stopped + and the remaining tasks are skipped. The task tree finishes with an error and + sends the TaskTree::done() signal with DoneWith::Error. + + \section1 Groups + + The parent of the Group sees it as a single task. Like other tasks, + the group can be started and it can finish with success or an error. + The Group elements can be nested to create a tree structure: + + \code + const Group root { + Group { + parallel, + QProcessTask(...), + ConcurrentCallTask<int>(...) + }, + NetworkQueryTask(...) + }; + \endcode + + The example above differs from the first example in that the root element has + a subgroup that contains the QProcessTask and ConcurrentCallTask<int>. The subgroup is a + sibling element of the NetworkQueryTask in the root. The subgroup contains an + additional \e parallel element that instructs its Group to execute its tasks + in parallel. + + So, when the tree above is started, the QProcessTask and ConcurrentCallTask<int> start + immediately and run in parallel. Since the root group doesn't contain a + \e parallel element, its direct child tasks are run in sequence. Thus, the + NetworkQueryTask starts when the whole subgroup finishes. The group is + considered as finished when all its tasks have finished. The order in which + the tasks finish is not relevant. + + So, depending on which task lasts longer (QProcessTask or ConcurrentCallTask<int>), the + following scenarios can take place: + + \table + \header + \li Scenario 1 + \li Scenario 2 + \row + \li Root Group starts + \li Root Group starts + \row + \li Sub Group starts + \li Sub Group starts + \row + \li QProcessTask starts + \li QProcessTask starts + \row + \li ConcurrentCallTask<int> starts + \li ConcurrentCallTask<int> starts + \row + \li ... + \li ... + \row + \li \b {QProcessTask finishes} + \li \b {ConcurrentCallTask<int> finishes} + \row + \li ... + \li ... + \row + \li \b {ConcurrentCallTask<int> finishes} + \li \b {QProcessTask finishes} + \row + \li Sub Group finishes + \li Sub Group finishes + \row + \li NetworkQueryTask starts + \li NetworkQueryTask starts + \row + \li ... + \li ... + \row + \li NetworkQueryTask finishes + \li NetworkQueryTask finishes + \row + \li Root Group finishes + \li Root Group finishes + \endtable + + The differences between the scenarios are marked with bold. Three dots mean + that an unspecified amount of time passes between previous and next events + (a task or tasks continue to run). No dots between events + means that they occur synchronously. + + The presented scenarios assume that all tasks run successfully. If a task + fails during execution, the task tree finishes with an error. In particular, + when QProcessTask finishes with an error while ConcurrentCallTask<int> is still being executed, + the ConcurrentCallTask<int> is automatically canceled, the subgroup finishes with an error, + the NetworkQueryTask is skipped, and the tree finishes with an error. + + \section1 Task Types + + Each task type is associated with its corresponding task class that executes + the task. For example, a QProcessTask inside a task tree is associated with + the QProcess class that executes the process. The associated objects are + automatically created, started, and destructed exclusively by the task tree + at the appropriate time. + + If a root group consists of five sequential QProcessTask tasks, and the task tree + executes the group, it creates an instance of QProcess for the first + QProcessTask and starts it. If the QProcess instance finishes successfully, + the task tree destructs it and creates a new QProcess instance for the + second QProcessTask, and so on. If the first task finishes with an error, the task + tree stops creating QProcess instances, and the root group finishes with an + error. + + The following table shows examples of task types and their corresponding task + classes: + + \table + \header + \li Task Type (Tasking Namespace) + \li Associated Task Class + \li Brief Description + \row + \li QProcessTask + \li QProcess + \li Starts process. + \row + \li ConcurrentCallTask<ReturnType> + \li Tasking::ConcurrentCall<ReturnType> + \li Starts asynchronous task, runs in separate thread. + \row + \li TaskTreeTask + \li Tasking::TaskTree + \li Starts nested task tree. + \row + \li NetworkQueryTask + \li NetworkQuery + \li Starts network download. + \endtable + + \section1 Task Handlers + + Use Task handlers to set up a task for execution and to enable reading + the output data from the task when it finishes with success or an error. + + \section2 Task's Start Handler + + When a corresponding task class object is created and before it's started, + the task tree invokes an optionally user-provided setup handler. The setup + handler should always take a \e reference to the associated task class object: + + \code + const auto onSetup = [](QProcess &process) { + process.setCommand({"sleep", {"3"}}); + }; + const Group root { + QProcessTask(onSetup) + }; + \endcode + + You can modify the passed QProcess in the setup handler, so that the task + tree can start the process according to your configuration. + You should not call \c {process.start();} in the setup handler, + as the task tree calls it when needed. The setup handler is optional. When used, + it must be the first argument of the task's constructor. + + Optionally, the setup handler may return a SetupResult. The returned + SetupResult influences the further start behavior of a given task. The + possible values are: + + \table + \header + \li SetupResult Value + \li Brief Description + \row + \li Continue + \li The task will be started normally. This is the default behavior when the + setup handler doesn't return SetupResult (that is, its return type is + void). + \row + \li StopWithSuccess + \li The task won't be started and it will report success to its parent. + \row + \li StopWithError + \li The task won't be started and it will report an error to its parent. + \endtable + + This is useful for running a task only when a condition is met and the data + needed to evaluate this condition is not known until previously started tasks + finish. In this way, the setup handler dynamically decides whether to start the + corresponding task normally or skip it and report success or an error. + For more information about inter-task data exchange, see \l Storage. + + \section2 Task's Done Handler + + When a running task finishes, the task tree invokes an optionally provided done handler. + The handler should always take a \c const \e reference to the associated task class object: + + \code + const auto onSetup = [](QProcess &process) { + process.setCommand({"sleep", {"3"}}); + }; + const auto onDone = [](const QProcess &process, DoneWith result) { + if (result == DoneWith::Success) + qDebug() << "Success" << process.cleanedStdOut(); + else + qDebug() << "Failure" << process.cleanedStdErr(); + }; + const Group root { + QProcessTask(onSetup, onDone) + }; + \endcode + + The done handler may collect output data from QProcess, and store it + for further processing or perform additional actions. + + \note If the task setup handler returns StopWithSuccess or StopWithError, + the done handler is not invoked. + + \section1 Group Handlers + + Similarly to task handlers, group handlers enable you to set up a group to + execute and to apply more actions when the whole group finishes with + success or an error. + + \section2 Group's Start Handler + + The task tree invokes the group start handler before it starts the child + tasks. The group handler doesn't take any arguments: + + \code + const auto onSetup = [] { + qDebug() << "Entering the group"; + }; + const Group root { + onGroupSetup(onSetup), + QProcessTask(...) + }; + \endcode + + The group setup handler is optional. To define a group setup handler, add an + onGroupSetup() element to a group. The argument of onGroupSetup() is a user + handler. If you add more than one onGroupSetup() element to a group, an assert + is triggered at runtime that includes an error message. + + Like the task's start handler, the group start handler may return SetupResult. + The returned SetupResult value affects the start behavior of the + whole group. If you do not specify a group start handler or its return type + is void, the default group's action is SetupResult::Continue, so that all + tasks are started normally. Otherwise, when the start handler returns + SetupResult::StopWithSuccess or SetupResult::StopWithError, the tasks are not + started (they are skipped) and the group itself reports success or failure, + depending on the returned value, respectively. + + \code + const Group root { + onGroupSetup([] { qDebug() << "Root setup"; }), + Group { + onGroupSetup([] { qDebug() << "Group 1 setup"; return SetupResult::Continue; }), + QProcessTask(...) // Process 1 + }, + Group { + onGroupSetup([] { qDebug() << "Group 2 setup"; return SetupResult::StopWithSuccess; }), + QProcessTask(...) // Process 2 + }, + Group { + onGroupSetup([] { qDebug() << "Group 3 setup"; return SetupResult::StopWithError; }), + QProcessTask(...) // Process 3 + }, + QProcessTask(...) // Process 4 + }; + \endcode + + In the above example, all subgroups of a root group define their setup handlers. + The following scenario assumes that all started processes finish with success: + + \table + \header + \li Scenario + \li Comment + \row + \li Root Group starts + \li Doesn't return SetupResult, so its tasks are executed. + \row + \li Group 1 starts + \li Returns Continue, so its tasks are executed. + \row + \li Process 1 starts + \li + \row + \li ... + \li ... + \row + \li Process 1 finishes (success) + \li + \row + \li Group 1 finishes (success) + \li + \row + \li Group 2 starts + \li Returns StopWithSuccess, so Process 2 is skipped and Group 2 reports + success. + \row + \li Group 2 finishes (success) + \li + \row + \li Group 3 starts + \li Returns StopWithError, so Process 3 is skipped and Group 3 reports + an error. + \row + \li Group 3 finishes (error) + \li + \row + \li Root Group finishes (error) + \li Group 3, which is a direct child of the root group, finished with an + error, so the root group stops executing, skips Process 4, which has + not started yet, and reports an error. + \endtable + + \section2 Groups's Done Handler + + A Group's done handler is executed after the successful or failed execution of its tasks. + The final value reported by the group depends on its \l {Workflow Policy}. + The handler can apply other necessary actions. + The done handler is defined inside the onGroupDone() element of a group. + It may take the optional DoneWith argument, indicating the successful or failed execution: + + \code + const Group root { + onGroupSetup([] { qDebug() << "Root setup"; }), + QProcessTask(...), + onGroupDone([](DoneWith result) { + if (result == DoneWith::Success) + qDebug() << "Root finished with success"; + else + qDebug() << "Root finished with an error"; + }) + }; + \endcode + + The group done handler is optional. If you add more than one onGroupDone() to a group, + an assert is triggered at runtime that includes an error message. + + \note Even if the group setup handler returns StopWithSuccess or StopWithError, + the group's done handler is invoked. This behavior differs from that of task done handler + and might change in the future. + + \section1 Other Group Elements + + A group can contain other elements that describe the processing flow, such as + the execution mode or workflow policy. It can also contain storage elements + that are responsible for collecting and sharing custom common data gathered + during group execution. + + \section2 Execution Mode + + The execution mode element in a Group specifies how the direct child tasks of + the Group are started. The most common execution modes are \l sequential and + \l parallel. It's also possible to specify the limit of tasks running + in parallel by using the parallelLimit() function. + + In all execution modes, a group starts tasks in the oder in which they appear. + + If a child of a group is also a group, the child group runs its tasks + according to its own execution mode. + + \section2 Workflow Policy + + The workflow policy element in a Group specifies how the group should behave + when any of its \e direct child's tasks finish. For a detailed description of possible + policies, refer to WorkflowPolicy. + + If a child of a group is also a group, the child group runs its tasks + according to its own workflow policy. + + \section2 Storage + + Use the \l {Tasking::Storage} {Storage} element to exchange information between tasks. + Especially, in the sequential execution mode, when a task needs data from another, + already finished task, before it can start. For example, a task tree that copies data by reading + it from a source and writing it to a destination might look as follows: + + \code + static QByteArray load(const QString &fileName) { ... } + static void save(const QString &fileName, const QByteArray &array) { ... } + + static Group copyRecipe(const QString &source, const QString &destination) + { + struct CopyStorage { // [1] custom inter-task struct + QByteArray content; // [2] custom inter-task data + }; + + // [3] instance of custom inter-task struct manageable by task tree + const Storage<CopyStorage> storage; + + const auto onLoaderSetup = [source](ConcurrentCall<QByteArray> &async) { + async.setConcurrentCallData(&load, source); + }; + // [4] runtime: task tree activates the instance from [7] before invoking handler + const auto onLoaderDone = [storage](const ConcurrentCall<QByteArray> &async) { + storage->content = async.result(); // [5] loader stores the result in storage + }; + + // [4] runtime: task tree activates the instance from [7] before invoking handler + const auto onSaverSetup = [storage, destination](ConcurrentCall<void> &async) { + const QByteArray content = storage->content; // [6] saver takes data from storage + async.setConcurrentCallData(&save, destination, content); + }; + const auto onSaverDone = [](const ConcurrentCall<void> &async) { + qDebug() << "Save done successfully"; + }; + + const Group root { + // [7] runtime: task tree creates an instance of CopyStorage when root is entered + storage, + ConcurrentCallTask<QByteArray>(onLoaderSetup, onLoaderDone, CallDoneIf::Success), + ConcurrentCallTask<void>(onSaverSetup, onSaverDone, CallDoneIf::Success) + }; + return root; + } + + const QString source = ...; + const QString destination = ...; + TaskTree taskTree(copyRecipe(source, destination)); + connect(&taskTree, &TaskTree::done, + &taskTree, [](DoneWith result) { + if (result == DoneWith::Success) + qDebug() << "The copying finished successfully."; + }); + tasktree.start(); + \endcode + + In the example above, the inter-task data consists of a QByteArray content + variable [2] enclosed in a \c CopyStorage custom struct [1]. If the loader + finishes successfully, it stores the data in a \c CopyStorage::content + variable [5]. The saver then uses the variable to configure the saving task [6]. + + To enable a task tree to manage the \c CopyStorage struct, an instance of + \l {Tasking::Storage} {Storage}<\c CopyStorage> is created [3]. If a copy of this object is + inserted as the group's child item [7], an instance of the \c CopyStorage struct is + created dynamically when the task tree enters this group. When the task + tree leaves this group, the existing instance of the \c CopyStorage struct is + destructed as it's no longer needed. + + If several task trees holding a copy of the common + \l {Tasking::Storage} {Storage}<\c CopyStorage> instance run simultaneously + (including the case when the task trees are run in different threads), + each task tree contains its own copy of the \c CopyStorage struct. + + You can access \c CopyStorage from any handler in the group with a storage object. + This includes all handlers of all descendant tasks of the group with + a storage object. To access the custom struct in a handler, pass the + copy of the \l {Tasking::Storage} {Storage}<\c CopyStorage> object to the handler + (for example, in a lambda capture) [4]. + + When the task tree invokes a handler in a subtree containing the storage [7], + the task tree activates its own \c CopyStorage instance inside the + \l {Tasking::Storage} {Storage}<\c CopyStorage> object. Therefore, the \c CopyStorage struct + may be accessed only from within the handler body. To access the currently active + \c CopyStorage from within \l {Tasking::Storage} {Storage}<\c CopyStorage>, use the + \l {Tasking::Storage::operator->()} {Storage::operator->()}, + \l {Tasking::Storage::operator*()} {Storage::operator*()}, or Storage::activeStorage() method. + + The following list summarizes how to employ a Storage object into the task + tree: + \list 1 + \li Define the custom structure \c MyStorage with custom data [1], [2] + \li Create an instance of the \l {Tasking::Storage} {Storage}<\c MyStorage> storage [3] + \li Pass the \l {Tasking::Storage} {Storage}<\c MyStorage> instance to handlers [4] + \li Access the \c MyStorage instance in handlers [5], [6] + \li Insert the \l {Tasking::Storage} {Storage}<\c MyStorage> instance into a group [7] + \endlist + + \section1 TaskTree class + + TaskTree executes the tree structure of asynchronous tasks according to the + recipe described by the Group root element. + + As TaskTree is also an asynchronous task, it can be a part of another TaskTree. + To place a nested TaskTree inside another TaskTree, insert the TaskTreeTask + element into another Group element. + + TaskTree reports progress of completed tasks when running. The progress value + is increased when a task finishes or is skipped or canceled. + When TaskTree is finished and the TaskTree::done() signal is emitted, + the current value of the progress equals the maximum progress value. + Maximum progress equals the total number of asynchronous tasks in a tree. + A nested TaskTree is counted as a single task, and its child tasks are not + counted in the top level tree. Groups themselves are not counted as tasks, + but their tasks are counted. \l {Tasking::Sync} {Sync} tasks are not asynchronous, + so they are not counted as tasks. + + To set additional initial data for the running tree, modify the storage + instances in a tree when it creates them by installing a storage setup + handler: + + \code + Storage<CopyStorage> storage; + const Group root = ...; // storage placed inside root's group and inside handlers + TaskTree taskTree(root); + auto initStorage = [](CopyStorage &storage) { + storage.content = "initial content"; + }; + taskTree.onStorageSetup(storage, initStorage); + taskTree.start(); + \endcode + + When the running task tree creates a \c CopyStorage instance, and before any + handler inside a tree is called, the task tree calls the initStorage handler, + to enable setting up initial data of the storage, unique to this particular + run of taskTree. + + Similarly, to collect some additional result data from the running tree, + read it from storage instances in the tree when they are about to be + destroyed. To do this, install a storage done handler: + + \code + Storage<CopyStorage> storage; + const Group root = ...; // storage placed inside root's group and inside handlers + TaskTree taskTree(root); + auto collectStorage = [](const CopyStorage &storage) { + qDebug() << "final content" << storage.content; + }; + taskTree.onStorageDone(storage, collectStorage); + taskTree.start(); + \endcode + + When the running task tree is about to destroy a \c CopyStorage instance, the + task tree calls the collectStorage handler, to enable reading the final data + from the storage, unique to this particular run of taskTree. + + \section1 Task Adapters + + To extend a TaskTree with a new task type, implement a simple adapter class + derived from the TaskAdapter class template. The following class is an + adapter for a single shot timer, which may be considered as a new asynchronous task: + + \code + class TimerTaskAdapter : public TaskAdapter<QTimer> + { + public: + TimerTaskAdapter() { + task()->setSingleShot(true); + task()->setInterval(1000); + connect(task(), &QTimer::timeout, this, [this] { emit done(DoneResult::Success); }); + } + private: + void start() final { task()->start(); } + }; + + using TimerTask = CustomTask<TimerTaskAdapter>; + \endcode + + You must derive the custom adapter from the TaskAdapter class template + instantiated with a template parameter of the class implementing a running + task. The code above uses QTimer to run the task. This class appears + later as an argument to the task's handlers. The instance of this class + parameter automatically becomes a member of the TaskAdapter template, and is + accessible through the TaskAdapter::task() method. The constructor + of \c TimerTaskAdapter initially configures the QTimer object and connects + to the QTimer::timeout() signal. When the signal is triggered, \c TimerTaskAdapter + emits the TaskInterface::done(DoneResult::Success) signal to inform the task tree that + the task finished successfully. If it emits TaskInterface::done(DoneResult::Error), + the task finished with an error. + The TaskAdapter::start() method starts the timer. + + To make QTimer accessible inside TaskTree under the \c TimerTask name, + define \c TimerTask to be an alias to the CustomTask<\c TimerTaskAdapter>. + \c TimerTask becomes a new custom task type, using \c TimerTaskAdapter. + + The new task type is now registered, and you can use it in TaskTree: + + \code + const auto onSetup = [](QTimer &task) { task.setInterval(2000); }; + const auto onDone = [] { qDebug() << "timer triggered"; }; + const Group root { + TimerTask(onSetup, onDone) + }; + \endcode + + When a task tree containing the root from the above example is started, it + prints a debug message within two seconds and then finishes successfully. + + \note The class implementing the running task should have a default constructor, + and objects of this class should be freely destructible. It should be allowed + to destroy a running object, preferably without waiting for the running task + to finish (that is, safe non-blocking destructor of a running task). + To achieve a non-blocking destruction of a task that has a blocking destructor, + consider using the optional \c Deleter template parameter of the TaskAdapter. +*/ + +/*! + Constructs an empty task tree. Use setRecipe() to pass a declarative description + on how the task tree should execute the tasks and how it should handle the finished tasks. + + Starting an empty task tree is no-op and the relevant warning message is issued. + + \sa setRecipe(), start() +*/ +TaskTree::TaskTree() + : d(new TaskTreePrivate(this)) +{} + +/*! + \overload + + Constructs a task tree with a given \a recipe. After the task tree is started, + it executes the tasks contained inside the \a recipe and + handles finished tasks according to the passed description. + + \sa setRecipe(), start() +*/ +TaskTree::TaskTree(const Group &recipe) : TaskTree() +{ + setRecipe(recipe); +} + +/*! + Destroys the task tree. + + When the task tree is running while being destructed, it cancels all the running tasks + immediately. In this case, no handlers are called, not even the groups' and + tasks' done handlers or onStorageDone() handlers. The task tree also doesn't emit any + signals from the destructor, not even done() or progressValueChanged() signals. + This behavior may always be relied on. + It is completely safe to destruct the running task tree. + + It's a usual pattern to destruct the running task tree. + It's guaranteed that the destruction will run quickly, without having to wait for + the currently running tasks to finish, provided that the used tasks implement + their destructors in a non-blocking way. + + \note Do not call the destructor directly from any of the running task's handlers + or task tree's signals. In these cases, use \l deleteLater() instead. + + \sa cancel() +*/ +TaskTree::~TaskTree() +{ + QT_ASSERT(!d->m_guard.isLocked(), qWarning("Deleting TaskTree instance directly from " + "one of its handlers will lead to a crash!")); + // TODO: delete storages explicitly here? + delete d; +} + +/*! + Sets a given \a recipe for the task tree. After the task tree is started, + it executes the tasks contained inside the \a recipe and + handles finished tasks according to the passed description. + + \note When called for a running task tree, the call is ignored. + + \sa TaskTree(const Tasking::Group &recipe), start() +*/ +void TaskTree::setRecipe(const Group &recipe) +{ + QT_ASSERT(!isRunning(), qWarning("The TaskTree is already running, ignoring..."); return); + QT_ASSERT(!d->m_guard.isLocked(), qWarning("The setRecipe() is called from one of the" + "TaskTree handlers, ignoring..."); return); + // TODO: Should we clear the m_storageHandlers, too? + d->m_storages.clear(); + d->m_root.emplace(d, recipe); +} + +/*! + Starts the task tree. + + Use setRecipe() or the constructor to set the declarative description according to which + the task tree will execute the contained tasks and handle finished tasks. + + When the task tree is empty, that is, constructed with a default constructor, + a call to \c start() is no-op and the relevant warning message is issued. + + Otherwise, when the task tree is already running, a call to \e start() is ignored and the + relevant warning message is issued. + + Otherwise, the task tree is started. + + The started task tree may finish synchronously, + for example when the main group's start handler returns SetupResult::StopWithError. + For this reason, the connection to the done signal should be established before calling + \c start(). Use isRunning() in order to detect whether the task tree is still running + after a call to \c start(). + + The task tree implementation relies on the running event loop. + Make sure you have a QEventLoop or QCoreApplication or one of its + subclasses running (or about to be run) when calling this method. + + \sa TaskTree(const Tasking::Group &), setRecipe(), isRunning(), cancel() +*/ +void TaskTree::start() +{ + QT_ASSERT(!isRunning(), qWarning("The TaskTree is already running, ignoring..."); return); + QT_ASSERT(!d->m_guard.isLocked(), qWarning("The start() is called from one of the" + "TaskTree handlers, ignoring..."); return); + d->start(); +} + +/*! + \fn void TaskTree::started() + + This signal is emitted when the task tree is started. The emission of this signal is + followed synchronously by the progressValueChanged() signal with an initial \c 0 value. + + \sa start(), done() +*/ + +/*! + \fn void TaskTree::done(DoneWith result) + + This signal is emitted when the task tree finished, passing the final \a result + of the execution. The task tree neither calls any handler, + nor emits any signal anymore after this signal was emitted. + + \note Do not delete the task tree directly from this signal's handler. + Use deleteLater() instead. + + \sa started() +*/ + +/*! + Cancels the execution of the running task tree. + + Cancels all the running tasks immediately. + All running tasks finish with an error, invoking their error handlers. + All running groups dispatch their handlers according to their workflow policies, + invoking their done handlers. The storages' onStorageDone() handlers are invoked, too. + The progressValueChanged() signals are also being sent. + This behavior may always be relied on. + + The \c cancel() function is executed synchronously, so that after a call to \c cancel() + all running tasks are finished and the tree is already canceled. + It's guaranteed that \c cancel() will run quickly, without any blocking wait for + the currently running tasks to finish, provided the used tasks implement their destructors + in a non-blocking way. + + When the task tree is empty, that is, constructed with a default constructor, + a call to \c cancel() is no-op and the relevant warning message is issued. + + Otherwise, when the task tree wasn't started, a call to \c cancel() is ignored. + + \note Do not call this function directly from any of the running task's handlers + or task tree's signals. + + \sa ~TaskTree() +*/ +void TaskTree::cancel() +{ + QT_ASSERT(!d->m_guard.isLocked(), qWarning("The cancel() is called from one of the" + "TaskTree handlers, ignoring..."); return); + d->stop(); +} + +/*! + Returns \c true if the task tree is currently running; otherwise returns \c false. + + \sa start(), cancel() +*/ +bool TaskTree::isRunning() const +{ + return bool(d->m_runtimeRoot); +} + +/*! + Executes a local event loop with QEventLoop::ExcludeUserInputEvents and starts the task tree. + + Returns DoneWith::Success if the task tree finished successfully; + otherwise returns DoneWith::Error. + + \note Avoid using this method from the main thread. Use asynchronous start() instead. + This method is to be used in non-main threads or in auto tests. + + \sa start() +*/ +DoneWith TaskTree::runBlocking() +{ + QPromise<void> dummy; + dummy.start(); + return runBlocking(dummy.future()); +} + +/*! + \overload runBlocking() + + The passed \a future is used for listening to the cancel event. + When the task tree is canceled, this method cancels the passed \a future. +*/ +DoneWith TaskTree::runBlocking(const QFuture<void> &future) +{ + if (future.isCanceled()) + return DoneWith::Cancel; + + DoneWith doneWith = DoneWith::Cancel; + QEventLoop loop; + connect(this, &TaskTree::done, &loop, [&loop, &doneWith](DoneWith result) { + doneWith = result; + // Otherwise, the tasks from inside the running tree that were deleteLater() + // will be leaked. Refer to the QObject::deleteLater() docs. + QMetaObject::invokeMethod(&loop, [&loop] { loop.quit(); }, Qt::QueuedConnection); + }); + QFutureWatcher<void> watcher; + connect(&watcher, &QFutureWatcherBase::canceled, this, &TaskTree::cancel); + watcher.setFuture(future); + + QTimer::singleShot(0, this, &TaskTree::start); + + loop.exec(QEventLoop::ExcludeUserInputEvents); + if (doneWith == DoneWith::Cancel) { + auto nonConstFuture = future; + nonConstFuture.cancel(); + } + return doneWith; +} + +/*! + Constructs a temporary task tree using the passed \a recipe and runs it blocking. + + The optionally provided \a timeout is used to cancel the tree automatically after + \a timeout milliseconds have passed. + + Returns DoneWith::Success if the task tree finished successfully; + otherwise returns DoneWith::Error. + + \note Avoid using this method from the main thread. Use asynchronous start() instead. + This method is to be used in non-main threads or in auto tests. + + \sa start() +*/ +DoneWith TaskTree::runBlocking(const Group &recipe, milliseconds timeout) +{ + QPromise<void> dummy; + dummy.start(); + return TaskTree::runBlocking(recipe, dummy.future(), timeout); +} + +/*! + \overload runBlocking(const Group &recipe, milliseconds timeout) + + The passed \a future is used for listening to the cancel event. + When the task tree is canceled, this method cancels the passed \a future. +*/ +DoneWith TaskTree::runBlocking(const Group &recipe, const QFuture<void> &future, milliseconds timeout) +{ + const Group root = timeout == milliseconds::max() ? recipe + : Group { recipe.withTimeout(timeout) }; + TaskTree taskTree(root); + return taskTree.runBlocking(future); +} + +/*! + Returns the current real count of asynchronous chains of invocations. + + The returned value indicates how many times the control returns to the caller's + event loop while the task tree is running. Initially, this value is 0. + If the execution of the task tree finishes fully synchronously, this value remains 0. + If the task tree contains any asynchronous tasks that are successfully started during + a call to start(), this value is bumped to 1 just before the call to start() finishes. + Later, when any asynchronous task finishes and any possible continuations are started, + this value is bumped again. The bumping continues until the task tree finishes. + When the task tree emits the done() signal, the bumping stops. + The asyncCountChanged() signal is emitted on every bump of this value. + + \sa asyncCountChanged() +*/ +int TaskTree::asyncCount() const +{ + return d->m_asyncCount; +} + +/*! + \fn void TaskTree::asyncCountChanged(int count) + + This signal is emitted when the running task tree is about to return control to the caller's + event loop. When the task tree is started, this signal is emitted with \a count value of 0, + and emitted later on every asyncCount() value bump with an updated \a count value. + Every signal sent (except the initial one with the value of 0) guarantees that the task tree + is still running asynchronously after the emission. + + \sa asyncCount() +*/ + +/*! + Returns the number of asynchronous tasks contained in the stored recipe. + + \note The returned number doesn't include \l {Tasking::Sync} {Sync} tasks. + \note Any task or group that was set up using withTimeout() increases the total number of + tasks by \c 1. + + \sa setRecipe(), progressMaximum() +*/ +int TaskTree::taskCount() const +{ + return d->m_root ? d->m_root->taskCount() : 0; +} + +/*! + \fn void TaskTree::progressValueChanged(int value) + + This signal is emitted when the running task tree finished, canceled, or skipped some tasks. + The \a value gives the current total number of finished, canceled or skipped tasks. + When the task tree is started, and after the started() signal was emitted, + this signal is emitted with an initial \a value of \c 0. + When the task tree is about to finish, and before the done() signal is emitted, + this signal is emitted with the final \a value of progressMaximum(). + + \sa progressValue(), progressMaximum() +*/ + +/*! + \fn int TaskTree::progressMaximum() const + + Returns the maximum progressValue(). + + \note Currently, it's the same as taskCount(). This might change in the future. + + \sa progressValue() +*/ + +/*! + Returns the current progress value, which is between the \c 0 and progressMaximum(). + + The returned number indicates how many tasks have been already finished, canceled, or skipped + while the task tree is running. + When the task tree is started, this number is set to \c 0. + When the task tree is finished, this number always equals progressMaximum(). + + \sa progressMaximum(), progressValueChanged() +*/ +int TaskTree::progressValue() const +{ + return d->m_progressValue; +} + +/*! + \fn template <typename StorageStruct, typename Handler> void TaskTree::onStorageSetup(const Storage<StorageStruct> &storage, Handler &&handler) + + Installs a storage setup \a handler for the \a storage to pass the initial data + dynamically to the running task tree. + + The \c StorageHandler takes a \e reference to the \c StorageStruct instance: + + \code + static void save(const QString &fileName, const QByteArray &array) { ... } + + Storage<QByteArray> storage; + + const auto onSaverSetup = [storage](ConcurrentCall<QByteArray> &concurrent) { + concurrent.setConcurrentCallData(&save, "foo.txt", *storage); + }; + + const Group root { + storage, + ConcurrentCallTask(onSaverSetup) + }; + + TaskTree taskTree(root); + auto initStorage = [](QByteArray &storage){ + storage = "initial content"; + }; + taskTree.onStorageSetup(storage, initStorage); + taskTree.start(); + \endcode + + When the running task tree enters a Group where the \a storage is placed in, + it creates a \c StorageStruct instance, ready to be used inside this group. + Just after the \c StorageStruct instance is created, and before any handler of this group + is called, the task tree invokes the passed \a handler. This enables setting up + initial content for the given storage dynamically. Later, when any group's handler is invoked, + the task tree activates the created and initialized storage, so that it's available inside + any group's handler. + + \sa onStorageDone() +*/ + +/*! + \fn template <typename StorageStruct, typename Handler> void TaskTree::onStorageDone(const Storage<StorageStruct> &storage, Handler &&handler) + + Installs a storage done \a handler for the \a storage to retrieve the final data + dynamically from the running task tree. + + The \c StorageHandler takes a \c const \e reference to the \c StorageStruct instance: + + \code + static QByteArray load(const QString &fileName) { ... } + + Storage<QByteArray> storage; + + const auto onLoaderSetup = [](ConcurrentCall<QByteArray> &concurrent) { + concurrent.setConcurrentCallData(&load, "foo.txt"); + }; + const auto onLoaderDone = [storage](const ConcurrentCall<QByteArray> &concurrent) { + *storage = concurrent.result(); + }; + + const Group root { + storage, + ConcurrentCallTask(onLoaderSetup, onLoaderDone, CallDoneIf::Success) + }; + + TaskTree taskTree(root); + auto collectStorage = [](const QByteArray &storage){ + qDebug() << "final content" << storage; + }; + taskTree.onStorageDone(storage, collectStorage); + taskTree.start(); + \endcode + + When the running task tree is about to leave a Group where the \a storage is placed in, + it destructs a \c StorageStruct instance. + Just before the \c StorageStruct instance is destructed, and after all possible handlers from + this group were called, the task tree invokes the passed \a handler. This enables reading + the final content of the given storage dynamically and processing it further outside of + the task tree. + + This handler is called also when the running tree is canceled. However, it's not called + when the running tree is destructed. + + \sa onStorageSetup() +*/ + +void TaskTree::setupStorageHandler(const StorageBase &storage, + StorageBase::StorageHandler setupHandler, + StorageBase::StorageHandler doneHandler) +{ + auto it = d->m_storageHandlers.find(storage); + if (it == d->m_storageHandlers.end()) { + d->m_storageHandlers.insert(storage, {setupHandler, doneHandler}); + return; + } + if (setupHandler) { + QT_ASSERT(!it->m_setupHandler, + qWarning("The storage has its setup handler defined, overriding...")); + it->m_setupHandler = setupHandler; + } + if (doneHandler) { + QT_ASSERT(!it->m_doneHandler, + qWarning("The storage has its done handler defined, overriding...")); + it->m_doneHandler = doneHandler; + } +} + +TaskTreeTaskAdapter::TaskTreeTaskAdapter() +{ + connect(task(), &TaskTree::done, this, + [this](DoneWith result) { emit done(toDoneResult(result)); }); +} + +void TaskTreeTaskAdapter::start() +{ + task()->start(); +} + +using TimeoutCallback = std::function<void()>; + +struct TimerData +{ + system_clock::time_point m_deadline; + QPointer<QObject> m_context; + TimeoutCallback m_callback; +}; + +struct TimerThreadData +{ + Q_DISABLE_COPY_MOVE(TimerThreadData) + + TimerThreadData() = default; // defult constructor is required for initializing with {} since C++20 by Mingw 11.20 + QHash<int, TimerData> m_timerIdToTimerData = {}; + QMap<system_clock::time_point, QList<int>> m_deadlineToTimerId = {}; + int m_timerIdCounter = 0; +}; + +// Please note the thread_local keyword below guarantees a separate instance per thread. +static thread_local TimerThreadData s_threadTimerData = {}; + +static void removeTimerId(int timerId) +{ + const auto it = s_threadTimerData.m_timerIdToTimerData.constFind(timerId); + QT_ASSERT(it != s_threadTimerData.m_timerIdToTimerData.cend(), + qWarning("Removing active timerId failed."); return); + + const system_clock::time_point deadline = it->m_deadline; + s_threadTimerData.m_timerIdToTimerData.erase(it); + + QList<int> &ids = s_threadTimerData.m_deadlineToTimerId[deadline]; + const int removedCount = ids.removeAll(timerId); + QT_ASSERT(removedCount == 1, qWarning("Removing active timerId failed."); return); + if (ids.isEmpty()) + s_threadTimerData.m_deadlineToTimerId.remove(deadline); +} + +static void handleTimeout(int timerId) +{ + const auto itData = s_threadTimerData.m_timerIdToTimerData.constFind(timerId); + if (itData == s_threadTimerData.m_timerIdToTimerData.cend()) + return; // The timer was already activated. + + const auto deadline = itData->m_deadline; + while (true) { + auto itMap = s_threadTimerData.m_deadlineToTimerId.begin(); + if (itMap == s_threadTimerData.m_deadlineToTimerId.end()) + return; + + if (itMap.key() > deadline) + return; + + std::optional<TimerData> timerData; + QList<int> &idList = *itMap; + if (!idList.isEmpty()) { + const int first = idList.first(); + idList.removeFirst(); + + const auto it = s_threadTimerData.m_timerIdToTimerData.constFind(first); + if (it != s_threadTimerData.m_timerIdToTimerData.cend()) { + timerData = it.value(); + s_threadTimerData.m_timerIdToTimerData.erase(it); + } else { + QT_CHECK(false); + } + } else { + QT_CHECK(false); + } + + if (idList.isEmpty()) + s_threadTimerData.m_deadlineToTimerId.erase(itMap); + if (timerData && timerData->m_context) + timerData->m_callback(); + } +} + +static int scheduleTimeout(milliseconds timeout, QObject *context, const TimeoutCallback &callback) +{ + const int timerId = ++s_threadTimerData.m_timerIdCounter; + const system_clock::time_point deadline = system_clock::now() + timeout; + QTimer::singleShot(timeout, context, [timerId] { handleTimeout(timerId); }); + s_threadTimerData.m_timerIdToTimerData.emplace(timerId, TimerData{deadline, context, callback}); + s_threadTimerData.m_deadlineToTimerId[deadline].append(timerId); + return timerId; +} + +TimeoutTaskAdapter::TimeoutTaskAdapter() +{ + *task() = milliseconds::zero(); +} + +TimeoutTaskAdapter::~TimeoutTaskAdapter() +{ + if (m_timerId) + removeTimerId(*m_timerId); +} + +void TimeoutTaskAdapter::start() +{ + m_timerId = scheduleTimeout(*task(), this, [this] { + m_timerId.reset(); + emit done(DoneResult::Success); + }); +} + +/*! + \typealias TaskTreeTask + + Type alias for the CustomTask, to be used inside recipes, associated with the TaskTree task. +*/ + +/*! + \typealias TimeoutTask + + Type alias for the CustomTask, to be used inside recipes, associated with the + \c std::chrono::milliseconds type. \c std::chrono::milliseconds is used to set up the + timeout duration. The default timeout is \c std::chrono::milliseconds::zero(), that is, + the TimeoutTask finishes as soon as the control returns to the running event loop. + + Example usage: + + \code + using namespace std::chrono; + using namespace std::chrono_literals; + + const auto onSetup = [](milliseconds &timeout) { timeout = 1000ms; } + const auto onDone = [] { qDebug() << "Timed out."; } + + const Group root { + Timeout(onSetup, onDone) + }; + \endcode +*/ + +} // namespace Tasking + +QT_END_NAMESPACE diff --git a/src/assets/downloader/tasking/tasktree.h b/src/assets/downloader/tasking/tasktree.h new file mode 100644 index 0000000000..fa5f5188df --- /dev/null +++ b/src/assets/downloader/tasking/tasktree.h @@ -0,0 +1,642 @@ +// Copyright (C) 2024 Jarek Kobus +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef TASKING_TASKTREE_H +#define TASKING_TASKTREE_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "tasking_global.h" + +#include <QtCore/QList> +#include <QtCore/QObject> + +#include <memory> + +QT_BEGIN_NAMESPACE +template <class T> +class QFuture; + +namespace Tasking { + +Q_NAMESPACE + +// WorkflowPolicy: +// 1. When all children finished with success -> report success, otherwise: +// a) Report error on first error and stop executing other children (including their subtree). +// b) On first error - continue executing all children and report error afterwards. +// 2. When all children finished with error -> report error, otherwise: +// a) Report success on first success and stop executing other children (including their subtree). +// b) On first success - continue executing all children and report success afterwards. +// 3. Stops on first finished child. In sequential mode it will never run other children then the first one. +// Useful only in parallel mode. +// 4. Always run all children, let them finish, ignore their results and report success afterwards. +// 5. Always run all children, let them finish, ignore their results and report error afterwards. + +enum class WorkflowPolicy +{ + StopOnError, // 1a - Reports error on first child error, otherwise success (if all children were success). + ContinueOnError, // 1b - The same, but children execution continues. Reports success when no children. + StopOnSuccess, // 2a - Reports success on first child success, otherwise error (if all children were error). + ContinueOnSuccess, // 2b - The same, but children execution continues. Reports error when no children. + StopOnSuccessOrError, // 3 - Stops on first finished child and report its result. + FinishAllAndSuccess, // 4 - Reports success after all children finished. + FinishAllAndError // 5 - Reports error after all children finished. +}; +Q_ENUM_NS(WorkflowPolicy) + +enum class SetupResult +{ + Continue, + StopWithSuccess, + StopWithError +}; +Q_ENUM_NS(SetupResult) + +enum class DoneResult +{ + Success, + Error +}; +Q_ENUM_NS(DoneResult) + +enum class DoneWith +{ + Success, + Error, + Cancel +}; +Q_ENUM_NS(DoneWith) + +enum class CallDoneIf +{ + SuccessOrError, + Success, + Error +}; +Q_ENUM_NS(CallDoneIf) + +TASKING_EXPORT DoneResult toDoneResult(bool success); + +class LoopData; +class StorageData; +class TaskTreePrivate; + +class TASKING_EXPORT TaskInterface : public QObject +{ + Q_OBJECT + +Q_SIGNALS: + void done(DoneResult result); + +private: + template <typename Task, typename Deleter> friend class TaskAdapter; + friend class TaskTreePrivate; + TaskInterface() = default; +#ifdef Q_QDOC +protected: +#endif + virtual void start() = 0; +}; + +class TASKING_EXPORT Loop +{ +public: + using Condition = std::function<bool(int)>; // Takes iteration, called prior to each iteration. + using ValueGetter = std::function<const void *(int)>; // Takes iteration, returns ptr to ref. + + int iteration() const; + +protected: + Loop(); // LoopForever + Loop(int count, const ValueGetter &valueGetter = {}); // LoopRepeat, LoopList + Loop(const Condition &condition); // LoopUntil + + const void *valuePtr() const; + +private: + friend class ExecutionContextActivator; + friend class TaskTreePrivate; + std::shared_ptr<LoopData> m_loopData; +}; + +class TASKING_EXPORT LoopForever final : public Loop +{ +public: + LoopForever() : Loop() {} +}; + +class TASKING_EXPORT LoopRepeat final : public Loop +{ +public: + LoopRepeat(int count) : Loop(count) {} +}; + +class TASKING_EXPORT LoopUntil final : public Loop +{ +public: + LoopUntil(const Condition &condition) : Loop(condition) {} +}; + +template <typename T> +class LoopList final : public Loop +{ +public: + LoopList(const QList<T> &list) : Loop(list.size(), [list](int i) { return &list.at(i); }) {} + const T *operator->() const { return static_cast<const T *>(valuePtr()); } + const T &operator*() const { return *static_cast<const T *>(valuePtr()); } +}; + +class TASKING_EXPORT StorageBase +{ +private: + using StorageConstructor = std::function<void *(void)>; + using StorageDestructor = std::function<void(void *)>; + using StorageHandler = std::function<void(void *)>; + + StorageBase(const StorageConstructor &ctor, const StorageDestructor &dtor); + + void *activeStorageVoid() const; + + friend bool operator==(const StorageBase &first, const StorageBase &second) + { return first.m_storageData == second.m_storageData; } + + friend bool operator!=(const StorageBase &first, const StorageBase &second) + { return first.m_storageData != second.m_storageData; } + + friend size_t qHash(const StorageBase &storage, uint seed = 0) + { return size_t(storage.m_storageData.get()) ^ seed; } + + std::shared_ptr<StorageData> m_storageData; + + template <typename StorageStruct> friend class Storage; + friend class ExecutionContextActivator; + friend class StorageData; + friend class RuntimeContainer; + friend class TaskTree; + friend class TaskTreePrivate; +}; + +template <typename StorageStruct> +class Storage final : public StorageBase +{ +public: + Storage() : StorageBase(Storage::ctor(), Storage::dtor()) {} + StorageStruct &operator*() const noexcept { return *activeStorage(); } + StorageStruct *operator->() const noexcept { return activeStorage(); } + StorageStruct *activeStorage() const { + return static_cast<StorageStruct *>(activeStorageVoid()); + } + +private: + static StorageConstructor ctor() { return [] { return new StorageStruct(); }; } + static StorageDestructor dtor() { + return [](void *storage) { delete static_cast<StorageStruct *>(storage); }; + } +}; + +class TASKING_EXPORT GroupItem +{ +public: + // Called when group entered, after group's storages are created + using GroupSetupHandler = std::function<SetupResult()>; + // Called when group done, before group's storages are deleted + using GroupDoneHandler = std::function<DoneResult(DoneWith)>; + + template <typename StorageStruct> + GroupItem(const Storage<StorageStruct> &storage) + : m_type(Type::Storage) + , m_storageList{storage} {} + + GroupItem(const Loop &loop) : GroupItem(GroupData{{}, {}, {}, loop}) {} + + // TODO: Add tests. + GroupItem(const QList<GroupItem> &children) : m_type(Type::List) { addChildren(children); } + GroupItem(std::initializer_list<GroupItem> children) : m_type(Type::List) { addChildren(children); } + +protected: + // Internal, provided by CustomTask + using InterfaceCreateHandler = std::function<TaskInterface *(void)>; + // Called prior to task start, just after createHandler + using InterfaceSetupHandler = std::function<SetupResult(TaskInterface &)>; + // Called on task done, just before deleteLater + using InterfaceDoneHandler = std::function<DoneResult(const TaskInterface &, DoneWith)>; + + struct TaskHandler { + InterfaceCreateHandler m_createHandler; + InterfaceSetupHandler m_setupHandler = {}; + InterfaceDoneHandler m_doneHandler = {}; + CallDoneIf m_callDoneIf = CallDoneIf::SuccessOrError; + }; + + struct GroupHandler { + GroupSetupHandler m_setupHandler; + GroupDoneHandler m_doneHandler = {}; + CallDoneIf m_callDoneIf = CallDoneIf::SuccessOrError; + }; + + struct GroupData { + GroupHandler m_groupHandler = {}; + std::optional<int> m_parallelLimit = {}; + std::optional<WorkflowPolicy> m_workflowPolicy = {}; + std::optional<Loop> m_loop = {}; + }; + + enum class Type { + List, + Group, + GroupData, + Storage, + TaskHandler + }; + + GroupItem() = default; + GroupItem(Type type) : m_type(type) { } + GroupItem(const GroupData &data) + : m_type(Type::GroupData) + , m_groupData(data) {} + GroupItem(const TaskHandler &handler) + : m_type(Type::TaskHandler) + , m_taskHandler(handler) {} + void addChildren(const QList<GroupItem> &children); + + static GroupItem groupHandler(const GroupHandler &handler) { return GroupItem({handler}); } + static GroupItem parallelLimit(int limit) { return GroupItem({{}, limit}); } + static GroupItem workflowPolicy(WorkflowPolicy policy) { return GroupItem({{}, {}, policy}); } + + // Checks if Function may be invoked with Args and if Function's return type is Result. + template <typename Result, typename Function, typename ...Args, + typename DecayedFunction = std::decay_t<Function>> + static constexpr bool isInvocable() + { + // Note, that std::is_invocable_r_v doesn't check Result type properly. + if constexpr (std::is_invocable_r_v<Result, DecayedFunction, Args...>) + return std::is_same_v<Result, std::invoke_result_t<DecayedFunction, Args...>>; + return false; + } + +private: + friend class ContainerNode; + friend class TaskNode; + friend class TaskTreePrivate; + Type m_type = Type::Group; + QList<GroupItem> m_children; + GroupData m_groupData; + QList<StorageBase> m_storageList; + TaskHandler m_taskHandler; +}; + +class TASKING_EXPORT ExecutableItem : public GroupItem +{ +public: + ExecutableItem withTimeout(std::chrono::milliseconds timeout, + const std::function<void()> &handler = {}) const; + ExecutableItem withLog(const QString &logName) const; + template <typename SenderSignalPairGetter> + ExecutableItem withCancel(SenderSignalPairGetter &&getter) const + { + const auto connectWrapper = [getter](QObject *guard, const std::function<void()> &trigger) { + const auto senderSignalPair = getter(); + QObject::connect(senderSignalPair.first, senderSignalPair.second, guard, [trigger] { + trigger(); + }, static_cast<Qt::ConnectionType>(Qt::QueuedConnection | Qt::SingleShotConnection)); + }; + return withCancelImpl(connectWrapper); + } + +protected: + ExecutableItem() = default; + ExecutableItem(const TaskHandler &handler) : GroupItem(handler) {} + +private: + ExecutableItem withCancelImpl( + const std::function<void(QObject *, const std::function<void()> &)> &connectWrapper) const; +}; + +class TASKING_EXPORT Group : public ExecutableItem +{ +public: + Group(const QList<GroupItem> &children) { addChildren(children); } + Group(std::initializer_list<GroupItem> children) { addChildren(children); } + + // GroupData related: + template <typename Handler> + static GroupItem onGroupSetup(Handler &&handler) { + return groupHandler({wrapGroupSetup(std::forward<Handler>(handler))}); + } + template <typename Handler> + static GroupItem onGroupDone(Handler &&handler, CallDoneIf callDoneIf = CallDoneIf::SuccessOrError) { + return groupHandler({{}, wrapGroupDone(std::forward<Handler>(handler)), callDoneIf}); + } + using GroupItem::parallelLimit; // Default: 1 (sequential). 0 means unlimited (parallel). + using GroupItem::workflowPolicy; // Default: WorkflowPolicy::StopOnError. + +private: + template <typename Handler> + static GroupSetupHandler wrapGroupSetup(Handler &&handler) + { + // R, V stands for: Setup[R]esult, [V]oid + static constexpr bool isR = isInvocable<SetupResult, Handler>(); + static constexpr bool isV = isInvocable<void, Handler>(); + static_assert(isR || isV, + "Group setup handler needs to take no arguments and has to return void or SetupResult. " + "The passed handler doesn't fulfill these requirements."); + return [handler] { + if constexpr (isR) + return std::invoke(handler); + std::invoke(handler); + return SetupResult::Continue; + }; + } + template <typename Handler> + static GroupDoneHandler wrapGroupDone(Handler &&handler) + { + // R, V, D stands for: Done[R]esult, [V]oid, [D]oneWith + static constexpr bool isRD = isInvocable<DoneResult, Handler, DoneWith>(); + static constexpr bool isR = isInvocable<DoneResult, Handler>(); + static constexpr bool isVD = isInvocable<void, Handler, DoneWith>(); + static constexpr bool isV = isInvocable<void, Handler>(); + static_assert(isRD || isR || isVD || isV, + "Group done handler needs to take (DoneWith) or (void) as an argument and has to " + "return void or DoneResult. The passed handler doesn't fulfill these requirements."); + return [handler](DoneWith result) { + if constexpr (isRD) + return std::invoke(handler, result); + if constexpr (isR) + return std::invoke(handler); + if constexpr (isVD) + std::invoke(handler, result); + else if constexpr (isV) + std::invoke(handler); + return result == DoneWith::Success ? DoneResult::Success : DoneResult::Error; + }; + } +}; + +template <typename Handler> +static GroupItem onGroupSetup(Handler &&handler) +{ + return Group::onGroupSetup(std::forward<Handler>(handler)); +} + +template <typename Handler> +static GroupItem onGroupDone(Handler &&handler, CallDoneIf callDoneIf = CallDoneIf::SuccessOrError) +{ + return Group::onGroupDone(std::forward<Handler>(handler), callDoneIf); +} + +TASKING_EXPORT GroupItem parallelLimit(int limit); +TASKING_EXPORT GroupItem workflowPolicy(WorkflowPolicy policy); + +TASKING_EXPORT extern const GroupItem nullItem; + +TASKING_EXPORT extern const GroupItem sequential; +TASKING_EXPORT extern const GroupItem parallel; +TASKING_EXPORT extern const GroupItem parallelIdealThreadCountLimit; + +TASKING_EXPORT extern const GroupItem stopOnError; +TASKING_EXPORT extern const GroupItem continueOnError; +TASKING_EXPORT extern const GroupItem stopOnSuccess; +TASKING_EXPORT extern const GroupItem continueOnSuccess; +TASKING_EXPORT extern const GroupItem stopOnSuccessOrError; +TASKING_EXPORT extern const GroupItem finishAllAndSuccess; +TASKING_EXPORT extern const GroupItem finishAllAndError; + +class TASKING_EXPORT Forever final : public Group +{ +public: + Forever(const QList<GroupItem> &children) : Group({LoopForever(), children}) {} + Forever(std::initializer_list<GroupItem> children) : Group({LoopForever(), children}) {} +}; + +// Synchronous invocation. Similarly to Group - isn't counted as a task inside taskCount() +class TASKING_EXPORT Sync final : public ExecutableItem +{ +public: + template <typename Handler> + Sync(Handler &&handler) { + addChildren({ onGroupSetup(wrapHandler(std::forward<Handler>(handler))) }); + } + +private: + template <typename Handler> + static GroupSetupHandler wrapHandler(Handler &&handler) { + // R, V stands for: Done[R]esult, [V]oid + static constexpr bool isR = isInvocable<DoneResult, Handler>(); + static constexpr bool isV = isInvocable<void, Handler>(); + static_assert(isR || isV, + "Sync handler needs to take no arguments and has to return void or DoneResult. " + "The passed handler doesn't fulfill these requirements."); + return [handler] { + if constexpr (isR) { + return std::invoke(handler) == DoneResult::Success ? SetupResult::StopWithSuccess + : SetupResult::StopWithError; + } + std::invoke(handler); + return SetupResult::StopWithSuccess; + }; + } +}; + +template <typename Task, typename Deleter = std::default_delete<Task>> +class TaskAdapter : public TaskInterface +{ +protected: + TaskAdapter() : m_task(new Task) {} + Task *task() { return m_task.get(); } + const Task *task() const { return m_task.get(); } + +private: + using TaskType = Task; + using DeleterType = Deleter; + template <typename Adapter> friend class CustomTask; + std::unique_ptr<Task, Deleter> m_task; +}; + +template <typename Adapter> +class CustomTask final : public ExecutableItem +{ +public: + using Task = typename Adapter::TaskType; + using Deleter = typename Adapter::DeleterType; + static_assert(std::is_base_of_v<TaskAdapter<Task, Deleter>, Adapter>, + "The Adapter type for the CustomTask<Adapter> needs to be derived from " + "TaskAdapter<Task>."); + using TaskSetupHandler = std::function<SetupResult(Task &)>; + using TaskDoneHandler = std::function<DoneResult(const Task &, DoneWith)>; + + template <typename SetupHandler = TaskSetupHandler, typename DoneHandler = TaskDoneHandler> + CustomTask(SetupHandler &&setup = TaskSetupHandler(), DoneHandler &&done = TaskDoneHandler(), + CallDoneIf callDoneIf = CallDoneIf::SuccessOrError) + : ExecutableItem({&createAdapter, wrapSetup(std::forward<SetupHandler>(setup)), + wrapDone(std::forward<DoneHandler>(done)), callDoneIf}) + {} + +private: + static Adapter *createAdapter() { return new Adapter; } + + template <typename Handler> + static InterfaceSetupHandler wrapSetup(Handler &&handler) { + if constexpr (std::is_same_v<Handler, TaskSetupHandler>) + return {}; // When user passed {} for the setup handler. + // R, V stands for: Setup[R]esult, [V]oid + static constexpr bool isR = isInvocable<SetupResult, Handler, Task &>(); + static constexpr bool isV = isInvocable<void, Handler, Task &>(); + static_assert(isR || isV, + "Task setup handler needs to take (Task &) as an argument and has to return void or " + "SetupResult. The passed handler doesn't fulfill these requirements."); + return [handler](TaskInterface &taskInterface) { + Adapter &adapter = static_cast<Adapter &>(taskInterface); + if constexpr (isR) + return std::invoke(handler, *adapter.task()); + std::invoke(handler, *adapter.task()); + return SetupResult::Continue; + }; + } + + template <typename Handler> + static InterfaceDoneHandler wrapDone(Handler &&handler) { + if constexpr (std::is_same_v<Handler, TaskDoneHandler>) + return {}; // When user passed {} for the done handler. + // R, V, T, D stands for: Done[R]esult, [V]oid, [T]ask, [D]oneWith + static constexpr bool isRTD = isInvocable<DoneResult, Handler, const Task &, DoneWith>(); + static constexpr bool isRT = isInvocable<DoneResult, Handler, const Task &>(); + static constexpr bool isRD = isInvocable<DoneResult, Handler, DoneWith>(); + static constexpr bool isR = isInvocable<DoneResult, Handler>(); + static constexpr bool isVTD = isInvocable<void, Handler, const Task &, DoneWith>(); + static constexpr bool isVT = isInvocable<void, Handler, const Task &>(); + static constexpr bool isVD = isInvocable<void, Handler, DoneWith>(); + static constexpr bool isV = isInvocable<void, Handler>(); + static_assert(isRTD || isRT || isRD || isR || isVTD || isVT || isVD || isV, + "Task done handler needs to take (const Task &, DoneWith), (const Task &), " + "(DoneWith) or (void) as arguments and has to return void or DoneResult. " + "The passed handler doesn't fulfill these requirements."); + return [handler](const TaskInterface &taskInterface, DoneWith result) { + const Adapter &adapter = static_cast<const Adapter &>(taskInterface); + if constexpr (isRTD) + return std::invoke(handler, *adapter.task(), result); + if constexpr (isRT) + return std::invoke(handler, *adapter.task()); + if constexpr (isRD) + return std::invoke(handler, result); + if constexpr (isR) + return std::invoke(handler); + if constexpr (isVTD) + std::invoke(handler, *adapter.task(), result); + else if constexpr (isVT) + std::invoke(handler, *adapter.task()); + else if constexpr (isVD) + std::invoke(handler, result); + else if constexpr (isV) + std::invoke(handler); + return result == DoneWith::Success ? DoneResult::Success : DoneResult::Error; + }; + } +}; + +class TASKING_EXPORT TaskTree final : public QObject +{ + Q_OBJECT + +public: + TaskTree(); + TaskTree(const Group &recipe); + ~TaskTree(); + + void setRecipe(const Group &recipe); + + void start(); + void cancel(); + bool isRunning() const; + + // Helper methods. They execute a local event loop with ExcludeUserInputEvents. + // The passed future is used for listening to the cancel event. + // Don't use it in main thread. To be used in non-main threads or in auto tests. + DoneWith runBlocking(); + DoneWith runBlocking(const QFuture<void> &future); + static DoneWith runBlocking(const Group &recipe, + std::chrono::milliseconds timeout = std::chrono::milliseconds::max()); + static DoneWith runBlocking(const Group &recipe, const QFuture<void> &future, + std::chrono::milliseconds timeout = std::chrono::milliseconds::max()); + + int asyncCount() const; + int taskCount() const; + int progressMaximum() const { return taskCount(); } + int progressValue() const; // all finished / skipped / stopped tasks, groups itself excluded + + template <typename StorageStruct, typename Handler> + void onStorageSetup(const Storage<StorageStruct> &storage, Handler &&handler) { + static_assert(std::is_invocable_v<std::decay_t<Handler>, StorageStruct &>, + "Storage setup handler needs to take (Storage &) as an argument. " + "The passed handler doesn't fulfill this requirement."); + setupStorageHandler(storage, + wrapHandler<StorageStruct>(std::forward<Handler>(handler)), {}); + } + template <typename StorageStruct, typename Handler> + void onStorageDone(const Storage<StorageStruct> &storage, Handler &&handler) { + static_assert(std::is_invocable_v<std::decay_t<Handler>, const StorageStruct &>, + "Storage done handler needs to take (const Storage &) as an argument. " + "The passed handler doesn't fulfill this requirement."); + setupStorageHandler(storage, {}, + wrapHandler<const StorageStruct>(std::forward<Handler>(handler))); + } + +Q_SIGNALS: + void started(); + void done(DoneWith result); + void asyncCountChanged(int count); + void progressValueChanged(int value); // updated whenever task finished / skipped / stopped + +private: + void setupStorageHandler(const StorageBase &storage, + StorageBase::StorageHandler setupHandler, + StorageBase::StorageHandler doneHandler); + template <typename StorageStruct, typename Handler> + StorageBase::StorageHandler wrapHandler(Handler &&handler) { + return [handler](void *voidStruct) { + auto *storageStruct = static_cast<StorageStruct *>(voidStruct); + std::invoke(handler, *storageStruct); + }; + } + + TaskTreePrivate *d; +}; + +class TASKING_EXPORT TaskTreeTaskAdapter : public TaskAdapter<TaskTree> +{ +public: + TaskTreeTaskAdapter(); + +private: + void start() final; +}; + +class TASKING_EXPORT TimeoutTaskAdapter : public TaskAdapter<std::chrono::milliseconds> +{ +public: + TimeoutTaskAdapter(); + ~TimeoutTaskAdapter(); + +private: + void start() final; + std::optional<int> m_timerId; +}; + +using TaskTreeTask = CustomTask<TaskTreeTaskAdapter>; +using TimeoutTask = CustomTask<TimeoutTaskAdapter>; + +} // namespace Tasking + +QT_END_NAMESPACE + +#endif // TASKING_TASKTREE_H diff --git a/src/assets/downloader/tasking/tasktreerunner.cpp b/src/assets/downloader/tasking/tasktreerunner.cpp new file mode 100644 index 0000000000..6ed642b1bf --- /dev/null +++ b/src/assets/downloader/tasking/tasktreerunner.cpp @@ -0,0 +1,45 @@ +// Copyright (C) 2024 Jarek Kobus +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "tasktreerunner.h" + +#include "tasktree.h" + +QT_BEGIN_NAMESPACE + +namespace Tasking { + +TaskTreeRunner::~TaskTreeRunner() = default; + +void TaskTreeRunner::start(const Group &recipe, + const SetupHandler &setupHandler, + const DoneHandler &doneHandler) +{ + m_taskTree.reset(new TaskTree(recipe)); + connect(m_taskTree.get(), &TaskTree::done, this, [this, doneHandler](DoneWith result) { + m_taskTree.release()->deleteLater(); + if (doneHandler) + doneHandler(result); + emit done(result); + }); + if (setupHandler) + setupHandler(m_taskTree.get()); + emit aboutToStart(m_taskTree.get()); + m_taskTree->start(); +} + +void TaskTreeRunner::cancel() +{ + if (m_taskTree) + m_taskTree->cancel(); +} + +void TaskTreeRunner::reset() +{ + m_taskTree.reset(); +} + +} // namespace Tasking + +QT_END_NAMESPACE diff --git a/src/assets/downloader/tasking/tasktreerunner.h b/src/assets/downloader/tasking/tasktreerunner.h new file mode 100644 index 0000000000..f91e760811 --- /dev/null +++ b/src/assets/downloader/tasking/tasktreerunner.h @@ -0,0 +1,63 @@ +// Copyright (C) 2024 Jarek Kobus +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef TASKING_TASKTREERUNNER_H +#define TASKING_TASKTREERUNNER_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "tasking_global.h" +#include "tasktree.h" + +#include <QtCore/QObject> + +QT_BEGIN_NAMESPACE + +namespace Tasking { + +class TASKING_EXPORT TaskTreeRunner : public QObject +{ + Q_OBJECT + +public: + using SetupHandler = std::function<void(TaskTree *)>; + using DoneHandler = std::function<void(DoneWith)>; + + ~TaskTreeRunner(); + + bool isRunning() const { return bool(m_taskTree); } + + // When task tree is running it resets the old task tree. + void start(const Group &recipe, + const SetupHandler &setupHandler = {}, + const DoneHandler &doneHandler = {}); + + // When task tree is running it emits done(DoneWith::Cancel) synchronously. + void cancel(); + + // No done() signal is emitted. + void reset(); + +Q_SIGNALS: + void aboutToStart(TaskTree *taskTree); + void done(DoneWith result); + +private: + std::unique_ptr<TaskTree> m_taskTree; +}; + +} // namespace Tasking + +QT_END_NAMESPACE + +#endif // TASKING_TASKTREERUNNER_H diff --git a/src/assets/icons/128x128/document-new.png b/src/assets/icons/128x128/document-new.png Binary files differnew file mode 100644 index 0000000000..8d86a4827a --- /dev/null +++ b/src/assets/icons/128x128/document-new.png diff --git a/src/assets/icons/128x128/document-open.png b/src/assets/icons/128x128/document-open.png Binary files differnew file mode 100644 index 0000000000..2183dbbea6 --- /dev/null +++ b/src/assets/icons/128x128/document-open.png diff --git a/src/assets/icons/128x128/document-print.png b/src/assets/icons/128x128/document-print.png Binary files differnew file mode 100644 index 0000000000..9e7378aab2 --- /dev/null +++ b/src/assets/icons/128x128/document-print.png diff --git a/src/assets/icons/128x128/document-save.png b/src/assets/icons/128x128/document-save.png Binary files differnew file mode 100644 index 0000000000..e8b2840643 --- /dev/null +++ b/src/assets/icons/128x128/document-save.png diff --git a/src/assets/icons/128x128/edit-copy.png b/src/assets/icons/128x128/edit-copy.png Binary files differnew file mode 100644 index 0000000000..7585f4baa0 --- /dev/null +++ b/src/assets/icons/128x128/edit-copy.png diff --git a/src/assets/icons/128x128/edit-cut.png b/src/assets/icons/128x128/edit-cut.png Binary files differnew file mode 100644 index 0000000000..51ede2fe37 --- /dev/null +++ b/src/assets/icons/128x128/edit-cut.png diff --git a/src/assets/icons/128x128/edit-delete.png b/src/assets/icons/128x128/edit-delete.png Binary files differnew file mode 100644 index 0000000000..bdf785c828 --- /dev/null +++ b/src/assets/icons/128x128/edit-delete.png diff --git a/src/assets/icons/128x128/edit-paste.png b/src/assets/icons/128x128/edit-paste.png Binary files differnew file mode 100644 index 0000000000..690ffa172d --- /dev/null +++ b/src/assets/icons/128x128/edit-paste.png diff --git a/src/assets/icons/128x128/edit-redo.png b/src/assets/icons/128x128/edit-redo.png Binary files differnew file mode 100644 index 0000000000..f1c97f71c2 --- /dev/null +++ b/src/assets/icons/128x128/edit-redo.png diff --git a/src/assets/icons/128x128/edit-undo.png b/src/assets/icons/128x128/edit-undo.png Binary files differnew file mode 100644 index 0000000000..e728cbf6e0 --- /dev/null +++ b/src/assets/icons/128x128/edit-undo.png diff --git a/src/assets/icons/128x128/format-justify-center.png b/src/assets/icons/128x128/format-justify-center.png Binary files differnew file mode 100644 index 0000000000..44ceb2af4d --- /dev/null +++ b/src/assets/icons/128x128/format-justify-center.png diff --git a/src/assets/icons/128x128/format-justify-fill.png b/src/assets/icons/128x128/format-justify-fill.png Binary files differnew file mode 100644 index 0000000000..b99a850704 --- /dev/null +++ b/src/assets/icons/128x128/format-justify-fill.png diff --git a/src/assets/icons/128x128/format-justify-left.png b/src/assets/icons/128x128/format-justify-left.png Binary files differnew file mode 100644 index 0000000000..2b63887b49 --- /dev/null +++ b/src/assets/icons/128x128/format-justify-left.png diff --git a/src/assets/icons/128x128/format-justify-right.png b/src/assets/icons/128x128/format-justify-right.png Binary files differnew file mode 100644 index 0000000000..6c61889d59 --- /dev/null +++ b/src/assets/icons/128x128/format-justify-right.png diff --git a/src/assets/icons/128x128/format-text-bold.png b/src/assets/icons/128x128/format-text-bold.png Binary files differnew file mode 100644 index 0000000000..96a5ca88a2 --- /dev/null +++ b/src/assets/icons/128x128/format-text-bold.png diff --git a/src/assets/icons/128x128/format-text-italic.png b/src/assets/icons/128x128/format-text-italic.png Binary files differnew file mode 100644 index 0000000000..2bb71b4a4d --- /dev/null +++ b/src/assets/icons/128x128/format-text-italic.png diff --git a/src/assets/icons/128x128/format-text-underline.png b/src/assets/icons/128x128/format-text-underline.png Binary files differnew file mode 100644 index 0000000000..ecf6830c92 --- /dev/null +++ b/src/assets/icons/128x128/format-text-underline.png diff --git a/src/assets/icons/128x128@2/document-new@2x.png b/src/assets/icons/128x128@2/document-new@2x.png Binary files differnew file mode 100644 index 0000000000..32776b51a9 --- /dev/null +++ b/src/assets/icons/128x128@2/document-new@2x.png diff --git a/src/assets/icons/128x128@2/document-open@2x.png b/src/assets/icons/128x128@2/document-open@2x.png Binary files differnew file mode 100644 index 0000000000..06e188b93b --- /dev/null +++ b/src/assets/icons/128x128@2/document-open@2x.png diff --git a/src/assets/icons/128x128@2/document-print@2x.png b/src/assets/icons/128x128@2/document-print@2x.png Binary files differnew file mode 100644 index 0000000000..644e3c149a --- /dev/null +++ b/src/assets/icons/128x128@2/document-print@2x.png diff --git a/src/assets/icons/128x128@2/document-save@2x.png b/src/assets/icons/128x128@2/document-save@2x.png Binary files differnew file mode 100644 index 0000000000..16fa70493a --- /dev/null +++ b/src/assets/icons/128x128@2/document-save@2x.png diff --git a/src/assets/icons/128x128@2/edit-copy@2x.png b/src/assets/icons/128x128@2/edit-copy@2x.png Binary files differnew file mode 100644 index 0000000000..b18bead117 --- /dev/null +++ b/src/assets/icons/128x128@2/edit-copy@2x.png diff --git a/src/assets/icons/128x128@2/edit-cut@2x.png b/src/assets/icons/128x128@2/edit-cut@2x.png Binary files differnew file mode 100644 index 0000000000..d9454cebf1 --- /dev/null +++ b/src/assets/icons/128x128@2/edit-cut@2x.png diff --git a/src/assets/icons/128x128@2/edit-delete@2x.png b/src/assets/icons/128x128@2/edit-delete@2x.png Binary files differnew file mode 100644 index 0000000000..4081cdb2ca --- /dev/null +++ b/src/assets/icons/128x128@2/edit-delete@2x.png diff --git a/src/assets/icons/128x128@2/edit-paste@2x.png b/src/assets/icons/128x128@2/edit-paste@2x.png Binary files differnew file mode 100644 index 0000000000..3358426818 --- /dev/null +++ b/src/assets/icons/128x128@2/edit-paste@2x.png diff --git a/src/assets/icons/128x128@2/edit-redo@2x.png b/src/assets/icons/128x128@2/edit-redo@2x.png Binary files differnew file mode 100644 index 0000000000..e28b28542c --- /dev/null +++ b/src/assets/icons/128x128@2/edit-redo@2x.png diff --git a/src/assets/icons/128x128@2/edit-undo@2x.png b/src/assets/icons/128x128@2/edit-undo@2x.png Binary files differnew file mode 100644 index 0000000000..fe10f57a39 --- /dev/null +++ b/src/assets/icons/128x128@2/edit-undo@2x.png diff --git a/src/assets/icons/128x128@2/format-justify-center@2x.png b/src/assets/icons/128x128@2/format-justify-center@2x.png Binary files differnew file mode 100644 index 0000000000..d4ad74b0d0 --- /dev/null +++ b/src/assets/icons/128x128@2/format-justify-center@2x.png diff --git a/src/assets/icons/128x128@2/format-justify-fill@2x.png b/src/assets/icons/128x128@2/format-justify-fill@2x.png Binary files differnew file mode 100644 index 0000000000..bf0dd84bbb --- /dev/null +++ b/src/assets/icons/128x128@2/format-justify-fill@2x.png diff --git a/src/assets/icons/128x128@2/format-justify-left@2x.png b/src/assets/icons/128x128@2/format-justify-left@2x.png Binary files differnew file mode 100644 index 0000000000..dde68c8514 --- /dev/null +++ b/src/assets/icons/128x128@2/format-justify-left@2x.png diff --git a/src/assets/icons/128x128@2/format-justify-right@2x.png b/src/assets/icons/128x128@2/format-justify-right@2x.png Binary files differnew file mode 100644 index 0000000000..8a5e7518bd --- /dev/null +++ b/src/assets/icons/128x128@2/format-justify-right@2x.png diff --git a/src/assets/icons/128x128@2/format-text-bold@2x.png b/src/assets/icons/128x128@2/format-text-bold@2x.png Binary files differnew file mode 100644 index 0000000000..665d3ce37b --- /dev/null +++ b/src/assets/icons/128x128@2/format-text-bold@2x.png diff --git a/src/assets/icons/128x128@2/format-text-italic@2x.png b/src/assets/icons/128x128@2/format-text-italic@2x.png Binary files differnew file mode 100644 index 0000000000..4b6846a6b9 --- /dev/null +++ b/src/assets/icons/128x128@2/format-text-italic@2x.png diff --git a/src/assets/icons/128x128@2/format-text-underline@2x.png b/src/assets/icons/128x128@2/format-text-underline@2x.png Binary files differnew file mode 100644 index 0000000000..601f73216a --- /dev/null +++ b/src/assets/icons/128x128@2/format-text-underline@2x.png diff --git a/src/assets/icons/16x16/document-new.png b/src/assets/icons/16x16/document-new.png Binary files differnew file mode 100644 index 0000000000..893e7e1aec --- /dev/null +++ b/src/assets/icons/16x16/document-new.png diff --git a/src/assets/icons/16x16/document-open.png b/src/assets/icons/16x16/document-open.png Binary files differnew file mode 100644 index 0000000000..b07906f40b --- /dev/null +++ b/src/assets/icons/16x16/document-open.png diff --git a/src/assets/icons/16x16/document-print.png b/src/assets/icons/16x16/document-print.png Binary files differnew file mode 100644 index 0000000000..9341060076 --- /dev/null +++ b/src/assets/icons/16x16/document-print.png diff --git a/src/assets/icons/16x16/document-save.png b/src/assets/icons/16x16/document-save.png Binary files differnew file mode 100644 index 0000000000..6238718191 --- /dev/null +++ b/src/assets/icons/16x16/document-save.png diff --git a/src/assets/icons/16x16/edit-copy.png b/src/assets/icons/16x16/edit-copy.png Binary files differnew file mode 100644 index 0000000000..585f5bfc8d --- /dev/null +++ b/src/assets/icons/16x16/edit-copy.png diff --git a/src/assets/icons/16x16/edit-cut.png b/src/assets/icons/16x16/edit-cut.png Binary files differnew file mode 100644 index 0000000000..661ef1ad03 --- /dev/null +++ b/src/assets/icons/16x16/edit-cut.png diff --git a/src/assets/icons/16x16/edit-delete.png b/src/assets/icons/16x16/edit-delete.png Binary files differnew file mode 100644 index 0000000000..7b5998df8a --- /dev/null +++ b/src/assets/icons/16x16/edit-delete.png diff --git a/src/assets/icons/16x16/edit-paste.png b/src/assets/icons/16x16/edit-paste.png Binary files differnew file mode 100644 index 0000000000..6318a22caf --- /dev/null +++ b/src/assets/icons/16x16/edit-paste.png diff --git a/src/assets/icons/16x16/edit-redo.png b/src/assets/icons/16x16/edit-redo.png Binary files differnew file mode 100644 index 0000000000..7eb10fe899 --- /dev/null +++ b/src/assets/icons/16x16/edit-redo.png diff --git a/src/assets/icons/16x16/edit-undo.png b/src/assets/icons/16x16/edit-undo.png Binary files differnew file mode 100644 index 0000000000..108712547c --- /dev/null +++ b/src/assets/icons/16x16/edit-undo.png diff --git a/src/assets/icons/16x16/format-justify-center.png b/src/assets/icons/16x16/format-justify-center.png Binary files differnew file mode 100644 index 0000000000..6b0951fa5d --- /dev/null +++ b/src/assets/icons/16x16/format-justify-center.png diff --git a/src/assets/icons/16x16/format-justify-fill.png b/src/assets/icons/16x16/format-justify-fill.png Binary files differnew file mode 100644 index 0000000000..6e1c10d7c4 --- /dev/null +++ b/src/assets/icons/16x16/format-justify-fill.png diff --git a/src/assets/icons/16x16/format-justify-left.png b/src/assets/icons/16x16/format-justify-left.png Binary files differnew file mode 100644 index 0000000000..9dfdc89b68 --- /dev/null +++ b/src/assets/icons/16x16/format-justify-left.png diff --git a/src/assets/icons/16x16/format-justify-right.png b/src/assets/icons/16x16/format-justify-right.png Binary files differnew file mode 100644 index 0000000000..36a52081f1 --- /dev/null +++ b/src/assets/icons/16x16/format-justify-right.png diff --git a/src/assets/icons/16x16/format-text-bold.png b/src/assets/icons/16x16/format-text-bold.png Binary files differnew file mode 100644 index 0000000000..a079317a94 --- /dev/null +++ b/src/assets/icons/16x16/format-text-bold.png diff --git a/src/assets/icons/16x16/format-text-italic.png b/src/assets/icons/16x16/format-text-italic.png Binary files differnew file mode 100644 index 0000000000..04202b2842 --- /dev/null +++ b/src/assets/icons/16x16/format-text-italic.png diff --git a/src/assets/icons/16x16/format-text-underline.png b/src/assets/icons/16x16/format-text-underline.png Binary files differnew file mode 100644 index 0000000000..a80368212d --- /dev/null +++ b/src/assets/icons/16x16/format-text-underline.png diff --git a/src/assets/icons/16x16@2/document-new@2x.png b/src/assets/icons/16x16@2/document-new@2x.png Binary files differnew file mode 100644 index 0000000000..482ae52024 --- /dev/null +++ b/src/assets/icons/16x16@2/document-new@2x.png diff --git a/src/assets/icons/16x16@2/document-open@2x.png b/src/assets/icons/16x16@2/document-open@2x.png Binary files differnew file mode 100644 index 0000000000..9858b146f4 --- /dev/null +++ b/src/assets/icons/16x16@2/document-open@2x.png diff --git a/src/assets/icons/16x16@2/document-print@2x.png b/src/assets/icons/16x16@2/document-print@2x.png Binary files differnew file mode 100644 index 0000000000..1672ec5897 --- /dev/null +++ b/src/assets/icons/16x16@2/document-print@2x.png diff --git a/src/assets/icons/16x16@2/document-save@2x.png b/src/assets/icons/16x16@2/document-save@2x.png Binary files differnew file mode 100644 index 0000000000..f04de74673 --- /dev/null +++ b/src/assets/icons/16x16@2/document-save@2x.png diff --git a/src/assets/icons/16x16@2/edit-copy@2x.png b/src/assets/icons/16x16@2/edit-copy@2x.png Binary files differnew file mode 100644 index 0000000000..bbb34cc4c2 --- /dev/null +++ b/src/assets/icons/16x16@2/edit-copy@2x.png diff --git a/src/assets/icons/16x16@2/edit-cut@2x.png b/src/assets/icons/16x16@2/edit-cut@2x.png Binary files differnew file mode 100644 index 0000000000..d89ef6c016 --- /dev/null +++ b/src/assets/icons/16x16@2/edit-cut@2x.png diff --git a/src/assets/icons/16x16@2/edit-delete@2x.png b/src/assets/icons/16x16@2/edit-delete@2x.png Binary files differnew file mode 100644 index 0000000000..4c97ee2495 --- /dev/null +++ b/src/assets/icons/16x16@2/edit-delete@2x.png diff --git a/src/assets/icons/16x16@2/edit-paste@2x.png b/src/assets/icons/16x16@2/edit-paste@2x.png Binary files differnew file mode 100644 index 0000000000..299fa77686 --- /dev/null +++ b/src/assets/icons/16x16@2/edit-paste@2x.png diff --git a/src/assets/icons/16x16@2/edit-redo@2x.png b/src/assets/icons/16x16@2/edit-redo@2x.png Binary files differnew file mode 100644 index 0000000000..4f8849c711 --- /dev/null +++ b/src/assets/icons/16x16@2/edit-redo@2x.png diff --git a/src/assets/icons/16x16@2/edit-undo@2x.png b/src/assets/icons/16x16@2/edit-undo@2x.png Binary files differnew file mode 100644 index 0000000000..b3d366c53f --- /dev/null +++ b/src/assets/icons/16x16@2/edit-undo@2x.png diff --git a/src/assets/icons/16x16@2/format-justify-center@2x.png b/src/assets/icons/16x16@2/format-justify-center@2x.png Binary files differnew file mode 100644 index 0000000000..80c3afd9a6 --- /dev/null +++ b/src/assets/icons/16x16@2/format-justify-center@2x.png diff --git a/src/assets/icons/16x16@2/format-justify-fill@2x.png b/src/assets/icons/16x16@2/format-justify-fill@2x.png Binary files differnew file mode 100644 index 0000000000..33589ea25d --- /dev/null +++ b/src/assets/icons/16x16@2/format-justify-fill@2x.png diff --git a/src/assets/icons/16x16@2/format-justify-left@2x.png b/src/assets/icons/16x16@2/format-justify-left@2x.png Binary files differnew file mode 100644 index 0000000000..ba02821135 --- /dev/null +++ b/src/assets/icons/16x16@2/format-justify-left@2x.png diff --git a/src/assets/icons/16x16@2/format-justify-right@2x.png b/src/assets/icons/16x16@2/format-justify-right@2x.png Binary files differnew file mode 100644 index 0000000000..8e15d0cb44 --- /dev/null +++ b/src/assets/icons/16x16@2/format-justify-right@2x.png diff --git a/src/assets/icons/16x16@2/format-text-bold@2x.png b/src/assets/icons/16x16@2/format-text-bold@2x.png Binary files differnew file mode 100644 index 0000000000..754efdd975 --- /dev/null +++ b/src/assets/icons/16x16@2/format-text-bold@2x.png diff --git a/src/assets/icons/16x16@2/format-text-italic@2x.png b/src/assets/icons/16x16@2/format-text-italic@2x.png Binary files differnew file mode 100644 index 0000000000..6db31a4f69 --- /dev/null +++ b/src/assets/icons/16x16@2/format-text-italic@2x.png diff --git a/src/assets/icons/16x16@2/format-text-underline@2x.png b/src/assets/icons/16x16@2/format-text-underline@2x.png Binary files differnew file mode 100644 index 0000000000..977cde9d97 --- /dev/null +++ b/src/assets/icons/16x16@2/format-text-underline@2x.png diff --git a/src/assets/icons/256x256/document-new.png b/src/assets/icons/256x256/document-new.png Binary files differnew file mode 100644 index 0000000000..32776b51a9 --- /dev/null +++ b/src/assets/icons/256x256/document-new.png diff --git a/src/assets/icons/256x256/document-open.png b/src/assets/icons/256x256/document-open.png Binary files differnew file mode 100644 index 0000000000..06e188b93b --- /dev/null +++ b/src/assets/icons/256x256/document-open.png diff --git a/src/assets/icons/256x256/document-print.png b/src/assets/icons/256x256/document-print.png Binary files differnew file mode 100644 index 0000000000..644e3c149a --- /dev/null +++ b/src/assets/icons/256x256/document-print.png diff --git a/src/assets/icons/256x256/document-save.png b/src/assets/icons/256x256/document-save.png Binary files differnew file mode 100644 index 0000000000..16fa70493a --- /dev/null +++ b/src/assets/icons/256x256/document-save.png diff --git a/src/assets/icons/256x256/edit-copy.png b/src/assets/icons/256x256/edit-copy.png Binary files differnew file mode 100644 index 0000000000..b18bead117 --- /dev/null +++ b/src/assets/icons/256x256/edit-copy.png diff --git a/src/assets/icons/256x256/edit-cut.png b/src/assets/icons/256x256/edit-cut.png Binary files differnew file mode 100644 index 0000000000..d9454cebf1 --- /dev/null +++ b/src/assets/icons/256x256/edit-cut.png diff --git a/src/assets/icons/256x256/edit-delete.png b/src/assets/icons/256x256/edit-delete.png Binary files differnew file mode 100644 index 0000000000..4081cdb2ca --- /dev/null +++ b/src/assets/icons/256x256/edit-delete.png diff --git a/src/assets/icons/256x256/edit-paste.png b/src/assets/icons/256x256/edit-paste.png Binary files differnew file mode 100644 index 0000000000..3358426818 --- /dev/null +++ b/src/assets/icons/256x256/edit-paste.png diff --git a/src/assets/icons/256x256/edit-redo.png b/src/assets/icons/256x256/edit-redo.png Binary files differnew file mode 100644 index 0000000000..e28b28542c --- /dev/null +++ b/src/assets/icons/256x256/edit-redo.png diff --git a/src/assets/icons/256x256/edit-undo.png b/src/assets/icons/256x256/edit-undo.png Binary files differnew file mode 100644 index 0000000000..fe10f57a39 --- /dev/null +++ b/src/assets/icons/256x256/edit-undo.png diff --git a/src/assets/icons/256x256/format-justify-center.png b/src/assets/icons/256x256/format-justify-center.png Binary files differnew file mode 100644 index 0000000000..d4ad74b0d0 --- /dev/null +++ b/src/assets/icons/256x256/format-justify-center.png diff --git a/src/assets/icons/256x256/format-justify-fill.png b/src/assets/icons/256x256/format-justify-fill.png Binary files differnew file mode 100644 index 0000000000..bf0dd84bbb --- /dev/null +++ b/src/assets/icons/256x256/format-justify-fill.png diff --git a/src/assets/icons/256x256/format-justify-left.png b/src/assets/icons/256x256/format-justify-left.png Binary files differnew file mode 100644 index 0000000000..dde68c8514 --- /dev/null +++ b/src/assets/icons/256x256/format-justify-left.png diff --git a/src/assets/icons/256x256/format-justify-right.png b/src/assets/icons/256x256/format-justify-right.png Binary files differnew file mode 100644 index 0000000000..8a5e7518bd --- /dev/null +++ b/src/assets/icons/256x256/format-justify-right.png diff --git a/src/assets/icons/256x256/format-text-bold.png b/src/assets/icons/256x256/format-text-bold.png Binary files differnew file mode 100644 index 0000000000..665d3ce37b --- /dev/null +++ b/src/assets/icons/256x256/format-text-bold.png diff --git a/src/assets/icons/256x256/format-text-italic.png b/src/assets/icons/256x256/format-text-italic.png Binary files differnew file mode 100644 index 0000000000..4b6846a6b9 --- /dev/null +++ b/src/assets/icons/256x256/format-text-italic.png diff --git a/src/assets/icons/256x256/format-text-underline.png b/src/assets/icons/256x256/format-text-underline.png Binary files differnew file mode 100644 index 0000000000..601f73216a --- /dev/null +++ b/src/assets/icons/256x256/format-text-underline.png diff --git a/src/assets/icons/256x256@2/document-new@2x.png b/src/assets/icons/256x256@2/document-new@2x.png Binary files differnew file mode 100644 index 0000000000..bfec6d0e6d --- /dev/null +++ b/src/assets/icons/256x256@2/document-new@2x.png diff --git a/src/assets/icons/256x256@2/document-open@2x.png b/src/assets/icons/256x256@2/document-open@2x.png Binary files differnew file mode 100644 index 0000000000..630a05f622 --- /dev/null +++ b/src/assets/icons/256x256@2/document-open@2x.png diff --git a/src/assets/icons/256x256@2/document-print@2x.png b/src/assets/icons/256x256@2/document-print@2x.png Binary files differnew file mode 100644 index 0000000000..c8611c31c4 --- /dev/null +++ b/src/assets/icons/256x256@2/document-print@2x.png diff --git a/src/assets/icons/256x256@2/document-save@2x.png b/src/assets/icons/256x256@2/document-save@2x.png Binary files differnew file mode 100644 index 0000000000..6f46095981 --- /dev/null +++ b/src/assets/icons/256x256@2/document-save@2x.png diff --git a/src/assets/icons/256x256@2/edit-copy@2x.png b/src/assets/icons/256x256@2/edit-copy@2x.png Binary files differnew file mode 100644 index 0000000000..2f350041a0 --- /dev/null +++ b/src/assets/icons/256x256@2/edit-copy@2x.png diff --git a/src/assets/icons/256x256@2/edit-cut@2x.png b/src/assets/icons/256x256@2/edit-cut@2x.png Binary files differnew file mode 100644 index 0000000000..e11cf6d234 --- /dev/null +++ b/src/assets/icons/256x256@2/edit-cut@2x.png diff --git a/src/assets/icons/256x256@2/edit-delete@2x.png b/src/assets/icons/256x256@2/edit-delete@2x.png Binary files differnew file mode 100644 index 0000000000..efe6b90bf5 --- /dev/null +++ b/src/assets/icons/256x256@2/edit-delete@2x.png diff --git a/src/assets/icons/256x256@2/edit-paste@2x.png b/src/assets/icons/256x256@2/edit-paste@2x.png Binary files differnew file mode 100644 index 0000000000..32f54b3959 --- /dev/null +++ b/src/assets/icons/256x256@2/edit-paste@2x.png diff --git a/src/assets/icons/256x256@2/edit-redo@2x.png b/src/assets/icons/256x256@2/edit-redo@2x.png Binary files differnew file mode 100644 index 0000000000..1f6e366535 --- /dev/null +++ b/src/assets/icons/256x256@2/edit-redo@2x.png diff --git a/src/assets/icons/256x256@2/edit-undo@2x.png b/src/assets/icons/256x256@2/edit-undo@2x.png Binary files differnew file mode 100644 index 0000000000..980ed37062 --- /dev/null +++ b/src/assets/icons/256x256@2/edit-undo@2x.png diff --git a/src/assets/icons/256x256@2/format-justify-center@2x.png b/src/assets/icons/256x256@2/format-justify-center@2x.png Binary files differnew file mode 100644 index 0000000000..af7044ddee --- /dev/null +++ b/src/assets/icons/256x256@2/format-justify-center@2x.png diff --git a/src/assets/icons/256x256@2/format-justify-fill@2x.png b/src/assets/icons/256x256@2/format-justify-fill@2x.png Binary files differnew file mode 100644 index 0000000000..da14563bd6 --- /dev/null +++ b/src/assets/icons/256x256@2/format-justify-fill@2x.png diff --git a/src/assets/icons/256x256@2/format-justify-left@2x.png b/src/assets/icons/256x256@2/format-justify-left@2x.png Binary files differnew file mode 100644 index 0000000000..c1025bf010 --- /dev/null +++ b/src/assets/icons/256x256@2/format-justify-left@2x.png diff --git a/src/assets/icons/256x256@2/format-justify-right@2x.png b/src/assets/icons/256x256@2/format-justify-right@2x.png Binary files differnew file mode 100644 index 0000000000..3a07e06e0f --- /dev/null +++ b/src/assets/icons/256x256@2/format-justify-right@2x.png diff --git a/src/assets/icons/256x256@2/format-text-bold@2x.png b/src/assets/icons/256x256@2/format-text-bold@2x.png Binary files differnew file mode 100644 index 0000000000..b0f4cb0995 --- /dev/null +++ b/src/assets/icons/256x256@2/format-text-bold@2x.png diff --git a/src/assets/icons/256x256@2/format-text-italic@2x.png b/src/assets/icons/256x256@2/format-text-italic@2x.png Binary files differnew file mode 100644 index 0000000000..85f0cfc1d6 --- /dev/null +++ b/src/assets/icons/256x256@2/format-text-italic@2x.png diff --git a/src/assets/icons/256x256@2/format-text-underline@2x.png b/src/assets/icons/256x256@2/format-text-underline@2x.png Binary files differnew file mode 100644 index 0000000000..51ee0aa778 --- /dev/null +++ b/src/assets/icons/256x256@2/format-text-underline@2x.png diff --git a/src/assets/icons/32x32/document-new.png b/src/assets/icons/32x32/document-new.png Binary files differnew file mode 100644 index 0000000000..482ae52024 --- /dev/null +++ b/src/assets/icons/32x32/document-new.png diff --git a/src/assets/icons/32x32/document-open.png b/src/assets/icons/32x32/document-open.png Binary files differnew file mode 100644 index 0000000000..9858b146f4 --- /dev/null +++ b/src/assets/icons/32x32/document-open.png diff --git a/src/assets/icons/32x32/document-print.png b/src/assets/icons/32x32/document-print.png Binary files differnew file mode 100644 index 0000000000..1672ec5897 --- /dev/null +++ b/src/assets/icons/32x32/document-print.png diff --git a/src/assets/icons/32x32/document-save.png b/src/assets/icons/32x32/document-save.png Binary files differnew file mode 100644 index 0000000000..f04de74673 --- /dev/null +++ b/src/assets/icons/32x32/document-save.png diff --git a/src/assets/icons/32x32/edit-copy.png b/src/assets/icons/32x32/edit-copy.png Binary files differnew file mode 100644 index 0000000000..bbb34cc4c2 --- /dev/null +++ b/src/assets/icons/32x32/edit-copy.png diff --git a/src/assets/icons/32x32/edit-cut.png b/src/assets/icons/32x32/edit-cut.png Binary files differnew file mode 100644 index 0000000000..d89ef6c016 --- /dev/null +++ b/src/assets/icons/32x32/edit-cut.png diff --git a/src/assets/icons/32x32/edit-delete.png b/src/assets/icons/32x32/edit-delete.png Binary files differnew file mode 100644 index 0000000000..4c97ee2495 --- /dev/null +++ b/src/assets/icons/32x32/edit-delete.png diff --git a/src/assets/icons/32x32/edit-paste.png b/src/assets/icons/32x32/edit-paste.png Binary files differnew file mode 100644 index 0000000000..299fa77686 --- /dev/null +++ b/src/assets/icons/32x32/edit-paste.png diff --git a/src/assets/icons/32x32/edit-redo.png b/src/assets/icons/32x32/edit-redo.png Binary files differnew file mode 100644 index 0000000000..4f8849c711 --- /dev/null +++ b/src/assets/icons/32x32/edit-redo.png diff --git a/src/assets/icons/32x32/edit-undo.png b/src/assets/icons/32x32/edit-undo.png Binary files differnew file mode 100644 index 0000000000..b3d366c53f --- /dev/null +++ b/src/assets/icons/32x32/edit-undo.png diff --git a/src/assets/icons/32x32/format-justify-center.png b/src/assets/icons/32x32/format-justify-center.png Binary files differnew file mode 100644 index 0000000000..80c3afd9a6 --- /dev/null +++ b/src/assets/icons/32x32/format-justify-center.png diff --git a/src/assets/icons/32x32/format-justify-fill.png b/src/assets/icons/32x32/format-justify-fill.png Binary files differnew file mode 100644 index 0000000000..33589ea25d --- /dev/null +++ b/src/assets/icons/32x32/format-justify-fill.png diff --git a/src/assets/icons/32x32/format-justify-left.png b/src/assets/icons/32x32/format-justify-left.png Binary files differnew file mode 100644 index 0000000000..ba02821135 --- /dev/null +++ b/src/assets/icons/32x32/format-justify-left.png diff --git a/src/assets/icons/32x32/format-justify-right.png b/src/assets/icons/32x32/format-justify-right.png Binary files differnew file mode 100644 index 0000000000..8e15d0cb44 --- /dev/null +++ b/src/assets/icons/32x32/format-justify-right.png diff --git a/src/assets/icons/32x32/format-text-bold.png b/src/assets/icons/32x32/format-text-bold.png Binary files differnew file mode 100644 index 0000000000..754efdd975 --- /dev/null +++ b/src/assets/icons/32x32/format-text-bold.png diff --git a/src/assets/icons/32x32/format-text-italic.png b/src/assets/icons/32x32/format-text-italic.png Binary files differnew file mode 100644 index 0000000000..6db31a4f69 --- /dev/null +++ b/src/assets/icons/32x32/format-text-italic.png diff --git a/src/assets/icons/32x32/format-text-underline.png b/src/assets/icons/32x32/format-text-underline.png Binary files differnew file mode 100644 index 0000000000..977cde9d97 --- /dev/null +++ b/src/assets/icons/32x32/format-text-underline.png diff --git a/src/assets/icons/32x32@2/document-new@2x.png b/src/assets/icons/32x32@2/document-new@2x.png Binary files differnew file mode 100644 index 0000000000..c924576061 --- /dev/null +++ b/src/assets/icons/32x32@2/document-new@2x.png diff --git a/src/assets/icons/32x32@2/document-open@2x.png b/src/assets/icons/32x32@2/document-open@2x.png Binary files differnew file mode 100644 index 0000000000..68e75b549a --- /dev/null +++ b/src/assets/icons/32x32@2/document-open@2x.png diff --git a/src/assets/icons/32x32@2/document-print@2x.png b/src/assets/icons/32x32@2/document-print@2x.png Binary files differnew file mode 100644 index 0000000000..b784336739 --- /dev/null +++ b/src/assets/icons/32x32@2/document-print@2x.png diff --git a/src/assets/icons/32x32@2/document-save@2x.png b/src/assets/icons/32x32@2/document-save@2x.png Binary files differnew file mode 100644 index 0000000000..f4cca4b323 --- /dev/null +++ b/src/assets/icons/32x32@2/document-save@2x.png diff --git a/src/assets/icons/32x32@2/edit-copy@2x.png b/src/assets/icons/32x32@2/edit-copy@2x.png Binary files differnew file mode 100644 index 0000000000..9690d6bb04 --- /dev/null +++ b/src/assets/icons/32x32@2/edit-copy@2x.png diff --git a/src/assets/icons/32x32@2/edit-cut@2x.png b/src/assets/icons/32x32@2/edit-cut@2x.png Binary files differnew file mode 100644 index 0000000000..408b0ae19b --- /dev/null +++ b/src/assets/icons/32x32@2/edit-cut@2x.png diff --git a/src/assets/icons/32x32@2/edit-delete@2x.png b/src/assets/icons/32x32@2/edit-delete@2x.png Binary files differnew file mode 100644 index 0000000000..58abfc1fa5 --- /dev/null +++ b/src/assets/icons/32x32@2/edit-delete@2x.png diff --git a/src/assets/icons/32x32@2/edit-paste@2x.png b/src/assets/icons/32x32@2/edit-paste@2x.png Binary files differnew file mode 100644 index 0000000000..b8c288f6c7 --- /dev/null +++ b/src/assets/icons/32x32@2/edit-paste@2x.png diff --git a/src/assets/icons/32x32@2/edit-redo@2x.png b/src/assets/icons/32x32@2/edit-redo@2x.png Binary files differnew file mode 100644 index 0000000000..89fcd33c30 --- /dev/null +++ b/src/assets/icons/32x32@2/edit-redo@2x.png diff --git a/src/assets/icons/32x32@2/edit-undo@2x.png b/src/assets/icons/32x32@2/edit-undo@2x.png Binary files differnew file mode 100644 index 0000000000..6f7ad2cb40 --- /dev/null +++ b/src/assets/icons/32x32@2/edit-undo@2x.png diff --git a/src/assets/icons/32x32@2/format-justify-center@2x.png b/src/assets/icons/32x32@2/format-justify-center@2x.png Binary files differnew file mode 100644 index 0000000000..9b2cc1ed16 --- /dev/null +++ b/src/assets/icons/32x32@2/format-justify-center@2x.png diff --git a/src/assets/icons/32x32@2/format-justify-fill@2x.png b/src/assets/icons/32x32@2/format-justify-fill@2x.png Binary files differnew file mode 100644 index 0000000000..1212e9f761 --- /dev/null +++ b/src/assets/icons/32x32@2/format-justify-fill@2x.png diff --git a/src/assets/icons/32x32@2/format-justify-left@2x.png b/src/assets/icons/32x32@2/format-justify-left@2x.png Binary files differnew file mode 100644 index 0000000000..8c0eca3037 --- /dev/null +++ b/src/assets/icons/32x32@2/format-justify-left@2x.png diff --git a/src/assets/icons/32x32@2/format-justify-right@2x.png b/src/assets/icons/32x32@2/format-justify-right@2x.png Binary files differnew file mode 100644 index 0000000000..fb0ed70252 --- /dev/null +++ b/src/assets/icons/32x32@2/format-justify-right@2x.png diff --git a/src/assets/icons/32x32@2/format-text-bold@2x.png b/src/assets/icons/32x32@2/format-text-bold@2x.png Binary files differnew file mode 100644 index 0000000000..0e67ead0b8 --- /dev/null +++ b/src/assets/icons/32x32@2/format-text-bold@2x.png diff --git a/src/assets/icons/32x32@2/format-text-italic@2x.png b/src/assets/icons/32x32@2/format-text-italic@2x.png Binary files differnew file mode 100644 index 0000000000..f746f8956f --- /dev/null +++ b/src/assets/icons/32x32@2/format-text-italic@2x.png diff --git a/src/assets/icons/32x32@2/format-text-underline@2x.png b/src/assets/icons/32x32@2/format-text-underline@2x.png Binary files differnew file mode 100644 index 0000000000..47d6fced02 --- /dev/null +++ b/src/assets/icons/32x32@2/format-text-underline@2x.png diff --git a/src/assets/icons/CMakeLists.txt b/src/assets/icons/CMakeLists.txt new file mode 100644 index 0000000000..f5adb229d8 --- /dev/null +++ b/src/assets/icons/CMakeLists.txt @@ -0,0 +1,174 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +qt_internal_add_module(ExampleIconsPrivate + CONFIG_MODULE_NAME example_icons + STATIC + INTERNAL_MODULE + NO_GENERATE_CPP_EXPORTS +) + +set(icons_resource_files + index.theme + 16x16/document-new.png + 16x16/document-open.png + 16x16/document-print.png + 16x16/document-save.png + 16x16/edit-copy.png + 16x16/edit-cut.png + 16x16/edit-delete.png + 16x16/edit-paste.png + 16x16/edit-redo.png + 16x16/edit-undo.png + 16x16/format-justify-center.png + 16x16/format-justify-fill.png + 16x16/format-justify-left.png + 16x16/format-justify-right.png + 16x16/format-text-bold.png + 16x16/format-text-italic.png + 16x16/format-text-underline.png + 16x16@2/document-new@2x.png + 16x16@2/document-open@2x.png + 16x16@2/document-print@2x.png + 16x16@2/document-save@2x.png + 16x16@2/edit-copy@2x.png + 16x16@2/edit-cut@2x.png + 16x16@2/edit-delete@2x.png + 16x16@2/edit-paste@2x.png + 16x16@2/edit-redo@2x.png + 16x16@2/edit-undo@2x.png + 16x16@2/format-justify-center@2x.png + 16x16@2/format-justify-fill@2x.png + 16x16@2/format-justify-left@2x.png + 16x16@2/format-justify-right@2x.png + 16x16@2/format-text-bold@2x.png + 16x16@2/format-text-italic@2x.png + 16x16@2/format-text-underline@2x.png + 32x32/document-new.png + 32x32/document-open.png + 32x32/document-print.png + 32x32/document-save.png + 32x32/edit-copy.png + 32x32/edit-cut.png + 32x32/edit-delete.png + 32x32/edit-paste.png + 32x32/edit-redo.png + 32x32/edit-undo.png + 32x32/format-justify-center.png + 32x32/format-justify-fill.png + 32x32/format-justify-left.png + 32x32/format-justify-right.png + 32x32/format-text-bold.png + 32x32/format-text-italic.png + 32x32/format-text-underline.png + 32x32@2/document-new@2x.png + 32x32@2/document-open@2x.png + 32x32@2/document-print@2x.png + 32x32@2/document-save@2x.png + 32x32@2/edit-copy@2x.png + 32x32@2/edit-cut@2x.png + 32x32@2/edit-delete@2x.png + 32x32@2/edit-paste@2x.png + 32x32@2/edit-redo@2x.png + 32x32@2/edit-undo@2x.png + 32x32@2/format-justify-center@2x.png + 32x32@2/format-justify-fill@2x.png + 32x32@2/format-justify-left@2x.png + 32x32@2/format-justify-right@2x.png + 32x32@2/format-text-bold@2x.png + 32x32@2/format-text-italic@2x.png + 32x32@2/format-text-underline@2x.png + 128x128/document-new.png + 128x128/document-open.png + 128x128/document-print.png + 128x128/document-save.png + 128x128/edit-copy.png + 128x128/edit-cut.png + 128x128/edit-delete.png + 128x128/edit-paste.png + 128x128/edit-redo.png + 128x128/edit-undo.png + 128x128/format-justify-center.png + 128x128/format-justify-fill.png + 128x128/format-justify-left.png + 128x128/format-justify-right.png + 128x128/format-text-bold.png + 128x128/format-text-italic.png + 128x128/format-text-underline.png + 128x128@2/document-new@2x.png + 128x128@2/document-open@2x.png + 128x128@2/document-print@2x.png + 128x128@2/document-save@2x.png + 128x128@2/edit-copy@2x.png + 128x128@2/edit-cut@2x.png + 128x128@2/edit-delete@2x.png + 128x128@2/edit-paste@2x.png + 128x128@2/edit-redo@2x.png + 128x128@2/edit-undo@2x.png + 128x128@2/format-justify-center@2x.png + 128x128@2/format-justify-fill@2x.png + 128x128@2/format-justify-left@2x.png + 128x128@2/format-justify-right@2x.png + 128x128@2/format-text-bold@2x.png + 128x128@2/format-text-italic@2x.png + 128x128@2/format-text-underline@2x.png + 256x256/document-new.png + 256x256/document-open.png + 256x256/document-print.png + 256x256/document-save.png + 256x256/edit-copy.png + 256x256/edit-cut.png + 256x256/edit-delete.png + 256x256/edit-paste.png + 256x256/edit-redo.png + 256x256/edit-undo.png + 256x256/format-justify-center.png + 256x256/format-justify-fill.png + 256x256/format-justify-left.png + 256x256/format-justify-right.png + 256x256/format-text-bold.png + 256x256/format-text-italic.png + 256x256/format-text-underline.png + 256x256@2/document-new@2x.png + 256x256@2/document-open@2x.png + 256x256@2/document-print@2x.png + 256x256@2/document-save@2x.png + 256x256@2/edit-copy@2x.png + 256x256@2/edit-cut@2x.png + 256x256@2/edit-delete@2x.png + 256x256@2/edit-paste@2x.png + 256x256@2/edit-redo@2x.png + 256x256@2/edit-undo@2x.png + 256x256@2/format-justify-center@2x.png + 256x256@2/format-justify-fill@2x.png + 256x256@2/format-justify-left@2x.png + 256x256@2/format-justify-right@2x.png + 256x256@2/format-text-bold@2x.png + 256x256@2/format-text-italic@2x.png + 256x256@2/format-text-underline@2x.png + scalable/document-new.svg + scalable/document-open.svg + scalable/document-print.svg + scalable/document-save.svg + scalable/edit-copy.svg + scalable/edit-cut.svg + scalable/edit-delete.svg + scalable/edit-paste.svg + scalable/edit-redo.svg + scalable/edit-undo.svg + scalable/format-justify-center.svg + scalable/format-justify-fill.svg + scalable/format-justify-left.svg + scalable/format-justify-right.svg + scalable/format-text-bold.svg + scalable/format-text-italic.svg + scalable/format-text-underline.svg +) + +qt_internal_add_resource(ExampleIconsPrivate "example_icons" + PREFIX + "/qt-project.org/icons/example_icons" + FILES + ${icons_resource_files} +) + diff --git a/src/assets/icons/README b/src/assets/icons/README new file mode 100644 index 0000000000..26d94e9ff1 --- /dev/null +++ b/src/assets/icons/README @@ -0,0 +1,29 @@ +Copyright (C) 2023 The Qt Company Ltd. +SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only + +Setting up a project for using Example icon library + +1. Add ExampleIconsPrivate component to your project CMakeList.txt file + ... + find_package(Qt6 + REQUIRED COMPONENTS Core Gui Widgets ExampleIconsPrivate + ) + + target_link_libraries(imageviewer PRIVATE + Qt6::Core + Qt6::Gui + Qt6::Widgets + Qt6::ExampleIconsPrivate + ) + ... + +2. Load the theme + ... + QIcon::setThemeSearchPaths(QIcon::themeSearchPaths() << u":/qt-project.org/icons"_s); + QIcon::setFallbackThemeName(u"example_icons"_s); + ... + +3. Use the icons + ... + const QIcon openIcon = QIcon::fromTheme("document-open"); + ... diff --git a/src/assets/icons/index.theme b/src/assets/icons/index.theme new file mode 100644 index 0000000000..e389719e01 --- /dev/null +++ b/src/assets/icons/index.theme @@ -0,0 +1,46 @@ +[Icon Theme] +Name=example_icons + +Directories=16x16,16x16@2,32x32,32x32@2,128x128,128x128@2,256x256,256x256@2,scalable + +[16x16] +Size=16 +Type=Fixed + +[16x16@2] +Size=16 +Scale=2 +Type=Fixed + +[32x32] +Size=32 +Type=Fixed + +[32x32@2] +Size=32 +Scale=2 +Type=Fixed + +[128x128] +Size=128 +Type=Fixed + +[128x128@2] +Size=128 +Scale=2 +Type=Fixed + +[256x256] +Size=256 +Type=Fixed + +[256x256@2] +Size=256 +Scale=2 +Type=Fixed + +[scalable] +Size=512 +Type=Scalable +MinSize=16 +MaxSize=512 diff --git a/src/assets/icons/scalable/document-new.svg b/src/assets/icons/scalable/document-new.svg new file mode 100644 index 0000000000..b926a7b0e1 --- /dev/null +++ b/src/assets/icons/scalable/document-new.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><svg id="Outlined_icons" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path id="iconDesktopApp_New" d="m15,13c0,.28-.22.5-.5.5h-2v2c0,.28-.22.5-.5.5s-.5-.22-.5-.5v-2h-2c-.28,0-.5-.22-.5-.5s.22-.5.5-.5h2v-2c0-.28.22-.5.5-.5s.5.22.5.5v2h2c.28,0,.5.22.5.5Zm-12,2h5v1H3c-1.1,0-2-.9-2-2V2C1,.9,1.9,0,3,0h7c1.14,1.14,2.93,2.93,4,4v5h-1v-4h-2c-1.1,0-2-.9-2-2V1H3c-.55,0-1,.45-1,1v12c0,.55.45,1,1,1ZM10,1.41v1.59c0,.55.45,1,1,1h1.59l-.82-.82-1.76-1.76Z"/></svg>
\ No newline at end of file diff --git a/src/assets/icons/scalable/document-open.svg b/src/assets/icons/scalable/document-open.svg new file mode 100644 index 0000000000..778c1b7c6e --- /dev/null +++ b/src/assets/icons/scalable/document-open.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><svg id="Outlined_icons" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path id="iconDesktopApp_Open" d="m14.5,5h-1.5v-1c0-1.1-.9-2-2-2h-5L4,0h-2C.9,0,0,.9,0,2v12c0,1.1.9,2,2,2h9s0,0,0,0c.12,0,.25-.01.36-.03.98-.16,1.94-.94,2.24-1.87l2.29-7.19c.33-1.05-.29-1.91-1.39-1.91ZM1,14V2c0-.55.45-1,1-1h1.59l1.71,1.71c.19.19.44.29.71.29h5c.55,0,1,.45,1,1v1h-5.5c-1.1,0-2.27.86-2.61,1.91l-2.29,7.19c-.09.28-.11.54-.07.78-.32-.17-.54-.49-.54-.87Zm13.94-7.4l-2.29,7.19c-.2.63-.99,1.21-1.65,1.21H3c-.2,0-.36-.05-.43-.15-.07-.1-.07-.26-.01-.45l2.29-7.19c.2-.63.99-1.21,1.65-1.21h8c.2,0,.36.05.43.15.07.1.07.26.01.45Z"/></svg>
\ No newline at end of file diff --git a/src/assets/icons/scalable/document-print.svg b/src/assets/icons/scalable/document-print.svg new file mode 100644 index 0000000000..fb8436af81 --- /dev/null +++ b/src/assets/icons/scalable/document-print.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><svg id="Outlined_icons" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path id="iconDesktopApp_Print" d="m14,4h-2v-2c0-1.1-.9-2-2-2h-4c-1.1,0-2,.9-2,2v2h-2c-1.1,0-2,.9-2,2v4c0,1.1.9,2,2,2h2v2c0,1.1.9,2,2,2h4c1.1,0,2-.9,2-2v-2h2c1.1,0,2-.9,2-2v-4c0-1.1-.9-2-2-2ZM5,2c0-.55.45-1,1-1h4c.55,0,1,.45,1,1v2h-6v-2Zm5,13h-4c-.55,0-1-.45-1-1v-5h6v5c0,.55-.45,1-1,1Zm5-5c0,.55-.45,1-1,1h-2v-2h.5c.28,0,.5-.22.5-.5s-.22-.5-.5-.5h-.5s-8,0-8,0h0s-.5,0-.5,0c-.28,0-.5.22-.5.5s.22.5.5.5h.5v2h-2c-.55,0-1-.45-1-1v-4c0-.55.45-1,1-1h12c.55,0,1,.45,1,1v4Zm-5,1.5c0,.28-.22.5-.5.5h-3c-.28,0-.5-.22-.5-.5s.22-.5.5-.5h3c.28,0,.5.22.5.5Zm0,2c0,.28-.22.5-.5.5h-3c-.28,0-.5-.22-.5-.5s.22-.5.5-.5h3c.28,0,.5.22.5.5Z"/></svg>
\ No newline at end of file diff --git a/src/assets/icons/scalable/document-save.svg b/src/assets/icons/scalable/document-save.svg new file mode 100644 index 0000000000..03675f4dab --- /dev/null +++ b/src/assets/icons/scalable/document-save.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><svg id="Outlined_icons" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path id="iconDesktopApp_Save" d="m16,11h0V2.29c0-1.26-1.02-2.29-2.29-2.29H2.29C1.02,0,0,1.02,0,2.29v8.71h0v1h0v1.71c0,1.26,1.02,2.29,2.29,2.29h11.43c1.26,0,2.29-1.02,2.29-2.29v-1.71h0v-1ZM5.71,1h5.29v4.5c0,.28-.22.5-.5.5h-5c-.28,0-.5-.22-.5-.5V1h.71ZM1,2.29c0-.71.58-1.29,1.29-1.29h1.71v4.5c0,.83.67,1.5,1.5,1.5h5c.83,0,1.5-.67,1.5-1.5V1h1.71c.71,0,1.29.58,1.29,1.29v8.71H1V2.29Zm14,11.43c0,.71-.58,1.29-1.29,1.29H2.29c-.71,0-1.29-.58-1.29-1.29v-1.71h14v1.71Z"/></svg>
\ No newline at end of file diff --git a/src/assets/icons/scalable/edit-copy.svg b/src/assets/icons/scalable/edit-copy.svg new file mode 100644 index 0000000000..db53ff1162 --- /dev/null +++ b/src/assets/icons/scalable/edit-copy.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><svg id="Outlined_icons" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path id="iconDesktopApp_Copy" d="m12,0h-5c-1.1,0-2,.9-2,2h1c0-.55.45-1,1-1h4v2c0,1.1.9,2,2,2h2v6c0,.55-.45,1-1,1h-2v1h2c1.1,0,2-.9,2-2v-7c-1.07-1.07-2.86-2.86-4-4Zm1,4c-.55,0-1-.45-1-1v-1.59l1.29,1.29,1.29,1.29h-1.59Zm-6-1H2c-1.1,0-2,.9-2,2v9c0,1.1.9,2,2,2h7c1.1,0,2-.9,2-2v-7c-1.07-1.07-2.86-2.86-4-4Zm3,6v5c0,.55-.45,1-1,1H2c-.55,0-1-.45-1-1V5c0-.55.45-1,1-1h4v2c0,1.1.9,2,2,2h2v1Zm-2-2c-.55,0-1-.45-1-1v-1.59l1.29,1.29,1.29,1.29h-1.59Z"/></svg>
\ No newline at end of file diff --git a/src/assets/icons/scalable/edit-cut.svg b/src/assets/icons/scalable/edit-cut.svg new file mode 100644 index 0000000000..7f75d0b829 --- /dev/null +++ b/src/assets/icons/scalable/edit-cut.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><svg id="Outlined_icons" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path id="iconDesktopApp_Cut" d="m15.22,12.05l-7.39-3.55,7.39-3.55c.25-.12.35-.42.23-.67-.12-.25-.42-.35-.67-.23l-8.12,3.9-2.48-1.19c1.07-.46,1.81-1.52,1.81-2.75,0-1.66-1.34-3-3-3S0,2.34,0,4c0,1.28.81,2.36,1.93,2.8.1.05.22.1.35.15l3.23,1.55-3.23,1.55c-.1.05-.18.09-.26.13-1.18.41-2.02,1.51-2.02,2.82,0,1.66,1.34,3,3,3s3-1.34,3-3c0-1.24-.75-2.29-1.81-2.75l2.48-1.19,8.12,3.9c.07.03.14.05.22.05.19,0,.37-.1.45-.28.12-.25.01-.55-.23-.67ZM3,2c1.1,0,2,.9,2,2s-.9,2-2,2-2-.9-2-2,.9-2,2-2Zm2,11c0,1.1-.9,2-2,2s-2-.9-2-2,.9-2,2-2,2,.9,2,2Z"/></svg>
\ No newline at end of file diff --git a/src/assets/icons/scalable/edit-delete.svg b/src/assets/icons/scalable/edit-delete.svg new file mode 100644 index 0000000000..15d1a9c7fd --- /dev/null +++ b/src/assets/icons/scalable/edit-delete.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><svg id="Outlined_icons" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path id="iconDesktopApp_Delete" d="m8,0C3.58,0,0,3.58,0,8s3.58,8,8,8,8-3.58,8-8S12.42,0,8,0Zm0,15c-3.86,0-7-3.14-7-7S4.14,1,8,1s7,3.14,7,7-3.14,7-7,7Zm2.83-9.12l-2.12,2.12,2.12,2.12c.2.2.2.51,0,.71-.1.1-.23.15-.35.15s-.26-.05-.35-.15l-2.12-2.12-2.12,2.12c-.1.1-.23.15-.35.15s-.26-.05-.35-.15c-.2-.2-.2-.51,0-.71l2.12-2.12-2.12-2.12c-.2-.2-.2-.51,0-.71s.51-.2.71,0l2.12,2.12,2.12-2.12c.2-.2.51-.2.71,0s.2.51,0,.71Z"/></svg>
\ No newline at end of file diff --git a/src/assets/icons/scalable/edit-paste.svg b/src/assets/icons/scalable/edit-paste.svg new file mode 100644 index 0000000000..57e94d917d --- /dev/null +++ b/src/assets/icons/scalable/edit-paste.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><svg id="Outlined_icons" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path id="iconDesktopApp_Paste" d="m14.09,6.09c-.71-.74-1.44-1.5-2.09-2.09h-3.78c-1.23,0-2.22.9-2.22,2v8c0,1.1.99,2,2.22,2h5.56c1.23,0,2.22-.9,2.22-2v-6c-.52-.52-1.21-1.21-1.91-1.91Zm-2.09-.68l1.38,1.38c.41.43.8.83,1.18,1.21h-1.56c-.55,0-1-.45-1-1v-1.59Zm3,5.59v3c0,.55-.55,1-1.22,1h-5.56c-.67,0-1.22-.45-1.22-1V6c0-.55.55-1,1.22-1h2.78v2c0,1.1.9,2,2,2h2v2Zm-6.5,0h5c.28,0,.5.22.5.5s-.22.5-.5.5h-5c-.28,0-.5-.22-.5-.5s.22-.5.5-.5ZM5.5,3h1c.83,0,1.5-.67,1.5-1.5v-.5h2c.55,0,1,.45,1,1v1h1v-1C12,.9,11.1,0,10,0h-2s-4,0-4,0h0s-2,0-2,0C.9,0,0,.9,0,2v10c0,1.1.9,2,2,2h3v-1h-3c-.55,0-1-.45-1-1V2c0-.55.45-1,1-1h2v.5c0,.83.67,1.5,1.5,1.5Zm1.5-1.5c0,.28-.22.5-.5.5h-1c-.28,0-.5-.22-.5-.5v-.5h2v.5Zm7,12c0,.28-.22.5-.5.5h-5c-.28,0-.5-.22-.5-.5s.22-.5.5-.5h5c.28,0,.5.22.5.5Z"/></svg>
\ No newline at end of file diff --git a/src/assets/icons/scalable/edit-redo.svg b/src/assets/icons/scalable/edit-redo.svg new file mode 100644 index 0000000000..92d60e1dd8 --- /dev/null +++ b/src/assets/icons/scalable/edit-redo.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><svg id="Outlined_icons" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path id="iconDesktopApp_Redo" d="m15.85,4.68l-3.54-3.54c-.2-.2-.51-.2-.71,0s-.2.51,0,.71l2.68,2.68H2.5c-1.38,0-2.5,1.12-2.5,2.5v4c0,1.38,1.12,2.5,2.5,2.5h7c.28,0,.5-.22.5-.5s-.22-.5-.5-.5H2.5c-.83,0-1.5-.67-1.5-1.5v-4c0-.83.67-1.5,1.5-1.5h11.79l-2.68,2.68c-.2.2-.2.51,0,.71.1.1.23.15.35.15s.26-.05.35-.15l3.54-3.54c.2-.2.2-.51,0-.71Z"/></svg>
\ No newline at end of file diff --git a/src/assets/icons/scalable/edit-undo.svg b/src/assets/icons/scalable/edit-undo.svg new file mode 100644 index 0000000000..91731bb86f --- /dev/null +++ b/src/assets/icons/scalable/edit-undo.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><svg id="Outlined_icons" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path id="iconDesktopApp_Undo" d="m13.5,4.54H1.71l2.68-2.68c.2-.2.2-.51,0-.71s-.51-.2-.71,0L.15,4.68c-.2.2-.2.51,0,.71l3.54,3.54c.1.1.23.15.35.15s.26-.05.35-.15c.2-.2.2-.51,0-.71l-2.68-2.68h11.79c.83,0,1.5.67,1.5,1.5v4c0,.83-.67,1.5-1.5,1.5h-7c-.28,0-.5.22-.5.5s.22.5.5.5h7c1.38,0,2.5-1.12,2.5-2.5v-4c0-1.38-1.12-2.5-2.5-2.5Z"/></svg>
\ No newline at end of file diff --git a/src/assets/icons/scalable/format-justify-center.svg b/src/assets/icons/scalable/format-justify-center.svg new file mode 100644 index 0000000000..9822c95f2f --- /dev/null +++ b/src/assets/icons/scalable/format-justify-center.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><svg id="Outlined_icons" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path id="iconTextEditor_alignCenter" d="m15.5,8H.5c-.28,0-.5-.22-.5-.5h0c0-.28.22-.5.5-.5h15c.28,0,.5.22.5.5h0c0,.28-.22.5-.5.5Zm-2,3.5h0c0-.28-.22-.5-.5-.5H3c-.28,0-.5.22-.5.5h0c0,.28.22.5.5.5h10c.28,0,.5-.22.5-.5Zm2.5,4h0c0-.28-.22-.5-.5-.5H.5c-.28,0-.5.22-.5.5h0c0,.28.22.5.5.5h15c.28,0,.5-.22.5-.5Zm-2.5-12h0c0-.28-.22-.5-.5-.5H3c-.28,0-.5.22-.5.5h0c0,.28.22.5.5.5h10c.28,0,.5-.22.5-.5Z"/></svg>
\ No newline at end of file diff --git a/src/assets/icons/scalable/format-justify-fill.svg b/src/assets/icons/scalable/format-justify-fill.svg new file mode 100644 index 0000000000..2fa7ddfa40 --- /dev/null +++ b/src/assets/icons/scalable/format-justify-fill.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><svg id="Outlined_icons" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path id="iconTextEditor_alignJustify" d="m15.5,8H.5c-.28,0-.5-.22-.5-.5h0c0-.28.22-.5.5-.5h15c.28,0,.5.22.5.5h0c0,.28-.22.5-.5.5Zm.5,3.5h0c0-.28-.22-.5-.5-.5H.5c-.28,0-.5.22-.5.5h0c0,.28.22.5.5.5h15c.28,0,.5-.22.5-.5Zm0,4h0c0-.28-.22-.5-.5-.5H.5c-.28,0-.5.22-.5.5h0c0,.28.22.5.5.5h15c.28,0,.5-.22.5-.5Zm0-12h0c0-.28-.22-.5-.5-.5H.5c-.28,0-.5.22-.5.5h0c0,.28.22.5.5.5h15c.28,0,.5-.22.5-.5Z"/></svg>
\ No newline at end of file diff --git a/src/assets/icons/scalable/format-justify-left.svg b/src/assets/icons/scalable/format-justify-left.svg new file mode 100644 index 0000000000..99d666428d --- /dev/null +++ b/src/assets/icons/scalable/format-justify-left.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><svg id="Outlined_icons" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path id="iconTextEditor_alignLeft" d="m15.5,8H.5c-.28,0-.5-.22-.5-.5h0c0-.28.22-.5.5-.5h15c.28,0,.5.22.5.5h0c0,.28-.22.5-.5.5Zm-4.5,3.5h0c0-.28-.22-.5-.5-.5H.5c-.28,0-.5.22-.5.5h0c0,.28.22.5.5.5h10c.28,0,.5-.22.5-.5Zm5,4h0c0-.28-.22-.5-.5-.5H.5c-.28,0-.5.22-.5.5h0c0,.28.22.5.5.5h15c.28,0,.5-.22.5-.5ZM11,3.5h0c0-.28-.22-.5-.5-.5H.5c-.28,0-.5.22-.5.5h0c0,.28.22.5.5.5h10c.28,0,.5-.22.5-.5Z"/></svg>
\ No newline at end of file diff --git a/src/assets/icons/scalable/format-justify-right.svg b/src/assets/icons/scalable/format-justify-right.svg new file mode 100644 index 0000000000..7041f5e3f8 --- /dev/null +++ b/src/assets/icons/scalable/format-justify-right.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><svg id="Outlined_icons" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path id="iconTextEditor_alignRight" d="m15.5,8H.5c-.28,0-.5-.22-.5-.5h0c0-.28.22-.5.5-.5h15c.28,0,.5.22.5.5h0c0,.28-.22.5-.5.5Zm.5,3.5h0c0-.28-.22-.5-.5-.5H5.5c-.28,0-.5.22-.5.5h0c0,.28.22.5.5.5h10c.28,0,.5-.22.5-.5Zm0,4h0c0-.28-.22-.5-.5-.5H.5c-.28,0-.5.22-.5.5h0c0,.28.22.5.5.5h15c.28,0,.5-.22.5-.5Zm0-12h0c0-.28-.22-.5-.5-.5H5.5c-.28,0-.5.22-.5.5h0c0,.28.22.5.5.5h10c.28,0,.5-.22.5-.5Z"/></svg>
\ No newline at end of file diff --git a/src/assets/icons/scalable/format-text-bold.svg b/src/assets/icons/scalable/format-text-bold.svg new file mode 100644 index 0000000000..c0f43e0a69 --- /dev/null +++ b/src/assets/icons/scalable/format-text-bold.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><svg id="Outlined_icons" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path id="iconTextEditor_Bold" d="m12.06,7.75c.65-.36,1.13-.81,1.44-1.35.3-.55.46-1.29.46-2.24,0-1.45-.4-2.5-1.21-3.16-.81-.66-2.03-.99-3.67-.99H3v16h6.26c3.39,0,5.09-1.51,5.09-4.53,0-1.88-.76-3.12-2.29-3.71ZM5.59,2.24h3.34c1.59,0,2.38.74,2.38,2.22,0,1.57-.76,2.36-2.29,2.36h-3.43V2.24Zm5.45,10.98c-.43.36-1.07.54-1.93.54h-3.53v-4.74h3.48c.75,0,1.37.17,1.87.53.5.35.75.96.75,1.83s-.21,1.49-.64,1.85Z"/></svg>
\ No newline at end of file diff --git a/src/assets/icons/scalable/format-text-italic.svg b/src/assets/icons/scalable/format-text-italic.svg new file mode 100644 index 0000000000..43df7ca54d --- /dev/null +++ b/src/assets/icons/scalable/format-text-italic.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><svg id="Outlined_icons" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><polygon id="iconTextEditor_Italic" points="6.63 0 6.18 2.01 8.18 2.01 5.45 13.99 3.45 13.99 3 16 9.56 16 10.01 13.99 8.01 13.99 10.74 2.01 12.74 2.01 13.19 0 6.63 0"/></svg>
\ No newline at end of file diff --git a/src/assets/icons/scalable/format-text-underline.svg b/src/assets/icons/scalable/format-text-underline.svg new file mode 100644 index 0000000000..62778fc579 --- /dev/null +++ b/src/assets/icons/scalable/format-text-underline.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><svg id="Outlined_icons" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path id="iconTextEditor_Underline" d="m3.38,11.88c-.92-.75-1.38-1.94-1.38-3.57V0h2.44v8.35c0,1.79,1.02,2.69,3.06,2.69s3.06-.9,3.06-2.69V0h2.44v8.31c0,1.63-.46,2.82-1.39,3.57-.92.75-2.3,1.12-4.13,1.12s-3.2-.37-4.11-1.12Zm10.12,3.12H1.5c-.28,0-.5.22-.5.5s.22.5.5.5h12c.28,0,.5-.22.5-.5s-.22-.5-.5-.5Z"/></svg>
\ No newline at end of file |