feat: able to cancel loading at any time & add tons of multithreading bugs
This commit is contained in:
+49
-24
@@ -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;
|
||||
}
|
||||
+52
-22
@@ -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;
|
||||
bool m_suppressAutoFocus = false;
|
||||
int m_pendingScrollValue = 0;
|
||||
QTimer* m_scrollDebounceTimer = nullptr;
|
||||
QHBoxLayout* m_imagesLayout = 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:
|
||||
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 {
|
||||
|
||||
+27
-4
@@ -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() {
|
||||
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) {
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user