From 7076d71e43ee5f6003e2abb8d45b7ad9f9966ccd Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Wed, 28 Feb 2024 11:55:09 +0100 Subject: [PATCH 01/15] Move main.qml to qml subdir --- src/app/CMakeLists.txt | 3 ++- src/app/app.cpp | 2 +- src/app/qml/CMakeLists.txt | 0 src/app/{ => qml}/main.qml | 0 4 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 src/app/qml/CMakeLists.txt rename src/app/{ => qml}/main.qml (100%) diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 3f999cb..b5763dc 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -11,7 +11,8 @@ qt_add_executable(${APP_TARGET} qt_add_qml_module(${APP_TARGET} URI ScratchCPP VERSION 1.0 - QML_FILES main.qml + QML_FILES + qml/main.qml ) set(QML_IMPORT_PATH "${QML_IMPORT_PATH};${CMAKE_CURRENT_LIST_DIR}" diff --git a/src/app/app.cpp b/src/app/app.cpp index 09365d2..3405791 100644 --- a/src/app/app.cpp +++ b/src/app/app.cpp @@ -65,7 +65,7 @@ int App::run(int argc, char **argv) QQmlApplicationEngine engine; engine.addImportPath(":/"); - const QUrl url(u"qrc:/ScratchCPP/main.qml"_qs); + const QUrl url(u"qrc:/ScratchCPP/qml/main.qml"_qs); QObject::connect( &engine, &QQmlApplicationEngine::objectCreated, diff --git a/src/app/qml/CMakeLists.txt b/src/app/qml/CMakeLists.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/app/main.qml b/src/app/qml/main.qml similarity index 100% rename from src/app/main.qml rename to src/app/qml/main.qml From fba3ee4980c7337170b3e0ee112aa6fa5488a8a7 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Wed, 28 Feb 2024 17:59:49 +0100 Subject: [PATCH 02/15] Add AccentButton component --- src/uicomponents/AccentButton.qml | 12 ++++++++++++ src/uicomponents/CMakeLists.txt | 1 + 2 files changed, 13 insertions(+) create mode 100644 src/uicomponents/AccentButton.qml diff --git a/src/uicomponents/AccentButton.qml b/src/uicomponents/AccentButton.qml new file mode 100644 index 0000000..44b3365 --- /dev/null +++ b/src/uicomponents/AccentButton.qml @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import QtQuick.Controls.Material + +CustomButton { + readonly property color accent: Material.accent + Material.foreground: Material.theme == Material.Dark ? "white" : "black" + Material.background: Qt.rgba(accent.r, accent.g, accent.b, 0.3) + background.layer.enabled: false + font.capitalization: Font.MixedCase +} diff --git a/src/uicomponents/CMakeLists.txt b/src/uicomponents/CMakeLists.txt index d97101c..9d33ed3 100644 --- a/src/uicomponents/CMakeLists.txt +++ b/src/uicomponents/CMakeLists.txt @@ -3,6 +3,7 @@ set(MODULE_URI UiComponents) set(MODULE_QML_FILES CustomButton.qml CustomToolButton.qml + AccentButton.qml HoverToolTip.qml CustomMenuBar.qml CustomMenu.qml From 36d5eb03f7dcc945fa3a8be499b645c561ce82cc Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Wed, 28 Feb 2024 18:15:05 +0100 Subject: [PATCH 03/15] Add ui module --- src/CMakeLists.txt | 1 + src/app/main.cpp | 2 ++ src/ui/CMakeLists.txt | 8 ++++++++ src/ui/uimodule.cpp | 14 ++++++++++++++ src/ui/uimodule.h | 18 ++++++++++++++++++ 5 files changed, 43 insertions(+) create mode 100644 src/ui/CMakeLists.txt create mode 100644 src/ui/uimodule.cpp create mode 100644 src/ui/uimodule.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 35484ce..58d1f61 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,5 +1,6 @@ add_subdirectory(app) add_subdirectory(global) +add_subdirectory(ui) add_subdirectory(uicomponents) add_subdirectory(keyboard) diff --git a/src/app/main.cpp b/src/app/main.cpp index ce74776..c51e881 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "app.h" +#include "ui/uimodule.h" #include "uicomponents/uicomponentsmodule.h" #include "keyboard/keyboardmodule.h" @@ -9,6 +10,7 @@ using namespace scratchcpp; int main(int argc, char *argv[]) { App app; + app.addModule(new ui::UiModule); app.addModule(new uicomponents::UiComponentsModule); app.addModule(new keyboard::KeyboardModule); diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt new file mode 100644 index 0000000..8d1df86 --- /dev/null +++ b/src/ui/CMakeLists.txt @@ -0,0 +1,8 @@ +set(MODULE ui) +set(MODULE_URI Ui) +set(MODULE_SRC + uimodule.cpp + uimodule.h +) + +include(${PROJECT_SOURCE_DIR}/build/module.cmake) diff --git a/src/ui/uimodule.cpp b/src/ui/uimodule.cpp new file mode 100644 index 0000000..da42054 --- /dev/null +++ b/src/ui/uimodule.cpp @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "uimodule.h" + +using namespace scratchcpp::ui; + +UiModule::UiModule() +{ +} + +std::string UiModule::moduleName() const +{ + return "ui"; +} diff --git a/src/ui/uimodule.h b/src/ui/uimodule.h new file mode 100644 index 0000000..363e4cc --- /dev/null +++ b/src/ui/uimodule.h @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "modularity/imodulesetup.h" + +namespace scratchcpp::ui +{ + +class UiModule : public modularity::IModuleSetup +{ + public: + UiModule(); + + std::string moduleName() const override; +}; + +} // namespace scratchcpp::ui From f59a912dedef81921305c7aa71d117300d782e20 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Wed, 28 Feb 2024 19:24:52 +0100 Subject: [PATCH 04/15] Add more include directories to module tests --- build/module_test.cmake | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build/module_test.cmake b/build/module_test.cmake index 56e7e29..c93b4a0 100644 --- a/build/module_test.cmake +++ b/build/module_test.cmake @@ -21,5 +21,7 @@ if (SCRATCHCPP_PLAYER_BUILD_UNIT_TESTS) ) target_include_directories(${TARGET} PRIVATE ${MODULE_SRC_DIR}) + target_include_directories(${TARGET} PRIVATE ${PROJECT_SOURCE_DIR}/src) + target_include_directories(${TARGET} PRIVATE ${PROJECT_SOURCE_DIR}/src/global) gtest_discover_tests(${TARGET}) endif() From 521d9a34fb344b124614aeb28cfcae05b8dd8f05 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Wed, 28 Feb 2024 19:32:51 +0100 Subject: [PATCH 05/15] Link tests with application's Qt libs --- build/module_test.cmake | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build/module_test.cmake b/build/module_test.cmake index c93b4a0..cd82923 100644 --- a/build/module_test.cmake +++ b/build/module_test.cmake @@ -15,8 +15,7 @@ if (SCRATCHCPP_PLAYER_BUILD_UNIT_TESTS) ${MODULE} GTest::gtest_main GTest::gmock_main - Qt6::Gui - Qt6::Qml + ${QT_LIBS} Qt6::Test ) From bf496a7406e549167dccc6df94197ad4fdf1d7c0 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Wed, 28 Feb 2024 19:34:29 +0100 Subject: [PATCH 06/15] Use QApplication in tests --- test/main.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/main.cpp b/test/main.cpp index 65ecd3f..c5dfc22 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -1,9 +1,9 @@ -#include +#include #include int main(int argc, char **argv) { - QGuiApplication a(argc, argv); + QApplication a(argc, argv); ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } From a6fa69ad29c25aa0dad2d8c4e7004c4230761113 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Wed, 28 Feb 2024 20:19:18 +0100 Subject: [PATCH 07/15] Add UiEngine class --- src/app/app.cpp | 3 ++ src/ui/CMakeLists.txt | 5 +++ src/ui/internal/uiengine.cpp | 68 ++++++++++++++++++++++++++++++++++++ src/ui/internal/uiengine.h | 47 +++++++++++++++++++++++++ src/ui/iuiengine.h | 24 +++++++++++++ src/ui/test/CMakeLists.txt | 5 +++ src/ui/test/uiengine.cpp | 57 ++++++++++++++++++++++++++++++ src/ui/uimodule.cpp | 10 ++++++ src/ui/uimodule.h | 2 ++ 9 files changed, 221 insertions(+) create mode 100644 src/ui/internal/uiengine.cpp create mode 100644 src/ui/internal/uiengine.h create mode 100644 src/ui/iuiengine.h create mode 100644 src/ui/test/CMakeLists.txt create mode 100644 src/ui/test/uiengine.cpp diff --git a/src/app/app.cpp b/src/app/app.cpp index 3405791..5606e23 100644 --- a/src/app/app.cpp +++ b/src/app/app.cpp @@ -9,6 +9,7 @@ #include "app.h" #include "globalmodule.h" #include "modularity/ioc.h" +#include "ui/internal/uiengine.h" using namespace scratchcpp; using namespace scratchcpp::modularity; @@ -77,6 +78,8 @@ int App::run(int argc, char **argv) Qt::QueuedConnection); engine.load(url); + ui::UiEngine::instance()->setQmlEngine(&engine); + // Run the event loop int exitCode = app.exec(); diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index 8d1df86..4c96021 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -3,6 +3,11 @@ set(MODULE_URI Ui) set(MODULE_SRC uimodule.cpp uimodule.h + iuiengine.h + internal/uiengine.cpp + internal/uiengine.h ) include(${PROJECT_SOURCE_DIR}/build/module.cmake) + +add_subdirectory(test) diff --git a/src/ui/internal/uiengine.cpp b/src/ui/internal/uiengine.cpp new file mode 100644 index 0000000..4ca1d0a --- /dev/null +++ b/src/ui/internal/uiengine.cpp @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include + +#include "uiengine.h" + +using namespace scratchcpp::ui; + +std::shared_ptr UiEngine::m_instance = std::make_shared(); + +UiEngine::UiEngine(QObject *parent) : + QObject(parent) +{ +} + +UiEngine::~UiEngine() +{ + if (m_dialogButtonBox) + m_dialogButtonBox->deleteLater(); +} + +std::shared_ptr UiEngine::instance() +{ + return m_instance; +} + +QQmlEngine *UiEngine::qmlEngine() const +{ + return m_qmlEngine; +} + +void UiEngine::setQmlEngine(QQmlEngine *engine) +{ + m_qmlEngine = engine; +} + +QString UiEngine::standardButtonText(QDialogButtonBox::StandardButton button) const +{ + if (!m_dialogButtonBox) + m_dialogButtonBox = new QDialogButtonBox; // construct on first use because widgets need QApplication + + QPushButton *btn = m_dialogButtonBox->button(button); + + if (!btn) { + m_dialogButtonBox->addButton(button); + btn = m_dialogButtonBox->button(button); + } + + if (btn) + return btn->text(); + else + return QString(); +} + +QQuickItem *UiEngine::activeFocusItem() const +{ + return m_activeFocusItem; +} + +void UiEngine::setActiveFocusItem(QQuickItem *newActiveFocusItem) +{ + if (m_activeFocusItem == newActiveFocusItem) + return; + + m_activeFocusItem = newActiveFocusItem; + emit activeFocusItemChanged(); +} diff --git a/src/ui/internal/uiengine.h b/src/ui/internal/uiengine.h new file mode 100644 index 0000000..3d01231 --- /dev/null +++ b/src/ui/internal/uiengine.h @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "ui/iuiengine.h" + +Q_MOC_INCLUDE() + +class QQuickItem; + +namespace scratchcpp::ui +{ + +class UiEngine + : public QObject + , public IUiEngine +{ + Q_OBJECT + Q_PROPERTY(QQuickItem *activeFocusItem READ activeFocusItem WRITE setActiveFocusItem NOTIFY activeFocusItemChanged) + + public: + explicit UiEngine(QObject *parent = nullptr); + ~UiEngine(); + + static std::shared_ptr instance(); + + QQmlEngine *qmlEngine() const override; + void setQmlEngine(QQmlEngine *engine); + + Q_INVOKABLE QString standardButtonText(QDialogButtonBox::StandardButton button) const override; + + QQuickItem *activeFocusItem() const; + void setActiveFocusItem(QQuickItem *newActiveFocusItem); + + signals: + void activeFocusItemChanged(); + + private: + static std::shared_ptr m_instance; + QQmlEngine *m_qmlEngine = nullptr; + QQuickItem *m_activeFocusItem = nullptr; + mutable QDialogButtonBox *m_dialogButtonBox = nullptr; +}; + +} // namespace scratchcpp::ui diff --git a/src/ui/iuiengine.h b/src/ui/iuiengine.h new file mode 100644 index 0000000..5f55b2f --- /dev/null +++ b/src/ui/iuiengine.h @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "modularity/ioc.h" + +class QQmlEngine; + +namespace scratchcpp::ui +{ + +class IUiEngine : MODULE_EXPORT_INTERFACE +{ + public: + virtual ~IUiEngine() { } + + virtual QQmlEngine *qmlEngine() const = 0; + + virtual QString standardButtonText(QDialogButtonBox::StandardButton button) const = 0; +}; + +} // namespace scratchcpp::ui diff --git a/src/ui/test/CMakeLists.txt b/src/ui/test/CMakeLists.txt new file mode 100644 index 0000000..654b6f5 --- /dev/null +++ b/src/ui/test/CMakeLists.txt @@ -0,0 +1,5 @@ +set(MODULE_TEST_SRC + uiengine.cpp +) + +include(${PROJECT_SOURCE_DIR}/build/module_test.cmake) diff --git a/src/ui/test/uiengine.cpp b/src/ui/test/uiengine.cpp new file mode 100644 index 0000000..737c5b4 --- /dev/null +++ b/src/ui/test/uiengine.cpp @@ -0,0 +1,57 @@ +#include +#include +#include +#include +#include + +#include "internal/uiengine.h" + +using namespace scratchcpp::ui; + +TEST(UiEngineTest, Instance) +{ + ASSERT_TRUE(UiEngine::instance()); +} + +TEST(UiEngineTest, QmlEngine) +{ + UiEngine engine; + ASSERT_EQ(engine.qmlEngine(), nullptr); + + QQmlEngine qmlEngine; + engine.setQmlEngine(&qmlEngine); + ASSERT_EQ(engine.qmlEngine(), &qmlEngine); +} + +TEST(UiEngineTest, StandardButtonText) +{ + static const std::vector buttons = { + QDialogButtonBox::Ok, QDialogButtonBox::Open, QDialogButtonBox::Save, + QDialogButtonBox::Cancel, QDialogButtonBox::Close, QDialogButtonBox::Discard, + QDialogButtonBox::Apply, QDialogButtonBox::Reset, QDialogButtonBox::RestoreDefaults, + QDialogButtonBox::Help, QDialogButtonBox::SaveAll, QDialogButtonBox::Yes, + QDialogButtonBox::YesToAll, QDialogButtonBox::No, QDialogButtonBox::NoToAll, + QDialogButtonBox::Abort, QDialogButtonBox::Retry, QDialogButtonBox::Ignore + }; + + UiEngine engine; + QDialogButtonBox dialogButtonBox; + + for (QDialogButtonBox::StandardButton button : buttons) { + dialogButtonBox.addButton(button); + ASSERT_EQ(engine.standardButtonText(button), dialogButtonBox.button(button)->text()); + ASSERT_FALSE(engine.standardButtonText(button).isEmpty()); + } +} + +TEST(UiEngineTest, ActiveFocusItem) +{ + UiEngine engine; + QSignalSpy spy(&engine, &UiEngine::activeFocusItemChanged); + ASSERT_EQ(engine.activeFocusItem(), nullptr); + + QQuickItem item; + engine.setActiveFocusItem(&item); + ASSERT_EQ(engine.activeFocusItem(), &item); + ASSERT_EQ(spy.count(), 1); +} diff --git a/src/ui/uimodule.cpp b/src/ui/uimodule.cpp index da42054..a9a953b 100644 --- a/src/ui/uimodule.cpp +++ b/src/ui/uimodule.cpp @@ -1,6 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later +#include + #include "uimodule.h" +#include "internal/uiengine.h" using namespace scratchcpp::ui; @@ -12,3 +15,10 @@ std::string UiModule::moduleName() const { return "ui"; } + +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()); +} diff --git a/src/ui/uimodule.h b/src/ui/uimodule.h index 363e4cc..1f60570 100644 --- a/src/ui/uimodule.h +++ b/src/ui/uimodule.h @@ -13,6 +13,8 @@ class UiModule : public modularity::IModuleSetup UiModule(); std::string moduleName() const override; + + void registerExports() override; }; } // namespace scratchcpp::ui From c3ec49032944f7e88be001ee441d5b959b4a675f Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Wed, 28 Feb 2024 20:58:44 +0100 Subject: [PATCH 08/15] Add CustomDialogButtonBox component --- src/uicomponents/CMakeLists.txt | 1 + .../internal/CustomDialogButtonBox.qml | 52 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 src/uicomponents/internal/CustomDialogButtonBox.qml diff --git a/src/uicomponents/CMakeLists.txt b/src/uicomponents/CMakeLists.txt index 9d33ed3..5f51138 100644 --- a/src/uicomponents/CMakeLists.txt +++ b/src/uicomponents/CMakeLists.txt @@ -9,6 +9,7 @@ set(MODULE_QML_FILES CustomMenu.qml CustomMenuItem.qml CustomMenuSeparator.qml + internal/CustomDialogButtonBox.qml ) set(MODULE_SRC uicomponentsmodule.cpp diff --git a/src/uicomponents/internal/CustomDialogButtonBox.qml b/src/uicomponents/internal/CustomDialogButtonBox.qml new file mode 100644 index 0000000..4c2a91e --- /dev/null +++ b/src/uicomponents/internal/CustomDialogButtonBox.qml @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +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 + radius: radius + } + + QtObject { + id: priv + readonly property var standardButtons: [ + Dialog.Ok, Dialog.Open, Dialog.Save, + Dialog.Cancel, Dialog.Close, Dialog.Discard, + Dialog.Apply, Dialog.Reset, Dialog.RestoreDefaults, + Dialog.Help, Dialog.SaveAll, Dialog.Yes, + Dialog.YesToAll, Dialog.No, Dialog.NoToAll, + Dialog.Abort, Dialog.Retry, Dialog.Ignore + ] + } + + function retranslateButtons() { + for (let i = 0; i < priv.standardButtons.length; i++) { + let standardBtn = priv.standardButtons[i]; + let button = standardButton(standardBtn); + + if (button) + button.text = UiEngine.standardButtonText(standardBtn); + } + } + + Connections { + readonly property Item firstButton: dialogButtonBox.contentChildren[0] + target: firstButton + + function onActiveFocusChanged() { + if (!firstButton.activeFocus) + focusOut(); + } + } + + Component.onCompleted: retranslateButtons() + onStandardButtonsChanged: retranslateButtons() +} From 9a9f8a7ce5a890962cc6782200541ab942aa0a13 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Wed, 28 Feb 2024 23:30:54 +0100 Subject: [PATCH 09/15] Set active focus item in UiEngine --- src/app/qml/main.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/qml/main.qml b/src/app/qml/main.qml index 48f4374..5a7a7d2 100644 --- a/src/app/qml/main.qml +++ b/src/app/qml/main.qml @@ -4,6 +4,7 @@ import QtQuick import QtQuick.Controls.Material import QtQuick.Layouts import ScratchCPP +import ScratchCPP.Ui import ScratchCPP.UiComponents import ScratchCPP.Render import ScratchCPP.Keyboard @@ -17,6 +18,7 @@ ApplicationWindow { color: Material.background Material.accent: "orange" Material.theme: Material.Dark + onActiveFocusItemChanged: UiEngine.activeFocusItem = activeFocusItem menuBar: CustomMenuBar { width: root.width From b336127c081c8b898abbb47ff56cbfa38af1fa9c Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Thu, 29 Feb 2024 12:49:53 +0100 Subject: [PATCH 10/15] Add QuickWindow class --- src/uicomponents/CMakeLists.txt | 2 + src/uicomponents/internal/quickwindow.cpp | 75 ++++++++++++++++ src/uicomponents/internal/quickwindow.h | 42 +++++++++ src/uicomponents/test/CMakeLists.txt | 1 + src/uicomponents/test/quickwindow.cpp | 101 ++++++++++++++++++++++ 5 files changed, 221 insertions(+) create mode 100644 src/uicomponents/internal/quickwindow.cpp create mode 100644 src/uicomponents/internal/quickwindow.h create mode 100644 src/uicomponents/test/quickwindow.cpp diff --git a/src/uicomponents/CMakeLists.txt b/src/uicomponents/CMakeLists.txt index 5f51138..76e0e60 100644 --- a/src/uicomponents/CMakeLists.txt +++ b/src/uicomponents/CMakeLists.txt @@ -22,6 +22,8 @@ set(MODULE_SRC menuitemmodel.h filedialog.cpp filedialog.h + internal/quickwindow.cpp + internal/quickwindow.h ) include(${PROJECT_SOURCE_DIR}/build/module.cmake) diff --git a/src/uicomponents/internal/quickwindow.cpp b/src/uicomponents/internal/quickwindow.cpp new file mode 100644 index 0000000..5bbb88c --- /dev/null +++ b/src/uicomponents/internal/quickwindow.cpp @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +#include "quickwindow.h" + +using namespace scratchcpp::uicomponents; + +QuickWindow::QuickWindow(QWindow *parent) : + QQuickWindow(parent) +{ + m_accessibleWidget = new QAccessibleWidget(&m_widget, QAccessible::Dialog); + m_accessibleWidget->setText(QAccessible::Name, title()); + m_widget.setAccessibleName(title()); + QAccessibleEvent accessibleEvent(m_accessibleWidget, QAccessible::NameChanged); + QAccessible::updateAccessibility(&accessibleEvent); + + connect(this, &QQuickWindow::windowTitleChanged, [this](const QString &title) { + m_accessibleWidget->setText(QAccessible::Name, title); + m_widget.setAccessibleName(title); + QAccessibleEvent accessibleEvent(m_accessibleWidget, QAccessible::NameChanged); + QAccessible::updateAccessibility(&accessibleEvent); + }); +} + +void QuickWindow::keyPressEvent(QKeyEvent *event) +{ + if (event->key() == Qt::Key_Escape && m_autoClose && m_closable) + close(); + + QQuickWindow::keyPressEvent(event); +} + +void QuickWindow::showEvent(QShowEvent *event) +{ + QQuickWindow::showEvent(event); + QAccessibleEvent accessibleEvent(m_accessibleWidget, QAccessible::Focus); + QAccessible::updateAccessibility(&accessibleEvent); +} + +void QuickWindow::closeEvent(QCloseEvent *event) +{ + if (m_closable) + QQuickWindow::closeEvent(event); + else + event->ignore(); +} + +void QuickWindow::setAutoClose(bool newAutoClose) +{ + if (m_autoClose == newAutoClose) + return; + + m_autoClose = newAutoClose; + emit autoCloseChanged(); +} + +bool QuickWindow::closable() const +{ + return m_closable; +} + +void QuickWindow::setClosable(bool newClosable) +{ + if (m_closable == newClosable) + return; + + m_closable = newClosable; + emit closableChanged(); +} + +bool QuickWindow::autoClose() const +{ + return m_autoClose; +} diff --git a/src/uicomponents/internal/quickwindow.h b/src/uicomponents/internal/quickwindow.h new file mode 100644 index 0000000..ad52b66 --- /dev/null +++ b/src/uicomponents/internal/quickwindow.h @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +namespace scratchcpp::uicomponents +{ + +class QuickWindow : public QQuickWindow +{ + Q_OBJECT + Q_PROPERTY(bool autoClose READ autoClose WRITE setAutoClose NOTIFY autoCloseChanged) + Q_PROPERTY(bool closable READ closable WRITE setClosable NOTIFY closableChanged) + + public: + explicit QuickWindow(QWindow *parent = nullptr); + + bool autoClose() const; + void setAutoClose(bool newAutoClose); + + bool closable() const; + void setClosable(bool newClosable); + + signals: + void autoCloseChanged(); + void closableChanged(); + + protected: + void keyPressEvent(QKeyEvent *event) override; + void showEvent(QShowEvent *event) override; + void closeEvent(QCloseEvent *event) override; + + private: + bool m_autoClose = true; + bool m_closable = true; + QWidget m_widget; + QAccessibleWidget *m_accessibleWidget = nullptr; +}; + +} // namespace scratchcpp::uicomponents diff --git a/src/uicomponents/test/CMakeLists.txt b/src/uicomponents/test/CMakeLists.txt index 434c04c..d5ec2ca 100644 --- a/src/uicomponents/test/CMakeLists.txt +++ b/src/uicomponents/test/CMakeLists.txt @@ -3,6 +3,7 @@ set(MODULE_TEST_SRC menumodel.cpp menubarmodel.cpp filedialog.cpp + quickwindow.cpp ) include(${PROJECT_SOURCE_DIR}/build/module_test.cmake) diff --git a/src/uicomponents/test/quickwindow.cpp b/src/uicomponents/test/quickwindow.cpp new file mode 100644 index 0000000..90dc798 --- /dev/null +++ b/src/uicomponents/test/quickwindow.cpp @@ -0,0 +1,101 @@ +#include +#include + +#include "internal/quickwindow.h" + +using namespace scratchcpp::uicomponents; + +TEST(QuickWindowTest, Constructor) +{ + QuickWindow window1; + QWindow window2(&window1); + ASSERT_EQ(window1.parent(), nullptr); + ASSERT_EQ(window2.parent(), &window1); +} + +TEST(QuickWindowTest, AutoClose) +{ + QuickWindow window; + QSignalSpy spy(&window, &QuickWindow::autoCloseChanged); + ASSERT_TRUE(window.autoClose()); + + window.setAutoClose(false); + ASSERT_FALSE(window.autoClose()); + ASSERT_EQ(spy.count(), 1); + + window.setAutoClose(false); + ASSERT_FALSE(window.autoClose()); + ASSERT_EQ(spy.count(), 1); + + window.setAutoClose(true); + ASSERT_TRUE(window.autoClose()); + ASSERT_EQ(spy.count(), 2); +} + +TEST(QuickWindowTest, Closable) +{ + QuickWindow window; + QSignalSpy spy(&window, &QuickWindow::closableChanged); + ASSERT_TRUE(window.closable()); + + window.setClosable(false); + ASSERT_FALSE(window.closable()); + ASSERT_EQ(spy.count(), 1); + + window.setClosable(false); + ASSERT_FALSE(window.closable()); + ASSERT_EQ(spy.count(), 1); + + window.setClosable(true); + ASSERT_TRUE(window.closable()); + ASSERT_EQ(spy.count(), 2); +} + +TEST(QuickWindowTest, KeyPressEvent) +{ + QuickWindow window; + window.setAutoClose(true); + window.setClosable(true); + ASSERT_FALSE(window.isVisible()); + window.show(); + ASSERT_TRUE(window.isVisible()); + + QKeyEvent event1(QEvent::KeyPress, Qt::Key_Space, Qt::NoModifier, " "); + QCoreApplication::sendEvent(&window, &event1); + ASSERT_TRUE(window.isVisible()); + + QKeyEvent event2(QEvent::KeyPress, Qt::Key_Escape, Qt::NoModifier, ""); + QCoreApplication::sendEvent(&window, &event2); + ASSERT_FALSE(window.isVisible()); + + window.show(); + window.setAutoClose(false); + QCoreApplication::sendEvent(&window, &event2); + ASSERT_TRUE(window.isVisible()); + + window.setClosable(false); + QCoreApplication::sendEvent(&window, &event2); + ASSERT_TRUE(window.isVisible()); + + window.setAutoClose(true); + QCoreApplication::sendEvent(&window, &event2); + ASSERT_TRUE(window.isVisible()); +} + +TEST(QuickWindowTest, CloseEvent) +{ + QuickWindow window; + window.setClosable(true); + ASSERT_FALSE(window.isVisible()); + window.show(); + ASSERT_TRUE(window.isVisible()); + + QCloseEvent event; + QCoreApplication::sendEvent(&window, &event); + ASSERT_FALSE(window.isVisible()); + + window.setClosable(false); + window.show(); + QCoreApplication::sendEvent(&window, &event); + ASSERT_TRUE(window.isVisible()); +} From 6a32e77729a9c7baa74d66f32d82391f26568268 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Thu, 29 Feb 2024 15:29:36 +0100 Subject: [PATCH 11/15] Add DialogView class --- src/uicomponents/CMakeLists.txt | 2 + src/uicomponents/dialogview.cpp | 168 +++++++++++++++++++++++++++ src/uicomponents/dialogview.h | 87 ++++++++++++++ src/uicomponents/test/CMakeLists.txt | 1 + src/uicomponents/test/dialogview.cpp | 158 +++++++++++++++++++++++++ 5 files changed, 416 insertions(+) create mode 100644 src/uicomponents/dialogview.cpp create mode 100644 src/uicomponents/dialogview.h create mode 100644 src/uicomponents/test/dialogview.cpp diff --git a/src/uicomponents/CMakeLists.txt b/src/uicomponents/CMakeLists.txt index 76e0e60..d8bbacd 100644 --- a/src/uicomponents/CMakeLists.txt +++ b/src/uicomponents/CMakeLists.txt @@ -22,6 +22,8 @@ set(MODULE_SRC menuitemmodel.h filedialog.cpp filedialog.h + dialogview.cpp + dialogview.h internal/quickwindow.cpp internal/quickwindow.h ) diff --git a/src/uicomponents/dialogview.cpp b/src/uicomponents/dialogview.cpp new file mode 100644 index 0000000..ce48ba1 --- /dev/null +++ b/src/uicomponents/dialogview.cpp @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +#include "dialogview.h" + +using namespace scratchcpp::uicomponents; + +DialogView::DialogView(QObject *parent) : + QObject(parent) +{ + if (m_modal) + m_window.setModality(Qt::ApplicationModal); + else + m_window.setModality(Qt::NonModal); + + m_window.setFlags(m_window.flags() | Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowMinimizeButtonHint | Qt::WindowCloseButtonHint); + + connect(&m_window, &QQuickWindow::visibleChanged, this, &DialogView::visibleChanged); + connect(&m_window, &QQuickWindow::windowTitleChanged, this, &DialogView::titleChanged); + connect(&m_window, &QQuickWindow::minimumWidthChanged, this, &DialogView::minimumWidthChanged); + connect(&m_window, &QQuickWindow::minimumHeightChanged, this, &DialogView::minimumHeightChanged); + connect(&m_window, &QQuickWindow::maximumWidthChanged, this, &DialogView::maximumWidthChanged); + connect(&m_window, &QQuickWindow::maximumHeightChanged, this, &DialogView::maximumHeightChanged); + connect(&m_window, &QuickWindow::autoCloseChanged, this, &DialogView::autoCloseChanged); + connect(&m_window, &QuickWindow::closableChanged, this, &DialogView::closableChanged); + connect(&m_window, &QQuickWindow::activeFocusItemChanged, this, &DialogView::activeFocusItemChanged); +} + +QQuickItem *DialogView::contentItem() const +{ + return m_contentItem; +} + +void DialogView::setContentItem(QQuickItem *newContentItem) +{ + if (m_contentItem == newContentItem) + return; + + m_contentItem = newContentItem; + m_contentItem->setParentItem(m_window.contentItem()); + emit contentItemChanged(); +} + +bool DialogView::visible() const +{ + return m_window.isVisible(); +} + +void DialogView::setVisible(bool newVisible) +{ + m_window.setVisible(newVisible); +} + +QString DialogView::title() const +{ + return m_window.title(); +} + +void DialogView::setTitle(const QString &newTitle) +{ + m_window.setTitle(newTitle); +} + +bool DialogView::modal() const +{ + return m_modal; +} + +void DialogView::setModal(bool newModal) +{ + if (m_modal == newModal) + return; + + m_modal = newModal; + + if (m_modal) + m_window.setModality(Qt::ApplicationModal); + else + m_window.setModality(Qt::NonModal); + + emit modalChanged(); +} + +int DialogView::minimumWidth() const +{ + return m_window.minimumWidth(); +} + +void DialogView::setMinimumWidth(int newMinimumWidth) +{ + m_window.setMinimumWidth(newMinimumWidth); +} + +int DialogView::minimumHeight() const +{ + return m_window.minimumHeight(); +} + +void DialogView::setMinimumHeight(int newMinimumHeight) +{ + m_window.setMinimumHeight(newMinimumHeight); +} + +int DialogView::maximumWidth() const +{ + return m_window.maximumWidth(); +} + +void DialogView::setMaximumWidth(int newMaximumWidth) +{ + m_window.setMaximumWidth(newMaximumWidth); +} + +int DialogView::maximumHeight() const +{ + return m_window.maximumHeight(); +} + +void DialogView::setMaximumHeight(int newMaximumHeight) +{ + m_window.setMaximumHeight(newMaximumHeight); +} + +void DialogView::setMaximizedState() +{ + m_window.setWindowStates(Qt::WindowMaximized); +} + +void DialogView::setNormalState() +{ + m_window.setWindowState(Qt::WindowNoState); +} + +void DialogView::showMaximized() +{ + m_window.showMaximized(); +} + +void DialogView::showNormal() +{ + m_window.showNormal(); +} + +bool DialogView::autoClose() const +{ + return m_window.autoClose(); +} + +void DialogView::setAutoClose(bool newAutoClose) +{ + m_window.setAutoClose(newAutoClose); +} + +bool DialogView::closable() const +{ + return m_window.closable(); +} + +void DialogView::setClosable(bool newClosable) +{ + m_window.setClosable(newClosable); +} + +QQuickItem *DialogView::activeFocusItem() const +{ + return m_window.activeFocusItem(); +} diff --git a/src/uicomponents/dialogview.h b/src/uicomponents/dialogview.h new file mode 100644 index 0000000..992764b --- /dev/null +++ b/src/uicomponents/dialogview.h @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "internal/quickwindow.h" + +Q_MOC_INCLUDE() + +namespace scratchcpp::uicomponents +{ + +class DialogView : public QObject +{ + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(QQuickItem *contentItem READ contentItem WRITE setContentItem NOTIFY contentItemChanged) + Q_PROPERTY(bool visible READ visible WRITE setVisible NOTIFY visibleChanged) + Q_PROPERTY(QString title READ title WRITE setTitle NOTIFY titleChanged) + Q_PROPERTY(bool modal READ modal WRITE setModal NOTIFY modalChanged) + Q_PROPERTY(int minimumWidth READ minimumWidth WRITE setMinimumWidth NOTIFY minimumWidthChanged) + Q_PROPERTY(int minimumHeight READ minimumHeight WRITE setMinimumHeight NOTIFY minimumHeightChanged) + Q_PROPERTY(int maximumWidth READ maximumWidth WRITE setMaximumWidth NOTIFY maximumWidthChanged) + Q_PROPERTY(int maximumHeight READ maximumHeight WRITE setMaximumHeight NOTIFY maximumHeightChanged) + Q_PROPERTY(bool autoClose READ autoClose WRITE setAutoClose NOTIFY autoCloseChanged) + Q_PROPERTY(bool closable READ closable WRITE setClosable NOTIFY closableChanged) + Q_PROPERTY(QQuickItem *activeFocusItem READ activeFocusItem NOTIFY activeFocusItemChanged) + + public: + explicit DialogView(QObject *parent = nullptr); + + QQuickItem *contentItem() const; + void setContentItem(QQuickItem *newContentItem); + + bool visible() const; + void setVisible(bool newVisible); + + QString title() const; + void setTitle(const QString &newTitle); + + bool modal() const; + void setModal(bool newModal); + + int minimumWidth() const; + void setMinimumWidth(int newMinimumWidth); + + int minimumHeight() const; + void setMinimumHeight(int newMinimumHeight); + + int maximumWidth() const; + void setMaximumWidth(int newMaximumWidth); + + int maximumHeight() const; + void setMaximumHeight(int newMaximumHeight); + + Q_INVOKABLE void setMaximizedState(); + Q_INVOKABLE void setNormalState(); + Q_INVOKABLE void showMaximized(); + Q_INVOKABLE void showNormal(); + + bool autoClose() const; + void setAutoClose(bool newAutoClose); + + bool closable() const; + void setClosable(bool newClosable); + + QQuickItem *activeFocusItem() const; + + signals: + void contentItemChanged(); + void visibleChanged(); + void titleChanged(); + void modalChanged(); + void minimumWidthChanged(); + void minimumHeightChanged(); + void maximumWidthChanged(); + void maximumHeightChanged(); + void autoCloseChanged(); + void closableChanged(); + void activeFocusItemChanged(); + + private: + QuickWindow m_window; + QQuickItem *m_contentItem = nullptr; + bool m_modal = true; +}; + +} // namespace scratchcpp::uicomponents diff --git a/src/uicomponents/test/CMakeLists.txt b/src/uicomponents/test/CMakeLists.txt index d5ec2ca..ef5148f 100644 --- a/src/uicomponents/test/CMakeLists.txt +++ b/src/uicomponents/test/CMakeLists.txt @@ -4,6 +4,7 @@ set(MODULE_TEST_SRC menubarmodel.cpp filedialog.cpp quickwindow.cpp + dialogview.cpp ) include(${PROJECT_SOURCE_DIR}/build/module_test.cmake) diff --git a/src/uicomponents/test/dialogview.cpp b/src/uicomponents/test/dialogview.cpp new file mode 100644 index 0000000..9a366e7 --- /dev/null +++ b/src/uicomponents/test/dialogview.cpp @@ -0,0 +1,158 @@ +#include +#include +#include + +#include "dialogview.h" + +using namespace scratchcpp::uicomponents; + +TEST(DialogViewTest, Constructor) +{ + DialogView dialog; + QObject object(&dialog); + ASSERT_EQ(dialog.parent(), nullptr); + ASSERT_EQ(object.parent(), &dialog); +} + +TEST(DialogViewTest, ContentItem) +{ + DialogView dialog; + QSignalSpy spy(&dialog, &DialogView::contentItemChanged); + ASSERT_EQ(dialog.contentItem(), nullptr); + + QQuickItem item; + dialog.setContentItem(&item); + ASSERT_EQ(dialog.contentItem(), &item); + ASSERT_EQ(spy.count(), 1); +} + +TEST(DialogViewTest, Visible) +{ + DialogView dialog; + QSignalSpy spy(&dialog, &DialogView::visibleChanged); + ASSERT_FALSE(dialog.visible()); + + dialog.setVisible(true); + ASSERT_TRUE(dialog.visible()); + ASSERT_EQ(spy.count(), 1); + + dialog.setVisible(true); + ASSERT_TRUE(dialog.visible()); + ASSERT_EQ(spy.count(), 1); + + dialog.setVisible(false); + ASSERT_FALSE(dialog.visible()); + ASSERT_EQ(spy.count(), 2); +} + +TEST(DialogViewTest, Title) +{ + DialogView dialog; + QSignalSpy spy(&dialog, &DialogView::titleChanged); + ASSERT_TRUE(dialog.title().isEmpty()); + + dialog.setTitle("test"); + ASSERT_EQ(dialog.title(), "test"); + ASSERT_EQ(spy.count(), 1); +} + +TEST(DialogViewTest, Modal) +{ + DialogView dialog; + QSignalSpy spy(&dialog, &DialogView::modalChanged); + ASSERT_TRUE(dialog.modal()); + + dialog.setModal(false); + ASSERT_FALSE(dialog.modal()); + ASSERT_EQ(spy.count(), 1); + + dialog.setModal(false); + ASSERT_FALSE(dialog.modal()); + ASSERT_EQ(spy.count(), 1); + + dialog.setModal(true); + ASSERT_TRUE(dialog.modal()); + ASSERT_EQ(spy.count(), 2); +} + +TEST(DialogViewTest, MinimumWidth) +{ + DialogView dialog; + QSignalSpy spy(&dialog, &DialogView::minimumWidthChanged); + ASSERT_EQ(dialog.minimumWidth(), 0); + + dialog.setMinimumWidth(45); + ASSERT_EQ(dialog.minimumWidth(), 45); + ASSERT_EQ(spy.count(), 1); +} + +TEST(DialogViewTest, MinimumHeight) +{ + DialogView dialog; + QSignalSpy spy(&dialog, &DialogView::minimumHeightChanged); + ASSERT_EQ(dialog.minimumHeight(), 0); + + dialog.setMinimumHeight(84); + ASSERT_EQ(dialog.minimumHeight(), 84); + ASSERT_EQ(spy.count(), 1); +} + +TEST(DialogViewTest, MaximumWidth) +{ + DialogView dialog; + QSignalSpy spy(&dialog, &DialogView::maximumWidthChanged); + ASSERT_EQ(dialog.maximumWidth(), QWIDGETSIZE_MAX); + + dialog.setMaximumWidth(45); + ASSERT_EQ(dialog.maximumWidth(), 45); + ASSERT_EQ(spy.count(), 1); +} + +TEST(DialogViewTest, MaximumHeight) +{ + DialogView dialog; + QSignalSpy spy(&dialog, &DialogView::maximumHeightChanged); + ASSERT_EQ(dialog.maximumHeight(), QWIDGETSIZE_MAX); + + dialog.setMaximumHeight(84); + ASSERT_EQ(dialog.maximumHeight(), 84); + ASSERT_EQ(spy.count(), 1); +} + +TEST(DialogViewTest, AutoClose) +{ + DialogView dialog; + QSignalSpy spy(&dialog, &DialogView::autoCloseChanged); + ASSERT_TRUE(dialog.autoClose()); + + dialog.setAutoClose(false); + ASSERT_FALSE(dialog.autoClose()); + ASSERT_EQ(spy.count(), 1); + + dialog.setAutoClose(false); + ASSERT_FALSE(dialog.autoClose()); + ASSERT_EQ(spy.count(), 1); + + dialog.setAutoClose(true); + ASSERT_TRUE(dialog.autoClose()); + ASSERT_EQ(spy.count(), 2); +} + +TEST(DialogViewTest, Closable) +{ + DialogView dialog; + QSignalSpy spy(&dialog, &DialogView::closableChanged); + ASSERT_TRUE(dialog.closable()); + + dialog.setClosable(false); + ASSERT_FALSE(dialog.closable()); + ASSERT_EQ(spy.count(), 1); + + dialog.setClosable(false); + ASSERT_FALSE(dialog.closable()); + ASSERT_EQ(spy.count(), 1); + + dialog.setClosable(true); + ASSERT_TRUE(dialog.closable()); + ASSERT_EQ(spy.count(), 2); +} From 59921e7d552ce1184bd2dc6f6d08600629fefdce Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Thu, 29 Feb 2024 15:30:20 +0100 Subject: [PATCH 12/15] Add CustomDialog component --- src/uicomponents/CMakeLists.txt | 1 + src/uicomponents/CustomDialog.qml | 193 ++++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 src/uicomponents/CustomDialog.qml diff --git a/src/uicomponents/CMakeLists.txt b/src/uicomponents/CMakeLists.txt index d8bbacd..283f65b 100644 --- a/src/uicomponents/CMakeLists.txt +++ b/src/uicomponents/CMakeLists.txt @@ -9,6 +9,7 @@ set(MODULE_QML_FILES CustomMenu.qml CustomMenuItem.qml CustomMenuSeparator.qml + CustomDialog.qml internal/CustomDialogButtonBox.qml ) set(MODULE_SRC diff --git a/src/uicomponents/CustomDialog.qml b/src/uicomponents/CustomDialog.qml new file mode 100644 index 0000000..4861a04 --- /dev/null +++ b/src/uicomponents/CustomDialog.qml @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Material +import QtQuick.Layouts +import ScratchCPP.Ui +import "internal" + +Item { + id: root + + property Component contentItem: Item {} + readonly property alias contents: dialog.contents + property int standardButtons + property string title: Qt.application.displayName + property bool fixedSize: true + property bool maximized: false + property bool autoClose: true + property int nativeDialogMinimumWidth: dialog.contentItem.contentLayout.implicitWidth + property int nativeDialogMinimumHeight: dialog.contentItem.contentLayout.implicitHeight + readonly property bool isNative: true + property bool closable: true + signal accepted() + signal applied() + signal discarded() + signal helpRequested() + signal rejected() + signal reset() + signal opened() + signal closed() + signal aboutToShow() + signal aboutToHide() + signal focusReset() + + visible: dialog.visible + + onVisibleChanged: { + if (!priv.initialized) { + priv.initialized = true; + return; + } + + priv.contentsActive = visible; + dialog.visible = visible; + + if (visible) { + UiEngine.activeFocusItem = dialog.activeFocusItem; + aboutToShow(); + opened(); + priv.sizeUpdate = !priv.sizeUpdate; + } else { + aboutToHide(); + closed(); + + if (!priv.closedFromQml) + rejected(); + } + + priv.closedFromQml = false; + visible = Qt.binding(function() { return dialog.visible }); + } + + onMaximizedChanged: { + if (maximized) + dialog.setMaximizedState(); + else + dialog.setNormalState(); + } + + function open() { + visible = true; + } + + function close() { + priv.closedFromQml = true; + visible = false; + } + + function accept() { + close(); + accepted(); + } + + function reject() { + close(); + rejected(); + } + + function standardButton(button) { + return dialog.contentItem.buttonBoxLoader.item.standardButton(button); + } + + QtObject { + id: priv + property bool closedFromQml: true + property bool initialized: false + property bool sizeUpdate: false + property bool contentsActive: true + } + + DialogView { + readonly property Item contents: contentItem.contentsLoader.item + id: dialog + title: root.title + visible: false + autoClose: root.autoClose + closable: root.closable + minimumWidth: nativeDialogMinimumWidth + minimumHeight: nativeDialogMinimumHeight + maximumWidth: { + priv.sizeUpdate; + return fixedSize ? Math.max(contentItem.contentLayout.implicitWidth, minimumWidth) : { maximumWidth = maximumWidth }; + } + maximumHeight: { + priv.sizeUpdate; + return fixedSize? Math.max(contentItem.contentLayout.implicitHeight, minimumHeight) : { maximumHeight = maximumHeight }; + } + onActiveFocusItemChanged: UiEngine.activeFocusItem = activeFocusItem + + contentItem: Rectangle { + property alias contentLayout: contentLayout + property alias contentsLoader: contentsLoader + property alias buttonBoxLoader: buttonBoxLoader + anchors.fill: parent + // TODO: Read colors from ThemeEngine + color: /*ThemeEngine.bgColor*/ Material.background + //Material.background: ThemeEngine.bgColor + //Material.accent: ThemeEngine.currentAccentColor + //Material.theme: ThemeEngine.theme === ThemeEngine.DarkTheme ? Material.Dark : Material.Light + Material.theme: Material.Dark + + ColumnLayout { + id: contentLayout + anchors.fill: parent + + Loader { + id: contentsLoader + sourceComponent: root.contentItem + active: priv.contentsActive + Layout.fillWidth: true + Layout.fillHeight: true + } + + Loader { + id: buttonBoxLoader + + sourceComponent: CustomDialogButtonBox { + standardButtons: root.standardButtons + onAccepted: root.accept() + onApplied: root.applied() + onDiscarded: root.discarded() + onHelpRequested: root.helpRequested() + onRejected: root.reject() + onReset: root.reset() + onFocusOut: { + root.focusReset(); + root.forceActiveFocus(Qt.TabFocusReason); + } + } + + Layout.fillWidth: true + + /*Connections { + target: LanguageManager + + function onLanguageChanged() { + translationTimer.start(); + } + } + + Timer { + id: translationTimer + interval: 16 + running: false + repeat: false + onTriggered: { + buttonBoxLoader.active = 0; + buttonBoxLoader.active = 1; + } + }*/ + } + } + } + } + + Component.onCompleted: { + if (!visible) + priv.contentsActive = false; + + maximizedChanged(); + } +} From c493c0ef5de0e3ba0746b04e08e97dedfad770d0 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Thu, 29 Feb 2024 16:29:00 +0100 Subject: [PATCH 13/15] Add AppInfo class --- src/global/CMakeLists.txt | 14 ++++++++++++++ src/global/globalmodule.cpp | 12 ++++++++++++ src/global/globalmodule.h | 9 +++++++++ src/global/iappinfo.h | 21 +++++++++++++++++++++ src/global/internal/appinfo.cpp | 22 ++++++++++++++++++++++ src/global/internal/appinfo.h | 24 ++++++++++++++++++++++++ src/global/test/CMakeLists.txt | 6 ++++++ src/global/test/appinfo.cpp | 25 +++++++++++++++++++++++++ 8 files changed, 133 insertions(+) create mode 100644 src/global/iappinfo.h create mode 100644 src/global/internal/appinfo.cpp create mode 100644 src/global/internal/appinfo.h create mode 100644 src/global/test/appinfo.cpp diff --git a/src/global/CMakeLists.txt b/src/global/CMakeLists.txt index 8c412b6..9b9c2a0 100644 --- a/src/global/CMakeLists.txt +++ b/src/global/CMakeLists.txt @@ -3,12 +3,26 @@ set(MODULE_URI Global) set(MODULE_SRC globalmodule.cpp globalmodule.h + iappinfo.h modularity/ioc.h modularity/modulesioc.h modularity/imoduleexportinterface.h modularity/imodulesetup.h + internal/appinfo.cpp + internal/appinfo.h ) include(${PROJECT_SOURCE_DIR}/build/module.cmake) +include(FetchContent) +FetchContent_Declare(cmake_git_version_tracking + GIT_REPOSITORY https://github.com/andrew-hardin/cmake-git-version-tracking.git + GIT_TAG 904dbda1336ba4b9a1415a68d5f203f576b696bb +) +FetchContent_MakeAvailable(cmake_git_version_tracking) + +target_link_libraries(${MODULE} PRIVATE cmake_git_version_tracking) +string(TIMESTAMP BUILD_YEAR "%Y") +target_compile_definitions(${MODULE} PRIVATE BUILD_YEAR=${BUILD_YEAR}) + add_subdirectory(test) diff --git a/src/global/globalmodule.cpp b/src/global/globalmodule.cpp index 77a18c0..c0cba8d 100644 --- a/src/global/globalmodule.cpp +++ b/src/global/globalmodule.cpp @@ -1,6 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later +#include + #include "globalmodule.h" +#include "internal/appinfo.h" using namespace scratchcpp; @@ -8,3 +11,12 @@ std::string GlobalModule::moduleName() const { return "global"; } + +void GlobalModule::registerExports() +{ + m_appInfo = std::make_shared(); + + QQmlEngine::setObjectOwnership(m_appInfo.get(), QQmlEngine::CppOwnership); + qmlRegisterSingletonInstance("ScratchCPP.Ui", 1, 0, "AppInfo", m_appInfo.get()); + modularity::ioc()->registerExport(m_appInfo); +} diff --git a/src/global/globalmodule.h b/src/global/globalmodule.h index 464a7d6..723f3df 100644 --- a/src/global/globalmodule.h +++ b/src/global/globalmodule.h @@ -2,15 +2,24 @@ #pragma once +#include + #include "modularity/imodulesetup.h" namespace scratchcpp { +class AppInfo; + class GlobalModule : public modularity::IModuleSetup { public: std::string moduleName() const override; + + void registerExports() override; + + private: + std::shared_ptr m_appInfo; }; } // namespace scratchcpp diff --git a/src/global/iappinfo.h b/src/global/iappinfo.h new file mode 100644 index 0000000..fd42012 --- /dev/null +++ b/src/global/iappinfo.h @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "modularity/ioc.h" + +class QString; + +namespace scratchcpp +{ + +class IAppInfo : MODULE_EXPORT_INTERFACE +{ + public: + virtual ~IAppInfo() { } + + virtual QString revision() const = 0; + virtual int buildYear() const = 0; +}; + +} // namespace scratchcpp diff --git a/src/global/internal/appinfo.cpp b/src/global/internal/appinfo.cpp new file mode 100644 index 0000000..2b80cff --- /dev/null +++ b/src/global/internal/appinfo.cpp @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +#include "appinfo.h" + +using namespace scratchcpp; + +AppInfo::AppInfo(QObject *parent) : + QObject(parent) +{ +} + +QString scratchcpp::AppInfo::revision() const +{ + return git_CommitSHA1(); +} + +int scratchcpp::AppInfo::buildYear() const +{ + return BUILD_YEAR; +} diff --git a/src/global/internal/appinfo.h b/src/global/internal/appinfo.h new file mode 100644 index 0000000..fa61df3 --- /dev/null +++ b/src/global/internal/appinfo.h @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "iappinfo.h" + +namespace scratchcpp +{ + +class AppInfo + : public QObject + , public IAppInfo +{ + Q_OBJECT + public: + explicit AppInfo(QObject *parent = nullptr); + + Q_INVOKABLE QString revision() const override; + Q_INVOKABLE int buildYear() const override; +}; + +} // namespace scratchcpp diff --git a/src/global/test/CMakeLists.txt b/src/global/test/CMakeLists.txt index d4e26da..96b56fd 100644 --- a/src/global/test/CMakeLists.txt +++ b/src/global/test/CMakeLists.txt @@ -1,6 +1,7 @@ set(MODULE_TEST_SRC modularity.cpp setup.cpp + appinfo.cpp fakeexport.h fakedependency.h mocks/moduleexportinterfacemock.h @@ -8,3 +9,8 @@ set(MODULE_TEST_SRC ) include(${PROJECT_SOURCE_DIR}/build/module_test.cmake) + +set(TARGET ${MODULE}_test) +target_link_libraries(${TARGET} cmake_git_version_tracking) +string(TIMESTAMP BUILD_YEAR "%Y") +target_compile_definitions(${TARGET} PRIVATE BUILD_YEAR=${BUILD_YEAR}) diff --git a/src/global/test/appinfo.cpp b/src/global/test/appinfo.cpp new file mode 100644 index 0000000..c02aaec --- /dev/null +++ b/src/global/test/appinfo.cpp @@ -0,0 +1,25 @@ +#include +#include + +#include "internal/appinfo.h" + +// Workaround for multiple definition error +namespace scratchcpp::test +{ +#include +} + +using namespace scratchcpp; +using namespace scratchcpp::test; + +TEST(AppInfoTest, Revision) +{ + AppInfo info; + ASSERT_EQ(info.revision(), git_CommitSHA1()); +} + +TEST(AppInfoTest, BuildYear) +{ + AppInfo info; + ASSERT_EQ(info.buildYear(), BUILD_YEAR); +} From 7709a08eee61817c8168201299d5e859441bf61f Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Thu, 29 Feb 2024 16:29:40 +0100 Subject: [PATCH 14/15] Add AboutDialog component --- src/app/CMakeLists.txt | 1 + src/app/qml/dialogs/AboutDialog.qml | 59 +++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 src/app/qml/dialogs/AboutDialog.qml diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index b5763dc..fe8879e 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -13,6 +13,7 @@ qt_add_qml_module(${APP_TARGET} VERSION 1.0 QML_FILES qml/main.qml + qml/dialogs/AboutDialog.qml ) set(QML_IMPORT_PATH "${QML_IMPORT_PATH};${CMAKE_CURRENT_LIST_DIR}" diff --git a/src/app/qml/dialogs/AboutDialog.qml b/src/app/qml/dialogs/AboutDialog.qml new file mode 100644 index 0000000..14e121f --- /dev/null +++ b/src/app/qml/dialogs/AboutDialog.qml @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import ScratchCPP.Ui +import ScratchCPP.UiComponents + +CustomDialog { + //: For example "About ScratchCPP" (%1 is the app name) + title: qsTr("About %1").arg(Qt.application.displayName) + standardButtons: Dialog.Ok + + contentItem: RowLayout { + spacing: 25 + + /*Image { + source: "qrc:/res/images/icon.ico" + sourceSize.width: 60 + sourceSize.height: 60 + Layout.alignment: Qt.AlignTop + }*/ + + ColumnLayout { + Label { + text: Qt.application.displayName + font.bold: true + } + + Label {} + + Label { + text: qsTr("Version: %1").arg(Qt.application.version) + } + + Label { + text: qsTr("Revision: %1").arg(AppInfo.revision()) + } + + Label { + readonly property string src: "https://github.com/scratchcpp/scratchcpp-player" + text: qsTr("Source code: %1").arg("" + src + "") + onLinkActivated: (link)=> Qt.openUrlExternally(link) + } + + Label {} + + Label { + readonly property int startYear: 2024 + readonly property string startStr: AppInfo.buildYear() === startYear ? "" : startYear + "-" + text: "Copyright © " + startStr + AppInfo.buildYear() + " adazem009" + } + + Label { + text: qsTr("Published with the GNU General Public License.") + } + } + } +} From 09852f009788f5ed33883a70118c746204bb7c92 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Thu, 29 Feb 2024 16:30:07 +0100 Subject: [PATCH 15/15] Add about program menu item --- src/app/appmenubar.cpp | 11 +++++++++++ src/app/appmenubar.h | 4 ++++ src/app/qml/main.qml | 7 +++++++ 3 files changed, 22 insertions(+) diff --git a/src/app/appmenubar.cpp b/src/app/appmenubar.cpp index 6a5a066..73ef1cf 100644 --- a/src/app/appmenubar.cpp +++ b/src/app/appmenubar.cpp @@ -54,6 +54,17 @@ AppMenuBar::AppMenuBar(QObject *parent) : m_fps60ModeItem->setChecked(false); m_editMenu->addItem(m_fps60ModeItem); connect(m_fps60ModeItem, &MenuItemModel::checkedChanged, this, &AppMenuBar::fps60ModeChanged); + + // Help menu + m_helpMenu = new MenuModel(m_model); + m_helpMenu->setTitle(tr("&Help")); + m_model->addMenu(m_helpMenu); + + // Help -> About program + m_aboutAppItem = new MenuItemModel(m_fileMenu); + m_aboutAppItem->setText(tr("About program...")); + m_helpMenu->addItem(m_aboutAppItem); + connect(m_aboutAppItem, &MenuItemModel::clicked, this, &AppMenuBar::aboutAppTriggered); } MenuBarModel *AppMenuBar::model() const diff --git a/src/app/appmenubar.h b/src/app/appmenubar.h index 15d8d30..8cb7d58 100644 --- a/src/app/appmenubar.h +++ b/src/app/appmenubar.h @@ -46,6 +46,7 @@ class AppMenuBar : public QObject void fileOpened(const QString &fileName); void turboModeChanged(); void fps60ModeChanged(); + void aboutAppTriggered(); private: void openFile(); @@ -63,6 +64,9 @@ class AppMenuBar : public QObject uicomponents::MenuModel *m_editMenu = nullptr; uicomponents::MenuItemModel *m_turboModeItem = nullptr; uicomponents::MenuItemModel *m_fps60ModeItem = nullptr; + + uicomponents::MenuModel *m_helpMenu = nullptr; + uicomponents::MenuItemModel *m_aboutAppItem = nullptr; }; } // namespace scratchcpp diff --git a/src/app/qml/main.qml b/src/app/qml/main.qml index 5a7a7d2..5e7e225 100644 --- a/src/app/qml/main.qml +++ b/src/app/qml/main.qml @@ -8,6 +8,7 @@ import ScratchCPP.Ui import ScratchCPP.UiComponents import ScratchCPP.Render import ScratchCPP.Keyboard +import "dialogs" ApplicationWindow { id: root @@ -31,9 +32,15 @@ ApplicationWindow { urlField.text = fileName; player.fileName = fileName; } + + function onAboutAppTriggered() { + aboutDialog.open(); + } } } + AboutDialog { id: aboutDialog } + ColumnLayout { id: layout anchors.fill: parent