wip: optimize
This commit is contained in:
+179
-113
@@ -1,13 +1,14 @@
|
||||
/*
|
||||
* @Author: Uyanide pywang0608@foxmail.com
|
||||
* @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.
|
||||
*/
|
||||
#include "images_carousel.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <pthread.h>
|
||||
// #include <stdlib.h>
|
||||
|
||||
#include <QLabel>
|
||||
#include <QMetaObject>
|
||||
@@ -18,6 +19,7 @@
|
||||
|
||||
#include "logger.h"
|
||||
#include "ui_images_carousel.h"
|
||||
#include "utils.h"
|
||||
|
||||
using namespace GeneralLogger;
|
||||
|
||||
@@ -31,7 +33,8 @@ ImagesCarousel::ImagesCarousel(const Config::StyleConfigItems& styleConfig,
|
||||
m_itemFocusWidth(styleConfig.imageFocusWidth),
|
||||
m_itemFocusHeight(static_cast<int>(styleConfig.imageFocusWidth / styleConfig.aspectRatio)),
|
||||
m_sortType(sortConfig.type),
|
||||
m_sortReverse(sortConfig.reverse) {
|
||||
m_sortReverse(sortConfig.reverse),
|
||||
m_noLoadingScreen(styleConfig.noLoadingScreen) {
|
||||
ui->setupUi(this);
|
||||
m_scrollArea = dynamic_cast<ImagesCarouselScrollArea*>(ui->scrollArea);
|
||||
m_imagesLayout = dynamic_cast<QHBoxLayout*>(ui->scrollAreaWidgetContents->layout());
|
||||
@@ -45,6 +48,12 @@ ImagesCarousel::ImagesCarousel(const Config::StyleConfigItems& styleConfig,
|
||||
this,
|
||||
&ImagesCarousel::_onInitImagesLoaded);
|
||||
|
||||
// Also handle subsequent image loads
|
||||
connect(this,
|
||||
&ImagesCarousel::loadingCompleted,
|
||||
this,
|
||||
&ImagesCarousel::_onImagesLoaded);
|
||||
|
||||
// Auto focus when scrolling
|
||||
m_scrollDebounceTimer = new QTimer(this);
|
||||
m_scrollDebounceTimer->setSingleShot(true);
|
||||
@@ -69,12 +78,31 @@ ImagesCarousel::ImagesCarousel(const Config::StyleConfigItems& styleConfig,
|
||||
|
||||
void ImagesCarousel::_onInitImagesLoaded() {
|
||||
disconnect(this, &ImagesCarousel::loadingCompleted, this, &ImagesCarousel::_onInitImagesLoaded);
|
||||
|
||||
// No images loaded
|
||||
if (m_loadedImages.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// Focus the first image
|
||||
if (m_currentIndex < 0) {
|
||||
m_currentIndex = 0;
|
||||
}
|
||||
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() {
|
||||
delete ui;
|
||||
// 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);
|
||||
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());
|
||||
emit loadingStarted(paths.size());
|
||||
for (const QString& path : paths) {
|
||||
@@ -111,7 +151,38 @@ ImageLoader::ImageLoader(const QString& path, ImagesCarousel* carousel)
|
||||
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(
|
||||
data,
|
||||
m_itemWidth,
|
||||
@@ -136,71 +207,88 @@ void ImagesCarousel::_insertImage(const ImageData* data) {
|
||||
};
|
||||
|
||||
// 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) {
|
||||
for (auto it = m_loadedImages.rbegin();
|
||||
it != m_loadedImages.rend() &&
|
||||
cmpFuncs[static_cast<int>(m_sortType)](*it, item) == m_sortReverse;
|
||||
(*it)->m_index++, ++it, --inserPos);
|
||||
auto cmp = cmpFuncs[static_cast<int>(m_sortType)];
|
||||
auto reverse = m_sortReverse;
|
||||
|
||||
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,
|
||||
&ImageItem::clicked,
|
||||
this,
|
||||
&ImagesCarousel::_onItemClicked);
|
||||
m_loadedImages.insert(inserPos, item);
|
||||
m_imagesLayout->insertWidget(inserPos, item);
|
||||
m_loadedImages.insert(insertPos, item);
|
||||
m_imagesLayout->insertWidget(insertPos, item);
|
||||
return insertPos;
|
||||
}
|
||||
|
||||
emit imageLoaded(m_loadedImages.size());
|
||||
void ImagesCarousel::_processImageInsertQueue() {
|
||||
QVector<ImageData*> batch;
|
||||
{
|
||||
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);
|
||||
}
|
||||
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 >= 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() {
|
||||
ImageData* data = nullptr;
|
||||
Defer defer([this, &data]() {
|
||||
QMetaObject::invokeMethod(m_carousel,
|
||||
"_insertImageQueue",
|
||||
Qt::QueuedConnection,
|
||||
Q_ARG(const ImageData*, data));
|
||||
});
|
||||
{
|
||||
QMutexLocker countLocker(&m_carousel->m_countMutex);
|
||||
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;
|
||||
}
|
||||
if (m_carousel->m_stopSign) return;
|
||||
}
|
||||
auto data = new ImageData(m_path, m_initWidth, m_initHeight);
|
||||
QMetaObject::invokeMethod(m_carousel,
|
||||
"_insertImage",
|
||||
Qt::QueuedConnection,
|
||||
Q_ARG(const ImageData*, data));
|
||||
}
|
||||
|
||||
ImageData::ImageData(const QString& p, const int initWidth, const int initHeight) : file(p) {
|
||||
if (!image.load(p)) {
|
||||
warn(QString("Failed to load image from path: %1").arg(p));
|
||||
return;
|
||||
}
|
||||
// resize in "cover" mode
|
||||
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());
|
||||
data = ImageData::create(m_path, m_initWidth, m_initHeight);
|
||||
}
|
||||
|
||||
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();
|
||||
if (m_loadedImages.size() <= 1) return;
|
||||
m_currentIndex++;
|
||||
@@ -211,6 +299,13 @@ void ImagesCarousel::focusNextImage() {
|
||||
}
|
||||
|
||||
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;
|
||||
unfocusCurrImage();
|
||||
m_currentIndex--;
|
||||
@@ -221,30 +316,42 @@ void ImagesCarousel::focusPrevImage() {
|
||||
}
|
||||
|
||||
void ImagesCarousel::unfocusCurrImage() {
|
||||
if (m_currentIndex < 0 || m_currentIndex >= m_loadedImages.size()) {
|
||||
error(QString("Invalid index to unfocus: %1").arg(m_currentIndex));
|
||||
if (m_currentIndex < 0) return;
|
||||
if (m_currentIndex >= m_loadedImages.size()) {
|
||||
warn(QString("Invalid index to unfocus: %1").arg(m_currentIndex));
|
||||
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() {
|
||||
if (m_currentIndex < 0 || m_currentIndex >= m_loadedImages.size()) {
|
||||
error(QString("Invalid index to focus: %1").arg(m_currentIndex));
|
||||
// If no focus, do nothing
|
||||
if (m_currentIndex < 0) return;
|
||||
if (m_currentIndex >= m_loadedImages.size()) {
|
||||
warn(QString("Invalid index to focus: %1").arg(m_currentIndex));
|
||||
return;
|
||||
}
|
||||
m_loadedImages[m_currentIndex]->setFocus(true);
|
||||
m_loadedImages[m_currentIndex]->setFocus(true, m_animationEnabled);
|
||||
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;
|
||||
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();
|
||||
@@ -292,66 +399,25 @@ void ImagesCarousel::_onScrollBarValueChanged(int value) {
|
||||
focusCurrImage();
|
||||
}
|
||||
|
||||
void ImagesCarousel::_onItemClicked(int index) {
|
||||
void ImagesCarousel::_onItemClicked(const QString& path) {
|
||||
// if (m_suppressAutoFocus) return;
|
||||
unfocusCurrImage();
|
||||
m_currentIndex = index;
|
||||
if (index < 0 || index >= m_loadedImages.size()) {
|
||||
return; // Out of bounds
|
||||
// Most likely the clicked item is near the current index
|
||||
for (int i = m_currentIndex, j = m_currentIndex + 1;
|
||||
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();
|
||||
}
|
||||
|
||||
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() {
|
||||
QMutexLocker locker(&m_stopSignMutex);
|
||||
m_stopSign = true;
|
||||
|
||||
Reference in New Issue
Block a user