diff --git a/CMakeLists.txt b/CMakeLists.txt index 5cb642d..4ed7272 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,8 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) option(SCRATCHCPP_PLAYER_BUILD_UNIT_TESTS "Build unit tests" ON) -find_package(Qt6 6.6 COMPONENTS Quick QuickControls2 REQUIRED) +find_package(Qt6 6.6 COMPONENTS Quick QuickControls2 Widgets REQUIRED) +set(QT_LIBS Qt6::Quick Qt6::QuickControls2 Qt6::Widgets) if (SCRATCHCPP_PLAYER_BUILD_UNIT_TESTS) set(GTEST_DIR thirdparty/googletest) diff --git a/README.md b/README.md index bf68806..149e812 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ When the project loads, click on the green flag button to run it. ## Roadmap - [x] Loading from URL -- [ ] Loading from a local file (path to the file can be used as a URL until this is implemented) +- [x] Loading from a local file - [x] Green flag button - [x] Stop button - [ ] Turbo mode diff --git a/build/module.cmake b/build/module.cmake index f479ea5..7d39360 100644 --- a/build/module.cmake +++ b/build/module.cmake @@ -19,6 +19,8 @@ set(QML_IMPORT_PATH "${QML_IMPORT_PATH};${CMAKE_CURRENT_LIST_DIR}" FORCE ) +target_link_libraries(${MODULE} PRIVATE ${QT_LIBS}) + list(APPEND QML_IMPORT_PATH ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) list(REMOVE_DUPLICATES QML_IMPORT_PATH) set(QML_IMPORT_PATH ${QML_IMPORT_PATH} CACHE STRING "" FORCE) diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index b2e2977..3f999cb 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -34,8 +34,7 @@ set_target_properties(${APP_TARGET} PROPERTIES target_compile_definitions(${APP_TARGET} PRIVATE $<$,$>:QT_QML_DEBUG>) target_compile_definitions(${APP_TARGET} PRIVATE BUILD_VERSION="${CMAKE_PROJECT_VERSION}") -target_link_libraries(${APP_TARGET} - PRIVATE Qt6::Quick Qt6::QuickControls2) +target_link_libraries(${APP_TARGET} PRIVATE ${QT_LIBS}) target_include_directories(${APP_TARGET} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/..) target_include_directories(${APP_TARGET} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../global) diff --git a/src/app/app.cpp b/src/app/app.cpp index fc500b8..09365d2 100644 --- a/src/app/app.cpp +++ b/src/app/app.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -#include +#include #include #include #include @@ -23,7 +23,7 @@ int App::run(int argc, char **argv) qputenv("QSG_RENDER_LOOP", "basic"); // Set up application object - QGuiApplication app(argc, argv); + QApplication app(argc, argv); QCoreApplication::setOrganizationDomain("scratchcpp.github.io"); QCoreApplication::setOrganizationName("ScratchCPP"); QCoreApplication::setApplicationName("ScratchCPP"); diff --git a/src/app/appmenubar.cpp b/src/app/appmenubar.cpp index 841fd58..9cedf46 100644 --- a/src/app/appmenubar.cpp +++ b/src/app/appmenubar.cpp @@ -1,20 +1,70 @@ // SPDX-License-Identifier: GPL-3.0-or-later +#include + #include "appmenubar.h" #include "uicomponents/menubarmodel.h" #include "uicomponents/menumodel.h" #include "uicomponents/menuitemmodel.h" +#include "uicomponents/filedialog.h" using namespace scratchcpp; using namespace scratchcpp::uicomponents; AppMenuBar::AppMenuBar(QObject *parent) : QObject(parent), - m_model(new MenuBarModel(this)) + m_model(new MenuBarModel(this)), + m_openFileDialog(new FileDialog(this)) { + m_openFileDialog->setShowAllFiles(false); + m_openFileDialog->setNameFilters({ tr("Scratch 3 projects (%1)").arg("*.sb3") }); + + // File menu + m_fileMenu = new MenuModel(m_model); + m_fileMenu->setTitle(tr("File")); + m_model->addMenu(m_fileMenu); + + // File -> Open + m_openFileItem = new MenuItemModel(m_fileMenu); + m_openFileItem->setText(tr("Open...")); + m_fileMenu->addItem(m_openFileItem); + + connect(m_openFileItem, &MenuItemModel::clicked, this, &AppMenuBar::openFile); +#ifdef Q_OS_WASM + connect(m_openFileDialog, &FileDialog::fileContentReady, this, &AppMenuBar::loadOpenedFile); +#endif } MenuBarModel *AppMenuBar::model() const { return m_model; } + +void AppMenuBar::openFile() +{ +#ifdef Q_OS_WASM + m_openFileDialog->getOpenFileContent(); +#else + QString fileName = m_openFileDialog->getOpenFileName(); + + if (!fileName.isEmpty()) + emit fileOpened(fileName); +#endif +} + +#ifdef Q_OS_WASM +void AppMenuBar::loadOpenedFile(const QByteArray &content) +{ + if (m_tmpFile) + m_tmpFile->deleteLater(); + + m_tmpFile = new QTemporaryFile(this); + + if (m_tmpFile->open()) { + m_tmpFile->write(content); + m_tmpFile->close(); + emit fileOpened(m_tmpFile->fileName()); + } else + qWarning("Failed to create temporary file."); +} +#endif diff --git a/src/app/appmenubar.h b/src/app/appmenubar.h index 3c883b9..0936081 100644 --- a/src/app/appmenubar.h +++ b/src/app/appmenubar.h @@ -6,6 +6,8 @@ Q_MOC_INCLUDE("uicomponents/menubarmodel.h") +class QTemporaryFile; + namespace scratchcpp { @@ -13,8 +15,11 @@ namespace uicomponents { class MenuBarModel; +class MenuModel; +class MenuItemModel; +class FileDialog; -} +} // namespace uicomponents class AppMenuBar : public QObject { @@ -30,9 +35,19 @@ class AppMenuBar : public QObject signals: void modelChanged(); + void fileOpened(const QString &fileName); private: + void openFile(); +#ifdef Q_OS_WASM + void loadOpenedFile(const QByteArray &content); +#endif + uicomponents::MenuBarModel *m_model = nullptr; + uicomponents::MenuModel *m_fileMenu = nullptr; + uicomponents::MenuItemModel *m_openFileItem = nullptr; + uicomponents::FileDialog *m_openFileDialog = nullptr; + QTemporaryFile *m_tmpFile = nullptr; }; } // namespace scratchcpp diff --git a/src/app/main.qml b/src/app/main.qml index 3b0714d..c260dc6 100644 --- a/src/app/main.qml +++ b/src/app/main.qml @@ -10,7 +10,7 @@ import ScratchCPP.Render ApplicationWindow { id: root minimumWidth: layout.implicitWidth + layout.anchors.margins * 2 - minimumHeight: layout.implicitHeight + layout.anchors.margins * 2 + minimumHeight: menuBar.height + layout.implicitHeight + layout.anchors.margins * 2 visible: true title: "ScratchCPP" color: Material.background @@ -20,6 +20,15 @@ ApplicationWindow { menuBar: CustomMenuBar { width: root.width model: AppMenuBar.model + + Connections { + target: AppMenuBar + + function onFileOpened(fileName) { + urlField.text = fileName; + player.fileName = fileName; + } + } } ColumnLayout { diff --git a/src/uicomponents/CMakeLists.txt b/src/uicomponents/CMakeLists.txt index ec5fece..0d9c1a8 100644 --- a/src/uicomponents/CMakeLists.txt +++ b/src/uicomponents/CMakeLists.txt @@ -16,6 +16,8 @@ set(MODULE_SRC menumodel.h menuitemmodel.cpp menuitemmodel.h + filedialog.cpp + filedialog.h ) include(${PROJECT_SOURCE_DIR}/build/module.cmake) diff --git a/src/uicomponents/filedialog.cpp b/src/uicomponents/filedialog.cpp new file mode 100644 index 0000000..242757a --- /dev/null +++ b/src/uicomponents/filedialog.cpp @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include + +#include "filedialog.h" + +using namespace scratchcpp::uicomponents; + +FileDialog::FileDialog(QObject *parent) : + QObject(parent) +{ +} + +const QStringList &FileDialog::nameFilters(void) const +{ + return m_nameFilters; +} + +void FileDialog::setNameFilters(const QStringList &filters) +{ + m_nameFilters = filters; + emit nameFiltersChanged(); +} + +bool FileDialog::showAllFiles(void) const +{ + return m_showAllFiles; +} + +void FileDialog::setShowAllFiles(bool value) +{ + m_showAllFiles = value; + emit showAllFilesChanged(); +} + +const QString &FileDialog::fileName(void) const +{ + return m_fileName; +} + +QString FileDialog::shortFileName(void) const +{ + QFileInfo fileInfo(m_fileName); + return fileInfo.fileName(); +} + +const QString &FileDialog::defaultSuffix() const +{ + return m_defaultSuffix; +} + +void FileDialog::setDefaultSuffix(const QString &newDefaultSuffix) +{ + if (m_defaultSuffix == newDefaultSuffix) + return; + + m_defaultSuffix = newDefaultSuffix; + emit defaultSuffixChanged(); +} + +void FileDialog::getOpenFileContent(void) +{ + auto fileContentReadyLambda = [this](const QString &fileName, const QByteArray &fileContent) { + if (!fileName.isEmpty()) { + m_fileName = fileName; + emit fileNameChanged(); + emit shortFileNameChanged(); + emit fileContentReady(fileContent); + } + }; + +#ifdef Q_OS_WASM + QFileDialog::getOpenFileContent(QString(), fileContentReadyLambda); +#else + QString fileName = QFileDialog::getOpenFileName(nullptr, QString(), QStandardPaths::standardLocations(QStandardPaths::HomeLocation)[0], getFilters()); + + if (fileName != "") { + QFile file(fileName); + + if (file.open(QIODevice::ReadOnly)) + fileContentReadyLambda(fileName, file.readAll()); + } +#endif +} + +QString FileDialog::getOpenFileName() const +{ + QFileDialog dialog(nullptr, QString(), QStandardPaths::standardLocations(QStandardPaths::HomeLocation)[0], getFilters()); + dialog.setFileMode(QFileDialog::AnyFile); + dialog.setAcceptMode(QFileDialog::AcceptOpen); + dialog.setDefaultSuffix(m_defaultSuffix); + + if (dialog.exec() == QDialog::Accepted) + return dialog.selectedFiles().at(0); + else + return ""; +} + +QString FileDialog::getSaveFileName() const +{ + QFileDialog dialog(nullptr, QString(), QStandardPaths::standardLocations(QStandardPaths::HomeLocation)[0], getFilters()); + dialog.setFileMode(QFileDialog::AnyFile); + dialog.setAcceptMode(QFileDialog::AcceptSave); + dialog.setDefaultSuffix(m_defaultSuffix); + + if (dialog.exec() == QDialog::Accepted) + return dialog.selectedFiles().at(0); + else + return ""; +} + +QString FileDialog::getFilters() const +{ + QString filtersStr = m_nameFilters.join(";;"); + + if (m_showAllFiles) + filtersStr += ";;" + tr("All files") + " (*)"; + + return filtersStr; +} diff --git a/src/uicomponents/filedialog.h b/src/uicomponents/filedialog.h new file mode 100644 index 0000000..af0a93d --- /dev/null +++ b/src/uicomponents/filedialog.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +namespace scratchcpp::uicomponents +{ + +class FileDialog : public QObject +{ + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(QStringList nameFilters READ nameFilters WRITE setNameFilters NOTIFY nameFiltersChanged) + Q_PROPERTY(bool showAllFiles READ showAllFiles WRITE setShowAllFiles NOTIFY showAllFilesChanged) + Q_PROPERTY(QString fileName READ fileName NOTIFY fileNameChanged) + Q_PROPERTY(QString shortFileName READ shortFileName NOTIFY shortFileNameChanged) + Q_PROPERTY(QString defaultSuffix READ defaultSuffix WRITE setDefaultSuffix NOTIFY defaultSuffixChanged) + + public: + explicit FileDialog(QObject *parent = nullptr); + + const QStringList &nameFilters(void) const; + void setNameFilters(const QStringList &filters); + + bool showAllFiles(void) const; + void setShowAllFiles(bool value); + + const QString &fileName(void) const; + QString shortFileName(void) const; + + const QString &defaultSuffix() const; + void setDefaultSuffix(const QString &newDefaultSuffix); + + Q_INVOKABLE void getOpenFileContent(void); + Q_INVOKABLE QString getOpenFileName(void) const; + Q_INVOKABLE QString getSaveFileName() const; + + private: + QString getFilters() const; + + QStringList m_nameFilters; + bool m_showAllFiles = true; + QString m_fileName; + QString m_defaultSuffix; + + signals: + void nameFiltersChanged(); + void showAllFilesChanged(); + void fileNameChanged(); + void shortFileNameChanged(); + void defaultSuffixChanged(); + void fileContentReady(const QByteArray &content); +}; + +} // namespace scratchcpp::uicomponents diff --git a/src/uicomponents/test/CMakeLists.txt b/src/uicomponents/test/CMakeLists.txt index 1ae4450..434c04c 100644 --- a/src/uicomponents/test/CMakeLists.txt +++ b/src/uicomponents/test/CMakeLists.txt @@ -2,6 +2,7 @@ set(MODULE_TEST_SRC menuitemmodel.cpp menumodel.cpp menubarmodel.cpp + filedialog.cpp ) include(${PROJECT_SOURCE_DIR}/build/module_test.cmake) diff --git a/src/uicomponents/test/filedialog.cpp b/src/uicomponents/test/filedialog.cpp new file mode 100644 index 0000000..a6898c6 --- /dev/null +++ b/src/uicomponents/test/filedialog.cpp @@ -0,0 +1,47 @@ +#include + +#include "filedialog.h" + +using namespace scratchcpp::uicomponents; + +TEST(FileDialogTest, Constructor) +{ + FileDialog dialog1; + FileDialog dialog2(&dialog1); + ASSERT_EQ(dialog1.parent(), nullptr); + ASSERT_EQ(dialog2.parent(), &dialog1); +} + +TEST(FileDialogTest, NameFilters) +{ + FileDialog dialog; + ASSERT_TRUE(dialog.nameFilters().isEmpty()); + + QStringList filters({ "a", "b", "c" }); + dialog.setNameFilters(filters); + ASSERT_EQ(dialog.nameFilters(), filters); +} + +TEST(FileDialogTest, ShowAllFiles) +{ + FileDialog dialog; + ASSERT_TRUE(dialog.showAllFiles()); + + dialog.setShowAllFiles(false); + ASSERT_FALSE(dialog.showAllFiles()); + + dialog.setShowAllFiles(false); + ASSERT_FALSE(dialog.showAllFiles()); + + dialog.setShowAllFiles(true); + ASSERT_TRUE(dialog.showAllFiles()); +} + +TEST(FileDialogTest, DefaultSuffix) +{ + FileDialog dialog; + ASSERT_TRUE(dialog.defaultSuffix().isEmpty()); + + dialog.setDefaultSuffix(".txt"); + ASSERT_EQ(dialog.defaultSuffix(), ".txt"); +}