summaryrefslogtreecommitdiffstats
path: root/src/assets
diff options
context:
space:
mode:
Diffstat (limited to 'src/assets')
-rw-r--r--src/assets/CMakeLists.txt7
-rw-r--r--src/assets/downloader/CMakeLists.txt26
-rw-r--r--src/assets/downloader/assetdownloader.cpp557
-rw-r--r--src/assets/downloader/assetdownloader.h112
-rw-r--r--src/assets/downloader/tasking/barrier.cpp54
-rw-r--r--src/assets/downloader/tasking/barrier.h112
-rw-r--r--src/assets/downloader/tasking/concurrentcall.h119
-rw-r--r--src/assets/downloader/tasking/networkquery.cpp58
-rw-r--r--src/assets/downloader/tasking/networkquery.h77
-rw-r--r--src/assets/downloader/tasking/qprocesstask.cpp279
-rw-r--r--src/assets/downloader/tasking/qprocesstask.h89
-rw-r--r--src/assets/downloader/tasking/tasking_global.h25
-rw-r--r--src/assets/downloader/tasking/tasktree.cpp3431
-rw-r--r--src/assets/downloader/tasking/tasktree.h642
-rw-r--r--src/assets/downloader/tasking/tasktreerunner.cpp45
-rw-r--r--src/assets/downloader/tasking/tasktreerunner.h63
-rw-r--r--src/assets/icons/128x128/document-new.pngbin0 -> 1258 bytes
-rw-r--r--src/assets/icons/128x128/document-open.pngbin0 -> 1855 bytes
-rw-r--r--src/assets/icons/128x128/document-print.pngbin0 -> 1576 bytes
-rw-r--r--src/assets/icons/128x128/document-save.pngbin0 -> 1298 bytes
-rw-r--r--src/assets/icons/128x128/edit-copy.pngbin0 -> 1480 bytes
-rw-r--r--src/assets/icons/128x128/edit-cut.pngbin0 -> 2012 bytes
-rw-r--r--src/assets/icons/128x128/edit-delete.pngbin0 -> 2665 bytes
-rw-r--r--src/assets/icons/128x128/edit-paste.pngbin0 -> 1892 bytes
-rw-r--r--src/assets/icons/128x128/edit-redo.pngbin0 -> 1239 bytes
-rw-r--r--src/assets/icons/128x128/edit-undo.pngbin0 -> 1233 bytes
-rw-r--r--src/assets/icons/128x128/format-justify-center.pngbin0 -> 663 bytes
-rw-r--r--src/assets/icons/128x128/format-justify-fill.pngbin0 -> 506 bytes
-rw-r--r--src/assets/icons/128x128/format-justify-left.pngbin0 -> 603 bytes
-rw-r--r--src/assets/icons/128x128/format-justify-right.pngbin0 -> 634 bytes
-rw-r--r--src/assets/icons/128x128/format-text-bold.pngbin0 -> 1459 bytes
-rw-r--r--src/assets/icons/128x128/format-text-italic.pngbin0 -> 1158 bytes
-rw-r--r--src/assets/icons/128x128/format-text-underline.pngbin0 -> 1263 bytes
-rw-r--r--src/assets/icons/128x128@2/document-new@2x.pngbin0 -> 2541 bytes
-rw-r--r--src/assets/icons/128x128@2/document-open@2x.pngbin0 -> 3865 bytes
-rw-r--r--src/assets/icons/128x128@2/document-print@2x.pngbin0 -> 3721 bytes
-rw-r--r--src/assets/icons/128x128@2/document-save@2x.pngbin0 -> 2682 bytes
-rw-r--r--src/assets/icons/128x128@2/edit-copy@2x.pngbin0 -> 3467 bytes
-rw-r--r--src/assets/icons/128x128@2/edit-cut@2x.pngbin0 -> 5004 bytes
-rw-r--r--src/assets/icons/128x128@2/edit-delete@2x.pngbin0 -> 5870 bytes
-rw-r--r--src/assets/icons/128x128@2/edit-paste@2x.pngbin0 -> 4005 bytes
-rw-r--r--src/assets/icons/128x128@2/edit-redo@2x.pngbin0 -> 2528 bytes
-rw-r--r--src/assets/icons/128x128@2/edit-undo@2x.pngbin0 -> 2530 bytes
-rw-r--r--src/assets/icons/128x128@2/format-justify-center@2x.pngbin0 -> 1599 bytes
-rw-r--r--src/assets/icons/128x128@2/format-justify-fill@2x.pngbin0 -> 1475 bytes
-rw-r--r--src/assets/icons/128x128@2/format-justify-left@2x.pngbin0 -> 1539 bytes
-rw-r--r--src/assets/icons/128x128@2/format-justify-right@2x.pngbin0 -> 1557 bytes
-rw-r--r--src/assets/icons/128x128@2/format-text-bold@2x.pngbin0 -> 3092 bytes
-rw-r--r--src/assets/icons/128x128@2/format-text-italic@2x.pngbin0 -> 2374 bytes
-rw-r--r--src/assets/icons/128x128@2/format-text-underline@2x.pngbin0 -> 2477 bytes
-rw-r--r--src/assets/icons/16x16/document-new.pngbin0 -> 241 bytes
-rw-r--r--src/assets/icons/16x16/document-open.pngbin0 -> 286 bytes
-rw-r--r--src/assets/icons/16x16/document-print.pngbin0 -> 261 bytes
-rw-r--r--src/assets/icons/16x16/document-save.pngbin0 -> 224 bytes
-rw-r--r--src/assets/icons/16x16/edit-copy.pngbin0 -> 216 bytes
-rw-r--r--src/assets/icons/16x16/edit-cut.pngbin0 -> 267 bytes
-rw-r--r--src/assets/icons/16x16/edit-delete.pngbin0 -> 250 bytes
-rw-r--r--src/assets/icons/16x16/edit-paste.pngbin0 -> 272 bytes
-rw-r--r--src/assets/icons/16x16/edit-redo.pngbin0 -> 253 bytes
-rw-r--r--src/assets/icons/16x16/edit-undo.pngbin0 -> 253 bytes
-rw-r--r--src/assets/icons/16x16/format-justify-center.pngbin0 -> 134 bytes
-rw-r--r--src/assets/icons/16x16/format-justify-fill.pngbin0 -> 122 bytes
-rw-r--r--src/assets/icons/16x16/format-justify-left.pngbin0 -> 128 bytes
-rw-r--r--src/assets/icons/16x16/format-justify-right.pngbin0 -> 134 bytes
-rw-r--r--src/assets/icons/16x16/format-text-bold.pngbin0 -> 264 bytes
-rw-r--r--src/assets/icons/16x16/format-text-italic.pngbin0 -> 255 bytes
-rw-r--r--src/assets/icons/16x16/format-text-underline.pngbin0 -> 186 bytes
-rw-r--r--src/assets/icons/16x16@2/document-new@2x.pngbin0 -> 318 bytes
-rw-r--r--src/assets/icons/16x16@2/document-open@2x.pngbin0 -> 455 bytes
-rw-r--r--src/assets/icons/16x16@2/document-print@2x.pngbin0 -> 393 bytes
-rw-r--r--src/assets/icons/16x16@2/document-save@2x.pngbin0 -> 311 bytes
-rw-r--r--src/assets/icons/16x16@2/edit-copy@2x.pngbin0 -> 333 bytes
-rw-r--r--src/assets/icons/16x16@2/edit-cut@2x.pngbin0 -> 416 bytes
-rw-r--r--src/assets/icons/16x16@2/edit-delete@2x.pngbin0 -> 495 bytes
-rw-r--r--src/assets/icons/16x16@2/edit-paste@2x.pngbin0 -> 464 bytes
-rw-r--r--src/assets/icons/16x16@2/edit-redo@2x.pngbin0 -> 346 bytes
-rw-r--r--src/assets/icons/16x16@2/edit-undo@2x.pngbin0 -> 347 bytes
-rw-r--r--src/assets/icons/16x16@2/format-justify-center@2x.pngbin0 -> 179 bytes
-rw-r--r--src/assets/icons/16x16@2/format-justify-fill@2x.pngbin0 -> 156 bytes
-rw-r--r--src/assets/icons/16x16@2/format-justify-left@2x.pngbin0 -> 171 bytes
-rw-r--r--src/assets/icons/16x16@2/format-justify-right@2x.pngbin0 -> 173 bytes
-rw-r--r--src/assets/icons/16x16@2/format-text-bold@2x.pngbin0 -> 430 bytes
-rw-r--r--src/assets/icons/16x16@2/format-text-italic@2x.pngbin0 -> 391 bytes
-rw-r--r--src/assets/icons/16x16@2/format-text-underline@2x.pngbin0 -> 290 bytes
-rw-r--r--src/assets/icons/256x256/document-new.pngbin0 -> 2541 bytes
-rw-r--r--src/assets/icons/256x256/document-open.pngbin0 -> 3865 bytes
-rw-r--r--src/assets/icons/256x256/document-print.pngbin0 -> 3721 bytes
-rw-r--r--src/assets/icons/256x256/document-save.pngbin0 -> 2682 bytes
-rw-r--r--src/assets/icons/256x256/edit-copy.pngbin0 -> 3467 bytes
-rw-r--r--src/assets/icons/256x256/edit-cut.pngbin0 -> 5004 bytes
-rw-r--r--src/assets/icons/256x256/edit-delete.pngbin0 -> 5870 bytes
-rw-r--r--src/assets/icons/256x256/edit-paste.pngbin0 -> 4005 bytes
-rw-r--r--src/assets/icons/256x256/edit-redo.pngbin0 -> 2528 bytes
-rw-r--r--src/assets/icons/256x256/edit-undo.pngbin0 -> 2530 bytes
-rw-r--r--src/assets/icons/256x256/format-justify-center.pngbin0 -> 1599 bytes
-rw-r--r--src/assets/icons/256x256/format-justify-fill.pngbin0 -> 1475 bytes
-rw-r--r--src/assets/icons/256x256/format-justify-left.pngbin0 -> 1539 bytes
-rw-r--r--src/assets/icons/256x256/format-justify-right.pngbin0 -> 1557 bytes
-rw-r--r--src/assets/icons/256x256/format-text-bold.pngbin0 -> 3092 bytes
-rw-r--r--src/assets/icons/256x256/format-text-italic.pngbin0 -> 2374 bytes
-rw-r--r--src/assets/icons/256x256/format-text-underline.pngbin0 -> 2477 bytes
-rw-r--r--src/assets/icons/256x256@2/document-new@2x.pngbin0 -> 5683 bytes
-rw-r--r--src/assets/icons/256x256@2/document-open@2x.pngbin0 -> 8174 bytes
-rw-r--r--src/assets/icons/256x256@2/document-print@2x.pngbin0 -> 8055 bytes
-rw-r--r--src/assets/icons/256x256@2/document-save@2x.pngbin0 -> 5666 bytes
-rw-r--r--src/assets/icons/256x256@2/edit-copy@2x.pngbin0 -> 7329 bytes
-rw-r--r--src/assets/icons/256x256@2/edit-cut@2x.pngbin0 -> 11388 bytes
-rw-r--r--src/assets/icons/256x256@2/edit-delete@2x.pngbin0 -> 12743 bytes
-rw-r--r--src/assets/icons/256x256@2/edit-paste@2x.pngbin0 -> 8485 bytes
-rw-r--r--src/assets/icons/256x256@2/edit-redo@2x.pngbin0 -> 5378 bytes
-rw-r--r--src/assets/icons/256x256@2/edit-undo@2x.pngbin0 -> 5433 bytes
-rw-r--r--src/assets/icons/256x256@2/format-justify-center@2x.pngbin0 -> 3855 bytes
-rw-r--r--src/assets/icons/256x256@2/format-justify-fill@2x.pngbin0 -> 3681 bytes
-rw-r--r--src/assets/icons/256x256@2/format-justify-left@2x.pngbin0 -> 3775 bytes
-rw-r--r--src/assets/icons/256x256@2/format-justify-right@2x.pngbin0 -> 3756 bytes
-rw-r--r--src/assets/icons/256x256@2/format-text-bold@2x.pngbin0 -> 7046 bytes
-rw-r--r--src/assets/icons/256x256@2/format-text-italic@2x.pngbin0 -> 6224 bytes
-rw-r--r--src/assets/icons/256x256@2/format-text-underline@2x.pngbin0 -> 5168 bytes
-rw-r--r--src/assets/icons/32x32/document-new.pngbin0 -> 318 bytes
-rw-r--r--src/assets/icons/32x32/document-open.pngbin0 -> 455 bytes
-rw-r--r--src/assets/icons/32x32/document-print.pngbin0 -> 393 bytes
-rw-r--r--src/assets/icons/32x32/document-save.pngbin0 -> 311 bytes
-rw-r--r--src/assets/icons/32x32/edit-copy.pngbin0 -> 333 bytes
-rw-r--r--src/assets/icons/32x32/edit-cut.pngbin0 -> 416 bytes
-rw-r--r--src/assets/icons/32x32/edit-delete.pngbin0 -> 495 bytes
-rw-r--r--src/assets/icons/32x32/edit-paste.pngbin0 -> 464 bytes
-rw-r--r--src/assets/icons/32x32/edit-redo.pngbin0 -> 346 bytes
-rw-r--r--src/assets/icons/32x32/edit-undo.pngbin0 -> 347 bytes
-rw-r--r--src/assets/icons/32x32/format-justify-center.pngbin0 -> 179 bytes
-rw-r--r--src/assets/icons/32x32/format-justify-fill.pngbin0 -> 156 bytes
-rw-r--r--src/assets/icons/32x32/format-justify-left.pngbin0 -> 171 bytes
-rw-r--r--src/assets/icons/32x32/format-justify-right.pngbin0 -> 173 bytes
-rw-r--r--src/assets/icons/32x32/format-text-bold.pngbin0 -> 430 bytes
-rw-r--r--src/assets/icons/32x32/format-text-italic.pngbin0 -> 391 bytes
-rw-r--r--src/assets/icons/32x32/format-text-underline.pngbin0 -> 290 bytes
-rw-r--r--src/assets/icons/32x32@2/document-new@2x.pngbin0 -> 588 bytes
-rw-r--r--src/assets/icons/32x32@2/document-open@2x.pngbin0 -> 923 bytes
-rw-r--r--src/assets/icons/32x32@2/document-print@2x.pngbin0 -> 762 bytes
-rw-r--r--src/assets/icons/32x32@2/document-save@2x.pngbin0 -> 565 bytes
-rw-r--r--src/assets/icons/32x32@2/edit-copy@2x.pngbin0 -> 710 bytes
-rw-r--r--src/assets/icons/32x32@2/edit-cut@2x.pngbin0 -> 843 bytes
-rw-r--r--src/assets/icons/32x32@2/edit-delete@2x.pngbin0 -> 954 bytes
-rw-r--r--src/assets/icons/32x32@2/edit-paste@2x.pngbin0 -> 909 bytes
-rw-r--r--src/assets/icons/32x32@2/edit-redo@2x.pngbin0 -> 628 bytes
-rw-r--r--src/assets/icons/32x32@2/edit-undo@2x.pngbin0 -> 608 bytes
-rw-r--r--src/assets/icons/32x32@2/format-justify-center@2x.pngbin0 -> 317 bytes
-rw-r--r--src/assets/icons/32x32@2/format-justify-fill@2x.pngbin0 -> 266 bytes
-rw-r--r--src/assets/icons/32x32@2/format-justify-left@2x.pngbin0 -> 298 bytes
-rw-r--r--src/assets/icons/32x32@2/format-justify-right@2x.pngbin0 -> 305 bytes
-rw-r--r--src/assets/icons/32x32@2/format-text-bold@2x.pngbin0 -> 777 bytes
-rw-r--r--src/assets/icons/32x32@2/format-text-italic@2x.pngbin0 -> 657 bytes
-rw-r--r--src/assets/icons/32x32@2/format-text-underline@2x.pngbin0 -> 577 bytes
-rw-r--r--src/assets/icons/CMakeLists.txt174
-rw-r--r--src/assets/icons/README29
-rw-r--r--src/assets/icons/index.theme46
-rw-r--r--src/assets/icons/scalable/document-new.svg1
-rw-r--r--src/assets/icons/scalable/document-open.svg1
-rw-r--r--src/assets/icons/scalable/document-print.svg1
-rw-r--r--src/assets/icons/scalable/document-save.svg1
-rw-r--r--src/assets/icons/scalable/edit-copy.svg1
-rw-r--r--src/assets/icons/scalable/edit-cut.svg1
-rw-r--r--src/assets/icons/scalable/edit-delete.svg1
-rw-r--r--src/assets/icons/scalable/edit-paste.svg1
-rw-r--r--src/assets/icons/scalable/edit-redo.svg1
-rw-r--r--src/assets/icons/scalable/edit-undo.svg1
-rw-r--r--src/assets/icons/scalable/format-justify-center.svg1
-rw-r--r--src/assets/icons/scalable/format-justify-fill.svg1
-rw-r--r--src/assets/icons/scalable/format-justify-left.svg1
-rw-r--r--src/assets/icons/scalable/format-justify-right.svg1
-rw-r--r--src/assets/icons/scalable/format-text-bold.svg1
-rw-r--r--src/assets/icons/scalable/format-text-italic.svg1
-rw-r--r--src/assets/icons/scalable/format-text-underline.svg1
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
new file mode 100644
index 0000000000..8d86a4827a
--- /dev/null
+++ b/src/assets/icons/128x128/document-new.png
Binary files differ
diff --git a/src/assets/icons/128x128/document-open.png b/src/assets/icons/128x128/document-open.png
new file mode 100644
index 0000000000..2183dbbea6
--- /dev/null
+++ b/src/assets/icons/128x128/document-open.png
Binary files differ
diff --git a/src/assets/icons/128x128/document-print.png b/src/assets/icons/128x128/document-print.png
new file mode 100644
index 0000000000..9e7378aab2
--- /dev/null
+++ b/src/assets/icons/128x128/document-print.png
Binary files differ
diff --git a/src/assets/icons/128x128/document-save.png b/src/assets/icons/128x128/document-save.png
new file mode 100644
index 0000000000..e8b2840643
--- /dev/null
+++ b/src/assets/icons/128x128/document-save.png
Binary files differ
diff --git a/src/assets/icons/128x128/edit-copy.png b/src/assets/icons/128x128/edit-copy.png
new file mode 100644
index 0000000000..7585f4baa0
--- /dev/null
+++ b/src/assets/icons/128x128/edit-copy.png
Binary files differ
diff --git a/src/assets/icons/128x128/edit-cut.png b/src/assets/icons/128x128/edit-cut.png
new file mode 100644
index 0000000000..51ede2fe37
--- /dev/null
+++ b/src/assets/icons/128x128/edit-cut.png
Binary files differ
diff --git a/src/assets/icons/128x128/edit-delete.png b/src/assets/icons/128x128/edit-delete.png
new file mode 100644
index 0000000000..bdf785c828
--- /dev/null
+++ b/src/assets/icons/128x128/edit-delete.png
Binary files differ
diff --git a/src/assets/icons/128x128/edit-paste.png b/src/assets/icons/128x128/edit-paste.png
new file mode 100644
index 0000000000..690ffa172d
--- /dev/null
+++ b/src/assets/icons/128x128/edit-paste.png
Binary files differ
diff --git a/src/assets/icons/128x128/edit-redo.png b/src/assets/icons/128x128/edit-redo.png
new file mode 100644
index 0000000000..f1c97f71c2
--- /dev/null
+++ b/src/assets/icons/128x128/edit-redo.png
Binary files differ
diff --git a/src/assets/icons/128x128/edit-undo.png b/src/assets/icons/128x128/edit-undo.png
new file mode 100644
index 0000000000..e728cbf6e0
--- /dev/null
+++ b/src/assets/icons/128x128/edit-undo.png
Binary files differ
diff --git a/src/assets/icons/128x128/format-justify-center.png b/src/assets/icons/128x128/format-justify-center.png
new file mode 100644
index 0000000000..44ceb2af4d
--- /dev/null
+++ b/src/assets/icons/128x128/format-justify-center.png
Binary files differ
diff --git a/src/assets/icons/128x128/format-justify-fill.png b/src/assets/icons/128x128/format-justify-fill.png
new file mode 100644
index 0000000000..b99a850704
--- /dev/null
+++ b/src/assets/icons/128x128/format-justify-fill.png
Binary files differ
diff --git a/src/assets/icons/128x128/format-justify-left.png b/src/assets/icons/128x128/format-justify-left.png
new file mode 100644
index 0000000000..2b63887b49
--- /dev/null
+++ b/src/assets/icons/128x128/format-justify-left.png
Binary files differ
diff --git a/src/assets/icons/128x128/format-justify-right.png b/src/assets/icons/128x128/format-justify-right.png
new file mode 100644
index 0000000000..6c61889d59
--- /dev/null
+++ b/src/assets/icons/128x128/format-justify-right.png
Binary files differ
diff --git a/src/assets/icons/128x128/format-text-bold.png b/src/assets/icons/128x128/format-text-bold.png
new file mode 100644
index 0000000000..96a5ca88a2
--- /dev/null
+++ b/src/assets/icons/128x128/format-text-bold.png
Binary files differ
diff --git a/src/assets/icons/128x128/format-text-italic.png b/src/assets/icons/128x128/format-text-italic.png
new file mode 100644
index 0000000000..2bb71b4a4d
--- /dev/null
+++ b/src/assets/icons/128x128/format-text-italic.png
Binary files differ
diff --git a/src/assets/icons/128x128/format-text-underline.png b/src/assets/icons/128x128/format-text-underline.png
new file mode 100644
index 0000000000..ecf6830c92
--- /dev/null
+++ b/src/assets/icons/128x128/format-text-underline.png
Binary files differ
diff --git a/src/assets/icons/128x128@2/document-new@2x.png b/src/assets/icons/128x128@2/document-new@2x.png
new file mode 100644
index 0000000000..32776b51a9
--- /dev/null
+++ b/src/assets/icons/128x128@2/document-new@2x.png
Binary files differ
diff --git a/src/assets/icons/128x128@2/document-open@2x.png b/src/assets/icons/128x128@2/document-open@2x.png
new file mode 100644
index 0000000000..06e188b93b
--- /dev/null
+++ b/src/assets/icons/128x128@2/document-open@2x.png
Binary files differ
diff --git a/src/assets/icons/128x128@2/document-print@2x.png b/src/assets/icons/128x128@2/document-print@2x.png
new file mode 100644
index 0000000000..644e3c149a
--- /dev/null
+++ b/src/assets/icons/128x128@2/document-print@2x.png
Binary files differ
diff --git a/src/assets/icons/128x128@2/document-save@2x.png b/src/assets/icons/128x128@2/document-save@2x.png
new file mode 100644
index 0000000000..16fa70493a
--- /dev/null
+++ b/src/assets/icons/128x128@2/document-save@2x.png
Binary files differ
diff --git a/src/assets/icons/128x128@2/edit-copy@2x.png b/src/assets/icons/128x128@2/edit-copy@2x.png
new file mode 100644
index 0000000000..b18bead117
--- /dev/null
+++ b/src/assets/icons/128x128@2/edit-copy@2x.png
Binary files differ
diff --git a/src/assets/icons/128x128@2/edit-cut@2x.png b/src/assets/icons/128x128@2/edit-cut@2x.png
new file mode 100644
index 0000000000..d9454cebf1
--- /dev/null
+++ b/src/assets/icons/128x128@2/edit-cut@2x.png
Binary files differ
diff --git a/src/assets/icons/128x128@2/edit-delete@2x.png b/src/assets/icons/128x128@2/edit-delete@2x.png
new file mode 100644
index 0000000000..4081cdb2ca
--- /dev/null
+++ b/src/assets/icons/128x128@2/edit-delete@2x.png
Binary files differ
diff --git a/src/assets/icons/128x128@2/edit-paste@2x.png b/src/assets/icons/128x128@2/edit-paste@2x.png
new file mode 100644
index 0000000000..3358426818
--- /dev/null
+++ b/src/assets/icons/128x128@2/edit-paste@2x.png
Binary files differ
diff --git a/src/assets/icons/128x128@2/edit-redo@2x.png b/src/assets/icons/128x128@2/edit-redo@2x.png
new file mode 100644
index 0000000000..e28b28542c
--- /dev/null
+++ b/src/assets/icons/128x128@2/edit-redo@2x.png
Binary files differ
diff --git a/src/assets/icons/128x128@2/edit-undo@2x.png b/src/assets/icons/128x128@2/edit-undo@2x.png
new file mode 100644
index 0000000000..fe10f57a39
--- /dev/null
+++ b/src/assets/icons/128x128@2/edit-undo@2x.png
Binary files differ
diff --git a/src/assets/icons/128x128@2/format-justify-center@2x.png b/src/assets/icons/128x128@2/format-justify-center@2x.png
new file mode 100644
index 0000000000..d4ad74b0d0
--- /dev/null
+++ b/src/assets/icons/128x128@2/format-justify-center@2x.png
Binary files differ
diff --git a/src/assets/icons/128x128@2/format-justify-fill@2x.png b/src/assets/icons/128x128@2/format-justify-fill@2x.png
new file mode 100644
index 0000000000..bf0dd84bbb
--- /dev/null
+++ b/src/assets/icons/128x128@2/format-justify-fill@2x.png
Binary files differ
diff --git a/src/assets/icons/128x128@2/format-justify-left@2x.png b/src/assets/icons/128x128@2/format-justify-left@2x.png
new file mode 100644
index 0000000000..dde68c8514
--- /dev/null
+++ b/src/assets/icons/128x128@2/format-justify-left@2x.png
Binary files differ
diff --git a/src/assets/icons/128x128@2/format-justify-right@2x.png b/src/assets/icons/128x128@2/format-justify-right@2x.png
new file mode 100644
index 0000000000..8a5e7518bd
--- /dev/null
+++ b/src/assets/icons/128x128@2/format-justify-right@2x.png
Binary files differ
diff --git a/src/assets/icons/128x128@2/format-text-bold@2x.png b/src/assets/icons/128x128@2/format-text-bold@2x.png
new file mode 100644
index 0000000000..665d3ce37b
--- /dev/null
+++ b/src/assets/icons/128x128@2/format-text-bold@2x.png
Binary files differ
diff --git a/src/assets/icons/128x128@2/format-text-italic@2x.png b/src/assets/icons/128x128@2/format-text-italic@2x.png
new file mode 100644
index 0000000000..4b6846a6b9
--- /dev/null
+++ b/src/assets/icons/128x128@2/format-text-italic@2x.png
Binary files differ
diff --git a/src/assets/icons/128x128@2/format-text-underline@2x.png b/src/assets/icons/128x128@2/format-text-underline@2x.png
new file mode 100644
index 0000000000..601f73216a
--- /dev/null
+++ b/src/assets/icons/128x128@2/format-text-underline@2x.png
Binary files differ
diff --git a/src/assets/icons/16x16/document-new.png b/src/assets/icons/16x16/document-new.png
new file mode 100644
index 0000000000..893e7e1aec
--- /dev/null
+++ b/src/assets/icons/16x16/document-new.png
Binary files differ
diff --git a/src/assets/icons/16x16/document-open.png b/src/assets/icons/16x16/document-open.png
new file mode 100644
index 0000000000..b07906f40b
--- /dev/null
+++ b/src/assets/icons/16x16/document-open.png
Binary files differ
diff --git a/src/assets/icons/16x16/document-print.png b/src/assets/icons/16x16/document-print.png
new file mode 100644
index 0000000000..9341060076
--- /dev/null
+++ b/src/assets/icons/16x16/document-print.png
Binary files differ
diff --git a/src/assets/icons/16x16/document-save.png b/src/assets/icons/16x16/document-save.png
new file mode 100644
index 0000000000..6238718191
--- /dev/null
+++ b/src/assets/icons/16x16/document-save.png
Binary files differ
diff --git a/src/assets/icons/16x16/edit-copy.png b/src/assets/icons/16x16/edit-copy.png
new file mode 100644
index 0000000000..585f5bfc8d
--- /dev/null
+++ b/src/assets/icons/16x16/edit-copy.png
Binary files differ
diff --git a/src/assets/icons/16x16/edit-cut.png b/src/assets/icons/16x16/edit-cut.png
new file mode 100644
index 0000000000..661ef1ad03
--- /dev/null
+++ b/src/assets/icons/16x16/edit-cut.png
Binary files differ
diff --git a/src/assets/icons/16x16/edit-delete.png b/src/assets/icons/16x16/edit-delete.png
new file mode 100644
index 0000000000..7b5998df8a
--- /dev/null
+++ b/src/assets/icons/16x16/edit-delete.png
Binary files differ
diff --git a/src/assets/icons/16x16/edit-paste.png b/src/assets/icons/16x16/edit-paste.png
new file mode 100644
index 0000000000..6318a22caf
--- /dev/null
+++ b/src/assets/icons/16x16/edit-paste.png
Binary files differ
diff --git a/src/assets/icons/16x16/edit-redo.png b/src/assets/icons/16x16/edit-redo.png
new file mode 100644
index 0000000000..7eb10fe899
--- /dev/null
+++ b/src/assets/icons/16x16/edit-redo.png
Binary files differ
diff --git a/src/assets/icons/16x16/edit-undo.png b/src/assets/icons/16x16/edit-undo.png
new file mode 100644
index 0000000000..108712547c
--- /dev/null
+++ b/src/assets/icons/16x16/edit-undo.png
Binary files differ
diff --git a/src/assets/icons/16x16/format-justify-center.png b/src/assets/icons/16x16/format-justify-center.png
new file mode 100644
index 0000000000..6b0951fa5d
--- /dev/null
+++ b/src/assets/icons/16x16/format-justify-center.png
Binary files differ
diff --git a/src/assets/icons/16x16/format-justify-fill.png b/src/assets/icons/16x16/format-justify-fill.png
new file mode 100644
index 0000000000..6e1c10d7c4
--- /dev/null
+++ b/src/assets/icons/16x16/format-justify-fill.png
Binary files differ
diff --git a/src/assets/icons/16x16/format-justify-left.png b/src/assets/icons/16x16/format-justify-left.png
new file mode 100644
index 0000000000..9dfdc89b68
--- /dev/null
+++ b/src/assets/icons/16x16/format-justify-left.png
Binary files differ
diff --git a/src/assets/icons/16x16/format-justify-right.png b/src/assets/icons/16x16/format-justify-right.png
new file mode 100644
index 0000000000..36a52081f1
--- /dev/null
+++ b/src/assets/icons/16x16/format-justify-right.png
Binary files differ
diff --git a/src/assets/icons/16x16/format-text-bold.png b/src/assets/icons/16x16/format-text-bold.png
new file mode 100644
index 0000000000..a079317a94
--- /dev/null
+++ b/src/assets/icons/16x16/format-text-bold.png
Binary files differ
diff --git a/src/assets/icons/16x16/format-text-italic.png b/src/assets/icons/16x16/format-text-italic.png
new file mode 100644
index 0000000000..04202b2842
--- /dev/null
+++ b/src/assets/icons/16x16/format-text-italic.png
Binary files differ
diff --git a/src/assets/icons/16x16/format-text-underline.png b/src/assets/icons/16x16/format-text-underline.png
new file mode 100644
index 0000000000..a80368212d
--- /dev/null
+++ b/src/assets/icons/16x16/format-text-underline.png
Binary files differ
diff --git a/src/assets/icons/16x16@2/document-new@2x.png b/src/assets/icons/16x16@2/document-new@2x.png
new file mode 100644
index 0000000000..482ae52024
--- /dev/null
+++ b/src/assets/icons/16x16@2/document-new@2x.png
Binary files differ
diff --git a/src/assets/icons/16x16@2/document-open@2x.png b/src/assets/icons/16x16@2/document-open@2x.png
new file mode 100644
index 0000000000..9858b146f4
--- /dev/null
+++ b/src/assets/icons/16x16@2/document-open@2x.png
Binary files differ
diff --git a/src/assets/icons/16x16@2/document-print@2x.png b/src/assets/icons/16x16@2/document-print@2x.png
new file mode 100644
index 0000000000..1672ec5897
--- /dev/null
+++ b/src/assets/icons/16x16@2/document-print@2x.png
Binary files differ
diff --git a/src/assets/icons/16x16@2/document-save@2x.png b/src/assets/icons/16x16@2/document-save@2x.png
new file mode 100644
index 0000000000..f04de74673
--- /dev/null
+++ b/src/assets/icons/16x16@2/document-save@2x.png
Binary files differ
diff --git a/src/assets/icons/16x16@2/edit-copy@2x.png b/src/assets/icons/16x16@2/edit-copy@2x.png
new file mode 100644
index 0000000000..bbb34cc4c2
--- /dev/null
+++ b/src/assets/icons/16x16@2/edit-copy@2x.png
Binary files differ
diff --git a/src/assets/icons/16x16@2/edit-cut@2x.png b/src/assets/icons/16x16@2/edit-cut@2x.png
new file mode 100644
index 0000000000..d89ef6c016
--- /dev/null
+++ b/src/assets/icons/16x16@2/edit-cut@2x.png
Binary files differ
diff --git a/src/assets/icons/16x16@2/edit-delete@2x.png b/src/assets/icons/16x16@2/edit-delete@2x.png
new file mode 100644
index 0000000000..4c97ee2495
--- /dev/null
+++ b/src/assets/icons/16x16@2/edit-delete@2x.png
Binary files differ
diff --git a/src/assets/icons/16x16@2/edit-paste@2x.png b/src/assets/icons/16x16@2/edit-paste@2x.png
new file mode 100644
index 0000000000..299fa77686
--- /dev/null
+++ b/src/assets/icons/16x16@2/edit-paste@2x.png
Binary files differ
diff --git a/src/assets/icons/16x16@2/edit-redo@2x.png b/src/assets/icons/16x16@2/edit-redo@2x.png
new file mode 100644
index 0000000000..4f8849c711
--- /dev/null
+++ b/src/assets/icons/16x16@2/edit-redo@2x.png
Binary files differ
diff --git a/src/assets/icons/16x16@2/edit-undo@2x.png b/src/assets/icons/16x16@2/edit-undo@2x.png
new file mode 100644
index 0000000000..b3d366c53f
--- /dev/null
+++ b/src/assets/icons/16x16@2/edit-undo@2x.png
Binary files differ
diff --git a/src/assets/icons/16x16@2/format-justify-center@2x.png b/src/assets/icons/16x16@2/format-justify-center@2x.png
new file mode 100644
index 0000000000..80c3afd9a6
--- /dev/null
+++ b/src/assets/icons/16x16@2/format-justify-center@2x.png
Binary files differ
diff --git a/src/assets/icons/16x16@2/format-justify-fill@2x.png b/src/assets/icons/16x16@2/format-justify-fill@2x.png
new file mode 100644
index 0000000000..33589ea25d
--- /dev/null
+++ b/src/assets/icons/16x16@2/format-justify-fill@2x.png
Binary files differ
diff --git a/src/assets/icons/16x16@2/format-justify-left@2x.png b/src/assets/icons/16x16@2/format-justify-left@2x.png
new file mode 100644
index 0000000000..ba02821135
--- /dev/null
+++ b/src/assets/icons/16x16@2/format-justify-left@2x.png
Binary files differ
diff --git a/src/assets/icons/16x16@2/format-justify-right@2x.png b/src/assets/icons/16x16@2/format-justify-right@2x.png
new file mode 100644
index 0000000000..8e15d0cb44
--- /dev/null
+++ b/src/assets/icons/16x16@2/format-justify-right@2x.png
Binary files differ
diff --git a/src/assets/icons/16x16@2/format-text-bold@2x.png b/src/assets/icons/16x16@2/format-text-bold@2x.png
new file mode 100644
index 0000000000..754efdd975
--- /dev/null
+++ b/src/assets/icons/16x16@2/format-text-bold@2x.png
Binary files differ
diff --git a/src/assets/icons/16x16@2/format-text-italic@2x.png b/src/assets/icons/16x16@2/format-text-italic@2x.png
new file mode 100644
index 0000000000..6db31a4f69
--- /dev/null
+++ b/src/assets/icons/16x16@2/format-text-italic@2x.png
Binary files differ
diff --git a/src/assets/icons/16x16@2/format-text-underline@2x.png b/src/assets/icons/16x16@2/format-text-underline@2x.png
new file mode 100644
index 0000000000..977cde9d97
--- /dev/null
+++ b/src/assets/icons/16x16@2/format-text-underline@2x.png
Binary files differ
diff --git a/src/assets/icons/256x256/document-new.png b/src/assets/icons/256x256/document-new.png
new file mode 100644
index 0000000000..32776b51a9
--- /dev/null
+++ b/src/assets/icons/256x256/document-new.png
Binary files differ
diff --git a/src/assets/icons/256x256/document-open.png b/src/assets/icons/256x256/document-open.png
new file mode 100644
index 0000000000..06e188b93b
--- /dev/null
+++ b/src/assets/icons/256x256/document-open.png
Binary files differ
diff --git a/src/assets/icons/256x256/document-print.png b/src/assets/icons/256x256/document-print.png
new file mode 100644
index 0000000000..644e3c149a
--- /dev/null
+++ b/src/assets/icons/256x256/document-print.png
Binary files differ
diff --git a/src/assets/icons/256x256/document-save.png b/src/assets/icons/256x256/document-save.png
new file mode 100644
index 0000000000..16fa70493a
--- /dev/null
+++ b/src/assets/icons/256x256/document-save.png
Binary files differ
diff --git a/src/assets/icons/256x256/edit-copy.png b/src/assets/icons/256x256/edit-copy.png
new file mode 100644
index 0000000000..b18bead117
--- /dev/null
+++ b/src/assets/icons/256x256/edit-copy.png
Binary files differ
diff --git a/src/assets/icons/256x256/edit-cut.png b/src/assets/icons/256x256/edit-cut.png
new file mode 100644
index 0000000000..d9454cebf1
--- /dev/null
+++ b/src/assets/icons/256x256/edit-cut.png
Binary files differ
diff --git a/src/assets/icons/256x256/edit-delete.png b/src/assets/icons/256x256/edit-delete.png
new file mode 100644
index 0000000000..4081cdb2ca
--- /dev/null
+++ b/src/assets/icons/256x256/edit-delete.png
Binary files differ
diff --git a/src/assets/icons/256x256/edit-paste.png b/src/assets/icons/256x256/edit-paste.png
new file mode 100644
index 0000000000..3358426818
--- /dev/null
+++ b/src/assets/icons/256x256/edit-paste.png
Binary files differ
diff --git a/src/assets/icons/256x256/edit-redo.png b/src/assets/icons/256x256/edit-redo.png
new file mode 100644
index 0000000000..e28b28542c
--- /dev/null
+++ b/src/assets/icons/256x256/edit-redo.png
Binary files differ
diff --git a/src/assets/icons/256x256/edit-undo.png b/src/assets/icons/256x256/edit-undo.png
new file mode 100644
index 0000000000..fe10f57a39
--- /dev/null
+++ b/src/assets/icons/256x256/edit-undo.png
Binary files differ
diff --git a/src/assets/icons/256x256/format-justify-center.png b/src/assets/icons/256x256/format-justify-center.png
new file mode 100644
index 0000000000..d4ad74b0d0
--- /dev/null
+++ b/src/assets/icons/256x256/format-justify-center.png
Binary files differ
diff --git a/src/assets/icons/256x256/format-justify-fill.png b/src/assets/icons/256x256/format-justify-fill.png
new file mode 100644
index 0000000000..bf0dd84bbb
--- /dev/null
+++ b/src/assets/icons/256x256/format-justify-fill.png
Binary files differ
diff --git a/src/assets/icons/256x256/format-justify-left.png b/src/assets/icons/256x256/format-justify-left.png
new file mode 100644
index 0000000000..dde68c8514
--- /dev/null
+++ b/src/assets/icons/256x256/format-justify-left.png
Binary files differ
diff --git a/src/assets/icons/256x256/format-justify-right.png b/src/assets/icons/256x256/format-justify-right.png
new file mode 100644
index 0000000000..8a5e7518bd
--- /dev/null
+++ b/src/assets/icons/256x256/format-justify-right.png
Binary files differ
diff --git a/src/assets/icons/256x256/format-text-bold.png b/src/assets/icons/256x256/format-text-bold.png
new file mode 100644
index 0000000000..665d3ce37b
--- /dev/null
+++ b/src/assets/icons/256x256/format-text-bold.png
Binary files differ
diff --git a/src/assets/icons/256x256/format-text-italic.png b/src/assets/icons/256x256/format-text-italic.png
new file mode 100644
index 0000000000..4b6846a6b9
--- /dev/null
+++ b/src/assets/icons/256x256/format-text-italic.png
Binary files differ
diff --git a/src/assets/icons/256x256/format-text-underline.png b/src/assets/icons/256x256/format-text-underline.png
new file mode 100644
index 0000000000..601f73216a
--- /dev/null
+++ b/src/assets/icons/256x256/format-text-underline.png
Binary files differ
diff --git a/src/assets/icons/256x256@2/document-new@2x.png b/src/assets/icons/256x256@2/document-new@2x.png
new file mode 100644
index 0000000000..bfec6d0e6d
--- /dev/null
+++ b/src/assets/icons/256x256@2/document-new@2x.png
Binary files differ
diff --git a/src/assets/icons/256x256@2/document-open@2x.png b/src/assets/icons/256x256@2/document-open@2x.png
new file mode 100644
index 0000000000..630a05f622
--- /dev/null
+++ b/src/assets/icons/256x256@2/document-open@2x.png
Binary files differ
diff --git a/src/assets/icons/256x256@2/document-print@2x.png b/src/assets/icons/256x256@2/document-print@2x.png
new file mode 100644
index 0000000000..c8611c31c4
--- /dev/null
+++ b/src/assets/icons/256x256@2/document-print@2x.png
Binary files differ
diff --git a/src/assets/icons/256x256@2/document-save@2x.png b/src/assets/icons/256x256@2/document-save@2x.png
new file mode 100644
index 0000000000..6f46095981
--- /dev/null
+++ b/src/assets/icons/256x256@2/document-save@2x.png
Binary files differ
diff --git a/src/assets/icons/256x256@2/edit-copy@2x.png b/src/assets/icons/256x256@2/edit-copy@2x.png
new file mode 100644
index 0000000000..2f350041a0
--- /dev/null
+++ b/src/assets/icons/256x256@2/edit-copy@2x.png
Binary files differ
diff --git a/src/assets/icons/256x256@2/edit-cut@2x.png b/src/assets/icons/256x256@2/edit-cut@2x.png
new file mode 100644
index 0000000000..e11cf6d234
--- /dev/null
+++ b/src/assets/icons/256x256@2/edit-cut@2x.png
Binary files differ
diff --git a/src/assets/icons/256x256@2/edit-delete@2x.png b/src/assets/icons/256x256@2/edit-delete@2x.png
new file mode 100644
index 0000000000..efe6b90bf5
--- /dev/null
+++ b/src/assets/icons/256x256@2/edit-delete@2x.png
Binary files differ
diff --git a/src/assets/icons/256x256@2/edit-paste@2x.png b/src/assets/icons/256x256@2/edit-paste@2x.png
new file mode 100644
index 0000000000..32f54b3959
--- /dev/null
+++ b/src/assets/icons/256x256@2/edit-paste@2x.png
Binary files differ
diff --git a/src/assets/icons/256x256@2/edit-redo@2x.png b/src/assets/icons/256x256@2/edit-redo@2x.png
new file mode 100644
index 0000000000..1f6e366535
--- /dev/null
+++ b/src/assets/icons/256x256@2/edit-redo@2x.png
Binary files differ
diff --git a/src/assets/icons/256x256@2/edit-undo@2x.png b/src/assets/icons/256x256@2/edit-undo@2x.png
new file mode 100644
index 0000000000..980ed37062
--- /dev/null
+++ b/src/assets/icons/256x256@2/edit-undo@2x.png
Binary files differ
diff --git a/src/assets/icons/256x256@2/format-justify-center@2x.png b/src/assets/icons/256x256@2/format-justify-center@2x.png
new file mode 100644
index 0000000000..af7044ddee
--- /dev/null
+++ b/src/assets/icons/256x256@2/format-justify-center@2x.png
Binary files differ
diff --git a/src/assets/icons/256x256@2/format-justify-fill@2x.png b/src/assets/icons/256x256@2/format-justify-fill@2x.png
new file mode 100644
index 0000000000..da14563bd6
--- /dev/null
+++ b/src/assets/icons/256x256@2/format-justify-fill@2x.png
Binary files differ
diff --git a/src/assets/icons/256x256@2/format-justify-left@2x.png b/src/assets/icons/256x256@2/format-justify-left@2x.png
new file mode 100644
index 0000000000..c1025bf010
--- /dev/null
+++ b/src/assets/icons/256x256@2/format-justify-left@2x.png
Binary files differ
diff --git a/src/assets/icons/256x256@2/format-justify-right@2x.png b/src/assets/icons/256x256@2/format-justify-right@2x.png
new file mode 100644
index 0000000000..3a07e06e0f
--- /dev/null
+++ b/src/assets/icons/256x256@2/format-justify-right@2x.png
Binary files differ
diff --git a/src/assets/icons/256x256@2/format-text-bold@2x.png b/src/assets/icons/256x256@2/format-text-bold@2x.png
new file mode 100644
index 0000000000..b0f4cb0995
--- /dev/null
+++ b/src/assets/icons/256x256@2/format-text-bold@2x.png
Binary files differ
diff --git a/src/assets/icons/256x256@2/format-text-italic@2x.png b/src/assets/icons/256x256@2/format-text-italic@2x.png
new file mode 100644
index 0000000000..85f0cfc1d6
--- /dev/null
+++ b/src/assets/icons/256x256@2/format-text-italic@2x.png
Binary files differ
diff --git a/src/assets/icons/256x256@2/format-text-underline@2x.png b/src/assets/icons/256x256@2/format-text-underline@2x.png
new file mode 100644
index 0000000000..51ee0aa778
--- /dev/null
+++ b/src/assets/icons/256x256@2/format-text-underline@2x.png
Binary files differ
diff --git a/src/assets/icons/32x32/document-new.png b/src/assets/icons/32x32/document-new.png
new file mode 100644
index 0000000000..482ae52024
--- /dev/null
+++ b/src/assets/icons/32x32/document-new.png
Binary files differ
diff --git a/src/assets/icons/32x32/document-open.png b/src/assets/icons/32x32/document-open.png
new file mode 100644
index 0000000000..9858b146f4
--- /dev/null
+++ b/src/assets/icons/32x32/document-open.png
Binary files differ
diff --git a/src/assets/icons/32x32/document-print.png b/src/assets/icons/32x32/document-print.png
new file mode 100644
index 0000000000..1672ec5897
--- /dev/null
+++ b/src/assets/icons/32x32/document-print.png
Binary files differ
diff --git a/src/assets/icons/32x32/document-save.png b/src/assets/icons/32x32/document-save.png
new file mode 100644
index 0000000000..f04de74673
--- /dev/null
+++ b/src/assets/icons/32x32/document-save.png
Binary files differ
diff --git a/src/assets/icons/32x32/edit-copy.png b/src/assets/icons/32x32/edit-copy.png
new file mode 100644
index 0000000000..bbb34cc4c2
--- /dev/null
+++ b/src/assets/icons/32x32/edit-copy.png
Binary files differ
diff --git a/src/assets/icons/32x32/edit-cut.png b/src/assets/icons/32x32/edit-cut.png
new file mode 100644
index 0000000000..d89ef6c016
--- /dev/null
+++ b/src/assets/icons/32x32/edit-cut.png
Binary files differ
diff --git a/src/assets/icons/32x32/edit-delete.png b/src/assets/icons/32x32/edit-delete.png
new file mode 100644
index 0000000000..4c97ee2495
--- /dev/null
+++ b/src/assets/icons/32x32/edit-delete.png
Binary files differ
diff --git a/src/assets/icons/32x32/edit-paste.png b/src/assets/icons/32x32/edit-paste.png
new file mode 100644
index 0000000000..299fa77686
--- /dev/null
+++ b/src/assets/icons/32x32/edit-paste.png
Binary files differ
diff --git a/src/assets/icons/32x32/edit-redo.png b/src/assets/icons/32x32/edit-redo.png
new file mode 100644
index 0000000000..4f8849c711
--- /dev/null
+++ b/src/assets/icons/32x32/edit-redo.png
Binary files differ
diff --git a/src/assets/icons/32x32/edit-undo.png b/src/assets/icons/32x32/edit-undo.png
new file mode 100644
index 0000000000..b3d366c53f
--- /dev/null
+++ b/src/assets/icons/32x32/edit-undo.png
Binary files differ
diff --git a/src/assets/icons/32x32/format-justify-center.png b/src/assets/icons/32x32/format-justify-center.png
new file mode 100644
index 0000000000..80c3afd9a6
--- /dev/null
+++ b/src/assets/icons/32x32/format-justify-center.png
Binary files differ
diff --git a/src/assets/icons/32x32/format-justify-fill.png b/src/assets/icons/32x32/format-justify-fill.png
new file mode 100644
index 0000000000..33589ea25d
--- /dev/null
+++ b/src/assets/icons/32x32/format-justify-fill.png
Binary files differ
diff --git a/src/assets/icons/32x32/format-justify-left.png b/src/assets/icons/32x32/format-justify-left.png
new file mode 100644
index 0000000000..ba02821135
--- /dev/null
+++ b/src/assets/icons/32x32/format-justify-left.png
Binary files differ
diff --git a/src/assets/icons/32x32/format-justify-right.png b/src/assets/icons/32x32/format-justify-right.png
new file mode 100644
index 0000000000..8e15d0cb44
--- /dev/null
+++ b/src/assets/icons/32x32/format-justify-right.png
Binary files differ
diff --git a/src/assets/icons/32x32/format-text-bold.png b/src/assets/icons/32x32/format-text-bold.png
new file mode 100644
index 0000000000..754efdd975
--- /dev/null
+++ b/src/assets/icons/32x32/format-text-bold.png
Binary files differ
diff --git a/src/assets/icons/32x32/format-text-italic.png b/src/assets/icons/32x32/format-text-italic.png
new file mode 100644
index 0000000000..6db31a4f69
--- /dev/null
+++ b/src/assets/icons/32x32/format-text-italic.png
Binary files differ
diff --git a/src/assets/icons/32x32/format-text-underline.png b/src/assets/icons/32x32/format-text-underline.png
new file mode 100644
index 0000000000..977cde9d97
--- /dev/null
+++ b/src/assets/icons/32x32/format-text-underline.png
Binary files differ
diff --git a/src/assets/icons/32x32@2/document-new@2x.png b/src/assets/icons/32x32@2/document-new@2x.png
new file mode 100644
index 0000000000..c924576061
--- /dev/null
+++ b/src/assets/icons/32x32@2/document-new@2x.png
Binary files differ
diff --git a/src/assets/icons/32x32@2/document-open@2x.png b/src/assets/icons/32x32@2/document-open@2x.png
new file mode 100644
index 0000000000..68e75b549a
--- /dev/null
+++ b/src/assets/icons/32x32@2/document-open@2x.png
Binary files differ
diff --git a/src/assets/icons/32x32@2/document-print@2x.png b/src/assets/icons/32x32@2/document-print@2x.png
new file mode 100644
index 0000000000..b784336739
--- /dev/null
+++ b/src/assets/icons/32x32@2/document-print@2x.png
Binary files differ
diff --git a/src/assets/icons/32x32@2/document-save@2x.png b/src/assets/icons/32x32@2/document-save@2x.png
new file mode 100644
index 0000000000..f4cca4b323
--- /dev/null
+++ b/src/assets/icons/32x32@2/document-save@2x.png
Binary files differ
diff --git a/src/assets/icons/32x32@2/edit-copy@2x.png b/src/assets/icons/32x32@2/edit-copy@2x.png
new file mode 100644
index 0000000000..9690d6bb04
--- /dev/null
+++ b/src/assets/icons/32x32@2/edit-copy@2x.png
Binary files differ
diff --git a/src/assets/icons/32x32@2/edit-cut@2x.png b/src/assets/icons/32x32@2/edit-cut@2x.png
new file mode 100644
index 0000000000..408b0ae19b
--- /dev/null
+++ b/src/assets/icons/32x32@2/edit-cut@2x.png
Binary files differ
diff --git a/src/assets/icons/32x32@2/edit-delete@2x.png b/src/assets/icons/32x32@2/edit-delete@2x.png
new file mode 100644
index 0000000000..58abfc1fa5
--- /dev/null
+++ b/src/assets/icons/32x32@2/edit-delete@2x.png
Binary files differ
diff --git a/src/assets/icons/32x32@2/edit-paste@2x.png b/src/assets/icons/32x32@2/edit-paste@2x.png
new file mode 100644
index 0000000000..b8c288f6c7
--- /dev/null
+++ b/src/assets/icons/32x32@2/edit-paste@2x.png
Binary files differ
diff --git a/src/assets/icons/32x32@2/edit-redo@2x.png b/src/assets/icons/32x32@2/edit-redo@2x.png
new file mode 100644
index 0000000000..89fcd33c30
--- /dev/null
+++ b/src/assets/icons/32x32@2/edit-redo@2x.png
Binary files differ
diff --git a/src/assets/icons/32x32@2/edit-undo@2x.png b/src/assets/icons/32x32@2/edit-undo@2x.png
new file mode 100644
index 0000000000..6f7ad2cb40
--- /dev/null
+++ b/src/assets/icons/32x32@2/edit-undo@2x.png
Binary files differ
diff --git a/src/assets/icons/32x32@2/format-justify-center@2x.png b/src/assets/icons/32x32@2/format-justify-center@2x.png
new file mode 100644
index 0000000000..9b2cc1ed16
--- /dev/null
+++ b/src/assets/icons/32x32@2/format-justify-center@2x.png
Binary files differ
diff --git a/src/assets/icons/32x32@2/format-justify-fill@2x.png b/src/assets/icons/32x32@2/format-justify-fill@2x.png
new file mode 100644
index 0000000000..1212e9f761
--- /dev/null
+++ b/src/assets/icons/32x32@2/format-justify-fill@2x.png
Binary files differ
diff --git a/src/assets/icons/32x32@2/format-justify-left@2x.png b/src/assets/icons/32x32@2/format-justify-left@2x.png
new file mode 100644
index 0000000000..8c0eca3037
--- /dev/null
+++ b/src/assets/icons/32x32@2/format-justify-left@2x.png
Binary files differ
diff --git a/src/assets/icons/32x32@2/format-justify-right@2x.png b/src/assets/icons/32x32@2/format-justify-right@2x.png
new file mode 100644
index 0000000000..fb0ed70252
--- /dev/null
+++ b/src/assets/icons/32x32@2/format-justify-right@2x.png
Binary files differ
diff --git a/src/assets/icons/32x32@2/format-text-bold@2x.png b/src/assets/icons/32x32@2/format-text-bold@2x.png
new file mode 100644
index 0000000000..0e67ead0b8
--- /dev/null
+++ b/src/assets/icons/32x32@2/format-text-bold@2x.png
Binary files differ
diff --git a/src/assets/icons/32x32@2/format-text-italic@2x.png b/src/assets/icons/32x32@2/format-text-italic@2x.png
new file mode 100644
index 0000000000..f746f8956f
--- /dev/null
+++ b/src/assets/icons/32x32@2/format-text-italic@2x.png
Binary files differ
diff --git a/src/assets/icons/32x32@2/format-text-underline@2x.png b/src/assets/icons/32x32@2/format-text-underline@2x.png
new file mode 100644
index 0000000000..47d6fced02
--- /dev/null
+++ b/src/assets/icons/32x32@2/format-text-underline@2x.png
Binary files differ
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