From 5a2df08ae08becf75003f1824572397c7c5a7266 Mon Sep 17 00:00:00 2001 From: Jarek Kobus Date: Thu, 26 Oct 2023 12:53:25 +0200 Subject: [PATCH] TaskTree: Add a new example showing input / output data exchange This example shows how to separate the business logic of the recipe from the GUI and how to write a reusable recipe. It shows how to feed the recipe with the initial data and how to retrieve the final result from the recipe using onStorageSetup() and onStorageDone() handlers. Change-Id: I04c0c0c9bd6cf25ac4e91317e527ad12832e9143 Reviewed-by: Qt CI Bot Reviewed-by: hjk --- tests/manual/manual.qbs | 1 + tests/manual/tasking/CMakeLists.txt | 1 + .../tasking/dataexchange/CMakeLists.txt | 10 ++ .../tasking/dataexchange/dataexchange.qbs | 15 +++ tests/manual/tasking/dataexchange/main.cpp | 17 ++++ tests/manual/tasking/dataexchange/recipe.cpp | 86 +++++++++++++++++ tests/manual/tasking/dataexchange/recipe.h | 36 +++++++ tests/manual/tasking/dataexchange/viewer.cpp | 94 +++++++++++++++++++ tests/manual/tasking/dataexchange/viewer.h | 33 +++++++ 9 files changed, 293 insertions(+) create mode 100644 tests/manual/tasking/dataexchange/CMakeLists.txt create mode 100644 tests/manual/tasking/dataexchange/dataexchange.qbs create mode 100644 tests/manual/tasking/dataexchange/main.cpp create mode 100644 tests/manual/tasking/dataexchange/recipe.cpp create mode 100644 tests/manual/tasking/dataexchange/recipe.h create mode 100644 tests/manual/tasking/dataexchange/viewer.cpp create mode 100644 tests/manual/tasking/dataexchange/viewer.h diff --git a/tests/manual/manual.qbs b/tests/manual/manual.qbs index 25ed3b9e183..b9a42250a2d 100644 --- a/tests/manual/manual.qbs +++ b/tests/manual/manual.qbs @@ -13,6 +13,7 @@ Project { "shootout/shootout.qbs", "spinner/spinner.qbs", "subdirfilecontainer/subdirfilecontainer.qbs", + "tasking/dataexchange/dataexchange.qbs", "tasking/demo/demo.qbs", "tasking/imagescaling/imagescaling.qbs", "terminal/terminal.qbs", diff --git a/tests/manual/tasking/CMakeLists.txt b/tests/manual/tasking/CMakeLists.txt index a27004eb3ec..79a524afd2a 100644 --- a/tests/manual/tasking/CMakeLists.txt +++ b/tests/manual/tasking/CMakeLists.txt @@ -1,2 +1,3 @@ +add_subdirectory(dataexchange) add_subdirectory(demo) add_subdirectory(imagescaling) diff --git a/tests/manual/tasking/dataexchange/CMakeLists.txt b/tests/manual/tasking/dataexchange/CMakeLists.txt new file mode 100644 index 00000000000..d5ac9cb8aa8 --- /dev/null +++ b/tests/manual/tasking/dataexchange/CMakeLists.txt @@ -0,0 +1,10 @@ +add_qtc_test(tst_tasking_dataexchange + MANUALTEST + DEPENDS Tasking Qt::Concurrent Qt::Network Qt::Widgets + SOURCES + main.cpp + recipe.cpp + recipe.h + viewer.cpp + viewer.h +) diff --git a/tests/manual/tasking/dataexchange/dataexchange.qbs b/tests/manual/tasking/dataexchange/dataexchange.qbs new file mode 100644 index 00000000000..0cda959a31f --- /dev/null +++ b/tests/manual/tasking/dataexchange/dataexchange.qbs @@ -0,0 +1,15 @@ +QtcManualTest { + name: "Tasking dataexchange" + type: ["application"] + + Depends { name: "Qt"; submodules: ["concurrent", "network", "widgets"] } + Depends { name: "Tasking" } + + files: [ + "main.cpp", + "recipe.cpp", + "recipe.h", + "viewer.cpp", + "viewer.h", + ] +} diff --git a/tests/manual/tasking/dataexchange/main.cpp b/tests/manual/tasking/dataexchange/main.cpp new file mode 100644 index 00000000000..d1cd06fe1e8 --- /dev/null +++ b/tests/manual/tasking/dataexchange/main.cpp @@ -0,0 +1,17 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "viewer.h" +#include + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + app.setOrganizationName("QtProject"); + app.setApplicationName("Data Exchange"); + + Viewer viewer; + viewer.show(); + + return app.exec(); +} diff --git a/tests/manual/tasking/dataexchange/recipe.cpp b/tests/manual/tasking/dataexchange/recipe.cpp new file mode 100644 index 00000000000..4446eee7b9a --- /dev/null +++ b/tests/manual/tasking/dataexchange/recipe.cpp @@ -0,0 +1,86 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "recipe.h" + +#include +#include + +using namespace Tasking; + +static void readImage(QPromise &promise, const QByteArray &data) +{ + const auto image = QImage::fromData(data); + if (image.isNull()) + promise.future().cancel(); + else + promise.addResult(image); +} + +static void scaleImage(QPromise &promise, const QImage &inputImage, const QSize &size) +{ + promise.addResult(inputImage.scaled(size, Qt::KeepAspectRatio)); +} + +class InternalData +{ +public: + QByteArray dataSource; + QImage imageSource; +}; + +static int sizeForIndex(int index) { return (index + 1) * s_sizeInterval; } + +Group recipe(const Tasking::TreeStorage &externalStorage) +{ + TreeStorage internalStorage; + + const auto onDownloadSetup = [externalStorage](NetworkQuery &query) { + query.setNetworkAccessManager(externalStorage->inputNam); + query.setRequest(QNetworkRequest(externalStorage->inputUrl)); + }; + const auto onDownloadDone = [internalStorage, externalStorage](const NetworkQuery &query, + DoneWith doneWith) { + if (doneWith == DoneWith::Success) { + internalStorage->dataSource = query.reply()->readAll(); + } else { + externalStorage->outputError + = QString("Download Error. Code: %1.").arg(query.reply()->error()); + } + }; + + const auto onReadSetup = [internalStorage](ConcurrentCall &data) { + data.setConcurrentCallData(&readImage, internalStorage->dataSource); + }; + const auto onReadDone = [internalStorage, externalStorage](const ConcurrentCall &data, + DoneWith doneWith) { + if (doneWith == DoneWith::Success) + internalStorage->imageSource = data.result(); + else + externalStorage->outputError = "Image Data Error."; + }; + + QList parallelTasks; + parallelTasks.reserve(s_imageCount + 1); // +1 for parallelLimit + parallelTasks.append(parallelLimit(QThread::idealThreadCount() - 1)); + + for (int i = 0; i < s_imageCount; ++i) { + const int s = sizeForIndex(i); + const auto onScaleSetup = [internalStorage, s](ConcurrentCall &data) { + data.setConcurrentCallData(&scaleImage, internalStorage->imageSource, QSize(s, s)); + }; + const auto onScaleDone = [externalStorage, s](const ConcurrentCall &data) { + externalStorage->outputImages.insert(s, data.result()); + }; + parallelTasks.append(ConcurrentCallTask(onScaleSetup, onScaleDone)); + } + + const QList recipe { + Storage(externalStorage), + Storage(internalStorage), + NetworkQueryTask(onDownloadSetup, onDownloadDone), + ConcurrentCallTask(onReadSetup, onReadDone), + Group { parallelTasks } + }; + return recipe; +} diff --git a/tests/manual/tasking/dataexchange/recipe.h b/tests/manual/tasking/dataexchange/recipe.h new file mode 100644 index 00000000000..7b17e1349a3 --- /dev/null +++ b/tests/manual/tasking/dataexchange/recipe.h @@ -0,0 +1,36 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef RECIPE_H +#define RECIPE_H + +#include +#include +#include + +#include + +namespace Tasking { +class Group; +template +class TreeStorage; +} + +static const int s_sizeInterval = 10; +static const int s_imageCount = 100; +static const int s_maxSize = s_sizeInterval * s_imageCount; + +class QNetworkAccessManager; + +class ExternalData +{ +public: + QNetworkAccessManager *inputNam = nullptr; + QUrl inputUrl; + QMap outputImages; + std::optional outputError; +}; + +Tasking::Group recipe(const Tasking::TreeStorage &externalStorage); + +#endif // RECIPE_H diff --git a/tests/manual/tasking/dataexchange/viewer.cpp b/tests/manual/tasking/dataexchange/viewer.cpp new file mode 100644 index 00000000000..62a4bdd3e88 --- /dev/null +++ b/tests/manual/tasking/dataexchange/viewer.cpp @@ -0,0 +1,94 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "viewer.h" +#include "recipe.h" + +#include +#include + +using namespace Tasking; + +Viewer::Viewer(QWidget *parent) + : QWidget(parent) + , m_recipe(recipe(m_storage)) +{ + setWindowTitle(tr("Data Exchange")); + + QLabel *urlLabel = new QLabel(tr("Url:")); + m_lineEdit = new QLineEdit("https://media.licdn.com/dms/image/D4D22AQFj3ksh5rmnrg/" + "feedshare-shrink_800/0/1697023188446?e=1701302400&v=beta" + "&t=6dy5dmhzgONaLu139A6XmFSGqDohiezq1fH-q2mmu3w"); + QPushButton *startButton = new QPushButton(tr("Start")); + QPushButton *stopButton = new QPushButton(tr("Stop")); + QPushButton *resetButton = new QPushButton(tr("Reset")); + m_progressBar = new QProgressBar; + m_listWidget = new QListWidget; + m_statusBar = new QStatusBar; + + QBoxLayout *mainLayout = new QVBoxLayout(this); + QBoxLayout *subLayout1 = new QHBoxLayout; + QBoxLayout *subLayout2 = new QHBoxLayout; + subLayout1->addWidget(urlLabel); + subLayout1->addWidget(m_lineEdit); + subLayout2->addWidget(startButton); + subLayout2->addWidget(stopButton); + subLayout2->addWidget(resetButton); + subLayout2->addWidget(m_progressBar); + mainLayout->addLayout(subLayout1); + mainLayout->addLayout(subLayout2); + mainLayout->addWidget(m_listWidget); + mainLayout->addWidget(m_statusBar); + + m_listWidget->setIconSize(QSize(s_maxSize, s_maxSize)); + + const auto reset = [this] { + m_listWidget->clear(); + m_statusBar->clearMessage(); + m_progressBar->setValue(0); + }; + + connect(startButton, &QAbstractButton::clicked, this, [this, reset] { + reset(); + const auto setInput = [this](ExternalData &data) { + data.inputNam = &m_nam; + data.inputUrl = m_lineEdit->text(); + m_statusBar->showMessage(tr("Executing recipe...")); + }; + + const auto getOutput = [this](const ExternalData &data) { + if (data.outputError) { + m_statusBar->showMessage(*data.outputError); + return; + } + m_statusBar->showMessage(tr("Recipe executed successfully.")); + for (auto it = data.outputImages.begin(); it != data.outputImages.end(); ++it) { + m_listWidget->addItem(new QListWidgetItem(QPixmap::fromImage(it.value()), + QString("%1x%1").arg(it.key()))); + } + }; + + const auto onDone = [this] { m_taskTree.release()->deleteLater(); }; + + m_taskTree.reset(new TaskTree(m_recipe)); + m_taskTree->onStorageSetup(m_storage, setInput); + m_taskTree->onStorageDone(m_storage, getOutput); + m_progressBar->setMaximum(m_taskTree->progressMaximum()); + QObject::connect(m_taskTree.get(), &TaskTree::progressValueChanged, + m_progressBar, &QProgressBar::setValue); + QObject::connect(m_taskTree.get(), &TaskTree::done, this, onDone); + m_taskTree->start(); + }); + + connect(stopButton, &QAbstractButton::clicked, this, [this] { + if (m_taskTree) { + m_statusBar->showMessage(tr("Recipe stopped by user.")); + m_taskTree.reset(); + } + }); + + connect(resetButton, &QAbstractButton::clicked, this, [this, reset] { + m_taskTree.reset(); + reset(); + }); +} diff --git a/tests/manual/tasking/dataexchange/viewer.h b/tests/manual/tasking/dataexchange/viewer.h new file mode 100644 index 00000000000..81575596f5f --- /dev/null +++ b/tests/manual/tasking/dataexchange/viewer.h @@ -0,0 +1,33 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef VIEWER_H +#define VIEWER_H + +#include "recipe.h" + +#include + +#include +#include + +class Viewer : public QWidget +{ + Q_OBJECT + +public: + Viewer(QWidget *parent = nullptr); + +private: + QLineEdit *m_lineEdit = nullptr; + QProgressBar *m_progressBar = nullptr; + QListWidget *m_listWidget = nullptr; + QStatusBar *m_statusBar = nullptr; + + QNetworkAccessManager m_nam; + const Tasking::TreeStorage m_storage; + const Tasking::Group m_recipe; + std::unique_ptr m_taskTree; +}; + +#endif // VIEWER_H