From 932d5d68d3fbd3500f4c531e810caa9fed03f511 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sun, 1 Sep 2024 10:13:27 +0200 Subject: [PATCH 01/57] Set version to 0.4.0 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 515ed40..07fd3b8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.16) -project(scratchcpp-player VERSION 0.3.0 LANGUAGES CXX) +project(scratchcpp-player VERSION 0.4.0 LANGUAGES CXX) set(CMAKE_AUTOMOC ON) set(CMAKE_CXX_STANDARD_REQUIRED ON) From 004eb8c2e11e72f35aab5f2ca7b21a5cb5f2884f Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Mon, 16 Sep 2024 16:48:18 +0200 Subject: [PATCH 02/57] README: Mark HQ pen as finished --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ab9525a..c5d4246 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ When the project loads, click on the green flag button to run it. - [x] Turbo mode - [x] FPS options - [x] Stage size options -- [ ] HQ pen +- [x] HQ pen - [ ] Mute sounds - [ ] Theme options (light/dark mode, accent color, etc.) From 2bf3cfbe92f0fb5b7346ffeae780bc6f54b30209 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Mon, 16 Sep 2024 16:48:48 +0200 Subject: [PATCH 03/57] README: Mark mute sounds as finished --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c5d4246..e8c04ba 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ When the project loads, click on the green flag button to run it. - [x] FPS options - [x] Stage size options - [x] HQ pen -- [ ] Mute sounds +- [x] Mute sounds - [ ] Theme options (light/dark mode, accent color, etc.) See the [open issues](https://github.com/scratchcpp/scratchcpp-player/issues) for a full list of proposed features (and known issues). From 13bf0ed5ec3ce95d924d6d99e8b3546f09619a03 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Tue, 17 Sep 2024 10:39:04 +0200 Subject: [PATCH 04/57] Update scratchcpp-render to latest master --- scratchcpp-render | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scratchcpp-render b/scratchcpp-render index 3b639b2..5c24904 160000 --- a/scratchcpp-render +++ b/scratchcpp-render @@ -1 +1 @@ -Subproject commit 3b639b29f495132a7fed206c6de0c821091c64fc +Subproject commit 5c2490442f5c26ac350c591ea1f532b220629512 From 317c1a1708200fcce82ba295cbd7a315b175d07a Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Tue, 17 Sep 2024 12:01:32 +0200 Subject: [PATCH 05/57] Update scratchcpp-render to latest master --- scratchcpp-render | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scratchcpp-render b/scratchcpp-render index 5c24904..d31c850 160000 --- a/scratchcpp-render +++ b/scratchcpp-render @@ -1 +1 @@ -Subproject commit 5c2490442f5c26ac350c591ea1f532b220629512 +Subproject commit d31c850ed66b8e9b0f3aae586d854157254eef59 From 034e5c1afd915c351d093de43aa5b69ee8506b3e Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Tue, 17 Sep 2024 12:07:54 +0200 Subject: [PATCH 06/57] Use gcc11 on Ubuntu 20.04 --- .github/workflows/linux-build.yml | 7 +++++++ .github/workflows/release.yml | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/.github/workflows/linux-build.yml b/.github/workflows/linux-build.yml index 06f8776..cd2fde1 100644 --- a/.github/workflows/linux-build.yml +++ b/.github/workflows/linux-build.yml @@ -34,6 +34,13 @@ jobs: sudo apt-get update sudo apt-get install -y libxkbcommon-x11-0 shell: bash + - name: Install GCC11 + shell: bash + run: | + sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test + sudo apt update + sudo apt install gcc-11 g++-11 + sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 110 --slave /usr/bin/g++ g++ /usr/bin/g++-11 --slave /usr/bin/gcov gcov /usr/bin/gcov-11 ## Install Qt - if: contains(matrix.arch, 'amd64') name: Install Qt (Ubuntu) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0d071b3..6a77f09 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,6 +35,13 @@ jobs: sudo apt-get update sudo apt-get install -y libxkbcommon-x11-0 shell: bash + - name: Install GCC11 + shell: bash + run: | + sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test + sudo apt update + sudo apt install gcc-11 g++-11 + sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 110 --slave /usr/bin/g++ g++ /usr/bin/g++-11 --slave /usr/bin/gcov gcov /usr/bin/gcov-11 - name: Get version run: | version=$(LC_ALL=en_US.utf8 grep -oP 'project\([^)]*\s+VERSION\s+\K[0-9]+\.[0-9]+\.[0-9]+' CMakeLists.txt) From 3ebf25d07d484ebd8d2ec1b1d3bba0fc5de3a608 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Tue, 17 Sep 2024 14:12:22 +0200 Subject: [PATCH 07/57] Update scratchcpp-render to latest master --- scratchcpp-render | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scratchcpp-render b/scratchcpp-render index d31c850..063ff38 160000 --- a/scratchcpp-render +++ b/scratchcpp-render @@ -1 +1 @@ -Subproject commit d31c850ed66b8e9b0f3aae586d854157254eef59 +Subproject commit 063ff386091db6f5074bc5c0c4dc75f6f491b592 From 5ed8b1554198e158f62df33e888b6de4018b34b7 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Tue, 17 Sep 2024 15:28:11 +0200 Subject: [PATCH 08/57] Add CustomMessageDialog --- src/uicomponents/CMakeLists.txt | 1 + src/uicomponents/CustomMessageDialog.qml | 30 ++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 src/uicomponents/CustomMessageDialog.qml diff --git a/src/uicomponents/CMakeLists.txt b/src/uicomponents/CMakeLists.txt index 283f65b..34d41ab 100644 --- a/src/uicomponents/CMakeLists.txt +++ b/src/uicomponents/CMakeLists.txt @@ -10,6 +10,7 @@ set(MODULE_QML_FILES CustomMenuItem.qml CustomMenuSeparator.qml CustomDialog.qml + CustomMessageDialog.qml internal/CustomDialogButtonBox.qml ) set(MODULE_SRC diff --git a/src/uicomponents/CustomMessageDialog.qml b/src/uicomponents/CustomMessageDialog.qml new file mode 100644 index 0000000..0a5aed0 --- /dev/null +++ b/src/uicomponents/CustomMessageDialog.qml @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +CustomDialog { + id: root + property string text + property string informativeText + standardButtons: Dialog.Ok + + contentItem: RowLayout { + spacing: 25 + + // TODO: Add icon + + ColumnLayout { + Label { + text: root.text + font.pointSize: 14 + font.bold: true + } + + Label { + text: root.informativeText + } + } + } +} From 016d90be0db20eaba1eeb448a56f7a8c9781ae7b Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Tue, 17 Sep 2024 15:28:34 +0200 Subject: [PATCH 09/57] Warn about unsupported blocks Resolves: #46 --- src/app/qml/main.qml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/app/qml/main.qml b/src/app/qml/main.qml index 15e6c0a..748309e 100644 --- a/src/app/qml/main.qml +++ b/src/app/qml/main.qml @@ -57,6 +57,13 @@ ApplicationWindow { projectPlayer: player } + CustomMessageDialog { + id: unsupportedBlocksDialog + title: qsTr("Warning") + text: qsTr("This project contains unsupported blocks:") + informativeText: player.unsupportedBlocks.join('\r\n') + } + ColumnLayout { id: layout anchors.fill: parent @@ -153,6 +160,10 @@ ApplicationWindow { focus: true turboMode: AppMenuBar.turboMode mute: AppMenuBar.mute + onLoaded: { + if(unsupportedBlocks.length > 0) + unsupportedBlocksDialog.open() + } } } } From 61b4b3e2ec6dd69c15bba917b85b52d78994e8bb Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Wed, 25 Sep 2024 08:05:32 +0200 Subject: [PATCH 10/57] Update scratchcpp-render to v0.7.0 --- scratchcpp-render | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scratchcpp-render b/scratchcpp-render index 063ff38..40223f4 160000 --- a/scratchcpp-render +++ b/scratchcpp-render @@ -1 +1 @@ -Subproject commit 063ff386091db6f5074bc5c0c4dc75f6f491b592 +Subproject commit 40223f47de8c5b90e0a77e4b2f280849e03c4a9c 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 11/57] 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 12/57] 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 13/57] 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 14/57] 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 15/57] 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 16/57] 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 17/57] 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 18/57] 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 19/57] 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 20/57] 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 21/57] 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 22/57] 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 23/57] 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 } From 16d433501b349599fc86d99571753114945d7857 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 28 Sep 2024 22:19:54 +0200 Subject: [PATCH 24/57] CustomToolButton: Read foreground color from ThemeEngine --- src/uicomponents/CustomToolButton.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/uicomponents/CustomToolButton.qml b/src/uicomponents/CustomToolButton.qml index 1a16f76..f4bafd7 100644 --- a/src/uicomponents/CustomToolButton.qml +++ b/src/uicomponents/CustomToolButton.qml @@ -3,11 +3,12 @@ import QtQuick import QtQuick.Controls import QtQuick.Controls.Material +import ScratchCPP.Ui // Similar to ToolButton, but it's always a (rounded) rectangle // Double click events are not supported, use the clicked() signal like with a QPushButton CustomButton { - property color foregroundColor: Material.theme === Material.Dark ? "white" : "black" + property color foregroundColor: ThemeEngine.foregroundColor property string toolTipText property string accessibleDescription: "" signal clicked() From fcfc30bccea73f1573af8d1a0ef2e213bc778b78 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 28 Sep 2024 22:20:20 +0200 Subject: [PATCH 25/57] CustomToolButton: Remove background color --- src/uicomponents/CustomToolButton.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/uicomponents/CustomToolButton.qml b/src/uicomponents/CustomToolButton.qml index f4bafd7..fa98517 100644 --- a/src/uicomponents/CustomToolButton.qml +++ b/src/uicomponents/CustomToolButton.qml @@ -14,7 +14,6 @@ CustomButton { signal clicked() id: control font.capitalization: Font.MixedCase - Material.background: Qt.rgba(foregroundColor.r, foregroundColor.g, foregroundColor.b, 0.15) Material.foreground: foregroundColor icon.color: foregroundColor onReleased: clicked() From a93ce94c18f8789e4149ae0d1bacaf3bc8c813a5 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 28 Sep 2024 22:20:38 +0200 Subject: [PATCH 26/57] CustomButton: Adjust insets --- src/uicomponents/CustomButton.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/uicomponents/CustomButton.qml b/src/uicomponents/CustomButton.qml index 1830809..aa4e252 100644 --- a/src/uicomponents/CustomButton.qml +++ b/src/uicomponents/CustomButton.qml @@ -6,6 +6,6 @@ import QtQuick.Controls Button { id: control Material.roundedScale: Material.SmallScale - leftInset: icon.source + icon.name == "" ? 0 : -2 - rightInset: icon.source + icon.name == "" ? 0 : 5 + leftInset: icon.source + icon.name == "" ? 0 : 0 + rightInset: icon.source + icon.name == "" ? 0 : 3 } From 6e53623a19d7a4b1c6391a49817a3c23ce96c11b Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 28 Sep 2024 22:22:05 +0200 Subject: [PATCH 27/57] Register AppInfo in Global module --- src/app/qml/dialogs/AboutDialog.qml | 1 + src/global/globalmodule.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/qml/dialogs/AboutDialog.qml b/src/app/qml/dialogs/AboutDialog.qml index 0a704a2..bee2253 100644 --- a/src/app/qml/dialogs/AboutDialog.qml +++ b/src/app/qml/dialogs/AboutDialog.qml @@ -4,6 +4,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import ScratchCPP +import ScratchCPP.Global import ScratchCPP.Ui import ScratchCPP.UiComponents diff --git a/src/global/globalmodule.cpp b/src/global/globalmodule.cpp index 329aa96..b79055b 100644 --- a/src/global/globalmodule.cpp +++ b/src/global/globalmodule.cpp @@ -19,7 +19,7 @@ void GlobalModule::registerExports() m_appInfo = std::make_shared(); QQmlEngine::setObjectOwnership(m_appInfo.get(), QQmlEngine::CppOwnership); - qmlRegisterSingletonInstance("ScratchCPP.Ui", 1, 0, "AppInfo", m_appInfo.get()); + qmlRegisterSingletonInstance("ScratchCPP.Global", 1, 0, "AppInfo", m_appInfo.get()); modularity::ioc()->registerExport(m_appInfo); modularity::ioc()->registerExport(FilePaths::instance()); From d5a9fe6734bf7eb424549b6e0e19db2653529b0a Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 28 Sep 2024 22:24:28 +0200 Subject: [PATCH 28/57] Add manual executable finalization https://doc.qt.io/qt-6/qt-add-library.html#qt-add-library-finalization --- src/app/CMakeLists.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 06209b0..94eaa90 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -8,6 +8,7 @@ qt_add_executable(${APP_TARGET} appmenubar.h libraryinfo.cpp libraryinfo.h + MANUAL_FINALIZATION ) set_source_files_properties(qml/Colors.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE) @@ -23,6 +24,8 @@ qt_add_qml_module(${APP_TARGET} qml/dialogs/PreferencesDialog.qml ) +qt_finalize_executable(${APP_TARGET}) + set(QML_IMPORT_PATH "${QML_IMPORT_PATH};${CMAKE_CURRENT_LIST_DIR}" CACHE STRING "Qt Creator extra QML import paths" FORCE From d3f35007d74ac3c5dc3485031cebbb08b6266f4d Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 28 Sep 2024 23:06:26 +0200 Subject: [PATCH 29/57] PreferencesDialog: Remove debug logging --- src/app/qml/dialogs/PreferencesDialog.qml | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app/qml/dialogs/PreferencesDialog.qml b/src/app/qml/dialogs/PreferencesDialog.qml index 00a6952..b7bbc99 100644 --- a/src/app/qml/dialogs/PreferencesDialog.qml +++ b/src/app/qml/dialogs/PreferencesDialog.qml @@ -78,8 +78,6 @@ CustomDialog { target: ThemeEngine function onThemeChanged() { - console.log(priv.accentColorIndex, ThemeEngine.theme); - if(ThemeEngine.theme == ThemeEngine.DarkTheme) { ThemeEngine.accentColor = Colors.darkAccentColors[priv.accentColorIndex]; } else { From 5157cda1d3536a2c6a8f2542cf0ec77066e0f100 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 28 Sep 2024 23:06:58 +0200 Subject: [PATCH 30/57] CustomToolButton: Improve highlighted color --- src/uicomponents/CustomToolButton.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/uicomponents/CustomToolButton.qml b/src/uicomponents/CustomToolButton.qml index fa98517..f90ca1c 100644 --- a/src/uicomponents/CustomToolButton.qml +++ b/src/uicomponents/CustomToolButton.qml @@ -14,6 +14,7 @@ CustomButton { signal clicked() id: control font.capitalization: Font.MixedCase + Material.background: ThemeEngine.theme == ThemeEngine.DarkTheme ? (highlighted ? Qt.darker(ThemeEngine.accentColor, 2) : Qt.rgba(0.25, 0.24, 0.25, 1)) : (highlighted ? Qt.lighter(ThemeEngine.accentColor, 2) : Qt.rgba(0.84, 0.84, 0.84, 1)) Material.foreground: foregroundColor icon.color: foregroundColor onReleased: clicked() From b917dc47825d100a5562ade33e561d51cde814e8 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 28 Sep 2024 23:07:24 +0200 Subject: [PATCH 31/57] Highlight green flag if project is running Resolves: #40 --- src/app/qml/main.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/qml/main.qml b/src/app/qml/main.qml index e30acea..7c6eb52 100644 --- a/src/app/qml/main.qml +++ b/src/app/qml/main.qml @@ -82,6 +82,7 @@ ApplicationWindow { id: greenFlagButton icon.name: "green_flag" icon.color: "transparent" + highlighted: player.running onClicked: { switch (KeyboardInfo.keyboardModifiers()) { case Qt.ShiftModifier: From a7048fde22740f181beab93f9b0d37e7d256d9e1 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 28 Sep 2024 23:47:15 +0200 Subject: [PATCH 32/57] Update scratchcpp-render to v0.7.1 --- scratchcpp-render | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scratchcpp-render b/scratchcpp-render index 40223f4..9dc264a 160000 --- a/scratchcpp-render +++ b/scratchcpp-render @@ -1 +1 @@ -Subproject commit 40223f47de8c5b90e0a77e4b2f280849e03c4a9c +Subproject commit 9dc264a9dbb737e109c1e05a0550c5ae8e6f3747 From 964fafb0e332c7c165f5e6d099976015065ea818 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sun, 29 Sep 2024 00:29:40 +0200 Subject: [PATCH 33/57] README: Mark theme options as finished --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e8c04ba..5f8c6c7 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ When the project loads, click on the green flag button to run it. - [x] Stage size options - [x] HQ pen - [x] Mute sounds -- [ ] Theme options (light/dark mode, accent color, etc.) +- [x] Theme options (light/dark mode, accent color, etc.) See the [open issues](https://github.com/scratchcpp/scratchcpp-player/issues) for a full list of proposed features (and known issues). From 4486d40942a23565f31929ce62895ee4c8c22557 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 5 Oct 2024 11:04:36 +0200 Subject: [PATCH 34/57] Set version to 0.5.0 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 07fd3b8..f784d0f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.16) -project(scratchcpp-player VERSION 0.4.0 LANGUAGES CXX) +project(scratchcpp-player VERSION 0.5.0 LANGUAGES CXX) set(CMAKE_AUTOMOC ON) set(CMAKE_CXX_STANDARD_REQUIRED ON) From b82e38c65ab4645b76b0882b2c604338898211d5 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 5 Oct 2024 11:04:39 +0200 Subject: [PATCH 35/57] Fix main window background color --- src/app/qml/main.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/qml/main.qml b/src/app/qml/main.qml index 7c6eb52..e3e0420 100644 --- a/src/app/qml/main.qml +++ b/src/app/qml/main.qml @@ -16,7 +16,7 @@ ApplicationWindow { minimumHeight: menuBar.height + layout.implicitHeight + layout.anchors.margins * 2 visible: true title: Qt.application.displayName - color: Material.background + color: ThemeEngine.bgColor Material.accent: ThemeEngine.accentColor Material.theme: ThemeEngine.theme == ThemeEngine.DarkTheme ? Material.Dark : Material.Light onActiveFocusItemChanged: UiEngine.activeFocusItem = activeFocusItem From dd335caf13c4941a36f46f8ce7a0ece473acb860 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Thu, 24 Oct 2024 18:51:35 +0200 Subject: [PATCH 36/57] Add IconLabel to UI components --- src/uicomponents/CMakeLists.txt | 1 + src/uicomponents/IconLabel.qml | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 src/uicomponents/IconLabel.qml diff --git a/src/uicomponents/CMakeLists.txt b/src/uicomponents/CMakeLists.txt index 2ba3eb6..8475ef4 100644 --- a/src/uicomponents/CMakeLists.txt +++ b/src/uicomponents/CMakeLists.txt @@ -12,6 +12,7 @@ set(MODULE_QML_FILES CustomMenuSeparator.qml CustomDialog.qml CustomMessageDialog.qml + IconLabel.qml internal/CustomDialogButtonBox.qml ) set(MODULE_SRC diff --git a/src/uicomponents/IconLabel.qml b/src/uicomponents/IconLabel.qml new file mode 100644 index 0000000..e02f687 --- /dev/null +++ b/src/uicomponents/IconLabel.qml @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import QtQuick.Controls.impl as Controls + +Controls.IconLabel {} From 808826543f6a6382b21fa231e4a06d238e142d01 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Mon, 4 Nov 2024 16:19:11 +0100 Subject: [PATCH 37/57] Update scratchcpp-render to v0.8.0 --- scratchcpp-render | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scratchcpp-render b/scratchcpp-render index 9dc264a..7b1ced7 160000 --- a/scratchcpp-render +++ b/scratchcpp-render @@ -1 +1 @@ -Subproject commit 9dc264a9dbb737e109c1e05a0550c5ae8e6f3747 +Subproject commit 7b1ced7b9e6edc56f961e1cdf7a9507462bc9158 From 9e42cb0c65b836f92dde7805adf48287fb55dc40 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Mon, 4 Nov 2024 16:19:49 +0100 Subject: [PATCH 38/57] Add FPS counter Resolves: #30 --- src/app/qml/dialogs/ProjectSettingsDialog.qml | 8 ++++++++ src/app/qml/main.qml | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/app/qml/dialogs/ProjectSettingsDialog.qml b/src/app/qml/dialogs/ProjectSettingsDialog.qml index 53fd2cd..74352e9 100644 --- a/src/app/qml/dialogs/ProjectSettingsDialog.qml +++ b/src/app/qml/dialogs/ProjectSettingsDialog.qml @@ -9,6 +9,7 @@ import ScratchCPP.Render CustomDialog { property ProjectPlayer projectPlayer: null + property bool showCurrentFps: false title: qsTr("Project settings") standardButtons: Dialog.Close @@ -44,6 +45,13 @@ CustomDialog { onCheckedChanged: projectPlayer.hqPen = checked } + CheckBox { + id: showFpsCheckBox + text: qsTr("Show current FPS") + checked: showCurrentFps + onCheckedChanged: showCurrentFps = checked + } + // Remove limits Label { text: qsTr("Remove limits") diff --git a/src/app/qml/main.qml b/src/app/qml/main.qml index e3e0420..aceacd2 100644 --- a/src/app/qml/main.qml +++ b/src/app/qml/main.qml @@ -170,6 +170,25 @@ ApplicationWindow { onLoaded: { if(unsupportedBlocks.length > 0) unsupportedBlocksDialog.open() + + currentFps.enable = true; + } + + Loader { + id: currentFps + property bool enable: false + active: projectSettingsDialog.showCurrentFps && enable + anchors.right: player.stageRect.right + anchors.top: player.stageRect.top + + sourceComponent: Text { + text: player.renderFps + font.pointSize: 20 * player.stageScale + font.family: "mono" + color: Qt.rgba(1, 0, 0, 0.75) + style: Text.Outline + styleColor: "black" + } } } } From cef3b4f71f6f581f066ae74be3a31abe1f55d687 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Mon, 4 Nov 2024 16:25:12 +0100 Subject: [PATCH 39/57] Update to Qt 6.8 --- .github/workflows/linux-build.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/utests.yml | 2 +- .github/workflows/windows-build.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/linux-build.yml b/.github/workflows/linux-build.yml index cd2fde1..2e5e847 100644 --- a/.github/workflows/linux-build.yml +++ b/.github/workflows/linux-build.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - qt-version: ['6.7'] + qt-version: ['6.8'] qt-target: ['desktop'] qt-modules: [''] arch: ['amd64'] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6a77f09..afacd16 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - qt-version: ['6.7'] + qt-version: ['6.8'] qt-target: ['desktop'] qt-modules: [''] arch: ['amd64'] @@ -99,7 +99,7 @@ jobs: runs-on: windows-latest strategy: matrix: - qt-version: ['6.7'] + qt-version: ['6.8'] qt-target: ['desktop'] qt-modules: [''] mingw-version: ['11.2.0'] diff --git a/.github/workflows/utests.yml b/.github/workflows/utests.yml index c0ca8bc..60d23e9 100644 --- a/.github/workflows/utests.yml +++ b/.github/workflows/utests.yml @@ -26,7 +26,7 @@ jobs: - name: Install Qt uses: jurplel/install-qt-action@v4 with: - version: '6.7.*' + version: '6.8.*' arch: 'linux_gcc_64' - name: Configure CMake run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DSCRATCHCPP_PLAYER_BUILD_UNIT_TESTS=ON diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index f087c6a..a595232 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -12,7 +12,7 @@ jobs: runs-on: windows-latest strategy: matrix: - qt-version: ['6.7'] + qt-version: ['6.8'] qt-target: ['desktop'] qt-modules: [''] mingw-version: ['11.2.0'] From 871f46ced1d0033dbf928c5326b20d7b61cd69a1 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Mon, 4 Nov 2024 17:32:49 +0100 Subject: [PATCH 40/57] Link against Qt DBus on Linux --- CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index f784d0f..2ffd010 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,6 +10,11 @@ option(SCRATCHCPP_PLAYER_BUILD_UNIT_TESTS "Build unit tests" ON) find_package(Qt6 6.6 COMPONENTS Quick QuickControls2 Widgets OpenGLWidgets REQUIRED) set(QT_LIBS Qt6::Quick Qt6::QuickControls2 Qt6::Widgets Qt6::OpenGLWidgets) +if (LINUX) + find_package(Qt6 6.6 COMPONENTS DBus REQUIRED) + set(QT_LIBS ${QT_LIBS} Qt6::DBus) +endif() + if (SCRATCHCPP_PLAYER_BUILD_UNIT_TESTS) set(GTEST_DIR thirdparty/googletest) add_subdirectory(${PROJECT_SOURCE_DIR}/${GTEST_DIR} ${CMAKE_CURRENT_BINARY_DIR}/${GTEST_DIR}) From e6534d60fd0614bf2526ca7ee5f3b0cfcd9f1bac Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Mon, 4 Nov 2024 17:33:13 +0100 Subject: [PATCH 41/57] UiEngine: Add useNativeMenuBar() method --- src/ui/internal/uiengine.cpp | 25 +++++++++++++++++++++++++ src/ui/internal/uiengine.h | 4 ++++ 2 files changed, 29 insertions(+) diff --git a/src/ui/internal/uiengine.cpp b/src/ui/internal/uiengine.cpp index 4ca1d0a..d4d3440 100644 --- a/src/ui/internal/uiengine.cpp +++ b/src/ui/internal/uiengine.cpp @@ -2,6 +2,10 @@ #include #include +#ifdef Q_OS_LINUX +#include +#include +#endif #include "uiengine.h" @@ -66,3 +70,24 @@ void UiEngine::setActiveFocusItem(QQuickItem *newActiveFocusItem) m_activeFocusItem = newActiveFocusItem; emit activeFocusItemChanged(); } + +bool UiEngine::useNativeMenuBar() const +{ +#if defined(Q_OS_MACOS) +#if QT_VERSION < QT_VERSION_CHECK(6, 8, 0) + return true; +#else + // Since Qt 6.8, Qt Quick Controls menu bar is native + return false; +#endif +#elif defined(Q_OS_LINUX) + const QDBusConnection connection = QDBusConnection::sessionBus(); + static const QString registrarService = QStringLiteral("com.canonical.AppMenu.Registrar"); + if (const auto iface = connection.interface()) + return iface->isServiceRegistered(registrarService); + else + return false; +#else + return false; +#endif +} diff --git a/src/ui/internal/uiengine.h b/src/ui/internal/uiengine.h index 3d01231..f0d2389 100644 --- a/src/ui/internal/uiengine.h +++ b/src/ui/internal/uiengine.h @@ -19,6 +19,7 @@ class UiEngine { Q_OBJECT Q_PROPERTY(QQuickItem *activeFocusItem READ activeFocusItem WRITE setActiveFocusItem NOTIFY activeFocusItemChanged) + Q_PROPERTY(bool useNativeMenuBar READ useNativeMenuBar NOTIFY useNativeMenuBarChanged FINAL) public: explicit UiEngine(QObject *parent = nullptr); @@ -34,8 +35,11 @@ class UiEngine QQuickItem *activeFocusItem() const; void setActiveFocusItem(QQuickItem *newActiveFocusItem); + bool useNativeMenuBar() const; + signals: void activeFocusItemChanged(); + void useNativeMenuBarChanged(); private: static std::shared_ptr m_instance; From f5f0a80cc6f6a494f3c8edece52b4f564015cfa8 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Mon, 4 Nov 2024 17:33:40 +0100 Subject: [PATCH 42/57] CustomMenuBar: Enable native menu bar code --- src/uicomponents/CustomMenuBar.qml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/uicomponents/CustomMenuBar.qml b/src/uicomponents/CustomMenuBar.qml index a495f24..3a789e1 100644 --- a/src/uicomponents/CustomMenuBar.qml +++ b/src/uicomponents/CustomMenuBar.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Controls +import Qt.labs.platform as Platform import ScratchCPP.Ui import ScratchCPP.UiComponents @@ -22,8 +23,7 @@ MenuBar { } function getComponentString(typeName) { - //var imports = "import QtQuick; import QtQuick.Controls; import Qt.labs.platform as Platform;" - var imports = "import QtQuick; import QtQuick.Controls;" + var imports = "import QtQuick; import QtQuick.Controls; import Qt.labs.platform as Platform;" return imports + " " + typeName + " {}"; } @@ -103,11 +103,11 @@ MenuBar { } function reload() { - /*if(nativeMenuBarEnabled) + if(UiEngine.useNativeMenuBar) { root.visible = false; return; - }*/ + } var oldObjects = []; @@ -132,14 +132,14 @@ MenuBar { Component.onCompleted: reload(); - /*onEnabledChanged: { + onEnabledChanged: { if(platformMenuBarLoader.active) platformMenuBarLoader.item.reload(); } Loader { id: platformMenuBarLoader - active: // whether the native menu bar is active + active: UiEngine.useNativeMenuBar sourceComponent: Platform.MenuBar { id: platformMenuBar @@ -149,14 +149,14 @@ MenuBar { createMenuBar(platformMenuBar, "Platform.Menu", "Platform.MenuItem", "Platform.MenuSeparator"); } - Connections { - target: QmlUtils + /*Connections { + target: // TODO: Add a class for the menu bar reload signal function onMenuBarReloadTriggered() { platformMenuBar.reload(); } - } + }*/ Component.onCompleted: reload(); } - }*/ + } } From 774eec61781f06cb86248105a4c063cc6d449a50 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sun, 8 Dec 2024 11:22:51 +0100 Subject: [PATCH 43/57] Set version to 0.6.0 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2ffd010..e446598 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.16) -project(scratchcpp-player VERSION 0.5.0 LANGUAGES CXX) +project(scratchcpp-player VERSION 0.6.0 LANGUAGES CXX) set(CMAKE_AUTOMOC ON) set(CMAKE_CXX_STANDARD_REQUIRED ON) From e237813ee4086c2b4120bffc2ac5cca6d3e88de0 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Thu, 23 Jan 2025 22:00:31 +0100 Subject: [PATCH 44/57] Enable arm64 Linux builds --- .github/workflows/linux-build.yml | 37 ++++++++++++------------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/.github/workflows/linux-build.yml b/.github/workflows/linux-build.yml index 2e5e847..619ba76 100644 --- a/.github/workflows/linux-build.yml +++ b/.github/workflows/linux-build.yml @@ -12,11 +12,18 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - qt-version: ['6.8'] - qt-target: ['desktop'] - qt-modules: [''] - arch: ['amd64'] - ubuntu-version: ['20.04'] + include: + - qt-host: 'linux_arm64' + qt-version: '6.8' + qt-target: 'desktop' + qt-modules: '' + qt-arch: 'linux_gcc_arm64' + arch: 'amd64' + - qt-version: '6.8' + qt-target: 'desktop' + qt-modules: '' + qt-arch: 'linux_gcc_64' + arch: 'aarch64' steps: - uses: actions/checkout@v3 with: @@ -47,26 +54,10 @@ jobs: uses: jurplel/install-qt-action@v4 with: version: ${{ matrix.qt-version }} - host: 'linux' - arch: 'linux_gcc_64' + host: ${{ matrix.qt-host}} + arch: ${{ matrix.qt-arch }} target: ${{ matrix.qt-target }} modules: ${{ matrix.qt-modules }} - - if: "!contains(matrix.arch, 'amd64')" - name: Restore cross-compiled Qt from cache - id: cache-qt-cross - uses: actions/cache@v3 - with: - path: | - ./qt-host/ - ./qt-cross/ - ./sysroot/ - key: qt-cross-${{ runner.os }}-${{ matrix.qt-version }}-${{ matrix.qt-target }}-${{ matrix.qt-modules }}-${{ matrix.arch }} - restore-keys: - qt-cross-${{ runner.os }}-${{ matrix.qt-version }}-${{ matrix.qt-target }}-${{ matrix.qt-modules }}-${{ matrix.arch }} - - if: "!contains(matrix.arch, 'amd64') && steps.cache-qt-cross.outputs.cache-hit != 'true'" - name: Cross-compile Qt - shell: bash - run: .ci/build_qt6.sh "${{ matrix.qt-version }}" "${{ matrix.qt-modules }}" "${{ matrix.arch }}" ## Build - if: "!contains(matrix.arch, 'amd64')" name: Prepare cross-compilation environment From d8475097c9922cee20f38e6cd9152cbf4686a96f Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Thu, 23 Jan 2025 22:06:11 +0100 Subject: [PATCH 45/57] Update to upload-artifact@v4 --- .github/workflows/linux-build.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/windows-build.yml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/linux-build.yml b/.github/workflows/linux-build.yml index 619ba76..c9954fe 100644 --- a/.github/workflows/linux-build.yml +++ b/.github/workflows/linux-build.yml @@ -68,7 +68,7 @@ jobs: shell: 'script -q -e -c "bash {0}"' ## Upload - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: build-Qt-${{ matrix.qt-version }}-${{ matrix.arch }} path: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index afacd16..fc7d072 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -88,7 +88,7 @@ jobs: shell: 'script -q -e -c "bash {0}"' # Upload artifacts - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: build-linux-${{ matrix.arch }} path: | @@ -190,7 +190,7 @@ jobs: shell: bash # Upload artifacts - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: build-windows path: '*installer.exe' diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index a595232..d6f3f80 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -75,7 +75,7 @@ jobs: shell: bash ## Upload - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: build-Qt-${{ matrix.qt-version }} path: win_release/ @@ -98,7 +98,7 @@ jobs: shell: bash # Upload installer - name: Upload installer - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: installer-Qt-${{ matrix.qt-version }} path: '*installer.exe' From 99f564881210634e88e0bb8d10d7e7c49b115e88 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Thu, 23 Jan 2025 22:01:25 +0100 Subject: [PATCH 46/57] Refactor cross compilation scripts --- .ci/build_appimage.sh | 85 +++++++++++++++++---------- .ci/build_qt6.sh | 98 ++++++++----------------------- .ci/common/build.sh | 2 + .ci/install_cross_compiler.sh | 2 - .ci/prepare_cross_build.sh | 70 +++++++++++++--------- .ci/qt6-toolchain.cmake | 79 +++++++++++++++++++++++++ .ci/qt6_deps.sh | 2 +- .github/workflows/linux-build.yml | 59 +++++++++++++++---- 8 files changed, 252 insertions(+), 145 deletions(-) create mode 100644 .ci/qt6-toolchain.cmake diff --git a/.ci/build_appimage.sh b/.ci/build_appimage.sh index f558edb..6bbe065 100755 --- a/.ci/build_appimage.sh +++ b/.ci/build_appimage.sh @@ -7,9 +7,12 @@ sudo () } # Build -if [[ "$1" != "0" ]]; then - .ci/common/build.sh appimage_build linux || exit 1 +if [[ "$1" == "" ]]; then + PLATFORM=linux +else + PLATFORM="linux_$1" fi +.ci/common/build.sh appimage_build $PLATFORM || exit 1 repo_dir=$(pwd) cd appimage_build @@ -45,45 +48,63 @@ cmake .. && make -j$(nproc --all) && mv src/linuxdeploy-plugin-appimage ../.. && cd ../.. && -rm -rf plugin-appimage && - -# Build AppImageKit -sudo apt install -y snapd squashfs-tools && -sudo snap install docker && -git clone https://github.com/AppImage/AppImageKit --recurse-submodules && -cd AppImageKit && -sudo env ARCH=$(arch) bash ci/build.sh -sudo cp out/appimagetool /usr/bin/ && -sudo cp out/digest /usr/bin/ && -sudo cp out/validate /usr/bin/ && -cd .. && -sudo mkdir -p /usr/lib/appimagekit && -sudo ln -s /usr/bin/mksquashfs /usr/lib/appimagekit/mksquashfs && +rm -rf plugin-appimage + +# Download appimagetool +wget https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage && +mv appimagetool-*.AppImage appimagetool +chmod +x appimagetool +export PATH=$(pwd):$PATH # Install patchelf from PyPI (see https://github.com/linuxdeploy/linuxdeploy-plugin-qt/issues/133#issuecomment-1608168363) sudo apt install -y python3-pip +python3 -m venv .venv +source .venv/bin/activate pip3 install patchelf -export PATH=$PATH:~/.local/bin +patchelf --version + +# Use custom ldd and strip +if [[ "$PLATFORM" == "linux_aarch64" ]]; then + ln -s /usr/bin/${BUILD_TOOLCHAIN_PREFIX}strip strip + sudo cp ../.ci/bin/xldd /usr/bin/${BUILD_TOOLCHAIN_PREFIX}ldd + ln -s /usr/bin/${BUILD_TOOLCHAIN_PREFIX}ldd ldd + export CT_XLDD_ROOT="$BUILD_SYSROOT_PATH" +fi + +# Set LD_LIBRARY_PATH (directories with *.so files) +# TODO: Installing with cmake is probably a better idea +LD_LIBRARY_PATH="" + +for file in $(find . -type f -name "*.so*"); do + dir=$(dirname "$file") + if [[ ":$LD_LIBRARY_PATH:" != *":$dir:"* ]]; then + LD_LIBRARY_PATH="$LD_LIBRARY_PATH:`readlink -f $dir`" + fi +done + +LD_LIBRARY_PATH=${LD_LIBRARY_PATH#:} +LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$QT_ROOT_DIR/lib" # Qt +export LD_LIBRARY_PATH +echo "LD_LIBRARY_PATH set to: $LD_LIBRARY_PATH" # Build AppImage +if [[ "$PLATFORM" == "linux_aarch64" ]]; then + wget https://github.com/AppImage/type2-runtime/releases/download/continuous/runtime-aarch64 + export ARCH=arm_aarch64 + APPIMAGE_ARCH=aarch64 + export LDAI_RUNTIME_FILE=runtime-aarch64 + export QEMU_LD_PREFIX="$BUILD_SYSROOT_PATH" + export PATH="$QT_ROOT_DIR/bin:$PATH" # use cross Qt + export PATH="$PATH:$QT_HOST_PATH/libexec" +else + APPIMAGE_ARCH=x86_64 +fi + export QML_SOURCES_PATHS=$(pwd)/src && -export EXTRA_QT_PLUGINS="svg;" && -export LDAI_UPDATE_INFORMATION="${appimage_zsync_prefix}${app_name}*-${APPIMAGE_ARCH-$(arch)}.AppImage.zsync" +export EXTRA_QT_MODULES="qml;svg;" && +export LDAI_UPDATE_INFORMATION="${appimage_zsync_prefix}${app_name}*-${APPIMAGE_ARCH}.AppImage.zsync" echo "AppImage update information: ${LDAI_UPDATE_INFORMATION}" -case "$(qmake -query QMAKE_XSPEC)" in - linux-arm-gnueabi-g++) - wget https://github.com/AppImage/AppImageKit/releases/download/continuous/runtime-armhf - export ARCH=arm - export LDAI_RUNTIME_FILE=runtime-armhf - ;; - linux-aarch64-gnu-g++) - wget https://github.com/AppImage/AppImageKit/releases/download/continuous/runtime-aarch64 - export ARCH=arm_aarch64 - export LDAI_RUNTIME_FILE=runtime-aarch64 - ;; -esac - ./linuxdeploy --appdir AppDir -e src/app/${executable_name} -i $repo_dir/res/${executable_name}.png -d $repo_dir/release/appimage.desktop --plugin qt --output appimage mv *.AppImage* $repo_dir diff --git a/.ci/build_qt6.sh b/.ci/build_qt6.sh index 8ccca89..15542e6 100755 --- a/.ci/build_qt6.sh +++ b/.ci/build_qt6.sh @@ -3,102 +3,56 @@ sudo apt install -y lsb-release qt_version="$1" -qt_modules="$(echo $2 | tr ' ' ',')" + +if [[ "$2" == "" ]]; then + qt_modules="" +else + qt_modules=",$(echo $2 | tr ' ' ',')" +fi + target_arch="$3" root_path="$(pwd)" -sysroot_path="${root_path}/sysroot" -sysroot_ubuntu_version="$(lsb_release -rs).1" -sysroot_ubuntu_codename="$(lsb_release -cs)" -host_prefix="${root_path}/qt-host" -cross_prefix="${root_path}/qt-cross" +#host_prefix="${root_path}/qt-host" +host_prefix="$QT_HOST_PATH" +cross_prefix="$QT_CROSS_PATH" target_prefix="/usr/local/qt" -toolchain_config="${root_path}/.ci/qt6-toolchain.cmake" - -case "$target_arch" in - aarch64) - target_arch_name="armv8-a" - toolchain_name="aarch64-linux-gnu" - target_platform="linux-aarch64-gnu-g++" - ;; - armv7) - target_arch_name="armv7-a" - toolchain_name="arm-linux-gnueabihf" - target_platform="linux-arm-gnueabi-g++" - ;; -esac - -toolchain_prefix="${toolchain_name}-" - -case "$target_arch" in - aarch64) - target_arch_debian_name="arm64" - ;; - armv7) - target_arch_debian_name="armhf" - ;; -esac echo "Qt version to build: ${qt_version}" echo "Qt modules: ${qt_modules}" -echo "Target architecture: ${target_arch} (${target_arch_name})" - -# Install dependencies -${root_path}/.ci/qt6_deps.sh || exit 1 -${root_path}/.ci/install_cross_compiler.sh "${target_arch}" || exit 1 -sudo apt install -y qemu-user-static || exit 1 -sudo apt install -y symlinks || exit 1 +echo "Target architecture: ${target_arch} (${BUILD_ARCH_NAME})" # Clone Qt git clone https://github.com/qt/qt5 qt || exit 1 cd qt -git checkout "v$qt_version" || exit 1 -./init-repository --module-subset=qtbase,qttools,qtdeclarative,qtsvg,${qt_modules} || exit 1 +git checkout $(git tag | grep '^v6\.8\.[0-9]*$' | sort -V | tail -n 1) || exit 1 +./init-repository --module-subset=qtbase,qttools,qtdeclarative,qtsvg${qt_modules} || exit 1 # Build Qt (host) -mkdir host-build -cd host-build -echo "Building host Qt..." -../configure -release -nomake examples -nomake tests -opensource -confirm-license -prefix "$host_prefix" || exit 1 -cmake --build . --parallel $(nproc --all) || exit 1 -echo "Installing host Qt..." -cmake --install . || exit 1 -cd .. -rm -rf host-build - -# Prepare sysroot -echo "Preparing sysroot..." -curl "https://cdimage.ubuntu.com/ubuntu-base/releases/${sysroot_ubuntu_codename}/release/ubuntu-base-${sysroot_ubuntu_version}-base-${target_arch_debian_name}.tar.gz" > ./ubuntu-base.tar.gz || exit 1 -mkdir -p "$sysroot_path" -sudo tar -xvzf ubuntu-base.tar.gz -C "$sysroot_path" || exit 1 -sudo update-binfmts --enable qemu-arm || exit 1 -sudo mount -o bind /dev "${sysroot_path}/dev" || exit 1 -sudo cp /etc/resolv.conf "${sysroot_path}/etc" || exit 1 -sudo chmod 1777 "${sysroot_path}/tmp" || exit 1 -sudo cp "${root_path}/.ci/qt6_deps.sh" "${sysroot_path}/" -sudo chroot "$sysroot_path" /bin/bash -c "/qt6_deps.sh" || exit 1 -sudo chroot "$sysroot_path" /bin/bash -c "apt install -y symlinks && symlinks -rc /" || exit 1 +#mkdir host-build +#cd host-build +#echo "Building host Qt..." +#../configure -release -nomake examples -nomake tests -opensource -confirm-license -prefix "$host_prefix" || exit 1 +#cmake --build . --parallel $(nproc --all) || exit 1 +#echo "Installing host Qt..." +#cmake --install . || exit 1 +#cd .. +#rm -rf host-build # Build Qt (cross) mkdir cross-build cd cross-build echo "Cross-compiling Qt..." -export BUILD_SYSROOT_PATH=${sysroot_path} -export BUILD_TOOLCHAIN_NAME=${toolchain_name} -export BUILD_TOOLCHAIN_PREFIX=${toolchain_prefix} -export BUILD_ARCH_NAME=${target_arch_name} -../configure -release -opengl es2 -nomake examples -nomake tests -qt-host-path "$host_prefix" -xplatform "$target_platform" \ - -device-option CROSS_COMPILE="$toolchain_prefix" -sysroot "$sysroot_path" -opensource -confirm-license \ - -prefix "$target_prefix" -extprefix "$cross_prefix" -- -DCMAKE_TOOLCHAIN_FILE="$toolchain_config" \ +../configure -release -opengl es2 -nomake examples -nomake tests -qt-host-path "$host_prefix" -xplatform "$BUILD_PLATFORM" \ + -device-option CROSS_COMPILE="$BUILD_TOOLCHAIN_PREFIX" -sysroot "$BUILD_SYSROOT_PATH" -opensource -confirm-license \ + -prefix "$target_prefix" -extprefix "$cross_prefix" -- -DCMAKE_TOOLCHAIN_FILE="$BUILD_TOOLCHAIN_CONFIG" \ -DQT_FEATURE_xcb=ON -DFEATURE_xcb_xlib=ON -DQT_FEATURE_xlib=ON || exit 1 cmake --build . --parallel $(nproc --all) || exit 1 echo "Installing cross-compiled Qt..." cmake --install . || exit 1 cd .. -rm -rf cross-build # Cleanup -sudo umount "${sysroot_path}/dev" || exit 1 cd .. rm -rf qt # Required for cache -sudo chmod 777 -R ${sysroot_path} +sudo chmod 777 -R ${BUILD_SYSROOT_PATH} diff --git a/.ci/common/build.sh b/.ci/common/build.sh index 3c83bab..00cce04 100755 --- a/.ci/common/build.sh +++ b/.ci/common/build.sh @@ -12,6 +12,8 @@ mkdir -p "$BUILD_DIR" if [[ "$PLATFORM" == "win64" ]] || [[ "$PLATFORM" == "win32" ]]; then cmake -B "$BUILD_DIR" -G "Ninja" -DCMAKE_BUILD_TYPE=Release -DSCRATCHCPP_PLAYER_BUILD_UNIT_TESTS=OFF || exit 3 +elif [[ "$PLATFORM" == "linux_aarch64" ]]; then + cmake -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=Release -DSCRATCHCPP_PLAYER_BUILD_UNIT_TESTS=OFF -DCMAKE_PREFIX_PATH="$QT_ROOT_DIR" -DCMAKE_TOOLCHAIN_FILE="$BUILD_TOOLCHAIN_CONFIG" -DCMAKE_FIND_ROOT_PATH="$QT_ROOT_DIR" -DQT_HOST_PATH="$QT_HOST_PATH" -DQT_HOST_CMAKE_DIR="$QT_HOST_PATH/lib/cmake" else cmake -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=Release -DSCRATCHCPP_PLAYER_BUILD_UNIT_TESTS=OFF || exit 3 fi diff --git a/.ci/install_cross_compiler.sh b/.ci/install_cross_compiler.sh index cf70b6d..ada76c7 100755 --- a/.ci/install_cross_compiler.sh +++ b/.ci/install_cross_compiler.sh @@ -5,6 +5,4 @@ case "$1" in aarch64) sudo apt install -y g++-aarch64-linux-gnu ;; - armv7) - sudo apt install -y g++-arm-linux-gnueabihf esac diff --git a/.ci/prepare_cross_build.sh b/.ci/prepare_cross_build.sh index 5588c2b..6cc5cae 100755 --- a/.ci/prepare_cross_build.sh +++ b/.ci/prepare_cross_build.sh @@ -1,36 +1,52 @@ #!/bin/bash +sudo apt install -y lsb-release + target_arch="$1" +root_path="$(pwd)" +sysroot_path="${root_path}/sysroot" +sysroot_ubuntu_version="$(lsb_release -rs).1" +sysroot_ubuntu_codename="$(lsb_release -cs)" +cross_prefix="${root_path}/qt-cross" +toolchain_config="${root_path}/.ci/qt6-toolchain.cmake" case "$target_arch" in aarch64) - toolchain_prefix="aarch64-linux-gnu-" - echo "APPIMAGE_ARCH=aarch64" >> "${GITHUB_ENV}" - ;; - armv7) - toolchain_prefix="arm-linux-gnueabihf-" - echo "APPIMAGE_ARCH=armhf" >> "${GITHUB_ENV}" + target_arch_name="armv8-a" + target_arch_debian_name="arm64" + toolchain_name="aarch64-linux-gnu" + target_platform="linux-aarch64-gnu-g++" ;; esac -echo "$(pwd)/qt-cross/bin:$(pwd)/qt-host/libexec" >> $GITHUB_PATH -echo "LD_LIBRARY_PATH=$(pwd)/qt-cross/lib:$(pwd)/qt-host/lib" >> "${GITHUB_ENV}" -.ci/install-cross-compiler.sh "$target_arch" -.ci/qt6-dependencies.sh -if [[ "$target_arch" == "armv7" ]]; then - echo "QMAKE_CC=arm-linux-gnueabihf-gcc - QMAKE_CXX=arm-linux-gnueabihf-g++ - QMAKE_LINK=arm-linux-gnueabihf-g++ - QMAKE_LINK_SHLIB=arm-linux-gnueabihf-g++ - QMAKE_AR=arm-linux-gnueabihf-ar cqs - QMAKE_OBJCOPY=arm-linux-gnueabihf-objcopy - QMAKE_NM=arm-linux-gnueabihf-nm -P - QMAKE_STRIP=arm-linux-gnueabihf-strip" >> .qmake.conf -fi - -# Prepare cross-tools for linuxdeploy -sudo cp /usr/bin/${toolchain_prefix}strip strip -sudo mv /usr/bin/ldd /usr/bin/ldd-amd64 -sudo cp .ci/bin/xldd /usr/bin/${toolchain_prefix}ldd -sudo ln -s /usr/bin/${toolchain_prefix}ldd /usr/bin/ldd -echo "CT_XLDD_ROOT=$(pwd)/sysroot" >> "${GITHUB_ENV}" +toolchain_prefix="${toolchain_name}-" + +echo "Target architecture: ${target_arch} (${target_arch_name})" + +# Install dependencies +${root_path}/.ci/install_cross_compiler.sh "${target_arch}" || exit 1 +sudo apt install -y qemu-user-static || exit 1 + +# Prepare sysroot +echo "Preparing sysroot..." +curl "https://cdimage.ubuntu.com/ubuntu-base/releases/${sysroot_ubuntu_codename}/release/ubuntu-base-${sysroot_ubuntu_version}-base-${target_arch_debian_name}.tar.gz" > ./ubuntu-base.tar.gz || exit 1 +mkdir -p "$sysroot_path" +sudo tar -xvzf ubuntu-base.tar.gz -C "$sysroot_path" || exit 1 +rm ubuntu-base.tar.gz +sudo update-binfmts --enable qemu-arm || exit 1 +sudo mount -o bind /dev "${sysroot_path}/dev" || exit 1 +sudo cp /etc/resolv.conf "${sysroot_path}/etc" || exit 1 +sudo chmod 1777 "${sysroot_path}/tmp" || exit 1 +sudo cp "${root_path}/.ci/qt6_deps.sh" "${sysroot_path}/" +sudo chroot "$sysroot_path" /bin/bash -c "/qt6_deps.sh" || exit 1 +sudo chroot "$sysroot_path" /bin/bash -c "apt update && apt install -y symlinks libssl-dev && symlinks -rc /" || exit 1 +sudo umount "${sysroot_path}/dev" || exit 1 + +# Prepare environment +echo "BUILD_SYSROOT_PATH=$sysroot_path" >> "$GITHUB_ENV" +echo "BUILD_TOOLCHAIN_NAME=$toolchain_name" >> "$GITHUB_ENV" +echo "BUILD_TOOLCHAIN_PREFIX=$toolchain_prefix" >> "$GITHUB_ENV" +echo "BUILD_TOOLCHAIN_CONFIG=$toolchain_config" >> "$GITHUB_ENV" +echo "BUILD_ARCH_NAME=$target_arch_name" >> "$GITHUB_ENV" +echo "BUILD_PLATFORM=$target_platform" >> "$GITHUB_ENV" +echo "QT_ROOT_DIR=$cross_prefix" >> "$GITHUB_ENV" diff --git a/.ci/qt6-toolchain.cmake b/.ci/qt6-toolchain.cmake new file mode 100644 index 0000000..babfa69 --- /dev/null +++ b/.ci/qt6-toolchain.cmake @@ -0,0 +1,79 @@ +cmake_minimum_required(VERSION 3.18) +include_guard(GLOBAL) + +set(CMAKE_SYSTEM_NAME Linux) +set(CMAKE_SYSTEM_PROCESSOR arm) + +set(TARGET_SYSROOT $ENV{BUILD_SYSROOT_PATH}) +set(CMAKE_SYSROOT ${TARGET_SYSROOT}) + +set(ENV{PKG_CONFIG_PATH} $PKG_CONFIG_PATH:/usr/lib/$ENV{BUILD_TOOLCHAIN_NAME}/pkgconfig) +set(ENV{PKG_CONFIG_LIBDIR} /usr/lib/pkgconfig:/usr/share/pkgconfig/:${TARGET_SYSROOT}/usr/lib/$ENV{BUILD_TOOLCHAIN_NAME}/pkgconfig:${TARGET_SYSROOT}/usr/lib/pkgconfig) +set(ENV{PKG_CONFIG_SYSROOT_DIR} ${CMAKE_SYSROOT}) + +set(CMAKE_C_COMPILER /usr/bin/$ENV{BUILD_TOOLCHAIN_PREFIX}gcc) +set(CMAKE_CXX_COMPILER /usr/bin/$ENV{BUILD_TOOLCHAIN_PREFIX}g++) + +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -I${TARGET_SYSROOT}/usr/include") +set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS}") + +set(QT_COMPILER_FLAGS "-march=$ENV{BUILD_ARCH_NAME}") +set(QT_COMPILER_FLAGS_RELEASE "-O2 -pipe") +set(QT_LINKER_FLAGS "-Wl,-O1 -Wl,--hash-style=gnu -Wl,--as-needed") + +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) +set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) +set(CMAKE_BUILD_RPATH ${TARGET_SYSROOT}) + + +include(CMakeInitializeConfigs) + +function(cmake_initialize_per_config_variable _PREFIX _DOCSTRING) + if (_PREFIX MATCHES "CMAKE_(C|CXX|ASM)_FLAGS") + set(CMAKE_${CMAKE_MATCH_1}_FLAGS_INIT "${QT_COMPILER_FLAGS}") + + foreach (config DEBUG RELEASE MINSIZEREL RELWITHDEBINFO) + if (DEFINED QT_COMPILER_FLAGS_${config}) + set(CMAKE_${CMAKE_MATCH_1}_FLAGS_${config}_INIT "${QT_COMPILER_FLAGS_${config}}") + endif() + endforeach() + endif() + + + if (_PREFIX MATCHES "CMAKE_(SHARED|MODULE|EXE)_LINKER_FLAGS") + foreach (config SHARED MODULE EXE) + set(CMAKE_${config}_LINKER_FLAGS_INIT "${QT_LINKER_FLAGS}") + endforeach() + endif() + + _cmake_initialize_per_config_variable(${ARGV}) +endfunction() + +set(XCB_PATH_VARIABLE ${TARGET_SYSROOT}) + +set(GL_INC_DIR ${TARGET_SYSROOT}/usr/include) +set(GL_LIB_DIR ${TARGET_SYSROOT}:${TARGET_SYSROOT}/usr/lib/$ENV{BUILD_TOOLCHAIN_NAME}/:${TARGET_SYSROOT}/usr:${TARGET_SYSROOT}/usr/lib) + +set(EGL_INCLUDE_DIR ${GL_INC_DIR}) +set(EGL_LIBRARY ${XCB_PATH_VARIABLE}/usr/lib/$ENV{BUILD_TOOLCHAIN_NAME}/libEGL.so) + +set(OPENGL_INCLUDE_DIR ${GL_INC_DIR}) +set(OPENGL_opengl_LIBRARY ${XCB_PATH_VARIABLE}/usr/lib/$ENV{BUILD_TOOLCHAIN_NAME}/libOpenGL.so) + +set(GLESv2_INCLUDE_DIR ${GL_INC_DIR}) +set(GLIB_LIBRARY ${XCB_PATH_VARIABLE}/usr/lib/$ENV{BUILD_TOOLCHAIN_NAME}/libGLESv2.so) + +set(GLESv2_INCLUDE_DIR ${GL_INC_DIR}) +set(GLESv2_LIBRARY ${XCB_PATH_VARIABLE}/usr/lib/$ENV{BUILD_TOOLCHAIN_NAME}/libGLESv2.so) + +set(gbm_INCLUDE_DIR ${GL_INC_DIR}) +set(gbm_LIBRARY ${XCB_PATH_VARIABLE}/usr/lib/$ENV{BUILD_TOOLCHAIN_NAME}/libgbm.so) + +set(Libdrm_INCLUDE_DIR ${GL_INC_DIR}) +set(Libdrm_LIBRARY ${XCB_PATH_VARIABLE}/usr/lib/$ENV{BUILD_TOOLCHAIN_NAME}/libdrm.so) + +set(XCB_XCB_INCLUDE_DIR ${GL_INC_DIR}) +set(XCB_XCB_LIBRARY ${XCB_PATH_VARIABLE}/usr/lib/$ENV{BUILD_TOOLCHAIN_NAME}/libxcb.so) diff --git a/.ci/qt6_deps.sh b/.ci/qt6_deps.sh index 15d564a..ab13bcf 100755 --- a/.ci/qt6_deps.sh +++ b/.ci/qt6_deps.sh @@ -33,6 +33,6 @@ sudo apt install -y libboost-all-dev libudev-dev libinput-dev libts-dev \ libxcb-glx0-dev libxi-dev libdrm-dev libxcb-xinerama0 libxcb-xinerama0-dev \ libatspi2.0-dev libxcursor-dev libxcomposite-dev libxdamage-dev libxss-dev \ libxtst-dev libpci-dev libcap-dev libxrandr-dev libdirectfb-dev libaudio-dev \ - libxkbcommon-x11-dev libclang-dev libclang-12-dev || exit 1 + libxkbcommon-x11-dev libclang-dev libclang-*-dev || exit 1 #sudo apt install -y gstreamer1.0-omx || exit 1 diff --git a/.github/workflows/linux-build.yml b/.github/workflows/linux-build.yml index c9954fe..b4b7bc2 100644 --- a/.github/workflows/linux-build.yml +++ b/.github/workflows/linux-build.yml @@ -13,18 +13,25 @@ jobs: strategy: matrix: include: - - qt-host: 'linux_arm64' - qt-version: '6.8' + - qt-version: '6.8' qt-target: 'desktop' qt-modules: '' - qt-arch: 'linux_gcc_arm64' arch: 'amd64' - qt-version: '6.8' qt-target: 'desktop' - qt-modules: '' - qt-arch: 'linux_gcc_64' + qt-modules: 'qtshadertools' arch: 'aarch64' steps: + - name: Maximize build space + uses: easimon/maximize-build-space@master + with: + root-reserve-mb: 512 + swap-size-mb: 1024 + remove-dotnet: true + remove-android: true + remove-haskell: true + remove-codeql: true + remove-docker-images: true - uses: actions/checkout@v3 with: fetch-depth: 0 @@ -49,22 +56,52 @@ jobs: sudo apt install gcc-11 g++-11 sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 110 --slave /usr/bin/g++ g++ /usr/bin/g++-11 --slave /usr/bin/gcov gcov /usr/bin/gcov-11 ## Install Qt - - if: contains(matrix.arch, 'amd64') - name: Install Qt (Ubuntu) + - name: Install Qt uses: jurplel/install-qt-action@v4 with: version: ${{ matrix.qt-version }} - host: ${{ matrix.qt-host}} - arch: ${{ matrix.qt-arch }} + arch: linux_gcc_64 target: ${{ matrix.qt-target }} modules: ${{ matrix.qt-modules }} - ## Build + dir: ${{ github.workspace }}/Qt_host + - name: Set host Qt path + run: echo "QT_HOST_PATH=${QT_ROOT_DIR}" >> "${GITHUB_ENV}" + shell: bash + - if: "!contains(matrix.arch, 'amd64')" + name: Restore cross-compiled Qt from cache + id: restore-qt-cross + uses: jlanga/cache/restore@remove-files + with: + path: ./qt-cross/ + key: qt-cross-${{ runner.os }}-${{ matrix.qt-version }}-${{ matrix.qt-target }}-${{ matrix.qt-modules }}-${{ matrix.arch }} + - if: "!contains(matrix.arch, 'amd64')" + name: Set cross Qt path + run: echo "QT_CROSS_PATH=$(pwd)/qt-cross" >> "$GITHUB_ENV" + shell: bash - if: "!contains(matrix.arch, 'amd64')" name: Prepare cross-compilation environment run: .ci/prepare_cross_build.sh "${{ matrix.arch }}" shell: bash + - if: "!contains(matrix.arch, 'amd64') && steps.restore-qt-cross.outputs.cache-hit != 'true'" + name: Cross-compile Qt + shell: bash + run: .ci/build_qt6.sh "${{ matrix.qt-version }}" "${{ matrix.qt-modules }}" "${{ matrix.arch }}" + - if: "!contains(matrix.arch, 'amd64') && steps.restore-qt-cross.outputs.cache-hit != 'true'" + name: Cache and remove cross-compiled Qt + id: cache-qt-cross + uses: jlanga/cache/save@remove-files + with: + path: ./qt-cross/ + key: qt-cross-${{ runner.os }}-${{ matrix.qt-version }}-${{ matrix.qt-target }}-${{ matrix.qt-modules }}-${{ matrix.arch }} + - if: "!contains(matrix.arch, 'amd64') && steps.restore-qt-cross.outputs.cache-hit != 'true'" + name: Restore cross-compiled Qt from cache + uses: jlanga/cache/restore@remove-files + with: + path: ./qt-cross/ + key: qt-cross-${{ runner.os }}-${{ matrix.qt-version }}-${{ matrix.qt-target }}-${{ matrix.qt-modules }}-${{ matrix.arch }} + ## Build - name: Build AppImage - run: .ci/build_appimage.sh + run: .ci/build_appimage.sh ${{ matrix.arch }} shell: 'script -q -e -c "bash {0}"' ## Upload - name: Upload artifacts From b109a5a8c6f264a202813f7279215f6fb78020d9 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Mon, 27 Jan 2025 17:06:19 +0100 Subject: [PATCH 47/57] Enable arm64 Linux builds in release workflow --- .github/workflows/release.yml | 73 ++++++++++++++++++++++++----------- 1 file changed, 51 insertions(+), 22 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fc7d072..f6e7525 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,11 +9,26 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - qt-version: ['6.8'] - qt-target: ['desktop'] - qt-modules: [''] - arch: ['amd64'] + include: + - qt-version: '6.8' + qt-target: 'desktop' + qt-modules: '' + arch: 'amd64' + - qt-version: '6.8' + qt-target: 'desktop' + qt-modules: 'qtshadertools' + arch: 'aarch64' steps: + - name: Maximize build space + uses: easimon/maximize-build-space@master + with: + root-reserve-mb: 512 + swap-size-mb: 1024 + remove-dotnet: true + remove-android: true + remove-haskell: true + remove-codeql: true + remove-docker-images: true - uses: actions/checkout@v4 with: fetch-depth: 0 @@ -53,44 +68,58 @@ jobs: run: echo is_prerelease=1 >> "${GITHUB_ENV}" shell: bash # Install Qt - - if: contains(matrix.arch, 'amd64') - name: Install Qt + - name: Install Qt uses: jurplel/install-qt-action@v4 with: version: ${{ matrix.qt-version }} - host: 'linux' - arch: 'linux_gcc_64' + arch: linux_gcc_64 target: ${{ matrix.qt-target }} modules: ${{ matrix.qt-modules }} + dir: ${{ github.workspace }}/Qt_host + - name: Set host Qt path + run: echo "QT_HOST_PATH=${QT_ROOT_DIR}" >> "${GITHUB_ENV}" + shell: bash - if: "!contains(matrix.arch, 'amd64')" name: Restore cross-compiled Qt from cache - id: cache-qt-cross - uses: actions/cache@v3 + id: restore-qt-cross + uses: jlanga/cache/restore@remove-files with: - path: | - ./qt-host/ - ./qt-cross/ - ./sysroot/ + path: ./qt-cross/ key: qt-cross-${{ runner.os }}-${{ matrix.qt-version }}-${{ matrix.qt-target }}-${{ matrix.qt-modules }}-${{ matrix.arch }} - restore-keys: - qt-cross-${{ runner.os }}-${{ matrix.qt-version }}-${{ matrix.qt-target }}-${{ matrix.qt-modules }}-${{ matrix.arch }} - - if: "!contains(matrix.arch, 'amd64') && steps.cache-qt-cross.outputs.cache-hit != 'true'" - name: Cross-compile Qt + - if: "!contains(matrix.arch, 'amd64')" + name: Set cross Qt path + run: echo "QT_CROSS_PATH=$(pwd)/qt-cross" >> "$GITHUB_ENV" shell: bash - run: .ci/build_qt6.sh "${{ matrix.qt-version }}" "${{ matrix.qt-modules }}" "${{ matrix.arch }}" - # Build - if: "!contains(matrix.arch, 'amd64')" name: Prepare cross-compilation environment run: .ci/prepare_cross_build.sh "${{ matrix.arch }}" shell: bash + - if: "!contains(matrix.arch, 'amd64') && steps.restore-qt-cross.outputs.cache-hit != 'true'" + name: Cross-compile Qt + shell: bash + run: .ci/build_qt6.sh "${{ matrix.qt-version }}" "${{ matrix.qt-modules }}" "${{ matrix.arch }}" + - if: "!contains(matrix.arch, 'amd64') && steps.restore-qt-cross.outputs.cache-hit != 'true'" + name: Cache and remove cross-compiled Qt + id: cache-qt-cross + uses: jlanga/cache/save@remove-files + with: + path: ./qt-cross/ + key: qt-cross-${{ runner.os }}-${{ matrix.qt-version }}-${{ matrix.qt-target }}-${{ matrix.qt-modules }}-${{ matrix.arch }} + - if: "!contains(matrix.arch, 'amd64') && steps.restore-qt-cross.outputs.cache-hit != 'true'" + name: Restore cross-compiled Qt from cache + uses: jlanga/cache/restore@remove-files + with: + path: ./qt-cross/ + key: qt-cross-${{ runner.os }}-${{ matrix.qt-version }}-${{ matrix.qt-target }}-${{ matrix.qt-modules }}-${{ matrix.arch }} + # Build - name: Build AppImage - run: .ci/build_appimage.sh + run: .ci/build_appimage.sh ${{ matrix.arch }} shell: 'script -q -e -c "bash {0}"' # Upload artifacts - name: Upload artifacts uses: actions/upload-artifact@v4 with: - name: build-linux-${{ matrix.arch }} + name: build-Qt-${{ matrix.qt-version }}-${{ matrix.arch }} path: | *.AppImage *.zsync From 774f796c542b4d502e5b7f6b3d7e9d68e6b96be8 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Mon, 27 Jan 2025 22:48:57 +0100 Subject: [PATCH 48/57] Build Qt with desktop OpenGL --- .ci/build_qt6.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/build_qt6.sh b/.ci/build_qt6.sh index 15542e6..e07a938 100755 --- a/.ci/build_qt6.sh +++ b/.ci/build_qt6.sh @@ -42,7 +42,7 @@ git checkout $(git tag | grep '^v6\.8\.[0-9]*$' | sort -V | tail -n 1) || exit mkdir cross-build cd cross-build echo "Cross-compiling Qt..." -../configure -release -opengl es2 -nomake examples -nomake tests -qt-host-path "$host_prefix" -xplatform "$BUILD_PLATFORM" \ +../configure -release -opengl desktop -nomake examples -nomake tests -qt-host-path "$host_prefix" -xplatform "$BUILD_PLATFORM" \ -device-option CROSS_COMPILE="$BUILD_TOOLCHAIN_PREFIX" -sysroot "$BUILD_SYSROOT_PATH" -opensource -confirm-license \ -prefix "$target_prefix" -extprefix "$cross_prefix" -- -DCMAKE_TOOLCHAIN_FILE="$BUILD_TOOLCHAIN_CONFIG" \ -DQT_FEATURE_xcb=ON -DFEATURE_xcb_xlib=ON -DQT_FEATURE_xlib=ON || exit 1 From b8d8f2a0931582175b5ef4f92cb1f75e5976cad0 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Tue, 28 Jan 2025 17:35:36 +0100 Subject: [PATCH 49/57] Update scratchcpp-render to latest master --- scratchcpp-render | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scratchcpp-render b/scratchcpp-render index 7b1ced7..c5ec812 160000 --- a/scratchcpp-render +++ b/scratchcpp-render @@ -1 +1 @@ -Subproject commit 7b1ced7b9e6edc56f961e1cdf7a9507462bc9158 +Subproject commit c5ec812a633de9a296c4cf2e5611de7d57a73a9c From 1a8f970b81f97c150fd4e5126b06e40ca001809d Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Wed, 29 Jan 2025 17:09:37 +0100 Subject: [PATCH 50/57] Add macOS workflow --- .ci/macos_build.sh | 26 ++++++++++++ .github/workflows/macos-build.yml | 48 +++++++++++++++++++++++ res/macos-release/bg.png | Bin 0 -> 12348 bytes res/macos-release/scratchcpp-player.icns | Bin 0 -> 48525 bytes res/macos-release/scratchcpp-player.json | 9 +++++ 5 files changed, 83 insertions(+) create mode 100755 .ci/macos_build.sh create mode 100644 .github/workflows/macos-build.yml create mode 100644 res/macos-release/bg.png create mode 100644 res/macos-release/scratchcpp-player.icns create mode 100644 res/macos-release/scratchcpp-player.json diff --git a/.ci/macos_build.sh b/.ci/macos_build.sh new file mode 100755 index 0000000..96a430e --- /dev/null +++ b/.ci/macos_build.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +.ci/common/build.sh macos || exit $? +mv $(find . -name "${executable_name}.app") . || exit $? + +mkdir -p "${executable_name}.app/Contents/Frameworks" + +for file in $(find . -type f -name "*.dylib"); do + install_name_tool -change "$file" "@rpath/$file" "${executable_name}.app/Contents/MacOS/${executable_name}" || exit $? + cp "$file" "${executable_name}.app/Contents/Frameworks/" +done + +macdeployqt "${executable_name}.app" -qmldir=src || exit $? + +# Sign using self-signed certificate +codesign --verify --verbose --force --deep -s - "${executable_name}.app" +codesign --verify --verbose "${executable_name}.app" + +# Create .dmg with an install screen +npm install -g appdmg || exit $? +mv ${executable_name}.app res/macos-release/ +dmg_name=`echo "${app_name}.dmg" | tr ' ' '_'` +appdmg res/macos-release/scratchcpp-player.json "$dmg_name" || exit $? + +# Verify +#spctl -a -t open --context context:primary-signature -v "$dmg_name" diff --git a/.github/workflows/macos-build.yml b/.github/workflows/macos-build.yml new file mode 100644 index 0000000..964c090 --- /dev/null +++ b/.github/workflows/macos-build.yml @@ -0,0 +1,48 @@ +name: macOS Build + +on: + push: + branches: '*' + tags: '*' + pull_request: + branches: [ "master" ] + +jobs: + build: + runs-on: macos-14 + strategy: + matrix: + os: [macos-14] + qt-version: ['6.8'] + qt-target: ['desktop'] + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: 'recursive' + - name: Setup environment + run: | + sed -i -e '/^#/d' .github/config.env + sed -i -e '/^$/d' .github/config.env + cat .github/config.env >> "${GITHUB_ENV}" + shell: bash + - name: Set up node.js + uses: actions/setup-node@v3 + # Install Qt + - name: Install Qt + uses: jurplel/install-qt-action@v3 + with: + version: ${{ matrix.qt-version }} + host: 'mac' + target: ${{ matrix.qt-target }} + modules: '' + # Build + - name: Build + run: .ci/macos_build.sh + shell: bash + # Upload + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: build-Qt-${{ matrix.qt-version }} + path: '*.dmg' diff --git a/res/macos-release/bg.png b/res/macos-release/bg.png new file mode 100644 index 0000000000000000000000000000000000000000..3f2fbf15405db64fa76b62a3464ce5ba6b7f4517 GIT binary patch literal 12348 zcmeHtXHZn#y5=S**ho;pKoA5$GAJUkNeT**Bs4iFAUP+=NktK834$O|Bqzza8%0Dw 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 literal 0 HcmV?d00001 diff --git a/res/macos-release/scratchcpp-player.icns b/res/macos-release/scratchcpp-player.icns new file mode 100644 index 0000000000000000000000000000000000000000..2e8f5bc8d8a018dc01d26145c969730e70f7c391 GIT binary patch literal 48525 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 Date: Fri, 7 Feb 2025 12:06:29 +0100 Subject: [PATCH 51/57] Add macOS release workflow --- .github/workflows/release.yml | 58 +++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f6e7525..5e620c5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -224,6 +224,61 @@ jobs: name: build-windows path: '*installer.exe' + build-macos-release: + runs-on: macos-14 + strategy: + matrix: + os: [macos-14] + qt-version: ['6.8'] + qt-target: ['desktop'] + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: 'recursive' + - name: Setup environment + run: | + sed -i -e '/^#/d' .github/config.env + sed -i -e '/^$/d' .github/config.env + cat .github/config.env >> "${GITHUB_ENV}" + shell: bash + - name: Set up node.js + uses: actions/setup-node@v3 + - if: env.build_on_new_tags != 1 + name: Cancel if build on new tags is disabled + uses: andymckay/cancel-action@0.2 + - name: Get version + run: | + brew install grep + version=$(LC_ALL=en_US.utf8 ggrep -oP 'project\([^)]*\s+VERSION\s+\K[0-9]+\.[0-9]+\.[0-9]+' CMakeLists.txt) + echo "Project version: $version" + echo previous_tag=$version >> "${GITHUB_ENV}" + shell: bash + - if: contains(github.ref, '-') + name: Check if this is a pre-release + run: echo is_prerelease=1 >> "${GITHUB_ENV}" + shell: bash + - name: Set up node.js + uses: actions/setup-node@v3 + # Install Qt + - name: Install Qt + uses: jurplel/install-qt-action@v3 + with: + version: ${{ matrix.qt-version }} + host: 'mac' + target: ${{ matrix.qt-target }} + modules: '' + # Build + - name: Build + run: .ci/macos_build.sh + shell: bash + # Upload + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: build-Qt-${{ matrix.qt-version }} + path: '*.dmg' + # Release release: runs-on: ubuntu-latest @@ -275,8 +330,7 @@ jobs: name: Create release uses: ncipollo/release-action@v1 with: - #artifacts: "build-windows/*.exe,build-linux-*/*.AppImage,build-linux-*/*.zsync,build-macos/*.dmg" - artifacts: "build-windows/*.exe,build-linux-*/*.AppImage,build-linux-*/*.zsync" + artifacts: "build-windows/*.exe,build-linux-*/*.AppImage,build-linux-*/*.zsync,build-macos/*.dmg" name: "${{ env.app_name }} ${{ env.version }}" owner: ${{ github.event.pusher.name }} draft: true From 9f9e47162e97d06d6338356d809a4146d5b3ce3f Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Fri, 7 Feb 2025 13:15:03 +0100 Subject: [PATCH 52/57] Fix artifact names in release workflow --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5e620c5..830051e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -119,7 +119,7 @@ jobs: - name: Upload artifacts uses: actions/upload-artifact@v4 with: - name: build-Qt-${{ matrix.qt-version }}-${{ matrix.arch }} + name: build-linux-${{ matrix.arch }} path: | *.AppImage *.zsync @@ -276,7 +276,7 @@ jobs: - name: Upload artifacts uses: actions/upload-artifact@v4 with: - name: build-Qt-${{ matrix.qt-version }} + name: build-macos path: '*.dmg' # Release From bc34db24198450ae8618b3df131e409a20959db0 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Fri, 7 Feb 2025 16:26:13 +0100 Subject: [PATCH 53/57] Update scratchcpp-render to v0.9.0 --- scratchcpp-render | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scratchcpp-render b/scratchcpp-render index c5ec812..e96c348 160000 --- a/scratchcpp-render +++ b/scratchcpp-render @@ -1 +1 @@ -Subproject commit c5ec812a633de9a296c4cf2e5611de7d57a73a9c +Subproject commit e96c3487005c8b086c95dbe14f5d8fb90e9cf50a From fddf060f53a0f974954b5d1daf3680db451f632a Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Fri, 7 Feb 2025 16:53:43 +0100 Subject: [PATCH 54/57] Disable native menu bar on macOS --- src/app/app.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app/app.cpp b/src/app/app.cpp index cd7c871..8148f11 100644 --- a/src/app/app.cpp +++ b/src/app/app.cpp @@ -36,6 +36,10 @@ int App::run(int argc, char **argv) QCoreApplication::setApplicationName("ScratchCPP Player"); QCoreApplication::setApplicationVersion(BUILD_VERSION); +#ifdef Q_OS_MACOS + // Disable native menu bar on macOS + app.setAttribute(Qt::AA_DontUseNativeMenuBar); +#endif // Set style and icon theme name QQuickStyle::setStyle("Material"); QIcon::setThemeName("scratchcpp"); From 1c2294044cecfc3d7e6877b408f276b3788640a2 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Fri, 7 Feb 2025 15:06:44 +0100 Subject: [PATCH 55/57] Use Qt 6.8.1 for arm64 Linux builds https://bugreports.qt.io/browse/QTBUG-133397 --- .github/workflows/linux-build.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/linux-build.yml b/.github/workflows/linux-build.yml index b4b7bc2..f1293c0 100644 --- a/.github/workflows/linux-build.yml +++ b/.github/workflows/linux-build.yml @@ -17,7 +17,7 @@ jobs: qt-target: 'desktop' qt-modules: '' arch: 'amd64' - - qt-version: '6.8' + - qt-version: '6.8.1' qt-target: 'desktop' qt-modules: 'qtshadertools' arch: 'aarch64' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 830051e..4b7d3ef 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: qt-target: 'desktop' qt-modules: '' arch: 'amd64' - - qt-version: '6.8' + - qt-version: '6.8.1' qt-target: 'desktop' qt-modules: 'qtshadertools' arch: 'aarch64' From d6426ad424e344fa038089a0b37b4646d082f56b Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Fri, 7 Feb 2025 15:49:28 +0100 Subject: [PATCH 56/57] Fix checkout in Qt build script --- .ci/build_qt6.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.ci/build_qt6.sh b/.ci/build_qt6.sh index e07a938..a96a993 100755 --- a/.ci/build_qt6.sh +++ b/.ci/build_qt6.sh @@ -24,7 +24,13 @@ echo "Target architecture: ${target_arch} (${BUILD_ARCH_NAME})" # Clone Qt git clone https://github.com/qt/qt5 qt || exit 1 cd qt -git checkout $(git tag | grep '^v6\.8\.[0-9]*$' | sort -V | tail -n 1) || exit 1 + +if [[ `grep -o '\.' <<<"$qt_version" | grep -c .` == "1" ]]; then + git checkout $(git tag | grep "^v${qt_version}.[0-9]*$" | sort -V | tail -n 1) || exit 1 +else + git checkout "v${qt_version}" +fi + ./init-repository --module-subset=qtbase,qttools,qtdeclarative,qtsvg${qt_modules} || exit 1 # Build Qt (host) From 755d25bf35abb0e0e82d2672c88254625e3edbeb Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Fri, 7 Feb 2025 23:55:47 +0100 Subject: [PATCH 57/57] Set version to 0.7.0 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e446598..fbf5734 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.16) -project(scratchcpp-player VERSION 0.6.0 LANGUAGES CXX) +project(scratchcpp-player VERSION 0.7.0 LANGUAGES CXX) set(CMAKE_AUTOMOC ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)