diff options
Diffstat (limited to 'examples/quick')
47 files changed, 1193 insertions, 1 deletions
diff --git a/examples/quick/models/CMakeLists.txt b/examples/quick/models/CMakeLists.txt index ca1ffc26a1..c874f21180 100644 --- a/examples/quick/models/CMakeLists.txt +++ b/examples/quick/models/CMakeLists.txt @@ -5,3 +5,4 @@ qt_internal_add_example(abstractitemmodel) qt_internal_add_example(objectlistmodel) qt_internal_add_example(stringlistmodel) qt_internal_add_example(threadedfetchmore) +qt_internal_add_example(threadedsonglist) diff --git a/examples/quick/models/models.pro b/examples/quick/models/models.pro index 7e86f14847..7675c4588f 100644 --- a/examples/quick/models/models.pro +++ b/examples/quick/models/models.pro @@ -3,4 +3,5 @@ SUBDIRS = \ abstractitemmodel \ objectlistmodel \ stringlistmodel \ - threadedfetchmore + threadedfetchmore \ + threadedsonglist diff --git a/examples/quick/models/threadedsonglist/CMakeLists.txt b/examples/quick/models/threadedsonglist/CMakeLists.txt new file mode 100644 index 0000000000..1da9d32deb --- /dev/null +++ b/examples/quick/models/threadedsonglist/CMakeLists.txt @@ -0,0 +1,80 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +cmake_minimum_required(VERSION 3.16) + +project(threadedsonglist VERSION 0.1 LANGUAGES CXX) + +find_package(Qt6 REQUIRED COMPONENTS Quick) + +qt_standard_project_setup(REQUIRES 6.8) + +qt_add_executable(appthreadedsonglist WIN32 MACOSX_BUNDLE + main.cpp +) + +qt_add_qml_module(appthreadedsonglist + URI threadedsonglist + QML_FILES + SongListDelegate.qml + ThreadedSongList.qml + SOURCES + datastorage.h + datastorage.cpp + mediaelement.h + mediaelement.cpp + queueworker.h + queueworker.cpp + remotemedia.h + remotemedia.cpp + songdatagenerator.h + songdatagenerator.cpp + threadedlistmodel.h + threadedlistmodel.cpp + RESOURCES + images/album_covers/classical1.jpeg + images/album_covers/classical2.jpeg + images/album_covers/classical3.jpeg + images/album_covers/classical4.jpeg + images/album_covers/classical5.jpeg + images/album_covers/classical6.jpeg + images/album_covers/metal1.jpeg + images/album_covers/metal2.jpeg + images/album_covers/metal3.jpeg + images/album_covers/metal4.jpeg + images/album_covers/metal5.jpeg + images/album_covers/metal6.jpeg + images/album_covers/pop1.jpeg + images/album_covers/pop2.jpeg + images/album_covers/pop3.jpeg + images/album_covers/pop4.jpeg + images/album_covers/pop5.jpeg + images/album_covers/pop6.jpeg + images/album_covers/rock1.jpeg + images/album_covers/rock2.jpeg + images/album_covers/rock3.jpeg + images/album_covers/rock4.jpeg + images/album_covers/rock5.jpeg + images/album_covers/rock6.jpeg + images/device/remote.jpeg +) + +target_link_libraries(appthreadedsonglist + PRIVATE Qt6::Quick +) + +include(GNUInstallDirs) +install(TARGETS appthreadedsonglist + BUNDLE DESTINATION . + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} +) + +qt_generate_deploy_qml_app_script( + TARGET appthreadedsonglist + OUTPUT_SCRIPT deploy_script + MACOS_BUNDLE_POST_BUILD + NO_UNSUPPORTED_PLATFORM_ERROR + DEPLOY_USER_QML_MODULES_ON_UNSUPPORTED_PLATFORM +) +install(SCRIPT ${deploy_script}) diff --git a/examples/quick/models/threadedsonglist/SongListDelegate.qml b/examples/quick/models/threadedsonglist/SongListDelegate.qml new file mode 100644 index 0000000000..ba96d3c3d5 --- /dev/null +++ b/examples/quick/models/threadedsonglist/SongListDelegate.qml @@ -0,0 +1,93 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls.Universal + +MouseArea { + id: delegateRoot + signal songSelected(albumName: string, albumArt: string, artistName: string, songTitle: string) + + implicitHeight: 60 + enabled: model.albumArt !== "" + onClicked: songSelected(model.album, model.albumArt, model.artist, model.song) + BusyIndicator { + id: loadingIndicator + anchors.verticalCenter: parent.verticalCenter + anchors.right: loadingText.left + anchors.rightMargin: 5 + height: loadingText.implicitHeight + width: height + running: model.loadingText === "" && model.isLoadingElement + } + Text { + id: loadingText + anchors.centerIn: parent + font.pixelSize: delegateRoot.implicitHeight / 4 + text: model.loadingText === "" ? qsTr("Loading...") : model.loadingText + visible: model.isLoadingElement + } + Item { + anchors.fill: parent + visible: !loadingIndicator.running + Image { + id: albumArtImage + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.leftMargin: 1 + anchors.topMargin: 1 + anchors.bottomMargin: 1 + source: model.albumArt + width: height + fillMode: Image.PreserveAspectCrop + } + Text { + id: artistName + anchors.left: albumArtImage.right + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: 1 + anchors.leftMargin: 1 + anchors.rightMargin: 1 + font.pixelSize: delegateRoot.implicitHeight / 5 + text: model.artist + } + Text { + id: songName + anchors.left: albumArtImage.right + anchors.right: parent.right + anchors.top: artistName.bottom + anchors.topMargin: 1 + anchors.leftMargin: 1 + anchors.rightMargin: 1 + font.pixelSize: delegateRoot.implicitHeight / 4 + text: model.song + } + Text { + id: albumName + anchors.left: albumArtImage.right + anchors.right: parent.right + anchors.top: songName.bottom + anchors.topMargin: 1 + anchors.leftMargin: 1 + anchors.rightMargin: 1 + font.pixelSize: delegateRoot.implicitHeight / 5 + text: model.album + } + } + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + implicitHeight: 1 + color: "lightgray" + } + Text { + anchors.right: parent.right + anchors.rightMargin: delegateRoot.implicitHeight / 2 + anchors.verticalCenter: parent.verticalCenter + font.pixelSize: delegateRoot.implicitHeight / 4 + text: "#"+(index+1) + } +} diff --git a/examples/quick/models/threadedsonglist/ThreadedSongList.qml b/examples/quick/models/threadedsonglist/ThreadedSongList.qml new file mode 100644 index 0000000000..9563b64639 --- /dev/null +++ b/examples/quick/models/threadedsonglist/ThreadedSongList.qml @@ -0,0 +1,151 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +pragma ComponentBehavior: Bound + +//! [Fixed Controls style] +import QtQuick +import QtQuick.Controls.Universal +//! [Fixed Controls style] + +Window { + id: mainWindow + minimumWidth: 800 + minimumHeight: 480 + width: minimumWidth + height: minimumHeight + visible: true + title: qsTr("Threaded Song List") + + property string currentAlbumName: "" + property string currentAlbumArt: "" + property string currentArtistName: "" + property string currentSongTitle: "" + ListView { + id: songListView + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + width: 2 * parent.width / 5 + model: ThreadedListModel{} + readonly property int delegateHeight: (songListView.height / 8) - songListView.spacing + cacheBuffer: delegateHeight * 2 + delegate: SongListDelegate{ + required property int index + required property var model + implicitHeight: songListView.delegateHeight + width: songListView.width + onSongSelected: (albumName, albumArt, artistName, songTitle) => { + mainWindow.currentAlbumName = albumName + mainWindow.currentAlbumArt = albumArt + mainWindow.currentArtistName = artistName + mainWindow.currentSongTitle = songTitle + } + } + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AlwaysOn + implicitWidth: songListView.height / 20 + } + BusyIndicator { + anchors.centerIn: parent + width: parent.width / 3 + height: width + running: songListView.count === 0 + } + } + Rectangle { + color: "white" + anchors.left: songListView.right + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + Label { + id: statusLabel + anchors.top: parent.top + anchors.right: parent.right + anchors.left: parent.left + anchors.leftMargin: 10 + anchors.topMargin: 5 + textFormat: Text.RichText + text: qsTr("🔗 Connected, %1 songs available").arg(songListView.count) + font.pixelSize: Math.min(parent.height, parent.width) / 32 + horizontalAlignment: Text.AlignHCenter + } + Item { + id: deviceImage + anchors.right: parent.right + anchors.top: statusLabel.bottom + anchors.left: parent.left + anchors.leftMargin: 10 + height: (3 * parent.height / 4) - statusLabel.height + Image { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + width: Math.min(parent.width, parent.height) + height: width + source: "qrc:/qt/qml/threadedsonglist/images/device/remote.jpeg" + fillMode: Image.PreserveAspectFit + } + } + Rectangle { + visible: mainWindow.currentSongTitle !== "" + color: "black" + anchors.top: deviceImage.bottom + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: parent.bottom + Rectangle { + id: nowPlayingBox + anchors.top: parent.top + anchors.right: parent.right + anchors.left: parent.left + height: nowPlayingTitle.implicitHeight + color: "#555555" + Label { + id: nowPlayingTitle + anchors.centerIn: parent + color: "white" + text: qsTr("Now playing:") + font.pixelSize: 16 + horizontalAlignment: Text.AlignHCenter + } + } + Image { + id: albumArtImage + anchors.top: nowPlayingBox.bottom + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.topMargin: 5 + source: mainWindow.currentAlbumArt + fillMode: Image.PreserveAspectFit + } + Column { + anchors.top: nowPlayingBox.bottom + anchors.right: parent.right + anchors.left: albumArtImage.right + anchors.bottom: parent.bottom + anchors.topMargin: 5 + anchors.leftMargin: 5 + spacing: 5 + Label { + color: "white" + text: "♫: " + mainWindow.currentSongTitle + textFormat: Text.RichText + font.pixelSize: (parent.height - 20) / 4 + } + Label { + color: "white" + text: "👤: " + mainWindow.currentArtistName + textFormat: Text.RichText + font.pixelSize: (parent.height - 20) / 4 + } + Label { + color: "white" + text: "💿: " + mainWindow.currentAlbumName + textFormat: Text.RichText + font.pixelSize: (parent.height - 20) / 4 + } + } + } + } +} diff --git a/examples/quick/models/threadedsonglist/datastorage.cpp b/examples/quick/models/threadedsonglist/datastorage.cpp new file mode 100644 index 0000000000..c38458960d --- /dev/null +++ b/examples/quick/models/threadedsonglist/datastorage.cpp @@ -0,0 +1,84 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "datastorage.h" +#include "queueworker.h" + +DataStorage::DataStorage() : QObject{ nullptr } +{ + /* Generate a list of IDs to use locally. + * DataStorage will use indexes for accessing the remote data. + */ + m_idList.reserve(RemoteMedia::count()); + for (int id = 1; id <= RemoteMedia::count(); ++id) { + m_idList.append(id); + } + m_worker = new QueueWorker; + connect(&m_workerThread, &QThread::finished, m_worker, &QObject::deleteLater); + m_worker->moveToThread(&m_workerThread); + connect(m_worker, &QueueWorker::dataFetched, this, &DataStorage::dataReceived); + connect(m_worker, &QueueWorker::processing, this, &DataStorage::fetchStarted); + connect(m_worker, &QueueWorker::dropped, this, &DataStorage::fetchAborted); + connect(this, &DataStorage::dataFetchNeeded, m_worker, &QueueWorker::fetchData); + m_workerThread.start(); +} + +DataStorage::~DataStorage() +{ + /* Clean the worker object by stopping it and then ending the thread */ + m_worker->abort(); + m_worker = nullptr; + m_workerThread.quit(); + m_workerThread.wait(); +} + +QList<int> DataStorage::idList() +{ + return m_idList; +} + +MediaElement DataStorage::item(int id) const +{ + if (id < 1) + return MediaElement{}; + //! [Send signal if no data] + if (!m_items.contains(id)) { + m_items.insert(id, MediaElement{}); + emit dataFetchNeeded(m_idList.indexOf(id)); + } + return m_items.value(id); + //! [Send signal if no data] +} + +std::optional<int> DataStorage::currentlyFetchedId() const +{ + return m_currentlyFetchedId; +} + +void DataStorage::fetchStarted(int index) +{ + const auto idBeingFetched = m_idList.at(index); + m_currentlyFetchedId = idBeingFetched; + emit dataUpdated(idBeingFetched); +} + +void DataStorage::fetchAborted(int index) +{ + /* The data will never be fetched. Remove the empty item (so storge will send a signal to thread + * if needed). Then send dataUpdated signal. If the item is still visible, view will re-request + * its data and item will be requeued to thread. If item is no longer in visible area, view will + * ignore the dataChanged signal and storage will not receive any calls. + */ + const auto idAborted = m_idList.at(index); + m_items.remove(idAborted); + if (m_currentlyFetchedId == idAborted) + m_currentlyFetchedId = std::nullopt; + emit dataUpdated(idAborted); +} + +void DataStorage::dataReceived(int index, MediaElement element) +{ + const auto idUpdated = m_idList.at(index); + m_items.insert(idUpdated, element); + emit dataUpdated(idUpdated); +} diff --git a/examples/quick/models/threadedsonglist/datastorage.h b/examples/quick/models/threadedsonglist/datastorage.h new file mode 100644 index 0000000000..d8cd4b9f2a --- /dev/null +++ b/examples/quick/models/threadedsonglist/datastorage.h @@ -0,0 +1,49 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef DATASTORAGE_H +#define DATASTORAGE_H + +#include "mediaelement.h" + +#include <QHash> +#include <QThread> + +class QueueWorker; + +class DataStorage : public QObject +{ + Q_OBJECT +public: + explicit DataStorage(); + ~DataStorage(); + + //! [Interface towards the model] + QList<int> idList(); + MediaElement item(int id) const; + std::optional<int> currentlyFetchedId() const; + //! [Interface towards the model] + +signals: + // Towards client + void dataUpdated(int id); + // Towards worker + void dataFetchNeeded(int index) const; + +private slots: + // From worker + void fetchStarted(int index); + void fetchAborted(int index); + void dataReceived(int index, MediaElement element); + +private: + QueueWorker *m_worker; + QList<int> m_idList; + QThread m_workerThread; + std::optional<int> m_currentlyFetchedId; + /* The item list is mutable as DataStorage needs to mark already signaled items to it in + * const data() call of model */ + mutable QHash<int, MediaElement> m_items; +}; + +#endif // DATASTORAGE_H diff --git a/examples/quick/models/threadedsonglist/doc/images/qml-threadedsonglist-example.webp b/examples/quick/models/threadedsonglist/doc/images/qml-threadedsonglist-example.webp Binary files differnew file mode 100644 index 0000000000..b0cd68b44c --- /dev/null +++ b/examples/quick/models/threadedsonglist/doc/images/qml-threadedsonglist-example.webp diff --git a/examples/quick/models/threadedsonglist/doc/src/threadedsonglist-example.qdoc b/examples/quick/models/threadedsonglist/doc/src/threadedsonglist-example.qdoc new file mode 100644 index 0000000000..b3c23602e7 --- /dev/null +++ b/examples/quick/models/threadedsonglist/doc/src/threadedsonglist-example.qdoc @@ -0,0 +1,96 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only + +/*! + \example models/threadedsonglist + \title Models and Views: List Model using a worker thread for data fetching + \brief Demonstrates how to implement a list model with a responsive UI + using a worker thread to fetch data. + \examplecategory {User Interface Components} + \examplecategory {Data Processing & I/O} + \meta {tag} {quick,threads} + \image qml-threadedsonglist-example.webp {Screenshot of the application + where a songlist with album arts, song names, artist names and album names + are visible} + + This example introduces a custom item model, inheriting QAbstractListModel. + The model gets its data from a worker object that is in separate QThread, + fetching data from a slow data source. + + \section1 Overview of the Threaded Song List example + + The data source simulates a slow data source by adding a delay of 100 + milliseconds per song fetched from it. This means that loading of the + entire list of 3600 songs would take 6 minutes, making the opening of the + application impractical. This delay is mitigated by fetching the data only + for the visible area of the view, using a QObject placed into a worker + thread. + + The worker object has a limit for the number of fetch requests it holds in + queue. This ensures that only the elements of the currently visible part of + the song list are fetched, removing the need to wait for the non-visible + part of the list to load, when user has already scrolled past some part of + the list. + + Focus of this example is in the source model of the view. The view itself + is an unmodified QML ListView with a simple delegate. The use of thread is + hidden behind the implementation of model data handling and the ListView + does not need to have any customization to be able to adapt to the + thread-based model. + + Also since the focus is in the model, the Qt Quick Controls is set to use + Universal style on all platforms to ensure identical UI behavior. + + \snippet models/threadedsonglist/ThreadedSongList.qml Fixed Controls style + + \section1 How it works + + The business logic of providing the song list data is separated into + \c DataStorage class that provides a simple ID-based interface for the model. + + \snippet models/threadedsonglist/datastorage.h Interface towards the model + + When model requests data from the DataStorage, the storage will first check + if it has the data already available. If it does, data is returned + instantly, as would be the case in a non-threaded model. In case the data + is not found, DataStorage will emit a \c dataFetchNeeded() signal to the + worker object and add an empty item to the list of already existing data. + Adding the empty item ensures that no further signals are sent to the + worker for the same list item. + + \snippet models/threadedsonglist/datastorage.cpp Send signal if no data + + \c QueueWorker - the worker thread object - processes the dataFetchNeeded() + signals it has received by sending a signal to itself, which makes it + possible to receive all signals already in QEventQueue before starting the + slow data read operation. + + \section1 Applying the approach to dynamic models + + If one wishes to expand the solution towards a case where items may be + added, moved or removed from the data source (in this case RemoteMedia), + DataStorage needs to be updated with signals to match + QAbstractItemModel::rowsMoved(), QAbstractItemModel::rowsInserted() and + two signals to trigger the QAbstractItemModel::beginRemoveRows() and + QAbstractItemModel::endRemoveRows() inside ThreadedListModel. + + For the insertion and move the ThreadedListModel can just call + QAbstractItemModel::beginInsertRows(), then add new IDs to its ID list and + call QAbstractItemModel::endInsertRows(). As ThreadedListModel holds a copy + of the ID list and accesses storage by ID, there is no need to signal the + begin and end from storage. Equally ThreadedListModel can call + QAbstractItemModel::beginMoveRows(), move IDs in its ID list and then call + QAbstractItemModel::endMoveRows(). + + Removal is a slightly more complex case. The view needs to have a + possibility to request the data that is going to be removed before it is + actually removed. So DataStorage needs to signal a warning of the removal, + causing the Model to call QAbstractItemModel::beginRemoveRows(). At this + stage ThreadedListModel may get one or more \c data() calls. Once the call + to direct connected signal returns at DataStorage, it is OK for DataStorage + to remove the item and then signal the model again with another signal that + triggers the model to call QAbstractItemModel::endRemoveRows(). + + \include examples-run.qdocinc + +*/ diff --git a/examples/quick/models/threadedsonglist/images/album_covers/classical1.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/classical1.jpeg Binary files differnew file mode 100644 index 0000000000..b7af47e53b --- /dev/null +++ b/examples/quick/models/threadedsonglist/images/album_covers/classical1.jpeg diff --git a/examples/quick/models/threadedsonglist/images/album_covers/classical2.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/classical2.jpeg Binary files differnew file mode 100644 index 0000000000..d06411375e --- /dev/null +++ b/examples/quick/models/threadedsonglist/images/album_covers/classical2.jpeg diff --git a/examples/quick/models/threadedsonglist/images/album_covers/classical3.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/classical3.jpeg Binary files differnew file mode 100644 index 0000000000..6e16f84ba9 --- /dev/null +++ b/examples/quick/models/threadedsonglist/images/album_covers/classical3.jpeg diff --git a/examples/quick/models/threadedsonglist/images/album_covers/classical4.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/classical4.jpeg Binary files differnew file mode 100644 index 0000000000..07e6d72c02 --- /dev/null +++ b/examples/quick/models/threadedsonglist/images/album_covers/classical4.jpeg diff --git a/examples/quick/models/threadedsonglist/images/album_covers/classical5.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/classical5.jpeg Binary files differnew file mode 100644 index 0000000000..a7e7dce536 --- /dev/null +++ b/examples/quick/models/threadedsonglist/images/album_covers/classical5.jpeg diff --git a/examples/quick/models/threadedsonglist/images/album_covers/classical6.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/classical6.jpeg Binary files differnew file mode 100644 index 0000000000..9b6c146d98 --- /dev/null +++ b/examples/quick/models/threadedsonglist/images/album_covers/classical6.jpeg diff --git a/examples/quick/models/threadedsonglist/images/album_covers/metal1.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/metal1.jpeg Binary files differnew file mode 100644 index 0000000000..edea49d108 --- /dev/null +++ b/examples/quick/models/threadedsonglist/images/album_covers/metal1.jpeg diff --git a/examples/quick/models/threadedsonglist/images/album_covers/metal2.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/metal2.jpeg Binary files differnew file mode 100644 index 0000000000..dac8e02ece --- /dev/null +++ b/examples/quick/models/threadedsonglist/images/album_covers/metal2.jpeg diff --git a/examples/quick/models/threadedsonglist/images/album_covers/metal3.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/metal3.jpeg Binary files differnew file mode 100644 index 0000000000..c3044af90a --- /dev/null +++ b/examples/quick/models/threadedsonglist/images/album_covers/metal3.jpeg diff --git a/examples/quick/models/threadedsonglist/images/album_covers/metal4.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/metal4.jpeg Binary files differnew file mode 100644 index 0000000000..bda98ee77e --- /dev/null +++ b/examples/quick/models/threadedsonglist/images/album_covers/metal4.jpeg diff --git a/examples/quick/models/threadedsonglist/images/album_covers/metal5.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/metal5.jpeg Binary files differnew file mode 100644 index 0000000000..c467f53a19 --- /dev/null +++ b/examples/quick/models/threadedsonglist/images/album_covers/metal5.jpeg diff --git a/examples/quick/models/threadedsonglist/images/album_covers/metal6.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/metal6.jpeg Binary files differnew file mode 100644 index 0000000000..0b90d744d2 --- /dev/null +++ b/examples/quick/models/threadedsonglist/images/album_covers/metal6.jpeg diff --git a/examples/quick/models/threadedsonglist/images/album_covers/pop1.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/pop1.jpeg Binary files differnew file mode 100644 index 0000000000..65c09a576c --- /dev/null +++ b/examples/quick/models/threadedsonglist/images/album_covers/pop1.jpeg diff --git a/examples/quick/models/threadedsonglist/images/album_covers/pop2.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/pop2.jpeg Binary files differnew file mode 100644 index 0000000000..0bd2b28ca9 --- /dev/null +++ b/examples/quick/models/threadedsonglist/images/album_covers/pop2.jpeg diff --git a/examples/quick/models/threadedsonglist/images/album_covers/pop3.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/pop3.jpeg Binary files differnew file mode 100644 index 0000000000..683dc2931b --- /dev/null +++ b/examples/quick/models/threadedsonglist/images/album_covers/pop3.jpeg diff --git a/examples/quick/models/threadedsonglist/images/album_covers/pop4.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/pop4.jpeg Binary files differnew file mode 100644 index 0000000000..72ca07f3d3 --- /dev/null +++ b/examples/quick/models/threadedsonglist/images/album_covers/pop4.jpeg diff --git a/examples/quick/models/threadedsonglist/images/album_covers/pop5.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/pop5.jpeg Binary files differnew file mode 100644 index 0000000000..d7abcb0cac --- /dev/null +++ b/examples/quick/models/threadedsonglist/images/album_covers/pop5.jpeg diff --git a/examples/quick/models/threadedsonglist/images/album_covers/pop6.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/pop6.jpeg Binary files differnew file mode 100644 index 0000000000..217816a694 --- /dev/null +++ b/examples/quick/models/threadedsonglist/images/album_covers/pop6.jpeg diff --git a/examples/quick/models/threadedsonglist/images/album_covers/rock1.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/rock1.jpeg Binary files differnew file mode 100644 index 0000000000..728353ce83 --- /dev/null +++ b/examples/quick/models/threadedsonglist/images/album_covers/rock1.jpeg diff --git a/examples/quick/models/threadedsonglist/images/album_covers/rock2.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/rock2.jpeg Binary files differnew file mode 100644 index 0000000000..0fc626173d --- /dev/null +++ b/examples/quick/models/threadedsonglist/images/album_covers/rock2.jpeg diff --git a/examples/quick/models/threadedsonglist/images/album_covers/rock3.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/rock3.jpeg Binary files differnew file mode 100644 index 0000000000..96f3c342e7 --- /dev/null +++ b/examples/quick/models/threadedsonglist/images/album_covers/rock3.jpeg diff --git a/examples/quick/models/threadedsonglist/images/album_covers/rock4.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/rock4.jpeg Binary files differnew file mode 100644 index 0000000000..4e35aa6124 --- /dev/null +++ b/examples/quick/models/threadedsonglist/images/album_covers/rock4.jpeg diff --git a/examples/quick/models/threadedsonglist/images/album_covers/rock5.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/rock5.jpeg Binary files differnew file mode 100644 index 0000000000..7d3456419a --- /dev/null +++ b/examples/quick/models/threadedsonglist/images/album_covers/rock5.jpeg diff --git a/examples/quick/models/threadedsonglist/images/album_covers/rock6.jpeg b/examples/quick/models/threadedsonglist/images/album_covers/rock6.jpeg Binary files differnew file mode 100644 index 0000000000..fb71999e14 --- /dev/null +++ b/examples/quick/models/threadedsonglist/images/album_covers/rock6.jpeg diff --git a/examples/quick/models/threadedsonglist/images/device/remote.jpeg b/examples/quick/models/threadedsonglist/images/device/remote.jpeg Binary files differnew file mode 100644 index 0000000000..9751188536 --- /dev/null +++ b/examples/quick/models/threadedsonglist/images/device/remote.jpeg diff --git a/examples/quick/models/threadedsonglist/main.cpp b/examples/quick/models/threadedsonglist/main.cpp new file mode 100644 index 0000000000..60c12ad601 --- /dev/null +++ b/examples/quick/models/threadedsonglist/main.cpp @@ -0,0 +1,21 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include <QGuiApplication> +#include <QQmlApplicationEngine> + +int main(int argc, char *argv[]) +{ + QGuiApplication app(argc, argv); + + QQmlApplicationEngine engine; + QObject::connect( + &engine, + &QQmlApplicationEngine::objectCreationFailed, + &app, + []() { QCoreApplication::exit(-1); }, + Qt::QueuedConnection); + engine.loadFromModule("threadedsonglist", "ThreadedSongList"); + + return app.exec(); +} diff --git a/examples/quick/models/threadedsonglist/mediaelement.cpp b/examples/quick/models/threadedsonglist/mediaelement.cpp new file mode 100644 index 0000000000..6d93cd55ea --- /dev/null +++ b/examples/quick/models/threadedsonglist/mediaelement.cpp @@ -0,0 +1,42 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "mediaelement.h" + +MediaElement::MediaElement() {} + +MediaElement::MediaElement(QStringView songName, + QStringView artistName, + QStringView albumName, + const QUrl& albumArt) + : m_isValid{true} + , m_song{songName} + , m_artist{artistName} + , m_album{albumName} + , m_albumArtUrl{albumArt} +{} + +bool MediaElement::isValid() const +{ + return m_isValid; +} + +QUrl MediaElement::albumArtFile() const +{ + return m_albumArtUrl; +} + +QString MediaElement::song() const +{ + return m_song; +} + +QString MediaElement::artist() const +{ + return m_artist; +} + +QString MediaElement::album() const +{ + return m_album; +} diff --git a/examples/quick/models/threadedsonglist/mediaelement.h b/examples/quick/models/threadedsonglist/mediaelement.h new file mode 100644 index 0000000000..d93942f65d --- /dev/null +++ b/examples/quick/models/threadedsonglist/mediaelement.h @@ -0,0 +1,31 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef MEDIAELEMENT_H +#define MEDIAELEMENT_H + +#include <QUrl> + +class MediaElement +{ +public: + explicit MediaElement(); + MediaElement(QStringView songName, + QStringView artistName, + QStringView albumName, + const QUrl &albumArt); + bool isValid() const; + QUrl albumArtFile() const; + QString song() const; + QString artist() const; + QString album() const; + +private: + bool m_isValid{false}; + QString m_song; + QString m_artist; + QString m_album; + QUrl m_albumArtUrl; +}; + +#endif // MEDIAELEMENT_H diff --git a/examples/quick/models/threadedsonglist/queueworker.cpp b/examples/quick/models/threadedsonglist/queueworker.cpp new file mode 100644 index 0000000000..0b41a0e395 --- /dev/null +++ b/examples/quick/models/threadedsonglist/queueworker.cpp @@ -0,0 +1,68 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "queueworker.h" + +/* view needs 8 items and cacheBuffer of 2 items above and below to show visible area, so limit queue max length to that. */ +static constexpr int s_queueMaxLength{12}; + +QueueWorker::QueueWorker() + : QObject{nullptr} +{ + connect(this, + &QueueWorker::triggerQueueProcessing, + this, + &QueueWorker::processQueue, + Qt::QueuedConnection); +} + +void QueueWorker::abort() +{ + m_killSignalReceived = true; +} + +void QueueWorker::fetchData(int id) +{ + if (m_indicesAlreadyFetched.contains(id) || m_indicesToFetch.contains(id) + || m_killSignalReceived) + return; + + /* This is signal from UI thread. It is likely that there are more than one in queue. + * Add our own triggerQueueProcessing() signal to the end of event queue and add id to queue of IDs. + * Then let all queued signals to be processed until QueueWorker receives its own signal. + */ + sendTriggerQueueProcessingSignal(id); +} + +void QueueWorker::processQueue() +{ + /* This slot receives QueueWorker's own signal after all signals from UI thread have been processed. */ + m_queueProcessingSignalSent = false; + const auto index = m_indicesToFetch.dequeue(); + emit processing(index); + const auto data = m_remoteMedia.getElements(1, index); + m_indicesAlreadyFetched.insert(index); + emit dataFetched(index, data.front()); + if (!m_indicesToFetch.empty() && !m_killSignalReceived) { + /* Queueworker should process more indices, but the UI thread may have sent more signals during previous processing. + * Add our own signal to end of event queue and let all preceding signals to be processed before continuing. + */ + emit triggerQueueProcessing(QPrivateSignal{}); + m_queueProcessingSignalSent = true; + } + /* Else all fetches have been processed so far. */ +} + +void QueueWorker::sendTriggerQueueProcessingSignal(int id) +{ + /* Make sure excess IDs are removed. */ + while (m_indicesToFetch.size() >= s_queueMaxLength) { + emit dropped(m_indicesToFetch.dequeue()); + } + /* Before calling this function QueueWorker has ensured that m_indicesToFetch doesn't contain id yet, so just add id. */ + m_indicesToFetch.enqueue(id); + if (!m_queueProcessingSignalSent) { + emit triggerQueueProcessing(QPrivateSignal{}); + m_queueProcessingSignalSent = true; + } +} diff --git a/examples/quick/models/threadedsonglist/queueworker.h b/examples/quick/models/threadedsonglist/queueworker.h new file mode 100644 index 0000000000..6a30639d64 --- /dev/null +++ b/examples/quick/models/threadedsonglist/queueworker.h @@ -0,0 +1,41 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef QUEUEWORKER_H +#define QUEUEWORKER_H + +#include "remotemedia.h" + +#include <QObject> +#include <QQueue> +#include <QSet> + +class QueueWorker : public QObject +{ + Q_OBJECT +public: + explicit QueueWorker(); + void abort(); + +signals: + void processing(int index); + void dropped(int index); + void dataFetched(int index, MediaElement element); + void triggerQueueProcessing(QPrivateSignal); // signal to self, see implementation of fetchData() + +public slots: + void fetchData(int index); + +private slots: + void processQueue(); + +private: + void sendTriggerQueueProcessingSignal(int index); + RemoteMedia m_remoteMedia; + QSet<int> m_indicesAlreadyFetched; + QQueue<int> m_indicesToFetch; + std::atomic<bool> m_killSignalReceived{false}; + bool m_queueProcessingSignalSent{false}; +}; + +#endif // QUEUEWORKER_H diff --git a/examples/quick/models/threadedsonglist/remotemedia.cpp b/examples/quick/models/threadedsonglist/remotemedia.cpp new file mode 100644 index 0000000000..0556458035 --- /dev/null +++ b/examples/quick/models/threadedsonglist/remotemedia.cpp @@ -0,0 +1,29 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "remotemedia.h" +#include "songdatagenerator.h" + +#include <QThread> + +static constexpr int s_fetchMilliSecondsPerSong{100}; + +RemoteMedia::RemoteMedia() +{ + SongDataGenerator dataGenerator; + m_items = dataGenerator.createSongList(); +} + +int RemoteMedia::count() +{ + return SongDataGenerator::elementMaxCount(); +} + +QList<MediaElement> RemoteMedia::getElements(int amount, int offset) const +{ + if (amount < 1) + return {}; + QList<MediaElement> returnValue{m_items.cbegin() + offset, m_items.cbegin() + offset + amount}; + QThread::sleep(std::chrono::milliseconds{s_fetchMilliSecondsPerSong * amount}); + return returnValue; +} diff --git a/examples/quick/models/threadedsonglist/remotemedia.h b/examples/quick/models/threadedsonglist/remotemedia.h new file mode 100644 index 0000000000..64c081deac --- /dev/null +++ b/examples/quick/models/threadedsonglist/remotemedia.h @@ -0,0 +1,25 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef REMOTEMEDIA_H +#define REMOTEMEDIA_H +#include "mediaelement.h" + +#include <QList> + +class RemoteMedia +{ +public: + explicit RemoteMedia(); + + /* Fast access to total count of items (so UI knows the total item list size). */ + static int count(); + + /* Getter (100 milliseconds per item, function call is blocking the thread) */ + QList<MediaElement> getElements(int amount, int offset) const; + +private: + QList<MediaElement> m_items; +}; + +#endif // REMOTEMEDIA_H diff --git a/examples/quick/models/threadedsonglist/songdatagenerator.cpp b/examples/quick/models/threadedsonglist/songdatagenerator.cpp new file mode 100644 index 0000000000..42bf9cfe33 --- /dev/null +++ b/examples/quick/models/threadedsonglist/songdatagenerator.cpp @@ -0,0 +1,182 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "songdatagenerator.h" + +#include <QStringLiteral> + +static constexpr int s_albumCount{6}; +static constexpr int s_bandNamePrefixSuffixCount{5}; +static constexpr int s_songNamePrefixSuffixCount{6}; +static constexpr int s_genreCount{4}; + +SongDataGenerator::SongDataGenerator() +{ + //classical music + m_bandPrefixes << QStringList{QStringLiteral("Qtie's"), + QStringLiteral("Kjuutburg"), + QStringLiteral("Qtional"), + QStringLiteral("Kjuutinese"), + QStringLiteral("Qtian's")}; + m_bandSuffixes << QStringList{QStringLiteral("Philharmonic QOrchestra"), + QStringLiteral("Theatre QOrchestra"), + QStringLiteral("Opera QOrchestra"), + QStringLiteral("City QChoir"), + QStringLiteral("University QOrchestra")}; + // metal music + m_bandPrefixes << QStringList{QStringLiteral("QtBlack"), + QStringLiteral("QtDark"), + QStringLiteral("QtHard"), + QStringLiteral("QTest"), + QStringLiteral("QtPale")}; + m_bandSuffixes << QStringList{QStringLiteral("Death"), + QStringLiteral("Creature"), + QStringLiteral("Devil"), + QStringLiteral("Souls"), + QStringLiteral("Being")}; + // pop music + m_bandPrefixes << QStringList{QStringLiteral("Happy"), + QStringLiteral("Joyful"), + QStringLiteral("Pretty"), + QStringLiteral("Crazy"), + QStringLiteral("Wild")}; + m_bandSuffixes << QStringList{QStringLiteral("QPink"), + QStringLiteral("QLife"), + QStringLiteral("QGirls"), + QStringLiteral("QBoys"), + QStringLiteral("QMood")}; + // rock music + m_bandPrefixes << QStringList{QStringLiteral("Grateful"), + QStringLiteral("Hard"), + QStringLiteral("Rolling"), + QStringLiteral("Clashing"), + QStringLiteral("Breaking")}; + m_bandSuffixes << QStringList{QStringLiteral("QML Thunder"), + QStringLiteral("QML Slam"), + QStringLiteral("QML Rocks"), + QStringLiteral("QML Rally"), + QStringLiteral("QML Tune")}; + + // classical music + m_songPrefixes << QStringList{"QWaltz", "QPrelude", "QDance", "QSonata", "QSymphony", "QPiano"}; + m_songSuffixes << QStringList{"in D Minor", + "of the Pixies", + "by Night", + "on Violin", + "no. 5", + "in tune"}; + // metal music + m_songPrefixes << QStringList{"QHunger", "QScreaming", "QPain", "QIllusion", "QBeauty", "QDream"}; + m_songSuffixes << QStringList{"That Never Ends", + "Rising", + "in My Soul", + "from The Dark", + "on the Night", + "from Beyond"}; + // pop music + m_songPrefixes << QStringList{"Jumping", "Running", "Playing", "Laughing", "Singing", "Joking"}; + m_songSuffixes << QStringList{"Qt Alone", + "of Qt Life", + "for Qt Fun", + "Q-Day", + "with Qties", + "All the QTime"}; + // rock music + m_songPrefixes << QStringList{"Full Steam", "Going", "Looking", "Always", "Crazy", "Drive Me"}; + m_songSuffixes << QStringList{"QWild", + "Qt-wards", + "to the QEnd", + "for you, Qtie", + "QTonight", + "QHome"}; + Q_ASSERT(m_bandPrefixes.size() == s_genreCount); + Q_ASSERT(m_bandSuffixes.size() == s_genreCount); + Q_ASSERT(m_songPrefixes.size() == s_genreCount); + Q_ASSERT(m_songSuffixes.size() == s_genreCount); + for (const auto &prefixList : std::as_const(m_bandPrefixes)) { + Q_ASSERT(prefixList.size() == s_bandNamePrefixSuffixCount); + } + for (const auto &suffixList : std::as_const(m_bandSuffixes)) { + Q_ASSERT(suffixList.size() == s_bandNamePrefixSuffixCount); + } + for (const auto &prefixList : std::as_const(m_songPrefixes)) { + Q_ASSERT(prefixList.size() == s_songNamePrefixSuffixCount); + } + for (const auto &suffixList : std::as_const(m_songSuffixes)) { + Q_ASSERT(suffixList.size() == s_songNamePrefixSuffixCount); + } +} + +int SongDataGenerator::elementMaxCount() +{ + return s_bandNamePrefixSuffixCount * s_songNamePrefixSuffixCount * s_bandNamePrefixSuffixCount + * s_songNamePrefixSuffixCount * s_genreCount; +} + +QList<MediaElement> SongDataGenerator::createSongList() const +{ + QList<MediaElement> returnList; + returnList.reserve(elementMaxCount()); + for (int bandSuffix = 0; bandSuffix < s_bandNamePrefixSuffixCount; ++bandSuffix) { + for (int songSuffix = 0; songSuffix < s_songNamePrefixSuffixCount; ++songSuffix) { + for (int bandPrefix = 0; bandPrefix < s_bandNamePrefixSuffixCount; ++bandPrefix) { + for (int songPrefix = 0; songPrefix < s_songNamePrefixSuffixCount; ++songPrefix) { + for (int genre = 0; genre < s_genreCount; ++genre) { + returnList.append( + MediaElement{songName(songPrefix, songSuffix, genre), + bandName(bandPrefix, bandSuffix, genre), + SongDataGenerator::albumName(bandPrefix, songPrefix, genre), + SongDataGenerator::albumArt(bandPrefix, songPrefix, genre)}); + } + } + } + } + } + Q_ASSERT(returnList.size() == elementMaxCount()); + return returnList; +} + +QString SongDataGenerator::bandName(int bandNamePrefixIndex, + int bandNameSuffixIndex, + int genreIndex) const +{ + return QStringLiteral("%1 %2").arg(m_bandPrefixes.at(genreIndex).at(bandNamePrefixIndex), + m_bandSuffixes.at(genreIndex).at(bandNameSuffixIndex)); +} + +QString SongDataGenerator::songName(int songNamePrefixIndex, + int songNameSuffixIndex, + int genreIndex) const +{ + return QStringLiteral("%1 %2").arg(m_songPrefixes.at(genreIndex).at(songNamePrefixIndex), + m_songSuffixes.at(genreIndex).at(songNameSuffixIndex)); +} + +QString SongDataGenerator::albumName(int bandNamePrefixIndex, + int songNamePrefixIndex, + int genreIndex) +{ + static const QStringList albums{"First QAlbum", + "The QBest Of", + "World QtTour", + "Live at Qt Arena", + "QEnchanted", + "QFarewell"}; + Q_ASSERT(albums.size() == s_albumCount); + return albums.at(albumIndex(bandNamePrefixIndex, songNamePrefixIndex, genreIndex)); +} + +QUrl SongDataGenerator::albumArt(int bandNamePrefixIndex, int songNamePrefixIndex, int genreIndex) +{ + static const QStringList genres{"classical", "metal", "pop", "rock"}; + return QUrl{QStringLiteral("qrc:/qt/qml/threadedsonglist/images/album_covers/%1%2.jpeg") + .arg(genres.at(genreIndex)) + .arg(albumIndex(bandNamePrefixIndex, songNamePrefixIndex, genreIndex) + 1)}; +} + +int SongDataGenerator::albumIndex(int bandNamePrefixIndex, int songNamePrefixIndex, int genreIndex) +{ + return ((bandNamePrefixIndex * s_songNamePrefixSuffixCount) + songNamePrefixIndex + + (genreIndex % s_genreCount)) + % s_albumCount; +} diff --git a/examples/quick/models/threadedsonglist/songdatagenerator.h b/examples/quick/models/threadedsonglist/songdatagenerator.h new file mode 100644 index 0000000000..9227a5badb --- /dev/null +++ b/examples/quick/models/threadedsonglist/songdatagenerator.h @@ -0,0 +1,30 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef SONGDATAGENERATOR_H +#define SONGDATAGENERATOR_H +#include "mediaelement.h" + +#include <QPair> +#include <QStringList> + +class SongDataGenerator +{ +public: + explicit SongDataGenerator(); + static int elementMaxCount(); + QList<MediaElement> createSongList() const; + +private: + QString bandName(int bandNamePrefixIndex, int bandNameSuffixIndex, int genreIndex) const; + QString songName(int songNamePrefixIndex, int songNameSuffixIndex, int genreIndex) const; + static QString albumName(int bandNamePrefixIndex, int songNamePrefixIndex, int genreIndex); + static QUrl albumArt(int bandNamePrefixIndex, int songNamePrefixIndex, int genreIndex); + static int albumIndex(int bandNamePrefixIndex, int songNamePrefixIndex, int genreIndex); + QList<QStringList> m_bandPrefixes; + QList<QStringList> m_bandSuffixes; + QList<QStringList> m_songPrefixes; + QList<QStringList> m_songSuffixes; +}; + +#endif // SONGDATAGENERATOR_H diff --git a/examples/quick/models/threadedsonglist/threadedlistmodel.cpp b/examples/quick/models/threadedsonglist/threadedlistmodel.cpp new file mode 100644 index 0000000000..b509d88ead --- /dev/null +++ b/examples/quick/models/threadedsonglist/threadedlistmodel.cpp @@ -0,0 +1,75 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "threadedlistmodel.h" + +ThreadedListModel::ThreadedListModel() +{ + m_idList = m_storage.idList(); + connect(&m_storage, &DataStorage::dataUpdated, this, &ThreadedListModel::dataUpdated); +} + +int ThreadedListModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + return m_idList.size(); +} + +QVariant ThreadedListModel::data(const QModelIndex &index, int role) const +{ + const auto id = m_idList.at(index.row()); + const auto &item = m_storage.item(id); + if (item.isValid()) { + switch (static_cast<Role>(role)) { + case Role::Album: + return item.album(); + case Role::AlbumArt: + return item.albumArtFile(); + case Role::Artist: + return item.artist(); + case Role::Song: + return item.song(); + case Role::LoadingElement: + return false; + case Role::LoadingText: + return QString(); + default: + break; + } + } else { + /* Item not fetched yet, pass "loading" item. */ + switch (static_cast<Role>(role)) { + case Role::Album: + case Role::AlbumArt: + case Role::Artist: + case Role::Song: + return QString{}; + case Role::LoadingElement: + return true; + case Role::LoadingText: + return m_storage.currentlyFetchedId() == id ? QString{} : tr("Waiting..."); + default: + break; + } + } + return QVariant{}; +} + +void ThreadedListModel::dataUpdated(int id) +{ + const QModelIndex changedIndex = index(m_idList.indexOf(id)); + emit dataChanged(changedIndex, changedIndex); +} + +QHash<int, QByteArray> ThreadedListModel::roleNames() const +{ + static const QHash<int, QByteArray> roles{{static_cast<int>(Role::Song), "song"}, + {static_cast<int>(Role::Artist), "artist"}, + {static_cast<int>(Role::Album), "album"}, + {static_cast<int>(Role::AlbumArt), "albumArt"}, + {static_cast<int>(Role::LoadingText), "loadingText"}, + {static_cast<int>(Role::LoadingElement), + "isLoadingElement"}}; + return roles; +} diff --git a/examples/quick/models/threadedsonglist/threadedlistmodel.h b/examples/quick/models/threadedsonglist/threadedlistmodel.h new file mode 100644 index 0000000000..b9d4056b15 --- /dev/null +++ b/examples/quick/models/threadedsonglist/threadedlistmodel.h @@ -0,0 +1,41 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef THREADEDLISTMODEL_H +#define THREADEDLISTMODEL_H + +#include "datastorage.h" + +#include <QAbstractListModel> +#include <QQmlEngine> + +class ThreadedListModel : public QAbstractListModel +{ + Q_OBJECT + QML_ELEMENT +public: + explicit ThreadedListModel(); + Q_INVOKABLE int rowCount(const QModelIndex &parent = QModelIndex()) const override; + Q_INVOKABLE QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash<int, QByteArray> roleNames() const override; + +protected: + enum class Role { + Song = Qt::ItemDataRole::UserRole, + Artist, + Album, + AlbumArt, + LoadingElement, + LoadingText + }; + +private slots: + void dataUpdated(int id); + +private: + DataStorage m_storage; + QList<int> m_idList; + int m_currentSongId{-1}; +}; + +#endif // THREADEDLISTMODEL_H diff --git a/examples/quick/models/threadedsonglist/threadedsonglist.pro b/examples/quick/models/threadedsonglist/threadedsonglist.pro new file mode 100644 index 0000000000..bc83d7c89f --- /dev/null +++ b/examples/quick/models/threadedsonglist/threadedsonglist.pro @@ -0,0 +1,20 @@ +TARGET = threadedsonglist +QT += qml quick + +HEADERS = datastorage.h \ + mediaelement.h \ + queueworker.h \ + remotemedia.h \ + songdatagenerator.h \ + threadedlistmodel.h +SOURCES = main.cpp \ + datastorage.cpp \ + mediaelement.cpp \ + queueworker.cpp \ + remotemedia.cpp \ + songdatagenerator.cpp \ + threadedlistmodel.cpp +RESOURCES += threadedsonglist.qrc + +target.path = $$[QT_INSTALL_EXAMPLES]/quick/models/threadedsonglist +INSTALLS += target diff --git a/examples/quick/models/threadedsonglist/threadedsonglist.qrc b/examples/quick/models/threadedsonglist/threadedsonglist.qrc new file mode 100644 index 0000000000..ac4cd3c049 --- /dev/null +++ b/examples/quick/models/threadedsonglist/threadedsonglist.qrc @@ -0,0 +1,32 @@ +<!DOCTYPE RCC><RCC version="1.0"> +<qresource prefix="/"> + <file>SongListDelegate.qml</file> + <file>ThreadedSongList.qml</file> + <file>images/album_covers/classical1.jpeg</file> + <file>images/album_covers/classical2.jpeg</file> + <file>images/album_covers/classical3.jpeg</file> + <file>images/album_covers/classical4.jpeg</file> + <file>images/album_covers/classical5.jpeg</file> + <file>images/album_covers/classical6.jpeg</file> + <file>images/album_covers/metal1.jpeg</file> + <file>images/album_covers/metal2.jpeg</file> + <file>images/album_covers/metal3.jpeg</file> + <file>images/album_covers/metal4.jpeg</file> + <file>images/album_covers/metal5.jpeg</file> + <file>images/album_covers/metal6.jpeg</file> + <file>images/album_covers/pop1.jpeg</file> + <file>images/album_covers/pop2.jpeg</file> + <file>images/album_covers/pop3.jpeg</file> + <file>images/album_covers/pop4.jpeg</file> + <file>images/album_covers/pop5.jpeg</file> + <file>images/album_covers/pop6.jpeg</file> + <file>images/album_covers/rock1.jpeg</file> + <file>images/album_covers/rock2.jpeg</file> + <file>images/album_covers/rock3.jpeg</file> + <file>images/album_covers/rock4.jpeg</file> + <file>images/album_covers/rock5.jpeg</file> + <file>images/album_covers/rock6.jpeg</file> + <file>images/device/remote.jpeg</file> +</qresource> +</RCC> + |