diff --git a/src/adaptors/UBThumbnailAdaptor.cpp b/src/adaptors/UBThumbnailAdaptor.cpp index dc33b2bf3..3e7c44c28 100644 --- a/src/adaptors/UBThumbnailAdaptor.cpp +++ b/src/adaptors/UBThumbnailAdaptor.cpp @@ -83,6 +83,27 @@ void UBThumbnailAdaptor::generateMissingThumbnails(std::shared_ptr proxy, int pageIndex) +{ + QString thumbFileName = proxy->persistencePath() + UBFileSystemUtils::digitFileFormat("/page%1.thumbnail.jpg", pageIndex); + + QFile thumbFile(thumbFileName); + + if (!thumbFile.exists()) + { + std::shared_ptr scene = UBSvgSubsetAdaptor::loadScene(proxy, pageIndex); + + if (scene) + { + persistScene(proxy, scene, pageIndex); + } + } + + QPixmap pix; + pix.load(thumbFileName); + return pix; +} + QPixmap UBThumbnailAdaptor::get(std::shared_ptr proxy, int pageIndex) { QString fileName = proxy->persistencePath() + UBFileSystemUtils::digitFileFormat("/page%1.thumbnail.jpg", pageIndex); diff --git a/src/adaptors/UBThumbnailAdaptor.h b/src/adaptors/UBThumbnailAdaptor.h index 922ac782e..767c7a2ac 100644 --- a/src/adaptors/UBThumbnailAdaptor.h +++ b/src/adaptors/UBThumbnailAdaptor.h @@ -47,6 +47,7 @@ class UBThumbnailAdaptor //static class static QPixmap get(std::shared_ptr proxy, int index); static void load(std::shared_ptr proxy, QList>& list); + static QPixmap generateMissingThumbnail(std::shared_ptr proxy, int pageIndex); private: static void generateMissingThumbnails(std::shared_ptr proxy); diff --git a/src/frameworks/CMakeLists.txt b/src/frameworks/CMakeLists.txt index aab3eb7be..faa702e72 100644 --- a/src/frameworks/CMakeLists.txt +++ b/src/frameworks/CMakeLists.txt @@ -1,4 +1,6 @@ target_sources(openboard PRIVATE + UBBackgroundLoader.cpp + UBBackgroundLoader.h UBBase32.cpp UBBase32.h UBCoreGraphicsScene.cpp diff --git a/src/frameworks/UBBackgroundLoader.cpp b/src/frameworks/UBBackgroundLoader.cpp new file mode 100644 index 000000000..10056cd33 --- /dev/null +++ b/src/frameworks/UBBackgroundLoader.cpp @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2015-2025 Département de l'Instruction Publique (DIP-SEM) + * + * This file is part of OpenBoard. + * + * OpenBoard is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, + * with a specific linking exception for the OpenSSL project's + * "OpenSSL" library (or with modified versions of it that use the + * same license as the "OpenSSL" library). + * + * OpenBoard is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with OpenBoard. If not, see . + */ + + +#include "UBBackgroundLoader.h" + +#include + +#include "core/UBApplication.h" + +UBBackgroundLoader::UBBackgroundLoader(QObject* parent) + : QThread{parent} +{ +} + +UBBackgroundLoader::UBBackgroundLoader(QList> paths, QObject* parent) + : QThread{parent} +{ + mPaths.insert(mPaths.cend(), paths.constBegin(), paths.constEnd()); + mPathCounter.release(paths.size()); +} + +UBBackgroundLoader::~UBBackgroundLoader() +{ + abort(); + wait(); +} + +bool UBBackgroundLoader::isResultAvailable() +{ + QMutexLocker lock{&mMutex}; + return !mResults.empty(); +} + +std::pair UBBackgroundLoader::takeResult() +{ + QMutexLocker lock{&mMutex}; + + if (mResults.empty()) + { + return {}; + } + + const auto result = mResults.front(); + mResults.pop_front(); + return result; +} + +void UBBackgroundLoader::start() +{ + mRunning = true; + QThread::start(); +} + +void UBBackgroundLoader::addPaths(QList> paths) +{ + QMutexLocker lock{&mMutex}; + mPaths.insert(mPaths.cend(), paths.constBegin(), paths.constEnd()); + mPathCounter.release(paths.size()); +} + +void UBBackgroundLoader::abort() +{ + mRunning = false; + mPathCounter.release(); +} + +void UBBackgroundLoader::run() +{ + while (mRunning && !UBApplication::isClosing) + { + mPathCounter.acquire(); + + if (mRunning && !UBApplication::isClosing) + { + std::pair path; + + { + QMutexLocker lock{&mMutex}; + path = mPaths.front(); + mPaths.pop_front(); + } + + QFile file{path.second}; + QByteArray result; + + if (file.open(QFile::ReadOnly)) + { + result = file.readAll(); + file.close(); + } + + { + QMutexLocker lock{&mMutex}; + mResults.push_back({path.first, result}); + } + + emit resultAvailable(path.first, result); + } + } + + quit(); +} diff --git a/src/frameworks/UBBackgroundLoader.h b/src/frameworks/UBBackgroundLoader.h new file mode 100644 index 000000000..c65f1bd95 --- /dev/null +++ b/src/frameworks/UBBackgroundLoader.h @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2015-2025 Département de l'Instruction Publique (DIP-SEM) + * + * This file is part of OpenBoard. + * + * OpenBoard is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, + * with a specific linking exception for the OpenSSL project's + * "OpenSSL" library (or with modified versions of it that use the + * same license as the "OpenSSL" library). + * + * OpenBoard is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with OpenBoard. If not, see . + */ + + +#pragma once + +#include +#include +#include +#include +#include + +class UBBackgroundLoader : public QThread +{ + Q_OBJECT + +public: + explicit UBBackgroundLoader(QObject* parent = nullptr); + UBBackgroundLoader(QList> paths, QObject* parent = nullptr); + virtual ~UBBackgroundLoader(); + + bool isResultAvailable(); + std::pair takeResult(); + +public slots: + void start(); + void addPaths(QList> paths); + void abort(); + +signals: + void resultAvailable(int index, QByteArray data); + +protected: + void run() override; + +private: + std::deque> mPaths{}; + std::deque> mResults{}; + QMutex mMutex{}; + QSemaphore mPathCounter{}; + bool mRunning{false}; +}; diff --git a/src/frameworks/frameworks.pri b/src/frameworks/frameworks.pri index 74d6edbbe..728b8f8df 100644 --- a/src/frameworks/frameworks.pri +++ b/src/frameworks/frameworks.pri @@ -6,6 +6,7 @@ HEADERS += src/frameworks/UBGeometryUtils.h \ src/frameworks/UBVersion.h \ src/frameworks/UBCoreGraphicsScene.h \ src/frameworks/UBCryptoUtils.h \ + src/frameworks/UBBackgroundLoader.h \ src/frameworks/UBBase32.h SOURCES += src/frameworks/UBGeometryUtils.cpp \ @@ -15,6 +16,7 @@ SOURCES += src/frameworks/UBGeometryUtils.cpp \ src/frameworks/UBVersion.cpp \ src/frameworks/UBCoreGraphicsScene.cpp \ src/frameworks/UBCryptoUtils.cpp \ + src/frameworks/UBBackgroundLoader.cpp \ src/frameworks/UBBase32.cpp diff --git a/src/gui/UBThumbnailScene.cpp b/src/gui/UBThumbnailScene.cpp index 38e0a7068..f8ff64469 100644 --- a/src/gui/UBThumbnailScene.cpp +++ b/src/gui/UBThumbnailScene.cpp @@ -26,6 +26,7 @@ #include "core/UBApplication.h" #include "document/UBDocument.h" #include "document/UBDocumentProxy.h" +#include "frameworks/UBBackgroundLoader.h" #include "gui/UBThumbnail.h" #include "gui/UBThumbnailArranger.h" #include "gui/UBThumbnailsView.h" @@ -172,7 +173,26 @@ void UBThumbnailScene::createThumbnails(int startIndex) delete item; } - // now create all missing thumbnails for document + // create the list of all thumbnail paths + QList> paths; + + for (int index = startIndex; index < mDocument->proxy()->pageCount(); ++index) + { + paths << std::pair{index, UBThumbnailAdaptor::thumbnailUrl(mDocument->proxy(), index).toLocalFile()}; + } + + // start background loading of files + if (mLoader) + { + // abort a running loader + mLoader->abort(); + delete mLoader; + } + + mLoader = new UBBackgroundLoader{paths, this}; + mLoader->start(); + + // now create all missing thumbnails for document as they arrive from the loader loadNextThumbnail(); } @@ -212,6 +232,12 @@ void UBThumbnailScene::insertThumbnail(int pageIndex, std::shared_ptrsetDeletable(false); @@ -252,6 +284,12 @@ void UBThumbnailScene::moveThumbnail(int fromIndex, int toIndex) renumberThumbnails(fromIndex, toIndex + 1); arrangeThumbnails(fromIndex, toIndex + 1); } + + if (mLoader) + { + // restart loading remaining thumbnails + createThumbnails(mThumbnailItems.size()); + } } void UBThumbnailScene::reloadThumbnail(int pageIndex) @@ -293,7 +331,7 @@ UBThumbnailArranger* UBThumbnailScene::currentThumbnailArranger() void UBThumbnailScene::loadNextThumbnail() { - // number of thumbnails to load in one pass + // max number of thumbnails to load in one pass constexpr int bulkSize{10}; if (mThumbnailItems.size() < mDocument->proxy()->pageCount()) @@ -303,20 +341,40 @@ void UBThumbnailScene::loadNextThumbnail() return; } + if (!mLoader->isResultAvailable()) + { + // no data available, defer next execution + QTimer::singleShot(50, mLoader, [this]() { loadNextThumbnail(); }); + return; + } + const auto firstIndex = mThumbnailItems.size(); for (int i = 0; i < bulkSize; ++i) { - int nextIndex = mThumbnailItems.size(); - - if (nextIndex == mDocument->proxy()->pageCount()) + if (!mLoader->isResultAvailable()) { break; } + // take next result and determine index from current number of thumbnails and + // not from result, because pages may have been added or removed in the meantime + const auto result = mLoader->takeResult(); + const auto nextIndex = mThumbnailItems.size(); + QPixmap pixmap; + + if (result.second.isEmpty()) + { + pixmap = UBThumbnailAdaptor::generateMissingThumbnail(mDocument->proxy(), nextIndex); + } + else + { + pixmap.loadFromData(result.second); + } + auto thumbnailItem = new UBThumbnail; - thumbnailItem->setPixmap(UBThumbnailAdaptor::get(mDocument->proxy(), nextIndex)); + thumbnailItem->setPixmap(pixmap); thumbnailItem->setSceneIndex(nextIndex); mThumbnailItems << thumbnailItem; @@ -326,7 +384,7 @@ void UBThumbnailScene::loadNextThumbnail() arrangeThumbnails(firstIndex); // load next thumbnails in a deferred task executed on the main thread when it is idle. - QTimer::singleShot(0, this, [this]() { loadNextThumbnail(); }); + QTimer::singleShot(1, mLoader, [this]() { loadNextThumbnail(); }); } else { @@ -335,6 +393,10 @@ void UBThumbnailScene::loadNextThumbnail() { mThumbnailItems.first()->setDeletable(false); } + + // delete background loader + delete mLoader; + mLoader = nullptr; } } diff --git a/src/gui/UBThumbnailScene.h b/src/gui/UBThumbnailScene.h index da659af0b..c17ce2624 100644 --- a/src/gui/UBThumbnailScene.h +++ b/src/gui/UBThumbnailScene.h @@ -28,6 +28,7 @@ #include "core/UBSettings.h" // forward +class UBBackgroundLoader; class UBDocument; class UBGraphicsScene; class UBThumbnail; @@ -76,4 +77,5 @@ class UBThumbnailScene : public QGraphicsScene UBDocument* mDocument{nullptr}; QList mThumbnailItems{}; int mThumbnailWidth{UBSettings::defaultThumbnailWidth}; + UBBackgroundLoader* mLoader{nullptr}; };