diff --git a/build/module_test.cmake b/build/module_test.cmake
index 56e7e29..cd82923 100644
--- a/build/module_test.cmake
+++ b/build/module_test.cmake
@@ -15,11 +15,12 @@ if (SCRATCHCPP_PLAYER_BUILD_UNIT_TESTS)
${MODULE}
GTest::gtest_main
GTest::gmock_main
- Qt6::Gui
- Qt6::Qml
+ ${QT_LIBS}
Qt6::Test
)
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()
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/CMakeLists.txt b/src/app/CMakeLists.txt
index 3f999cb..fe8879e 100644
--- a/src/app/CMakeLists.txt
+++ b/src/app/CMakeLists.txt
@@ -11,7 +11,9 @@ 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
+ qml/dialogs/AboutDialog.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..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;
@@ -65,7 +66,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,
@@ -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/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/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/app/qml/CMakeLists.txt b/src/app/qml/CMakeLists.txt
new file mode 100644
index 0000000..e69de29
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.")
+ }
+ }
+ }
+}
diff --git a/src/app/main.qml b/src/app/qml/main.qml
similarity index 93%
rename from src/app/main.qml
rename to src/app/qml/main.qml
index 48f4374..5e7e225 100644
--- a/src/app/main.qml
+++ b/src/app/qml/main.qml
@@ -4,9 +4,11 @@ import QtQuick
import QtQuick.Controls.Material
import QtQuick.Layouts
import ScratchCPP
+import ScratchCPP.Ui
import ScratchCPP.UiComponents
import ScratchCPP.Render
import ScratchCPP.Keyboard
+import "dialogs"
ApplicationWindow {
id: root
@@ -17,6 +19,7 @@ ApplicationWindow {
color: Material.background
Material.accent: "orange"
Material.theme: Material.Dark
+ onActiveFocusItemChanged: UiEngine.activeFocusItem = activeFocusItem
menuBar: CustomMenuBar {
width: root.width
@@ -29,9 +32,15 @@ ApplicationWindow {
urlField.text = fileName;
player.fileName = fileName;
}
+
+ function onAboutAppTriggered() {
+ aboutDialog.open();
+ }
}
}
+ AboutDialog { id: aboutDialog }
+
ColumnLayout {
id: layout
anchors.fill: parent
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);
+}
diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt
new file mode 100644
index 0000000..4c96021
--- /dev/null
+++ b/src/ui/CMakeLists.txt
@@ -0,0 +1,13 @@
+set(MODULE ui)
+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
new file mode 100644
index 0000000..a9a953b
--- /dev/null
+++ b/src/ui/uimodule.cpp
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include
+
+#include "uimodule.h"
+#include "internal/uiengine.h"
+
+using namespace scratchcpp::ui;
+
+UiModule::UiModule()
+{
+}
+
+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
new file mode 100644
index 0000000..1f60570
--- /dev/null
+++ b/src/ui/uimodule.h
@@ -0,0 +1,20 @@
+// 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;
+
+ void registerExports() override;
+};
+
+} // namespace scratchcpp::ui
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..283f65b 100644
--- a/src/uicomponents/CMakeLists.txt
+++ b/src/uicomponents/CMakeLists.txt
@@ -3,11 +3,14 @@ set(MODULE_URI UiComponents)
set(MODULE_QML_FILES
CustomButton.qml
CustomToolButton.qml
+ AccentButton.qml
HoverToolTip.qml
CustomMenuBar.qml
CustomMenu.qml
CustomMenuItem.qml
CustomMenuSeparator.qml
+ CustomDialog.qml
+ internal/CustomDialogButtonBox.qml
)
set(MODULE_SRC
uicomponentsmodule.cpp
@@ -20,6 +23,10 @@ set(MODULE_SRC
menuitemmodel.h
filedialog.cpp
filedialog.h
+ dialogview.cpp
+ dialogview.h
+ internal/quickwindow.cpp
+ internal/quickwindow.h
)
include(${PROJECT_SOURCE_DIR}/build/module.cmake)
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();
+ }
+}
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/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()
+}
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..ef5148f 100644
--- a/src/uicomponents/test/CMakeLists.txt
+++ b/src/uicomponents/test/CMakeLists.txt
@@ -3,6 +3,8 @@ set(MODULE_TEST_SRC
menumodel.cpp
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);
+}
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());
+}
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();
}