/* * @Author: Uyanide pywang0608@foxmail.com * @Date: 2025-08-05 01:22:53 * @LastEditTime: 2025-12-01 01:40:01 * @Description: Animated carousel widget for displaying and selecting images. */ #include "images_carousel.h" #include #include // #include #include #include #include #include #include #include #include #include "image_item.h" #include "logger.h" #include "ui_images_carousel.h" #include "utils.h" using namespace GeneralLogger; ImagesCarousel::ImagesCarousel(const Config::StyleConfigItems& styleConfig, const Config::SortConfigItems& sortConfig, QWidget* parent) : QWidget(parent), ui(new Ui::ImagesCarousel), m_itemWidth(styleConfig.imageWidth), m_itemHeight(static_cast(m_itemWidth / styleConfig.aspectRatio)), m_itemFocusWidth(styleConfig.imageFocusWidth), m_itemFocusHeight(static_cast(styleConfig.imageFocusWidth / styleConfig.aspectRatio)), m_sortType(sortConfig.type), m_sortReverse(sortConfig.reverse), m_noLoadingScreen(styleConfig.noLoadingScreen) { ui->setupUi(this); m_scrollArea = dynamic_cast(ui->scrollArea); m_imagesLayout = dynamic_cast(ui->scrollAreaWidgetContents->layout()); // Remove border ui->scrollArea->setFrameShape(QFrame::NoFrame); connect(this, &ImagesCarousel::loadingCompleted, this, &ImagesCarousel::_onImagesLoaded); connect(this, &ImagesCarousel::stopped, this, &ImagesCarousel::_onImagesLoaded); // Auto focus when scrolling m_scrollDebounceTimer = new QTimer(this); m_scrollDebounceTimer->setSingleShot(true); m_scrollDebounceTimer->setInterval(s_debounceInterval); 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(); }); } void ImagesCarousel::_onImagesLoaded() { m_animationEnabled = true; if (!m_noLoadingScreen) { _enableUIUpdates(true); } else if (m_imageInsertQueueTimer) { m_imageInsertQueueTimer->stop(); m_imageInsertQueueTimer->deleteLater(); m_imageInsertQueueTimer = nullptr; } if (m_initialImagesLoaded) { // No images loaded if (!getLoadedImagesCount()) { return; } // Focus the first image if (m_currentIndex < 0) { m_currentIndex = 0; focusCurrImage(); } } // exit(1); // for debug } ImagesCarousel::~ImagesCarousel() { delete ui; if (m_scrollAnimation) { m_scrollAnimation->stop(); m_scrollAnimation->deleteLater(); } } void ImagesCarousel::appendImages(const QStringList& paths) { if (paths.isEmpty()) { warn("No images to add to display."); emit loadingCompleted(0); return; } { 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(); } emit loadingStarted(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::_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(getLoadedImagesCount()); { 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, m_itemHeight, m_itemFocusWidth, m_itemFocusHeight, this); 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(); }, }; // insert into correct position based on sort type and direction qint64 insertPos = getLoadedImagesCount(); if (m_sortType != Config::SortType::None) { auto cmp = cmpFuncs[static_cast(m_sortType)]; auto reverse = m_sortReverse; int left = 0, right = getLoadedImagesCount(); while (left < right) { int mid = left + (right - left) / 2; if (reverse ? cmp(item, getImageItemAt(mid)) : cmp(getImageItemAt(mid), item)) { right = mid; } else { left = mid + 1; } } insertPos = left; } connect(item, &ImageItem::clicked, this, &ImagesCarousel::_onItemClicked); m_imagesLayout->insertWidget(insertPos, item); return insertPos; } void ImagesCarousel::_processImageInsertQueue() { QVector batch; { 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 >= getLoadedImagesCount()) { 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]() { if (m_carousel.isNull()) { delete data; return; } QMetaObject::invokeMethod(m_carousel, "_insertImageQueue", Qt::QueuedConnection, Q_ARG(const ImageData*, data)); }); { QMutexLocker stopSignLocker(&m_carousel->m_stopSignMutex); if (m_carousel->m_stopSign) return; } data = ImageData::create(m_path, m_initWidth, m_initHeight); } void ImagesCarousel::focusNextImage() { const auto count = getLoadedImagesCount(); // If no focus, focus the first image if (m_currentIndex < 0) { if (!count) return; m_currentIndex = 0; focusCurrImage(); return; } unfocusCurrImage(); if (count <= 1) return; m_currentIndex++; if (m_currentIndex >= count) { m_currentIndex = 0; } focusCurrImage(); } void ImagesCarousel::focusPrevImage() { const auto count = getLoadedImagesCount(); // If no focus, focus the last image if (m_currentIndex < 0) { if (!count) return; m_currentIndex = count - 1; focusCurrImage(); return; } if (count <= 1) return; unfocusCurrImage(); m_currentIndex--; if (m_currentIndex < 0) { m_currentIndex = count - 1; } focusCurrImage(); } void ImagesCarousel::unfocusCurrImage() { if (m_currentIndex < 0) return; if (m_currentIndex >= getLoadedImagesCount()) { warn(QString("Invalid index to unfocus: %1").arg(m_currentIndex)); return; } auto item = getImageItemAt(m_currentIndex); if (item) item->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 no focus, do nothing if (m_currentIndex < 0) return; if (m_currentIndex >= getLoadedImagesCount()) { warn(QString("Invalid index to focus: %1").arg(m_currentIndex)); return; } auto item = getImageItemAt(m_currentIndex); if (!item) { warn(QString("Failed to get item at index: %1").arg(m_currentIndex)); return; } item->setFocus(true, m_animationEnabled); emit imageFocused(item->getFileFullPath(), m_currentIndex, getLoadedImagesCount()); 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(); m_scrollAnimation = nullptr; } m_scrollAnimation = new QPropertyAnimation(hScrollBar, "value"); m_scrollAnimation->setDuration(s_animationDuration); 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 >= getLoadedImagesCount()) { return; // Out of bounds } if (index == m_currentIndex) { return; // Already focused } unfocusCurrImage(); m_currentIndex = index; focusCurrImage(); } void ImagesCarousel::_onItemClicked(const QString& path) { // if (m_suppressAutoFocus) return; unfocusCurrImage(); // Most likely the clicked item is near the current index const auto count = getLoadedImagesCount(); for (int i = m_currentIndex, j = m_currentIndex + 1; i >= 0 || j < count; --i, ++j) { if (i >= 0 && getImageItemAt(i)->getFileFullPath() == path) { m_currentIndex = i; break; } if (j < count && getImageItemAt(j)->getFileFullPath() == path) { m_currentIndex = j; break; } } focusCurrImage(); } void ImagesCarousel::onStop() { QMutexLocker locker(&m_stopSignMutex); m_stopSign = true; }