diff --git a/WallReel/Core/CMakeLists.txt b/WallReel/Core/CMakeLists.txt index b14ffc6..3c7a876 100644 --- a/WallReel/Core/CMakeLists.txt +++ b/WallReel/Core/CMakeLists.txt @@ -7,6 +7,8 @@ qt_add_qml_module(${CORELIB_NAME} Image/provider.hpp Image/provider.cpp Palette/data.hpp Palette/manager.hpp Palette/manager.cpp + Palette/domcolor.hpp Palette/domcolor.cpp + Palette/matchcolor.hpp Palette/matchcolor.cpp Config/data.hpp Config/manager.hpp Config/manager.cpp logger.hpp logger.cpp diff --git a/WallReel/Core/Config/data.hpp b/WallReel/Core/Config/data.hpp index 4ed4e00..bf196a4 100644 --- a/WallReel/Core/Config/data.hpp +++ b/WallReel/Core/Config/data.hpp @@ -28,8 +28,8 @@ // action.previewDebounceTime number 300 Debounce time for preview action in milliseconds // action.printSelected boolean true Whether to print the selected wallpaper path to stdout on confirm // action.printPreview boolean false Whether to print the previewed wallpaper path to stdout on preview -// action.onSelected string "" Command to execute on confirmation ({{ path }} -> full path) -// action.onPreview string "" Command to execute on preview ({{ path }} -> full path) +// action.onSelected string "" Command to execute on confirmation +// action.onPreview string "" Command to execute on preview // action.saveState array [] Useful for restore command // action.saveState[].key string "" Key of value to save, used as {{ key }} in onRestore command // action.saveState[].default string "" Value to save, used when "cmd" is not set or command execution fails or output is empty @@ -37,6 +37,7 @@ // action.saveState[].timeout number 3000 Timeout for executing "cmd" in milliseconds. 0 or negative means no timeout // action.onRestore string "" Command to execute on restore ({{ key }} -> value defined or obtained in saveState) // action.quitOnSelected boolean false Whether to quit the application after confirming a wallpaper +// action.restoreOnCancel boolean true Whether to run the restore command after cancel without confirming a wallpaper // // style.image_width number 320 Width of each image // style.image_height number 200 Height of each image @@ -103,6 +104,7 @@ struct ActionConfigItems { bool printSelected = true; bool printPreview = false; bool quitOnSelected = false; + bool restoreOnCancel = true; }; struct StyleConfigItems { diff --git a/WallReel/Core/Config/manager.cpp b/WallReel/Core/Config/manager.cpp index 43197d2..23acb4f 100644 --- a/WallReel/Core/Config/manager.cpp +++ b/WallReel/Core/Config/manager.cpp @@ -230,6 +230,12 @@ void WallReel::Core::Config::Manager::_loadActionConfig(const QJsonObject& root) m_actionConfig.quitOnSelected = val.toBool(); } } + if (config.contains("restoreOnCancel")) { + const auto& val = config["restoreOnCancel"]; + if (val.isBool()) { + m_actionConfig.restoreOnCancel = val.toBool(); + } + } } void WallReel::Core::Config::Manager::_loadStyleConfig(const QJsonObject& root) { @@ -405,6 +411,7 @@ void WallReel::Core::Config::Manager::captureState() { process->disconnect(); QString result = success ? output : defaultVal; + Logger::debug(QString("Capture result for key '%1': %2 (success: %3)").arg(key).arg(result).arg(success)); if (result.isEmpty()) result = defaultVal; _onCaptureResult(key, result); @@ -444,7 +451,7 @@ void WallReel::Core::Config::Manager::captureState() { if (timer) { timer->start(); } - process->startCommand(item.cmd); + process->start("sh", QStringList() << "-c" << item.cmd); } } diff --git a/WallReel/Core/Image/data.cpp b/WallReel/Core/Image/data.cpp index 3f3f325..37940c8 100644 --- a/WallReel/Core/Image/data.cpp +++ b/WallReel/Core/Image/data.cpp @@ -2,7 +2,8 @@ #include -#include "../logger.hpp" +#include "Palette/domcolor.hpp" +#include "logger.hpp" WallReel::Core::Image::Data* WallReel::Core::Image::Data::create(const QString& path, const QSize& size) { Data* ret = new Data(path, size); @@ -59,4 +60,7 @@ WallReel::Core::Image::Data::Data(const QString& path, const QSize& targetSize) // Create ID m_id = QString::number(qHash(m_file.absoluteFilePath())); + + // Get dominant color + m_dominantColor = Palette::getDominantColor(m_image); } diff --git a/WallReel/Core/Image/data.hpp b/WallReel/Core/Image/data.hpp index 9e90a89..7d72bf6 100644 --- a/WallReel/Core/Image/data.hpp +++ b/WallReel/Core/Image/data.hpp @@ -10,6 +10,8 @@ class Data { QString m_id; QFileInfo m_file; QImage m_image; + QColor m_dominantColor; + QHash m_colorCache; Data(const QString& path, const QSize& size); @@ -32,7 +34,18 @@ class Data { const QFileInfo& getFileInfo() const { return m_file; } - private: + const QColor& getDominantColor() const { return m_dominantColor; } + + std::optional getCachedColor(const QString& paletteName) const { + if (m_colorCache.contains(paletteName)) { + return m_colorCache.value(paletteName); + } + return std::nullopt; + } + + void cacheColor(const QString& paletteName, const QString& colorName) { + m_colorCache.insert(paletteName, colorName); + } }; } // namespace WallReel::Core::Image diff --git a/WallReel/Core/Image/model.cpp b/WallReel/Core/Image/model.cpp index cff8931..2d51e10 100644 --- a/WallReel/Core/Image/model.cpp +++ b/WallReel/Core/Image/model.cpp @@ -29,14 +29,13 @@ WallReel::Core::Image::Model::Model( emit progressChanged(); }); - // Update state when sort method changes + // Pipeline: sort -> filter -> update properties connect(this, &Model::currentSortTypeChanged, this, &Model::_onSortMethodChanged); connect(this, &Model::currentSortReverseChanged, this, &Model::_onSortMethodChanged); + connect(this, &Model::searchTextChanged, this, &Model::_onSearchTextChanged); + connect(this, &Model::focusedImageChanged, this, &Model::_updateFocusedProperties); m_sortIndices.resize(4); // None, Name, Date, Size - - // Search text - connect(this, &Model::searchTextChanged, this, &Model::_onSearchTextChanged); } WallReel::Core::Image::Model::~Model() { @@ -58,7 +57,7 @@ QVariant WallReel::Core::Image::Model::data(const QModelIndex& index, int role) return QVariant(); } - int actualIndex = fromProxyIndex(index.row()); + int actualIndex = _convertProxyIndex(index.row()); if (actualIndex < 0 || actualIndex >= m_data.count()) { Logger::debug("Actual index out of bounds: " + QString::number(actualIndex)); return QVariant(); @@ -126,12 +125,12 @@ void WallReel::Core::Image::Model::setSearchText(const QString& text) { } } -const WallReel::Core::Image::Data* WallReel::Core::Image::Model::getDataPtrAt(int index) const { +WallReel::Core::Image::Data* WallReel::Core::Image::Model::imageAt(int index) { if (index < 0 || index >= m_filteredIndices.count()) { Logger::debug("Invalid index requested: " + QString::number(index)); return nullptr; } - int actualIndex = fromProxyIndex(index); + int actualIndex = _convertProxyIndex(index); if (actualIndex < 0 || actualIndex >= m_data.count()) { Logger::debug("Actual index out of bounds: " + QString::number(actualIndex)); return nullptr; @@ -139,13 +138,17 @@ const WallReel::Core::Image::Data* WallReel::Core::Image::Model::getDataPtrAt(in return m_data[actualIndex]; } +WallReel::Core::Image::Data* WallReel::Core::Image::Model::focusedImage() { + return imageAt(m_focusedIndex); +} + QVariant WallReel::Core::Image::Model::dataAt(int index, const QString& roleName) const { if (index < 0 || index >= m_filteredIndices.count()) { Logger::debug("Invalid index requested: " + QString::number(index)); return QVariant(); } - int actualIndex = fromProxyIndex(index); + int actualIndex = _convertProxyIndex(index); if (actualIndex < 0 || actualIndex >= m_data.count()) { Logger::debug("Actual index out of bounds: " + QString::number(actualIndex)); return QVariant(); @@ -185,26 +188,21 @@ void WallReel::Core::Image::Model::loadAndProcess(const QStringList& paths) { emit totalCountChanged(); } -int WallReel::Core::Image::Model::fromProxyIndex(int proxyIndex) const { - if (proxyIndex < 0 || proxyIndex >= m_filteredIndices.size()) { - Logger::debug("Invalid proxy index requested: " + QString::number(proxyIndex)); - return -1; - } - return m_filteredIndices[m_currentSortReverse ? (m_filteredIndices.size() - 1 - proxyIndex) : proxyIndex]; -} - void WallReel::Core::Image::Model::focusOnIndex(int index) { if (index < 0 || index >= m_filteredIndices.count()) { Logger::debug("Invalid index to focus on: " + QString::number(index)); return; } - int actualIndex = fromProxyIndex(index); + int actualIndex = _convertProxyIndex(index); if (actualIndex < 0 || actualIndex >= m_data.count()) { Logger::debug("Actual index out of bounds for focus: " + QString::number(actualIndex)); return; } - m_focusedIndex = index; - _updateFocusedName(); + if (m_focusedIndex != index) { + m_focusedIndex = index; + emit focusedImageChanged(); + _updateFocusedProperties(); + } } void WallReel::Core::Image::Model::stop() { @@ -216,6 +214,14 @@ void WallReel::Core::Image::Model::stop() { } } +int WallReel::Core::Image::Model::_convertProxyIndex(int proxyIndex) const { + if (proxyIndex < 0 || proxyIndex >= m_filteredIndices.size()) { + Logger::debug("Invalid proxy index requested: " + QString::number(proxyIndex)); + return -1; + } + return m_filteredIndices[proxyIndex]; +} + void WallReel::Core::Image::Model::_clearData() { beginResetModel(); m_provider.clear(); @@ -257,11 +263,11 @@ void WallReel::Core::Image::Model::_updateSortIndices(Config::SortType type) { std::sort(indices.begin(), indices.end(), compareFunc); } -void WallReel::Core::Image::Model::_updateFocusedName() { - if (m_focusedIndex < 0 || m_focusedIndex >= m_data.count()) { +void WallReel::Core::Image::Model::_updateFocusedProperties() { + if (m_focusedIndex < 0 || m_focusedIndex >= m_filteredIndices.size()) { m_focusedName = ""; } else { - int actualIndex = fromProxyIndex(m_focusedIndex); + int actualIndex = _convertProxyIndex(m_focusedIndex); if (actualIndex < 0 || actualIndex >= m_data.count()) { m_focusedName = ""; } else { @@ -272,18 +278,36 @@ void WallReel::Core::Image::Model::_updateFocusedName() { emit focusedNameChanged(); } -void WallReel::Core::Image::Model::_applySearchFilter() { - emit layoutAboutToBeChanged(); - m_filteredIndices.clear(); +void WallReel::Core::Image::Model::_applySearchFilter(bool informView) { const auto& sortedIndices = m_sortIndices[static_cast(m_currentSortType)]; - for (int i = 0; i < sortedIndices.size(); ++i) { - int actualIndex = sortedIndices[i]; + int srcPos = 0, resPos = 0; + for (; srcPos < sortedIndices.size(); ++srcPos) { + int actualIndex = m_currentSortReverse ? sortedIndices[sortedIndices.size() - 1 - srcPos] : sortedIndices[srcPos]; + const auto& item = m_data[actualIndex]; + if (item->getFileName().contains(m_searchText, Qt::CaseInsensitive)) { + if (resPos >= m_filteredIndices.size() || m_filteredIndices[resPos] != actualIndex) { + break; + } + resPos++; + } + } + if (resPos == m_filteredIndices.size() && srcPos == sortedIndices.size()) { + return; // No change in filtered results + } + if (informView) { + emit layoutAboutToBeChanged(); + } + m_filteredIndices.resize(resPos); + for (int i = srcPos; i < sortedIndices.size(); ++i) { + int actualIndex = m_currentSortReverse ? sortedIndices[sortedIndices.size() - 1 - i] : sortedIndices[i]; const auto& item = m_data[actualIndex]; if (item->getFileName().contains(m_searchText, Qt::CaseInsensitive)) { m_filteredIndices.append(actualIndex); } } - emit layoutChanged(); + if (informView) { + emit layoutChanged(); + } } void WallReel::Core::Image::Model::_onProgressValueChanged(int value) { @@ -311,7 +335,7 @@ void WallReel::Core::Image::Model::_onProcessingFinished() { _updateSortIndices(static_cast(i)); } - _applySearchFilter(); + _applySearchFilter(false); endResetModel(); @@ -320,7 +344,6 @@ void WallReel::Core::Image::Model::_onProcessingFinished() { m_isLoading = false; m_progressUpdateTimer.stop(); emit progressChanged(); - // emit isLoadingChanged(); // QTimer::singleShot(s_IsLoadingUpdateIntervalMs, this, [this]() { // emit isLoadingChanged(); // }); @@ -329,8 +352,9 @@ void WallReel::Core::Image::Model::_onProcessingFinished() { void WallReel::Core::Image::Model::_onSortMethodChanged() { _applySearchFilter(); + emit focusedImageChanged(); } void WallReel::Core::Image::Model::_onSearchTextChanged() { - _updateFocusedName(); + emit focusedImageChanged(); } diff --git a/WallReel/Core/Image/model.hpp b/WallReel/Core/Image/model.hpp index 827aad8..e3a679b 100644 --- a/WallReel/Core/Image/model.hpp +++ b/WallReel/Core/Image/model.hpp @@ -78,23 +78,24 @@ class Model : public QAbstractListModel { Q_INVOKABLE void setSearchText(const QString& text); - const Data* getDataPtrAt(int index) const; + Data* imageAt(int index); + + Data* focusedImage(); Q_INVOKABLE QVariant dataAt(int index, const QString& roleName) const; void loadAndProcess(const QStringList& paths); - int fromProxyIndex(int proxyIndex) const; - Q_INVOKABLE void focusOnIndex(int index); Q_INVOKABLE void stop(); private: + int _convertProxyIndex(int proxyIndex) const; void _clearData(); void _updateSortIndices(Config::SortType type); - void _updateFocusedName(); - void _applySearchFilter(); + void _updateFocusedProperties(); + void _applySearchFilter(bool informView = true); signals: void isLoadingChanged(); @@ -104,6 +105,7 @@ class Model : public QAbstractListModel { void currentSortReverseChanged(); void focusedNameChanged(); void searchTextChanged(); + void focusedImageChanged(); private slots: void _onProgressValueChanged(int value); @@ -129,6 +131,9 @@ class Model : public QAbstractListModel { // QTimer m_searchDebounceTimer; // static constexpr int s_SearchDebounceIntervalMs = 300; + QColor m_focusedColor{}; + QString m_focusedColorName{}; + QFutureWatcher m_watcher; bool m_isLoading = false; diff --git a/WallReel/Core/Palette/data.hpp b/WallReel/Core/Palette/data.hpp index c13b687..03273c5 100644 --- a/WallReel/Core/Palette/data.hpp +++ b/WallReel/Core/Palette/data.hpp @@ -1,6 +1,8 @@ #ifndef WALLREEL_PALETTE_DATA_HPP #define WALLREEL_PALETTE_DATA_HPP +#include + #include #include #include @@ -37,6 +39,10 @@ struct PaletteItem { } return QColor(); } + + bool operator==(const PaletteItem& other) const { + return name == other.name; + } }; } // namespace WallReel::Core::Palette diff --git a/WallReel/Core/Palette/manager.cpp b/WallReel/Core/Palette/manager.cpp index 3e957a1..c904f02 100644 --- a/WallReel/Core/Palette/manager.cpp +++ b/WallReel/Core/Palette/manager.cpp @@ -1,10 +1,16 @@ #include "manager.hpp" +#include "Palette/matchcolor.hpp" +#include "Utils/misc.hpp" +#include "logger.hpp" #include "predefined.hpp" WallReel::Core::Palette::Manager::Manager( const Config::PaletteConfigItems& config, - QObject* parent) : QObject(parent) { + Image::Model& imageModel, + QObject* parent) : QObject(parent), m_imageModel(imageModel) { + connect(&m_imageModel, &Image::Model::focusedImageChanged, this, &Manager::updateColor); + // The new ones overrides the old ones, use a hashtable to track // the latest index of each palette name, then only insert the // ones whose index matches the latest index in the hashtable @@ -40,3 +46,59 @@ WallReel::Core::Palette::Manager::Manager( } } } + +void WallReel::Core::Palette::Manager::updateColor() { + bool hasResult = false; + Utils::Defer defer([&]() { + if (!hasResult) { + m_displayColor = QColor(); + m_displayColorName = ""; + } + emit colorChanged(); + emit colorNameChanged(); + }); + auto imageData = m_imageModel.focusedImage(); + if (!imageData || !imageData->isValid()) { + return; + } + // No palette selected, use dominant color + if (!m_selectedPalette.has_value()) { + m_displayColor = imageData->getDominantColor(); + m_displayColorName = ""; + hasResult = true; + return; + } + // Only palette selected, use the colosest color in the palette + if (!m_selectedColor.has_value()) { + auto cached = imageData->getCachedColor(m_selectedPalette->name); + if (cached.has_value()) { + auto it = std::find_if(m_selectedPalette->colors.begin(), + m_selectedPalette->colors.end(), + [&](const ColorItem& item) { + return item.name == cached.value(); + }); + if (it != m_selectedPalette->colors.end()) { + Logger::debug("Using cached color match for image " + imageData->getFileName() + + ": " + it->name); + m_displayColor = it->color; + m_displayColorName = it->name; + hasResult = true; + return; + } + } + auto matched = bestMatch( + imageData->getDominantColor(), + m_selectedPalette.value().colors); + Logger::debug("Computed color match for image " + imageData->getFileName() + ": " + + matched.name); + imageData->cacheColor(m_selectedPalette->name, matched.name); + m_displayColor = matched.color; + m_displayColorName = matched.name; + hasResult = true; + return; + } + // Both are set, use them + m_displayColor = m_selectedColor.value().color; + m_displayColorName = m_selectedColor.value().name; + hasResult = true; +} diff --git a/WallReel/Core/Palette/manager.hpp b/WallReel/Core/Palette/manager.hpp index c2775bb..21499c0 100644 --- a/WallReel/Core/Palette/manager.hpp +++ b/WallReel/Core/Palette/manager.hpp @@ -1,7 +1,10 @@ #ifndef WALLREEL_PALETTE_MANAGER_HPP #define WALLREEL_PALETTE_MANAGER_HPP +#include + #include "Config/data.hpp" +#include "Image/model.hpp" #include "data.hpp" namespace WallReel::Core::Palette { @@ -9,24 +12,74 @@ namespace WallReel::Core::Palette { class Manager : public QObject { Q_OBJECT Q_PROPERTY(QList availablePalettes READ availablePalettes CONSTANT) + Q_PROPERTY(QColor color READ color NOTIFY colorChanged) + Q_PROPERTY(QString colorName READ colorName NOTIFY colorNameChanged) public: Manager(const Config::PaletteConfigItems& config, + Image::Model& imageModel, QObject* parent = nullptr); const QList& availablePalettes() const { return m_palettes; } - Q_INVOKABLE PaletteItem getPalette(const QString& name) const { - for (const auto& p : m_palettes) { - if (p.name == name) return p; - } - return PaletteItem(); + const QColor& color() const { + return m_displayColor; } + const QString& colorName() const { + return m_displayColorName; + } + + Q_INVOKABLE void setSelectedPalette(const QVariant& paletteVar) { + if (paletteVar.isNull() || !paletteVar.isValid()) { + m_selectedPalette = std::nullopt; + } else { + m_selectedPalette = paletteVar.value(); + } + updateColor(); + } + + Q_INVOKABLE void setSelectedColor(const QVariant& colorVar) { + if (colorVar.isNull() || !colorVar.isValid()) { + m_selectedColor = std::nullopt; + } else { + m_selectedColor = colorVar.value(); + } + updateColor(); + } + + QString getSelectedPaletteName() const { + return m_selectedPalette ? m_selectedPalette->name : QString(); + } + + QString getCurrentColorName() const { + return m_displayColorName; + } + + QString getCurrentColorHex() const { + return m_displayColor.isValid() + ? m_displayColor.name() + : QString(); + } + + public slots: + void updateColor(); + + signals: + void colorChanged(); + void colorNameChanged(); + private: + Image::Model& m_imageModel; + QList m_palettes; + std::optional m_selectedPalette = std::nullopt; + std::optional m_selectedColor = std::nullopt; + + QColor m_displayColor; + QString m_displayColorName; }; } // namespace WallReel::Core::Palette diff --git a/WallReel/Core/Palette/matchcolor.cpp b/WallReel/Core/Palette/matchcolor.cpp new file mode 100644 index 0000000..fd3ac9c --- /dev/null +++ b/WallReel/Core/Palette/matchcolor.cpp @@ -0,0 +1,70 @@ +#include "matchcolor.hpp" + +#include +#include +#include + +namespace WallReel::Core::Palette { + +const ColorItem& bestMatch(const QColor& target, const QList& candidates) { + if (candidates.isEmpty() || !target.isValid()) { + static ColorItem emptyItem; + return emptyItem; + } + + int target_r = target.red(); + int target_g = target.green(); + int target_b = target.blue(); + + int target_h = target.hsvHue(); + double target_s = target.hsvSaturationF(); + + const ColorItem* closest_flavor = nullptr; + double min_distance = std::numeric_limits::max(); + + for (const auto& candidate : candidates) { + QColor p_color = candidate.color; + int p_r = p_color.red(); + int p_g = p_color.green(); + int p_b = p_color.blue(); + + int p_h = p_color.hsvHue(); + + // RGB distance with weighting + double rmean = (target_r + p_r) / 2.0; + double dr = target_r - p_r; + double dg = target_g - p_g; + double db = target_b - p_b; + + double rgb_distance = std::sqrt((2.0 + rmean / 256.0) * dr * dr + 4.0 * dg * dg + + (2.0 + (255.0 - rmean) / 256.0) * db * db); + + // Hue difference (with wrapping) + double hue_diff = 0.0; + if (target_h != -1 && p_h != -1) { + hue_diff = std::abs(target_h - p_h); + if (hue_diff > 180.0) { + hue_diff = 360.0 - hue_diff; + } + } + + // Increase hue weight when saturation is high + double hue_weight = (target_s > 0.20) ? 2.0 : 0.5; + + double total_distance = rgb_distance + (hue_diff * hue_weight * 3.0); + + if (total_distance < min_distance) { + min_distance = total_distance; + closest_flavor = &candidate; + } + } + + if (closest_flavor) { + return *closest_flavor; + } + + static ColorItem emptyItem; + return emptyItem; +} + +} // namespace WallReel::Core::Palette diff --git a/WallReel/Core/Palette/matchcolor.hpp b/WallReel/Core/Palette/matchcolor.hpp new file mode 100644 index 0000000..5105920 --- /dev/null +++ b/WallReel/Core/Palette/matchcolor.hpp @@ -0,0 +1,12 @@ +#ifndef WALLREEL_CORE_PALETTE_MATCHCOLOR_HPP +#define WALLREEL_CORE_PALETTE_MATCHCOLOR_HPP + +#include "data.hpp" + +namespace WallReel::Core::Palette { + +const ColorItem& bestMatch(const QColor& target, const QList& candidates); + +} // namespace WallReel::Core::Palette + +#endif // WALLREEL_CORE_PALETTE_MATCHCOLOR_HPP diff --git a/WallReel/Core/Service/manager.hpp b/WallReel/Core/Service/manager.hpp index dc751a3..803d186 100644 --- a/WallReel/Core/Service/manager.hpp +++ b/WallReel/Core/Service/manager.hpp @@ -6,6 +6,7 @@ #include "Config/data.hpp" #include "Image/model.hpp" +#include "Palette/manager.hpp" #include "Service/wallpaper.hpp" #include "logger.hpp" @@ -19,10 +20,16 @@ class Manager : public QObject { public: Manager( const Config::ActionConfigItems& actionConfig, - const Image::Model& imageModel, - QObject* parent = nullptr) : m_imageModel(imageModel) { - m_wallpaperService = new WallpaperService(actionConfig, this); + Image::Model& imageModel, + Palette::Manager& paletteManager, + QObject* parent = nullptr) : m_actionConfig(actionConfig), m_imageModel(imageModel), m_paletteManager(paletteManager) { + m_wallpaperService = new WallpaperService(m_actionConfig, m_paletteManager, this); + // Listen on image change + connect(&m_imageModel, &Image::Model::focusedImageChanged, this, &Manager::previewWallpaper); + // Listen on palette change + connect(&m_paletteManager, &Palette::Manager::colorChanged, this, &Manager::previewWallpaper); + connect(&m_paletteManager, &Palette::Manager::colorNameChanged, this, &Manager::previewWallpaper); // Forward signals // Direct signal 2 signal connection connect(m_wallpaperService, &WallpaperService::previewCompleted, this, &Manager::previewCompleted); @@ -38,7 +45,7 @@ class Manager : public QObject { } m_isProcessing = true; emit isProcessingChanged(); - const auto* data = m_imageModel.getDataPtrAt(index); + const auto* data = m_imageModel.imageAt(index); if (data) { m_wallpaperService->select(*data); } else { @@ -48,15 +55,6 @@ class Manager : public QObject { } } - Q_INVOKABLE void previewWallpaper(int index) { - const auto* data = m_imageModel.getDataPtrAt(index); - if (data) { - m_wallpaperService->preview(*data); - } else { - emit previewCompleted(); - } - } - Q_INVOKABLE void restore() { if (m_isProcessing) { Logger::debug("Already processing an action, ignoring restore request"); @@ -67,8 +65,31 @@ class Manager : public QObject { m_wallpaperService->restore(); } + Q_INVOKABLE void cancel() { + m_wallpaperService->stopAll(); + if (m_actionConfig.restoreOnCancel) { + connect(m_wallpaperService, &WallpaperService::restoreCompleted, this, [this]() { + emit cancelCompleted(); + }); + restore(); + } else { + emit cancelCompleted(); + } + } + bool isProcessing() const { return m_isProcessing; } + public slots: + + void previewWallpaper() { + const auto* data = m_imageModel.focusedImage(); + if (data) { + m_wallpaperService->preview(*data); + } else { + emit previewCompleted(); + } + } + private slots: void _onSelectCompleted() { @@ -91,10 +112,13 @@ class Manager : public QObject { void selectCompleted(); void previewCompleted(); void restoreCompleted(); + void cancelCompleted(); private: WallpaperService* m_wallpaperService; - const Image::Model& m_imageModel; + const Config::ActionConfigItems& m_actionConfig; + Image::Model& m_imageModel; + Palette::Manager& m_paletteManager; bool m_isProcessing = false; }; diff --git a/WallReel/Core/Service/wallpaper.cpp b/WallReel/Core/Service/wallpaper.cpp index 8f33fb0..8a87ac9 100644 --- a/WallReel/Core/Service/wallpaper.cpp +++ b/WallReel/Core/Service/wallpaper.cpp @@ -8,8 +8,9 @@ WallReel::Core::Service::WallpaperService::WallpaperService( const Config::ActionConfigItems& actionConfig, + const Palette::Manager& paletteManager, QObject* parent) - : QObject(parent), m_actionConfig(actionConfig) { + : QObject(parent), m_actionConfig(actionConfig), m_paletteManager(paletteManager) { m_previewDebounceTimer = new QTimer(this); m_previewDebounceTimer->setSingleShot(true); m_previewDebounceTimer->setInterval(m_actionConfig.previewDebounceTime); @@ -48,6 +49,22 @@ WallReel::Core::Service::WallpaperService::WallpaperService( }); } +void WallReel::Core::Service::WallpaperService::stopAll() { + if (m_previewProcess->state() != QProcess::NotRunning) { + m_previewProcess->kill(); + m_previewProcess->waitForFinished(); + } + if (m_selectProcess->state() != QProcess::NotRunning) { + m_selectProcess->kill(); + m_selectProcess->waitForFinished(); + } + if (m_restoreProcess->state() != QProcess::NotRunning) { + m_restoreProcess->kill(); + m_restoreProcess->waitForFinished(); + } + m_previewDebounceTimer->stop(); +} + void WallReel::Core::Service::WallpaperService::preview(const Image::Data& imageData) { m_pendingImageData = &imageData; m_previewDebounceTimer->start(); @@ -69,6 +86,32 @@ void WallReel::Core::Service::WallpaperService::restore() { _doRestore(); } +QHash WallReel::Core::Service::WallpaperService::_generateVariables(const Image::Data& imageData) { + auto palette = m_paletteManager.getSelectedPaletteName(); + if (palette.isEmpty()) { + palette = "null"; + } + auto color = m_paletteManager.getCurrentColorName(); + if (color.isEmpty()) { + color = "null"; + } + auto hex = m_paletteManager.getCurrentColorHex(); + if (hex.isEmpty()) { + hex = "null"; + } + return { + {"path", imageData.getFullPath()}, + {"name", imageData.getFileName()}, + {"size", QString::number(imageData.getSize())}, + {"width", QString::number(imageData.getImage().width())}, + {"height", QString::number(imageData.getImage().height())}, + {"palette", palette}, + {"color", color}, + {"colorHex", hex}, + {"domColor", m_paletteManager.color().name()}, + }; +} + void WallReel::Core::Service::WallpaperService::_doPreview(const Image::Data& imageData) { QString path = imageData.getFullPath(); @@ -81,15 +124,13 @@ void WallReel::Core::Service::WallpaperService::_doPreview(const Image::Data& im std::cout << path.toStdString() << std::endl; } - const QHash variables{ - {"path", path}, - {"name", imageData.getFileName()}, - }; - auto command = Utils::renderTemplate(m_actionConfig.onPreview, variables); + const auto variables = _generateVariables(imageData); + auto command = Utils::renderTemplate(m_actionConfig.onPreview, variables); if (command.isEmpty()) { emit previewCompleted(); return; } + Logger::debug(QString("Executing preview command: %1").arg(command)); if (m_previewProcess->state() != QProcess::NotRunning) { m_previewProcess->kill(); @@ -110,15 +151,13 @@ void WallReel::Core::Service::WallpaperService::_doSelect(const Image::Data& ima std::cout << path.toStdString() << std::endl; } - const QHash variables{ - {"path", path}, - {"name", imageData.getFileName()}, - }; - auto command = Utils::renderTemplate(m_actionConfig.onSelected, variables); + const auto variables = _generateVariables(imageData); + auto command = Utils::renderTemplate(m_actionConfig.onSelected, variables); if (command.isEmpty()) { emit selectCompleted(); return; } + Logger::debug(QString("Executing select command: %1").arg(command)); m_selectProcess->start("sh", QStringList() << "-c" << command); } @@ -133,5 +172,6 @@ void WallReel::Core::Service::WallpaperService::_doRestore() { emit restoreCompleted(); return; } + Logger::debug(QString("Executing restore command: %1").arg(command)); m_restoreProcess->start("sh", QStringList() << "-c" << command); } diff --git a/WallReel/Core/Service/wallpaper.hpp b/WallReel/Core/Service/wallpaper.hpp index 8ab0ff8..e368634 100644 --- a/WallReel/Core/Service/wallpaper.hpp +++ b/WallReel/Core/Service/wallpaper.hpp @@ -6,6 +6,7 @@ #include "Config/data.hpp" #include "Image/data.hpp" +#include "Palette/manager.hpp" namespace WallReel::Core::Service { @@ -15,8 +16,11 @@ class WallpaperService : public QObject { public: WallpaperService( const Config::ActionConfigItems& actionConfig, + const Palette::Manager& paletteManager, QObject* parent = nullptr); + void stopAll(); + public slots: void preview(const Image::Data& imageData); // execute after 500ms of inactivity void select(const Image::Data& imageData); // execute immediately, ignore if already running @@ -31,8 +35,10 @@ class WallpaperService : public QObject { void _doPreview(const Image::Data& imageData); void _doSelect(const Image::Data& imageData); void _doRestore(); + QHash _generateVariables(const Image::Data& imageData); const Config::ActionConfigItems& m_actionConfig; + const Palette::Manager& m_paletteManager; QTimer* m_previewDebounceTimer; const Image::Data* m_pendingImageData; QProcess* m_previewProcess; diff --git a/WallReel/UI/CMakeLists.txt b/WallReel/UI/CMakeLists.txt index 68f34ee..4495cb1 100644 --- a/WallReel/UI/CMakeLists.txt +++ b/WallReel/UI/CMakeLists.txt @@ -8,10 +8,10 @@ qt_add_qml_module(${UILIB_NAME} Providers/CarouselProvider.qml Modules/Carousel.qml Modules/TitleBar.qml - Modules/SearchBar.qml Modules/SortControl.qml Modules/ColorControl.qml Modules/TopBar.qml Modules/BottomBar.qml + Components/WRSearchBar.qml Components/WRTextButton.qml ) diff --git a/WallReel/UI/Modules/SearchBar.qml b/WallReel/UI/Components/WRSearchBar.qml similarity index 100% rename from WallReel/UI/Modules/SearchBar.qml rename to WallReel/UI/Components/WRSearchBar.qml diff --git a/WallReel/UI/Modules/ColorControl.qml b/WallReel/UI/Modules/ColorControl.qml index 22a6f7f..ac6e71c 100644 --- a/WallReel/UI/Modules/ColorControl.qml +++ b/WallReel/UI/Modules/ColorControl.qml @@ -28,7 +28,7 @@ Item { ComboBox { id: paletteCombo - implicitWidth: 110 + implicitWidth: 200 // -1 means nothing selected currentIndex: -1 displayText: currentIndex < 0 ? "— palette —" : currentText @@ -77,7 +77,7 @@ Item { if (root.colorHex.length > 0) return root.colorName.length > 0 ? root.colorName + " " + root.colorHex : root.colorHex; - return root.colorName == "Auto" ? "" : root.colorName; + return root.colorName; } visible: root.colorName.length > 0 || root.colorHex.length > 0 } diff --git a/WallReel/UI/Modules/TopBar.qml b/WallReel/UI/Modules/TopBar.qml index 3e2006c..aba0849 100644 --- a/WallReel/UI/Modules/TopBar.qml +++ b/WallReel/UI/Modules/TopBar.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import WallReel.UI.Components Item { id: root @@ -43,7 +44,7 @@ Item { Layout.fillWidth: true } - SearchBar { + WRSearchBar { id: searchBar Layout.alignment: Qt.AlignVCenter diff --git a/WallReel/UI/Providers/CarouselProvider.qml b/WallReel/UI/Providers/CarouselProvider.qml index 4a98925..7290a26 100644 --- a/WallReel/UI/Providers/CarouselProvider.qml +++ b/WallReel/UI/Providers/CarouselProvider.qml @@ -22,13 +22,13 @@ QtObject { property string selectedSortType: ImageModel.currentSortType property bool isSortReverse: ImageModel.currentSortReverse //// Palette / Color - readonly property var availablePalettes: [] + readonly property var availablePalettes: PaletteManager.availablePalettes property var selectedPalette: null // PaletteItem | null readonly property var availableColors: selectedPalette ? selectedPalette.colors : [] property var selectedColor: null // ColorItem | null (null means "auto") - readonly property string colorName: selectedColor ? selectedColor.name : "Auto" - readonly property string colorHex: selectedColor ? selectedColor.color.toString().toUpperCase() : "" - readonly property color colorValue: selectedColor ? selectedColor.color : "transparent" + readonly property string colorName: PaletteManager.colorName + readonly property string colorHex: PaletteManager.color + readonly property color colorValue: PaletteManager.color //// Actions state readonly property bool isProcessing: ServiceManager.isProcessing @@ -42,7 +42,7 @@ QtObject { } function cancel() { - Qt.quit(); + ServiceManager.cancel(); } function focusSearch() { @@ -68,19 +68,25 @@ QtObject { function setSearchText(text) { ImageModel.setSearchText(text); - currentIndex = 0; // reset index when search text changes + if (currentIndex != 0) + currentIndex = 0; + } onCurrentIndexChanged: () => { - if (!isLoading) { - ServiceManager.previewWallpaper(currentIndex); + if (!isLoading) ImageModel.focusOnIndex(currentIndex); - } + } Component.onCompleted: () => { - if (!isLoading) { - ServiceManager.previewWallpaper(currentIndex); + if (!isLoading) ImageModel.focusOnIndex(currentIndex); - } + + } + onSelectedPaletteChanged: () => { + PaletteManager.setSelectedPalette(selectedPalette); + } + onSelectedColorChanged: () => { + PaletteManager.setSelectedColor(selectedColor); } } diff --git a/WallReel/main.cpp b/WallReel/main.cpp index b1edcd1..adc38fb 100644 --- a/WallReel/main.cpp +++ b/WallReel/main.cpp @@ -1,3 +1,5 @@ +#include + #include #include #include @@ -46,13 +48,6 @@ int main(int argc, char* argv[]) { "Config", config); - auto paletteMgr = new Palette::Manager( - config->getPaletteConfig(), - &a); - engine.rootContext()->setContextProperty("PaletteManager", paletteMgr); - qRegisterMetaType("PaletteItem"); - qRegisterMetaType("ColorItem"); - auto imageModel = new Image::Model( *imageProvider, config->getSortConfig(), @@ -65,10 +60,19 @@ int main(int argc, char* argv[]) { "ImageModel", imageModel); + auto paletteMgr = new Palette::Manager( + config->getPaletteConfig(), + *imageModel, + imageModel); + engine.rootContext()->setContextProperty("PaletteManager", paletteMgr); + qRegisterMetaType("PaletteItem"); + qRegisterMetaType("ColorItem"); + auto Service = new Service::Manager( config->getActionConfig(), *imageModel, - imageModel); + *paletteMgr, + paletteMgr); qmlRegisterSingletonInstance( COREMODULE_URI, MODULE_VERSION_MAJOR, @@ -82,6 +86,11 @@ int main(int argc, char* argv[]) { &a, []() { QCoreApplication::quit(); }); } + QObject::connect( + Service, + &Service::Manager::cancelCompleted, + &a, + []() { QCoreApplication::quit(); }); QObject::connect( &engine, @@ -91,6 +100,7 @@ int main(int argc, char* argv[]) { Qt::QueuedConnection); engine.loadFromModule(UIMODULE_URI, "Main"); + config->captureState(); imageModel->loadAndProcess(config->getWallpapers()); return a.exec();