/* * @Author: Uyanide pywang0608@foxmail.com * @Date: 2025-08-05 01:22:53 * @LastEditTime: 2025-08-06 22:44:49 * @Description: Animated carousel widget for displaying and selecting images. */ #include "images_carousel.h" #include #include #include #include #include #include #include #include #include #include #include "logger.h" #include "ui_images_carousel.h" using namespace GeneralLogger; ImagesCarousel::ImagesCarousel(const double itemAspectRatio, const int itemWidth, const int itemFocusWidth, const Config::SortType sortType, const bool sortReverse, QWidget* parent) : QWidget(parent), ui(new Ui::ImagesCarousel), m_updateTimer(new QTimer(this)), m_itemWidth(itemWidth), m_itemHeight(static_cast(itemWidth / itemAspectRatio)), m_itemFocusWidth(itemFocusWidth), m_itemFocusHeight(static_cast(itemFocusWidth / itemAspectRatio)), m_sortType(sortType), m_sortReverse(sortReverse) { ui->setupUi(this); m_scrollArea = dynamic_cast(ui->scrollArea); m_imagesLayout = dynamic_cast(ui->scrollAreaWidgetContents->layout()); // Load initial images connect(m_updateTimer, &QTimer::timeout, this, &ImagesCarousel::_updateImages); connect(this, &ImagesCarousel::imagesLoaded, this, [this]() { _focusCurrImage(); disconnect(this, &ImagesCarousel::imagesLoaded, this, nullptr); }); m_updateTimer->start(100); // Auto focus when scrolling m_scrollDebounceTimer = new QTimer(this); m_scrollDebounceTimer->setSingleShot(true); m_scrollDebounceTimer->setInterval(200); connect(m_scrollDebounceTimer, &QTimer::timeout, this, [this]() { _onScrollBarValueChanged(m_pendingScrollValue); }); connect(ui->scrollArea->horizontalScrollBar(), &QScrollBar::valueChanged, this, [this](int value) { m_pendingScrollValue = value; if (m_suppressAutoFocus) { return; } m_scrollDebounceTimer->start(); }); } ImagesCarousel::~ImagesCarousel() { delete ui; for (auto item : std::as_const(m_imageQueue)) { delete item; } // memory of items in m_loadedImages managed by Qt parent-child system // ... if (m_scrollAnimation) { m_scrollAnimation->stop(); m_scrollAnimation->deleteLater(); } } void ImagesCarousel::appendImages(const QStringList& paths) { { QMutexLocker locker(&m_imageCountMutex); m_imageCount += paths.size(); } for (const QString& path : paths) { ImageLoader* loader = new ImageLoader(path, this); QThreadPool::globalInstance()->start(loader); } } ImageLoader::ImageLoader(const QString& path, ImagesCarousel* carousel) : m_path(path), m_carousel(carousel), m_initWidth(carousel->m_itemFocusWidth), m_initHeight(carousel->m_itemFocusHeight) { setAutoDelete(true); } void ImagesCarousel::_addImageToQueue(const ImageData* data) { QMutexLocker locker(&m_queueMutex); auto imageItem = new ImageItem( data, m_itemWidth, m_itemHeight, m_itemFocusWidth, m_itemFocusHeight, this); m_imageQueue.enqueue(imageItem); } void ImagesCarousel::_updateImages() { QMutexLocker locker(&m_queueMutex); static const QVector> cmpFuncs = { [](auto, auto) { return false; }, // None [](auto a, auto b) { return a->getFileName() < b->getFileName(); }, [](auto a, auto b) { return a->getFileDate() < b->getFileDate(); }, [](auto a, auto b) { return a->getFileSize() < b->getFileSize(); }, }; int processCount = 0; while (!m_imageQueue.isEmpty() && processCount < 5) { ImageItem* item = m_imageQueue.dequeue(); // insert into correct position based on sort type and direction // currently O(n^2), but better as O(n * (n + log(n))) with vector and binary search qint64 inserPos = 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); } item->m_index = inserPos; connect(item, &ImageItem::clicked, this, [this](int index) { // if (m_suppressAutoFocus) return; _unfocusCurrImage(); m_currentIndex = index; _focusCurrImage(); }); m_loadedImages.insert(inserPos, item); m_imagesLayout->insertWidget(inserPos, item); processCount++; } { QMutexLocker countLocker(&m_imageCountMutex); if (m_loadedImages.size() >= m_imageCount) { emit imagesLoaded(); } } } void ImageLoader::run() { auto data = new ImageData(m_path, m_initWidth, m_initHeight); QMetaObject::invokeMethod(m_carousel, "_addImageToQueue", Qt::QueuedConnection, Q_ARG(const ImageData*, data)); } ImageData::ImageData(const QString& p, const int initWidth, const int initHeight) : file(p) { if (!pixmap.load(p)) { warn(QString("Failed to load image from path: %1").arg(p)); } // resize in "cover" mode const QSize targetSize(initWidth, initHeight); pixmap = pixmap.scaled(targetSize, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); // Crop to center int x = (pixmap.width() - targetSize.width()) / 2; int y = (pixmap.height() - targetSize.height()) / 2; pixmap = pixmap.copy(x, y, targetSize.width(), targetSize.height()); } void ImagesCarousel::focusNextImage() { _unfocusCurrImage(); if (m_loadedImages.size() <= 1) return; m_currentIndex++; if (m_currentIndex >= m_loadedImages.size()) { m_currentIndex = 0; } _focusCurrImage(); } void ImagesCarousel::focusPrevImage() { if (m_loadedImages.size() <= 1) return; _unfocusCurrImage(); m_currentIndex--; if (m_currentIndex < 0) { m_currentIndex = m_loadedImages.size() - 1; } _focusCurrImage(); } void ImagesCarousel::_unfocusCurrImage() { // bound check was (or should) done by caller m_loadedImages[m_currentIndex]->setFocus(false); } void ImagesCarousel::_focusCurrImage() { // bound check was (or should) done by caller m_loadedImages[m_currentIndex]->setFocus(true); 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; if (leftOffset < 0) { leftOffset = 0; } if (m_scrollAnimation) { m_scrollAnimation->stop(); m_scrollAnimation->deleteLater(); m_scrollAnimation = nullptr; } m_scrollAnimation = new QPropertyAnimation(hScrollBar, "value"); m_scrollAnimation->setDuration(300); m_scrollAnimation->setStartValue(hScrollBar->value()); m_scrollAnimation->setEndValue(leftOffset); m_scrollAnimation->setEasingCurve(QEasingCurve::OutCubic); // Suppress auto focus during animation connect(m_scrollAnimation, &QPropertyAnimation::finished, this, [this]() { m_suppressAutoFocus = false; m_scrollArea->setBlockInput(false); }); m_suppressAutoFocus = true; m_scrollArea->setBlockInput(true); m_scrollAnimation->start(); } void ImagesCarousel::_onScrollBarValueChanged(int value) { // Stop the animation if it is running if (m_scrollAnimation && m_scrollAnimation->state() == QPropertyAnimation::Running) { m_scrollAnimation->stop(); m_scrollAnimation->deleteLater(); m_scrollAnimation = nullptr; } int centerOffset = (value + m_itemFocusWidth / 2); int itemOffset = m_itemWidth + ui->scrollAreaWidgetContents->layout()->spacing(); int index = centerOffset / itemOffset; if (index < 0 || index >= m_loadedImages.size()) { return; // Out of bounds } if (index == m_currentIndex) { return; // Already focused } _unfocusCurrImage(); m_currentIndex = index; _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) { setScaledContents(true); setPixmap(data->pixmap); 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(); }