wip: optimize
This commit is contained in:
@@ -12,6 +12,10 @@ set(CMAKE_CXX_STANDARD 17)
|
|||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
set(CMAKE_EXPORT_COMPILE_COMMANDS 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 NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets)
|
||||||
find_package(Qt${QT_VERSION_MAJOR} 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
|
qt_add_executable(wallpaper-carousel
|
||||||
MANUAL_FINALIZATION
|
MANUAL_FINALIZATION
|
||||||
${PROJECT_SOURCES}
|
${PROJECT_SOURCES}
|
||||||
|
src/utils.h
|
||||||
src/images_carousel.h src/images_carousel.cpp src/designer/images_carousel.ui
|
src/images_carousel.h src/images_carousel.cpp src/designer/images_carousel.ui
|
||||||
src/config.h src/config.cpp
|
src/config.h src/config.cpp
|
||||||
src/logger.h src/logger.cpp
|
src/logger.h src/logger.cpp
|
||||||
src/loading_indicator.h src/loading_indicator.cpp src/designer/loading_indicator.ui
|
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:
|
# Define target properties for Android with Qt 6 as:
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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 <QDateTime>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QImage>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QPropertyAnimation>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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
|
||||||
+176
-110
@@ -1,13 +1,14 @@
|
|||||||
/*
|
/*
|
||||||
* @Author: Uyanide pywang0608@foxmail.com
|
* @Author: Uyanide pywang0608@foxmail.com
|
||||||
* @Date: 2025-08-05 01:22:53
|
* @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.
|
* @Description: Animated carousel widget for displaying and selecting images.
|
||||||
*/
|
*/
|
||||||
#include "images_carousel.h"
|
#include "images_carousel.h"
|
||||||
|
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
|
// #include <stdlib.h>
|
||||||
|
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QMetaObject>
|
#include <QMetaObject>
|
||||||
@@ -18,6 +19,7 @@
|
|||||||
|
|
||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
#include "ui_images_carousel.h"
|
#include "ui_images_carousel.h"
|
||||||
|
#include "utils.h"
|
||||||
|
|
||||||
using namespace GeneralLogger;
|
using namespace GeneralLogger;
|
||||||
|
|
||||||
@@ -31,7 +33,8 @@ ImagesCarousel::ImagesCarousel(const Config::StyleConfigItems& styleConfig,
|
|||||||
m_itemFocusWidth(styleConfig.imageFocusWidth),
|
m_itemFocusWidth(styleConfig.imageFocusWidth),
|
||||||
m_itemFocusHeight(static_cast<int>(styleConfig.imageFocusWidth / styleConfig.aspectRatio)),
|
m_itemFocusHeight(static_cast<int>(styleConfig.imageFocusWidth / styleConfig.aspectRatio)),
|
||||||
m_sortType(sortConfig.type),
|
m_sortType(sortConfig.type),
|
||||||
m_sortReverse(sortConfig.reverse) {
|
m_sortReverse(sortConfig.reverse),
|
||||||
|
m_noLoadingScreen(styleConfig.noLoadingScreen) {
|
||||||
ui->setupUi(this);
|
ui->setupUi(this);
|
||||||
m_scrollArea = dynamic_cast<ImagesCarouselScrollArea*>(ui->scrollArea);
|
m_scrollArea = dynamic_cast<ImagesCarouselScrollArea*>(ui->scrollArea);
|
||||||
m_imagesLayout = dynamic_cast<QHBoxLayout*>(ui->scrollAreaWidgetContents->layout());
|
m_imagesLayout = dynamic_cast<QHBoxLayout*>(ui->scrollAreaWidgetContents->layout());
|
||||||
@@ -45,6 +48,12 @@ ImagesCarousel::ImagesCarousel(const Config::StyleConfigItems& styleConfig,
|
|||||||
this,
|
this,
|
||||||
&ImagesCarousel::_onInitImagesLoaded);
|
&ImagesCarousel::_onInitImagesLoaded);
|
||||||
|
|
||||||
|
// Also handle subsequent image loads
|
||||||
|
connect(this,
|
||||||
|
&ImagesCarousel::loadingCompleted,
|
||||||
|
this,
|
||||||
|
&ImagesCarousel::_onImagesLoaded);
|
||||||
|
|
||||||
// Auto focus when scrolling
|
// Auto focus when scrolling
|
||||||
m_scrollDebounceTimer = new QTimer(this);
|
m_scrollDebounceTimer = new QTimer(this);
|
||||||
m_scrollDebounceTimer->setSingleShot(true);
|
m_scrollDebounceTimer->setSingleShot(true);
|
||||||
@@ -69,12 +78,31 @@ ImagesCarousel::ImagesCarousel(const Config::StyleConfigItems& styleConfig,
|
|||||||
|
|
||||||
void ImagesCarousel::_onInitImagesLoaded() {
|
void ImagesCarousel::_onInitImagesLoaded() {
|
||||||
disconnect(this, &ImagesCarousel::loadingCompleted, this, &ImagesCarousel::_onInitImagesLoaded);
|
disconnect(this, &ImagesCarousel::loadingCompleted, this, &ImagesCarousel::_onInitImagesLoaded);
|
||||||
|
|
||||||
|
// No images loaded
|
||||||
if (m_loadedImages.isEmpty()) {
|
if (m_loadedImages.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Focus the first image
|
||||||
|
if (m_currentIndex < 0) {
|
||||||
|
m_currentIndex = 0;
|
||||||
|
}
|
||||||
focusCurrImage();
|
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() {
|
ImagesCarousel::~ImagesCarousel() {
|
||||||
delete ui;
|
delete ui;
|
||||||
// memory of items in m_loadedImages managed by Qt parent-child system
|
// 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);
|
QMutexLocker locker(&m_countMutex);
|
||||||
m_addedImagesCount += paths.size();
|
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());
|
m_loadedImages.reserve(m_loadedImages.size() + paths.size());
|
||||||
emit loadingStarted(paths.size());
|
emit loadingStarted(paths.size());
|
||||||
for (const QString& path : paths) {
|
for (const QString& path : paths) {
|
||||||
@@ -111,7 +151,38 @@ ImageLoader::ImageLoader(const QString& path, ImagesCarousel* carousel)
|
|||||||
setAutoDelete(true);
|
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<ImageData*>(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(
|
auto item = new ImageItem(
|
||||||
data,
|
data,
|
||||||
m_itemWidth,
|
m_itemWidth,
|
||||||
@@ -136,71 +207,88 @@ void ImagesCarousel::_insertImage(const ImageData* data) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// insert into correct position based on sort type and direction
|
// 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) {
|
if (m_sortType != Config::SortType::None) {
|
||||||
for (auto it = m_loadedImages.rbegin();
|
auto cmp = cmpFuncs[static_cast<int>(m_sortType)];
|
||||||
it != m_loadedImages.rend() &&
|
auto reverse = m_sortReverse;
|
||||||
cmpFuncs[static_cast<int>(m_sortType)](*it, item) == m_sortReverse;
|
|
||||||
(*it)->m_index++, ++it, --inserPos);
|
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,
|
connect(item,
|
||||||
&ImageItem::clicked,
|
&ImageItem::clicked,
|
||||||
this,
|
this,
|
||||||
&ImagesCarousel::_onItemClicked);
|
&ImagesCarousel::_onItemClicked);
|
||||||
m_loadedImages.insert(inserPos, item);
|
m_loadedImages.insert(insertPos, item);
|
||||||
m_imagesLayout->insertWidget(inserPos, item);
|
m_imagesLayout->insertWidget(insertPos, item);
|
||||||
|
return insertPos;
|
||||||
|
}
|
||||||
|
|
||||||
emit imageLoaded(m_loadedImages.size());
|
void ImagesCarousel::_processImageInsertQueue() {
|
||||||
|
QVector<ImageData*> batch;
|
||||||
{
|
{
|
||||||
QMutexLocker countLocker(&m_countMutex);
|
QMutexLocker locker(&m_imageInsertQueueMutex);
|
||||||
if (++m_loadedImagesCount >= m_addedImagesCount) {
|
while (!m_imageInsertQueue.isEmpty() && batch.size() < s_processBatchSize) {
|
||||||
QMutexLocker stopSignLocker(&m_stopSignMutex);
|
batch.append(m_imageInsertQueue.dequeue());
|
||||||
if (m_stopSign) {
|
|
||||||
// if all stopped
|
|
||||||
emit stopped();
|
|
||||||
} else {
|
|
||||||
emit loadingCompleted(m_loadedImagesCount);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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() {
|
void ImageLoader::run() {
|
||||||
{
|
ImageData* data = nullptr;
|
||||||
QMutexLocker countLocker(&m_carousel->m_countMutex);
|
Defer defer([this, &data]() {
|
||||||
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,
|
QMetaObject::invokeMethod(m_carousel,
|
||||||
"_insertImage",
|
"_insertImageQueue",
|
||||||
Qt::QueuedConnection,
|
Qt::QueuedConnection,
|
||||||
Q_ARG(const ImageData*, data));
|
Q_ARG(const ImageData*, data));
|
||||||
}
|
});
|
||||||
|
{
|
||||||
ImageData::ImageData(const QString& p, const int initWidth, const int initHeight) : file(p) {
|
QMutexLocker stopSignLocker(&m_carousel->m_stopSignMutex);
|
||||||
if (!image.load(p)) {
|
if (m_carousel->m_stopSign) return;
|
||||||
warn(QString("Failed to load image from path: %1").arg(p));
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
// resize in "cover" mode
|
data = ImageData::create(m_path, m_initWidth, m_initHeight);
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ImagesCarousel::focusNextImage() {
|
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();
|
unfocusCurrImage();
|
||||||
if (m_loadedImages.size() <= 1) return;
|
if (m_loadedImages.size() <= 1) return;
|
||||||
m_currentIndex++;
|
m_currentIndex++;
|
||||||
@@ -211,6 +299,13 @@ void ImagesCarousel::focusNextImage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ImagesCarousel::focusPrevImage() {
|
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;
|
if (m_loadedImages.size() <= 1) return;
|
||||||
unfocusCurrImage();
|
unfocusCurrImage();
|
||||||
m_currentIndex--;
|
m_currentIndex--;
|
||||||
@@ -221,30 +316,42 @@ void ImagesCarousel::focusPrevImage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ImagesCarousel::unfocusCurrImage() {
|
void ImagesCarousel::unfocusCurrImage() {
|
||||||
if (m_currentIndex < 0 || m_currentIndex >= m_loadedImages.size()) {
|
if (m_currentIndex < 0) return;
|
||||||
error(QString("Invalid index to unfocus: %1").arg(m_currentIndex));
|
if (m_currentIndex >= m_loadedImages.size()) {
|
||||||
|
warn(QString("Invalid index to unfocus: %1").arg(m_currentIndex));
|
||||||
return;
|
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() {
|
void ImagesCarousel::focusCurrImage() {
|
||||||
if (m_currentIndex < 0 || m_currentIndex >= m_loadedImages.size()) {
|
// If no focus, do nothing
|
||||||
error(QString("Invalid index to focus: %1").arg(m_currentIndex));
|
if (m_currentIndex < 0) return;
|
||||||
|
if (m_currentIndex >= m_loadedImages.size()) {
|
||||||
|
warn(QString("Invalid index to focus: %1").arg(m_currentIndex));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
m_loadedImages[m_currentIndex]->setFocus(true);
|
m_loadedImages[m_currentIndex]->setFocus(true, m_animationEnabled);
|
||||||
emit imageFocused(m_loadedImages[m_currentIndex]->getFileFullPath(),
|
emit imageFocused(m_loadedImages[m_currentIndex]->getFileFullPath(),
|
||||||
m_currentIndex,
|
m_currentIndex,
|
||||||
m_loadedImages.size());
|
m_loadedImages.size());
|
||||||
auto hScrollBar = ui->scrollArea->horizontalScrollBar();
|
auto hScrollBar = ui->scrollArea->horizontalScrollBar();
|
||||||
int spacing = ui->scrollAreaWidgetContents->layout()->spacing();
|
int leftOffset = _focusingLeftOffset(m_currentIndex);
|
||||||
int centerOffset = (m_itemWidth + spacing) * m_currentIndex + m_itemFocusWidth / 2 - spacing;
|
|
||||||
int leftOffset = centerOffset - ui->scrollArea->width() / 2;
|
|
||||||
if (leftOffset < 0) {
|
if (leftOffset < 0) {
|
||||||
leftOffset = 0;
|
leftOffset = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!m_animationEnabled) {
|
||||||
|
hScrollBar->setValue(leftOffset);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (m_scrollAnimation) {
|
if (m_scrollAnimation) {
|
||||||
m_scrollAnimation->stop();
|
m_scrollAnimation->stop();
|
||||||
m_scrollAnimation->deleteLater();
|
m_scrollAnimation->deleteLater();
|
||||||
@@ -292,66 +399,25 @@ void ImagesCarousel::_onScrollBarValueChanged(int value) {
|
|||||||
focusCurrImage();
|
focusCurrImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ImagesCarousel::_onItemClicked(int index) {
|
void ImagesCarousel::_onItemClicked(const QString& path) {
|
||||||
// if (m_suppressAutoFocus) return;
|
// if (m_suppressAutoFocus) return;
|
||||||
unfocusCurrImage();
|
unfocusCurrImage();
|
||||||
m_currentIndex = index;
|
// Most likely the clicked item is near the current index
|
||||||
if (index < 0 || index >= m_loadedImages.size()) {
|
for (int i = m_currentIndex, j = m_currentIndex + 1;
|
||||||
return; // Out of bounds
|
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();
|
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() {
|
void ImagesCarousel::onStop() {
|
||||||
QMutexLocker locker(&m_stopSignMutex);
|
QMutexLocker locker(&m_stopSignMutex);
|
||||||
m_stopSign = true;
|
m_stopSign = true;
|
||||||
|
|||||||
+22
-63
@@ -1,15 +1,15 @@
|
|||||||
/*
|
/*
|
||||||
* @Author: Uyanide pywang0608@foxmail.com
|
* @Author: Uyanide pywang0608@foxmail.com
|
||||||
* @Date: 2025-08-05 01:22:53
|
* @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.
|
* @Description: Animated carousel widget for displaying and selecting images.
|
||||||
*/
|
*/
|
||||||
#ifndef IMAGES_CAROUSEL_H
|
#ifndef IMAGES_CAROUSEL_H
|
||||||
#define IMAGES_CAROUSEL_H
|
#define IMAGES_CAROUSEL_H
|
||||||
|
|
||||||
|
#include <qqueue.h>
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
#include <QFileInfo>
|
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QKeyEvent>
|
#include <QKeyEvent>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
#include "image_item.h"
|
||||||
|
|
||||||
class ImageData;
|
class ImageData;
|
||||||
class ImageItem;
|
class ImageItem;
|
||||||
@@ -32,64 +33,6 @@ class ImageLoader;
|
|||||||
class ImagesCarousel;
|
class ImagesCarousel;
|
||||||
class ImagesCarouselScrollArea;
|
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.
|
* @brief Worker class for loading images in a separate thread.
|
||||||
*/
|
*/
|
||||||
@@ -122,6 +65,8 @@ class ImagesCarousel : public QWidget {
|
|||||||
|
|
||||||
static constexpr int s_debounceInterval = 200;
|
static constexpr int s_debounceInterval = 200;
|
||||||
static constexpr int s_animationDuration = 300;
|
static constexpr int s_animationDuration = 300;
|
||||||
|
static constexpr int s_processBatchTimeout = 50; // ms
|
||||||
|
static constexpr int s_processBatchSize = 10; // items
|
||||||
|
|
||||||
[[nodiscard]] QString getCurrentImagePath() const {
|
[[nodiscard]] QString getCurrentImagePath() const {
|
||||||
if (m_currentIndex < 0 || m_currentIndex >= m_loadedImages.size()) {
|
if (m_currentIndex < 0 || m_currentIndex >= m_loadedImages.size()) {
|
||||||
@@ -147,6 +92,7 @@ class ImagesCarousel : public QWidget {
|
|||||||
const int m_itemFocusHeight;
|
const int m_itemFocusHeight;
|
||||||
const Config::SortType m_sortType;
|
const Config::SortType m_sortType;
|
||||||
const bool m_sortReverse;
|
const bool m_sortReverse;
|
||||||
|
const bool m_noLoadingScreen;
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void focusNextImage();
|
void focusNextImage();
|
||||||
@@ -157,14 +103,21 @@ class ImagesCarousel : public QWidget {
|
|||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void _onScrollBarValueChanged(int value);
|
void _onScrollBarValueChanged(int value);
|
||||||
void _onItemClicked(int index);
|
void _onItemClicked(const QString& path);
|
||||||
void _onInitImagesLoaded();
|
void _onInitImagesLoaded();
|
||||||
|
void _onImagesLoaded();
|
||||||
|
|
||||||
|
void _processImageInsertQueue();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
void appendImages(const QStringList& paths);
|
void appendImages(const QStringList& paths);
|
||||||
|
|
||||||
private:
|
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:
|
private:
|
||||||
// UI elements
|
// 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_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
|
int m_addedImagesCount = 0; // increase when appendImages called
|
||||||
QMutex m_countMutex; // for m_loadedImagesCount and m_addedImagesCount
|
QMutex m_countMutex; // for m_loadedImagesCount and m_addedImagesCount
|
||||||
int m_currentIndex = 0;
|
int m_currentIndex = -1; // initially no focus
|
||||||
|
|
||||||
|
// Threading
|
||||||
|
QQueue<ImageData*> m_imageInsertQueue;
|
||||||
|
QMutex m_imageInsertQueueMutex;
|
||||||
|
QTimer* m_imageInsertQueueTimer = nullptr;
|
||||||
|
|
||||||
// Animations
|
// Animations
|
||||||
QPropertyAnimation* m_scrollAnimation = nullptr;
|
QPropertyAnimation* m_scrollAnimation = nullptr;
|
||||||
|
bool m_animationEnabled = true;
|
||||||
|
|
||||||
// Auto focusing
|
// Auto focusing
|
||||||
bool m_suppressAutoFocus = false;
|
bool m_suppressAutoFocus = false;
|
||||||
|
|||||||
+10
-1
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* @Author: Uyanide pywang0608@foxmail.com
|
* @Author: Uyanide pywang0608@foxmail.com
|
||||||
* @Date: 2025-08-05 00:37:58
|
* @Date: 2025-08-05 00:37:58
|
||||||
* @LastEditTime: 2025-10-25 12:04:38
|
* @LastEditTime: 2025-11-30 22:37:48
|
||||||
* @Description: MainWindow implementation.
|
* @Description: MainWindow implementation.
|
||||||
*/
|
*/
|
||||||
#include "main_window.h"
|
#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() {
|
void MainWindow::onConfirm() {
|
||||||
close();
|
close();
|
||||||
const auto path = m_carousel->getCurrentImagePath();
|
const auto path = m_carousel->getCurrentImagePath();
|
||||||
|
|||||||
+10
-9
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* @Author: Uyanide pywang0608@foxmail.com
|
* @Author: Uyanide pywang0608@foxmail.com
|
||||||
* @Date: 2025-08-05 00:37:58
|
* @Date: 2025-08-05 00:37:58
|
||||||
* @LastEditTime: 2025-08-08 03:37:24
|
* @LastEditTime: 2025-11-30 22:37:27
|
||||||
* @Description: MainWindow implementation.
|
* @Description: MainWindow implementation.
|
||||||
*/
|
*/
|
||||||
#ifndef MAINWINDOW_H
|
#ifndef MAINWINDOW_H
|
||||||
@@ -25,7 +25,7 @@ class MainWindow : public QMainWindow {
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
MainWindow(const Config &config, QWidget *parent = nullptr);
|
MainWindow(const Config& config, QWidget* parent = nullptr);
|
||||||
~MainWindow();
|
~MainWindow();
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
@@ -33,14 +33,15 @@ class MainWindow : public QMainWindow {
|
|||||||
void onCancel();
|
void onCancel();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void keyPressEvent(QKeyEvent *event) override;
|
void keyPressEvent(QKeyEvent* event) override;
|
||||||
void wheelEvent(QWheelEvent *event) override;
|
void wheelEvent(QWheelEvent* event) override;
|
||||||
|
void closeEvent(QCloseEvent* event) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void _setupUI();
|
void _setupUI();
|
||||||
|
|
||||||
private slots:
|
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 _onLoadingStarted(const qsizetype amount);
|
||||||
void _onLoadingCompleted(const qsizetype amount);
|
void _onLoadingCompleted(const qsizetype amount);
|
||||||
|
|
||||||
@@ -55,11 +56,11 @@ class MainWindow : public QMainWindow {
|
|||||||
Ready,
|
Ready,
|
||||||
} m_state = Init;
|
} m_state = Init;
|
||||||
|
|
||||||
Ui::MainWindow *ui;
|
Ui::MainWindow* ui;
|
||||||
ImagesCarousel *m_carousel = nullptr;
|
ImagesCarousel* m_carousel = nullptr;
|
||||||
LoadingIndicator *m_loadingIndicator = nullptr;
|
LoadingIndicator* m_loadingIndicator = nullptr;
|
||||||
int m_carouselIndex, m_loadingIndicatorIndex;
|
int m_carouselIndex, m_loadingIndicatorIndex;
|
||||||
const Config &m_config;
|
const Config& m_config;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void stop();
|
void stop();
|
||||||
|
|||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
#ifndef UTILS_H
|
||||||
|
#define UTILS_H
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
template <typename Callable>
|
||||||
|
class Defer {
|
||||||
|
Callable m_func;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit Defer(Callable&& func)
|
||||||
|
: m_func(std::forward<Callable>(func)) {}
|
||||||
|
|
||||||
|
Defer() = delete;
|
||||||
|
Defer(const Defer&) = delete;
|
||||||
|
|
||||||
|
~Defer() {
|
||||||
|
m_func();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // UTILS_H
|
||||||
Reference in New Issue
Block a user