feat: able to cancel loading at any time & add tons of multithreading bugs

This commit is contained in:
2025-08-08 02:50:27 +02:00
parent aa1a844413
commit 490f924bcc
5 changed files with 138 additions and 56 deletions
+5 -5
View File
@@ -53,11 +53,11 @@ target_link_libraries(wallpaper_chooser PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)
target_include_directories(wallpaper_chooser PRIVATE src)
if(NOT ${CMAKE_BUILD_TYPE} STREQUAL "Debug")
target_compile_definitions(wallpaper_chooser PRIVATE
GENERAL_LOGGER_DISABLED
)
endif()
# if(NOT ${CMAKE_BUILD_TYPE} STREQUAL "Debug")
# target_compile_definitions(wallpaper_chooser PRIVATE
# GENERAL_LOGGER_DISABLED
# )
# endif()
# Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1.
# If you are developing for iOS or macOS you should consider setting an
+49 -24
View File
@@ -1,17 +1,13 @@
/*
* @Author: Uyanide pywang0608@foxmail.com
* @Date: 2025-08-05 01:22:53
* @LastEditTime: 2025-08-08 00:43:47
* @LastEditTime: 2025-08-08 02:34:24
* @Description: Animated carousel widget for displaying and selecting images.
*/
#include "images_carousel.h"
#include <assert.h>
#include <pthread.h>
#include <qboxlayout.h>
#include <qdebug.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qpixmap.h>
#include <QLabel>
#include <QMetaObject>
@@ -76,7 +72,7 @@ void ImagesCarousel::_onInitImagesLoaded() {
if (m_loadedImages.isEmpty()) {
return;
}
_focusCurrImage();
focusCurrImage();
}
ImagesCarousel::~ImagesCarousel() {
@@ -91,8 +87,8 @@ ImagesCarousel::~ImagesCarousel() {
void ImagesCarousel::appendImages(const QStringList& paths) {
{
QMutexLocker locker(&m_imageCountMutex);
m_imageCount += paths.size();
QMutexLocker locker(&m_countMutex);
m_addedImagesCount += paths.size();
}
emit loadingStarted(paths.size());
for (const QString& path : paths) {
@@ -152,14 +148,31 @@ void ImagesCarousel::_insertImage(const ImageData* data) {
emit imageLoaded(m_loadedImages.size());
{
QMutexLocker countLocker(&m_imageCountMutex);
if (m_loadedImages.size() >= m_imageCount) {
emit loadingCompleted(m_loadedImages.size());
QMutexLocker countLocker(&m_countMutex);
if (++m_loadedImagesCount >= m_addedImagesCount) {
QMutexLocker stopSignLocker(&m_stopSignMutex);
if (m_stopSign) {
// if all stopped
emit stopped();
} else {
emit loadingCompleted(m_loadedImagesCount);
}
}
}
}
void ImageLoader::run() {
{
QMutexLocker countLocker(&m_carousel->m_countMutex);
QMutexLocker stopSignLocker(&m_carousel->m_stopSignMutex);
if (m_carousel->m_stopSign) {
// if all stopped
if (++m_carousel->m_loadedImagesCount == m_carousel->m_addedImagesCount) {
emit m_carousel->stopped();
}
return;
}
}
auto data = new ImageData(m_path, m_initWidth, m_initHeight);
QMetaObject::invokeMethod(m_carousel,
"_insertImage",
@@ -183,32 +196,38 @@ ImageData::ImageData(const QString& p, const int initWidth, const int initHeight
}
void ImagesCarousel::focusNextImage() {
_unfocusCurrImage();
unfocusCurrImage();
if (m_loadedImages.size() <= 1) return;
m_currentIndex++;
if (m_currentIndex >= m_loadedImages.size()) {
m_currentIndex = 0;
}
_focusCurrImage();
focusCurrImage();
}
void ImagesCarousel::focusPrevImage() {
if (m_loadedImages.size() <= 1) return;
_unfocusCurrImage();
unfocusCurrImage();
m_currentIndex--;
if (m_currentIndex < 0) {
m_currentIndex = m_loadedImages.size() - 1;
}
_focusCurrImage();
focusCurrImage();
}
void ImagesCarousel::_unfocusCurrImage() {
// bound check was (or should) done by caller
void ImagesCarousel::unfocusCurrImage() {
if (m_currentIndex < 0 || m_currentIndex >= m_loadedImages.size()) {
error(QString("Invalid index to unfocus: %1").arg(m_currentIndex));
return;
}
m_loadedImages[m_currentIndex]->setFocus(false);
}
void ImagesCarousel::_focusCurrImage() {
// bound check was (or should) done by caller
void ImagesCarousel::focusCurrImage() {
if (m_currentIndex < 0 || m_currentIndex >= m_loadedImages.size()) {
error(QString("Invalid index to focus: %1").arg(m_currentIndex));
return;
}
m_loadedImages[m_currentIndex]->setFocus(true);
emit imageFocused(m_loadedImages[m_currentIndex]->getFileFullPath(),
m_currentIndex,
@@ -263,19 +282,19 @@ void ImagesCarousel::_onScrollBarValueChanged(int value) {
if (index == m_currentIndex) {
return; // Already focused
}
_unfocusCurrImage();
unfocusCurrImage();
m_currentIndex = index;
_focusCurrImage();
focusCurrImage();
}
void ImagesCarousel::_onItemClicked(int index) {
// if (m_suppressAutoFocus) return;
_unfocusCurrImage();
unfocusCurrImage();
m_currentIndex = index;
if (index < 0 || index >= m_loadedImages.size()) {
return; // Out of bounds
}
_focusCurrImage();
focusCurrImage();
}
ImageItem::ImageItem(const ImageData* data,
@@ -288,6 +307,7 @@ ImageItem::ImageItem(const ImageData* data,
m_data(data),
m_itemSize(itemWidth, itemHeight),
m_itemFocusSize(itemFocusWidth, itemFocusHeight) {
assert(data != nullptr);
setScaledContents(true);
if (data->image.isNull()) {
setText(":(");
@@ -326,3 +346,8 @@ void ImageItem::setFocus(bool focus) {
});
m_scaleAnimation->start();
}
void ImagesCarousel::onStop() {
QMutexLocker locker(&m_stopSignMutex);
m_stopSign = true;
}
+49 -19
View File
@@ -1,7 +1,7 @@
/*
* @Author: Uyanide pywang0608@foxmail.com
* @Date: 2025-08-05 01:22:53
* @LastEditTime: 2025-08-07 22:16:37
* @LastEditTime: 2025-08-08 02:41:05
* @Description: Animated carousel widget for displaying and selecting images.
*/
#ifndef IMAGES_CAROUSEL_H
@@ -44,7 +44,8 @@ struct ImageData {
};
/**
* @brief Image label that displays an image
* @brief Image label that displays an image,
* which should always be created in the main thread.
*/
class ImageItem : public QLabel {
Q_OBJECT
@@ -89,10 +90,13 @@ class ImageItem : public QLabel {
void clicked(int index);
};
/**
* @brief Worker class for loading images in a separate thread.
*/
class ImageLoader : public QRunnable {
public:
ImageLoader(const QString& path, ImagesCarousel* carousel);
void run() override;
void run() override; // friend to ImagesCarousel
private:
QString m_path;
@@ -126,49 +130,75 @@ class ImagesCarousel : public QWidget {
return m_loadedImages[m_currentIndex]->getFileFullPath();
}
const int m_itemWidth = 320;
const int m_itemHeight = 180;
const int m_itemFocusWidth = 480;
const int m_itemFocusHeight = 270;
const Config::SortType m_sortType = Config::SortType::None;
const bool m_sortReverse = false;
// Should always be called in the main thread
[[nodiscard]] qsizetype getLoadedImagesCount() {
return m_loadedImages.size();
}
[[nodiscard]] qsizetype getAddedImagesCount() {
QMutexLocker locker(&m_countMutex);
return m_addedImagesCount;
}
// config items
const int m_itemWidth;
const int m_itemHeight;
const int m_itemFocusWidth;
const int m_itemFocusHeight;
const Config::SortType m_sortType;
const bool m_sortReverse;
public slots:
void focusNextImage();
void focusPrevImage();
void focusCurrImage();
void unfocusCurrImage();
void onStop();
private slots:
void _unfocusCurrImage();
void _onScrollBarValueChanged(int value);
void _onItemClicked(int index);
void _onInitImagesLoaded();
public:
void
appendImages(const QStringList& paths);
void appendImages(const QStringList& paths);
private:
void _focusCurrImage();
Q_INVOKABLE void _insertImage(const ImageData* item);
private:
// UI elements
Ui::ImagesCarousel* ui;
QList<ImageItem*> m_loadedImages;
int m_currentIndex = 0;
QPropertyAnimation* m_scrollAnimation = nullptr;
QHBoxLayout* m_imagesLayout = nullptr;
QMutex m_imageCountMutex;
int m_imageCount = 0;
ImagesCarouselScrollArea* m_scrollArea = nullptr;
// Items and counters
QList<ImageItem*> m_loadedImages; // m_loadedImages.size() may != m_loadedImagesCount
int m_loadedImagesCount = 0; // increase when _insertImage is called OR ImageLoader::run() is called with m_stopSign as true
int m_addedImagesCount = 0; // increase when appendImages called
QMutex m_countMutex; // for m_loadedImagesCount and m_addedImagesCount
int m_currentIndex = 0;
// Animations
QPropertyAnimation* m_scrollAnimation = nullptr;
// Auto focusing
bool m_suppressAutoFocus = false;
int m_pendingScrollValue = 0;
QTimer* m_scrollDebounceTimer = nullptr;
ImagesCarouselScrollArea* m_scrollArea = nullptr;
// Loading stopped by user
QMutex m_stopSignMutex;
bool m_stopSign = false;
signals:
void imageFocused(const QString& path, const int index, const int count);
void loadingStarted(const qsizetype amount);
void loadingCompleted(const qsizetype amount);
void imageLoaded(const qsizetype count);
void stopped();
};
class ImagesCarouselScrollArea : public QScrollArea {
+25 -2
View File
@@ -1,7 +1,7 @@
/*
* @Author: Uyanide pywang0608@foxmail.com
* @Date: 2025-08-05 00:37:58
* @LastEditTime: 2025-08-07 22:15:47
* @LastEditTime: 2025-08-08 02:25:20
* @Description: MainWindow implementation.
*/
#include "main_window.h"
@@ -33,7 +33,6 @@ MainWindow::~MainWindow() {
}
void MainWindow::_setupUI() {
// create images carousel
m_carousel = new ImagesCarousel(
m_config.getStyleConfig(),
@@ -44,6 +43,14 @@ void MainWindow::_setupUI() {
&ImagesCarousel::imageFocused,
this,
&MainWindow::_onImageFocused);
connect(this, &MainWindow::stop, m_carousel, &ImagesCarousel::onStop);
connect(m_carousel, &ImagesCarousel::stopped, this,
// &MainWindow::close); // instead of closing, we just stop the loading
[this]() {
_onLoadingCompleted(m_carousel->getLoadedImagesCount());
m_carousel->focusCurrImage(); },
// ensure this is called in the main thread
Qt::QueuedConnection);
m_carouselIndex = ui->stackedWidget->addWidget(m_carousel);
// create loading indicator
@@ -85,6 +92,10 @@ void MainWindow::keyPressEvent(QKeyEvent *event) {
onCancel();
} else if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) {
onConfirm();
}
// if loadingScreen is enabled and loading is in progress, ignore other keys
else if (!m_config.getStyleConfig().noLoadingScreen && m_isLoading) {
event->ignore();
} else if (event->key() == Qt::Key_Space || event->key() == Qt::Key_Tab || event->key() == Qt::Key_Right) {
m_carousel->focusNextImage();
} else if (event->key() == Qt::Key_Left) {
@@ -105,6 +116,10 @@ void MainWindow::wheelEvent(QWheelEvent *event) {
}
void MainWindow::onConfirm() {
if (m_isLoading) {
warn("Loading is still in progress, please wait until it finishes.");
return;
}
close();
const auto path = m_carousel->getCurrentImagePath();
if (path.isEmpty()) {
@@ -129,7 +144,12 @@ void MainWindow::onConfirm() {
}
void MainWindow::onCancel() {
if (m_isLoading) {
warn("Loading stopped by user, waiting all threads to finish...");
emit stop();
} else {
close();
}
}
void MainWindow::_onImageFocused(const QString &path, const int index, const int count) {
@@ -137,6 +157,7 @@ void MainWindow::_onImageFocused(const QString &path, const int index, const int
}
void MainWindow::_onLoadingStarted(const qsizetype amount) {
m_isLoading = true;
if (m_config.getStyleConfig().noLoadingScreen) {
return;
}
@@ -145,5 +166,7 @@ void MainWindow::_onLoadingStarted(const qsizetype amount) {
}
void MainWindow::_onLoadingCompleted(const qsizetype amount) {
info(QString("Loading completed, loaded %1 images").arg(amount));
ui->stackedWidget->setCurrentIndex(m_carouselIndex);
m_isLoading = false;
}
+5 -1
View File
@@ -1,7 +1,7 @@
/*
* @Author: Uyanide pywang0608@foxmail.com
* @Date: 2025-08-05 00:37:58
* @LastEditTime: 2025-08-07 01:08:12
* @LastEditTime: 2025-08-08 02:11:57
* @Description: MainWindow implementation.
*/
#ifndef MAINWINDOW_H
@@ -50,5 +50,9 @@ class MainWindow : public QMainWindow {
LoadingIndicator *m_loadingIndicator = nullptr;
int m_carouselIndex, m_loadingIndicatorIndex;
const Config &m_config;
bool m_isLoading = false;
signals:
void stop();
};
#endif // MAINWINDOW_H