From bcdfca82c53ac771f2f1182fcc8e8e30c6e43600 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 28 Sep 2024 12:26:01 +0200 Subject: [PATCH 01/13] Add FilePaths class --- src/global/CMakeLists.txt | 3 +++ src/global/globalmodule.cpp | 3 +++ src/global/ifilepaths.h | 20 ++++++++++++++++++++ src/global/internal/filepaths.cpp | 20 ++++++++++++++++++++ src/global/internal/filepaths.h | 23 +++++++++++++++++++++++ 5 files changed, 69 insertions(+) create mode 100644 src/global/ifilepaths.h create mode 100644 src/global/internal/filepaths.cpp create mode 100644 src/global/internal/filepaths.h diff --git a/src/global/CMakeLists.txt b/src/global/CMakeLists.txt index 9b9c2a0..a57911c 100644 --- a/src/global/CMakeLists.txt +++ b/src/global/CMakeLists.txt @@ -4,12 +4,15 @@ set(MODULE_SRC globalmodule.cpp globalmodule.h iappinfo.h + ifilepaths.h modularity/ioc.h modularity/modulesioc.h modularity/imoduleexportinterface.h modularity/imodulesetup.h internal/appinfo.cpp internal/appinfo.h + internal/filepaths.cpp + internal/filepaths.h ) include(${PROJECT_SOURCE_DIR}/build/module.cmake) diff --git a/src/global/globalmodule.cpp b/src/global/globalmodule.cpp index c0cba8d..771328a 100644 --- a/src/global/globalmodule.cpp +++ b/src/global/globalmodule.cpp @@ -4,6 +4,7 @@ #include "globalmodule.h" #include "internal/appinfo.h" +#include "internal/filepaths.h" using namespace scratchcpp; @@ -19,4 +20,6 @@ void GlobalModule::registerExports() QQmlEngine::setObjectOwnership(m_appInfo.get(), QQmlEngine::CppOwnership); qmlRegisterSingletonInstance("ScratchCPP.Ui", 1, 0, "AppInfo", m_appInfo.get()); modularity::ioc()->registerExport(m_appInfo); + + modularity::ioc()->registerExport(FilePaths::instance()); } diff --git a/src/global/ifilepaths.h b/src/global/ifilepaths.h new file mode 100644 index 0000000..68bd881 --- /dev/null +++ b/src/global/ifilepaths.h @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "modularity/ioc.h" + +namespace scratchcpp +{ + +class IFilePaths : MODULE_EXPORT_INTERFACE +{ + public: + virtual ~IFilePaths() { } + + virtual QString configLocation() const = 0; +}; + +} // namespace scratchcpp diff --git a/src/global/internal/filepaths.cpp b/src/global/internal/filepaths.cpp new file mode 100644 index 0000000..06cf347 --- /dev/null +++ b/src/global/internal/filepaths.cpp @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include + +#include "filepaths.h" + +using namespace scratchcpp; + +std::shared_ptr FilePaths::m_instance = std::make_shared(); + +std::shared_ptr FilePaths::instance() +{ + return m_instance; +} + +QString scratchcpp::FilePaths::configLocation() const +{ + return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + "/" + qApp->applicationName() + "/config.ini"; +} diff --git a/src/global/internal/filepaths.h b/src/global/internal/filepaths.h new file mode 100644 index 0000000..e6e936a --- /dev/null +++ b/src/global/internal/filepaths.h @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "ifilepaths.h" + +namespace scratchcpp +{ + +class FilePaths : public IFilePaths +{ + public: + static std::shared_ptr instance(); + + QString configLocation() const override; + + private: + static std::shared_ptr m_instance; +}; + +} // namespace scratchcpp From ec28963409142e2d05ca48a59783e01e705a3cab Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 28 Sep 2024 13:03:49 +0200 Subject: [PATCH 02/13] Set application name in tests --- test/main.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/test/main.cpp b/test/main.cpp index c5dfc22..eb6569c 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -4,6 +4,7 @@ int main(int argc, char **argv) { QApplication a(argc, argv); + a.setApplicationName("ScratchCPP Player"); ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } From 93625df966aa9cd7637be3c47fd28a4dec7653ed Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 28 Sep 2024 13:04:15 +0200 Subject: [PATCH 03/13] Add Settings class --- src/global/CMakeLists.txt | 3 + src/global/globalmodule.cpp | 7 ++ src/global/globalmodule.h | 2 + src/global/internal/settings.cpp | 162 ++++++++++++++++++++++++++ src/global/internal/settings.h | 60 ++++++++++ src/global/isettings.h | 29 +++++ src/global/test/CMakeLists.txt | 1 + src/global/test/mocks/filepathsmock.h | 15 +++ src/global/test/settings.cpp | 125 ++++++++++++++++++++ 9 files changed, 404 insertions(+) create mode 100644 src/global/internal/settings.cpp create mode 100644 src/global/internal/settings.h create mode 100644 src/global/isettings.h create mode 100644 src/global/test/mocks/filepathsmock.h create mode 100644 src/global/test/settings.cpp diff --git a/src/global/CMakeLists.txt b/src/global/CMakeLists.txt index a57911c..4acc8de 100644 --- a/src/global/CMakeLists.txt +++ b/src/global/CMakeLists.txt @@ -5,6 +5,7 @@ set(MODULE_SRC globalmodule.h iappinfo.h ifilepaths.h + isettings.h modularity/ioc.h modularity/modulesioc.h modularity/imoduleexportinterface.h @@ -13,6 +14,8 @@ set(MODULE_SRC internal/appinfo.h internal/filepaths.cpp internal/filepaths.h + internal/settings.cpp + internal/settings.h ) include(${PROJECT_SOURCE_DIR}/build/module.cmake) diff --git a/src/global/globalmodule.cpp b/src/global/globalmodule.cpp index 771328a..329aa96 100644 --- a/src/global/globalmodule.cpp +++ b/src/global/globalmodule.cpp @@ -5,6 +5,7 @@ #include "globalmodule.h" #include "internal/appinfo.h" #include "internal/filepaths.h" +#include "internal/settings.h" using namespace scratchcpp; @@ -22,4 +23,10 @@ void GlobalModule::registerExports() modularity::ioc()->registerExport(m_appInfo); modularity::ioc()->registerExport(FilePaths::instance()); + + m_settings = std::make_shared(); + + QQmlEngine::setObjectOwnership(m_settings.get(), QQmlEngine::CppOwnership); + qmlRegisterSingletonInstance("ScratchCPP.Global", 1, 0, "Settings", m_settings.get()); + modularity::ioc()->registerExport(m_settings); } diff --git a/src/global/globalmodule.h b/src/global/globalmodule.h index 723f3df..750725e 100644 --- a/src/global/globalmodule.h +++ b/src/global/globalmodule.h @@ -10,6 +10,7 @@ namespace scratchcpp { class AppInfo; +class Settings; class GlobalModule : public modularity::IModuleSetup { @@ -20,6 +21,7 @@ class GlobalModule : public modularity::IModuleSetup private: std::shared_ptr m_appInfo; + std::shared_ptr m_settings; }; } // namespace scratchcpp diff --git a/src/global/internal/settings.cpp b/src/global/internal/settings.cpp new file mode 100644 index 0000000..1d6f379 --- /dev/null +++ b/src/global/internal/settings.cpp @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include + +#include "settings.h" +#include "ifilepaths.h" + +using namespace scratchcpp; + +Settings::Settings(QObject *parent) : + QObject(parent), + m_mainSettingsInstance(paths()->configLocation(), QSettings::IniFormat), + m_tmpSettingsInstance(paths()->configLocation() + ".tmp", QSettings::IniFormat) +{ + m_settingsInstance = &m_mainSettingsInstance; +} + +void Settings::addKey(const QString &moduleName, const QString &keyName, const QVariant &defaultValue) +{ + m_defaults.insert({ moduleName, keyName }, defaultValue); +} + +void Settings::setValue(const QString &moduleName, const QString &keyName, const QVariant &value) +{ + set(moduleName + "/" + keyName, value); +} + +QVariant Settings::getValue(const QString &moduleName, const QString &keyName) const +{ + QPair key(moduleName, keyName); + QVariant defaultValue; + auto it = m_defaults.find(key); + + if (it != m_defaults.cend()) + defaultValue = it.value(); + + return get(moduleName + "/" + keyName, defaultValue); +} + +bool Settings::containsKey(const QString &moduleName, const QString &keyName) const +{ + QPair key(moduleName, keyName); + return contains(moduleName + "/" + keyName); +} + +/* + * Switches to temporary settings. You can decide to saveChanges() or discardChanges() later. + * This is useful for settings dialogs with a discard button. + */ +void Settings::freeze(void) +{ + Q_ASSERT(!m_frozen); + m_settingsInstance = &m_tmpSettingsInstance; + copySettings(&m_mainSettingsInstance, m_settingsInstance); + m_frozen = true; + emit stateChanged(); +} + +// Saves changes to real settings and switches back to them. +void Settings::saveChanges(void) +{ + Q_ASSERT(m_frozen); + copySettings(m_settingsInstance, &m_mainSettingsInstance); + m_mainSettingsInstance.sync(); + m_settingsInstance = &m_mainSettingsInstance; + m_frozen = false; + emit stateChanged(); + emit saved(); +} + +// Discards changes and switches back to real settings. +void Settings::discardChanges(void) +{ + Q_ASSERT(m_frozen); + m_settingsInstance = &m_mainSettingsInstance; + m_frozen = false; + emit stateChanged(); + emit discarded(); +} + +bool Settings::isFrozen(void) const +{ + return m_frozen; +} + +QVariant Settings::get(const QString &key, const QVariant &defaultValue) const +{ + Q_ASSERT(m_settingsInstance != nullptr); +#ifdef Q_OS_WASM + if (m_settingsInstance->isWritable()) { + if (!m_tempSettingsCopied) + const_cast(this)->copyTempSettings(); + return m_settingsInstance->value(key, defaultValue); + } else { + // Use temporary settings until sandbox is initialized + QSettings settings(paths()->configLocation(), QSettings::IniFormat); + return settings.value(key, defaultValue); + } +#else + return m_settingsInstance->value(key, defaultValue); +#endif // Q_OS_WASM +} + +bool Settings::contains(const QString &key) const +{ + Q_ASSERT(m_settingsInstance != nullptr); +#ifdef Q_OS_WASM + if (m_settingsInstance->isWritable()) { + if (!m_tempSettingsCopied) + const_cast(this)->copyTempSettings(); + return m_settingsInstance->contains(key); + } else { + // Use temporary settings until sandbox is initialized + QSettings settings(paths()->configLocation(), QSettings::IniFormat); + return settings.contains(key); + } +#else + return m_settingsInstance->contains(key); +#endif // Q_OS_WASM +} + +void Settings::set(const QString &key, const QVariant &value) +{ + Q_ASSERT(m_settingsInstance != nullptr); +#ifdef Q_OS_WASM + if (m_settingsInstance->isWritable()) { + if (!m_tempSettingsCopied) + copyTempSettings(); + m_settingsInstance->setValue(key, value); + m_settingsInstance->sync(); + } else { + // Use temporary settings until sandbox is initialized + QSettings settings(paths()->configLocation(), QSettings::IniFormat); + settings.setValue(key, value); + } +#else + m_settingsInstance->setValue(key, value); +#endif // Q_OS_WASM +} + +void Settings::copySettings(QSettings *source, QSettings *target) +{ +#ifndef Q_OS_WASM + target->clear(); +#endif + QStringList keys = source->allKeys(); + for (int i = 0; i < keys.count(); i++) + target->setValue(keys[i], source->value(keys[i])); +} + +#ifdef Q_OS_WASM +void Settings::copyTempSettings(void) +{ + QSettings settings(paths()->configLocation(), QSettings::IniFormat); + copySettings(&settings, m_settingsInstance); + m_settingsInstance->sync(); + m_tempSettingsCopied = true; +} +#endif // Q_OS_WASM diff --git a/src/global/internal/settings.h b/src/global/internal/settings.h new file mode 100644 index 0000000..7348ac0 --- /dev/null +++ b/src/global/internal/settings.h @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +#include "isettings.h" +#include "ifilepaths.h" + +class QSettings; + +namespace scratchcpp +{ + +// class IFilePaths; + +class Settings + : public QObject + , public ISettings +{ + Q_OBJECT + INJECT(IFilePaths, paths) + public: + Settings(QObject *parent = nullptr); + + void addKey(const QString &moduleName, const QString &keyName, const QVariant &defaultValue) override; + Q_INVOKABLE void setValue(const QString &moduleName, const QString &keyName, const QVariant &value) override; + Q_INVOKABLE QVariant getValue(const QString &moduleName, const QString &keyName) const override; + Q_INVOKABLE bool containsKey(const QString &moduleName, const QString &keyName) const override; + Q_INVOKABLE void freeze(void) override; + Q_INVOKABLE void saveChanges(void) override; + Q_INVOKABLE void discardChanges(void) override; + Q_INVOKABLE bool isFrozen(void) const override; + + signals: + void stateChanged(); + void saved(); + void discarded(); + + private: + QVariant get(const QString &key, const QVariant &defaultValue) const; + bool contains(const QString &key) const; + void set(const QString &key, const QVariant &value); + + static void copySettings(QSettings *source, QSettings *target); + + QSettings *m_settingsInstance = nullptr; + QSettings m_mainSettingsInstance; + QSettings m_tmpSettingsInstance; + bool m_frozen = false; + QMap, QVariant> m_defaults; + +#ifdef Q_OS_WASM + bool m_tempSettingsCopied = false; + void copyTempSettings(void); +#endif // Q_OS_WASM +}; + +} // namespace scratchcpp diff --git a/src/global/isettings.h b/src/global/isettings.h new file mode 100644 index 0000000..56ed9d2 --- /dev/null +++ b/src/global/isettings.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include "modularity/ioc.h" + +#define INIT_SETTINGS_KEY(keyName, defaultValue) modularity::ioc()->resolve()->addKey(QString::fromStdString(moduleName()), keyName, defaultValue) + +namespace scratchcpp +{ + +class ISettings : MODULE_EXPORT_INTERFACE +{ + public: + virtual ~ISettings() { } + + virtual void addKey(const QString &moduleName, const QString &keyName, const QVariant &defaultValue) = 0; + virtual void setValue(const QString &moduleName, const QString &keyName, const QVariant &value) = 0; + virtual QVariant getValue(const QString &moduleName, const QString &keyName) const = 0; + virtual bool containsKey(const QString &moduleName, const QString &keyName) const = 0; + + virtual void freeze() = 0; + virtual void saveChanges() = 0; + virtual void discardChanges() = 0; + virtual bool isFrozen() const = 0; +}; + +} // namespace scratchcpp diff --git a/src/global/test/CMakeLists.txt b/src/global/test/CMakeLists.txt index c7d28e9..b9390b0 100644 --- a/src/global/test/CMakeLists.txt +++ b/src/global/test/CMakeLists.txt @@ -2,6 +2,7 @@ set(MODULE_TEST_SRC modularity.cpp setup.cpp appinfo.cpp + settings.cpp fakeexport.h fakedependency.h mocks/moduleexportinterfacemock.h diff --git a/src/global/test/mocks/filepathsmock.h b/src/global/test/mocks/filepathsmock.h new file mode 100644 index 0000000..7603783 --- /dev/null +++ b/src/global/test/mocks/filepathsmock.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +namespace scratchcpp +{ + +class FilePathsMock : public IFilePaths +{ + public: + MOCK_METHOD(QString, configLocation, (), (const, override)); +}; + +} // namespace scratchcpp diff --git a/src/global/test/settings.cpp b/src/global/test/settings.cpp new file mode 100644 index 0000000..cd46d59 --- /dev/null +++ b/src/global/test/settings.cpp @@ -0,0 +1,125 @@ +#include +#include +#include +#include +#include + +#include "mocks/filepathsmock.h" + +#include "internal/settings.h" + +using namespace scratchcpp; + +using ::testing::Return; + +class SettingsTest : public testing::Test +{ + public: + void SetUp() override + { + m_paths = std::make_shared(); + modularity::ioc()->registerExport(m_paths); + EXPECT_CALL(*m_paths, configLocation()) + .WillRepeatedly(Return(QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + "/" + qApp->applicationName() + "/config_test.ini")); + QFile::remove(m_paths->configLocation()); + m_settings = std::make_shared(); + } + + void TearDown() override + { + modularity::ioc()->reset(); + m_settings->setpaths(nullptr); + } + + std::shared_ptr m_settings; + std::shared_ptr m_paths; +}; + +TEST_F(SettingsTest, SetGetContains) +{ + ASSERT_FALSE(m_settings->containsKey("test", "something")); + ASSERT_TRUE(m_settings->getValue("test", "something").isNull()); + + m_settings->setValue("test", "something", "hello world"); + ASSERT_TRUE(m_settings->containsKey("test", "something")); + ASSERT_FALSE(m_settings->containsKey("test", "test")); + ASSERT_EQ(m_settings->getValue("test", "something").toString(), "hello world"); + ASSERT_TRUE(m_settings->getValue("test", "test").isNull()); + + m_settings->setValue("test", "test", 10); + ASSERT_TRUE(m_settings->containsKey("test", "something")); + ASSERT_TRUE(m_settings->containsKey("test", "test")); + ASSERT_EQ(m_settings->getValue("test", "something").toString(), "hello world"); + ASSERT_EQ(m_settings->getValue("test", "test").toInt(), 10); + + ASSERT_FALSE(m_settings->containsKey("test2", "something")); + ASSERT_FALSE(m_settings->containsKey("test2", "test")); + ASSERT_TRUE(m_settings->getValue("test2", "something").isNull()); + ASSERT_TRUE(m_settings->getValue("test2", "test").isNull()); + + m_settings->setValue("test2", "test", true); + ASSERT_TRUE(m_settings->containsKey("test", "something")); + ASSERT_TRUE(m_settings->containsKey("test", "test")); + ASSERT_FALSE(m_settings->containsKey("test2", "something")); + ASSERT_TRUE(m_settings->containsKey("test2", "test")); + ASSERT_EQ(m_settings->getValue("test", "something").toString(), "hello world"); + ASSERT_TRUE(m_settings->getValue("test2", "test").toBool()); + ASSERT_TRUE(m_settings->getValue("test2", "something").isNull()); + + // Test persistence + m_settings.reset(); + m_settings = std::make_shared(); + ASSERT_EQ(m_settings->getValue("test", "something").toString(), "hello world"); + ASSERT_TRUE(m_settings->containsKey("test", "something")); + ASSERT_TRUE(m_settings->containsKey("test", "test")); + ASSERT_EQ(m_settings->getValue("test", "something").toString(), "hello world"); + ASSERT_EQ(m_settings->getValue("test", "test").toInt(), 10); + ASSERT_TRUE(m_settings->getValue("test2", "test").toBool()); + ASSERT_TRUE(m_settings->getValue("test2", "something").isNull()); +} + +TEST_F(SettingsTest, DefaultValue) +{ + m_settings->addKey("test", "something", 5); + ASSERT_FALSE(m_settings->containsKey("test", "something")); + ASSERT_EQ(m_settings->getValue("test", "something").toInt(), 5); + + m_settings->setValue("test", "something", 10); + ASSERT_TRUE(m_settings->containsKey("test", "something")); + ASSERT_EQ(m_settings->getValue("test", "something").toInt(), 10); +} + +TEST_F(SettingsTest, Freeze) +{ + ASSERT_FALSE(m_settings->isFrozen()); + + // Discard + m_settings->setValue("test", "something", "hello"); + m_settings->freeze(); + ASSERT_TRUE(m_settings->isFrozen()); + ASSERT_TRUE(m_settings->containsKey("test", "something")); + ASSERT_EQ(m_settings->getValue("test", "something").toString(), "hello"); + + m_settings->setValue("test", "test", 15); + ASSERT_TRUE(m_settings->containsKey("test", "test")); + ASSERT_EQ(m_settings->getValue("test", "test").toInt(), 15); + + m_settings->discardChanges(); + ASSERT_FALSE(m_settings->isFrozen()); + ASSERT_TRUE(m_settings->containsKey("test", "something")); + ASSERT_EQ(m_settings->getValue("test", "something").toString(), "hello"); + ASSERT_FALSE(m_settings->containsKey("test", "test")); + ASSERT_TRUE(m_settings->getValue("test", "test").isNull()); + + // Save + m_settings->freeze(); + ASSERT_TRUE(m_settings->isFrozen()); + m_settings->setValue("test", "test", 15); + + m_settings->saveChanges(); + ASSERT_FALSE(m_settings->isFrozen()); + ASSERT_TRUE(m_settings->containsKey("test", "something")); + ASSERT_EQ(m_settings->getValue("test", "something").toString(), "hello"); + ASSERT_TRUE(m_settings->containsKey("test", "test")); + ASSERT_EQ(m_settings->getValue("test", "test").toInt(), 15); +} From d33ae9a53b5cbfa729021d350ad9e908f4822fc7 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 28 Sep 2024 17:09:25 +0200 Subject: [PATCH 04/13] Add ThemeEngine class --- src/ui/CMakeLists.txt | 3 ++ src/ui/internal/themeengine.cpp | 53 ++++++++++++++++++++++++ src/ui/internal/themeengine.h | 43 +++++++++++++++++++ src/ui/ithemeengine.h | 36 ++++++++++++++++ src/ui/test/CMakeLists.txt | 1 + src/ui/test/mocks/settingsmock.h | 23 +++++++++++ src/ui/test/themeengine.cpp | 71 ++++++++++++++++++++++++++++++++ src/ui/uimodule.cpp | 11 +++++ src/ui/uimodule.h | 1 + 9 files changed, 242 insertions(+) create mode 100644 src/ui/internal/themeengine.cpp create mode 100644 src/ui/internal/themeengine.h create mode 100644 src/ui/ithemeengine.h create mode 100644 src/ui/test/mocks/settingsmock.h create mode 100644 src/ui/test/themeengine.cpp diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index 4c96021..eed3f01 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -4,8 +4,11 @@ set(MODULE_SRC uimodule.cpp uimodule.h iuiengine.h + ithemeengine.h internal/uiengine.cpp internal/uiengine.h + internal/themeengine.cpp + internal/themeengine.h ) include(${PROJECT_SOURCE_DIR}/build/module.cmake) diff --git a/src/ui/internal/themeengine.cpp b/src/ui/internal/themeengine.cpp new file mode 100644 index 0000000..2f59f2d --- /dev/null +++ b/src/ui/internal/themeengine.cpp @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "themeengine.h" +#include "isettings.h" + +using namespace scratchcpp; + +static const QString MODULE = "ui"; +static const QString THEME_KEY = "theme"; + +std::shared_ptr ThemeEngine::m_instance = std::make_shared(); + +std::shared_ptr ThemeEngine::instance() +{ + return m_instance; +} + +IThemeEngine::Theme ThemeEngine::theme() const +{ + return static_cast(settings()->getValue(MODULE, THEME_KEY).toInt()); +} + +void ThemeEngine::setTheme(Theme newTheme) +{ + settings()->setValue(MODULE, THEME_KEY, static_cast(newTheme)); + reloadTheme(); +} + +void ThemeEngine::reloadTheme() +{ + emit bgColorChanged(); + emit borderColorChanged(); + emit themeChanged(); +} + +void ThemeEngine::resetTheme() +{ + setTheme(Theme::DarkTheme); +} + +const QColor &ThemeEngine::bgColor() const +{ + static const QColor dark = QColor(31, 30, 28); + static const QColor light = QColor(255, 255, 255); + return theme() == Theme::DarkTheme ? dark : light; +} + +const QColor &ThemeEngine::borderColor() const +{ + static const QColor dark = QColor(255, 255, 255, 64); + static const QColor light = QColor(0, 0, 0, 64); + return theme() == Theme::DarkTheme ? dark : light; +} diff --git a/src/ui/internal/themeengine.h b/src/ui/internal/themeengine.h new file mode 100644 index 0000000..f0bccde --- /dev/null +++ b/src/ui/internal/themeengine.h @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "ithemeengine.h" + +Q_MOC_INCLUDE("isettings.h") + +namespace scratchcpp +{ + +class ISettings; + +class ThemeEngine : public IThemeEngine +{ + Q_OBJECT + INJECT(ISettings, settings) + Q_PROPERTY(Theme theme READ theme WRITE setTheme NOTIFY themeChanged FINAL) + Q_PROPERTY(QColor bgColor READ bgColor NOTIFY bgColorChanged FINAL) + Q_PROPERTY(QColor borderColor READ borderColor NOTIFY borderColorChanged FINAL) + public: + static std::shared_ptr instance(); + + Theme theme() const override; + void setTheme(Theme newTheme) override; + Q_INVOKABLE void reloadTheme() override; + Q_INVOKABLE void resetTheme() override; + + const QColor &bgColor() const override; + const QColor &borderColor() const override; + + signals: + void themeChanged(); + void bgColorChanged(); + void borderColorChanged(); + + private: + static std::shared_ptr m_instance; +}; + +} // namespace scratchcpp diff --git a/src/ui/ithemeengine.h b/src/ui/ithemeengine.h new file mode 100644 index 0000000..d17461b --- /dev/null +++ b/src/ui/ithemeengine.h @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "modularity/ioc.h" + +namespace scratchcpp +{ + +class IThemeEngine + : public QObject + , MODULE_EXPORT_INTERFACE +{ + Q_OBJECT + public: + virtual ~IThemeEngine() { } + + enum class Theme + { + LightTheme = 1, + DarkTheme = 0 + }; + Q_ENUM(Theme) + + virtual Theme theme() const = 0; + virtual void setTheme(Theme newTheme) = 0; + virtual void reloadTheme() = 0; + virtual void resetTheme() = 0; + + virtual const QColor &bgColor() const = 0; + virtual const QColor &borderColor() const = 0; +}; + +} // namespace scratchcpp diff --git a/src/ui/test/CMakeLists.txt b/src/ui/test/CMakeLists.txt index 654b6f5..1b4177d 100644 --- a/src/ui/test/CMakeLists.txt +++ b/src/ui/test/CMakeLists.txt @@ -1,5 +1,6 @@ set(MODULE_TEST_SRC uiengine.cpp + themeengine.cpp ) include(${PROJECT_SOURCE_DIR}/build/module_test.cmake) diff --git a/src/ui/test/mocks/settingsmock.h b/src/ui/test/mocks/settingsmock.h new file mode 100644 index 0000000..32b9b3a --- /dev/null +++ b/src/ui/test/mocks/settingsmock.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include + +namespace scratchcpp +{ + +class SettingsMock : public ISettings +{ + public: + MOCK_METHOD(void, addKey, (const QString &, const QString &, const QVariant &), (override)); + MOCK_METHOD(void, setValue, (const QString &, const QString &, const QVariant &), (override)); + MOCK_METHOD(QVariant, getValue, (const QString &, const QString &), (const, override)); + MOCK_METHOD(bool, containsKey, (const QString &, const QString &), (const, override)); + + MOCK_METHOD(void, freeze, (), (override)); + MOCK_METHOD(void, saveChanges, (), (override)); + MOCK_METHOD(void, discardChanges, (), (override)); + MOCK_METHOD(bool, isFrozen, (), (const, override)); +}; + +} // namespace scratchcpp diff --git a/src/ui/test/themeengine.cpp b/src/ui/test/themeengine.cpp new file mode 100644 index 0000000..aef5e8b --- /dev/null +++ b/src/ui/test/themeengine.cpp @@ -0,0 +1,71 @@ +#include +#include +#include + +#include "mocks/settingsmock.h" + +#include "internal/themeengine.h" + +using namespace scratchcpp; + +using ::testing::Return; +using ::testing::_; + +static const QString MODULE = "ui"; +static const QString THEME_KEY = "theme"; + +class ThemeEngineTest : public testing::Test +{ + public: + void SetUp() override + { + m_settings = std::make_shared(); + m_themeEngine.setsettings(m_settings); + + m_themeSpies.push_back(std::make_unique(&m_themeEngine, &ThemeEngine::themeChanged)); + m_themeSpies.push_back(std::make_unique(&m_themeEngine, &ThemeEngine::bgColorChanged)); + m_themeSpies.push_back(std::make_unique(&m_themeEngine, &ThemeEngine::borderColorChanged)); + } + + void TearDown() override { m_themeEngine.setsettings(nullptr); } + + void checkThemeSpies(int count) + { + for (const auto &spy : m_themeSpies) + ASSERT_EQ(spy->count(), count); + } + + ThemeEngine m_themeEngine; + std::shared_ptr m_settings; + std::vector> m_themeSpies; +}; + +TEST_F(ThemeEngineTest, Instance) +{ + ASSERT_TRUE(ThemeEngine::instance()); +} + +TEST_F(ThemeEngineTest, Theme) +{ + EXPECT_CALL(*m_settings, getValue(MODULE, THEME_KEY)).WillOnce(Return(0)); + ASSERT_EQ(m_themeEngine.theme(), ThemeEngine::Theme::DarkTheme); + + EXPECT_CALL(*m_settings, getValue(MODULE, THEME_KEY)).WillOnce(Return(1)); + ASSERT_EQ(m_themeEngine.theme(), ThemeEngine::Theme::LightTheme); + checkThemeSpies(0); + + EXPECT_CALL(*m_settings, setValue(MODULE, THEME_KEY, QVariant(0))); + m_themeEngine.setTheme(ThemeEngine::Theme::DarkTheme); + checkThemeSpies(1); + + EXPECT_CALL(*m_settings, setValue(MODULE, THEME_KEY, QVariant(1))); + m_themeEngine.setTheme(ThemeEngine::Theme::LightTheme); + checkThemeSpies(2); + + EXPECT_CALL(*m_settings, setValue(MODULE, THEME_KEY, _)); + m_themeEngine.resetTheme(); + checkThemeSpies(3); + + m_themeEngine.reloadTheme(); + checkThemeSpies(4); +} diff --git a/src/ui/uimodule.cpp b/src/ui/uimodule.cpp index a9a953b..96d16a3 100644 --- a/src/ui/uimodule.cpp +++ b/src/ui/uimodule.cpp @@ -3,7 +3,9 @@ #include #include "uimodule.h" +#include "isettings.h" #include "internal/uiengine.h" +#include "internal/themeengine.h" using namespace scratchcpp::ui; @@ -21,4 +23,13 @@ void scratchcpp::ui::UiModule::registerExports() QQmlEngine::setObjectOwnership(UiEngine::instance().get(), QQmlEngine::CppOwnership); qmlRegisterSingletonInstance("ScratchCPP.Ui", 1, 0, "UiEngine", UiEngine::instance().get()); modularity::ioc()->registerExport(UiEngine::instance()); + + QQmlEngine::setObjectOwnership(ThemeEngine::instance().get(), QQmlEngine::CppOwnership); + qmlRegisterSingletonInstance("ScratchCPP.Ui", 1, 0, "ThemeEngine", ThemeEngine::instance().get()); + modularity::ioc()->registerExport(ThemeEngine::instance()); +} + +void UiModule::initSettings() +{ + INIT_SETTINGS_KEY("theme", static_cast(IThemeEngine::Theme::DarkTheme)); } diff --git a/src/ui/uimodule.h b/src/ui/uimodule.h index 1f60570..04d80ec 100644 --- a/src/ui/uimodule.h +++ b/src/ui/uimodule.h @@ -15,6 +15,7 @@ class UiModule : public modularity::IModuleSetup std::string moduleName() const override; void registerExports() override; + void initSettings() override; }; } // namespace scratchcpp::ui From 52d76478dc8ef2b18949e7af3853718a3295aed0 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 28 Sep 2024 17:12:17 +0200 Subject: [PATCH 05/13] Use ThemeEngine to read theme colors --- src/app/qml/main.qml | 2 +- src/uicomponents/CustomDialog.qml | 7 +++---- src/uicomponents/CustomMenu.qml | 5 +++-- src/uicomponents/CustomMenuBar.qml | 5 ++--- src/uicomponents/internal/CustomDialogButtonBox.qml | 3 +-- 5 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/app/qml/main.qml b/src/app/qml/main.qml index 748309e..fc64cd5 100644 --- a/src/app/qml/main.qml +++ b/src/app/qml/main.qml @@ -18,7 +18,7 @@ ApplicationWindow { title: Qt.application.displayName color: Material.background Material.accent: "orange" - Material.theme: Material.Dark + Material.theme: ThemeEngine.theme == ThemeEngine.DarkTheme ? Material.Dark : Material.Light onActiveFocusItemChanged: UiEngine.activeFocusItem = activeFocusItem menuBar: CustomMenuBar { diff --git a/src/uicomponents/CustomDialog.qml b/src/uicomponents/CustomDialog.qml index 4861a04..167d1af 100644 --- a/src/uicomponents/CustomDialog.qml +++ b/src/uicomponents/CustomDialog.qml @@ -124,11 +124,10 @@ Item { property alias buttonBoxLoader: buttonBoxLoader anchors.fill: parent // TODO: Read colors from ThemeEngine - color: /*ThemeEngine.bgColor*/ Material.background - //Material.background: ThemeEngine.bgColor + color: ThemeEngine.bgColor + Material.background: ThemeEngine.bgColor //Material.accent: ThemeEngine.currentAccentColor - //Material.theme: ThemeEngine.theme === ThemeEngine.DarkTheme ? Material.Dark : Material.Light - Material.theme: Material.Dark + Material.theme: ThemeEngine.theme === ThemeEngine.DarkTheme ? Material.Dark : Material.Light ColumnLayout { id: contentLayout diff --git a/src/uicomponents/CustomMenu.qml b/src/uicomponents/CustomMenu.qml index a7340c9..8449b63 100644 --- a/src/uicomponents/CustomMenu.qml +++ b/src/uicomponents/CustomMenu.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Controls +import ScratchCPP.Ui Menu { property bool isSubMenu: false @@ -27,8 +28,8 @@ Menu { font.pointSize: 10 background: Rectangle { // Load colors from theme - color: /*ThemeEngine.bgColor*/ Material.background - border.color: /*ThemeEngine.borderColor*/ Qt.rgba(1, 1, 1, 0.25) + color: ThemeEngine.bgColor + border.color: ThemeEngine.borderColor radius: 10 implicitHeight: 40 } diff --git a/src/uicomponents/CustomMenuBar.qml b/src/uicomponents/CustomMenuBar.qml index bfb7f96..a495f24 100644 --- a/src/uicomponents/CustomMenuBar.qml +++ b/src/uicomponents/CustomMenuBar.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Controls +import ScratchCPP.Ui import ScratchCPP.UiComponents MenuBar { @@ -72,7 +73,7 @@ MenuBar { } background: Rectangle { - color: Material.backgroundColor // Load the color from the theme + color: ThemeEngine.bgColor // Load the color from the theme } delegate: MenuBarItem { @@ -94,8 +95,6 @@ MenuBar { topPadding: 5 bottomPadding: 5 font.pointSize: 10 - Material.background: Qt.rgba(0, 0, 0, 0) - Material.foreground: Material.theme == Material.Dark ? "white" : "black" contentItem: Label { text: replaceText(menuBarItem.text) font: menuBarItem.font diff --git a/src/uicomponents/internal/CustomDialogButtonBox.qml b/src/uicomponents/internal/CustomDialogButtonBox.qml index 4c2a91e..d0e06de 100644 --- a/src/uicomponents/internal/CustomDialogButtonBox.qml +++ b/src/uicomponents/internal/CustomDialogButtonBox.qml @@ -5,13 +5,12 @@ import QtQuick.Controls import ScratchCPP.Ui DialogButtonBox { - property color bgColor: /*ThemeEngine.bgColor*/ Material.background property int radius: 10 signal focusOut() id: dialogButtonBox font.capitalization: Font.MixedCase background: Rectangle { - color: bgColor + color: "transparent" radius: radius } From 8a574c16de437e64b024edb5abb80501a0958fce Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 28 Sep 2024 17:12:45 +0200 Subject: [PATCH 06/13] Add PreferencesDialog --- src/app/CMakeLists.txt | 1 + src/app/qml/dialogs/PreferencesDialog.qml | 42 +++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 src/app/qml/dialogs/PreferencesDialog.qml diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 3ec2ccd..abf5038 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -17,6 +17,7 @@ qt_add_qml_module(${APP_TARGET} qml/main.qml qml/dialogs/AboutDialog.qml qml/dialogs/ProjectSettingsDialog.qml + qml/dialogs/PreferencesDialog.qml ) set(QML_IMPORT_PATH "${QML_IMPORT_PATH};${CMAKE_CURRENT_LIST_DIR}" diff --git a/src/app/qml/dialogs/PreferencesDialog.qml b/src/app/qml/dialogs/PreferencesDialog.qml new file mode 100644 index 0000000..62f5c0d --- /dev/null +++ b/src/app/qml/dialogs/PreferencesDialog.qml @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import ScratchCPP.Global +import ScratchCPP.Ui +import ScratchCPP.UiComponents + +CustomDialog { + title: qsTr("Preferences") + standardButtons: Dialog.Cancel | Dialog.Ok + onOpened: Settings.freeze() + onAccepted: Settings.saveChanges() + onRejected: { + Settings.discardChanges(); + ThemeEngine.reloadTheme(); + } + + contentItem: ColumnLayout { + // Themes + Label { + text: qsTr("Themes") + font.pointSize: 14 + font.bold: true + } + + RowLayout { + RadioButton { + text: qsTr("Light") + checked: ThemeEngine.theme === ThemeEngine.LightTheme + onCheckedChanged: if(checked) ThemeEngine.theme = ThemeEngine.LightTheme + } + + RadioButton { + text: qsTr("Dark") + checked: ThemeEngine.theme === ThemeEngine.DarkTheme + onCheckedChanged: if(checked) ThemeEngine.theme = ThemeEngine.DarkTheme + } + } + } +} From 84d11111956af19d9edcd81cfcdd45bcc26e6a3b Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 28 Sep 2024 17:13:20 +0200 Subject: [PATCH 07/13] Add preferences menu option --- src/app/appmenubar.cpp | 11 +++++++++++ src/app/appmenubar.h | 3 +++ src/app/qml/main.qml | 6 ++++++ 3 files changed, 20 insertions(+) diff --git a/src/app/appmenubar.cpp b/src/app/appmenubar.cpp index 24229c1..6bd8b9c 100644 --- a/src/app/appmenubar.cpp +++ b/src/app/appmenubar.cpp @@ -69,6 +69,17 @@ AppMenuBar::AppMenuBar(QObject *parent) : m_editMenu->addItem(m_projectSettingsItem); connect(m_projectSettingsItem, &MenuItemModel::clicked, this, &AppMenuBar::projectSettingsTriggered); + // Edit -> (separator) + m_editSeparator = new MenuItemModel(m_editMenu); + m_editSeparator->setIsSeparator(true); + m_editMenu->addItem(m_editSeparator); + + // Edit -> Preferences + m_preferencesItem = new MenuItemModel(m_editMenu); + m_preferencesItem->setText(tr("Preferences...")); + m_editMenu->addItem(m_preferencesItem); + connect(m_preferencesItem, &MenuItemModel::clicked, this, &AppMenuBar::preferencesTriggered); + // Help menu m_helpMenu = new MenuModel(m_model); m_helpMenu->setTitle(tr("&Help")); diff --git a/src/app/appmenubar.h b/src/app/appmenubar.h index d957c84..5925021 100644 --- a/src/app/appmenubar.h +++ b/src/app/appmenubar.h @@ -52,6 +52,7 @@ class AppMenuBar : public QObject void fps60ModeChanged(); void muteChanged(); void projectSettingsTriggered(); + void preferencesTriggered(); void aboutAppTriggered(); private: @@ -72,6 +73,8 @@ class AppMenuBar : public QObject uicomponents::MenuItemModel *m_fps60ModeItem = nullptr; uicomponents::MenuItemModel *m_muteItem = nullptr; uicomponents::MenuItemModel *m_projectSettingsItem = nullptr; + uicomponents::MenuItemModel *m_editSeparator = nullptr; + uicomponents::MenuItemModel *m_preferencesItem = nullptr; uicomponents::MenuModel *m_helpMenu = nullptr; uicomponents::MenuItemModel *m_aboutAppItem = nullptr; diff --git a/src/app/qml/main.qml b/src/app/qml/main.qml index fc64cd5..aaf21c1 100644 --- a/src/app/qml/main.qml +++ b/src/app/qml/main.qml @@ -44,6 +44,10 @@ ApplicationWindow { projectSettingsDialog.open(); } + function onPreferencesTriggered() { + preferencesDialog.open(); + } + function onAboutAppTriggered() { aboutDialog.open(); } @@ -57,6 +61,8 @@ ApplicationWindow { projectPlayer: player } + PreferencesDialog { id: preferencesDialog } + CustomMessageDialog { id: unsupportedBlocksDialog title: qsTr("Warning") From d0332b570c765bf874574f4eed10ea400c49068e Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 28 Sep 2024 17:42:55 +0200 Subject: [PATCH 08/13] ThemeEngine: Add foregroundColor property --- src/ui/internal/themeengine.cpp | 8 ++++++++ src/ui/internal/themeengine.h | 3 +++ src/ui/ithemeengine.h | 1 + src/ui/test/themeengine.cpp | 1 + 4 files changed, 13 insertions(+) diff --git a/src/ui/internal/themeengine.cpp b/src/ui/internal/themeengine.cpp index 2f59f2d..cd99c74 100644 --- a/src/ui/internal/themeengine.cpp +++ b/src/ui/internal/themeengine.cpp @@ -29,6 +29,7 @@ void ThemeEngine::setTheme(Theme newTheme) void ThemeEngine::reloadTheme() { emit bgColorChanged(); + emit foregroundColorChanged(); emit borderColorChanged(); emit themeChanged(); } @@ -45,6 +46,13 @@ const QColor &ThemeEngine::bgColor() const return theme() == Theme::DarkTheme ? dark : light; } +const QColor &ThemeEngine::foregroundColor() const +{ + static const QColor dark = QColor(255, 255, 255); + static const QColor light = QColor(0, 0, 0); + return theme() == Theme::DarkTheme ? dark : light; +} + const QColor &ThemeEngine::borderColor() const { static const QColor dark = QColor(255, 255, 255, 64); diff --git a/src/ui/internal/themeengine.h b/src/ui/internal/themeengine.h index f0bccde..eaf1e50 100644 --- a/src/ui/internal/themeengine.h +++ b/src/ui/internal/themeengine.h @@ -19,6 +19,7 @@ class ThemeEngine : public IThemeEngine INJECT(ISettings, settings) Q_PROPERTY(Theme theme READ theme WRITE setTheme NOTIFY themeChanged FINAL) Q_PROPERTY(QColor bgColor READ bgColor NOTIFY bgColorChanged FINAL) + Q_PROPERTY(QColor foregroundColor READ foregroundColor NOTIFY foregroundColorChanged FINAL) Q_PROPERTY(QColor borderColor READ borderColor NOTIFY borderColorChanged FINAL) public: static std::shared_ptr instance(); @@ -29,11 +30,13 @@ class ThemeEngine : public IThemeEngine Q_INVOKABLE void resetTheme() override; const QColor &bgColor() const override; + const QColor &foregroundColor() const override; const QColor &borderColor() const override; signals: void themeChanged(); void bgColorChanged(); + void foregroundColorChanged(); void borderColorChanged(); private: diff --git a/src/ui/ithemeengine.h b/src/ui/ithemeengine.h index d17461b..e9ca779 100644 --- a/src/ui/ithemeengine.h +++ b/src/ui/ithemeengine.h @@ -30,6 +30,7 @@ class IThemeEngine virtual void resetTheme() = 0; virtual const QColor &bgColor() const = 0; + virtual const QColor &foregroundColor() const = 0; virtual const QColor &borderColor() const = 0; }; diff --git a/src/ui/test/themeengine.cpp b/src/ui/test/themeengine.cpp index aef5e8b..56e0fcd 100644 --- a/src/ui/test/themeengine.cpp +++ b/src/ui/test/themeengine.cpp @@ -24,6 +24,7 @@ class ThemeEngineTest : public testing::Test m_themeSpies.push_back(std::make_unique(&m_themeEngine, &ThemeEngine::themeChanged)); m_themeSpies.push_back(std::make_unique(&m_themeEngine, &ThemeEngine::bgColorChanged)); + m_themeSpies.push_back(std::make_unique(&m_themeEngine, &ThemeEngine::foregroundColorChanged)); m_themeSpies.push_back(std::make_unique(&m_themeEngine, &ThemeEngine::borderColorChanged)); } From fe1bf79fa580ba7d222ebe7e2b86b2343feb7b63 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 28 Sep 2024 17:44:27 +0200 Subject: [PATCH 09/13] Add ColorButton component --- src/uicomponents/CMakeLists.txt | 1 + src/uicomponents/ColorButton.qml | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 src/uicomponents/ColorButton.qml diff --git a/src/uicomponents/CMakeLists.txt b/src/uicomponents/CMakeLists.txt index 34d41ab..2ba3eb6 100644 --- a/src/uicomponents/CMakeLists.txt +++ b/src/uicomponents/CMakeLists.txt @@ -4,6 +4,7 @@ set(MODULE_QML_FILES CustomButton.qml CustomToolButton.qml AccentButton.qml + ColorButton.qml HoverToolTip.qml CustomMenuBar.qml CustomMenu.qml diff --git a/src/uicomponents/ColorButton.qml b/src/uicomponents/ColorButton.qml new file mode 100644 index 0000000..3b3fd9e --- /dev/null +++ b/src/uicomponents/ColorButton.qml @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Material +import ScratchCPP.Ui + +RoundButton { + property color color: Material.background + id: control + onColorChanged: Material.background = color + + Rectangle { + width: control.background.width + height: control.background.height + anchors.centerIn: parent + radius: width / 2 + color: Qt.rgba(0, 0, 0, 0) + border.color: ThemeEngine.borderColor + } + + Rectangle { + visible: control.checked + width: control.background.width + 6 + height: control.background.height + 6 + anchors.centerIn: parent + radius: width / 2 + color: Qt.rgba(0, 0, 0, 0) + border.color: ThemeEngine.foregroundColor + } +} From 5f9239e5163919934fee2a4623b648469fe2a478 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 28 Sep 2024 18:13:39 +0200 Subject: [PATCH 10/13] ThemeEngine: Add accentColor property --- src/ui/internal/themeengine.cpp | 13 +++++++++++++ src/ui/internal/themeengine.h | 5 +++++ src/ui/ithemeengine.h | 3 +++ src/ui/test/themeengine.cpp | 23 +++++++++++++++++++++++ 4 files changed, 44 insertions(+) diff --git a/src/ui/internal/themeengine.cpp b/src/ui/internal/themeengine.cpp index cd99c74..4fa8b5a 100644 --- a/src/ui/internal/themeengine.cpp +++ b/src/ui/internal/themeengine.cpp @@ -7,6 +7,7 @@ using namespace scratchcpp; static const QString MODULE = "ui"; static const QString THEME_KEY = "theme"; +static const QString ACCENT_COLOR_KEY = "accentColor"; std::shared_ptr ThemeEngine::m_instance = std::make_shared(); @@ -31,6 +32,7 @@ void ThemeEngine::reloadTheme() emit bgColorChanged(); emit foregroundColorChanged(); emit borderColorChanged(); + emit accentColorChanged(); emit themeChanged(); } @@ -59,3 +61,14 @@ const QColor &ThemeEngine::borderColor() const static const QColor light = QColor(0, 0, 0, 64); return theme() == Theme::DarkTheme ? dark : light; } + +QColor ThemeEngine::accentColor() const +{ + return settings()->getValue(MODULE, ACCENT_COLOR_KEY).value(); +} + +void ThemeEngine::setAccentColor(const QColor &newAccentColor) +{ + settings()->setValue(MODULE, ACCENT_COLOR_KEY, newAccentColor); + emit accentColorChanged(); +} diff --git a/src/ui/internal/themeengine.h b/src/ui/internal/themeengine.h index eaf1e50..c28d1a9 100644 --- a/src/ui/internal/themeengine.h +++ b/src/ui/internal/themeengine.h @@ -21,6 +21,7 @@ class ThemeEngine : public IThemeEngine Q_PROPERTY(QColor bgColor READ bgColor NOTIFY bgColorChanged FINAL) Q_PROPERTY(QColor foregroundColor READ foregroundColor NOTIFY foregroundColorChanged FINAL) Q_PROPERTY(QColor borderColor READ borderColor NOTIFY borderColorChanged FINAL) + Q_PROPERTY(QColor accentColor READ accentColor WRITE setAccentColor NOTIFY accentColorChanged FINAL) public: static std::shared_ptr instance(); @@ -33,11 +34,15 @@ class ThemeEngine : public IThemeEngine const QColor &foregroundColor() const override; const QColor &borderColor() const override; + QColor accentColor() const override; + void setAccentColor(const QColor &newAccentColor) override; + signals: void themeChanged(); void bgColorChanged(); void foregroundColorChanged(); void borderColorChanged(); + void accentColorChanged(); private: static std::shared_ptr m_instance; diff --git a/src/ui/ithemeengine.h b/src/ui/ithemeengine.h index e9ca779..a6eecad 100644 --- a/src/ui/ithemeengine.h +++ b/src/ui/ithemeengine.h @@ -32,6 +32,9 @@ class IThemeEngine virtual const QColor &bgColor() const = 0; virtual const QColor &foregroundColor() const = 0; virtual const QColor &borderColor() const = 0; + + virtual QColor accentColor() const = 0; + virtual void setAccentColor(const QColor &newAccentColor) = 0; }; } // namespace scratchcpp diff --git a/src/ui/test/themeengine.cpp b/src/ui/test/themeengine.cpp index 56e0fcd..b8e0d7d 100644 --- a/src/ui/test/themeengine.cpp +++ b/src/ui/test/themeengine.cpp @@ -13,6 +13,7 @@ using ::testing::_; static const QString MODULE = "ui"; static const QString THEME_KEY = "theme"; +static const QString ACCENT_COLOR_KEY = "accentColor"; class ThemeEngineTest : public testing::Test { @@ -26,6 +27,7 @@ class ThemeEngineTest : public testing::Test m_themeSpies.push_back(std::make_unique(&m_themeEngine, &ThemeEngine::bgColorChanged)); m_themeSpies.push_back(std::make_unique(&m_themeEngine, &ThemeEngine::foregroundColorChanged)); m_themeSpies.push_back(std::make_unique(&m_themeEngine, &ThemeEngine::borderColorChanged)); + m_themeSpies.push_back(std::make_unique(&m_themeEngine, &ThemeEngine::accentColorChanged)); } void TearDown() override { m_themeEngine.setsettings(nullptr); } @@ -70,3 +72,24 @@ TEST_F(ThemeEngineTest, Theme) m_themeEngine.reloadTheme(); checkThemeSpies(4); } + +TEST_F(ThemeEngineTest, AccentColor) +{ + QSignalSpy spy(&m_themeEngine, &ThemeEngine::accentColorChanged); + + EXPECT_CALL(*m_settings, getValue(MODULE, ACCENT_COLOR_KEY)).WillOnce(Return(QColor(255, 0, 0))); + ASSERT_EQ(m_themeEngine.accentColor(), QColor(255, 0, 0)); + ASSERT_EQ(spy.count(), 0); + + EXPECT_CALL(*m_settings, getValue(MODULE, ACCENT_COLOR_KEY)).WillOnce(Return(QColor(0, 255, 128))); + ASSERT_EQ(m_themeEngine.accentColor(), QColor(0, 255, 128)); + ASSERT_EQ(spy.count(), 0); + + EXPECT_CALL(*m_settings, setValue(MODULE, ACCENT_COLOR_KEY, QVariant(QColor(255, 255, 255)))); + m_themeEngine.setAccentColor(QColor(255, 255, 255)); + ASSERT_EQ(spy.count(), 1); + + EXPECT_CALL(*m_settings, setValue(MODULE, ACCENT_COLOR_KEY, QVariant(QColor(0, 0, 0)))); + m_themeEngine.setAccentColor(QColor(0, 0, 0)); + ASSERT_EQ(spy.count(), 2); +} From 4a60c112838943f846fedb9610dab00f85cce191 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 28 Sep 2024 18:14:01 +0200 Subject: [PATCH 11/13] Read accent color from ThemeEngine --- src/app/qml/main.qml | 2 +- src/uicomponents/CustomDialog.qml | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/qml/main.qml b/src/app/qml/main.qml index aaf21c1..7b42cb0 100644 --- a/src/app/qml/main.qml +++ b/src/app/qml/main.qml @@ -17,7 +17,7 @@ ApplicationWindow { visible: true title: Qt.application.displayName color: Material.background - Material.accent: "orange" + Material.accent: ThemeEngine.accentColor Material.theme: ThemeEngine.theme == ThemeEngine.DarkTheme ? Material.Dark : Material.Light onActiveFocusItemChanged: UiEngine.activeFocusItem = activeFocusItem diff --git a/src/uicomponents/CustomDialog.qml b/src/uicomponents/CustomDialog.qml index 167d1af..a8dd33f 100644 --- a/src/uicomponents/CustomDialog.qml +++ b/src/uicomponents/CustomDialog.qml @@ -123,10 +123,9 @@ Item { property alias contentsLoader: contentsLoader property alias buttonBoxLoader: buttonBoxLoader anchors.fill: parent - // TODO: Read colors from ThemeEngine color: ThemeEngine.bgColor Material.background: ThemeEngine.bgColor - //Material.accent: ThemeEngine.currentAccentColor + Material.accent: ThemeEngine.accentColor Material.theme: ThemeEngine.theme === ThemeEngine.DarkTheme ? Material.Dark : Material.Light ColumnLayout { From 519d1b57e5b7d1f773e06516f01812a6b67f8082 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 28 Sep 2024 18:14:16 +0200 Subject: [PATCH 12/13] PreferencesDialog: Add accent color option --- src/app/qml/dialogs/PreferencesDialog.qml | 67 +++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/src/app/qml/dialogs/PreferencesDialog.qml b/src/app/qml/dialogs/PreferencesDialog.qml index 62f5c0d..77b9f78 100644 --- a/src/app/qml/dialogs/PreferencesDialog.qml +++ b/src/app/qml/dialogs/PreferencesDialog.qml @@ -17,6 +17,31 @@ CustomDialog { ThemeEngine.reloadTheme(); } + QtObject { + id: priv + property int accentColorIndex: -1 + + readonly property list darkAccentColors: [ + Qt.rgba(0.85, 0.31, 0.33, 1), + Qt.rgba(0.85, 0.62, 0.31, 1), + Qt.rgba(0.85, 0.84, 0.31, 1), + Qt.rgba(0.39, 0.85, 0.31, 1), + Qt.rgba(0.31, 0.75, 0.85, 1), + Qt.rgba(0.32, 0.32, 0.85, 1), + Qt.rgba(0.68, 0.31, 0.85, 1), + ] + + readonly property list lightAccentColors: [ + Qt.rgba(0.75, 0.08, 0.09, 1), + Qt.rgba(0.75, 0.47, 0.08, 1), + Qt.rgba(0.75, 0.74, 0.08, 1), + Qt.rgba(0.17, 0.75, 0.08, 1), + Qt.rgba(0.08, 0.63, 0.75, 1), + Qt.rgba(0.08, 0.08, 0.75, 1), + Qt.rgba(0.54, 0.08, 0.75, 1), + ] + } + contentItem: ColumnLayout { // Themes Label { @@ -38,5 +63,47 @@ CustomDialog { onCheckedChanged: if(checked) ThemeEngine.theme = ThemeEngine.DarkTheme } } + + RowLayout { + Label { + text: qsTr("Accent color:") + } + + Repeater { + id: accentColors + model: ThemeEngine.theme == ThemeEngine.DarkTheme ? priv.darkAccentColors : priv.lightAccentColors + + ColorButton { + required property color modelData + required property int index + color: modelData + checked: { + if(ThemeEngine.accentColor === modelData) { + priv.accentColorIndex = index; + return true; + } else { + return false; + } + } + autoExclusive: true + checkable: true + onPressed: ThemeEngine.accentColor = modelData; + } + } + } + + Connections { + target: ThemeEngine + + function onThemeChanged() { + console.log(priv.accentColorIndex, ThemeEngine.theme); + + if(ThemeEngine.theme == ThemeEngine.DarkTheme) { + ThemeEngine.accentColor = priv.darkAccentColors[priv.accentColorIndex]; + } else { + ThemeEngine.accentColor = priv.lightAccentColors[priv.accentColorIndex]; + } + } + } } } From bc12722443c407abd331dffb09edf946aa2132be Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 28 Sep 2024 19:42:03 +0200 Subject: [PATCH 13/13] Set default accent color --- src/app/CMakeLists.txt | 3 +++ src/app/qml/Colors.qml | 29 +++++++++++++++++++++++ src/app/qml/dialogs/PreferencesDialog.qml | 28 ++++------------------ src/app/qml/main.qml | 5 ++++ src/ui/uimodule.cpp | 1 + 5 files changed, 43 insertions(+), 23 deletions(-) create mode 100644 src/app/qml/Colors.qml diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index abf5038..06209b0 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -10,11 +10,14 @@ qt_add_executable(${APP_TARGET} libraryinfo.h ) +set_source_files_properties(qml/Colors.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE) + qt_add_qml_module(${APP_TARGET} URI ScratchCPP VERSION 1.0 QML_FILES qml/main.qml + qml/Colors.qml qml/dialogs/AboutDialog.qml qml/dialogs/ProjectSettingsDialog.qml qml/dialogs/PreferencesDialog.qml diff --git a/src/app/qml/Colors.qml b/src/app/qml/Colors.qml new file mode 100644 index 0000000..4b6af59 --- /dev/null +++ b/src/app/qml/Colors.qml @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma Singleton +import QtQuick +import ScratchCPP.Ui + +QtObject { + readonly property list darkAccentColors: [ + Qt.rgba(0.85, 0.31, 0.33, 1), + Qt.rgba(0.85, 0.62, 0.31, 1), + Qt.rgba(0.85, 0.84, 0.31, 1), + Qt.rgba(0.39, 0.85, 0.31, 1), + Qt.rgba(0.31, 0.75, 0.85, 1), + Qt.rgba(0.32, 0.32, 0.85, 1), + Qt.rgba(0.68, 0.31, 0.85, 1), + ] + + readonly property list lightAccentColors: [ + Qt.rgba(0.75, 0.08, 0.09, 1), + Qt.rgba(0.75, 0.47, 0.08, 1), + Qt.rgba(0.75, 0.74, 0.08, 1), + Qt.rgba(0.17, 0.75, 0.08, 1), + Qt.rgba(0.08, 0.63, 0.75, 1), + Qt.rgba(0.08, 0.08, 0.75, 1), + Qt.rgba(0.54, 0.08, 0.75, 1), + ] + + readonly property color defaultAccentColor: ThemeEngine.theme == ThemeEngine.DarkTheme ? darkAccentColors[1] : lightAccentColors[1] +} diff --git a/src/app/qml/dialogs/PreferencesDialog.qml b/src/app/qml/dialogs/PreferencesDialog.qml index 77b9f78..00a6952 100644 --- a/src/app/qml/dialogs/PreferencesDialog.qml +++ b/src/app/qml/dialogs/PreferencesDialog.qml @@ -7,6 +7,8 @@ import ScratchCPP.Global import ScratchCPP.Ui import ScratchCPP.UiComponents +import ".." + CustomDialog { title: qsTr("Preferences") standardButtons: Dialog.Cancel | Dialog.Ok @@ -20,26 +22,6 @@ CustomDialog { QtObject { id: priv property int accentColorIndex: -1 - - readonly property list darkAccentColors: [ - Qt.rgba(0.85, 0.31, 0.33, 1), - Qt.rgba(0.85, 0.62, 0.31, 1), - Qt.rgba(0.85, 0.84, 0.31, 1), - Qt.rgba(0.39, 0.85, 0.31, 1), - Qt.rgba(0.31, 0.75, 0.85, 1), - Qt.rgba(0.32, 0.32, 0.85, 1), - Qt.rgba(0.68, 0.31, 0.85, 1), - ] - - readonly property list lightAccentColors: [ - Qt.rgba(0.75, 0.08, 0.09, 1), - Qt.rgba(0.75, 0.47, 0.08, 1), - Qt.rgba(0.75, 0.74, 0.08, 1), - Qt.rgba(0.17, 0.75, 0.08, 1), - Qt.rgba(0.08, 0.63, 0.75, 1), - Qt.rgba(0.08, 0.08, 0.75, 1), - Qt.rgba(0.54, 0.08, 0.75, 1), - ] } contentItem: ColumnLayout { @@ -71,7 +53,7 @@ CustomDialog { Repeater { id: accentColors - model: ThemeEngine.theme == ThemeEngine.DarkTheme ? priv.darkAccentColors : priv.lightAccentColors + model: ThemeEngine.theme == ThemeEngine.DarkTheme ? Colors.darkAccentColors : Colors.lightAccentColors ColorButton { required property color modelData @@ -99,9 +81,9 @@ CustomDialog { console.log(priv.accentColorIndex, ThemeEngine.theme); if(ThemeEngine.theme == ThemeEngine.DarkTheme) { - ThemeEngine.accentColor = priv.darkAccentColors[priv.accentColorIndex]; + ThemeEngine.accentColor = Colors.darkAccentColors[priv.accentColorIndex]; } else { - ThemeEngine.accentColor = priv.lightAccentColors[priv.accentColorIndex]; + ThemeEngine.accentColor = Colors.lightAccentColors[priv.accentColorIndex]; } } } diff --git a/src/app/qml/main.qml b/src/app/qml/main.qml index 7b42cb0..e30acea 100644 --- a/src/app/qml/main.qml +++ b/src/app/qml/main.qml @@ -172,4 +172,9 @@ ApplicationWindow { } } } + + Component.onCompleted: { + if(ThemeEngine.accentColor === Qt.rgba(0, 0, 0, 0)) + ThemeEngine.accentColor = Colors.defaultAccentColor; + } } diff --git a/src/ui/uimodule.cpp b/src/ui/uimodule.cpp index 96d16a3..b0bd617 100644 --- a/src/ui/uimodule.cpp +++ b/src/ui/uimodule.cpp @@ -32,4 +32,5 @@ void scratchcpp::ui::UiModule::registerExports() void UiModule::initSettings() { INIT_SETTINGS_KEY("theme", static_cast(IThemeEngine::Theme::DarkTheme)); + INIT_SETTINGS_KEY("accentColor", QColor(0, 0, 0, 0)); // default accent color should be set by the application }