diff --git a/CMakeLists.txt b/CMakeLists.txt index 01d4d51..739caeb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/src/images_carousel.cpp b/src/images_carousel.cpp index 76d8b96..b0060d5 100644 --- a/src/images_carousel.cpp +++ b/src/images_carousel.cpp @@ -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 #include -#include -#include -#include -#include -#include #include #include @@ -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; +} \ No newline at end of file diff --git a/src/images_carousel.h b/src/images_carousel.h index 0102ed3..3712948 100644 --- a/src/images_carousel.h +++ b/src/images_carousel.h @@ -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 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 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 { diff --git a/src/main_window.cpp b/src/main_window.cpp index 85aa6e1..8dd52a7 100644 --- a/src/main_window.cpp +++ b/src/main_window.cpp @@ -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); -} \ No newline at end of file + m_isLoading = false; +} diff --git a/src/main_window.h b/src/main_window.h index a4af628..f8c2465 100644 --- a/src/main_window.h +++ b/src/main_window.h @@ -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