refactor: more comments and minor optimizations

This commit is contained in:
2026-01-15 07:28:52 +01:00
parent 063b8bc430
commit 0c218a1e3c
12 changed files with 275 additions and 83 deletions
-1
View File
@@ -44,4 +44,3 @@ jobs:
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
with: with:
files: wallpaper-carousel-linux-x64.tar.gz files: wallpaper-carousel-linux-x64.tar.gz
generate_release_notes: true
+35 -15
View File
@@ -1,7 +1,7 @@
/* /*
* @Author: Uyanide pywang0608@foxmail.com * @Author: Uyanide pywang0608@foxmail.com
* @Date: 2025-08-05 01:34:52 * @Date: 2025-08-05 01:34:52
* @LastEditTime: 2026-01-15 03:40:25 * @LastEditTime: 2026-01-15 07:18:46
* @Description: Configuration manager. * @Description: Configuration manager.
*/ */
#ifndef CONFIG_H #ifndef CONFIG_H
@@ -11,6 +11,26 @@
#include <QString> #include <QString>
#include <QStringList> #include <QStringList>
// Config entries:
//
// wallpaper.paths array image paths
// wallpaper.dirs array directories to search for images.
// all images in these directories will be added.
// NOT recursive.
// wallpaper.excludes array exclude patterns
//
// action.confirm string command to execute on confirm
//
// style.aspect_ratio number (width / height) of each image
// style.image_width number width of each image
// style.image_focus_width number width of focused image
// style.window_width number fixed window width
// style.window_height number fixed window height
// style.no_loading_screen boolean disable loading screen and load images while updating UI in batches
//
// sort.type string sorting type: "none", "name", "date", "size"
// sort.reverse boolean whether to reverse the sorting order
class Config : public QObject { class Config : public QObject {
Q_OBJECT Q_OBJECT
@@ -23,33 +43,33 @@ class Config : public QObject {
}; };
struct WallpaperConfigItems { struct WallpaperConfigItems {
QStringList paths; QStringList paths; // "wallpaper.paths"
QStringList dirs; QStringList dirs; // "wallpaper.dirs"
QStringList excludes; QStringList excludes; // "wallpaper.excludes"
}; };
struct ActionConfigItems { struct ActionConfigItems {
QString confirm; QString confirm; // "action.confirm"
}; };
struct StyleConfigItems { struct StyleConfigItems {
double aspectRatio = 1.6; double aspectRatio = 1.6; // "style.aspect_ratio"
int imageWidth = 320; int imageWidth = 320; // "style.image_width"
int imageFocusWidth = 480; int imageFocusWidth = 480; // "style.image_focus_width"
int windowWidth = 750; int windowWidth = 750; // "style.window_width"
int windowHeight = 500; int windowHeight = 500; // "style.window_height"
bool noLoadingScreen = false; bool noLoadingScreen = false; // "style.no_loading_screen"
}; };
struct SortConfigItems { struct SortConfigItems {
SortType type = SortType::Name; SortType type = SortType::Name; // "sort.type"
bool reverse = false; bool reverse = false; // "sort.reverse"
}; };
Config( Config(
const QString& configDir, const QString& configDir, // Fixed, usually "~/.config/wallpaper-carousel"
const QStringList& searchDirs = {}, const QStringList& searchDirs = {},
const QString& configPath = "", const QString& configPath = "", // Override the default config path
QObject* parent = nullptr); QObject* parent = nullptr);
~Config(); ~Config();
+4 -1
View File
@@ -1,7 +1,7 @@
/* /*
* @Author: Uyanide pywang0608@foxmail.com * @Author: Uyanide pywang0608@foxmail.com
* @Date: 2025-11-30 20:32:27 * @Date: 2025-11-30 20:32:27
* @LastEditTime: 2026-01-15 00:48:24 * @LastEditTime: 2026-01-15 07:09:29
* @Description: Image item widget for displaying an image. * @Description: Image item widget for displaying an image.
*/ */
#include "image_item.h" #include "image_item.h"
@@ -16,6 +16,7 @@ ImageData* ImageData::create(const QString& p, const int initWidth, const int in
ImageData* data = new ImageData(p); ImageData* data = new ImageData(p);
data->image = new QImage(); data->image = new QImage();
// Use QImageReader for better performance
QImageReader reader(p); QImageReader reader(p);
if (!reader.canRead()) { if (!reader.canRead()) {
warn(QString("Failed to load image from path: %1").arg(p)); warn(QString("Failed to load image from path: %1").arg(p));
@@ -26,6 +27,7 @@ ImageData* ImageData::create(const QString& p, const int initWidth, const int in
const QSize targetSize(initWidth, initHeight); const QSize targetSize(initWidth, initHeight);
const QSize originalSize = reader.size(); const QSize originalSize = reader.size();
// Scale the image to fit the target size while maintaining aspect ratio
if (originalSize.isValid()) { if (originalSize.isValid()) {
double widthRatio = (double)targetSize.width() / originalSize.width(); double widthRatio = (double)targetSize.width() / originalSize.width();
double heightRatio = (double)targetSize.height() / originalSize.height(); double heightRatio = (double)targetSize.height() / originalSize.height();
@@ -41,6 +43,7 @@ ImageData* ImageData::create(const QString& p, const int initWidth, const int in
return nullptr; return nullptr;
} }
// Crop to target size if necessary
if (data->image->size() != targetSize) { if (data->image->size() != targetSize) {
int x = (data->image->width() - targetSize.width()) / 2; int x = (data->image->width() - targetSize.width()) / 2;
int y = (data->image->height() - targetSize.height()) / 2; int y = (data->image->height() - targetSize.height()) / 2;
+11 -1
View File
@@ -1,7 +1,7 @@
/* /*
* @Author: Uyanide pywang0608@foxmail.com * @Author: Uyanide pywang0608@foxmail.com
* @Date: 2025-11-30 20:31:15 * @Date: 2025-11-30 20:31:15
* @LastEditTime: 2026-01-14 23:32:58 * @LastEditTime: 2026-01-15 07:10:21
* @Description: Image item widget for displaying an image. * @Description: Image item widget for displaying an image.
*/ */
#ifndef IMAGE_ITEM_H #ifndef IMAGE_ITEM_H
@@ -13,6 +13,9 @@
#include <QLabel> #include <QLabel>
#include <QPropertyAnimation> #include <QPropertyAnimation>
class ImageData;
class ImageItem;
/** /**
* @brief Data structure to hold image information * @brief Data structure to hold image information
* and can be safely created and passed between threads. * and can be safely created and passed between threads.
@@ -26,6 +29,7 @@ class ImageData {
~ImageData() { releaseImage(); } ~ImageData() { releaseImage(); }
// Optimization: release image data as soon as they are no longer needed
void releaseImage() { delete image, image = nullptr; } void releaseImage() { delete image, image = nullptr; }
[[nodiscard]] const QImage& getImage() const { return *image; } [[nodiscard]] const QImage& getImage() const { return *image; }
@@ -67,6 +71,12 @@ class ImageItem : public QLabel {
[[nodiscard]] qint64 getFileSize() const { return m_data->getFileInfo().size(); } [[nodiscard]] qint64 getFileSize() const { return m_data->getFileInfo().size(); }
/**
* @brief Set focus state by scaling the image label
*
* @param focus whether to focus
* @param animate whether to animate the transition
*/
void setFocus(bool focus = true, bool animate = true); void setFocus(bool focus = true, bool animate = true);
protected: protected:
+29 -27
View File
@@ -1,7 +1,7 @@
/* /*
* @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: 2026-01-15 05:11:52 * @LastEditTime: 2026-01-15 07:24:52
* @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"
@@ -50,11 +50,6 @@ ImagesCarousel::ImagesCarousel(const Config::StyleConfigItems& styleConfig,
this, this,
&ImagesCarousel::_onImagesLoaded); &ImagesCarousel::_onImagesLoaded);
connect(this,
&ImagesCarousel::stopped,
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);
@@ -78,6 +73,12 @@ ImagesCarousel::ImagesCarousel(const Config::StyleConfigItems& styleConfig,
} }
void ImagesCarousel::_onImagesLoaded() { void ImagesCarousel::_onImagesLoaded() {
// reset stop sign
{
// No need to lock m_countMutex here, but just for safety
QMutexLocker locker(&m_stopSignMutex);
m_stopSign = false;
}
m_animationEnabled = true; m_animationEnabled = true;
if (!m_noLoadingScreen) { if (!m_noLoadingScreen) {
_enableUIUpdates(true); _enableUIUpdates(true);
@@ -86,19 +87,17 @@ void ImagesCarousel::_onImagesLoaded() {
m_imageInsertQueueTimer->deleteLater(); m_imageInsertQueueTimer->deleteLater();
m_imageInsertQueueTimer = nullptr; m_imageInsertQueueTimer = nullptr;
} }
if (m_initialImagesLoaded) { // No images loaded
// No images loaded if (!getLoadedImagesCount()) {
if (!getLoadedImagesCount()) { return;
return; }
} // Focus the first image
// Focus the first image if (m_currentIndex < 0) {
if (m_currentIndex < 0) { m_currentIndex = 0;
m_currentIndex = 0; // Ensure the layout events are processed before focusing
// Ensure the layout events are processed QTimer::singleShot(0, this, [this]() {
QTimer::singleShot(0, this, [this]() { focusCurrImage();
focusCurrImage(); });
});
}
} }
// exit(1); // for debug // exit(1); // for debug
@@ -156,7 +155,7 @@ void ImagesCarousel::_insertImageQueue(ImageData* data) {
} }
{ {
QMutexLocker locker(&m_imageInsertQueueMutex); QMutexLocker locker(&m_imageInsertQueueMutex);
m_imageInsertQueue.enqueue(const_cast<ImageData*>(data)); m_imageInsertQueue.enqueue(data);
} }
} }
@@ -166,14 +165,15 @@ int ImagesCarousel::_insertImage(ImageData* data) {
emit imageLoaded(getLoadedImagesCount()); emit imageLoaded(getLoadedImagesCount());
{ {
QMutexLocker countLocker(&m_countMutex); QMutexLocker countLocker(&m_countMutex);
if (++m_loadedImagesCount >= m_addedImagesCount) { if (++m_processedImagesCount >= m_addedImagesCount) {
QMutexLocker stopSignLocker(&m_stopSignMutex); {
if (m_stopSign) { QMutexLocker stopSignLocker(&m_stopSignMutex);
// if all stopped if (m_stopSign) {
emit stopped(); // if all stopped
} else { emit stopped();
emit loadingCompleted(m_loadedImagesCount); }
} }
emit loadingCompleted(m_processedImagesCount);
} }
} }
return; return;
@@ -241,6 +241,7 @@ void ImagesCarousel::_processImageInsertQueue() {
int currPos = m_currentIndex; int currPos = m_currentIndex;
for (ImageData* data : batch) { for (ImageData* data : batch) {
int pos = _insertImage(data); int pos = _insertImage(data);
// Keep the focusing index correct
if (pos >= 0 && pos <= currPos) { if (pos >= 0 && pos <= currPos) {
currPos++; currPos++;
} }
@@ -276,6 +277,7 @@ void ImageLoader::run() {
Qt::QueuedConnection, Qt::QueuedConnection,
Q_ARG(ImageData*, data)); Q_ARG(ImageData*, data));
}); });
// We need to call _insertImageQueue even if stopped to increase the loaded count
{ {
QMutexLocker stopSignLocker(&m_carousel->m_stopSignMutex); QMutexLocker stopSignLocker(&m_carousel->m_stopSignMutex);
if (m_carousel->m_stopSign) return; if (m_carousel->m_stopSign) return;
+87 -15
View File
@@ -1,7 +1,7 @@
/* /*
* @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: 2026-01-15 03:42:17 * @LastEditTime: 2026-01-15 07:06:46
* @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
@@ -20,8 +20,57 @@
#include "config.h" #include "config.h"
#include "image_item.h" #include "image_item.h"
class ImageData; // Two different image loading strategies:
class ImageItem; // - With loading screen: load all images directly
// 1. appendImages called -> increace m_addedImagesCount & spawn all ImageLoader
// threads
// 2. Each ImageLoader calls _insertImageQueue with queued connection
// 3. _insertImageQueue calls _insertImage directly
// - Without loading screen: queue loaded images and insert them in batches
// 1. appendImages called -> increace m_addedImagesCount & spawn all ImageLoader
// threads and a timer m_imageInsertQueueTimer, disable UI updates
// 2. Each ImageLoader calls _insertImageQueue with queued connection
// 3. _insertImageQueue enqueues the ImageData
// 4. m_imageInsertQueueTimer calls _processImageInsertQueue every
// s_processBatchTimeout ms
// 5. _processImageInsertQueue processes up to s_processBatchSize items from the
// queue and calls _insertImage for each
//
// The stop logic is identical:
// - Force stop
// 1. Set m_stopSign to true
// 2. ImageLoader::run checks m_stopSign and returns early if true
// and calls _insertImageQueue using queued connection, but passing a
// nullptr as parameter
// 3. The callstack from _insertImageQueue to _insertImage is same as above
// 4. _insertImage ignores nullptr and just increases m_processedImagesCount
// 5. when m_processedImagesCount >= m_addedImagesCount, emit stopped()
// 6. Call ImagesCarousel::_onImagesLoaded
// - Normal completion
// 1. Same as above until _insertImage, but m_stopSign is false and ImageLoader::run
// passes valid ImageData pointer to _insertImageQueue
// 2. When m_processedImagesCount >= m_addedImagesCount, emit loadingCompleted()
// 3. Call ImagesCarousel::_onImagesLoaded
//
// 3 different ways to change focusing image:
// - focusNextImage / focusPrevImage: directly change m_currentIndex and call
// focusCurrImage
// These can be triggered by different events, e.g. key press, button click, etc.
// - Auto focus on scroll: debounce scroll events and calculate the nearest image
// index to focus, then change m_currentIndex and call focusCurrImage
// - Initial focus: set m_currentIndex to 0 and call focusCurrImage
//
// Note:
// - All methods and slots of ImageCarousel should be called from the main thread.
// - ImageCarousel::m_addedImagesCount and ImageCarousel::m_processedImagesCount
// should be identical after loading is finished, regardless of whether loading is
// forcedly stopped or completed normally.
// - ImageCarousel::getLoadedImagesCount() returns the number of images currently
// displayed in the carousel, which may be less than m_addedImagesCount if loading
// is not yet completed or some images failed to load.
// - The current implementation actually supports dynamic addition of images during runtime,
// but the UI does not provide such functionality yet and thus it is not tested :)
class ImageLoader; class ImageLoader;
class ImagesCarousel; class ImagesCarousel;
class ImagesCarouselScrollArea; class ImagesCarouselScrollArea;
@@ -56,11 +105,19 @@ class ImagesCarousel : public QWidget {
QWidget* parent = nullptr); QWidget* parent = nullptr);
~ImagesCarousel(); ~ImagesCarousel();
static constexpr int s_debounceInterval = 200; static constexpr int s_debounceInterval = 200; // ms
static constexpr int s_animationDuration = 300; static constexpr int s_animationDuration = 300; // ms
static constexpr int s_processBatchTimeout = 50; // ms static constexpr int s_processBatchTimeout = 50; // ms
static constexpr int s_processBatchSize = 30; // items static constexpr int s_processBatchSize = 30; // items
/**
* @brief Get the Current Image Path
*
* @return QString
*
* @note This method should be always called from the main thread.
*/
[[nodiscard]] QString getCurrentImagePath() const { [[nodiscard]] QString getCurrentImagePath() const {
if (m_currentIndex >= 0 && m_currentIndex < getLoadedImagesCount()) { if (m_currentIndex >= 0 && m_currentIndex < getLoadedImagesCount()) {
auto item = getImageItemAt(m_currentIndex); auto item = getImageItemAt(m_currentIndex);
@@ -71,11 +128,25 @@ class ImagesCarousel : public QWidget {
return ""; return "";
} }
// Should always be called in the main thread /**
* @brief Get count of loaded images
*
* @return qsizetype
*
* @note This method should be always called from the main thread.
*/
[[nodiscard]] qsizetype getLoadedImagesCount() const { [[nodiscard]] qsizetype getLoadedImagesCount() const {
return m_imagesLayout->count(); return m_imagesLayout->count();
} }
/**
* @brief Get the Image object at index
*
* @param index
* @return ImageItem*
*
* @note This method should be always called from the main thread.
*/
[[nodiscard]] ImageItem* getImageItemAt(int index) const { [[nodiscard]] ImageItem* getImageItemAt(int index) const {
if (index < 0 || index >= getLoadedImagesCount()) { if (index < 0 || index >= getLoadedImagesCount()) {
return nullptr; return nullptr;
@@ -86,6 +157,11 @@ class ImagesCarousel : public QWidget {
->widget()); ->widget());
} }
/**
* @brief Get count of added images
*
* @return qsizetype
*/
[[nodiscard]] qsizetype getAddedImagesCount() { [[nodiscard]] qsizetype getAddedImagesCount() {
QMutexLocker locker(&m_countMutex); QMutexLocker locker(&m_countMutex);
return m_addedImagesCount; return m_addedImagesCount;
@@ -110,8 +186,7 @@ class ImagesCarousel : public QWidget {
private slots: private slots:
void _onScrollBarValueChanged(int value); void _onScrollBarValueChanged(int value);
void _onItemClicked(const QString& path); void _onItemClicked(const QString& path);
void _onImagesLoaded(); void _onImagesLoaded(); // Called when loading is completed or stopped
void _processImageInsertQueue(); void _processImageInsertQueue();
public: public:
@@ -131,10 +206,10 @@ class ImagesCarousel : public QWidget {
ImagesCarouselScrollArea* m_scrollArea = nullptr; ImagesCarouselScrollArea* m_scrollArea = nullptr;
// Items and counters // Items and counters
int m_loadedImagesCount = 0; // increase when _insertImage is called OR ImageLoader::run() is called with m_stopSign as true int m_processedImagesCount = 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_processedImagesCount and m_addedImagesCount
int m_currentIndex = -1; // initially no focus int m_currentIndex = -1; // initially no focus
// Threading // Threading
QQueue<ImageData*> m_imageInsertQueue; QQueue<ImageData*> m_imageInsertQueue;
@@ -154,9 +229,6 @@ class ImagesCarousel : public QWidget {
QMutex m_stopSignMutex; QMutex m_stopSignMutex;
bool m_stopSign = false; bool m_stopSign = false;
// Flags
bool m_initialImagesLoaded = true;
signals: signals:
void imageFocused(const QString& path, const int index, const int count); void imageFocused(const QString& path, const int index, const int count);
+5 -1
View File
@@ -1,7 +1,7 @@
/* /*
* @Author: Uyanide pywang0608@foxmail.com * @Author: Uyanide pywang0608@foxmail.com
* @Date: 2025-08-07 01:12:37 * @Date: 2025-08-07 01:12:37
* @LastEditTime: 2026-01-15 04:07:40 * @LastEditTime: 2026-01-15 06:26:35
* @Description: Implementation of logger. * @Description: Implementation of logger.
*/ */
#include "logger.h" #include "logger.h"
@@ -23,6 +23,7 @@ static QTextStream* s_logStream = nullptr;
static bool s_isColored = false; static bool s_isColored = false;
static QMutex s_logMutex; static QMutex s_logMutex;
// Check if the output stream supports colored output
static bool checkIsColored(FILE* stream) { static bool checkIsColored(FILE* stream) {
if (!stream || !isatty(fileno(stream))) { if (!stream || !isatty(fileno(stream))) {
return false; return false;
@@ -35,6 +36,7 @@ static bool checkIsColored(FILE* stream) {
return true; return true;
} }
// Custom message handler
static void messageOutput(QtMsgType type, const QMessageLogContext& context, const QString& msg) { static void messageOutput(QtMsgType type, const QMessageLogContext& context, const QString& msg) {
Q_UNUSED(context); Q_UNUSED(context);
@@ -126,3 +128,5 @@ void GeneralLogger::warn(const QString& msg) {
void GeneralLogger::critical(const QString& msg) { void GeneralLogger::critical(const QString& msg) {
qCCritical(logMain).noquote() << msg; qCCritical(logMain).noquote() << msg;
} }
// No fatal because qCFatal does not exist before Qt 6.5
+15 -1
View File
@@ -1,7 +1,7 @@
/* /*
* @Author: Uyanide pywang0608@foxmail.com * @Author: Uyanide pywang0608@foxmail.com
* @Date: 2025-08-05 10:43:31 * @Date: 2025-08-05 10:43:31
* @LastEditTime: 2026-01-15 04:07:46 * @LastEditTime: 2026-01-15 06:25:57
* @Description: A simple thread-safe logger. * @Description: A simple thread-safe logger.
*/ */
#ifndef GENERAL_LOGGER_H #ifndef GENERAL_LOGGER_H
@@ -27,10 +27,24 @@ void critical(const QString& msg);
class Logger { class Logger {
public: public:
/**
* @brief Initialize the logger and set the output stream.
*
* @param stream
*/
static void init(FILE* stream = stderr); static void init(FILE* stream = stderr);
/**
* @brief Set the log level.
*
* @param level
*/
static void setLogLevel(QtMsgType level); static void setLogLevel(QtMsgType level);
/**
* @brief Suppress all log output.
*
*/
static void quiet(); static void quiet();
}; };
+18 -2
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: 2026-01-15 03:41:55 * @LastEditTime: 2026-01-15 05:58:06
* @Description: Argument parser and entry point. * @Description: Argument parser and entry point.
*/ */
#include <QApplication> #include <QApplication>
@@ -16,21 +16,32 @@
#include "utils.h" #include "utils.h"
#include "version.h" #include "version.h"
/**
* @brief A static & single-instance class to handle application options.
*
*/
static class AppOptions { static class AppOptions {
QCommandLineParser parser{}; QCommandLineParser parser{};
// The following 3 functions handle specific command line options
// and mark doReturn as true to indicate that the application should exit
// after parsing arguments.
// -v --version
void printVersion() { void printVersion() {
QTextStream out(stdout); QTextStream out(stdout);
out << APP_NAME << " version " << APP_VERSION << Qt::endl; out << APP_NAME << " version " << APP_VERSION << Qt::endl;
doReturn = true; doReturn = true;
} }
// -h --help
void printHelp() { void printHelp() {
QTextStream out(stdout); QTextStream out(stdout);
out << parser.helpText() << Qt::endl; out << parser.helpText() << Qt::endl;
doReturn = true; doReturn = true;
} }
// Print error message and help
void printError() { void printError() {
if (!errorText.isEmpty()) { if (!errorText.isEmpty()) {
QTextStream out(stderr); QTextStream out(stderr);
@@ -44,7 +55,7 @@ static class AppOptions {
QString configPath = ""; QString configPath = "";
QStringList appendDirs; QStringList appendDirs;
QString errorText = ""; QString errorText = "";
bool doReturn = false; bool doReturn = false; ///< Indicates whether the application should exit after parsing arguments.
void parseArgs(QApplication* a) { void parseArgs(QApplication* a) {
parser.setApplicationDescription("A small wallpaper utility made with Qt"); parser.setApplicationDescription("A small wallpaper utility made with Qt");
@@ -64,6 +75,9 @@ static class AppOptions {
QCommandLineOption configFileOption(QStringList() << "c" << "config-file", "Specify a custom configuration file", "file"); QCommandLineOption configFileOption(QStringList() << "c" << "config-file", "Specify a custom configuration file", "file");
parser.addOption(configFileOption); parser.addOption(configFileOption);
// Not parser.process(a->arguments()) because we want to handle exit logics ourselves.
// parser.process(...) will do something like exit(...) that will terminate
// the application brutally and produce unwanted warnings.
if (!parser.parse(a->arguments())) { if (!parser.parse(a->arguments())) {
errorText = parser.errorText(); errorText = parser.errorText();
doReturn = true; doReturn = true;
@@ -85,6 +99,7 @@ static class AppOptions {
} else if (parser.isSet(quietOption)) { } else if (parser.isSet(quietOption)) {
Logger::quiet(); Logger::quiet();
} else { } else {
// Default to INFO level
Logger::setLogLevel(QtInfoMsg); Logger::setLogLevel(QtInfoMsg);
} }
@@ -113,6 +128,7 @@ static class AppOptions {
} s_options; } s_options;
static QString getConfigDir() { static QString getConfigDir() {
// This will be ~/.config/AppName, where AppName is the name of executable target in CMakeLists.txt
auto configDir = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); auto configDir = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
if (configDir.isEmpty()) { if (configDir.isEmpty()) {
configDir = QDir::homePath() + QDir::separator() + ".config" + QDir::separator() + APP_NAME; configDir = QDir::homePath() + QDir::separator() + ".config" + QDir::separator() + APP_NAME;
+22 -17
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: 2026-01-15 05:30:06 * @LastEditTime: 2026-01-15 07:25:41
* @Description: MainWindow implementation. * @Description: MainWindow implementation.
*/ */
#include "main_window.h" #include "main_window.h"
@@ -15,14 +15,10 @@
#include "./ui_main_window.h" #include "./ui_main_window.h"
#include "images_carousel.h" #include "images_carousel.h"
#include "logger.h" #include "logger.h"
#include "utils.h"
using namespace GeneralLogger; using namespace GeneralLogger;
static QString splitNameFromPath(const QString& path) {
QFileInfo fileInfo(path);
return fileInfo.fileName();
}
MainWindow::MainWindow(const Config& config, QWidget* parent) MainWindow::MainWindow(const Config& config, QWidget* parent)
: QMainWindow(parent), ui(new Ui::MainWindow), m_config(config) { : QMainWindow(parent), ui(new Ui::MainWindow), m_config(config) {
ui->setupUi(this); ui->setupUi(this);
@@ -85,6 +81,7 @@ void MainWindow::_setupUI() {
} }
void MainWindow::keyPressEvent(QKeyEvent* event) { void MainWindow::keyPressEvent(QKeyEvent* event) {
// Same effects as clicking the confirm/cancel buttons
if (event->key() == Qt::Key_Escape) { if (event->key() == Qt::Key_Escape) {
_onCancelPressed(); _onCancelPressed();
return; return;
@@ -136,6 +133,8 @@ void MainWindow::keyPressEvent(QKeyEvent* event) {
} }
} }
// Stop loading images and call onStopped when loading is really stopped
// emit stop() -> ImagesCarousel::onStop() -> ImagesCarousel::stopped() -> call onStopped
void MainWindow::_stopLoadingAndQuit(const std::function<void()>& onStopped) { void MainWindow::_stopLoadingAndQuit(const std::function<void()>& onStopped) {
if (m_state != Loading) { if (m_state != Loading) {
return; return;
@@ -170,8 +169,7 @@ void MainWindow::_onCancelPressed() {
debug("Stopping loading and displaying loaded images..."); debug("Stopping loading and displaying loaded images...");
_stopLoadingAndQuit([this]() { _stopLoadingAndQuit([this]() {
debug("Loading stopped."); debug("Loading stopped.");
_onLoadingCompleted(m_carousel->getLoadedImagesCount()); // and do nothing
m_carousel->focusCurrImage();
}); });
} }
break; break;
@@ -190,13 +188,13 @@ void MainWindow::_onConfirmPressed() {
// case loading screen is disabled, confirm the selection // case loading screen is disabled, confirm the selection
if (m_config.getStyleConfig().noLoadingScreen) { if (m_config.getStyleConfig().noLoadingScreen) {
debug("Stopping loading and confirming selection..."); debug("Stopping loading and confirming selection...");
connect( // Save current path because the stopping process may take some time
m_carousel, const QString currentPath = m_carousel->getCurrentImagePath();
&ImagesCarousel::stopped, debug("Loading stopped. Confirming selection...");
this, _stopLoadingAndQuit([this, currentPath]() {
&MainWindow::onConfirm); close();
m_state = Stopping; _runConfirmAction(currentPath);
emit stop(); });
} }
break; break;
case Ready: case Ready:
@@ -213,6 +211,7 @@ void MainWindow::wheelEvent(QWheelEvent* event) {
event->ignore(); event->ignore();
return; return;
} }
// angleDelta().x() is handled by QScrollArea
if (event->angleDelta().y() > 0) { if (event->angleDelta().y() > 0) {
m_carousel->focusPrevImage(); m_carousel->focusPrevImage();
} else if (event->angleDelta().y() < 0) { } else if (event->angleDelta().y() < 0) {
@@ -226,7 +225,7 @@ void MainWindow::closeEvent(QCloseEvent* event) {
if (m_state == Loading) { if (m_state == Loading) {
event->ignore(); event->ignore();
_stopLoadingAndQuit([this]() { _stopLoadingAndQuit([this]() {
debug("Quitting app."); debug("Quitting app...");
close(); close();
}); });
} else { } else {
@@ -236,12 +235,16 @@ void MainWindow::closeEvent(QCloseEvent* event) {
void MainWindow::onConfirm() { void MainWindow::onConfirm() {
close(); close();
const auto path = m_carousel->getCurrentImagePath(); _runConfirmAction(m_carousel->getCurrentImagePath());
}
void MainWindow::_runConfirmAction(const QString& path) {
if (path.isEmpty()) { if (path.isEmpty()) {
warn("No image selected"); warn("No image selected");
return; return;
} }
info(QString("Selected image: %1").arg(path)); info(QString("Selected image: %1").arg(path));
// Output the selected path to stdout
QTextStream out(stdout); QTextStream out(stdout);
out << path << Qt::endl; out << path << Qt::endl;
const auto cmdOrig = m_config.getActionConfig().confirm; const auto cmdOrig = m_config.getActionConfig().confirm;
@@ -270,11 +273,13 @@ void MainWindow::_onLoadingStarted(const qsizetype amount) {
return; return;
} }
m_loadingIndicator->setMaximum(amount); m_loadingIndicator->setMaximum(amount);
// Change to loading indicator view
ui->stackedWidget->setCurrentIndex(m_loadingIndicatorIndex); ui->stackedWidget->setCurrentIndex(m_loadingIndicatorIndex);
} }
void MainWindow::_onLoadingCompleted(const qsizetype amount) { void MainWindow::_onLoadingCompleted(const qsizetype amount) {
info(QString("Loading completed, loaded %1 images").arg(amount)); info(QString("Loading completed, loaded %1 images").arg(amount));
// Change to carousel view
ui->stackedWidget->setCurrentIndex(m_carouselIndex); ui->stackedWidget->setCurrentIndex(m_carouselIndex);
m_state = Ready; m_state = Ready;
} }
+6 -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-12-01 00:37:35 * @LastEditTime: 2026-01-15 06:16:07
* @Description: MainWindow implementation. * @Description: MainWindow implementation.
*/ */
#ifndef MAINWINDOW_H #ifndef MAINWINDOW_H
@@ -39,6 +39,7 @@ class MainWindow : public QMainWindow {
private: private:
void _setupUI(); void _setupUI();
void _stopLoadingAndQuit(const std::function<void()>& onStopped = nullptr); void _stopLoadingAndQuit(const std::function<void()>& onStopped = nullptr);
void _runConfirmAction(const QString& path = "");
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);
@@ -56,6 +57,10 @@ class MainWindow : public QMainWindow {
Ready, Ready,
} m_state = Init; } m_state = Init;
// Init -> Loading -> Ready -> (Quit)
// Init -> Loading -> Stopping -> Ready -> (Quit) (with loading screen)
// Init -> Loading -> Stopping -> (Quit) (without loading screen)
Ui::MainWindow* ui; Ui::MainWindow* ui;
ImagesCarousel* m_carousel = nullptr; ImagesCarousel* m_carousel = nullptr;
LoadingIndicator* m_loadingIndicator = nullptr; LoadingIndicator* m_loadingIndicator = nullptr;
+43 -1
View File
@@ -1,7 +1,7 @@
/* /*
* @Author: Uyanide pywang0608@foxmail.com * @Author: Uyanide pywang0608@foxmail.com
* @Date: 2025-11-30 20:59:57 * @Date: 2025-11-30 20:59:57
* @LastEditTime: 2026-01-15 03:11:08 * @LastEditTime: 2026-01-15 06:00:51
* @Description: THE utils header that every project needs :) * @Description: THE utils header that every project needs :)
*/ */
@@ -15,6 +15,11 @@
#include <QStandardPaths> #include <QStandardPaths>
#include <utility> #include <utility>
/**
* @brief Defer execution of a callable until the end of the current scope.
*
* @tparam Callable
*/
template <typename Callable> template <typename Callable>
class Defer { class Defer {
Callable m_func; Callable m_func;
@@ -31,16 +36,34 @@ class Defer {
} }
}; };
/**
* @brief Check if a file exists, is a regular file, and is readable.
*
* @param path
*/
inline bool checkFile(const QString& path) { inline bool checkFile(const QString& path) {
QFileInfo checkFile(path); QFileInfo checkFile(path);
// According to Qt docs, "exists() returns true if the symlink points to an existing target, otherwise it returns false."
// So no need to separately check for isSymbolicLink() or isSymLink().
return checkFile.exists() && checkFile.isFile() && checkFile.isReadable(); return checkFile.exists() && checkFile.isFile() && checkFile.isReadable();
} }
/**
* @brief Check if a directory exists, is a directory, and is readable.
*
* @param path
*/
inline bool checkDir(const QString& path) { inline bool checkDir(const QString& path) {
QFileInfo checkFile(path); QFileInfo checkFile(path);
return checkFile.exists() && checkFile.isDir() && checkFile.isReadable(); return checkFile.exists() && checkFile.isDir() && checkFile.isReadable();
} }
/**
* @brief Expand environment variables and ~ in a given path.
*
* @param path Input path
* @return QString Expanded path
*/
inline QString expandPath(const QString& path) { inline QString expandPath(const QString& path) {
QString expandedPath = path; QString expandedPath = path;
@@ -66,6 +89,25 @@ inline QString expandPath(const QString& path) {
return QDir::cleanPath(expandedPath); return QDir::cleanPath(expandedPath);
} }
/**
* @brief Split the file name from a given path.
*
* @param path
* @return QString
*/
static QString splitNameFromPath(const QString& path) {
QFileInfo fileInfo(path);
return fileInfo.fileName();
}
/**
* @brief In addition to checking if the file exists and is readable,
* also checks if the file has a valid image extension.
*
* @param filePath
* @return true
* @return false
*/
inline bool checkImageFile(const QString& filePath) { inline bool checkImageFile(const QString& filePath) {
static const QStringList validExtensions = { static const QStringList validExtensions = {
".jpg", ".jpg",