wip: optimize

This commit is contained in:
2025-12-01 00:31:51 +01:00
parent 149dc3ba49
commit a633a9b04c
8 changed files with 412 additions and 188 deletions
+6
View File
@@ -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:
+83
View File
@@ -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();
}
+78
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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