🚧 wip: che kku po in to

This commit is contained in:
2026-02-17 22:23:20 +01:00
parent e3b4d42762
commit 9622c5b1fe
21 changed files with 1662 additions and 219 deletions
+16 -1
View File
@@ -13,6 +13,8 @@ set(MODULE_VERSION_MINOR 0)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_CXX_STANDARD 23)
if(NOT CMAKE_BUILD_TYPE) if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release) set(CMAKE_BUILD_TYPE Release)
endif() endif()
@@ -23,15 +25,28 @@ endif()
configure_file(WallReel/version.h.in ${CMAKE_BINARY_DIR}/generated/version.h) configure_file(WallReel/version.h.in ${CMAKE_BINARY_DIR}/generated/version.h)
find_package(Qt6 REQUIRED COMPONENTS Quick Widgets QuickControls2 Concurrent) find_package(Qt6 REQUIRED COMPONENTS Quick Widgets QuickControls2 Concurrent Test)
qt_standard_project_setup(REQUIRES 6.5) qt_standard_project_setup(REQUIRES 6.5)
qt_policy(SET QTP0004 NEW) qt_policy(SET QTP0004 NEW)
option(BUILD_TESTING "Build the testing tree." ON)
option(ADDRESS_SANITIZER "Enable Address Sanitizer for debugging." OFF)
add_subdirectory(WallReel/Core) add_subdirectory(WallReel/Core)
add_subdirectory(WallReel/UI) add_subdirectory(WallReel/UI)
if(BUILD_TESTING)
enable_testing()
add_subdirectory(Tests)
endif()
if(ADDRESS_SANITIZER)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -g")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address")
endif()
add_executable(${EXECUTABLE_NAME} add_executable(${EXECUTABLE_NAME}
WallReel/main.cpp WallReel/main.cpp
) )
+40
View File
@@ -0,0 +1,40 @@
project(Tests LANGUAGES CXX)
find_package(Qt6 REQUIRED COMPONENTS Core Test)
add_executable(tst_configmgr
tst_configmgr.cpp
)
add_executable(tst_imagemodel
tst_imagemodel.cpp
)
add_test(NAME tst_configmgr COMMAND tst_configmgr)
add_test(NAME tst_imagemodel COMMAND tst_imagemodel)
target_link_libraries(tst_configmgr PRIVATE
Qt6::Core
Qt6::Test
Qt6::Gui
wallreel-core
)
target_link_libraries(tst_imagemodel PRIVATE
Qt6::Core
Qt6::Test
Qt6::Gui
Qt6::Quick
wallreel-core
)
target_include_directories(tst_configmgr PRIVATE
${CMAKE_SOURCE_DIR}/WallReel/Core
${CMAKE_BINARY_DIR}/generated
)
target_include_directories(tst_imagemodel PRIVATE
${CMAKE_SOURCE_DIR}/WallReel/Core
${CMAKE_BINARY_DIR}/generated
)
+396
View File
@@ -0,0 +1,396 @@
#include <QtGui/qcolor.h>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSignalSpy>
#include <QTemporaryDir>
#include <QTest>
#include "configmgr.hpp"
class TestConfigMgr : public QObject {
Q_OBJECT
private slots:
void initTestCase();
void cleanupTestCase();
void testDefaults();
void testFullConfigParsing();
void testInvalidConfigValues();
void testWallpaperScanRecursive();
void testWallpaperScanNonRecursive();
void testWallpaperExcludes();
void testExplicitPaths();
void testImageExtensions();
void testSortTypes();
private:
QTemporaryDir m_tempDir;
QString m_configPath;
QString m_wallpaperRoot;
void writeConfig(const QJsonObject& json);
void createDummyFile(const QString& relPath);
};
void TestConfigMgr::initTestCase() {
QVERIFY(m_tempDir.isValid());
m_configPath = m_tempDir.path() + "/config.json";
m_wallpaperRoot = m_tempDir.path() + "/wallpapers";
QDir().mkpath(m_wallpaperRoot);
}
void TestConfigMgr::cleanupTestCase() {
}
void TestConfigMgr::writeConfig(const QJsonObject& json) {
QFile configFile(m_configPath);
QVERIFY(configFile.open(QIODevice::WriteOnly));
configFile.write(QJsonDocument(json).toJson());
configFile.close();
}
void TestConfigMgr::createDummyFile(const QString& relPath) {
QString absPath = m_wallpaperRoot + "/" + relPath;
QFileInfo fi(absPath);
QDir().mkpath(fi.absolutePath());
QFile file(absPath);
QVERIFY(file.open(QIODevice::WriteOnly));
file.write("foobar");
file.close();
}
void TestConfigMgr::testDefaults() {
// Empty config file
writeConfig(QJsonObject());
Config config(m_tempDir.path(), {}, m_configPath);
// Check Style Defaults
QCOMPARE(config.getImageWidth(), 320);
QCOMPARE(config.getImageHeight(), 200);
QCOMPARE(config.getImageFocusScale(), 1.5);
QCOMPARE(config.getWindowWidth(), 750);
QCOMPARE(config.getWindowHeight(), 500);
// Check Sort Defaults
QCOMPARE(config.getSortConfig().type, Config::SortType::Name);
QCOMPARE(config.getSortConfig().reverse, false);
// Check Action Defaults
QCOMPARE(config.getActionConfig().previewDebounceTime, 300);
QCOMPARE(config.getActionConfig().printSelected, false);
QCOMPARE(config.getActionConfig().printPreview, false);
QVERIFY(config.getActionConfig().onSelected.isEmpty());
QVERIFY(config.getActionConfig().onPreview.isEmpty());
QVERIFY(config.getActionConfig().onRestore.isEmpty());
QVERIFY(config.getActionConfig().saveState.isEmpty());
}
void TestConfigMgr::testFullConfigParsing() {
QJsonObject root;
// Wallpaper settings
QJsonObject wallpaperObj;
QJsonArray dirsArray;
QJsonObject dir1;
dir1["path"] = "/tmp/w1";
dir1["recursive"] = true;
dirsArray.append(dir1);
wallpaperObj["dirs"] = dirsArray;
QJsonArray pathsArray;
pathsArray.append("/tmp/p1.jpg");
wallpaperObj["paths"] = pathsArray;
QJsonArray excludesArray;
excludesArray.append(".*bad.*");
wallpaperObj["excludes"] = excludesArray;
root["wallpaper"] = wallpaperObj;
// Palette
QJsonArray palettesArray;
QJsonObject palette1;
palette1["name"] = "Default";
QJsonArray colorsArray;
QJsonObject color1;
color1["name"] = "Red";
color1["value"] = "#FF0000";
colorsArray.append(color1);
palette1["colors"] = colorsArray;
palettesArray.append(palette1);
root["palettes"] = palettesArray;
// Action
QJsonObject actionObj;
actionObj["printSelected"] = true;
actionObj["onSelected"] = "echo {{ path }}";
root["action"] = actionObj;
// Style
QJsonObject styleObj;
styleObj["image_width"] = 100;
styleObj["image_height"] = 100;
root["style"] = styleObj;
// Sort
QJsonObject sortObj;
sortObj["type"] = "date";
sortObj["reverse"] = true;
root["sort"] = sortObj;
writeConfig(root);
Config config(m_tempDir.path(), {}, m_configPath);
// Assertions
QCOMPARE(config.getWallpaperConfig().dirs.size(), 1);
QCOMPARE(config.getWallpaperConfig().dirs[0].path, "/tmp/w1");
QCOMPARE(config.getWallpaperConfig().dirs[0].recursive, true);
QCOMPARE(config.getWallpaperConfig().paths.size(), 1);
QCOMPARE(config.getWallpaperConfig().paths[0], "/tmp/p1.jpg");
QCOMPARE(config.getWallpaperConfig().excludes.size(), 1);
QCOMPARE(config.getWallpaperConfig().excludes[0].pattern(), ".*bad.*");
QCOMPARE(config.getPaletteConfig().palettes.size(), 1);
QCOMPARE(config.getPaletteConfig().palettes[0].name, "Default");
QCOMPARE(config.getPaletteConfig().palettes[0].colors.size(), 1);
QCOMPARE(config.getPaletteConfig().palettes[0].colors[0].name, "Red");
QCOMPARE(config.getPaletteConfig().palettes[0].colors[0].value.name().toLower(), "#ff0000");
QCOMPARE(config.getActionConfig().printSelected, true);
QCOMPARE(config.getActionConfig().onSelected, "echo {{ path }}");
QCOMPARE(config.getImageWidth(), 100);
QCOMPARE(config.getImageHeight(), 100);
QCOMPARE(config.getSortConfig().type, Config::SortType::Date);
QCOMPARE(config.getSortConfig().reverse, true);
}
void TestConfigMgr::testInvalidConfigValues() {
QJsonObject root;
QJsonObject styleObj;
styleObj["image_width"] = "not a number"; // Should be ignored
root["style"] = styleObj;
QJsonObject sortObj;
sortObj["type"] = "invalid_type";
root["sort"] = sortObj;
writeConfig(root);
Config config(m_tempDir.path(), {}, m_configPath);
// Should retain defaults
QCOMPARE(config.getImageWidth(), 320);
QCOMPARE(config.getSortConfig().type, Config::SortType::Name);
}
void TestConfigMgr::testWallpaperScanRecursive() {
// Setup files
createDummyFile("rec/root.jpg");
createDummyFile("rec/sub/deep.png"); // should be found
createDummyFile("rec/ignore.txt"); // should be ignored
QJsonObject root;
QJsonObject wallpaperObj;
QJsonArray dirsArray;
QJsonObject dirConfig;
dirConfig["path"] = m_wallpaperRoot + "/rec";
dirConfig["recursive"] = true;
dirsArray.append(dirConfig);
wallpaperObj["dirs"] = dirsArray;
root["wallpaper"] = wallpaperObj;
writeConfig(root);
Config config(m_tempDir.path(), {}, m_configPath);
QStringList wallpapers = config.getWallpapers();
QCOMPARE(wallpapers.size(), 2);
// Sort to verify presence
wallpapers.sort();
// Paths are absolute
QVERIFY(wallpapers[0].endsWith("root.jpg"));
QVERIFY(wallpapers[1].endsWith("deep.png"));
}
void TestConfigMgr::testWallpaperScanNonRecursive() {
createDummyFile("nonrec/root.jpg");
createDummyFile("nonrec/sub/deep.png");
QJsonObject root;
QJsonObject wallpaperObj;
QJsonArray dirsArray;
QJsonObject dirConfig;
dirConfig["path"] = m_wallpaperRoot + "/nonrec";
dirConfig["recursive"] = false;
dirsArray.append(dirConfig);
wallpaperObj["dirs"] = dirsArray;
root["wallpaper"] = wallpaperObj;
writeConfig(root);
Config config(m_tempDir.path(), {}, m_configPath);
QStringList wallpapers = config.getWallpapers();
QCOMPARE(wallpapers.size(), 1);
QVERIFY(wallpapers[0].endsWith("root.jpg"));
}
void TestConfigMgr::testWallpaperExcludes() {
createDummyFile("excl/good.jpg");
createDummyFile("excl/bad.jpg");
QJsonObject root;
QJsonObject wallpaperObj;
QJsonArray dirsArray;
QJsonObject dirConfig;
dirConfig["path"] = m_wallpaperRoot + "/excl";
dirConfig["recursive"] = false;
dirsArray.append(dirConfig);
wallpaperObj["dirs"] = dirsArray;
QJsonArray excludes;
excludes.append(".*bad\\.jpg$");
wallpaperObj["excludes"] = excludes;
root["wallpaper"] = wallpaperObj;
writeConfig(root);
Config config(m_tempDir.path(), {}, m_configPath);
QStringList wallpapers = config.getWallpapers();
QCOMPARE(wallpapers.size(), 1);
QVERIFY(wallpapers[0].endsWith("good.jpg"));
}
void TestConfigMgr::testExplicitPaths() {
createDummyFile("explicit/a.jpg");
QString absPath = m_wallpaperRoot + "/explicit/a.jpg";
QJsonObject root;
QJsonObject wallpaperObj;
QJsonArray pathsArray;
pathsArray.append(absPath);
wallpaperObj["paths"] = pathsArray;
root["wallpaper"] = wallpaperObj;
writeConfig(root);
Config config(m_tempDir.path(), {}, m_configPath);
QStringList wallpapers = config.getWallpapers();
QCOMPARE(wallpapers.size(), 1);
QCOMPARE(wallpapers[0], absPath);
}
void TestConfigMgr::testImageExtensions() {
createDummyFile("ext/image1.jpg");
createDummyFile("ext/image2.jpeg");
createDummyFile("ext/image3.png");
createDummyFile("ext/image4.bmp");
createDummyFile("ext/text.txt");
createDummyFile("ext/script.sh");
createDummyFile("ext/noext");
QJsonObject root;
QJsonObject wallpaperObj;
QJsonArray dirsArray;
QJsonObject dirConfig;
dirConfig["path"] = m_wallpaperRoot + "/ext";
dirConfig["recursive"] = false;
dirsArray.append(dirConfig);
wallpaperObj["dirs"] = dirsArray;
root["wallpaper"] = wallpaperObj;
writeConfig(root);
Config config(m_tempDir.path(), {}, m_configPath);
QStringList wallpapers = config.getWallpapers();
int imageCount = 0;
for (const auto& w : wallpapers) {
if (w.endsWith(".txt") || w.endsWith(".sh") || w.endsWith("noext")) {
QFAIL(qPrintable("Found non-image file: " + w));
}
imageCount++;
}
QVERIFY(imageCount >= 3);
}
void TestConfigMgr::testSortTypes() {
// 1. None sort
{
QJsonObject root;
QJsonObject sortObj;
sortObj["type"] = "none";
sortObj["reverse"] = false;
root["sort"] = sortObj;
writeConfig(root);
Config config(m_tempDir.path(), {}, m_configPath);
QCOMPARE(config.getSortConfig().type, Config::SortType::None);
QCOMPARE(config.getSortConfig().reverse, false);
}
// 2. Name sort (default)
{
QJsonObject root;
QJsonObject sortObj;
sortObj["type"] = "name";
sortObj["reverse"] = true;
root["sort"] = sortObj;
writeConfig(root);
Config config(m_tempDir.path(), {}, m_configPath);
QCOMPARE(config.getSortConfig().type, Config::SortType::Name);
QCOMPARE(config.getSortConfig().reverse, true);
}
// 3. Size sort
{
QJsonObject root;
QJsonObject sortObj;
sortObj["type"] = "size";
root["sort"] = sortObj;
writeConfig(root);
Config config(m_tempDir.path(), {}, m_configPath);
QCOMPARE(config.getSortConfig().type, Config::SortType::Size);
}
// 4. Date sort
{
QJsonObject root;
QJsonObject sortObj;
sortObj["type"] = "date";
root["sort"] = sortObj;
writeConfig(root);
Config config(m_tempDir.path(), {}, m_configPath);
QCOMPARE(config.getSortConfig().type, Config::SortType::Date);
}
// 5. Invalid sort -> fallback to default (Name)
{
QJsonObject root;
QJsonObject sortObj;
sortObj["type"] = "invalid_blah";
root["sort"] = sortObj;
writeConfig(root);
Config config(m_tempDir.path(), {}, m_configPath);
// Default initialized in Config constructor is Name
// But warning is logged
QCOMPARE(config.getSortConfig().type, Config::SortType::Name);
}
// 6. Case insensitivity for type string
{
QJsonObject root;
QJsonObject sortObj;
sortObj["type"] = "DaTe";
root["sort"] = sortObj;
writeConfig(root);
Config config(m_tempDir.path(), {}, m_configPath);
QCOMPARE(config.getSortConfig().type, Config::SortType::Date);
}
}
QTEST_MAIN(TestConfigMgr)
#include "tst_configmgr.moc"
+218
View File
@@ -0,0 +1,218 @@
#include <QDate>
#include <QSignalSpy>
#include <QTemporaryDir>
#include <QtTest>
#include "configmgr.hpp"
#include "imagemodel.hpp"
#include "imageprovider.hpp"
class TestImageModel : public QObject {
Q_OBJECT
private slots:
void initTestCase();
void testSortName();
void testSortDate();
void testSortSize();
private:
QTemporaryDir m_tempDir;
QString m_pathA;
QString m_pathB;
QString m_pathC;
void createTestFiles();
void waitForModel(ImageModel* model);
};
// clang-format off
// xxd <file> -i
static const unsigned char smallGIF[] = {
0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0xf0, 0x00,
0x00, 0xcd, 0xcf, 0xd2, 0x00, 0x00, 0x00, 0x21, 0xf9, 0x04, 0x00, 0x00,
0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00,
0x00, 0x02, 0x02, 0x44, 0x01, 0x00, 0x3b
};
static const unsigned char mediumGIF[] = {
0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x02, 0x00, 0x02, 0x00, 0xf1, 0x00,
0x00, 0xb0, 0xb8, 0xc0, 0xb7, 0xbc, 0xc2, 0xd8, 0xdb, 0xda, 0xe2, 0xdd,
0xdb, 0x21, 0xf9, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00,
0x00, 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, 0x02, 0x03, 0xd4, 0x10, 0x05,
0x00, 0x3b
};
static const unsigned char largeGIF[] = {
0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x03, 0x00, 0x03, 0x00, 0xf3, 0x00,
0x00, 0x80, 0x8b, 0x9c, 0xa9, 0xad, 0xac, 0xcf, 0xd5, 0xd6, 0xc9, 0xd2,
0xdc, 0xde, 0xd7, 0xd8, 0xdf, 0xdf, 0xdf, 0xd3, 0xda, 0xe0, 0xe9, 0xea,
0xeb, 0xf8, 0xf0, 0xee, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x21, 0xf9, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00,
0x00, 0x00, 0x03, 0x00, 0x03, 0x00, 0x00, 0x04, 0x07, 0xf0, 0x14, 0x24,
0x02, 0x19, 0xc0, 0x44, 0x00, 0x3b
};
// clang-format on
void TestImageModel::initTestCase() {
createTestFiles();
}
void TestImageModel::createTestFiles() {
QVERIFY(m_tempDir.isValid());
// Create files with specific names, sizes, dates
// a.gif: medium size, medium date
// c.gif: small size, old date
// b.gif: big size, new date
// Note: Names are a.gif, b.gif, c.gif for name sort.
m_pathA = m_tempDir.path() + "/a.gif";
m_pathB = m_tempDir.path() + "/b.gif";
m_pathC = m_tempDir.path() + "/c.gif";
{
QFile f(m_pathA);
QVERIFY(f.open(QIODevice::WriteOnly));
f.write(reinterpret_cast<const char*>(mediumGIF), sizeof(mediumGIF));
f.close();
}
{
QFile f(m_pathB);
QVERIFY(f.open(QIODevice::WriteOnly));
f.write(reinterpret_cast<const char*>(largeGIF), sizeof(largeGIF));
f.close();
}
{
QFile f(m_pathC);
QVERIFY(f.open(QIODevice::WriteOnly));
f.write(reinterpret_cast<const char*>(smallGIF), sizeof(smallGIF));
f.close();
}
// Set times
QDateTime now = QDateTime::currentDateTime();
QDateTime timeOld = now.addDays(-10);
QDateTime timeMid = now.addDays(-5);
QDateTime timeNew = now;
{
QFile f(m_pathC);
QVERIFY(f.open(QIODevice::ReadWrite));
QVERIFY(f.setFileTime(timeOld, QFileDevice::FileModificationTime));
}
{
QFile f(m_pathA);
QVERIFY(f.open(QIODevice::ReadWrite));
QVERIFY(f.setFileTime(timeMid, QFileDevice::FileModificationTime));
}
{
QFile f(m_pathB);
QVERIFY(f.open(QIODevice::ReadWrite));
QVERIFY(f.setFileTime(timeNew, QFileDevice::FileModificationTime));
}
}
void TestImageModel::waitForModel(ImageModel* model) {
if (!model->isLoading()) {
return;
}
QSignalSpy spy(model, &ImageModel::isLoadingChanged);
while (model->isLoading()) {
if (!spy.wait(5000)) {
qWarning() << "Timeout waiting for model to load";
break;
}
}
}
void TestImageModel::testSortName() {
Config::SortConfigItems sortConfig;
sortConfig.type = Config::SortType::Name;
sortConfig.reverse = false;
ImageProvider provider;
ImageModel model(provider, sortConfig, QSize(100, 100));
QStringList paths = {m_pathB, m_pathA, m_pathC}; // Unordered input
model.loadAndProcess(paths);
waitForModel(&model);
QCOMPARE(model.rowCount(), 3);
// Expected: a.gif, b.gif, c.gif
QCOMPARE(model.data(model.index(0), ImageModel::NameRole).toString(), "a.gif");
QCOMPARE(model.data(model.index(1), ImageModel::NameRole).toString(), "b.gif");
QCOMPARE(model.data(model.index(2), ImageModel::NameRole).toString(), "c.gif");
// Reverse
sortConfig.reverse = true;
model.sortUpdate();
QCOMPARE(model.rowCount(), 3);
// Expected: c.gif, b.gif, a.gif
QCOMPARE(model.data(model.index(0), ImageModel::NameRole).toString(), "c.gif");
QCOMPARE(model.data(model.index(1), ImageModel::NameRole).toString(), "b.gif");
QCOMPARE(model.data(model.index(2), ImageModel::NameRole).toString(), "a.gif");
}
void TestImageModel::testSortDate() {
Config::SortConfigItems sortConfig;
sortConfig.type = Config::SortType::Date;
sortConfig.reverse = false;
ImageProvider provider;
ImageModel model(provider, sortConfig, QSize(100, 100));
QStringList paths = {m_pathA, m_pathC, m_pathB};
model.loadAndProcess(paths);
waitForModel(&model);
QCOMPARE(model.rowCount(), 3);
// Expected: c (old), a (mid), b (new)
QCOMPARE(model.data(model.index(0), ImageModel::NameRole).toString(), "c.gif");
QCOMPARE(model.data(model.index(1), ImageModel::NameRole).toString(), "a.gif");
QCOMPARE(model.data(model.index(2), ImageModel::NameRole).toString(), "b.gif");
// Reverse (Newest first)
sortConfig.reverse = true;
model.sortUpdate();
QCOMPARE(model.data(model.index(0), ImageModel::NameRole).toString(), "b.gif");
QCOMPARE(model.data(model.index(1), ImageModel::NameRole).toString(), "a.gif");
QCOMPARE(model.data(model.index(2), ImageModel::NameRole).toString(), "c.gif");
}
void TestImageModel::testSortSize() {
Config::SortConfigItems sortConfig;
sortConfig.type = Config::SortType::Size;
sortConfig.reverse = false;
ImageProvider provider;
ImageModel model(provider, sortConfig, QSize(100, 100));
QStringList paths = {m_pathB, m_pathC, m_pathA};
model.loadAndProcess(paths);
waitForModel(&model);
QCOMPARE(model.rowCount(), 3);
QCOMPARE(model.data(model.index(0), ImageModel::NameRole).toString(), "c.gif");
QCOMPARE(model.data(model.index(1), ImageModel::NameRole).toString(), "a.gif");
QCOMPARE(model.data(model.index(2), ImageModel::NameRole).toString(), "b.gif");
// Reverse
sortConfig.reverse = true;
model.sortUpdate();
QCOMPARE(model.data(model.index(0), ImageModel::NameRole).toString(), "b.gif");
QCOMPARE(model.data(model.index(1), ImageModel::NameRole).toString(), "a.gif");
QCOMPARE(model.data(model.index(2), ImageModel::NameRole).toString(), "c.gif");
}
QTEST_MAIN(TestImageModel)
#include "tst_imagemodel.moc"
+3
View File
@@ -8,6 +8,9 @@ qt_add_qml_module(${CORELIB_NAME}
configmgr.hpp configmgr.cpp configmgr.hpp configmgr.cpp
imagemodel.hpp imagemodel.cpp imagemodel.hpp imagemodel.cpp
imageprovider.hpp imageprovider.cpp imageprovider.hpp imageprovider.cpp
wallpaperservice.hpp wallpaperservice.cpp
palette/data.hpp
palette/manager.hpp palette/manager.cpp
) )
target_link_libraries(${CORELIB_NAME} PRIVATE target_link_libraries(${CORELIB_NAME} PRIVATE
+245 -113
View File
@@ -28,7 +28,9 @@ Config::Config(
} }
if (!searchDirs.isEmpty()) { if (!searchDirs.isEmpty()) {
info(QString("Additional search directories: %1").arg(searchDirs.join(", "))); info(QString("Additional search directories: %1").arg(searchDirs.join(", ")));
m_wallpaperConfig.dirs.append(searchDirs); for (const auto& dir : searchDirs) {
m_wallpaperConfig.dirs.append({dir, false});
}
} }
debug("Loading wallpapers ..."); debug("Loading wallpapers ...");
@@ -56,117 +58,227 @@ void Config::_loadConfig(const QString& configPath) {
const auto jsonObj = jsonDoc.object(); const auto jsonObj = jsonDoc.object();
struct ConfigMapping { _loadWallpaperConfig(jsonObj);
QString path; _loadPaletteConfig(jsonObj);
QString key; _loadActionConfig(jsonObj);
std::function<void(const QJsonValue&)> parser; _loadStyleConfig(jsonObj);
}; _loadSortConfig(jsonObj);
}
static const auto parseJsonArray = [](const QJsonValue& val, QStringList& list) { void Config::_loadWallpaperConfig(const QJsonObject& root) {
if (val.isArray()) { if (!root.contains("wallpaper") || !root["wallpaper"].isObject()) {
for (const auto& item : val.toArray()) { return;
if (item.isString()) { }
list.append(::expandPath(item.toString())); const QJsonObject& config = root["wallpaper"].toObject();
if (config.contains("paths") && config["paths"].isArray()) {
for (const auto& item : config["paths"].toArray()) {
if (item.isString()) {
m_wallpaperConfig.paths.append(::expandPath(item.toString()));
}
}
}
if (config.contains("dirs") && config["dirs"].isArray()) {
for (const auto& item : config["dirs"].toArray()) {
if (item.isObject()) {
QJsonObject obj = item.toObject();
if (obj.contains("path") && obj["path"].isString()) {
WallpaperConfigItems::WallpaperDirConfigItem dirConfig;
dirConfig.path = ::expandPath(obj["path"].toString());
if (obj.contains("recursive") && obj["recursive"].isBool()) {
dirConfig.recursive = obj["recursive"].toBool();
} else {
dirConfig.recursive = false;
}
m_wallpaperConfig.dirs.append(dirConfig);
} }
} }
} }
}; }
std::vector<ConfigMapping> if (config.contains("excludes") && config["excludes"].isArray()) {
mappings = { for (const auto& item : config["excludes"].toArray()) {
{"wallpaper.paths", "paths", [this](const QJsonValue& val) { if (item.isString()) {
parseJsonArray(val, m_wallpaperConfig.paths); auto regex = QRegularExpression(item.toString());
}}, if (!regex.isValid()) {
{"wallpaper.dirs", "dirs", [this](const QJsonValue& val) { warn(QString("Invalid regular expression in config: %1").arg(item.toString()));
parseJsonArray(val, m_wallpaperConfig.dirs);
}},
{"wallpaper.excludes", "excludes", [this](const QJsonValue& val) {
parseJsonArray(val, m_wallpaperConfig.excludes);
}},
{"action.confirm", "confirm", [this](const QJsonValue& val) {
if (val.isString()) {
m_actionConfig.confirm = ::expandPath(val.toString());
debug(QString("Action confirm: %1").arg(m_actionConfig.confirm));
}
}},
{"style.aspect_ratio", "aspect_ratio", [this](const QJsonValue& val) {
if (val.isDouble() && val.toDouble() > 0) {
m_styleConfig.aspectRatio = val.toDouble();
debug(QString("Aspect ratio: %1").arg(m_styleConfig.aspectRatio));
}
}},
{"style.image_width", "image_width", [this](const QJsonValue& val) {
if (val.isDouble() && val.toDouble() > 0) {
m_styleConfig.imageWidth = val.toInt();
debug(QString("Image width: %1").arg(m_styleConfig.imageWidth));
}
}},
{"style.image_focus_width", "image_focus_width", [this](const QJsonValue& val) {
if (val.isDouble() && val.toDouble() > 0) {
m_styleConfig.imageFocusWidth = val.toInt();
debug(QString("Image focus width: %1").arg(m_styleConfig.imageFocusWidth));
}
}},
{"style.window_width", "window_width", [this](const QJsonValue& val) {
if (val.isDouble() && val.toDouble() > 0) {
m_styleConfig.windowWidth = val.toInt();
debug(QString("Window width: %1").arg(m_styleConfig.windowWidth));
}
}},
{"style.window_height", "window_height", [this](const QJsonValue& val) {
if (val.isDouble() && val.toDouble() > 0) {
m_styleConfig.windowHeight = val.toInt();
debug(QString("Window height: %1").arg(m_styleConfig.windowHeight));
}
}},
{"sort.type", "type", [this](const QJsonValue& val) {
if (val.isString()) {
QString type = val.toString().toLower();
if (type == "none") {
m_sortConfig.type = SortType::None;
} else if (type == "name") {
m_sortConfig.type = SortType::Name;
} else if (type == "date") {
m_sortConfig.type = SortType::Date;
} else if (type == "size") {
m_sortConfig.type = SortType::Size;
} else {
warn(QString("Unknown sort type: %1").arg(type));
}
}
debug(QString("Sort type: %1").arg(static_cast<int>(m_sortConfig.type)));
}},
{"sort.reverse", "reverse", [this](const QJsonValue& val) {
if (val.isBool()) {
m_sortConfig.reverse = val.toBool();
debug(QString("Sort reverse: %1").arg(m_sortConfig.reverse));
}
}},
};
for (const auto& mapping : mappings) {
([&mapping, &jsonObj]() {
auto pathParts = mapping.path.split('.');
QJsonObject currentObj = jsonObj;
QJsonValue targetValue;
for (int i = 0; i < pathParts.size() - 1; ++i) {
if (currentObj.contains(pathParts[i]) && currentObj[pathParts[i]].isObject()) {
currentObj = currentObj[pathParts[i]].toObject();
} else { } else {
debug(QString("Path '%1' not found").arg(pathParts.mid(0, i + 1).join('.'))); m_wallpaperConfig.excludes.append(regex);
return;
} }
} }
}
}
}
const QString& finalKey = pathParts.last(); void Config::_loadPaletteConfig(const QJsonObject& root) {
if (currentObj.contains(finalKey)) { if (!root.contains("palettes") || !root["palettes"].isArray()) {
mapping.parser(currentObj[finalKey]); return;
} else { }
debug(QString("Key '%1' not found in '%2'").arg(finalKey).arg(mapping.path)); const QJsonArray& palettes = root["palettes"].toArray();
for (const auto& palItem : palettes) {
if (palItem.isObject()) {
QJsonObject palObj = palItem.toObject();
PaletteConfigItems::PaletteConfigItem palette;
if (palObj.contains("name") && palObj["name"].isString()) {
palette.name = palObj["name"].toString();
} }
})(); if (palObj.contains("colors") && palObj["colors"].isArray()) {
for (const auto& colorItem : palObj["colors"].toArray()) {
PaletteConfigItems::PaletteColorConfigItem colorConfig;
if (colorItem.isObject()) {
QJsonObject colorObj = colorItem.toObject();
if (colorObj.contains("name") && colorObj["name"].isString()) {
colorConfig.name = colorObj["name"].toString();
}
if (colorObj.contains("value") && colorObj["value"].isString()) {
QColor color(colorObj["value"].toString());
if (color.isValid()) {
colorConfig.value = color;
} else {
warn(QString("Invalid color string in config: %1").arg(colorObj["value"].toString()));
}
}
} else if (colorItem.isString()) {
QColor color(colorItem.toString());
if (color.isValid()) {
colorConfig.value = color;
} else {
warn(QString("Invalid color string in config: %1").arg(colorItem.toString()));
}
}
if (colorConfig.value.isValid()) {
palette.colors.append(colorConfig);
}
}
}
m_paletteConfig.palettes.append(palette);
}
}
}
void Config::_loadActionConfig(const QJsonObject& root) {
if (!root.contains("action") || !root["action"].isObject()) {
return;
}
const QJsonObject& config = root["action"].toObject();
if (config.contains("previewDebounceTime")) {
const auto& val = config["previewDebounceTime"];
if (val.isDouble() && val.toDouble() >= 0) {
m_actionConfig.previewDebounceTime = val.toInt();
}
}
if (config.contains("printSelected")) {
const auto& val = config["printSelected"];
if (val.isBool()) {
m_actionConfig.printSelected = val.toBool();
}
}
if (config.contains("printPreview")) {
const auto& val = config["printPreview"];
if (val.isBool()) {
m_actionConfig.printPreview = val.toBool();
}
}
if (config.contains("saveState")) {
const auto& val = config["saveState"];
if (val.isObject()) {
QJsonObject obj = val.toObject();
for (const auto& key : obj.keys()) {
if (obj[key].isString()) {
m_actionConfig.saveState.insert(key, obj[key].toString());
}
}
}
}
if (config.contains("onRestore")) {
const auto& val = config["onRestore"];
if (val.isString()) {
m_actionConfig.onRestore = val.toString();
}
}
if (config.contains("onSelected")) {
const auto& val = config["onSelected"];
if (val.isString()) {
m_actionConfig.onSelected = val.toString();
}
}
if (config.contains("onPreview")) {
const auto& val = config["onPreview"];
if (val.isString()) {
m_actionConfig.onPreview = val.toString();
}
}
}
void Config::_loadStyleConfig(const QJsonObject& root) {
if (!root.contains("style") || !root["style"].isObject()) {
return;
}
const QJsonObject& config = root["style"].toObject();
if (config.contains("image_width")) {
const auto& val = config["image_width"];
if (val.isDouble() && val.toDouble() > 0) {
m_styleConfig.imageWidth = val.toInt();
}
}
if (config.contains("image_height")) {
const auto& val = config["image_height"];
if (val.isDouble() && val.toDouble() > 0) {
m_styleConfig.imageHeight = val.toInt();
}
}
if (config.contains("image_focus_scale")) {
const auto& val = config["image_focus_scale"];
if (val.isDouble() && val.toDouble() > 0) {
m_styleConfig.imageFocusScale = val.toDouble();
}
}
if (config.contains("window_width")) {
const auto& val = config["window_width"];
if (val.isDouble() && val.toDouble() > 0) {
m_styleConfig.windowWidth = val.toInt();
}
}
if (config.contains("window_height")) {
const auto& val = config["window_height"];
if (val.isDouble() && val.toDouble() > 0) {
m_styleConfig.windowHeight = val.toInt();
}
}
}
void Config::_loadSortConfig(const QJsonObject& root) {
if (!root.contains("sort") || !root["sort"].isObject()) {
return;
}
const QJsonObject& config = root["sort"].toObject();
if (config.contains("type")) {
const auto& val = config["type"];
if (val.isString()) {
QString type = val.toString().toLower();
if (type == "none") {
m_sortConfig.type = SortType::None;
} else if (type == "name") {
m_sortConfig.type = SortType::Name;
} else if (type == "date") {
m_sortConfig.type = SortType::Date;
} else if (type == "size") {
m_sortConfig.type = SortType::Size;
} else {
warn(QString("Unknown sort type: %1").arg(type));
}
}
}
if (config.contains("reverse")) {
const auto& val = config["reverse"];
if (val.isBool()) {
m_sortConfig.reverse = val.toBool();
}
} }
} }
@@ -181,22 +293,42 @@ void Config::_loadWallpapers() {
} }
debug(QString("Loading wallpapers from %1 specified directories...").arg(m_wallpaperConfig.dirs.size())); debug(QString("Loading wallpapers from %1 specified directories...").arg(m_wallpaperConfig.dirs.size()));
for (const QString& dirPath : std::as_const(m_wallpaperConfig.dirs)) { for (const auto& dirConfig : std::as_const(m_wallpaperConfig.dirs)) {
QDir dir(dirPath); if (checkDir(dirConfig.path)) {
if (checkDir(dirPath)) { std::function<void(const QDir&)> scanDir;
QStringList files = dir.entryList(QDir::Files | QDir::NoDotAndDotDot); scanDir = [&](const QDir& d) {
for (const QString& file : std::as_const(files)) { QStringList files = d.entryList(QDir::Files | QDir::NoDotAndDotDot);
QString filePath = dir.filePath(file); for (const QString& file : std::as_const(files)) {
paths.insert(expandPath(filePath)); QString filePath = d.filePath(file);
} paths.insert(expandPath(filePath));
}
if (dirConfig.recursive) {
QStringList subDirs = d.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
debug(QString("Scanning directory '%1' for subdirectories... Found %2").arg(d.absolutePath()).arg(subDirs.size()));
for (const QString& subDir : std::as_const(subDirs)) {
scanDir(QDir(d.filePath(subDir)));
}
}
};
scanDir(QDir(dirConfig.path));
} else { } else {
warn(QString("Directory '%1' does not exist").arg(dirPath)); warn(QString("Directory '%1' does not exist").arg(dirConfig.path));
} }
} }
debug(QString("Excluding %1 specified paths...").arg(m_wallpaperConfig.excludes.size())); debug(QString("Excluding %1 specified paths...").arg(m_wallpaperConfig.excludes.size()));
for (const QString& exclude : std::as_const(m_wallpaperConfig.excludes)) { QStringList toRemove;
paths.remove(exclude); for (const auto& exclude : std::as_const(m_wallpaperConfig.excludes)) {
for (const QString& path : std::as_const(paths)) {
if (exclude.match(path).hasMatch()) {
toRemove.append(path);
debug(QString("Excluded path '%1' matched by regex '%2'").arg(path).arg(exclude.pattern()));
}
}
}
for (const auto& path : toRemove) {
paths.remove(path);
} }
m_wallpapers.reserve(paths.size()); m_wallpapers.reserve(paths.size());
+84 -34
View File
@@ -1,37 +1,55 @@
#ifndef WALLREEL_CONFIGMGR_HPP #ifndef WALLREEL_CONFIGMGR_HPP
#define WALLREEL_CONFIGMGR_HPP #define WALLREEL_CONFIGMGR_HPP
#include <qregularexpression.h>
#include <QColor>
#include <QObject> #include <QObject>
#include <QQmlEngine> #include <QRegularExpression>
#include <QSize> #include <QSize>
#include <QString> #include <QString>
#include <QStringList> #include <QStringList>
// Config entries: // Config entries:
// //
// wallpaper.paths array image paths // wallpaper.paths array [] List of paths to images.
// wallpaper.dirs array directories to search for images. // wallpaper.dirs array [] Directories to search for images.
// all images in these directories will be added. // wallpaper.dirs[].path string "" Path to the directory.
// NOT recursive. // wallpaper.dirs[].recursive boolean false Whether to search the directory recursively.
// wallpaper.excludes array exclude patterns // wallpaper.excludes array [] Exclude patterns (regex)
// //
// action.confirm string command to execute on confirm // palettes array []
// palettes[].name string "" Name of the palette
// palettes[].colors array [] List of colors in the palette
// palettes[].colors[].name string "" Name of the color
// palettes[].colors[].value string "" Color value in hex format, e.g. "#ff0000" for red
// //
// style.aspect_ratio number (width / height) of each image // action.previewDebounceTime number 300 Minimum debounce time for preview action in milliseconds
// style.image_width number width of each image // action.printSelected boolean false Whether to print the selected wallpaper path to stdout on confirm
// style.image_focus_width number width of focused image // action.printPreview boolean false Whether to print the previewed wallpaper path to stdout on preview
// style.window_width number fixed window width // action.saveState object {} Key-value pairs to save the state, useful for restore command
// style.window_height number fixed window height // action.onRestore string "" Command to execute on restore ({{ key }} -> value in saveState)
// action.onSelected string "" Command to execute on confirmation ({{ path }} -> full path)
// action.onPreview string "" Command to execute on preview ({{ path }} -> full path)
// //
// sort.type string sorting type: "none", "name", "date", "size" // style.image_width number 320 Width of each image
// sort.reverse boolean whether to reverse the sorting order // style.image_height number 200 Height of each image
// style.image_focus_scale number 1.5 Scale of the focused image (relative to unfocused image)
// style.window_width number 750 Initial window width
// style.window_height number 500 Initial window height
//
// sort.type string "name" Sorting type: "none", "name", "date", "size"
// sort.reverse boolean false Whether to reverse the sorting order
// Normal order: name: lexicographical, e.g. "a.jpg" before "b.jpg"
// date: older before newer
// size: smaller before larger
class Config : public QObject { class Config : public QObject {
Q_OBJECT Q_OBJECT
Q_PROPERTY(double aspectRatio READ getAspectRatio CONSTANT)
Q_PROPERTY(int imageWidth READ getImageWidth CONSTANT) Q_PROPERTY(int imageWidth READ getImageWidth CONSTANT)
Q_PROPERTY(int imageFocusWidth READ getImageFocusWidth CONSTANT) Q_PROPERTY(int imageHeight READ getImageHeight CONSTANT)
Q_PROPERTY(double imageFocusScale READ getImageFocusScale CONSTANT)
Q_PROPERTY(int windowWidth READ getWindowWidth CONSTANT) Q_PROPERTY(int windowWidth READ getWindowWidth CONSTANT)
Q_PROPERTY(int windowHeight READ getWindowHeight CONSTANT) Q_PROPERTY(int windowHeight READ getWindowHeight CONSTANT)
@@ -44,30 +62,56 @@ class Config : public QObject {
}; };
struct WallpaperConfigItems { struct WallpaperConfigItems {
QStringList paths; // "wallpaper.paths" struct WallpaperDirConfigItem {
QStringList dirs; // "wallpaper.dirs" QString path;
QStringList excludes; // "wallpaper.excludes" bool recursive;
};
QStringList paths;
QList<WallpaperDirConfigItem> dirs;
QList<QRegularExpression> excludes;
};
struct PaletteConfigItems {
struct PaletteColorConfigItem {
QString name;
QColor value;
};
struct PaletteConfigItem {
QString name;
QList<PaletteColorConfigItem> colors;
};
QList<PaletteConfigItem> palettes;
}; };
struct ActionConfigItems { struct ActionConfigItems {
QString confirm; // "action.confirm"
QHash<QString, QString> saveState;
QString onSelected;
QString onPreview;
QString onRestore;
int previewDebounceTime = 300; // milliseconds
bool printSelected = false;
bool printPreview = false;
}; };
struct StyleConfigItems { struct StyleConfigItems {
double aspectRatio = 1.6; // "style.aspect_ratio" double imageFocusScale = 1.5;
int imageWidth = 320; // "style.image_width" int imageWidth = 320;
int imageFocusWidth = 480; // "style.image_focus_width" int imageHeight = 200;
int windowWidth = 750; // "style.window_width" int windowWidth = 750;
int windowHeight = 500; // "style.window_height" int windowHeight = 500;
}; };
struct SortConfigItems { struct SortConfigItems {
SortType type = SortType::Name; // "sort.type" SortType type = SortType::Name;
bool reverse = false; // "sort.reverse" bool reverse = false;
}; };
Config( Config(
const QString& configDir, // Fixed, usually "~/.config/wallpaper-carousel" const QString& configDir,
const QStringList& searchDirs = {}, const QStringList& searchDirs = {},
const QString& configPath = "", // Override the default config path const QString& configPath = "", // Override the default config path
QObject* parent = nullptr); QObject* parent = nullptr);
@@ -80,26 +124,26 @@ class Config : public QObject {
const WallpaperConfigItems& getWallpaperConfig() const { return m_wallpaperConfig; } const WallpaperConfigItems& getWallpaperConfig() const { return m_wallpaperConfig; }
const PaletteConfigItems& getPaletteConfig() const { return m_paletteConfig; }
const ActionConfigItems& getActionConfig() const { return m_actionConfig; } const ActionConfigItems& getActionConfig() const { return m_actionConfig; }
const StyleConfigItems& getStyleConfig() const { return m_styleConfig; } const StyleConfigItems& getStyleConfig() const { return m_styleConfig; }
const SortConfigItems& getSortConfig() const { return m_sortConfig; } const SortConfigItems& getSortConfig() const { return m_sortConfig; }
double getAspectRatio() const { return m_styleConfig.aspectRatio; }
int getImageWidth() const { return m_styleConfig.imageWidth; } int getImageWidth() const { return m_styleConfig.imageWidth; }
int getImageFocusWidth() const { return m_styleConfig.imageFocusWidth; } int getImageHeight() const { return m_styleConfig.imageHeight; }
double getImageFocusScale() const { return m_styleConfig.imageFocusScale; }
int getWindowWidth() const { return m_styleConfig.windowWidth; } int getWindowWidth() const { return m_styleConfig.windowWidth; }
int getWindowHeight() const { return m_styleConfig.windowHeight; } int getWindowHeight() const { return m_styleConfig.windowHeight; }
QSize getFocusImageSize() const { QSize getFocusImageSize() const {
int width = m_styleConfig.imageFocusWidth; return QSize{m_styleConfig.imageWidth, m_styleConfig.imageHeight} * m_styleConfig.imageFocusScale;
int height = static_cast<int>(width / m_styleConfig.aspectRatio);
return {width, height};
} }
static const QString s_DefaultConfigFileName; static const QString s_DefaultConfigFileName;
@@ -108,9 +152,15 @@ class Config : public QObject {
private: private:
void _loadConfig(const QString& configPath); void _loadConfig(const QString& configPath);
void _loadWallpapers(); void _loadWallpapers();
void _loadWallpaperConfig(const QJsonObject& config);
void _loadPaletteConfig(const QJsonObject& config);
void _loadActionConfig(const QJsonObject& config);
void _loadStyleConfig(const QJsonObject& config);
void _loadSortConfig(const QJsonObject& config);
private: private:
WallpaperConfigItems m_wallpaperConfig; WallpaperConfigItems m_wallpaperConfig;
PaletteConfigItems m_paletteConfig;
ActionConfigItems m_actionConfig; ActionConfigItems m_actionConfig;
StyleConfigItems m_styleConfig; StyleConfigItems m_styleConfig;
SortConfigItems m_sortConfig; SortConfigItems m_sortConfig;
+11 -11
View File
@@ -16,7 +16,7 @@ ImageData* ImageData::create(const QString& path, const QSize& size) {
} }
ImageData::ImageData(const QString& path, const QSize& targetSize) ImageData::ImageData(const QString& path, const QSize& targetSize)
: file(path) { : m_file(path) {
QImageReader reader(path); QImageReader reader(path);
if (!reader.canRead()) { if (!reader.canRead()) {
warn(QString("Failed to load image from path: %1").arg(path)); warn(QString("Failed to load image from path: %1").arg(path));
@@ -38,27 +38,27 @@ ImageData::ImageData(const QString& path, const QSize& targetSize)
} }
} }
if (!reader.read(&image)) { if (!reader.read(&m_image)) {
warn(QString("Failed to load image from path: %1").arg(path)); warn(QString("Failed to load image from path: %1").arg(path));
return; return;
} }
if (image.width() > processSize.width() || image.height() > processSize.height()) { if (m_image.width() > processSize.width() || m_image.height() > processSize.height()) {
image = image.scaled(processSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); m_image = m_image.scaled(processSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
} }
// Crop to target size if necessary // Crop to target size if necessary
if (image.size() != targetSize) { if (m_image.size() != targetSize) {
int x = (image.width() - targetSize.width()) / 2; int x = (m_image.width() - targetSize.width()) / 2;
int y = (image.height() - targetSize.height()) / 2; int y = (m_image.height() - targetSize.height()) / 2;
image = image.copy(x, y, targetSize.width(), targetSize.height()); m_image = m_image.copy(x, y, targetSize.width(), targetSize.height());
} }
// Convert to GPU-friendly format // Convert to GPU-friendly format
if (image.format() != QImage::Format_ARGB32_Premultiplied) { if (m_image.format() != QImage::Format_ARGB32_Premultiplied) {
image = image.convertToFormat(QImage::Format_ARGB32_Premultiplied); m_image = m_image.convertToFormat(QImage::Format_ARGB32_Premultiplied);
} }
// Create ID // Create ID
id = QString::number(qHash(file.absoluteFilePath())); m_id = QString::number(qHash(m_file.absoluteFilePath()));
} }
+11 -11
View File
@@ -5,30 +5,30 @@
#include <QImage> #include <QImage>
class ImageData { class ImageData {
QString id; QString m_id;
QFileInfo file; QFileInfo m_file;
QImage image; QImage m_image;
ImageData(const QString& path, const QSize& size); ImageData(const QString& path, const QSize& size);
public: public:
static ImageData* create(const QString& path, const QSize& size); static ImageData* create(const QString& path, const QSize& size);
const QImage& getImage() const { return image; } const QImage& getImage() const { return m_image; }
const QString& getId() const { return id; } const QString& getId() const { return m_id; }
bool isValid() const { return !image.isNull(); } bool isValid() const { return !m_image.isNull(); }
QString getFullPath() const { return file.absoluteFilePath(); } QString getFullPath() const { return m_file.absoluteFilePath(); }
QString getFileName() const { return file.fileName(); } QString getFileName() const { return m_file.fileName(); }
QDateTime getLastModified() const { return file.lastModified(); } QDateTime getLastModified() const { return m_file.lastModified(); }
qint64 getSize() const { return file.size(); } qint64 getSize() const { return m_file.size(); }
const QFileInfo& getFileInfo() const { return file; } const QFileInfo& getFileInfo() const { return m_file; }
private: private:
}; };
+49 -24
View File
@@ -7,7 +7,7 @@
#include "imagedata.hpp" #include "imagedata.hpp"
ImageModel::ImageModel( ImageModel::ImageModel(
ImageProvider* provider, ImageProvider& provider,
const Config::SortConfigItems& sortConfig, const Config::SortConfigItems& sortConfig,
QSize thumbnailSize, QSize thumbnailSize,
QObject* parent) QObject* parent)
@@ -19,7 +19,7 @@ ImageModel::ImageModel(
&m_watcher, &m_watcher,
&QFutureWatcher<ImageData*>::finished, &QFutureWatcher<ImageData*>::finished,
this, this,
&ImageModel::onProcessingFinished); &ImageModel::_onProcessingFinished);
connect( connect(
&m_progressUpdateTimer, &m_progressUpdateTimer,
&QTimer::timeout, &QTimer::timeout,
@@ -33,6 +33,7 @@ ImageModel::~ImageModel() {
m_watcher.cancel(); m_watcher.cancel();
m_watcher.waitForFinished(); m_watcher.waitForFinished();
qDeleteAll(m_data); qDeleteAll(m_data);
m_data.clear();
} }
int ImageModel::rowCount(const QModelIndex& parent) const { int ImageModel::rowCount(const QModelIndex& parent) const {
@@ -67,16 +68,10 @@ void ImageModel::loadAndProcess(const QStringList& paths) {
m_isLoading = true; m_isLoading = true;
emit isLoadingChanged(); emit isLoadingChanged();
beginResetModel(); _clearData();
if (!m_data.isEmpty()) {
qDeleteAll(m_data);
}
m_data.clear();
m_provider->clear();
endResetModel();
m_processedCount = 0; m_processedCount = 0;
m_progressUpdateTimer.start(s_progressUpdateInterval); m_progressUpdateTimer.start(s_ProgressUpdateIntervalMs);
const auto thumbnailSize = m_thumbnailSize; const auto thumbnailSize = m_thumbnailSize;
const auto counterPtr = &m_processedCount; const auto counterPtr = &m_processedCount;
QFuture<ImageData*> future = QtConcurrent::mapped(paths, [thumbnailSize, counterPtr](const QString& path) { QFuture<ImageData*> future = QtConcurrent::mapped(paths, [thumbnailSize, counterPtr](const QString& path) {
@@ -94,12 +89,12 @@ void ImageModel::stop() {
} }
} }
void ImageModel::onProgressValueChanged(int value) { void ImageModel::_onProgressValueChanged(int value) {
Q_UNUSED(value); Q_UNUSED(value);
emit progressChanged(); emit progressChanged();
} }
void ImageModel::onProcessingFinished() { void ImageModel::_onProcessingFinished() {
auto results = m_watcher.future().results(); auto results = m_watcher.future().results();
for (auto& data : results) { for (auto& data : results) {
if (data && data->isValid()) { if (data && data->isValid()) {
@@ -116,7 +111,7 @@ void ImageModel::onProcessingFinished() {
m_progressUpdateTimer.stop(); m_progressUpdateTimer.stop();
emit progressChanged(); emit progressChanged();
// emit isLoadingChanged(); // emit isLoadingChanged();
QTimer::singleShot(s_isLoadingUpdateInterval, this, [this]() { QTimer::singleShot(s_IsLoadingUpdateIntervalMs, this, [this]() {
emit isLoadingChanged(); emit isLoadingChanged();
}); });
} }
@@ -128,27 +123,29 @@ void ImageModel::sortUpdate() {
if (!a || !b) { if (!a || !b) {
return false; return false;
} }
bool result = false; if (a == b) {
return false;
}
ImageData* first = reverse ? b : a;
ImageData* second = reverse ? a : b;
switch (type) { switch (type) {
case Config::SortType::Name: case Config::SortType::Name:
result = QString::compare(a->getFileName(), b->getFileName(), Qt::CaseInsensitive) < 0; return QString::compare(first->getFileName(), second->getFileName(), Qt::CaseInsensitive) < 0;
break;
case Config::SortType::Date: case Config::SortType::Date:
result = a->getLastModified() < b->getLastModified(); return first->getLastModified() < second->getLastModified();
break;
case Config::SortType::Size: case Config::SortType::Size:
result = a->getSize() < b->getSize(); return first->getSize() < second->getSize();
break;
default: default:
break; return false;
} }
return reverse ? !result : result;
}); });
beginResetModel(); beginResetModel();
m_provider->clear(); m_provider.clear();
for (const auto& item : m_data) { for (const auto& item : m_data) {
m_provider->insert(item); m_provider.insert(item);
} }
endResetModel(); endResetModel();
} }
@@ -169,3 +166,31 @@ QVariant ImageModel::dataAt(int index, const QString& roleName) const {
return QVariant(); return QVariant();
} }
} }
void ImageModel::_clearData() {
beginResetModel();
m_provider.clear();
qDeleteAll(m_data);
m_data.clear();
endResetModel();
}
void ImageModel::selectImage(int index) {
if (index < 0 || index >= m_data.count()) {
return;
}
const auto& item = m_data[index];
if (item) {
emit imageSelected(*item);
}
}
void ImageModel::previewImage(int index) {
if (index < 0 || index >= m_data.count()) {
return;
}
const auto& item = m_data[index];
if (item) {
emit imagePreviewed(*item);
}
}
+16 -8
View File
@@ -32,7 +32,7 @@ class ImageModel : public QAbstractListModel {
} }
ImageModel( ImageModel(
ImageProvider* provider, ImageProvider& provider,
const Config::SortConfigItems& sortConfig, const Config::SortConfigItems& sortConfig,
QSize thumbnailSize, QSize thumbnailSize,
QObject* parent = nullptr); QObject* parent = nullptr);
@@ -57,30 +57,38 @@ class ImageModel : public QAbstractListModel {
Q_INVOKABLE QVariant dataAt(int index, const QString& roleName) const; Q_INVOKABLE QVariant dataAt(int index, const QString& roleName) const;
Q_INVOKABLE void selectImage(int index);
Q_INVOKABLE void previewImage(int index);
private:
void _clearData();
signals: signals:
void isLoadingChanged(); void isLoadingChanged();
void progressChanged(); void progressChanged();
void totalCountChanged(); void totalCountChanged();
void imageSelected(const QString& path); void imageSelected(const ImageData& imageData);
void imagePreviewed(const ImageData& imageData);
private slots: private slots:
void onProgressValueChanged(int value); void _onProgressValueChanged(int value);
void onProcessingFinished(); void _onProcessingFinished();
private: private:
ImageProvider* m_provider; ImageProvider& m_provider;
const Config::SortConfigItems& m_sortConfig; const Config::SortConfigItems& m_sortConfig;
QSize m_thumbnailSize; QSize m_thumbnailSize;
QList<ImageData*> m_data; QVector<ImageData*> m_data;
QFutureWatcher<ImageData*> m_watcher; QFutureWatcher<ImageData*> m_watcher;
bool m_isLoading = false; bool m_isLoading = false;
std::atomic<int> m_processedCount{0}; std::atomic<int> m_processedCount{0};
QTimer m_progressUpdateTimer; QTimer m_progressUpdateTimer;
static constexpr int s_progressUpdateInterval = 30; static constexpr int s_ProgressUpdateIntervalMs = 30;
static constexpr int s_isLoadingUpdateInterval = 50; static constexpr int s_IsLoadingUpdateIntervalMs = 50;
}; };
#endif // WALLREEL_IMAGEMODEL_HPP #endif // WALLREEL_IMAGEMODEL_HPP
+43
View File
@@ -0,0 +1,43 @@
#ifndef WALLREEL_PALETTE_DATA_HPP
#define WALLREEL_PALETTE_DATA_HPP
#include <QColor>
#include <QList>
#include <QObject>
#include <QString>
struct ColorItem {
Q_GADGET
Q_PROPERTY(QString name MEMBER name CONSTANT)
Q_PROPERTY(QColor color MEMBER color CONSTANT)
public:
QString name;
QColor color;
bool operator==(const ColorItem& other) const {
return name == other.name;
}
};
struct PaletteItem {
Q_GADGET
Q_PROPERTY(QString name MEMBER name CONSTANT)
Q_PROPERTY(QList<ColorItem> colors MEMBER colors CONSTANT)
public:
QString name;
QList<ColorItem> colors;
Q_INVOKABLE QColor getColor(const QString& colorName) const {
for (const auto& entry : colors) {
if (entry.name == colorName) return entry.color;
}
return QColor();
}
};
Q_DECLARE_METATYPE(ColorItem)
Q_DECLARE_METATYPE(PaletteItem)
#endif // WALLREEL_PALETTE_DATA_HPP
+42
View File
@@ -0,0 +1,42 @@
#include "manager.hpp"
#include "predefined.hpp"
PaletteManager::PaletteManager(
const Config::PaletteConfigItems& config,
QObject* parent) : QObject(parent) {
// 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
QHash<QString, int> lastSeen;
lastSeen.reserve(preDefinedPalettes.size() + config.palettes.size());
for (int i = 0; i < preDefinedPalettes.size(); i++) {
lastSeen[preDefinedPalettes[i].name] = i;
}
for (int i = 0; i < config.palettes.size(); i++) {
lastSeen[config.palettes[i].name] = preDefinedPalettes.size() + i;
}
m_palettes.reserve(lastSeen.size());
for (int i = 0; i < preDefinedPalettes.size(); i++) {
const auto& p = preDefinedPalettes[i];
if (lastSeen[p.name] == i) {
m_palettes.append({p.name, p.colors});
}
}
for (int i = 0; i < config.palettes.size(); i++) {
const auto& p = config.palettes[i];
if (lastSeen[p.name] == preDefinedPalettes.size() + i) {
auto newP = PaletteItem{p.name, {}};
newP.colors.reserve(p.colors.size());
for (const auto& c : p.colors) {
if (!c.value.isValid()) {
continue;
}
newP.colors.append({c.name, c.value});
}
m_palettes.append(newP);
}
}
}
+30
View File
@@ -0,0 +1,30 @@
#ifndef WALLREEL_PALETTE_MANAGER_HPP
#define WALLREEL_PALETTE_MANAGER_HPP
#include "../configmgr.hpp"
#include "data.hpp"
class PaletteManager : public QObject {
Q_OBJECT
Q_PROPERTY(QList<PaletteItem> availablePalettes READ availablePalettes CONSTANT)
public:
PaletteManager(const Config::PaletteConfigItems& config,
QObject* parent = nullptr);
const QList<PaletteItem>& 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();
}
private:
QList<PaletteItem> m_palettes;
};
#endif // WALLREEL_PALETTE_MANAGER_HPP
+88
View File
@@ -0,0 +1,88 @@
#ifndef WALLREEL_PALETTES_PREDEFINED_HPP
#define WALLREEL_PALETTES_PREDEFINED_HPP
#include "data.hpp"
inline const QList<PaletteItem> preDefinedPalettes = {
{
.name = "Catppuccin Latte",
.colors = {
{"rosewater", "#dc8a78"},
{"flamingo", "#dd7878"},
{"pink", "#ea76cb"},
{"mauve", "#8839ef"},
{"red", "#d20f39"},
{"maroon", "#e64553"},
{"peach", "#fe640b"},
{"yellow", "#df8e1d"},
{"green", "#40a02b"},
{"teal", "#179299"},
{"sky", "#04a5e5"},
{"sapphire", "#209fb5"},
{"blue", "#1e66f5"},
{"lavender", "#7287fd"},
},
},
{
.name = "Catppuccin Frappe",
.colors = {
{"rosewater", "#f2d5cf"},
{"flamingo", "#eebebe"},
{"pink", "#f4b8e4"},
{"mauve", "#ca9ee6"},
{"red", "#e78284"},
{"maroon", "#ea999c"},
{"peach", "#ef9f76"},
{"yellow", "#e5c890"},
{"green", "#a6d189"},
{"teal", "#81c8be"},
{"sky", "#99d1db"},
{"sapphire", "#85c1dc"},
{"blue", "#8caaee"},
{"lavender", "#babbf1"},
},
},
{
.name = "Catppuccin Macchiato",
.colors = {
{"rosewater", "#f4dbd6"},
{"flamingo", "#f0c6c6"},
{"pink", "#f5bde6"},
{"mauve", "#c6a0f6"},
{"red", "#ed8796"},
{"maroon", "#ee99a0"},
{"peach", "#f5a97f"},
{"yellow", "#eed49f"},
{"green", "#a6da95"},
{"teal", "#8bd5ca"},
{"sky", "#91d7e3"},
{"sapphire", "#7dc4e4"},
{"blue", "#8aadf4"},
{"lavender", "#b7bdf8"},
},
},
{
.name = "Catppuccin Mocha",
.colors = {
{"rosewater", "#f5e0dc"},
{"flamingo", "#f2cdcd"},
{"pink", "#f5c2e7"},
{"mauve", "#cba6f7"},
{"red", "#f38ba8"},
{"maroon", "#eba0ac"},
{"peach", "#fab387"},
{"yellow", "#f9e2af"},
{"green", "#a6e3a1"},
{"teal", "#94e2d5"},
{"sky", "#89dceb"},
{"sapphire", "#74c7ec"},
{"blue", "#89b4fa"},
{"lavender", "#b4befe"},
},
},
};
#endif // WALLREEL_PALETTES_PREDEFINED_HPP
+1
View File
@@ -0,0 +1 @@
+138
View File
@@ -0,0 +1,138 @@
#ifndef TEXTTEMPLATE_HPP
#define TEXTTEMPLATE_HPP
#include <QMap>
#include <QRegularExpression>
#include <QString>
/**
* @brief Replaces {{ key }} style placeholders in a template string with corresponding values from a map.
*
* Supports:
* - Whitespace tolerance: {{ key }}, {{key}}, {{ key }} are all valid.
* - Missing keys are left as-is (no replacement).
* - Nested braces or malformed placeholders are ignored.
* - Empty keys are ignored.
* - Keys are trimmed before lookup.
*
* @param templateStr The template string containing {{ key }} placeholders.
* @param variables A map of key-value pairs for substitution.
* @return The rendered string with placeholders replaced.
*/
inline QString renderTemplate(const QString& templateStr, const QMap<QString, QString>& variables) {
if (templateStr.isEmpty() || variables.isEmpty()) {
return templateStr;
}
// Match {{ ... }} with possible whitespace around the key.
// Use a non-greedy match for the content inside braces to handle multiple placeholders correctly.
static const QRegularExpression regex(
QStringLiteral(R"(\{\{\s*([^{}]+?)\s*\}\})"),
QRegularExpression::DontCaptureOption);
// We need the capture group, so rebuild without DontCaptureOption
static const QRegularExpression placeholderRegex(
QStringLiteral(R"(\{\{\s*([^{}]+?)\s*\}\})"));
QString result;
result.reserve(templateStr.size());
qsizetype lastPos = 0;
QRegularExpressionMatchIterator it = placeholderRegex.globalMatch(templateStr);
while (it.hasNext()) {
const QRegularExpressionMatch match = it.next();
const qsizetype matchStart = match.capturedStart(0);
const qsizetype matchLength = match.capturedLength(0);
const QString key = match.captured(1).trimmed();
// Append everything before this match
result.append(templateStr.mid(lastPos, matchStart - lastPos));
if (!key.isEmpty() && variables.contains(key)) {
// Replace with the value from the map
result.append(variables.value(key));
} else {
// Key not found or empty — leave the placeholder as-is
result.append(match.captured(0));
}
lastPos = matchStart + matchLength;
}
// Append any remaining text after the last match
if (lastPos < templateStr.size()) {
result.append(templateStr.mid(lastPos));
}
return result;
}
/**
* @brief Overload accepting QHash for convenience.
*/
inline QString renderTemplate(const QString& templateStr, const QHash<QString, QString>& variables) {
if (templateStr.isEmpty() || variables.isEmpty()) {
return templateStr;
}
static const QRegularExpression placeholderRegex(
QStringLiteral(R"(\{\{\s*([^{}]+?)\s*\}\})"));
QString result;
result.reserve(templateStr.size());
qsizetype lastPos = 0;
QRegularExpressionMatchIterator it = placeholderRegex.globalMatch(templateStr);
while (it.hasNext()) {
const QRegularExpressionMatch match = it.next();
const qsizetype matchStart = match.capturedStart(0);
const qsizetype matchLength = match.capturedLength(0);
const QString key = match.captured(1).trimmed();
result.append(templateStr.mid(lastPos, matchStart - lastPos));
if (!key.isEmpty() && variables.contains(key)) {
result.append(variables.value(key));
} else {
result.append(match.captured(0));
}
lastPos = matchStart + matchLength;
}
if (lastPos < templateStr.size()) {
result.append(templateStr.mid(lastPos));
}
return result;
}
/**
* @brief Extracts all placeholder keys from a template string.
*
* @param templateStr The template string to scan.
* @return A list of unique keys found in the template (trimmed).
*/
inline QStringList extractTemplateKeys(const QString& templateStr) {
static const QRegularExpression placeholderRegex(
QStringLiteral(R"(\{\{\s*([^{}]+?)\s*\}\})"));
QSet<QString> seen;
QStringList keys;
QRegularExpressionMatchIterator it = placeholderRegex.globalMatch(templateStr);
while (it.hasNext()) {
const QRegularExpressionMatch match = it.next();
const QString key = match.captured(1).trimmed();
if (!key.isEmpty() && !seen.contains(key)) {
seen.insert(key);
keys.append(key);
}
}
return keys;
}
#endif // TEXTTEMPLATE_HPP
+133
View File
@@ -0,0 +1,133 @@
#include "wallpaperservice.hpp"
#include <QColor>
#include <iostream>
#include "utils/logger.hpp"
#include "utils/texttemplate.hpp"
using namespace GeneralLogger;
WallpaperService::WallpaperService(
const Config::ActionConfigItems& actionConfig,
QObject* parent)
: QObject(parent), m_actionConfig(actionConfig) {
m_previewDebounceTimer = new QTimer(this);
m_previewDebounceTimer->setSingleShot(true);
m_previewDebounceTimer->setInterval(m_actionConfig.previewDebounceTime);
connect(m_previewDebounceTimer, &QTimer::timeout, this, [this]() {
_doPreview(*m_pendingImageData);
});
m_previewProcess = new QProcess(this);
connect(m_previewProcess,
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this,
[this](int exitCode, QProcess::ExitStatus exitStatus) {
Q_UNUSED(exitCode);
Q_UNUSED(exitStatus);
emit previewCompleted();
});
m_selectProcess = new QProcess(this);
connect(m_selectProcess,
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this,
[this](int exitCode, QProcess::ExitStatus exitStatus) {
Q_UNUSED(exitCode);
Q_UNUSED(exitStatus);
emit selectCompleted();
});
m_restoreProcess = new QProcess(this);
connect(m_restoreProcess,
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this,
[this](int exitCode, QProcess::ExitStatus exitStatus) {
Q_UNUSED(exitCode);
Q_UNUSED(exitStatus);
emit restoreCompleted();
});
}
void WallpaperService::preview(const ImageData& imageData) {
m_pendingImageData = &imageData;
m_previewDebounceTimer->start();
}
void WallpaperService::select(const ImageData& imageData) {
if (m_selectProcess->state() != QProcess::NotRunning) {
warn("Previous select command is still running. Ignoring new command.");
return;
}
_doSelect(imageData);
}
void WallpaperService::restore() {
if (m_restoreProcess->state() != QProcess::NotRunning) {
warn("Previous restore command is still running. Ignoring new command.");
return;
}
_doRestore();
}
void WallpaperService::_doPreview(const ImageData& imageData) {
QString path = imageData.getFullPath();
if (path.isEmpty()) {
return;
}
if (m_actionConfig.printPreview) {
std::cout << path.toStdString() << std::endl;
}
const QHash<QString, QString> variables{
{"path", path},
{"name", imageData.getFileName()},
};
auto command = renderTemplate(m_actionConfig.onPreview, variables);
if (command.isEmpty()) {
return;
}
if (m_previewProcess->state() != QProcess::NotRunning) {
m_previewProcess->kill();
m_previewProcess->waitForFinished();
}
m_previewProcess->start("sh", QStringList() << "-c" << command);
}
void WallpaperService::_doSelect(const ImageData& imageData) {
QString path = imageData.getFullPath();
if (path.isEmpty()) {
return;
}
if (m_actionConfig.printSelected) {
std::cout << path.toStdString() << std::endl;
}
const QHash<QString, QString> variables{
{"path", path},
{"name", imageData.getFileName()},
};
auto command = renderTemplate(m_actionConfig.onSelected, variables);
if (command.isEmpty()) {
return;
}
m_selectProcess->start("sh", QStringList() << "-c" << command);
}
void WallpaperService::_doRestore() {
if (m_actionConfig.onRestore.isEmpty()) {
return;
}
const QString command = renderTemplate(m_actionConfig.onRestore, m_actionConfig.saveState);
if (command.isEmpty()) {
return;
}
m_restoreProcess->start("sh", QStringList() << "-c" << command);
}
+41
View File
@@ -0,0 +1,41 @@
#ifndef WALLREEL_WALLPAPERSERVICE_HPP
#define WALLREEL_WALLPAPERSERVICE_HPP
#include <QProcess>
#include <QTimer>
#include "configmgr.hpp"
#include "imagedata.hpp"
class WallpaperService : public QObject {
Q_OBJECT
public:
WallpaperService(
const Config::ActionConfigItems& actionConfig,
QObject* parent = nullptr);
public slots:
void preview(const ImageData& imageData); // execute after 500ms of inactivity
void select(const ImageData& imageData); // execute immediately, ignore if already running
void restore(); // execute immediately, ignore if already running
signals:
void previewCompleted();
void selectCompleted();
void restoreCompleted();
private:
void _doPreview(const ImageData& imageData);
void _doSelect(const ImageData& imageData);
void _doRestore();
const Config::ActionConfigItems& m_actionConfig;
QTimer* m_previewDebounceTimer;
const ImageData* m_pendingImageData;
QProcess* m_previewProcess;
QProcess* m_selectProcess;
QProcess* m_restoreProcess;
};
#endif // WALLREEL_WALLPAPERSERVICE_HPP
+16 -4
View File
@@ -19,10 +19,22 @@ Item {
} else if (e.key === Qt.Key_Escape) } else if (e.key === Qt.Key_Escape)
Qt.quit(); Qt.quit();
else if (e.key === Qt.Key_Return || e.key === Qt.Key_Enter) else if (e.key === Qt.Key_Return || e.key === Qt.Key_Enter)
ImageModel.confirm(carousel.currentIndex); ImageModel.selectImage(carousel.currentIndex);
else else
e.accepted = false; e.accepted = false;
} }
Component.onCompleted: {
ImageModel.previewImage(carousel.currentIndex);
root.forceActiveFocus();
}
Connections {
function onCurrentIndexChanged() {
ImageModel.previewImage(carousel.currentIndex);
}
target: carousel
}
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
@@ -42,9 +54,9 @@ Item {
Layout.fillHeight: true Layout.fillHeight: true
model: ImageModel model: ImageModel
itemWidth: Config.imageWidth itemWidth: Config.imageWidth
itemHeight: Config.imageWidth / Config.aspectRatio itemHeight: Config.imageHeight
focusedItemWidth: Config.imageFocusWidth focusedItemWidth: Config.imageWidth * Config.imageFocusScale
focusedItemHeight: Config.imageFocusWidth / Config.aspectRatio focusedItemHeight: Config.imageHeight * Config.imageFocusScale
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
+41 -13
View File
@@ -1,17 +1,21 @@
#include <qtypes.h> #include <qobject.h>
#include <QApplication> #include <QApplication>
#include <QCommandLineParser> #include <QCommandLineParser>
#include <QDir> #include <QDir>
#include <QQmlApplicationEngine> #include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QStandardPaths> #include <QStandardPaths>
#include <QTextStream> #include <QTextStream>
#include "Core/configmgr.hpp" #include "Core/configmgr.hpp"
#include "Core/imagemodel.hpp" #include "Core/imagemodel.hpp"
#include "Core/imageprovider.hpp" #include "Core/imageprovider.hpp"
#include "Core/palette/data.hpp"
#include "Core/palette/manager.hpp"
#include "Core/utils/logger.hpp" #include "Core/utils/logger.hpp"
#include "Core/utils/misc.hpp" #include "Core/utils/misc.hpp"
#include "Core/wallpaperservice.hpp"
#include "version.h" #include "version.h"
/** /**
@@ -145,34 +149,58 @@ int main(int argc, char* argv[]) {
return s_options.errorText.isEmpty() ? 0 : 1; return s_options.errorText.isEmpty() ? 0 : 1;
} }
Config config(
::getConfigDir(),
s_options.appendDirs,
s_options.configPath,
&a);
QQmlApplicationEngine engine; QQmlApplicationEngine engine;
ImageProvider* imageProvider = new ImageProvider(); ImageProvider* imageProvider = new ImageProvider();
engine.addImageProvider(QLatin1String("processed"), imageProvider); engine.addImageProvider(QLatin1String("processed"), imageProvider);
ImageModel imageModel( auto config = new Config(
imageProvider, ::getConfigDir(),
config.getSortConfig(), s_options.appendDirs,
config.getFocusImageSize(), s_options.configPath,
imageProvider);
auto paletteMgr = new PaletteManager(
config->getPaletteConfig(),
&a); &a);
engine.rootContext()->setContextProperty("PaletteManager", paletteMgr);
qRegisterMetaType<PaletteItem>();
qRegisterMetaType<ColorItem>();
auto imageModel = new ImageModel(
*imageProvider,
config->getSortConfig(),
config->getFocusImageSize(),
config);
auto wallpaperService = new WallpaperService(
config->getActionConfig(),
config);
QObject::connect(
imageModel,
&ImageModel::imageSelected,
wallpaperService,
&WallpaperService::select);
QObject::connect(
imageModel,
&ImageModel::imagePreviewed,
wallpaperService,
&WallpaperService::preview);
qmlRegisterSingletonInstance( qmlRegisterSingletonInstance(
COREMODULE_URI, COREMODULE_URI,
MODULE_VERSION_MAJOR, MODULE_VERSION_MAJOR,
MODULE_VERSION_MINOR, MODULE_VERSION_MINOR,
"Config", "Config",
&config); config);
qmlRegisterSingletonInstance( qmlRegisterSingletonInstance(
COREMODULE_URI, COREMODULE_URI,
MODULE_VERSION_MAJOR, MODULE_VERSION_MAJOR,
MODULE_VERSION_MINOR, MODULE_VERSION_MINOR,
"ImageModel", "ImageModel",
&imageModel); imageModel);
QObject::connect( QObject::connect(
&engine, &engine,
@@ -182,7 +210,7 @@ int main(int argc, char* argv[]) {
Qt::QueuedConnection); Qt::QueuedConnection);
engine.loadFromModule(UIMODULE_URI, "Main"); engine.loadFromModule(UIMODULE_URI, "Main");
imageModel.loadAndProcess(config.getWallpapers()); imageModel->loadAndProcess(config->getWallpapers());
return a.exec(); return a.exec();
} }