From 0c218a1e3ce45a7538430286f52dc0897cbdffda Mon Sep 17 00:00:00 2001 From: Uyanide Date: Thu, 15 Jan 2026 07:28:52 +0100 Subject: [PATCH] refactor: more comments and minor optimizations --- .github/workflows/release.yml | 1 - src/config.h | 50 ++++++++++++----- src/image_item.cpp | 5 +- src/image_item.h | 12 +++- src/images_carousel.cpp | 56 ++++++++++--------- src/images_carousel.h | 102 +++++++++++++++++++++++++++++----- src/logger.cpp | 6 +- src/logger.h | 16 +++++- src/main.cpp | 20 ++++++- src/main_window.cpp | 39 +++++++------ src/main_window.h | 7 ++- src/utils.h | 44 ++++++++++++++- 12 files changed, 275 insertions(+), 83 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9030e81..d6314a3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,4 +44,3 @@ jobs: if: startsWith(github.ref, 'refs/tags/') with: files: wallpaper-carousel-linux-x64.tar.gz - generate_release_notes: true diff --git a/src/config.h b/src/config.h index 8d7f6ba..e28fc86 100644 --- a/src/config.h +++ b/src/config.h @@ -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 #include +// 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(); diff --git a/src/image_item.cpp b/src/image_item.cpp index b770532..fb8e147 100644 --- a/src/image_item.cpp +++ b/src/image_item.cpp @@ -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; diff --git a/src/image_item.h b/src/image_item.h index 894c293..353ba6a 100644 --- a/src/image_item.h +++ b/src/image_item.h @@ -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 #include +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: diff --git a/src/images_carousel.cpp b/src/images_carousel.cpp index a3bb9e1..51bc3f9 100644 --- a/src/images_carousel.cpp +++ b/src/images_carousel.cpp @@ -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,19 +87,17 @@ void ImagesCarousel::_onImagesLoaded() { m_imageInsertQueueTimer->deleteLater(); m_imageInsertQueueTimer = nullptr; } - if (m_initialImagesLoaded) { - // No images loaded - if (!getLoadedImagesCount()) { - return; - } - // Focus the first image - if (m_currentIndex < 0) { - m_currentIndex = 0; - // Ensure the layout events are processed - QTimer::singleShot(0, this, [this]() { - focusCurrImage(); - }); - } + // No images loaded + if (!getLoadedImagesCount()) { + return; + } + // Focus the first image + if (m_currentIndex < 0) { + m_currentIndex = 0; + // 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(data)); + m_imageInsertQueue.enqueue(data); } } @@ -166,14 +165,15 @@ int ImagesCarousel::_insertImage(ImageData* data) { emit imageLoaded(getLoadedImagesCount()); { 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); + if (++m_processedImagesCount >= m_addedImagesCount) { + { + QMutexLocker stopSignLocker(&m_stopSignMutex); + if (m_stopSign) { + // if all stopped + emit stopped(); + } } + 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; diff --git a/src/images_carousel.h b/src/images_carousel.h index d7ec102..f4b2765 100644 --- a/src/images_carousel.h +++ b/src/images_carousel.h @@ -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,10 +206,10 @@ 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_addedImagesCount = 0; // increase when appendImages called - QMutex m_countMutex; // for m_loadedImagesCount and m_addedImagesCount - int m_currentIndex = -1; // initially no focus + 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_processedImagesCount and m_addedImagesCount + int m_currentIndex = -1; // initially no focus // Threading QQueue m_imageInsertQueue; @@ -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); diff --git a/src/logger.cpp b/src/logger.cpp index bb98911..838531d 100644 --- a/src/logger.cpp +++ b/src/logger.cpp @@ -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 diff --git a/src/logger.h b/src/logger.h index 1e5b1b6..ebd72f4 100644 --- a/src/logger.h +++ b/src/logger.h @@ -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(); }; diff --git a/src/main.cpp b/src/main.cpp index 1a76fc0..a74cc87 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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 @@ -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; diff --git a/src/main_window.cpp b/src/main_window.cpp index 5b7db40..ec942db 100644 --- a/src/main_window.cpp +++ b/src/main_window.cpp @@ -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& 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; } diff --git a/src/main_window.h b/src/main_window.h index 11ace03..8154edb 100644 --- a/src/main_window.h +++ b/src/main_window.h @@ -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& 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; diff --git a/src/utils.h b/src/utils.h index 22d0f9b..dfc45be 100644 --- a/src/utils.h +++ b/src/utils.h @@ -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 #include +/** + * @brief Defer execution of a callable until the end of the current scope. + * + * @tparam Callable + */ template 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",