diff --git a/CMakeLists.txt b/CMakeLists.txt index d4969c2..eb87566 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,6 +12,10 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) +endif() + find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets) @@ -26,10 +30,12 @@ if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) qt_add_executable(wallpaper-carousel MANUAL_FINALIZATION ${PROJECT_SOURCES} + src/utils.h src/images_carousel.h src/images_carousel.cpp src/designer/images_carousel.ui src/config.h src/config.cpp src/logger.h src/logger.cpp src/loading_indicator.h src/loading_indicator.cpp src/designer/loading_indicator.ui + src/image_item.h src/image_item.cpp ) # Define target properties for Android with Qt 6 as: diff --git a/src/image_item.cpp b/src/image_item.cpp new file mode 100644 index 0000000..32c5598 --- /dev/null +++ b/src/image_item.cpp @@ -0,0 +1,83 @@ +/* + * @Author: Uyanide pywang0608@foxmail.com + * @Date: 2025-11-30 20:32:27 + * @LastEditTime: 2025-11-30 23:07:52 + * @Description: Image item widget for displaying an image. + */ +#include "image_item.h" + +#include "logger.h" + +using namespace GeneralLogger; + +ImageData* ImageData::create(const QString& p, const int initWidth, const int initHeight) { + ImageData* data = new ImageData(p); + + if (!data->image.load(p)) { + error(QString("Failed to load image from path: %1").arg(p)); + delete data; + return nullptr; + } + const QSize targetSize(initWidth, initHeight); + data->image = data->image.scaled(targetSize, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); + + // Crop to center + int x = (data->image.width() - targetSize.width()) / 2; + int y = (data->image.height() - targetSize.height()) / 2; + data->image = data->image.copy(x, y, targetSize.width(), targetSize.height()); + return data; +} + +ImageItem::ImageItem(const ImageData* data, + const int itemWidth, + const int itemHeight, + const int itemFocusWidth, + const int itemFocusHeight, + QWidget* parent) + : QLabel(parent), + m_data(data), + m_itemSize(itemWidth, itemHeight), + m_itemFocusSize(itemFocusWidth, itemFocusHeight) { + assert(data != nullptr); + setScaledContents(true); + if (data->image.isNull()) { + setText(":("); + setAlignment(Qt::AlignCenter); + } else { + setPixmap(QPixmap::fromImage(data->image)); + } + setFixedSize(itemWidth, itemHeight); +} + +ImageItem::~ImageItem() { + if (m_scaleAnimation) { + m_scaleAnimation->stop(); + delete m_scaleAnimation; + m_scaleAnimation = nullptr; + } + delete m_data; +} + +void ImageItem::setFocus(bool focus, bool animate) { + if (!animate) { + setFixedSize(focus ? m_itemFocusSize : m_itemSize); + return; + } + if (m_scaleAnimation) { + m_scaleAnimation->stop(); + delete m_scaleAnimation; + m_scaleAnimation = nullptr; + } + m_scaleAnimation = new QPropertyAnimation(this, "size"); + m_scaleAnimation->setDuration(ImageItem::s_animationDuration); + m_scaleAnimation->setStartValue(size()); + m_scaleAnimation->setEndValue(focus ? m_itemFocusSize : m_itemSize); + m_scaleAnimation->setEasingCurve(QEasingCurve::OutCubic); + connect(m_scaleAnimation, + &QPropertyAnimation::valueChanged, + this, + [this](const QVariant& value) { + setFixedSize(value.toSize()); + }); + m_scaleAnimation->start(); +} diff --git a/src/image_item.h b/src/image_item.h new file mode 100644 index 0000000..01731d1 --- /dev/null +++ b/src/image_item.h @@ -0,0 +1,78 @@ +/* + * @Author: Uyanide pywang0608@foxmail.com + * @Date: 2025-11-30 20:31:15 + * @LastEditTime: 2025-11-30 23:08:22 + * @Description: Image item widget for displaying an image. + */ +#ifndef IMAGE_ITEM_H +#define IMAGE_ITEM_H + +#include +#include +#include +#include +#include + +/** + * @brief Data structure to hold image information + * and can be safely created and passed between threads. + */ +struct ImageData { + QFileInfo file; + QImage image; + + public: + static ImageData* create(const QString& p, const int initWidth, const int initHeight); + + private: + ImageData(const QString& path) : file(path), image() {} +}; + +/** + * @brief Image label that displays an image, + * which should always be created in the main thread. + */ +class ImageItem : public QLabel { + Q_OBJECT + + public: + static constexpr int s_animationDuration = 300; + + explicit ImageItem(const ImageData* data, + const int itemWidth, + const int itemHeight, + const int itemFocusWidth, + const int itemFocusHeight, + QWidget* parent = nullptr); + + ~ImageItem() override; + + [[nodiscard]] QString getFileFullPath() const { return m_data->file.absoluteFilePath(); } + + [[nodiscard]] QString getFileName() const { return m_data->file.fileName(); } + + [[nodiscard]] QDateTime getFileDate() const { return m_data->file.lastModified(); } + + [[nodiscard]] const QImage& getThumbnail() const { return m_data->image; } + + [[nodiscard]] qint64 getFileSize() const { return m_data->file.size(); } + + void setFocus(bool focus = true, bool animate = true); + + protected: + void mousePressEvent(QMouseEvent* event) override { + emit clicked(getFileFullPath()); + QLabel::mousePressEvent(event); + } + + private: + const ImageData* m_data; + QSize m_itemSize; + QSize m_itemFocusSize; + QPropertyAnimation* m_scaleAnimation = nullptr; + + signals: + void clicked(const QString& path); +}; + +#endif // IMAGE_ITEM_H \ No newline at end of file diff --git a/src/images_carousel.cpp b/src/images_carousel.cpp index 3b51ece..54b9713 100644 --- a/src/images_carousel.cpp +++ b/src/images_carousel.cpp @@ -1,13 +1,14 @@ /* * @Author: Uyanide pywang0608@foxmail.com * @Date: 2025-08-05 01:22:53 - * @LastEditTime: 2025-08-11 19:44:06 + * @LastEditTime: 2025-11-30 23:21:30 * @Description: Animated carousel widget for displaying and selecting images. */ #include "images_carousel.h" #include #include +// #include #include #include @@ -18,6 +19,7 @@ #include "logger.h" #include "ui_images_carousel.h" +#include "utils.h" using namespace GeneralLogger; @@ -31,7 +33,8 @@ ImagesCarousel::ImagesCarousel(const Config::StyleConfigItems& styleConfig, m_itemFocusWidth(styleConfig.imageFocusWidth), m_itemFocusHeight(static_cast(styleConfig.imageFocusWidth / styleConfig.aspectRatio)), m_sortType(sortConfig.type), - m_sortReverse(sortConfig.reverse) { + m_sortReverse(sortConfig.reverse), + m_noLoadingScreen(styleConfig.noLoadingScreen) { ui->setupUi(this); m_scrollArea = dynamic_cast(ui->scrollArea); m_imagesLayout = dynamic_cast(ui->scrollAreaWidgetContents->layout()); @@ -45,6 +48,12 @@ ImagesCarousel::ImagesCarousel(const Config::StyleConfigItems& styleConfig, this, &ImagesCarousel::_onInitImagesLoaded); + // Also handle subsequent image loads + connect(this, + &ImagesCarousel::loadingCompleted, + this, + &ImagesCarousel::_onImagesLoaded); + // Auto focus when scrolling m_scrollDebounceTimer = new QTimer(this); m_scrollDebounceTimer->setSingleShot(true); @@ -69,12 +78,31 @@ ImagesCarousel::ImagesCarousel(const Config::StyleConfigItems& styleConfig, void ImagesCarousel::_onInitImagesLoaded() { disconnect(this, &ImagesCarousel::loadingCompleted, this, &ImagesCarousel::_onInitImagesLoaded); + + // No images loaded if (m_loadedImages.isEmpty()) { return; } + // Focus the first image + if (m_currentIndex < 0) { + m_currentIndex = 0; + } focusCurrImage(); } +void ImagesCarousel::_onImagesLoaded() { + m_animationEnabled = true; + if (!m_noLoadingScreen) { + _enableUIUpdates(true); + } else if (m_imageInsertQueueTimer) { + m_imageInsertQueueTimer->stop(); + m_imageInsertQueueTimer->deleteLater(); + m_imageInsertQueueTimer = nullptr; + } + + // exit(1); // for debug +} + ImagesCarousel::~ImagesCarousel() { delete ui; // memory of items in m_loadedImages managed by Qt parent-child system @@ -95,6 +123,18 @@ void ImagesCarousel::appendImages(const QStringList& paths) { QMutexLocker locker(&m_countMutex); m_addedImagesCount += paths.size(); } + m_animationEnabled = false; + if (!m_noLoadingScreen) { + _enableUIUpdates(false); + } else if (m_imageInsertQueueTimer == nullptr) { + m_imageInsertQueueTimer = new QTimer(this); + m_imageInsertQueueTimer->setInterval(s_processBatchTimeout); + connect(m_imageInsertQueueTimer, + &QTimer::timeout, + this, + &ImagesCarousel::_processImageInsertQueue); + m_imageInsertQueueTimer->start(); + } m_loadedImages.reserve(m_loadedImages.size() + paths.size()); emit loadingStarted(paths.size()); for (const QString& path : paths) { @@ -111,7 +151,38 @@ ImageLoader::ImageLoader(const QString& path, ImagesCarousel* carousel) setAutoDelete(true); } -void ImagesCarousel::_insertImage(const ImageData* data) { +void ImagesCarousel::_insertImageQueue(const ImageData* data) { + if (!m_noLoadingScreen) { + _insertImage(data); + return; + } + { + QMutexLocker locker(&m_imageInsertQueueMutex); + m_imageInsertQueue.enqueue(const_cast(data)); + } +} + +int ImagesCarousel::_insertImage(const ImageData* data) { + // Increase loaded count regardless of success or failure + Defer defer([this]() { + emit imageLoaded(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); + } + } + } + return; + }); + + if (!data) return -1; + auto item = new ImageItem( data, m_itemWidth, @@ -136,71 +207,88 @@ void ImagesCarousel::_insertImage(const ImageData* data) { }; // insert into correct position based on sort type and direction - qint64 inserPos = m_loadedImages.size(); + qint64 insertPos = m_loadedImages.size(); if (m_sortType != Config::SortType::None) { - for (auto it = m_loadedImages.rbegin(); - it != m_loadedImages.rend() && - cmpFuncs[static_cast(m_sortType)](*it, item) == m_sortReverse; - (*it)->m_index++, ++it, --inserPos); + auto cmp = cmpFuncs[static_cast(m_sortType)]; + auto reverse = m_sortReverse; + + auto it = std::upper_bound( + m_loadedImages.begin(), + m_loadedImages.end(), + item, + [cmp, reverse](const ImageItem* a, const ImageItem* b) { + return reverse ? cmp(b, a) : cmp(a, b); + }); + + insertPos = std::distance(m_loadedImages.begin(), it); } - item->m_index = inserPos; connect(item, &ImageItem::clicked, this, &ImagesCarousel::_onItemClicked); - m_loadedImages.insert(inserPos, item); - m_imagesLayout->insertWidget(inserPos, item); + m_loadedImages.insert(insertPos, item); + m_imagesLayout->insertWidget(insertPos, item); + return insertPos; +} - emit imageLoaded(m_loadedImages.size()); +void ImagesCarousel::_processImageInsertQueue() { + QVector batch; { - 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); - } + QMutexLocker locker(&m_imageInsertQueueMutex); + while (!m_imageInsertQueue.isEmpty() && batch.size() < s_processBatchSize) { + batch.append(m_imageInsertQueue.dequeue()); } } + if (m_noLoadingScreen) _enableUIUpdates(false); + int currPos = m_currentIndex; + for (ImageData* data : batch) { + int pos = _insertImage(data); + if (pos >= 0 && pos <= currPos) { + currPos++; + } + } + // Update focusing index if any + if (m_currentIndex >= 0) { + m_currentIndex = currPos; + if (m_currentIndex < 0 || m_currentIndex >= m_loadedImages.size()) { + m_currentIndex = 0; + } + } + if (m_noLoadingScreen) _enableUIUpdates(true); + focusCurrImage(); +} + +void ImagesCarousel::_enableUIUpdates(bool enable) { + m_imagesLayout->setEnabled(enable); + if (enable) { + m_imagesLayout->activate(); + } + ui->scrollAreaWidgetContents->setUpdatesEnabled(enable); } void ImageLoader::run() { + ImageData* data = nullptr; + Defer defer([this, &data]() { + QMetaObject::invokeMethod(m_carousel, + "_insertImageQueue", + Qt::QueuedConnection, + Q_ARG(const ImageData*, data)); + }); { - 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; - } + if (m_carousel->m_stopSign) return; } - auto data = new ImageData(m_path, m_initWidth, m_initHeight); - QMetaObject::invokeMethod(m_carousel, - "_insertImage", - Qt::QueuedConnection, - Q_ARG(const ImageData*, data)); -} - -ImageData::ImageData(const QString& p, const int initWidth, const int initHeight) : file(p) { - if (!image.load(p)) { - warn(QString("Failed to load image from path: %1").arg(p)); - return; - } - // resize in "cover" mode - const QSize targetSize(initWidth, initHeight); - image = image.scaled(targetSize, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); - - // Crop to center - int x = (image.width() - targetSize.width()) / 2; - int y = (image.height() - targetSize.height()) / 2; - image = image.copy(x, y, targetSize.width(), targetSize.height()); + data = ImageData::create(m_path, m_initWidth, m_initHeight); } void ImagesCarousel::focusNextImage() { + // If no focus, focus the first image + if (m_currentIndex < 0) { + if (m_loadedImages.isEmpty()) return; + m_currentIndex = 0; + focusCurrImage(); + return; + } unfocusCurrImage(); if (m_loadedImages.size() <= 1) return; m_currentIndex++; @@ -211,6 +299,13 @@ void ImagesCarousel::focusNextImage() { } void ImagesCarousel::focusPrevImage() { + // If no focus, focus the last image + if (m_currentIndex < 0) { + if (m_loadedImages.isEmpty()) return; + m_currentIndex = m_loadedImages.size() - 1; + focusCurrImage(); + return; + } if (m_loadedImages.size() <= 1) return; unfocusCurrImage(); m_currentIndex--; @@ -221,30 +316,42 @@ void ImagesCarousel::focusPrevImage() { } void ImagesCarousel::unfocusCurrImage() { - if (m_currentIndex < 0 || m_currentIndex >= m_loadedImages.size()) { - error(QString("Invalid index to unfocus: %1").arg(m_currentIndex)); + if (m_currentIndex < 0) return; + if (m_currentIndex >= m_loadedImages.size()) { + warn(QString("Invalid index to unfocus: %1").arg(m_currentIndex)); return; } - m_loadedImages[m_currentIndex]->setFocus(false); + m_loadedImages[m_currentIndex]->setFocus(false, m_animationEnabled); +} + +int ImagesCarousel::_focusingLeftOffset(int index) { + int spacing = ui->scrollAreaWidgetContents->layout()->spacing(); + int centerOffset = (m_itemWidth + spacing) * index + m_itemFocusWidth / 2 - spacing; + return centerOffset - ui->scrollArea->width() / 2; } void ImagesCarousel::focusCurrImage() { - if (m_currentIndex < 0 || m_currentIndex >= m_loadedImages.size()) { - error(QString("Invalid index to focus: %1").arg(m_currentIndex)); + // If no focus, do nothing + if (m_currentIndex < 0) return; + if (m_currentIndex >= m_loadedImages.size()) { + warn(QString("Invalid index to focus: %1").arg(m_currentIndex)); return; } - m_loadedImages[m_currentIndex]->setFocus(true); + m_loadedImages[m_currentIndex]->setFocus(true, m_animationEnabled); emit imageFocused(m_loadedImages[m_currentIndex]->getFileFullPath(), m_currentIndex, m_loadedImages.size()); - auto hScrollBar = ui->scrollArea->horizontalScrollBar(); - int spacing = ui->scrollAreaWidgetContents->layout()->spacing(); - int centerOffset = (m_itemWidth + spacing) * m_currentIndex + m_itemFocusWidth / 2 - spacing; - int leftOffset = centerOffset - ui->scrollArea->width() / 2; + auto hScrollBar = ui->scrollArea->horizontalScrollBar(); + int leftOffset = _focusingLeftOffset(m_currentIndex); if (leftOffset < 0) { leftOffset = 0; } + if (!m_animationEnabled) { + hScrollBar->setValue(leftOffset); + return; + } + if (m_scrollAnimation) { m_scrollAnimation->stop(); m_scrollAnimation->deleteLater(); @@ -292,66 +399,25 @@ void ImagesCarousel::_onScrollBarValueChanged(int value) { focusCurrImage(); } -void ImagesCarousel::_onItemClicked(int index) { +void ImagesCarousel::_onItemClicked(const QString& path) { // if (m_suppressAutoFocus) return; unfocusCurrImage(); - m_currentIndex = index; - if (index < 0 || index >= m_loadedImages.size()) { - return; // Out of bounds + // Most likely the clicked item is near the current index + for (int i = m_currentIndex, j = m_currentIndex + 1; + i >= 0 || j < m_loadedImages.size(); + --i, ++j) { + if (i >= 0 && m_loadedImages[i]->getFileFullPath() == path) { + m_currentIndex = i; + break; + } + if (j < m_loadedImages.size() && m_loadedImages[j]->getFileFullPath() == path) { + m_currentIndex = j; + break; + } } focusCurrImage(); } -ImageItem::ImageItem(const ImageData* data, - const int itemWidth, - const int itemHeight, - const int itemFocusWidth, - const int itemFocusHeight, - QWidget* parent) - : QLabel(parent), - m_data(data), - m_itemSize(itemWidth, itemHeight), - m_itemFocusSize(itemFocusWidth, itemFocusHeight) { - assert(data != nullptr); - setScaledContents(true); - if (data->image.isNull()) { - setText(":("); - setAlignment(Qt::AlignCenter); - } else { - setPixmap(QPixmap::fromImage(data->image)); - } - setFixedSize(itemWidth, itemHeight); -} - -ImageItem::~ImageItem() { - if (m_scaleAnimation) { - m_scaleAnimation->stop(); - delete m_scaleAnimation; - m_scaleAnimation = nullptr; - } - delete m_data; -} - -void ImageItem::setFocus(bool focus) { - if (m_scaleAnimation) { - m_scaleAnimation->stop(); - delete m_scaleAnimation; - m_scaleAnimation = nullptr; - } - m_scaleAnimation = new QPropertyAnimation(this, "size"); - m_scaleAnimation->setDuration(ImagesCarousel::s_animationDuration); - m_scaleAnimation->setStartValue(size()); - m_scaleAnimation->setEndValue(focus ? m_itemFocusSize : m_itemSize); - m_scaleAnimation->setEasingCurve(QEasingCurve::OutCubic); - connect(m_scaleAnimation, - &QPropertyAnimation::valueChanged, - this, - [this](const QVariant& value) { - setFixedSize(value.toSize()); - }); - m_scaleAnimation->start(); -} - void ImagesCarousel::onStop() { QMutexLocker locker(&m_stopSignMutex); m_stopSign = true; diff --git a/src/images_carousel.h b/src/images_carousel.h index 0082183..1b35dec 100644 --- a/src/images_carousel.h +++ b/src/images_carousel.h @@ -1,15 +1,15 @@ /* * @Author: Uyanide pywang0608@foxmail.com * @Date: 2025-08-05 01:22:53 - * @LastEditTime: 2025-08-08 04:17:53 + * @LastEditTime: 2025-11-30 23:10:44 * @Description: Animated carousel widget for displaying and selecting images. */ #ifndef IMAGES_CAROUSEL_H #define IMAGES_CAROUSEL_H +#include #include -#include #include #include #include @@ -25,6 +25,7 @@ #include #include "config.h" +#include "image_item.h" class ImageData; class ImageItem; @@ -32,64 +33,6 @@ class ImageLoader; class ImagesCarousel; class ImagesCarouselScrollArea; -/** - * @brief Data structure to hold image information - * and can be safely created and passed between threads. - */ -struct ImageData { - QFileInfo file; - QImage image; - - explicit ImageData(const QString& p, const int initWidth, const int initHeight); -}; - -/** - * @brief Image label that displays an image, - * which should always be created in the main thread. - */ -class ImageItem : public QLabel { - Q_OBJECT - - public: - explicit ImageItem(const ImageData* data, - const int itemWidth, - const int itemHeight, - const int itemFocusWidth, - const int itemFocusHeight, - QWidget* parent = nullptr); - - ~ImageItem() override; - - [[nodiscard]] QString getFileFullPath() const { return m_data->file.absoluteFilePath(); } - - [[nodiscard]] QString getFileName() const { return m_data->file.fileName(); } - - [[nodiscard]] QDateTime getFileDate() const { return m_data->file.lastModified(); } - - [[nodiscard]] const QImage& getThumbnail() const { return m_data->image; } - - [[nodiscard]] qint64 getFileSize() const { return m_data->file.size(); } - - void setFocus(bool focus = true); - - int m_index = 0; - - protected: - void mousePressEvent(QMouseEvent* event) override { - emit clicked(m_index); - QLabel::mousePressEvent(event); - } - - private: - const ImageData* m_data; - QSize m_itemSize; - QSize m_itemFocusSize; - QPropertyAnimation* m_scaleAnimation = nullptr; - - signals: - void clicked(int index); -}; - /** * @brief Worker class for loading images in a separate thread. */ @@ -120,8 +63,10 @@ class ImagesCarousel : public QWidget { QWidget* parent = nullptr); ~ImagesCarousel(); - static constexpr int s_debounceInterval = 200; - static constexpr int s_animationDuration = 300; + static constexpr int s_debounceInterval = 200; + static constexpr int s_animationDuration = 300; + static constexpr int s_processBatchTimeout = 50; // ms + static constexpr int s_processBatchSize = 10; // items [[nodiscard]] QString getCurrentImagePath() const { if (m_currentIndex < 0 || m_currentIndex >= m_loadedImages.size()) { @@ -147,6 +92,7 @@ class ImagesCarousel : public QWidget { const int m_itemFocusHeight; const Config::SortType m_sortType; const bool m_sortReverse; + const bool m_noLoadingScreen; public slots: void focusNextImage(); @@ -157,14 +103,21 @@ class ImagesCarousel : public QWidget { private slots: void _onScrollBarValueChanged(int value); - void _onItemClicked(int index); + void _onItemClicked(const QString& path); void _onInitImagesLoaded(); + void _onImagesLoaded(); + + void _processImageInsertQueue(); public: void appendImages(const QStringList& paths); private: - Q_INVOKABLE void _insertImage(const ImageData* item); + int _insertImage(const ImageData* item); + Q_INVOKABLE void _insertImageQueue(const ImageData* item); + + void _enableUIUpdates(bool enable); + int _focusingLeftOffset(int index); private: // UI elements @@ -177,10 +130,16 @@ class ImagesCarousel : public QWidget { 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; + int m_currentIndex = -1; // initially no focus + + // Threading + QQueue m_imageInsertQueue; + QMutex m_imageInsertQueueMutex; + QTimer* m_imageInsertQueueTimer = nullptr; // Animations QPropertyAnimation* m_scrollAnimation = nullptr; + bool m_animationEnabled = true; // Auto focusing bool m_suppressAutoFocus = false; diff --git a/src/main_window.cpp b/src/main_window.cpp index a355a66..fb4a632 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-10-25 12:04:38 + * @LastEditTime: 2025-11-30 22:37:48 * @Description: MainWindow implementation. */ #include "main_window.h" @@ -210,6 +210,15 @@ void MainWindow::wheelEvent(QWheelEvent* event) { } } +void MainWindow::closeEvent(QCloseEvent* event) { + if (m_state == Loading) { + event->ignore(); + _onCancelPressed(); + } else { + event->accept(); + } +} + void MainWindow::onConfirm() { close(); const auto path = m_carousel->getCurrentImagePath(); diff --git a/src/main_window.h b/src/main_window.h index 037c310..c7cd2bf 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-08 03:37:24 + * @LastEditTime: 2025-11-30 22:37:27 * @Description: MainWindow implementation. */ #ifndef MAINWINDOW_H @@ -25,7 +25,7 @@ class MainWindow : public QMainWindow { Q_OBJECT public: - MainWindow(const Config &config, QWidget *parent = nullptr); + MainWindow(const Config& config, QWidget* parent = nullptr); ~MainWindow(); public slots: @@ -33,14 +33,15 @@ class MainWindow : public QMainWindow { void onCancel(); protected: - void keyPressEvent(QKeyEvent *event) override; - void wheelEvent(QWheelEvent *event) override; + void keyPressEvent(QKeyEvent* event) override; + void wheelEvent(QWheelEvent* event) override; + void closeEvent(QCloseEvent* event) override; private: void _setupUI(); private slots: - void _onImageFocused(const QString &path, const int index, const int count); + void _onImageFocused(const QString& path, const int index, const int count); void _onLoadingStarted(const qsizetype amount); void _onLoadingCompleted(const qsizetype amount); @@ -55,11 +56,11 @@ class MainWindow : public QMainWindow { Ready, } m_state = Init; - Ui::MainWindow *ui; - ImagesCarousel *m_carousel = nullptr; - LoadingIndicator *m_loadingIndicator = nullptr; + Ui::MainWindow* ui; + ImagesCarousel* m_carousel = nullptr; + LoadingIndicator* m_loadingIndicator = nullptr; int m_carouselIndex, m_loadingIndicatorIndex; - const Config &m_config; + const Config& m_config; signals: void stop(); diff --git a/src/utils.h b/src/utils.h new file mode 100644 index 0000000..595010b --- /dev/null +++ b/src/utils.h @@ -0,0 +1,22 @@ +#ifndef UTILS_H +#define UTILS_H + +#include + +template +class Defer { + Callable m_func; + + public: + explicit Defer(Callable&& func) + : m_func(std::forward(func)) {} + + Defer() = delete; + Defer(const Defer&) = delete; + + ~Defer() { + m_func(); + } +}; + +#endif // UTILS_H \ No newline at end of file