refactor: more comments and minor optimizations
This commit is contained in:
@@ -44,4 +44,3 @@ jobs:
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: wallpaper-carousel-linux-x64.tar.gz
|
||||
generate_release_notes: true
|
||||
|
||||
+35
-15
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* @Author: Uyanide pywang0608@foxmail.com
|
||||
* @Date: 2025-08-05 01:34:52
|
||||
* @LastEditTime: 2026-01-15 03:40:25
|
||||
* @LastEditTime: 2026-01-15 07:18:46
|
||||
* @Description: Configuration manager.
|
||||
*/
|
||||
#ifndef CONFIG_H
|
||||
@@ -11,6 +11,26 @@
|
||||
#include <QString>
|
||||
#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 {
|
||||
Q_OBJECT
|
||||
|
||||
@@ -23,33 +43,33 @@ class Config : public QObject {
|
||||
};
|
||||
|
||||
struct WallpaperConfigItems {
|
||||
QStringList paths;
|
||||
QStringList dirs;
|
||||
QStringList excludes;
|
||||
QStringList paths; // "wallpaper.paths"
|
||||
QStringList dirs; // "wallpaper.dirs"
|
||||
QStringList excludes; // "wallpaper.excludes"
|
||||
};
|
||||
|
||||
struct ActionConfigItems {
|
||||
QString confirm;
|
||||
QString confirm; // "action.confirm"
|
||||
};
|
||||
|
||||
struct StyleConfigItems {
|
||||
double aspectRatio = 1.6;
|
||||
int imageWidth = 320;
|
||||
int imageFocusWidth = 480;
|
||||
int windowWidth = 750;
|
||||
int windowHeight = 500;
|
||||
bool noLoadingScreen = false;
|
||||
double aspectRatio = 1.6; // "style.aspect_ratio"
|
||||
int imageWidth = 320; // "style.image_width"
|
||||
int imageFocusWidth = 480; // "style.image_focus_width"
|
||||
int windowWidth = 750; // "style.window_width"
|
||||
int windowHeight = 500; // "style.window_height"
|
||||
bool noLoadingScreen = false; // "style.no_loading_screen"
|
||||
};
|
||||
|
||||
struct SortConfigItems {
|
||||
SortType type = SortType::Name;
|
||||
bool reverse = false;
|
||||
SortType type = SortType::Name; // "sort.type"
|
||||
bool reverse = false; // "sort.reverse"
|
||||
};
|
||||
|
||||
Config(
|
||||
const QString& configDir,
|
||||
const QString& configDir, // Fixed, usually "~/.config/wallpaper-carousel"
|
||||
const QStringList& searchDirs = {},
|
||||
const QString& configPath = "",
|
||||
const QString& configPath = "", // Override the default config path
|
||||
QObject* parent = nullptr);
|
||||
|
||||
~Config();
|
||||
|
||||
+4
-1
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* @Author: Uyanide pywang0608@foxmail.com
|
||||
* @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.
|
||||
*/
|
||||
#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);
|
||||
data->image = new QImage();
|
||||
|
||||
// Use QImageReader for better performance
|
||||
QImageReader reader(p);
|
||||
if (!reader.canRead()) {
|
||||
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 originalSize = reader.size();
|
||||
|
||||
// Scale the image to fit the target size while maintaining aspect ratio
|
||||
if (originalSize.isValid()) {
|
||||
double widthRatio = (double)targetSize.width() / originalSize.width();
|
||||
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;
|
||||
}
|
||||
|
||||
// Crop to target size if necessary
|
||||
if (data->image->size() != targetSize) {
|
||||
int x = (data->image->width() - targetSize.width()) / 2;
|
||||
int y = (data->image->height() - targetSize.height()) / 2;
|
||||
|
||||
+11
-1
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* @Author: Uyanide pywang0608@foxmail.com
|
||||
* @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.
|
||||
*/
|
||||
#ifndef IMAGE_ITEM_H
|
||||
@@ -13,6 +13,9 @@
|
||||
#include <QLabel>
|
||||
#include <QPropertyAnimation>
|
||||
|
||||
class ImageData;
|
||||
class ImageItem;
|
||||
|
||||
/**
|
||||
* @brief Data structure to hold image information
|
||||
* and can be safely created and passed between threads.
|
||||
@@ -26,6 +29,7 @@ class ImageData {
|
||||
|
||||
~ImageData() { releaseImage(); }
|
||||
|
||||
// Optimization: release image data as soon as they are no longer needed
|
||||
void releaseImage() { delete image, image = nullptr; }
|
||||
|
||||
[[nodiscard]] const QImage& getImage() const { return *image; }
|
||||
@@ -67,6 +71,12 @@ class ImageItem : public QLabel {
|
||||
|
||||
[[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);
|
||||
|
||||
protected:
|
||||
|
||||
+15
-13
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* @Author: Uyanide pywang0608@foxmail.com
|
||||
* @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.
|
||||
*/
|
||||
#include "images_carousel.h"
|
||||
@@ -50,11 +50,6 @@ ImagesCarousel::ImagesCarousel(const Config::StyleConfigItems& styleConfig,
|
||||
this,
|
||||
&ImagesCarousel::_onImagesLoaded);
|
||||
|
||||
connect(this,
|
||||
&ImagesCarousel::stopped,
|
||||
this,
|
||||
&ImagesCarousel::_onImagesLoaded);
|
||||
|
||||
// Auto focus when scrolling
|
||||
m_scrollDebounceTimer = new QTimer(this);
|
||||
m_scrollDebounceTimer->setSingleShot(true);
|
||||
@@ -78,6 +73,12 @@ ImagesCarousel::ImagesCarousel(const Config::StyleConfigItems& styleConfig,
|
||||
}
|
||||
|
||||
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;
|
||||
if (!m_noLoadingScreen) {
|
||||
_enableUIUpdates(true);
|
||||
@@ -86,7 +87,6 @@ void ImagesCarousel::_onImagesLoaded() {
|
||||
m_imageInsertQueueTimer->deleteLater();
|
||||
m_imageInsertQueueTimer = nullptr;
|
||||
}
|
||||
if (m_initialImagesLoaded) {
|
||||
// No images loaded
|
||||
if (!getLoadedImagesCount()) {
|
||||
return;
|
||||
@@ -94,12 +94,11 @@ void ImagesCarousel::_onImagesLoaded() {
|
||||
// Focus the first image
|
||||
if (m_currentIndex < 0) {
|
||||
m_currentIndex = 0;
|
||||
// Ensure the layout events are processed
|
||||
// Ensure the layout events are processed before focusing
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
focusCurrImage();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// exit(1); // for debug
|
||||
}
|
||||
@@ -156,7 +155,7 @@ void ImagesCarousel::_insertImageQueue(ImageData* data) {
|
||||
}
|
||||
{
|
||||
QMutexLocker locker(&m_imageInsertQueueMutex);
|
||||
m_imageInsertQueue.enqueue(const_cast<ImageData*>(data));
|
||||
m_imageInsertQueue.enqueue(data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,15 +165,16 @@ int ImagesCarousel::_insertImage(ImageData* data) {
|
||||
emit imageLoaded(getLoadedImagesCount());
|
||||
{
|
||||
QMutexLocker countLocker(&m_countMutex);
|
||||
if (++m_loadedImagesCount >= m_addedImagesCount) {
|
||||
if (++m_processedImagesCount >= m_addedImagesCount) {
|
||||
{
|
||||
QMutexLocker stopSignLocker(&m_stopSignMutex);
|
||||
if (m_stopSign) {
|
||||
// if all stopped
|
||||
emit stopped();
|
||||
} else {
|
||||
emit loadingCompleted(m_loadedImagesCount);
|
||||
}
|
||||
}
|
||||
emit loadingCompleted(m_processedImagesCount);
|
||||
}
|
||||
}
|
||||
return;
|
||||
});
|
||||
@@ -241,6 +241,7 @@ void ImagesCarousel::_processImageInsertQueue() {
|
||||
int currPos = m_currentIndex;
|
||||
for (ImageData* data : batch) {
|
||||
int pos = _insertImage(data);
|
||||
// Keep the focusing index correct
|
||||
if (pos >= 0 && pos <= currPos) {
|
||||
currPos++;
|
||||
}
|
||||
@@ -276,6 +277,7 @@ void ImageLoader::run() {
|
||||
Qt::QueuedConnection,
|
||||
Q_ARG(ImageData*, data));
|
||||
});
|
||||
// We need to call _insertImageQueue even if stopped to increase the loaded count
|
||||
{
|
||||
QMutexLocker stopSignLocker(&m_carousel->m_stopSignMutex);
|
||||
if (m_carousel->m_stopSign) return;
|
||||
|
||||
+85
-13
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* @Author: Uyanide pywang0608@foxmail.com
|
||||
* @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.
|
||||
*/
|
||||
#ifndef IMAGES_CAROUSEL_H
|
||||
@@ -20,8 +20,57 @@
|
||||
#include "config.h"
|
||||
#include "image_item.h"
|
||||
|
||||
class ImageData;
|
||||
class ImageItem;
|
||||
// Two different image loading strategies:
|
||||
// - 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 ImagesCarousel;
|
||||
class ImagesCarouselScrollArea;
|
||||
@@ -56,11 +105,19 @@ class ImagesCarousel : public QWidget {
|
||||
QWidget* parent = nullptr);
|
||||
~ImagesCarousel();
|
||||
|
||||
static constexpr int s_debounceInterval = 200;
|
||||
static constexpr int s_animationDuration = 300;
|
||||
static constexpr int s_debounceInterval = 200; // ms
|
||||
static constexpr int s_animationDuration = 300; // ms
|
||||
|
||||
static constexpr int s_processBatchTimeout = 50; // ms
|
||||
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 {
|
||||
if (m_currentIndex >= 0 && m_currentIndex < getLoadedImagesCount()) {
|
||||
auto item = getImageItemAt(m_currentIndex);
|
||||
@@ -71,11 +128,25 @@ class ImagesCarousel : public QWidget {
|
||||
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 {
|
||||
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 {
|
||||
if (index < 0 || index >= getLoadedImagesCount()) {
|
||||
return nullptr;
|
||||
@@ -86,6 +157,11 @@ class ImagesCarousel : public QWidget {
|
||||
->widget());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get count of added images
|
||||
*
|
||||
* @return qsizetype
|
||||
*/
|
||||
[[nodiscard]] qsizetype getAddedImagesCount() {
|
||||
QMutexLocker locker(&m_countMutex);
|
||||
return m_addedImagesCount;
|
||||
@@ -110,8 +186,7 @@ class ImagesCarousel : public QWidget {
|
||||
private slots:
|
||||
void _onScrollBarValueChanged(int value);
|
||||
void _onItemClicked(const QString& path);
|
||||
void _onImagesLoaded();
|
||||
|
||||
void _onImagesLoaded(); // Called when loading is completed or stopped
|
||||
void _processImageInsertQueue();
|
||||
|
||||
public:
|
||||
@@ -131,9 +206,9 @@ class ImagesCarousel : public QWidget {
|
||||
ImagesCarouselScrollArea* m_scrollArea = nullptr;
|
||||
|
||||
// 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
|
||||
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
|
||||
|
||||
// Threading
|
||||
@@ -154,9 +229,6 @@ class ImagesCarousel : public QWidget {
|
||||
QMutex m_stopSignMutex;
|
||||
bool m_stopSign = false;
|
||||
|
||||
// Flags
|
||||
bool m_initialImagesLoaded = true;
|
||||
|
||||
signals:
|
||||
void imageFocused(const QString& path, const int index, const int count);
|
||||
|
||||
|
||||
+5
-1
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* @Author: Uyanide pywang0608@foxmail.com
|
||||
* @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.
|
||||
*/
|
||||
#include "logger.h"
|
||||
@@ -23,6 +23,7 @@ static QTextStream* s_logStream = nullptr;
|
||||
static bool s_isColored = false;
|
||||
static QMutex s_logMutex;
|
||||
|
||||
// Check if the output stream supports colored output
|
||||
static bool checkIsColored(FILE* stream) {
|
||||
if (!stream || !isatty(fileno(stream))) {
|
||||
return false;
|
||||
@@ -35,6 +36,7 @@ static bool checkIsColored(FILE* stream) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Custom message handler
|
||||
static void messageOutput(QtMsgType type, const QMessageLogContext& context, const QString& msg) {
|
||||
Q_UNUSED(context);
|
||||
|
||||
@@ -126,3 +128,5 @@ void GeneralLogger::warn(const QString& msg) {
|
||||
void GeneralLogger::critical(const QString& msg) {
|
||||
qCCritical(logMain).noquote() << msg;
|
||||
}
|
||||
|
||||
// No fatal because qCFatal does not exist before Qt 6.5
|
||||
|
||||
+15
-1
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* @Author: Uyanide pywang0608@foxmail.com
|
||||
* @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.
|
||||
*/
|
||||
#ifndef GENERAL_LOGGER_H
|
||||
@@ -27,10 +27,24 @@ void critical(const QString& msg);
|
||||
|
||||
class Logger {
|
||||
public:
|
||||
/**
|
||||
* @brief Initialize the logger and set the output stream.
|
||||
*
|
||||
* @param stream
|
||||
*/
|
||||
static void init(FILE* stream = stderr);
|
||||
|
||||
/**
|
||||
* @brief Set the log level.
|
||||
*
|
||||
* @param level
|
||||
*/
|
||||
static void setLogLevel(QtMsgType level);
|
||||
|
||||
/**
|
||||
* @brief Suppress all log output.
|
||||
*
|
||||
*/
|
||||
static void quiet();
|
||||
};
|
||||
|
||||
|
||||
+18
-2
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* @Author: Uyanide pywang0608@foxmail.com
|
||||
* @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.
|
||||
*/
|
||||
#include <QApplication>
|
||||
@@ -16,21 +16,32 @@
|
||||
#include "utils.h"
|
||||
#include "version.h"
|
||||
|
||||
/**
|
||||
* @brief A static & single-instance class to handle application options.
|
||||
*
|
||||
*/
|
||||
static class AppOptions {
|
||||
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() {
|
||||
QTextStream out(stdout);
|
||||
out << APP_NAME << " version " << APP_VERSION << Qt::endl;
|
||||
doReturn = true;
|
||||
}
|
||||
|
||||
// -h --help
|
||||
void printHelp() {
|
||||
QTextStream out(stdout);
|
||||
out << parser.helpText() << Qt::endl;
|
||||
doReturn = true;
|
||||
}
|
||||
|
||||
// Print error message and help
|
||||
void printError() {
|
||||
if (!errorText.isEmpty()) {
|
||||
QTextStream out(stderr);
|
||||
@@ -44,7 +55,7 @@ static class AppOptions {
|
||||
QString configPath = "";
|
||||
QStringList appendDirs;
|
||||
QString errorText = "";
|
||||
bool doReturn = false;
|
||||
bool doReturn = false; ///< Indicates whether the application should exit after parsing arguments.
|
||||
|
||||
void parseArgs(QApplication* a) {
|
||||
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");
|
||||
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())) {
|
||||
errorText = parser.errorText();
|
||||
doReturn = true;
|
||||
@@ -85,6 +99,7 @@ static class AppOptions {
|
||||
} else if (parser.isSet(quietOption)) {
|
||||
Logger::quiet();
|
||||
} else {
|
||||
// Default to INFO level
|
||||
Logger::setLogLevel(QtInfoMsg);
|
||||
}
|
||||
|
||||
@@ -113,6 +128,7 @@ static class AppOptions {
|
||||
} s_options;
|
||||
|
||||
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);
|
||||
if (configDir.isEmpty()) {
|
||||
configDir = QDir::homePath() + QDir::separator() + ".config" + QDir::separator() + APP_NAME;
|
||||
|
||||
+22
-17
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* @Author: Uyanide pywang0608@foxmail.com
|
||||
* @Date: 2025-08-05 00:37:58
|
||||
* @LastEditTime: 2026-01-15 05:30:06
|
||||
* @LastEditTime: 2026-01-15 07:25:41
|
||||
* @Description: MainWindow implementation.
|
||||
*/
|
||||
#include "main_window.h"
|
||||
@@ -15,14 +15,10 @@
|
||||
#include "./ui_main_window.h"
|
||||
#include "images_carousel.h"
|
||||
#include "logger.h"
|
||||
#include "utils.h"
|
||||
|
||||
using namespace GeneralLogger;
|
||||
|
||||
static QString splitNameFromPath(const QString& path) {
|
||||
QFileInfo fileInfo(path);
|
||||
return fileInfo.fileName();
|
||||
}
|
||||
|
||||
MainWindow::MainWindow(const Config& config, QWidget* parent)
|
||||
: QMainWindow(parent), ui(new Ui::MainWindow), m_config(config) {
|
||||
ui->setupUi(this);
|
||||
@@ -85,6 +81,7 @@ void MainWindow::_setupUI() {
|
||||
}
|
||||
|
||||
void MainWindow::keyPressEvent(QKeyEvent* event) {
|
||||
// Same effects as clicking the confirm/cancel buttons
|
||||
if (event->key() == Qt::Key_Escape) {
|
||||
_onCancelPressed();
|
||||
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) {
|
||||
if (m_state != Loading) {
|
||||
return;
|
||||
@@ -170,8 +169,7 @@ void MainWindow::_onCancelPressed() {
|
||||
debug("Stopping loading and displaying loaded images...");
|
||||
_stopLoadingAndQuit([this]() {
|
||||
debug("Loading stopped.");
|
||||
_onLoadingCompleted(m_carousel->getLoadedImagesCount());
|
||||
m_carousel->focusCurrImage();
|
||||
// and do nothing
|
||||
});
|
||||
}
|
||||
break;
|
||||
@@ -190,13 +188,13 @@ void MainWindow::_onConfirmPressed() {
|
||||
// case loading screen is disabled, confirm the selection
|
||||
if (m_config.getStyleConfig().noLoadingScreen) {
|
||||
debug("Stopping loading and confirming selection...");
|
||||
connect(
|
||||
m_carousel,
|
||||
&ImagesCarousel::stopped,
|
||||
this,
|
||||
&MainWindow::onConfirm);
|
||||
m_state = Stopping;
|
||||
emit stop();
|
||||
// Save current path because the stopping process may take some time
|
||||
const QString currentPath = m_carousel->getCurrentImagePath();
|
||||
debug("Loading stopped. Confirming selection...");
|
||||
_stopLoadingAndQuit([this, currentPath]() {
|
||||
close();
|
||||
_runConfirmAction(currentPath);
|
||||
});
|
||||
}
|
||||
break;
|
||||
case Ready:
|
||||
@@ -213,6 +211,7 @@ void MainWindow::wheelEvent(QWheelEvent* event) {
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
// angleDelta().x() is handled by QScrollArea
|
||||
if (event->angleDelta().y() > 0) {
|
||||
m_carousel->focusPrevImage();
|
||||
} else if (event->angleDelta().y() < 0) {
|
||||
@@ -226,7 +225,7 @@ void MainWindow::closeEvent(QCloseEvent* event) {
|
||||
if (m_state == Loading) {
|
||||
event->ignore();
|
||||
_stopLoadingAndQuit([this]() {
|
||||
debug("Quitting app.");
|
||||
debug("Quitting app...");
|
||||
close();
|
||||
});
|
||||
} else {
|
||||
@@ -236,12 +235,16 @@ void MainWindow::closeEvent(QCloseEvent* event) {
|
||||
|
||||
void MainWindow::onConfirm() {
|
||||
close();
|
||||
const auto path = m_carousel->getCurrentImagePath();
|
||||
_runConfirmAction(m_carousel->getCurrentImagePath());
|
||||
}
|
||||
|
||||
void MainWindow::_runConfirmAction(const QString& path) {
|
||||
if (path.isEmpty()) {
|
||||
warn("No image selected");
|
||||
return;
|
||||
}
|
||||
info(QString("Selected image: %1").arg(path));
|
||||
// Output the selected path to stdout
|
||||
QTextStream out(stdout);
|
||||
out << path << Qt::endl;
|
||||
const auto cmdOrig = m_config.getActionConfig().confirm;
|
||||
@@ -270,11 +273,13 @@ void MainWindow::_onLoadingStarted(const qsizetype amount) {
|
||||
return;
|
||||
}
|
||||
m_loadingIndicator->setMaximum(amount);
|
||||
// Change to loading indicator view
|
||||
ui->stackedWidget->setCurrentIndex(m_loadingIndicatorIndex);
|
||||
}
|
||||
|
||||
void MainWindow::_onLoadingCompleted(const qsizetype amount) {
|
||||
info(QString("Loading completed, loaded %1 images").arg(amount));
|
||||
// Change to carousel view
|
||||
ui->stackedWidget->setCurrentIndex(m_carouselIndex);
|
||||
m_state = Ready;
|
||||
}
|
||||
|
||||
+6
-1
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* @Author: Uyanide pywang0608@foxmail.com
|
||||
* @Date: 2025-08-05 00:37:58
|
||||
* @LastEditTime: 2025-12-01 00:37:35
|
||||
* @LastEditTime: 2026-01-15 06:16:07
|
||||
* @Description: MainWindow implementation.
|
||||
*/
|
||||
#ifndef MAINWINDOW_H
|
||||
@@ -39,6 +39,7 @@ class MainWindow : public QMainWindow {
|
||||
private:
|
||||
void _setupUI();
|
||||
void _stopLoadingAndQuit(const std::function<void()>& onStopped = nullptr);
|
||||
void _runConfirmAction(const QString& path = "");
|
||||
|
||||
private slots:
|
||||
void _onImageFocused(const QString& path, const int index, const int count);
|
||||
@@ -56,6 +57,10 @@ class MainWindow : public QMainWindow {
|
||||
Ready,
|
||||
} 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;
|
||||
ImagesCarousel* m_carousel = nullptr;
|
||||
LoadingIndicator* m_loadingIndicator = nullptr;
|
||||
|
||||
+43
-1
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* @Author: Uyanide pywang0608@foxmail.com
|
||||
* @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 :)
|
||||
*/
|
||||
|
||||
@@ -15,6 +15,11 @@
|
||||
#include <QStandardPaths>
|
||||
#include <utility>
|
||||
|
||||
/**
|
||||
* @brief Defer execution of a callable until the end of the current scope.
|
||||
*
|
||||
* @tparam Callable
|
||||
*/
|
||||
template <typename Callable>
|
||||
class Defer {
|
||||
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) {
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if a directory exists, is a directory, and is readable.
|
||||
*
|
||||
* @param path
|
||||
*/
|
||||
inline bool checkDir(const QString& path) {
|
||||
QFileInfo checkFile(path);
|
||||
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) {
|
||||
QString expandedPath = path;
|
||||
|
||||
@@ -66,6 +89,25 @@ inline QString expandPath(const QString& path) {
|
||||
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) {
|
||||
static const QStringList validExtensions = {
|
||||
".jpg",
|
||||
|
||||
Reference in New Issue
Block a user