246 lines
6.3 KiB
C++
246 lines
6.3 KiB
C++
/*
|
|
* @Author: Uyanide pywang0608@foxmail.com
|
|
* @Date: 2025-08-05 01:22:53
|
|
* @LastEditTime: 2025-08-08 04:17:53
|
|
* @Description: Animated carousel widget for displaying and selecting images.
|
|
*/
|
|
#ifndef IMAGES_CAROUSEL_H
|
|
#define IMAGES_CAROUSEL_H
|
|
|
|
#include <qtmetamacros.h>
|
|
|
|
#include <QFileInfo>
|
|
#include <QHBoxLayout>
|
|
#include <QKeyEvent>
|
|
#include <QLabel>
|
|
#include <QMutex>
|
|
#include <QObject>
|
|
#include <QPixmap>
|
|
#include <QPropertyAnimation>
|
|
#include <QQueue>
|
|
#include <QRunnable>
|
|
#include <QScrollArea>
|
|
#include <QThreadPool>
|
|
#include <QTimer>
|
|
#include <QWidget>
|
|
|
|
#include "config.h"
|
|
|
|
class ImageData;
|
|
class ImageItem;
|
|
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.
|
|
*/
|
|
class ImageLoader : public QRunnable {
|
|
public:
|
|
ImageLoader(const QString& path, ImagesCarousel* carousel);
|
|
void run() override; // friend to ImagesCarousel
|
|
|
|
private:
|
|
QString m_path;
|
|
ImagesCarousel* m_carousel;
|
|
const int m_initWidth;
|
|
const int m_initHeight;
|
|
};
|
|
|
|
namespace Ui {
|
|
class ImagesCarousel;
|
|
}
|
|
|
|
class ImagesCarousel : public QWidget {
|
|
Q_OBJECT
|
|
|
|
friend void ImageLoader::run();
|
|
|
|
public:
|
|
explicit ImagesCarousel(const Config::StyleConfigItems& styleConfig,
|
|
const Config::SortConfigItems& sortConfig,
|
|
QWidget* parent = nullptr);
|
|
~ImagesCarousel();
|
|
|
|
static constexpr int s_debounceInterval = 200;
|
|
static constexpr int s_animationDuration = 300;
|
|
|
|
[[nodiscard]] QString getCurrentImagePath() const {
|
|
if (m_currentIndex < 0 || m_currentIndex >= m_loadedImages.size()) {
|
|
return "";
|
|
}
|
|
return m_loadedImages[m_currentIndex]->getFileFullPath();
|
|
}
|
|
|
|
// 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 _onScrollBarValueChanged(int value);
|
|
void _onItemClicked(int index);
|
|
void _onInitImagesLoaded();
|
|
|
|
public:
|
|
void appendImages(const QStringList& paths);
|
|
|
|
private:
|
|
Q_INVOKABLE void _insertImage(const ImageData* item);
|
|
|
|
private:
|
|
// UI elements
|
|
Ui::ImagesCarousel* ui;
|
|
QHBoxLayout* m_imagesLayout = nullptr;
|
|
ImagesCarouselScrollArea* m_scrollArea = nullptr;
|
|
|
|
// Items and counters
|
|
QVector<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 {
|
|
Q_OBJECT
|
|
|
|
public:
|
|
explicit ImagesCarouselScrollArea(QWidget* parent = nullptr)
|
|
: QScrollArea(parent) {
|
|
// setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
// setWidgetResizable(true);
|
|
}
|
|
|
|
void setBlockInput(bool block) { m_blockInput = block; }
|
|
|
|
protected:
|
|
void keyPressEvent(QKeyEvent* event) override {
|
|
if (m_blockInput) {
|
|
event->ignore();
|
|
return;
|
|
}
|
|
if (event->key() == Qt::Key_Left || event->key() == Qt::Key_Right) {
|
|
event->ignore();
|
|
} else {
|
|
QScrollArea::keyPressEvent(event);
|
|
}
|
|
}
|
|
|
|
void wheelEvent(QWheelEvent* event) override {
|
|
if (m_blockInput) {
|
|
event->ignore();
|
|
return;
|
|
}
|
|
if (event->angleDelta().y() != 0) {
|
|
event->ignore();
|
|
} else {
|
|
QScrollArea::wheelEvent(event);
|
|
}
|
|
}
|
|
|
|
private:
|
|
bool m_blockInput = false;
|
|
};
|
|
|
|
#endif // IMAGES_CAROUSEL_H
|