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