From ddd63bc45f34d906a7dd5ccda5126cd62dfd26d4 Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Mon, 26 Jul 2021 01:39:56 -0500 Subject: [PATCH] [nostalgia/studio] Start on ImGui version of Studio --- src/nostalgia/core/CMakeLists.txt | 3 +- src/nostalgia/core/gba/gfx.cpp | 7 +- src/nostalgia/core/qt/CMakeLists.txt | 22 - src/nostalgia/core/qt/gfx.cpp | 44 -- src/nostalgia/studio/CMakeLists.txt | 37 +- src/nostalgia/studio/filedialogmanager.cpp | 47 ++ src/nostalgia/studio/filedialogmanager.hpp | 50 ++ src/nostalgia/studio/json_read.cpp | 87 --- src/nostalgia/studio/json_read.hpp | 105 --- src/nostalgia/studio/json_test.cpp | 87 --- src/nostalgia/studio/json_write.cpp | 36 -- src/nostalgia/studio/json_write.hpp | 76 --- src/nostalgia/studio/lib/CMakeLists.txt | 37 +- src/nostalgia/studio/lib/configio.cpp | 13 + src/nostalgia/studio/lib/configio.hpp | 105 +++ src/nostalgia/studio/lib/editor.cpp | 17 +- src/nostalgia/studio/lib/editor.hpp | 32 +- .../filedialog.hpp} | 11 +- src/nostalgia/studio/lib/filedialog.mm | 32 + src/nostalgia/studio/lib/filedialog_gtk.cpp | 45 ++ src/nostalgia/studio/lib/project.cpp | 71 +- src/nostalgia/studio/lib/project.hpp | 105 +-- src/nostalgia/studio/lib/task.cpp | 25 + src/nostalgia/studio/lib/task.hpp | 36 ++ src/nostalgia/studio/lib/undostack.cpp | 29 + src/nostalgia/studio/lib/undostack.hpp | 37 ++ src/nostalgia/studio/lib/widget.cpp | 13 + .../studio/lib/{context.hpp => widget.hpp} | 14 +- src/nostalgia/studio/lib/window.cpp | 13 + .../studio/{json_err.hpp => lib/window.hpp} | 9 +- src/nostalgia/studio/lib/wizard.cpp | 357 ---------- src/nostalgia/studio/lib/wizard.hpp | 151 ----- src/nostalgia/studio/main.cpp | 79 ++- src/nostalgia/studio/mainwindow.cpp | 611 ------------------ src/nostalgia/studio/mainwindow.hpp | 172 ----- src/nostalgia/studio/oxfstreeview.cpp | 220 ------- src/nostalgia/studio/oxfstreeview.hpp | 88 --- src/nostalgia/studio/projectexplorer.cpp | 41 ++ src/nostalgia/studio/projectexplorer.hpp | 27 + src/nostalgia/studio/projecttreemodel.cpp | 42 ++ src/nostalgia/studio/projecttreemodel.hpp | 31 + src/nostalgia/studio/studio.hpp | 6 +- src/nostalgia/studio/studioapp.cpp | 144 +++++ src/nostalgia/studio/studioapp.hpp | 52 ++ src/nostalgia/studio/studiorsrc.qrc | 6 - src/nostalgia/world/CMakeLists.txt | 2 +- 46 files changed, 1005 insertions(+), 2269 deletions(-) delete mode 100644 src/nostalgia/core/qt/CMakeLists.txt delete mode 100644 src/nostalgia/core/qt/gfx.cpp create mode 100644 src/nostalgia/studio/filedialogmanager.cpp create mode 100644 src/nostalgia/studio/filedialogmanager.hpp delete mode 100644 src/nostalgia/studio/json_read.cpp delete mode 100644 src/nostalgia/studio/json_read.hpp delete mode 100644 src/nostalgia/studio/json_test.cpp delete mode 100644 src/nostalgia/studio/json_write.cpp delete mode 100644 src/nostalgia/studio/json_write.hpp create mode 100644 src/nostalgia/studio/lib/configio.cpp create mode 100644 src/nostalgia/studio/lib/configio.hpp rename src/nostalgia/studio/{builtinmodules.hpp => lib/filedialog.hpp} (57%) create mode 100644 src/nostalgia/studio/lib/filedialog.mm create mode 100644 src/nostalgia/studio/lib/filedialog_gtk.cpp create mode 100644 src/nostalgia/studio/lib/task.cpp create mode 100644 src/nostalgia/studio/lib/task.hpp create mode 100644 src/nostalgia/studio/lib/undostack.cpp create mode 100644 src/nostalgia/studio/lib/undostack.hpp create mode 100644 src/nostalgia/studio/lib/widget.cpp rename src/nostalgia/studio/lib/{context.hpp => widget.hpp} (65%) create mode 100644 src/nostalgia/studio/lib/window.cpp rename src/nostalgia/studio/{json_err.hpp => lib/window.hpp} (81%) delete mode 100644 src/nostalgia/studio/lib/wizard.cpp delete mode 100644 src/nostalgia/studio/lib/wizard.hpp delete mode 100644 src/nostalgia/studio/mainwindow.cpp delete mode 100644 src/nostalgia/studio/mainwindow.hpp delete mode 100644 src/nostalgia/studio/oxfstreeview.cpp delete mode 100644 src/nostalgia/studio/oxfstreeview.hpp create mode 100644 src/nostalgia/studio/projectexplorer.cpp create mode 100644 src/nostalgia/studio/projectexplorer.hpp create mode 100644 src/nostalgia/studio/projecttreemodel.cpp create mode 100644 src/nostalgia/studio/projecttreemodel.hpp create mode 100644 src/nostalgia/studio/studioapp.cpp create mode 100644 src/nostalgia/studio/studioapp.hpp delete mode 100644 src/nostalgia/studio/studiorsrc.qrc diff --git a/src/nostalgia/core/CMakeLists.txt b/src/nostalgia/core/CMakeLists.txt index 1982b985..4958fc80 100644 --- a/src/nostalgia/core/CMakeLists.txt +++ b/src/nostalgia/core/CMakeLists.txt @@ -19,8 +19,7 @@ if(NOSTALGIA_BUILD_TYPE STREQUAL "Native") add_subdirectory(userland) endif() if(NOSTALGIA_BUILD_STUDIO) - add_subdirectory(qt) - add_subdirectory(studio) + #add_subdirectory(studio) endif() install( diff --git a/src/nostalgia/core/gba/gfx.cpp b/src/nostalgia/core/gba/gfx.cpp index f66d64ef..76d5ca0d 100644 --- a/src/nostalgia/core/gba/gfx.cpp +++ b/src/nostalgia/core/gba/gfx.cpp @@ -28,7 +28,6 @@ constexpr uint16_t DispStat_irq_vcount = 1 << 5; struct GbaPaletteTarget { static constexpr auto TypeName = NostalgiaPalette::TypeName; - static constexpr auto Fields = NostalgiaPalette::Fields; static constexpr auto TypeVersion = NostalgiaPalette::TypeVersion; volatile uint16_t *palette = nullptr; }; @@ -44,7 +43,7 @@ struct GbaTileMapTarget { }; template -ox::Error modelRead(T *io, GbaPaletteTarget *t) noexcept { +constexpr ox::Error modelRead(T *io, GbaPaletteTarget *t) noexcept { io->template setTypeInfo(); const auto colorHandler = [t](std::size_t i, Color16 *c) { t->palette[i] = *c; @@ -54,8 +53,8 @@ ox::Error modelRead(T *io, GbaPaletteTarget *t) noexcept { } template -ox::Error modelRead(T *io, GbaTileMapTarget *t) noexcept { - io->template setTypeInfo(); +constexpr ox::Error modelRead(T *io, GbaTileMapTarget *t) noexcept { + io->template setTypeInfo(GbaTileMapTarget::TypeName, GbaTileMapTarget::Fields); uint8_t bpp; int dummy; oxReturnError(io->field("bpp", &bpp)); diff --git a/src/nostalgia/core/qt/CMakeLists.txt b/src/nostalgia/core/qt/CMakeLists.txt deleted file mode 100644 index 5c0f33be..00000000 --- a/src/nostalgia/core/qt/CMakeLists.txt +++ /dev/null @@ -1,22 +0,0 @@ - -set(CMAKE_AUTOMOC ON) -set(CMAKE_AUTORCC ON) - -add_library( - NostalgiaCore-Qt OBJECT - gfx.cpp -) - -target_link_libraries( - NostalgiaCore-Qt PUBLIC - NostalgiaStudio - OxFS - OxStd -) - -install( - TARGETS - NostalgiaCore-Qt - LIBRARY DESTINATION - ${NOSTALGIA_DIST_LIB}/nostalgia -) diff --git a/src/nostalgia/core/qt/gfx.cpp b/src/nostalgia/core/qt/gfx.cpp deleted file mode 100644 index 9aac3b44..00000000 --- a/src/nostalgia/core/qt/gfx.cpp +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2016 - 2021 gary@drinkingtea.net - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -#include "../gfx.hpp" - -namespace nostalgia::core { - -ox::Error initGfx(Context*) noexcept { - return OxError(1); -} - -ox::Error shutdownGfx(Context*) noexcept { - return OxError(1); -} - -ox::Error initConsole(Context*) noexcept { - return OxError(1); -} - -ox::Error loadBgTileSheet(Context*, - int, - ox::FileAddress, - ox::FileAddress) noexcept { - return OxError(1); -} - -void puts(Context *ctx, int column, int row, const char *str) noexcept { - for (int i = 0; str[i]; i++) { - setTile(ctx, 0, column + i, row, static_cast(charMap[static_cast(str[i])])); - } -} - -void setTile(Context*, int, int, int, uint8_t) noexcept { -} - -void setSprite(Context*, unsigned, unsigned, unsigned, unsigned, unsigned, unsigned, unsigned) noexcept { -} - -} diff --git a/src/nostalgia/studio/CMakeLists.txt b/src/nostalgia/studio/CMakeLists.txt index d8c305f6..83cfd526 100644 --- a/src/nostalgia/studio/CMakeLists.txt +++ b/src/nostalgia/studio/CMakeLists.txt @@ -1,29 +1,27 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON) -set(CMAKE_AUTOMOC ON) -set(CMAKE_AUTORCC ON) - add_executable( nostalgia-studio MACOSX_BUNDLE - json_read.cpp - json_write.cpp + filedialogmanager.cpp main.cpp - mainwindow.cpp - oxfstreeview.cpp - studiorsrc.qrc + projectexplorer.cpp + projecttreemodel.cpp + studioapp.cpp ) target_link_libraries( nostalgia-studio - QDarkStyle OxClArgs - NostalgiaCore-Studio + OxFS + NostalgiaCore-Userspace NostalgiaStudio NostalgiaPack ) -# enable LTO -set_property(TARGET nostalgia-studio PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE) +if (CMAKE_BUILD_TYPE STREQUAL "Release") + # enable LTO + set_property(TARGET nostalgia-studio PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE) +endif() if(APPLE) set_target_properties(nostalgia-studio PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist) @@ -44,19 +42,4 @@ install( BUNDLE DESTINATION . ) -add_executable( - NostalgiaStudioJsonTest - json_read.cpp - json_write.cpp - json_test.cpp -) - -target_link_libraries( - NostalgiaStudioJsonTest - OxStd - Qt${QT_VERSION_MAJOR}::Widgets -) - -add_test("[nostalgia/studio] NostalgiaStudioJson" NostalgiaStudioJsonTest) - add_subdirectory(lib) diff --git a/src/nostalgia/studio/filedialogmanager.cpp b/src/nostalgia/studio/filedialogmanager.cpp new file mode 100644 index 00000000..4548e9d0 --- /dev/null +++ b/src/nostalgia/studio/filedialogmanager.cpp @@ -0,0 +1,47 @@ +/* + * Copyright 2016 - 2021 gary@drinkingtea.net + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include + +#include + +#include "filedialogmanager.hpp" + +namespace nostalgia { + +studio::TaskState FileDialogManager::update(core::Context *ctx) noexcept { + switch (m_state) { + case UpdateProjectPathState::EnableSystemCursor: { + // switch to system cursor in this update and open file dialog in the next + // the cursor state has to be set before the ImGui frame starts + auto &io = ImGui::GetIO(); + io.MouseDrawCursor = false; + m_state = UpdateProjectPathState::RunFileDialog; + break; + } + case UpdateProjectPathState::RunFileDialog: { + // switch to system cursor + auto [path, err] = studio::chooseDirectory(); + // Mac file dialog doesn't restore focus to main window when closed... + core::focusWindow(ctx); + if (!err) { + err = pathChosen.emitCheckError(path); + oxAssert(err, "Path chosen response failed"); + } + auto &io = ImGui::GetIO(); + io.MouseDrawCursor = true; + m_state = UpdateProjectPathState::None; + return nostalgia::studio::TaskState::Done; + } + case UpdateProjectPathState::None: + break; + } + return nostalgia::studio::TaskState::Running; +} + +} diff --git a/src/nostalgia/studio/filedialogmanager.hpp b/src/nostalgia/studio/filedialogmanager.hpp new file mode 100644 index 00000000..2130a553 --- /dev/null +++ b/src/nostalgia/studio/filedialogmanager.hpp @@ -0,0 +1,50 @@ +/* + * Copyright 2016 - 2021 gary@drinkingtea.net + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#pragma once + +#include +#include + +#include + +#include "lib/filedialog.hpp" +#include "lib/task.hpp" + +namespace nostalgia { + +class FileDialogManager : public nostalgia::studio::Task { + private: + enum class UpdateProjectPathState { + None, + EnableSystemCursor, + RunFileDialog, + Start = EnableSystemCursor, + } m_state = UpdateProjectPathState::Start; + + public: + FileDialogManager() noexcept = default; + + template + explicit FileDialogManager(Args ...args) noexcept; + + ~FileDialogManager() noexcept override = default; + + nostalgia::studio::TaskState update(nostalgia::core::Context *ctx) noexcept final; + + // signals + ox::Signal pathChosen; + +}; + +template +FileDialogManager::FileDialogManager(Args ...args) noexcept { + pathChosen.connect(args...); +} + +} \ No newline at end of file diff --git a/src/nostalgia/studio/json_read.cpp b/src/nostalgia/studio/json_read.cpp deleted file mode 100644 index a8d92829..00000000 --- a/src/nostalgia/studio/json_read.cpp +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2016 - 2021 gary@drinkingtea.net - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -#include "json_read.hpp" - -namespace nostalgia::studio { - -JsonReader::JsonReader(QJsonObject &obj): m_src(obj) { -} - -ox::Error JsonReader::field(QString fieldName, int *dest) { - if (m_src.contains(fieldName)) { - return field(m_src[fieldName], dest); - } else { - return OxError(JSON_ERR_FIELD_MISSING); - } -} - -ox::Error JsonReader::field(QString fieldName, bool *dest) { - if (m_src.contains(fieldName)) { - return field(m_src[fieldName], dest); - } else { - return OxError(JSON_ERR_FIELD_MISSING); - } -} - -ox::Error JsonReader::field(QString fieldName, double *dest) { - if (m_src.contains(fieldName)) { - return field(m_src[fieldName], dest); - } else { - return OxError(JSON_ERR_FIELD_MISSING); - } -} - -ox::Error JsonReader::field(QString fieldName, QString *dest) { - if (m_src.contains(fieldName)) { - return field(m_src[fieldName], dest); - } else { - return OxError(JSON_ERR_FIELD_MISSING); - } -} - - - -ox::Error JsonReader::field(QJsonValueRef src, int *dest) { - if (src.isDouble()) { - *dest = src.toInt(); - return OxError(0); - } else { - return OxError(JSON_ERR_UNEXPECTED_TYPE); - } -} - -ox::Error JsonReader::field(QJsonValueRef src, bool *dest) { - if (src.isBool()) { - *dest = src.toBool(); - return OxError(0); - } else { - return OxError(JSON_ERR_UNEXPECTED_TYPE); - } -} - -ox::Error JsonReader::field(QJsonValueRef src, double *dest) { - if (src.isDouble()) { - *dest = src.toDouble(); - return OxError(0); - } else { - return OxError(JSON_ERR_UNEXPECTED_TYPE); - } -} - -ox::Error JsonReader::field(QJsonValueRef src, QString *dest) { - if (src.isString()) { - *dest = src.toString(); - return OxError(0); - } else { - return OxError(JSON_ERR_UNEXPECTED_TYPE); - } -} - - -} diff --git a/src/nostalgia/studio/json_read.hpp b/src/nostalgia/studio/json_read.hpp deleted file mode 100644 index 2adfac61..00000000 --- a/src/nostalgia/studio/json_read.hpp +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2016 - 2021 gary@drinkingtea.net - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -#pragma once - -#include -#include -#include -#include - -#include - -#include "json_err.hpp" - -namespace nostalgia::studio { - -class JsonReader { - - private: - QJsonObject &m_src; - - public: - explicit JsonReader(QJsonObject &obj); - - ox::Error setTypeInfo(const char*, int) { return OxError(0); }; - - ox::Error field(QString fieldName, int *dest); - - ox::Error field(QString fieldName, bool *dest); - - ox::Error field(QString fieldName, double *dest); - - ox::Error field(QString fieldName, QString *dest); - - template - ox::Error field(QString fieldName, T *dest); - - template - ox::Error field(QString fieldName, QVector *dest); - - private: - static ox::Error field(QJsonValueRef src, int *dest); - - static ox::Error field(QJsonValueRef src, bool *dest); - - static ox::Error field(QJsonValueRef src, double *dest); - - static ox::Error field(QJsonValueRef src, QString *dest); - - template - static ox::Error field(QJsonValueRef src, T *dest); - -}; - -template -ox::Error JsonReader::field(QString fieldName, T *dest) { - if (m_src.contains(fieldName)) { - auto obj = m_src[fieldName].toObject(); - auto reader = JsonReader(obj); - return model(&reader, dest); - } else { - return OxError(JSON_ERR_FIELD_MISSING); - } -} - -template -ox::Error JsonReader::field(QString fieldName, QVector *dest) { - auto err = OxError(0); - if (m_src.contains(fieldName)) { - auto a = m_src[fieldName].toArray(); - dest->resize(a.size()); - for (int i = 0; i < dest->size(); i++) { - oxReturnError(field(a[i], &(*dest)[i])); - } - } else { - err = OxError(JSON_ERR_FIELD_MISSING); - } - return err; -} - -template -ox::Error JsonReader::field(QJsonValueRef src, T *dest) { - auto obj = src.toObject(); - auto reader = JsonReader(obj); - return model(&reader, dest); -} - -template -ox::Error readJson(QString json, T *dest) { - QJsonParseError err; - auto obj = QJsonDocument::fromJson(json.toUtf8(), &err).object(); - if (err.error) { - qDebug() << "JSON parsing error:" << err.errorString(); - return OxError(1); - } - JsonReader rdr(obj); - return model(&rdr, dest); -} - -} diff --git a/src/nostalgia/studio/json_test.cpp b/src/nostalgia/studio/json_test.cpp deleted file mode 100644 index d2704bad..00000000 --- a/src/nostalgia/studio/json_test.cpp +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2016 - 2021 gary@drinkingtea.net - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -#include -#include -#include "json_read.hpp" -#include "json_write.hpp" - -using namespace nostalgia::studio; - -struct TestStructNest { - bool Bool; - int Int; - double Double; - QString String; -}; - -template -ox::Error model(T *io, TestStructNest *obj) { - oxReturnError(io->setTypeInfo("TestStructNest", 4)); - oxReturnError(io->field("Bool", &obj->Bool)); - oxReturnError(io->field("Int", &obj->Int)); - oxReturnError(io->field("Double", &obj->Double)); - oxReturnError(io->field("String", &obj->String)); - return OxError(0); -} - -struct TestStruct { - bool Bool; - int Int; - double Double; - QString String; - TestStructNest Struct; -}; - -template -ox::Error model(T *io, TestStruct *obj) { - oxReturnError(io->setTypeInfo("TestStruct", 5)); - oxReturnError(io->field("Bool", &obj->Bool)); - oxReturnError(io->field("Int", &obj->Int)); - oxReturnError(io->field("Double", &obj->Double)); - oxReturnError(io->field("String", &obj->String)); - oxReturnError(io->field("Struct", &obj->Struct)); - return OxError(0); -} - -int main() { - auto err = OxError(0); - QString json; - TestStruct ts = { - true, - 42, - 42.42, - "Test String", - { - true, - 42, - 42.42, - "Test String" - } - }; - TestStruct tsOut; - oxReturnError(writeJson(&json, &ts)); - oxReturnError(readJson(json, &tsOut)); - - std::cout << tsOut.Bool << '\n'; - std::cout << tsOut.Int << '\n'; - std::cout << tsOut.Double << '\n'; - std::cout << tsOut.String.toStdString() << '\n'; - - oxAssert(tsOut.Bool, "Arg 1 failed"); - oxAssert(tsOut.Int == 42, "Arg 2 failed"); - oxAssert(tsOut.Double == 42.42, "Arg 3 failed"); - oxAssert(tsOut.String == "Test String", "Arg 4 failed"); - - oxAssert(tsOut.Struct.Bool, "Arg 5 failed"); - oxAssert(tsOut.Struct.Int == 42, "Arg 6 failed"); - oxAssert(tsOut.Struct.Double == 42.42, "Arg 7 failed"); - oxAssert(tsOut.Struct.String == "Test String", "Arg 8 failed"); - - return static_cast(err); -} diff --git a/src/nostalgia/studio/json_write.cpp b/src/nostalgia/studio/json_write.cpp deleted file mode 100644 index 4aa27797..00000000 --- a/src/nostalgia/studio/json_write.cpp +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2016 - 2021 gary@drinkingtea.net - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -#include "json_write.hpp" - -namespace nostalgia::studio { - -JsonWriter::JsonWriter(QJsonObject &obj): m_dest(obj) { -} - -ox::Error JsonWriter::field(QString fieldName, int *src) { - m_dest[fieldName] = *src; - return OxError(0); -} - -ox::Error JsonWriter::field(QString fieldName, bool *src) { - m_dest[fieldName] = *src; - return OxError(0); -} - -ox::Error JsonWriter::field(QString fieldName, double *src) { - m_dest[fieldName] = *src; - return OxError(0); -} - -ox::Error JsonWriter::field(QString fieldName, QString *src) { - m_dest[fieldName] = *src; - return OxError(0); -} - -} diff --git a/src/nostalgia/studio/json_write.hpp b/src/nostalgia/studio/json_write.hpp deleted file mode 100644 index 0a7801b8..00000000 --- a/src/nostalgia/studio/json_write.hpp +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2016 - 2021 gary@drinkingtea.net - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -#pragma once - -#include -#include -#include - -#include - -#include "json_err.hpp" - -namespace nostalgia::studio { - -class JsonWriter { - - private: - QJsonObject &m_dest; - - public: - explicit JsonWriter(QJsonObject &obj); - - ox::Error setTypeInfo(const char*, int) { return OxError(0); }; - - ox::Error field(QString fieldName, int *src); - - ox::Error field(QString fieldName, bool *src); - - ox::Error field(QString fieldName, double *src); - - ox::Error field(QString fieldName, QString *src); - - template - ox::Error field(QString fieldName, T *src); - - template - ox::Error field(QString fieldName, QVector *src); - -}; - -template -ox::Error JsonWriter::field(QString fieldName, T *src) { - auto obj = QJsonObject(); - auto reader = JsonWriter(obj); - auto err = model(&reader, src); - m_dest[fieldName] = obj; - return err; -} - -template -ox::Error JsonWriter::field(QString fieldName, QVector *src) { - auto err = OxError(0); - QJsonArray a; - for (int i = 0; i < src->size(); i++) { - err |= field(a[i], &src->at(i)); - } - m_dest[fieldName] = a; - return err; -} - -template -ox::Error writeJson(QString *json, T *src) { - auto obj = QJsonObject(); - JsonWriter rdr(obj); - auto err = model(&rdr, src); - *json = QJsonDocument(obj).toJson(); - return err; -} - -} diff --git a/src/nostalgia/studio/lib/CMakeLists.txt b/src/nostalgia/studio/lib/CMakeLists.txt index 76119099..03b526be 100644 --- a/src/nostalgia/studio/lib/CMakeLists.txt +++ b/src/nostalgia/studio/lib/CMakeLists.txt @@ -1,12 +1,23 @@ -set(CMAKE_INCLUDE_CURRENT_DIR ON) -set(CMAKE_AUTOMOC ON) +if(APPLE) + enable_language(OBJCXX) +endif() + +if(NOT APPLE AND NOT WIN32) + find_package(PkgConfig REQUIRED) + pkg_check_modules(GTK3 REQUIRED gtk+-3.0) +endif() add_library( NostalgiaStudio + configio.cpp editor.cpp - wizard.cpp - module.cpp project.cpp + task.cpp + undostack.cpp + widget.cpp + window.cpp + filedialog_gtk.cpp + $<$:filedialog.mm> ) install(TARGETS NostalgiaStudio @@ -15,19 +26,31 @@ install(TARGETS NostalgiaStudio generate_export_header(NostalgiaStudio) target_include_directories(NostalgiaStudio PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) +find_package(imgui REQUIRED) + +include_directories( + SYSTEM + ${GTK3_INCLUDE_DIRS} +) + target_link_libraries( NostalgiaStudio PUBLIC - Qt${QT_VERSION_MAJOR}::Widgets + imgui::imgui + ${GTK3_LIBRARIES} + OxEvent OxFS OxClaw + NostalgiaCore-Userspace + NostalgiaCore-GLFW ) install( FILES editor.hpp - wizard.hpp - module.hpp + filedialog.hpp project.hpp + task.hpp + undostack.hpp ${CMAKE_CURRENT_BINARY_DIR}/nostalgiastudio_export.h DESTINATION include/nostalgia/studio/lib diff --git a/src/nostalgia/studio/lib/configio.cpp b/src/nostalgia/studio/lib/configio.cpp new file mode 100644 index 00000000..96211ced --- /dev/null +++ b/src/nostalgia/studio/lib/configio.cpp @@ -0,0 +1,13 @@ +/* + * Copyright 2016 - 2021 gary@drinkingtea.net + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "configio.hpp" + + + + diff --git a/src/nostalgia/studio/lib/configio.hpp b/src/nostalgia/studio/lib/configio.hpp new file mode 100644 index 00000000..9e60b753 --- /dev/null +++ b/src/nostalgia/studio/lib/configio.hpp @@ -0,0 +1,105 @@ +/* + * Copyright 2016 - 2021 gary@drinkingtea.net + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace nostalgia::studio { + +constexpr auto ConfigDir = [] { + switch (ox::defines::OS) { + case ox::defines::OS::Darwin: + return "{}/Library/Preferences/NostalgiaStudio"; + case ox::defines::OS::DragonFlyBSD: + case ox::defines::OS::FreeBSD: + case ox::defines::OS::Linux: + case ox::defines::OS::NetBSD: + case ox::defines::OS::OpenBSD: + return "{}/.config/NostalgiaStudio"; + case ox::defines::OS::BareMetal: + case ox::defines::OS::Windows: + return ""; + } +}(); + +template +ox::Result readConfig(const ox::String &name = ox::getModelTypeName()) noexcept { + const auto homeDir = std::getenv("HOME"); + const auto configPath = ox::sfmt(ConfigDir, homeDir).toStdString(); + const auto path = ox::sfmt("{}/{}.json", configPath, name).toStdString(); + std::ifstream file(path, std::ios::binary | std::ios::ate); + if (!file.good()) { + oxErrorf("Could not find config file: {}", path); + return OxError(1, "Could not find config file"); + } + try { + const auto size = file.tellg(); + file.seekg(0, std::ios::beg); + ox::Buffer buff(static_cast(size)); + file.read(buff.data(), size); + return ox::readOC(buff); + } catch (const std::ios_base::failure &e) { + oxErrorf("Could not read config file: {}", e.what()); + return OxError(2, "Could not read config file"); + } +} + +template +ox::Error writeConfig(const ox::String &name, T *data) noexcept { + const auto homeDir = std::getenv("HOME"); + const auto configPath = ox::sfmt(ConfigDir, homeDir).toStdString(); + const auto path = ox::sfmt("{}/{}.json", configPath, name).toStdString(); + std::error_code ec; + std::filesystem::create_directory(configPath, ec); + if (ec) { + oxErrf("Could not create config directory: {}\n", ec.message()); + } + std::ofstream file(path, std::ios::binary | std::ios::ate); + if (!file.good()) { + oxErrorf("Could not find config file: {}", path); + return OxError(1, "Could not find config file"); + } + oxRequireM(buff, ox::writeOC(data)); + buff.back().value = '\n'; + try { + file.write(buff.data(), static_cast>(buff.size())); + return OxError(0); + } catch (const std::ios_base::failure &e) { + oxErrorf("Could not read config file: {}", e.what()); + return OxError(2, "Could not read config file"); + } +} + +template +ox::Error writeConfig(T *data) noexcept { + return writeConfig(ox::getModelTypeName(), data); +} + +template +ox::Error editConfig(const ox::String &name, Func f) noexcept { + auto c = readConfig(name); + f(&c.value); + return writeConfig(name, &c.value); +} + +template +ox::Error editConfig(Func f) noexcept { + return editConfig(ox::getModelTypeName(), f); +} + +} diff --git a/src/nostalgia/studio/lib/editor.cpp b/src/nostalgia/studio/lib/editor.cpp index 6d2c5177..5cc7603d 100644 --- a/src/nostalgia/studio/lib/editor.cpp +++ b/src/nostalgia/studio/lib/editor.cpp @@ -6,13 +6,12 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include + #include "editor.hpp" namespace nostalgia::studio { -Editor::Editor(QWidget *parent): QWidget(parent) { -} - void Editor::cut() { } @@ -32,20 +31,20 @@ void Editor::save() { void Editor::setUnsavedChanges(bool uc) { m_unsavedChanges = uc; - emit unsavedChangesChanged(uc); + unsavedChangesChanged.emit(uc); } bool Editor::unsavedChanges() const noexcept { return m_unsavedChanges; } -QUndoStack *Editor::undoStack() { +UndoStack *Editor::undoStack() { return &m_cmdStack; } void Editor::setExportable(bool exportable) { m_exportable = exportable; - emit exportableChanged(exportable); + exportableChanged.emit(exportable); } bool Editor::exportable() const { @@ -54,7 +53,7 @@ bool Editor::exportable() const { void Editor::setCutEnabled(bool v) { m_cutEnabled = v; - emit cutEnabledChanged(v); + cutEnabledChanged.emit(v); } bool Editor::cutEnabled() const { @@ -63,7 +62,7 @@ bool Editor::cutEnabled() const { void Editor::setCopyEnabled(bool v) { m_copyEnabled = v; - emit copyEnabledChanged(v); + copyEnabledChanged.emit(v); } bool Editor::copyEnabled() const { @@ -72,7 +71,7 @@ bool Editor::copyEnabled() const { void Editor::setPasteEnabled(bool v) { m_pasteEnabled = v; - emit pasteEnabledChanged(v); + pasteEnabledChanged.emit(v); } bool Editor::pasteEnabled() const { diff --git a/src/nostalgia/studio/lib/editor.hpp b/src/nostalgia/studio/lib/editor.hpp index 8a5cb67a..79253461 100644 --- a/src/nostalgia/studio/lib/editor.hpp +++ b/src/nostalgia/studio/lib/editor.hpp @@ -8,18 +8,18 @@ #pragma once -#include -#include +#include + +#include "undostack.hpp" #include "nostalgiastudio_export.h" namespace nostalgia::studio { -class NOSTALGIASTUDIO_EXPORT Editor: public QWidget { - Q_OBJECT +class NOSTALGIASTUDIO_EXPORT Editor: public ox::SignalHandler { private: - QUndoStack m_cmdStack; + UndoStack m_cmdStack; bool m_unsavedChanges = false; bool m_exportable = false; bool m_cutEnabled = false; @@ -27,15 +27,13 @@ class NOSTALGIASTUDIO_EXPORT Editor: public QWidget { bool m_pasteEnabled = false; public: - explicit Editor(QWidget *parent); - ~Editor() override = default; /** * Returns the name of item being edited. */ [[nodiscard]] - virtual QString itemName() const = 0; + virtual ox::String itemName() const = 0; virtual void cut(); @@ -63,7 +61,7 @@ class NOSTALGIASTUDIO_EXPORT Editor: public QWidget { * Returns the undo stack holding changes to the item being edited. */ [[nodiscard]] - QUndoStack *undoStack(); + UndoStack *undoStack(); void setExportable(bool); @@ -91,16 +89,12 @@ class NOSTALGIASTUDIO_EXPORT Editor: public QWidget { */ virtual void saveItem(); - signals: - void unsavedChangesChanged(bool); - - void exportableChanged(bool); - - void cutEnabledChanged(bool); - - void copyEnabledChanged(bool); - - void pasteEnabledChanged(bool); + public: + ox::Signal unsavedChangesChanged; + ox::Signal exportableChanged; + ox::Signal cutEnabledChanged; + ox::Signal copyEnabledChanged; + ox::Signal pasteEnabledChanged; }; diff --git a/src/nostalgia/studio/builtinmodules.hpp b/src/nostalgia/studio/lib/filedialog.hpp similarity index 57% rename from src/nostalgia/studio/builtinmodules.hpp rename to src/nostalgia/studio/lib/filedialog.hpp index 4c6a987e..3b0c9561 100644 --- a/src/nostalgia/studio/builtinmodules.hpp +++ b/src/nostalgia/studio/lib/filedialog.hpp @@ -8,15 +8,8 @@ #pragma once -#include - namespace nostalgia::studio { -[[maybe_unused]] // GCC warns about the existence of this "unused" constexpr list in a header file... -constexpr auto BuiltinModules = { - [] { - return new core::Module(); - }, -}; +ox::Result chooseDirectory() noexcept; -} +} \ No newline at end of file diff --git a/src/nostalgia/studio/lib/filedialog.mm b/src/nostalgia/studio/lib/filedialog.mm new file mode 100644 index 00000000..4b27aa5d --- /dev/null +++ b/src/nostalgia/studio/lib/filedialog.mm @@ -0,0 +1,32 @@ +/* + * Copyright 2016 - 2021 gary@drinkingtea.net + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#import + +#include + +#include "filedialog.hpp" + +namespace nostalgia::studio { + +ox::Result chooseDirectory() noexcept { + auto panel = [NSOpenPanel openPanel]; + [panel setAllowsMultipleSelection:NO]; + [panel setCanChooseDirectories:YES]; + [panel setCanChooseFiles:NO]; + [panel setFloatingPanel:YES]; + if ([panel runModal] == NSModalResponseOK) { + const auto url = [panel URL]; + const auto urlStr = [url absoluteString]; + const auto urlCStr = [urlStr UTF8String]; + return ox::String(urlCStr).substr(7); + } + return OxError(1); +} + +} \ No newline at end of file diff --git a/src/nostalgia/studio/lib/filedialog_gtk.cpp b/src/nostalgia/studio/lib/filedialog_gtk.cpp new file mode 100644 index 00000000..7f7c0d8c --- /dev/null +++ b/src/nostalgia/studio/lib/filedialog_gtk.cpp @@ -0,0 +1,45 @@ +/* + * Copyright 2016 - 2021 gary@drinkingtea.net + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include + +#if !defined(OX_OS_Windows) && !defined(OX_OS_Darwin) +#include + +#include +#include +#include +#include + +namespace nostalgia::studio { + +ox::Result chooseDirectory() noexcept { + ox::Result out; + const auto action = GTK_FILE_CHOOSER_ACTION_OPEN; + gtk_init(0, nullptr); + auto dialog = gtk_file_chooser_dialog_new("Open File", nullptr, action, + ("_Cancel"), GTK_RESPONSE_CANCEL, + ("_Open"), GTK_RESPONSE_ACCEPT, + nullptr); + const auto res = gtk_dialog_run(GTK_DIALOG(dialog)); + oxDebug("gtk_dialog_run returned"); + if (res == GTK_RESPONSE_ACCEPT) { + auto chooser = GTK_FILE_CHOOSER(dialog); + auto path = gtk_file_chooser_get_filename(chooser); + out = ox::String(path); + g_free(path); + } else { + out = OxError(1); + } + gtk_widget_destroy(dialog); + return out; +} + +} + +#endif \ No newline at end of file diff --git a/src/nostalgia/studio/lib/project.cpp b/src/nostalgia/studio/lib/project.cpp index ea498b51..e7792ca8 100644 --- a/src/nostalgia/studio/lib/project.cpp +++ b/src/nostalgia/studio/lib/project.cpp @@ -6,80 +6,83 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -#include -#include +#include + +#include #include "project.hpp" namespace nostalgia::studio { -QString filePathToName(const QString &path, const QString &prefix, const QString &suffix) { - const auto begin = prefix.size(); - const auto end = path.size() - (suffix.size() + prefix.size()); - return path.mid(begin, end); +ox::String filePathToName(const ox::String &path, const ox::String &prefix, const ox::String &suffix) noexcept { + const auto begin = prefix.len(); + const auto end = path.len() - (suffix.len() + prefix.len()); + return path.substr(begin, end); } -Project::Project(const QString &path): m_fs(std::make_unique(path.toUtf8())) { - qDebug() << "Project:" << path; +Project::Project(const ox::String &path) noexcept: m_fs(ox::make_unique(path.c_str())) { + oxTracef("nostalgia::studio", "Project: {}", path); m_path = path; } -void Project::create() { - QDir().mkpath(m_path); +ox::Error Project::create() noexcept { + std::error_code ec; + std::filesystem::create_directory(m_path.toStdString(), ec); + return OxError(ec.value(), "PassThroughFS: mkdir failed"); } -ox::FileSystem *Project::romFs() { +ox::FileSystem *Project::romFs() noexcept { return m_fs.get(); } -void Project::mkdir(const QString &path) const { - oxThrowError(m_fs->mkdir(path.toUtf8().data(), true)); - emit fileUpdated(path); +ox::Error Project::mkdir(const ox::String &path) const noexcept { + oxReturnError(m_fs->mkdir(path.c_str(), true)); + fileUpdated.emit(path); + return OxError(0); } -ox::FileStat Project::stat(const QString &path) const { - oxRequireT(s, m_fs->stat(path.toUtf8().data())); - return s; +ox::Result Project::stat(const ox::String &path) const noexcept { + return m_fs->stat(path.c_str()); } -bool Project::exists(const QString &path) const { - return m_fs->stat(path.toUtf8().data()).error == 0; +bool Project::exists(const ox::String &path) const noexcept { + return m_fs->stat(path.c_str()).error == 0; } -void Project::writeBuff(const QString &path, uint8_t *buff, size_t buffLen) const { - oxThrowError(m_fs->write(path.toUtf8().data(), buff, buffLen)); - emit fileUpdated(path); +ox::Error Project::writeBuff(const ox::String &path, const ox::Buffer &buff) const noexcept { + oxReturnError(m_fs->write(path.c_str(), buff.data(), buff.size())); + fileUpdated.emit(path); + return OxError(0); } -ox::Buffer Project::loadBuff(const QString &path) const { - const auto csPath = path.toUtf8(); - oxRequireMT(buff, m_fs->read(csPath.data())); - return std::move(buff); +ox::Result Project::loadBuff(const ox::String &path) const noexcept { + return m_fs->read(path.c_str()); } -void Project::lsProcDir(QStringList *paths, const QString &path) const { - oxRequireT(files, m_fs->ls(path.toUtf8())); +ox::Error Project::lsProcDir(ox::Vector *paths, const ox::String &path) const noexcept { + oxRequire(files, m_fs->ls(path.c_str())); for (const auto &name : files) { const auto fullPath = path + "/" + name.c_str(); - oxRequireT(stat, m_fs->stat(fullPath.toUtf8().data())); + oxRequire(stat, m_fs->stat(fullPath.c_str())); switch (stat.fileType) { case ox::FileType::NormalFile: paths->push_back(fullPath); break; case ox::FileType::Directory: - lsProcDir(paths, fullPath); + oxReturnError(lsProcDir(paths, fullPath)); break; case ox::FileType::None: break; } } + return OxError(0); } -QStringList Project::listFiles(const QString &path) const { - QStringList paths; - lsProcDir(&paths, path); - return ox::move(paths); +ox::Result> Project::listFiles(const ox::String &path) const noexcept { + ox::Vector paths; + oxReturnError(lsProcDir(&paths, path)); + return paths; } } diff --git a/src/nostalgia/studio/lib/project.hpp b/src/nostalgia/studio/lib/project.hpp index 916e6560..6f9dd3aa 100644 --- a/src/nostalgia/studio/lib/project.hpp +++ b/src/nostalgia/studio/lib/project.hpp @@ -10,13 +10,13 @@ #include -#include -#include - #include +#include +#include #include #include #include +#include #include #include "nostalgiastudio_export.h" @@ -24,6 +24,7 @@ namespace nostalgia::studio { enum class ProjectEvent { + None, FileAdded, // FileRecognized is triggered for all matching files upon a new // subscription to a section of the project and upon the addition of a file. @@ -33,112 +34,114 @@ enum class ProjectEvent { }; [[nodiscard]] -QString filePathToName(const QString &path, const QString &prefix, const QString &suffix); - -class NOSTALGIASTUDIO_EXPORT Project: public QObject { - Q_OBJECT +ox::String filePathToName(const ox::String &path, const ox::String &prefix, const ox::String &suffix) noexcept; +class NOSTALGIASTUDIO_EXPORT Project { private: - QString m_path = ""; - mutable std::unique_ptr m_fs; + ox::String m_path = ""; + mutable ox::UniquePtr m_fs; public: - explicit Project(const QString &path); + explicit Project(const ox::String &path) noexcept; - ~Project() override = default; + ox::Error create() noexcept; - void create(); + [[nodiscard]] + ox::FileSystem *romFs() noexcept; - ox::FileSystem *romFs(); - - void mkdir(const QString &path) const; + ox::Error mkdir(const ox::String &path) const noexcept; /** * Writes a MetalClaw object to the project at the given path. */ template - void writeObj(const QString &path, T *obj) const; + ox::Error writeObj(const ox::String &path, T *obj) const noexcept; template - std::unique_ptr loadObj(const QString &path) const; + ox::Result> loadObj(const ox::String &path) const noexcept; - ox::FileStat stat(const QString &path) const; + ox::Result stat(const ox::String &path) const noexcept; - bool exists(const QString& path) const; + [[nodiscard]] + bool exists(const ox::String& path) const noexcept; template - void subscribe(ProjectEvent e, QObject *tgt, Functor &&slot) const; + ox::Error subscribe(ProjectEvent e, ox::SignalHandler *tgt, Functor &&slot) const noexcept; private: - void writeBuff(const QString &path, uint8_t *buff, size_t buffLen) const; + ox::Error writeBuff(const ox::String &path, const ox::Buffer &buff) const noexcept; - ox::Buffer loadBuff(const QString &path) const; + ox::Result loadBuff(const ox::String &path) const noexcept; - void lsProcDir(QStringList *paths, const QString &path) const; + ox::Error lsProcDir(ox::Vector *paths, const ox::String &path) const noexcept; - QStringList listFiles(const QString &path = "") const; + ox::Result> listFiles(const ox::String &path = "") const noexcept; - signals: - void fileEvent(ProjectEvent, QString path) const; - void fileAdded(QString) const; + // signals + public: + ox::Signal fileEvent; + ox::Signal fileAdded; // FileRecognized is triggered for all matching files upon a new // subscription to a section of the project and upon the addition of a // file. - void fileRecognized(QString) const; - void fileDeleted(QString) const; - void fileUpdated(QString) const; + ox::Signal fileRecognized; + ox::Signal fileDeleted; + ox::Signal fileUpdated; }; template -void Project::writeObj(const QString &path, T *obj) const { +ox::Error Project::writeObj(const ox::String &path, T *obj) const noexcept { // write MetalClaw - oxRequireMT(buff, ox::writeClaw(obj, ox::ClawFormat::Metal)); + oxRequireM(buff, ox::writeClaw(obj, ox::ClawFormat::Metal)); // write to FS - writeBuff(path, ox::bit_cast(buff.data()), buff.size()); - + oxReturnError(writeBuff(path, buff)); // write type descriptor - oxRequireT(type, ox::buildTypeDef(obj)); - oxRequireMT(typeOut, ox::writeClaw(type.get(), ox::ClawFormat::Organic)); + oxRequire(type, ox::buildTypeDef(obj)); + oxRequireM(typeOut, ox::writeClaw(type.get(), ox::ClawFormat::Organic)); // replace garbage last character with new line typeOut.back().value = '\n'; // write to FS - QString descPath = "/.nostalgia/type_descriptors/"; + ox::String descPath = "/.nostalgia/type_descriptors/"; const auto typePath = descPath + type->typeName.c_str(); - mkdir(descPath); - writeBuff(typePath, ox::bit_cast(typeOut.data()), typeOut.size()); - emit fileUpdated(path); + oxReturnError(mkdir(descPath)); + oxReturnError(writeBuff(typePath, typeOut)); + fileUpdated.emit(path); + return OxError(0); } template -std::unique_ptr Project::loadObj(const QString &path) const { - auto obj = std::make_unique(); - auto buff = loadBuff(path); - oxThrowError(ox::readClaw(buff.data(), buff.size(), obj.get())); - return obj; +ox::Result> Project::loadObj(const ox::String &path) const noexcept { + auto obj = ox::make_unique(); + oxRequire(buff, loadBuff(path)); + return ox::readClaw(buff); } template -void Project::subscribe(ProjectEvent e, QObject *tgt, Functor &&slot) const { +ox::Error Project::subscribe(ProjectEvent e, ox::SignalHandler *tgt, Functor &&slot) const noexcept { switch (e) { + case ProjectEvent::None: + break; case ProjectEvent::FileAdded: - connect(this, &Project::fileAdded, tgt, (slot)); + connect(this, &Project::fileAdded, tgt, slot); break; case ProjectEvent::FileRecognized: { - for (auto f : listFiles()) { + oxRequire(fileList, listFiles()); + for (auto f : fileList) { slot(f); } - connect(this, &Project::fileRecognized, tgt, (slot)); + connect(this, &Project::fileRecognized, tgt, slot); break; } case ProjectEvent::FileDeleted: - connect(this, &Project::fileDeleted, tgt, (slot)); + connect(this, &Project::fileDeleted, tgt, slot); break; case ProjectEvent::FileUpdated: - connect(this, &Project::fileUpdated, tgt, (slot)); + connect(this, &Project::fileUpdated, tgt, slot); break; } + return OxError(0); } } diff --git a/src/nostalgia/studio/lib/task.cpp b/src/nostalgia/studio/lib/task.cpp new file mode 100644 index 00000000..85842db5 --- /dev/null +++ b/src/nostalgia/studio/lib/task.cpp @@ -0,0 +1,25 @@ +/* + * Copyright 2016 - 2021 gary@drinkingtea.net + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "task.hpp" + +namespace nostalgia::studio { + +void TaskRunner::update(core::Context *ctx) noexcept { + for (auto i = 0u; i < m_tasks.size(); ++i) { + if (m_tasks[i]->update(ctx) == studio::TaskState::Done) { + oxIgnoreError(m_tasks.erase(i--)); + } + } +} + +void TaskRunner::add(Task *task) noexcept { + m_tasks.emplace_back(task); +} + +} \ No newline at end of file diff --git a/src/nostalgia/studio/lib/task.hpp b/src/nostalgia/studio/lib/task.hpp new file mode 100644 index 00000000..b5138f1f --- /dev/null +++ b/src/nostalgia/studio/lib/task.hpp @@ -0,0 +1,36 @@ +/* + * Copyright 2016 - 2021 gary@drinkingtea.net + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#pragma once + +#include + +#include + +namespace nostalgia::studio { + +enum class TaskState { + Running, + Done +}; + +class Task: public ox::SignalHandler { + public: + virtual ~Task() noexcept = default; + virtual TaskState update(nostalgia::core::Context *ctx) noexcept = 0; +}; + +class TaskRunner { + private: + ox::Vector> m_tasks; + public: + void update(core::Context *ctx) noexcept; + void add(Task *task) noexcept; +}; + +} \ No newline at end of file diff --git a/src/nostalgia/studio/lib/undostack.cpp b/src/nostalgia/studio/lib/undostack.cpp new file mode 100644 index 00000000..2f38bb1e --- /dev/null +++ b/src/nostalgia/studio/lib/undostack.cpp @@ -0,0 +1,29 @@ +/* + * Copyright 2016 - 2021 gary@drinkingtea.net + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "undostack.hpp" + +namespace nostalgia::studio { + +void UndoStack::push(UndoCommand *cmd) noexcept { + for (const auto i = m_stackIdx; i < m_stack.size();) { + oxIgnoreError(m_stack.erase(i)); + } + m_stack.emplace_back(cmd); + ++m_stackIdx; +} + +void UndoStack::redo() noexcept { + m_stack[m_stackIdx++]->redo(); +} + +void UndoStack::undo() noexcept { + m_stack[m_stackIdx--]->undo(); +} + +} \ No newline at end of file diff --git a/src/nostalgia/studio/lib/undostack.hpp b/src/nostalgia/studio/lib/undostack.hpp new file mode 100644 index 00000000..68227c97 --- /dev/null +++ b/src/nostalgia/studio/lib/undostack.hpp @@ -0,0 +1,37 @@ +/* + * Copyright 2016 - 2021 gary@drinkingtea.net + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#pragma once + +#include +#include +#include + +namespace nostalgia::studio { + +class UndoCommand { + public: + virtual ~UndoCommand() noexcept = default; + virtual void redo() noexcept = 0; + virtual void undo() noexcept = 0; +}; + +class UndoStack { + private: + ox::Vector> m_stack; + std::size_t m_stackIdx = 0; + + public: + void push(UndoCommand *cmd) noexcept; + + void redo() noexcept; + + void undo() noexcept; +}; + +} \ No newline at end of file diff --git a/src/nostalgia/studio/lib/widget.cpp b/src/nostalgia/studio/lib/widget.cpp new file mode 100644 index 00000000..fd71e3f5 --- /dev/null +++ b/src/nostalgia/studio/lib/widget.cpp @@ -0,0 +1,13 @@ +/* + * Copyright 2016 - 2021 gary@drinkingtea.net + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "widget.hpp" + +namespace nostalgia::studio { + +} \ No newline at end of file diff --git a/src/nostalgia/studio/lib/context.hpp b/src/nostalgia/studio/lib/widget.hpp similarity index 65% rename from src/nostalgia/studio/lib/context.hpp rename to src/nostalgia/studio/lib/widget.hpp index b305627d..f821cfaa 100644 --- a/src/nostalgia/studio/lib/context.hpp +++ b/src/nostalgia/studio/lib/widget.hpp @@ -8,17 +8,15 @@ #pragma once -#include -#include +#include + +#include namespace nostalgia::studio { -class Context { +class Widget: public ox::SignalHandler { public: - QString appName; - QString orgName; - QWidget *tabParent = nullptr; - const class Project *project = nullptr; + virtual void draw(core::Context*) noexcept = 0; }; -} +} \ No newline at end of file diff --git a/src/nostalgia/studio/lib/window.cpp b/src/nostalgia/studio/lib/window.cpp new file mode 100644 index 00000000..5c72c90f --- /dev/null +++ b/src/nostalgia/studio/lib/window.cpp @@ -0,0 +1,13 @@ +/* + * Copyright 2016 - 2021 gary@drinkingtea.net + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "window.hpp" + + + + diff --git a/src/nostalgia/studio/json_err.hpp b/src/nostalgia/studio/lib/window.hpp similarity index 81% rename from src/nostalgia/studio/json_err.hpp rename to src/nostalgia/studio/lib/window.hpp index 57984099..b55a303d 100644 --- a/src/nostalgia/studio/json_err.hpp +++ b/src/nostalgia/studio/lib/window.hpp @@ -8,11 +8,12 @@ #pragma once +#include "widget.hpp" + namespace nostalgia::studio { -enum { - JSON_ERR_FIELD_MISSING = 1, - JSON_ERR_UNEXPECTED_TYPE = 2, +class Window: public Widget { + }; -} +} \ No newline at end of file diff --git a/src/nostalgia/studio/lib/wizard.cpp b/src/nostalgia/studio/lib/wizard.cpp deleted file mode 100644 index 5d2ad34a..00000000 --- a/src/nostalgia/studio/lib/wizard.cpp +++ /dev/null @@ -1,357 +0,0 @@ -/* - * Copyright 2016 - 2021 gary@drinkingtea.net - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "context.hpp" -#include "project.hpp" -#include "module.hpp" -#include "wizard.hpp" - -namespace nostalgia::studio { - -PathExistsValidator::PathExistsValidator(const Context *ctx, QString pathTemplate, bool shouldExist) { - m_ctx = ctx; - m_pathTemplate = pathTemplate; - m_shouldExist = shouldExist; -} - -QValidator::State PathExistsValidator::validate(QString &input, int&) const { - auto path = m_pathTemplate.arg(input); - if (m_ctx->project->exists(path) == m_shouldExist) { - return QValidator::Acceptable; - } - return QValidator::Invalid; -} - -WizardSelect::WizardSelect() { - m_listWidget = new QListWidget(this); - auto layout = new QVBoxLayout(this); - layout->addWidget(m_listWidget); - setLayout(layout); - - connect(m_listWidget, &QListWidget::currentRowChanged, this, &WizardSelect::itemSelected); - connect(m_listWidget, &QListWidget::itemSelectionChanged, [this]() { - m_complete = true; - emit completeChanged(); - } - ); - connect(m_listWidget, &QListWidget::doubleClicked, [this]() { - wizard()->next(); - } - ); -} - -void WizardSelect::initializePage() { - emit completeChanged(); -} - -void WizardSelect::addOption(const WizardMaker &wm) { - m_options[wm.name] = {wm.make, wm.onAccept}; - m_listWidget->addItem(wm.name); -} - -bool WizardSelect::isComplete() const { - return m_complete; -} - -void WizardSelect::itemSelected(int row) { - auto w = dynamic_cast(wizard()); - if (w && row > -1) { - // remove other pages - while (nextId() > -1) { - w->removePage(nextId()); - } - - auto selected = m_listWidget->currentItem()->text(); - if (m_options.contains(selected)) { - auto &o = m_options[selected]; - for (auto p : o.make()) { - w->addPage(p); - } - w->setAccept(o.onAccept); - // for some reason the continue button only appears correctly after remove runs - w->removePage(w->addPage(new QWizardPage())); - } - } -} - - -WizardConclusionPage::WizardConclusionPage(QString msg, QVector fields) { - m_baseMsg = msg; - m_fields = fields; - setLayout(new QVBoxLayout(this)); -} - -void WizardConclusionPage::initializePage() { - QString msg = m_baseMsg; - for (auto field : m_fields) { - msg = msg.arg(this->field(field).toString()); - } - - auto text = new QLabel(msg, this); - if (m_text) { - layout()->replaceWidget(m_text, text); - delete m_text; - } else { - layout()->addWidget(text); - } - m_text = text; -} - - -void WizardFormPage::Field::setDisplayText(QString text) { - auto le = dynamic_cast(this->valueControl); - auto cb = dynamic_cast(this->valueControl); - if (le) { - le->setText(text); - } else if (cb) { - cb->setCurrentText(text); - } -} - -QString WizardFormPage::Field::getDisplayText() const { - auto le = dynamic_cast(this->valueControl); - auto cb = dynamic_cast(this->valueControl); - if (le) { - return le->text(); - } else if (cb) { - return cb->currentText(); - } else { - return ""; - } -} - -WizardFormPage::WizardFormPage() { - m_layout = new QGridLayout(this); - m_layout->setColumnMinimumWidth(0, 20); - this->setLayout(m_layout); -} - -WizardFormPage::~WizardFormPage() { - for (auto l : m_subLayout) { - delete l; - } -} - -int WizardFormPage::accept() { - return 0; -} - -void WizardFormPage::initializePage() { - for (auto it = m_fields.begin(); it != m_fields.end(); it++) { - auto key = it.key(); - auto defaultVal = it.value().defaultValue; - m_fields[key].setDisplayText(defaultVal); - } -} - -bool WizardFormPage::validatePage() { - bool retval = true; - - // check validators - for (auto f : m_fields) { - if (f.validator != nullptr) { - if (f.validator(f.getDisplayText()) != 0) { - retval = false; - break; - } - } - } - - // clear error - if (retval) { - showValidationError(""); - } - - return retval; -} - -QComboBox *WizardFormPage::addComboBox(QString displayName, QString fieldName, QStringList options) { - auto lbl = new QLabel(displayName, this); - auto cb = new QComboBox(this); - lbl->setBuddy(cb); - - m_layout->addWidget(lbl, m_currentLine, 0); - m_layout->addWidget(cb, m_currentLine, 1); - - auto field = &m_fields[fieldName]; - - field->valueControl = cb; - - registerField(fieldName, cb); - - for (auto o : options) { - cb->addItem(o); - } - - connect(cb, &QComboBox::currentTextChanged, [this, fieldName, field](QString txt) { - if (field->value == "" && txt != "") { - m_validFields++; - } else if (field->value != "" && txt == "") { - m_validFields--; - } - field->value = txt; - emit completeChanged(); - } - ); - - m_currentLine++; - - return cb; -} - -QLineEdit *WizardFormPage::addLineEdit(QString displayName, QString fieldName, QString defaultVal, std::function validator) { - auto lbl = new QLabel(displayName, this); - auto le = new QLineEdit(this); - lbl->setBuddy(le); - - m_layout->addWidget(lbl, m_currentLine, 0); - m_layout->addWidget(le, m_currentLine, 1); - - auto field = &m_fields[fieldName]; - - field->defaultValue = defaultVal; - field->valueControl = le; - field->validator = validator; - - registerField(fieldName, le); - - connect(le, &QLineEdit::textChanged, [this, fieldName, field](QString txt) { - if (field->value == "" && txt != "") { - m_validFields++; - } else if (field->value != "" && txt == "") { - m_validFields--; - } - field->value = txt; - emit completeChanged(); - } - ); - - m_currentLine++; - return le; -} - -void WizardFormPage::addPathBrowse(QString displayName, QString fieldName, QString defaultVal, - QFileDialog::FileMode fileMode, QString fileExtensions) { - auto layout = new QHBoxLayout(); - auto lbl = new QLabel(displayName, this); - auto le = new QLineEdit("", this); - auto btn = new QPushButton("Browse...", this); - lbl->setBuddy(le); - - layout->addWidget(le); - layout->addWidget(btn); - m_layout->addWidget(lbl, m_currentLine, 0); - m_layout->addLayout(layout, m_currentLine, 1); - - m_subLayout.push_back(layout); - m_fields[fieldName].defaultValue = defaultVal; - m_fields[fieldName].valueControl = le; - m_fields[fieldName].validator = [this, fileMode](QString path) { - if (fileMode == QFileDialog::Directory) { - if (!QDir(path).exists()) { - showValidationError(tr("Specified directory path does not exist.")); - return 1; - } else { - return 0; - } - } else if (fileMode == QFileDialog::ExistingFile) { - if (!QFile(path).exists()) { - showValidationError(tr("Specified directory path does not exist.")); - return 1; - } else { - return 0; - } - } else { - return 2; - } - }; - - registerField(fieldName, le); - - connect(le, &QLineEdit::textChanged, [this, fieldName](QString txt) { - if (m_fields[fieldName].value == "" && txt != "") { - m_validFields++; - } else if (m_fields[fieldName].value != "" && txt == "") { - m_validFields--; - } - m_fields[fieldName].value = txt; - emit completeChanged(); - } - ); - - connect(btn, &QPushButton::clicked, [this, defaultVal, le, fileMode, fileExtensions]() { - auto dir = defaultVal; - if (dir == "") { - dir = QDir::homePath(); - } - if (fileMode == QFileDialog::Directory) { - auto p = QFileDialog::getExistingDirectory(this, tr("Select Directory..."), dir); - if (p != "") { - le->setText(p); - } - } else if (fileMode == QFileDialog::ExistingFile) { - auto p = QFileDialog::getOpenFileName(this, tr("Select File..."), dir, fileExtensions); - if (p != "") { - le->setText(p); - } - } - } - ); - - m_currentLine++; -} - -void WizardFormPage::showValidationError(QString msg) { - // create label if it is null - if (!m_errorMsg) { - m_errorMsg = new QLabel(this); - m_layout->addWidget(m_errorMsg, m_currentLine, 0, m_currentLine, 2); - - // set text color - auto pal = m_errorMsg->palette(); - pal.setColor(m_errorMsg->backgroundRole(), Qt::red); - pal.setColor(m_errorMsg->foregroundRole(), Qt::red); - m_errorMsg->setPalette(pal); - } - - // set message - if (msg != "") { - m_errorMsg->setText(msg); - } else { - m_errorMsg->setText(""); - } -} - - -Wizard::Wizard(QString windowTitle, QWidget *parent): QWizard(parent) { - setWindowTitle(windowTitle); - setModal(true); -} - -void Wizard::setAccept(std::function acceptFunc) { - m_acceptFunc = acceptFunc; -} - -void Wizard::accept() { - auto page = dynamic_cast(currentPage()); - if ((page == nullptr || page->accept() == 0) && - m_acceptFunc != nullptr && m_acceptFunc(this) == 0) { - QDialog::accept(); - } -} - -} diff --git a/src/nostalgia/studio/lib/wizard.hpp b/src/nostalgia/studio/lib/wizard.hpp deleted file mode 100644 index fa367662..00000000 --- a/src/nostalgia/studio/lib/wizard.hpp +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright 2016 - 2021 gary@drinkingtea.net - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -#pragma once - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "nostalgiastudio_export.h" - -namespace nostalgia::studio { - -class PathExistsValidator: public QValidator { - private: - const class Context *m_ctx = nullptr; - QString m_pathTemplate; - bool m_shouldExist = true; - - public: - PathExistsValidator(const class Context *ctx, QString pathTemplate, bool shouldExist); - - QValidator::State validate(QString &input, int &pos) const override; - -}; - -struct NOSTALGIASTUDIO_EXPORT WizardMaker { - QString name; - std::function()> make; - std::function onAccept = [](QWizard*) { return 0; }; -}; - -class NOSTALGIASTUDIO_EXPORT WizardSelect: public QWizardPage { - Q_OBJECT - - private: - struct WizardFuncs { - std::function()> make; - std::function onAccept; - }; - - QHash m_options; - QListWidget *m_listWidget = nullptr; - bool m_complete = false; - - public: - WizardSelect(); - - void addOption(const WizardMaker &wm); - - void initializePage() override; - - bool isComplete() const override; - - private slots: - void itemSelected(int row); -}; - - -class NOSTALGIASTUDIO_EXPORT WizardFormPage: public QWizardPage { - Q_OBJECT - - private: - struct Field { - QString defaultValue = ""; - QString value = ""; - QWidget *valueControl = nullptr; - std::function validator; - - void setDisplayText(QString text); - - QString getDisplayText() const; - }; - QLabel *m_errorMsg = nullptr; - QGridLayout *m_layout = nullptr; - QVector m_subLayout; - QMap m_fields; - int m_currentLine = 0; - int m_validFields = 0; - - public: - WizardFormPage(); - - ~WizardFormPage(); - - virtual int accept(); - - void initializePage() override; - - bool validatePage() override; - - QComboBox *addComboBox(QString displayName, QString fieldName, QStringList options); - - QLineEdit *addLineEdit(QString displayName, QString fieldName, - QString defaultVal = "", - std::function validator = [](QString) { return 0; }); - - void addPathBrowse(QString displayName, QString fieldName, - QString defaultVal = QDir::homePath(), - QFileDialog::FileMode fileMode = QFileDialog::AnyFile, - QString fileExtensions = ""); - - void showValidationError(QString msg); -}; - - -class NOSTALGIASTUDIO_EXPORT WizardConclusionPage: public QWizardPage { - Q_OBJECT - private: - QString m_baseMsg = ""; - QLabel *m_text = nullptr; - QVector m_fields; - - public: - WizardConclusionPage(QString msg, QVector field); - - ~WizardConclusionPage() override = default; - - void initializePage() override; -}; - - -class NOSTALGIASTUDIO_EXPORT Wizard: public QWizard { - Q_OBJECT - private: - std::function m_acceptFunc; - - public: - Wizard(QString windowTitle, QWidget *parent = 0); - - void setAccept(std::function acceptFunc); - - void accept(); -}; - -} diff --git a/src/nostalgia/studio/main.cpp b/src/nostalgia/studio/main.cpp index a3a9b2f3..11c10af6 100644 --- a/src/nostalgia/studio/main.cpp +++ b/src/nostalgia/studio/main.cpp @@ -6,38 +6,59 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -#include -#include -#include -#include #include -#include -#include "mainwindow.hpp" -using namespace nostalgia::studio; +#include -int main(int argc, char **args) { +#include "studioapp.hpp" + +namespace nostalgia { + +class StudioUIDrawer: public core::Drawer { + private: + StudioUI *m_ui = nullptr; + public: + explicit StudioUIDrawer(StudioUI *ui) noexcept: m_ui(ui) { + } + protected: + void draw(core::Context *ctx) noexcept final { + m_ui->draw(ctx); + } +}; + +static int eventHandler(core::Context *ctx) noexcept { + auto ui = ctx->customData(); + ui->update(ctx); + return 16; +} + +static ox::Error run(ox::FileSystem *fs) noexcept { + oxRequireM(ctx, core::init(fs)); + core::setWindowTitle(ctx.get(), "Nostalgia Studio"); + core::setEventHandler(ctx.get(), eventHandler); + StudioUI ui; + StudioUIDrawer drawer(&ui); + ctx->setCustomData(&ui); + ctx->drawers.emplace_back(&drawer); + return core::run(ctx.get()); +} + +static ox::Error run(int argc, const char **argv) noexcept { ox::trace::init(); - // get profile path from command args - ox::ClArgs clargs(argc, const_cast(args)); - QString argProfilePath = clargs.getString("profile", ":/profiles/nostalgia-studio.json").c_str(); - QApplication app(argc, args); - // load theme - if constexpr(ox::defines::OS != ox::defines::OS::Darwin) { - qdark::load(&app); - } - // force QtQuick to use OpenGL (https://doc.qt.io/qt-6/quick-changes-qt6.html#changes-to-qquickwidget) -#if QT_VERSION >= 0x060000 - QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGLRhi); -#endif - // open window - try { - MainWindow w(argProfilePath); - app.setApplicationName(w.windowTitle()); - w.show(); - QObject::connect(&app, &QApplication::aboutToQuit, &w, &MainWindow::onExit); - return app.exec(); - } catch (const ox::Error &err) { - oxPanic(err, "Unhandled ox::Error"); + if (argc >= 2) { + const auto path = argv[1]; + oxRequire(fs, core::loadRomFs(path)); + return run(fs.get()); + } else { + return run(nullptr); } } + +} + +int main(int argc, const char **argv) { + ox::trace::init(); + const auto err = nostalgia::run(argc, argv); + oxAssert(err, "Something went wrong..."); + return static_cast(err); +} diff --git a/src/nostalgia/studio/mainwindow.cpp b/src/nostalgia/studio/mainwindow.cpp deleted file mode 100644 index ffd68c86..00000000 --- a/src/nostalgia/studio/mainwindow.cpp +++ /dev/null @@ -1,611 +0,0 @@ -/* - * Copyright 2016 - 2021 gary@drinkingtea.net - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "lib/editor.hpp" -#include "lib/project.hpp" -#include "lib/wizard.hpp" - -#include "json_read.hpp" -#include "json_write.hpp" -#include "mainwindow.hpp" - -namespace nostalgia::studio { - -MainWindow::MainWindow(QString profilePath) { - m_profilePath = profilePath; - // load in profile file - QFile file(profilePath); - if (file.exists()) { - file.open(QIODevice::ReadOnly); - QTextStream in(&file); - oxThrowError(readJson(in.readAll(), &m_profile)); - qDebug() << m_profile.appName; - } - - auto screenSize = QApplication::screens().first()->geometry(); - - // set window to 75% of screen width, and center NostalgiaStudioProfile - constexpr auto sizePct = 0.75; - resize(static_cast(screenSize.width() * sizePct), static_cast(screenSize.height() * sizePct)); - move(-x(), -y()); - move(static_cast(screenSize.width() * (1 - sizePct) / 2), static_cast(screenSize.height() * (1 - sizePct) / 2)); - - setWindowTitle(m_profile.appName); - m_ctx.appName = m_profile.appName; - m_ctx.orgName = m_profile.orgName; - m_ctx.tabParent = m_tabs; - - m_tabs = new QTabWidget(this); - auto tabBar = m_tabs->tabBar(); - setCentralWidget(m_tabs); - m_tabs->setTabsClosable(true); - connect(m_tabs, &QTabWidget::tabCloseRequested, this, &MainWindow::closeTab); - connect(tabBar, &QTabBar::tabMoved, this, &MainWindow::moveTab); - connect(tabBar, &QTabBar::currentChanged, this, &MainWindow::changeTab); - tabBar->setMovable(true); - - setupMenu(); - setupProjectExplorer(); - statusBar(); // setup status bar - - loadModules(); - - readState(); -} - -MainWindow::~MainWindow() { - closeProject(); -} - -void MainWindow::loadModules() { - for (auto p : BuiltinModules) { - auto module = p(); - loadModule(module); - delete module; - } - for (auto dir : m_profile.modulesPath) { - QFileInfo dirInfo(m_profilePath); - dir = dirInfo.absolutePath() + "/" + dir; - if (dirInfo.exists()) { - qDebug() << "Checking module directory:" << dir; - loadModuleDir(dir); - } - } -} - -void MainWindow::loadModuleDir(QString dir) { - for (auto modulePath : QDir(dir).entryList()) { - modulePath = dir + '/' + modulePath; - if (QLibrary::isLibrary(modulePath)) { - loadModule(modulePath); - } - } -} - -void MainWindow::loadModule(QString modulePath) { - qDebug() << "Loading module:" << modulePath; - QPluginLoader loader(modulePath); - auto module = qobject_cast(loader.instance()); - if (module) { - loadModule(module); - delete module; - } else { - qInfo() << loader.errorString(); - } -} - -void MainWindow::loadModule(Module *module) { - m_modules.push_back(module); - auto editorMakers = module->editors(&m_ctx); - for (const auto &em : editorMakers) { - for (const auto &ext : em.fileTypes) { - m_editorMakers[ext] = em; - } - } -} - -void MainWindow::setupMenu() { - auto menu = menuBar(); - auto fileMenu = menu->addMenu(tr("&File")); - auto editMenu = menu->addMenu(tr("&Edit")); - m_viewMenu = menu->addMenu(tr("&View")); - - // New... - addAction( - fileMenu, - tr("&New..."), - tr(""), - QKeySequence::New, - this, - SLOT(showNewWizard()) - ); - - // Import... - m_importAction = addAction( - fileMenu, - tr("&Import..."), - tr(""), - this, - SLOT(showImportWizard()) - ); - m_importAction->setEnabled(false); - - // Open Project - addAction( - fileMenu, - tr("&Open Project"), - tr(""), - QKeySequence::Open, - this, - SLOT(openProject()) - ); - - // Save Project - m_saveAction = addAction( - fileMenu, - tr("&Save File"), - tr(""), - QKeySequence::Save, - this, - SLOT(saveFile()) - ); - m_saveAction->setEnabled(false); - - // Save Project - m_exportAction = addAction( - fileMenu, - tr("&Export File..."), - tr(""), - this, - SLOT(exportFile()) - ); - m_exportAction->setEnabled(false); - - // Exit - addAction( - fileMenu, - tr("E&xit"), - tr("Exit the application"), - QKeySequence::Quit, - QApplication::quit - ); - - // Undo - auto undoAction = m_undoGroup.createUndoAction(this, tr("&Undo")); - editMenu->addAction(undoAction); - undoAction->setShortcuts(QKeySequence::Undo); - - // Redo - auto redoAction = m_undoGroup.createRedoAction(this, tr("&Redo")); - editMenu->addAction(redoAction); - redoAction->setShortcuts(QKeySequence::Redo); - - editMenu->addSeparator(); - - // Copy - m_cutAction = addAction( - editMenu, - tr("&Cut"), - tr(""), - QKeySequence::Cut, - this, - SLOT(cutAction()) - ); - m_cutAction->setEnabled(false); - - // Copy - m_copyAction = addAction( - editMenu, - tr("&Copy"), - tr(""), - QKeySequence::Copy, - this, - SLOT(copyAction()) - ); - m_copyAction->setEnabled(false); - - // Paste - m_pasteAction = addAction( - editMenu, - tr("&Paste"), - tr(""), - QKeySequence::Paste, - this, - SLOT(pasteAction()) - ); - m_pasteAction->setEnabled(false); -} - -void MainWindow::setupProjectExplorer() { - // setup dock - auto dock = new QDockWidget(tr("Project"), this); - dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); - dock->setObjectName("Project Explorer"); - addDockWidget(Qt::LeftDockWidgetArea, dock); - resizeDocks({dock}, {static_cast(width() * 0.25)}, Qt::Horizontal); - - // setup tree view - m_projectExplorer = new QTreeView(dock); - m_projectExplorer->header()->hide(); - dock->setWidget(m_projectExplorer); - - connect(m_projectExplorer, &QTreeView::activated, this, &MainWindow::openFileSlot); -} - -void MainWindow::addDockWidget(Qt::DockWidgetArea area, QDockWidget *dockWidget) { - QMainWindow::addDockWidget(area, dockWidget); - m_viewMenu->addAction(dockWidget->toggleViewAction()); - m_dockWidgets.push_back(dockWidget); -} - -QAction *MainWindow::addAction(QMenu *menu, QString text, QString toolTip, const QObject *tgt, const char *cb) { - auto action = menu->addAction(text); - action->setStatusTip(toolTip); - connect(action, SIGNAL(triggered()), tgt, cb); - return action; -} - -QAction *MainWindow::addAction(QMenu *menu, QString text, QString toolTip, - QKeySequence::StandardKey key, const QObject *tgt, const char *cb) { - auto action = menu->addAction(text); - action->setShortcuts(key); - action->setStatusTip(toolTip); - connect(action, SIGNAL(triggered()), tgt, cb); - return action; -} - -QAction *MainWindow::addAction(QMenu *menu, QString text, QString toolTip, - QKeySequence::StandardKey key, void (*cb)()) { - auto action = menu->addAction(text); - action->setShortcuts(key); - action->setStatusTip(toolTip); - connect(action, &QAction::triggered, cb); - return action; -} - -int MainWindow::readState() { - int err = 0; - - QSettings settings(m_profile.orgName, m_profile.appName); - settings.beginGroup("MainWindow"); - restoreGeometry(settings.value("geometry").toByteArray()); - restoreState(settings.value("windowState").toByteArray()); - auto json = settings.value("json").toString(); - oxReturnError(readJson(json, &m_state)); - settings.endGroup(); - - openProject(m_state.projectPath); - - return err; -} - -void MainWindow::writeState() { - // generate JSON for application specific state info - QString json; - oxIgnoreError(writeJson(&json, &m_state)); - - QSettings settings(m_profile.orgName, m_profile.appName); - settings.beginGroup("MainWindow"); - settings.setValue("geometry", saveGeometry()); - settings.setValue("windowState", saveState()); - settings.setValue("json", json); - settings.endGroup(); -} - -/** - * Read open editor tabs for current project. - */ -QStringList MainWindow::readTabs() const { - QStringList tabs; - QSettings settings(m_profile.orgName, m_profile.appName); - settings.beginReadArray("MainWindow/Project:" + m_state.projectPath); - auto size = settings.beginReadArray("openEditors"); - for (int i = 0; i < size; i++) { - settings.setArrayIndex(i); - tabs.append(settings.value("filePath").toString()); - } - settings.endArray(); - return tabs; -} - -/** - * Write open editor tabs for current project. - */ -void MainWindow::writeTabs(QStringList tabs) const { - QSettings settings(m_profile.orgName, m_profile.appName); - settings.beginWriteArray("MainWindow/Project:" + m_state.projectPath + "/openEditors"); - for (int i = 0; i < tabs.size(); i++) { - settings.setArrayIndex(i); - settings.setValue("filePath", tabs[i]); - } - settings.endArray(); -} - -void MainWindow::openProject(QString projectPath) { - closeProject(); - auto project = new Project(projectPath); - if (m_ctx.project) { - delete m_ctx.project; - m_ctx.project = nullptr; - } - m_ctx.project = project; - m_oxfsView = new OxFSModel(project->romFs()); - m_projectExplorer->setModel(m_oxfsView); - connect(m_ctx.project, &Project::fileUpdated, m_oxfsView, &OxFSModel::updateFile); - m_importAction->setEnabled(true); - m_state.projectPath = projectPath; - // reopen tabs - auto openTabs = readTabs(); - // clear open tabs - writeTabs({}); - for (auto t : openTabs) { - try { - openFile(t, true); - } catch (const ox::Error &err) { - qInfo().nospace() << "Error opening tab: " << t << ", " << static_cast(err) << ", " << err.file << ":" << err.line; - oxTracef("nostalgia::studio::MainWindow::openProject", "Error opening tab: {}, {}, {}:{}", static_cast(err), static_cast(err), err.file, err.line); - } catch (...) { - qInfo() << "Error opening tab: " << t; - oxTracef("nostalgia::studio::MainWindow::openProject", "Error opening tab: {}", t); - } - } - qInfo() << "Open project:" << projectPath; -} - -void MainWindow::closeProject() { - // delete tabs - while (m_tabs->count()) { - auto tab = dynamic_cast(m_tabs->widget(0)); - m_undoGroup.removeStack(tab->undoStack()); - m_tabs->removeTab(0); - delete tab; - } - - if (m_ctx.project) { - disconnect(m_ctx.project, &Project::fileUpdated, m_oxfsView, &OxFSModel::updateFile); - - delete m_ctx.project; - m_ctx.project = nullptr; - - delete m_oxfsView; - m_oxfsView = nullptr; - } - if (m_projectExplorer->model()) { - delete m_projectExplorer->model(); - } - m_projectExplorer->setModel(nullptr); - - m_importAction->setEnabled(false); - - m_state.projectPath = ""; -} - -void MainWindow::openFile(QString path, bool force) { - if (!force && readTabs().contains(path)) { - return; - } - const auto dotIdx = path.lastIndexOf('.') + 1; - if (dotIdx == -1) { - return; - } - const auto ext = path.mid(dotIdx); - const auto lastSlash = path.lastIndexOf('/') + 1; - auto tabName = path.mid(lastSlash); - if (m_editorMakers.contains(ext)) { - auto tab = m_editorMakers[ext].make(path); - m_tabs->addTab(tab, tabName); - m_undoGroup.addStack(tab->undoStack()); - // save new tab to application state - auto openTabs = readTabs(); - if (!openTabs.contains(path)) { - openTabs.append(path); - } - writeTabs(openTabs); - } -} - -void MainWindow::onExit() { - writeState(); -} - - -// private slots - -void MainWindow::openProject() { - auto projectPath = QFileDialog::getExistingDirectory(this, tr("Select Project Directory..."), QDir::homePath()); - if (projectPath != "") { - openProject(projectPath); - writeState(); - } -} - -void MainWindow::openFileSlot(QModelIndex file) { - auto path = static_cast(file.internalPointer())->path(); - return openFile(path); -} - -void MainWindow::showNewWizard() { - const QString ProjectName = "projectName"; - const QString ProjectPath = "projectPath"; - Wizard wizard(tr("New...")); - auto ws = new WizardSelect(); - wizard.addPage(ws); - - // add new project wizard - ws->addOption( - WizardMaker { - tr("Project"), - - [&wizard, ProjectName, ProjectPath]() { - QVector pgs; - auto pg = new WizardFormPage(); - pg->addLineEdit(tr("Project &Name:"), ProjectName + "*", "", [ProjectPath, pg, &wizard](QString projectName) { - auto projectPath = wizard.field(ProjectPath).toString(); - auto path = projectPath + "/" + projectName; - if (!QDir(path).exists()) { - return 0; - } else { - pg->showValidationError(tr("This project directory already exists.")); - return 1; - } - } - ); - pg->addPathBrowse(tr("Project &Path:"), ProjectPath + "*", QDir::homePath(), QFileDialog::Directory); - pgs.push_back(pg); - pgs.push_back(new WizardConclusionPage(tr("Creating project: %1/%2"), {ProjectPath, ProjectName})); - return pgs; - }, - - [this, ProjectName, ProjectPath](QWizard *wizard) { - auto projectName = wizard->field(ProjectName).toString(); - auto projectPath = wizard->field(ProjectPath).toString(); - qInfo() << "Project creation: final step"; - if (QDir(projectPath).exists()) { - auto path = projectPath + "/" + projectName; - if (!QDir(path).exists()) { - Project(path).create(); - openProject(path); - qInfo() << "Project creation successful:" << path; - return 0; - } else { - qInfo() << "Project file exists:" << path; - return 1; - } - } else { - qInfo() << "Project destination directory does not exist:" << projectPath; - return 2; - } - } - } - ); - - // add module options - for (auto p : m_modules) { - for (auto w : p->newWizards(&m_ctx)) { - ws->addOption(w); - } - } - - wizard.show(); - wizard.exec(); -} - -void MainWindow::saveFile() { - m_currentEditor->save(); -} - -void MainWindow::exportFile() { - m_currentEditor->exportFile(); -} - -void MainWindow::cutAction() { - m_currentEditor->cut(); -} - -void MainWindow::copyAction() { - m_currentEditor->copy(); -} - -void MainWindow::pasteAction() { - m_currentEditor->paste(); -} - -void MainWindow::closeTab(int idx) { - auto tab = dynamic_cast(m_tabs->widget(idx)); - m_undoGroup.removeStack(tab->undoStack()); - m_tabs->removeTab(idx); - delete tab; - - // remove from open tabs list - auto tabs = readTabs(); - tabs.removeAt(idx); - writeTabs(tabs); -} - -void MainWindow::moveTab(int from, int to) { - // move tab in open tabs list - auto tabs = readTabs(); - tabs.move(from, to); - writeTabs(tabs); -} - -void MainWindow::changeTab(int idx) { - auto tab = dynamic_cast(m_tabs->widget(idx)); - if (m_currentEditor) { - disconnect(m_currentEditor, &Editor::unsavedChangesChanged, m_saveAction, &QAction::setEnabled); - disconnect(m_currentEditor, &Editor::unsavedChangesChanged, this, &MainWindow::markUnsavedChanges); - disconnect(m_currentEditor, &Editor::exportableChanged, m_exportAction, &QAction::setEnabled); - disconnect(m_currentEditor, &Editor::cutEnabledChanged, m_cutAction, &QAction::setEnabled); - disconnect(m_currentEditor, &Editor::copyEnabledChanged, m_copyAction, &QAction::setEnabled); - disconnect(m_currentEditor, &Editor::pasteEnabledChanged, m_pasteAction, &QAction::setEnabled); - } - m_currentEditor = tab; - if (m_currentEditor) { - m_saveAction->setEnabled(m_currentEditor->unsavedChanges()); - connect(m_currentEditor, &Editor::unsavedChangesChanged, m_saveAction, &QAction::setEnabled); - connect(m_currentEditor, &Editor::unsavedChangesChanged, this, &MainWindow::markUnsavedChanges); - connect(m_currentEditor, &Editor::cutEnabledChanged, m_cutAction, &QAction::setEnabled); - connect(m_currentEditor, &Editor::copyEnabledChanged, m_copyAction, &QAction::setEnabled); - connect(m_currentEditor, &Editor::pasteEnabledChanged, m_pasteAction, &QAction::setEnabled); - m_exportAction->setEnabled(m_currentEditor->exportable()); - connect(m_currentEditor, &Editor::exportableChanged, m_exportAction, &QAction::setEnabled); - m_undoGroup.setActiveStack(tab->undoStack()); - } else { - m_undoGroup.setActiveStack(nullptr); - } -} - -void MainWindow::markUnsavedChanges(bool unsavedChanges) { - auto idx = m_tabs->indexOf(m_currentEditor); - if (idx > -1) { - const auto path = m_currentEditor->itemName(); - const auto lastSlash = path.lastIndexOf('/') + 1; - const auto tabName = path.mid(lastSlash); - if (unsavedChanges) { - m_tabs->setTabText(idx, tabName + "*"); - } else { - m_tabs->setTabText(idx, tabName); - } - } -} - -void MainWindow::showImportWizard() { - const QString BPP = "bpp"; - Wizard wizard(tr("Import...")); - auto ws = new WizardSelect(); - wizard.addPage(ws); - - for (auto p : m_modules) { - for (auto w : p->importWizards(&m_ctx)) { - ws->addOption(w); - } - } - - wizard.show(); - wizard.exec(); -} - -void MainWindow::refreshProjectExplorer(QString path) { - if (m_oxfsView) { - m_oxfsView->updateFile(path); - } -} - -} diff --git a/src/nostalgia/studio/mainwindow.hpp b/src/nostalgia/studio/mainwindow.hpp deleted file mode 100644 index eacbc09e..00000000 --- a/src/nostalgia/studio/mainwindow.hpp +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright 2016 - 2021 gary@drinkingtea.net - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -#pragma once - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "lib/context.hpp" -#include "lib/module.hpp" -#include "lib/project.hpp" - -#include "builtinmodules.hpp" -#include "oxfstreeview.hpp" - -namespace nostalgia::studio { - -struct NostalgiaStudioState { - QString projectPath; -}; - -template -ox::Error model(T *io, NostalgiaStudioState *obj) { - oxReturnError(io->setTypeInfo("net.drinkingtea.nostalgia.studio.NostalgiaStudioState", 1)); - oxReturnError(io->field("project_path", &obj->projectPath)); - return OxError(0); -} - -struct NostalgiaStudioProfile { - QString appName; - QString orgName; - QVector modulesPath; -}; - -template -ox::Error model(T *io, NostalgiaStudioProfile *obj) { - oxReturnError(io->setTypeInfo("net.drinkingtea.nostalgia.studio.NostalgiaStudioProfile", 3)); - oxReturnError(io->field("app_name", &obj->appName)); - oxReturnError(io->field("org_name", &obj->orgName)); - oxReturnError(io->field("modules_path", &obj->modulesPath)); - return OxError(0); -} - - -class MainWindow: public QMainWindow { - Q_OBJECT - - private: - QString m_profilePath; - NostalgiaStudioProfile m_profile; - NostalgiaStudioState m_state; - QAction *m_importAction = nullptr; - QAction *m_saveAction = nullptr; - QAction *m_exportAction = nullptr; - QAction *m_cutAction = nullptr; - QAction *m_copyAction = nullptr; - QAction *m_pasteAction = nullptr; - Context m_ctx; - QPointer m_viewMenu; - QVector> m_dockWidgets; - QTreeView *m_projectExplorer = nullptr; - QVector m_modules; - QHash m_editorMakers; - QPointer m_oxfsView = nullptr; - QTabWidget *m_tabs = nullptr; - QUndoGroup m_undoGroup; - Editor *m_currentEditor = nullptr; - - public: - explicit MainWindow(QString profilePath); - - ~MainWindow() override; - - private: - void loadModules(); - - void loadModuleDir(QString path); - - void loadModule(QString path); - - void loadModule(Module *module); - - void setupMenu(); - - void setupProjectExplorer(); - - void addDockWidget(Qt::DockWidgetArea area, QDockWidget *dockwidget); - - static QAction *addAction(QMenu *menu, QString text, QString toolTip, const QObject *tgt, const char *cb); - - static QAction *addAction(QMenu *menu, QString text, QString toolTip, - QKeySequence::StandardKey key, const QObject *tgt, const char *cb); - - static QAction *addAction(QMenu *menu, QString text, QString toolTip, - QKeySequence::StandardKey key, void (*cb)()); - - int readState(); - - void writeState(); - - /** - * Read open editor tabs for current project. - */ - [[nodiscard]] QStringList readTabs() const; - - /** - * Write open editor tabs for current project. - */ - void writeTabs(QStringList tabs) const; - - void openProject(QString); - - void closeProject(); - - /** - * @param force forces the reopening of the file even if it is already open - */ - void openFile(QString path, bool force = false); - - public slots: - void onExit(); - - private slots: - void openProject(); - - void openFileSlot(QModelIndex); - - void saveFile(); - - void exportFile(); - - void cutAction(); - - void copyAction(); - - void pasteAction(); - - void closeTab(int idx); - - void moveTab(int from, int to); - - void changeTab(int idx); - - void markUnsavedChanges(bool); - - void showNewWizard(); - - void showImportWizard(); - - void refreshProjectExplorer(QString path); - -}; - -} diff --git a/src/nostalgia/studio/oxfstreeview.cpp b/src/nostalgia/studio/oxfstreeview.cpp deleted file mode 100644 index 55b92b7a..00000000 --- a/src/nostalgia/studio/oxfstreeview.cpp +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Copyright 2016 - 2021 gary@drinkingtea.net - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -#include - -#include -#include -#include - -#include "oxfstreeview.hpp" - -namespace nostalgia::studio { - -OxFSFile::OxFSFile(ox::FileSystem *fs, const QString &path, OxFSFile *parentItem) { - m_path = path; - m_parentItem = parentItem; - if (!fs) { - return; - } - // find children - oxRequireT(stat, fs->stat(static_cast(m_path.toUtf8()))); - QVector ls; - if (stat.fileType == ox::FileType::Directory) { - oxRequireT(names, fs->ls(m_path.toUtf8())); - for (const auto &name : names) { - if (name[0] != '.') { - ls.push_back(name.c_str()); - } - } - std::sort(ls.begin(), ls.end()); - } - auto p = m_path; - // make sure ends with path separator - if (fs->stat(p.toUtf8().data()).value.fileType == ox::FileType::Directory && - p.size() && p.back() != QDir::separator()) { - p += QDir::separator(); - } - for (const auto &name : ls) { - if (name != "." && name != "..") { - const auto filePath = m_path.size() ? m_path + '/' + name : name; - const auto ch = new OxFSFile(fs, filePath, this); - m_childItems.push_back(ch); - } - } -} - -OxFSFile::~OxFSFile() { - qDeleteAll(m_childItems); -} - -void OxFSFile::appendChild(OxFSModel *model, QStringList pathItems, QString currentPath) { - if (!pathItems.empty()) { - const auto &target = pathItems[0]; - currentPath += "/" + target; - int index = static_cast(m_childItems.size()); - for (int i = 0; i < m_childItems.size(); i++) { - if (m_childItems[i]->name() >= target) { - index = i; - break; - } - } - - if (m_childItems.size() == index || m_childItems[index]->name() != target) { - auto idx = model->createIndex(row(), 0, this); - model->beginInsertRows(idx, index, index); - m_childItems.insert(index, new OxFSFile(nullptr, currentPath, this)); - model->endInsertRows(); - } - - m_childItems[index]->appendChild(model, pathItems.mid(1), currentPath); - } -} - -OxFSFile *OxFSFile::child(int row) { - if (row < m_childItems.size()) { - return m_childItems[row]; - } else { - return nullptr; - } -} - -int OxFSFile::childCount() const { - return static_cast(m_childItems.size()); -} - -int OxFSFile::columnCount() const { - return 1; -} - -QVariant OxFSFile::data(int) const { - return name(); -} - -int OxFSFile::row() const { - if (m_parentItem) { - return static_cast(m_parentItem->m_childItems.indexOf(const_cast(this))); - } else { - return 0; - } -} - -OxFSFile *OxFSFile::parentItem() { - return m_parentItem; -} - -QString OxFSFile::name() const { - return m_path.mid(m_path.lastIndexOf("/") + 1); -} - -QString OxFSFile::path() const { - return "/" + m_path; -} - - -// OxFSModel - -OxFSModel::OxFSModel(ox::FileSystem *fs, QObject*) { - m_rootItem = new OxFSFile(fs, ""); -} - -OxFSModel::~OxFSModel() { - if (m_rootItem) { - delete m_rootItem; - m_rootItem = nullptr; - } -} - -QVariant OxFSModel::data(const QModelIndex &index, int role) const { - if (!index.isValid() || role != Qt::DisplayRole) { - return QVariant(); - } else { - auto item = static_cast(index.internalPointer()); - return item->data(index.column()); - } -} - -Qt::ItemFlags OxFSModel::flags(const QModelIndex &index) const { - if (!index.isValid()) { - return {}; - } else { - return QAbstractItemModel::flags(index); - } -} - -QVariant OxFSModel::headerData(int section, Qt::Orientation orientation, int role) const { - if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { - return m_rootItem->data(section); - } else { - return QVariant(); - } -} - -QModelIndex OxFSModel::index(int row, int column, const QModelIndex &parent) const { - if (!hasIndex(row, column, parent)) { - return QModelIndex(); - } - - OxFSFile *parentItem; - if (!parent.isValid()) { - parentItem = m_rootItem; - } else { - parentItem = static_cast(parent.internalPointer()); - } - - OxFSFile *childItem = parentItem->child(row); - if (childItem) { - return createIndex(row, column, childItem); - } else { - return QModelIndex(); - } -} - -QModelIndex OxFSModel::parent(const QModelIndex &index) const { - if (!index.isValid()) { - return QModelIndex(); - } - - auto childItem = static_cast(index.internalPointer()); - auto parentItem = childItem->parentItem(); - if (parentItem == m_rootItem) { - return QModelIndex(); - } - - return createIndex(parentItem->row(), 0, parentItem); -} - -int OxFSModel::rowCount(const QModelIndex &parent) const { - if (parent.column() > 0) { - return 0; - } - - OxFSFile *parentItem; - if (!parent.isValid()) { - parentItem = m_rootItem; - } else { - parentItem = static_cast(parent.internalPointer()); - } - - return parentItem->childCount(); -} - -int OxFSModel::columnCount(const QModelIndex &parent) const { - if (parent.isValid()) { - return static_cast(parent.internalPointer())->columnCount(); - } else { - return m_rootItem->columnCount(); - } -} - -void OxFSModel::updateFile(const QString &path) { - auto pathItems = path.split("/").mid(1); - m_rootItem->appendChild(this, pathItems, ""); -} - -} diff --git a/src/nostalgia/studio/oxfstreeview.hpp b/src/nostalgia/studio/oxfstreeview.hpp deleted file mode 100644 index a7c238bd..00000000 --- a/src/nostalgia/studio/oxfstreeview.hpp +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2016 - 2021 gary@drinkingtea.net - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -#pragma once - -#include -#include -#include - -#include - -namespace nostalgia::studio { - -class OxFSFile { - private: - OxFSFile *m_parentItem = nullptr; - QString m_path; - QVector m_childItems; - - public: - OxFSFile(ox::FileSystem *fs, const QString &path, OxFSFile *parentItem = nullptr); - - ~OxFSFile(); - - void appendChild(class OxFSModel *model, QStringList pathItems, QString fullPath); - - OxFSFile *child(int row); - - [[nodiscard]] - int childCount() const; - - [[nodiscard]] - int columnCount() const; - - [[nodiscard]] - QVariant data(int column) const; - - [[nodiscard]] - int row() const; - - OxFSFile *parentItem(); - - [[nodiscard]] - QString name() const; - - [[nodiscard]] - QString path() const; -}; - -class OxFSModel: public QAbstractItemModel { - Q_OBJECT - - friend OxFSFile; - - private: - OxFSFile *m_rootItem = nullptr; - - public: - explicit OxFSModel(ox::FileSystem *fs, QObject *parent = nullptr); - - ~OxFSModel() override; - - [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override; - - [[nodiscard]] Qt::ItemFlags flags(const QModelIndex &index) const override; - - [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role) const override; - - [[nodiscard]] QModelIndex index(int row, int column, - const QModelIndex &parent) const override; - - [[nodiscard]] QModelIndex parent(const QModelIndex &index) const override; - - [[nodiscard]] int rowCount(const QModelIndex &parent) const override; - - [[nodiscard]] int columnCount(const QModelIndex &parent) const override; - - public slots: - void updateFile(const QString &path); - -}; - -} diff --git a/src/nostalgia/studio/projectexplorer.cpp b/src/nostalgia/studio/projectexplorer.cpp new file mode 100644 index 00000000..bec07759 --- /dev/null +++ b/src/nostalgia/studio/projectexplorer.cpp @@ -0,0 +1,41 @@ +/* + * Copyright 2016 - 2021 gary@drinkingtea.net + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include + +#include + +#include "projectexplorer.hpp" + +namespace nostalgia { + +void ProjectExplorer::draw(core::Context *ctx) noexcept { + const auto viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x, viewport->Pos.y + 71)); + ImGui::SetNextWindowSize(ImVec2(313, viewport->Size.y - 71)); + const auto flags = ImGuiWindowFlags_NoTitleBar + | ImGuiWindowFlags_NoResize + | ImGuiWindowFlags_NoMove + | ImGuiWindowFlags_NoScrollbar + | ImGuiWindowFlags_NoSavedSettings; + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0); + ImGui::Begin("ProjectExplorer", nullptr, flags); + ImGui::PopStyleVar(); + ImGui::SetNextTreeNodeOpen(true); + if (m_treeModel) { + m_treeModel->draw(ctx); + } + ImGui::End(); +} + +void ProjectExplorer::setModel(ox::UniquePtr model) noexcept { + m_treeModel = std::move(model); +} + +} + diff --git a/src/nostalgia/studio/projectexplorer.hpp b/src/nostalgia/studio/projectexplorer.hpp new file mode 100644 index 00000000..a117c855 --- /dev/null +++ b/src/nostalgia/studio/projectexplorer.hpp @@ -0,0 +1,27 @@ +/* + * Copyright 2016 - 2021 gary@drinkingtea.net + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#pragma once + +#include + +#include "lib/widget.hpp" +#include "projecttreemodel.hpp" + +namespace nostalgia { + +class ProjectExplorer: public studio::Widget { + private: + ox::UniquePtr m_treeModel; + public: + void draw(core::Context *ctx) noexcept override; + + void setModel(ox::UniquePtr model) noexcept; +}; + +} \ No newline at end of file diff --git a/src/nostalgia/studio/projecttreemodel.cpp b/src/nostalgia/studio/projecttreemodel.cpp new file mode 100644 index 00000000..48192172 --- /dev/null +++ b/src/nostalgia/studio/projecttreemodel.cpp @@ -0,0 +1,42 @@ +/* + * Copyright 2016 - 2021 gary@drinkingtea.net + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include + +#include "projecttreemodel.hpp" + +namespace nostalgia { + +ProjectTreeModel::ProjectTreeModel(const ox::String &name, + ox::Vector> children) noexcept { + m_name = name; + m_children = std::move(children); +} + +ProjectTreeModel::ProjectTreeModel(ProjectTreeModel &&other) noexcept { + m_name = std::move(other.m_name); + m_children = std::move(other.m_children); +} + +void ProjectTreeModel::draw(core::Context *ctx) noexcept { + constexpr auto dirFlags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick; + if (m_children.size()) { + if (ImGui::TreeNodeEx(m_name.c_str(), dirFlags)) { + for (auto &child : m_children) { + child->draw(ctx); + } + ImGui::TreePop(); + } + } else { + if (ImGui::TreeNodeEx(m_name.c_str(), ImGuiTreeNodeFlags_Leaf)) { + ImGui::TreePop(); + } + } +} + +} \ No newline at end of file diff --git a/src/nostalgia/studio/projecttreemodel.hpp b/src/nostalgia/studio/projecttreemodel.hpp new file mode 100644 index 00000000..848c9752 --- /dev/null +++ b/src/nostalgia/studio/projecttreemodel.hpp @@ -0,0 +1,31 @@ +/* + * Copyright 2016 - 2021 gary@drinkingtea.net + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#pragma once + +#include +#include + +#include + +namespace nostalgia { + +class ProjectTreeModel { + private: + ox::String m_name; + ox::Vector > m_children; + public: + explicit ProjectTreeModel(const ox::String &name, + ox::Vector > children) noexcept; + + ProjectTreeModel(ProjectTreeModel &&other) noexcept; + + void draw(core::Context *ctx) noexcept; +}; + +} \ No newline at end of file diff --git a/src/nostalgia/studio/studio.hpp b/src/nostalgia/studio/studio.hpp index 07a983db..351c4e8e 100644 --- a/src/nostalgia/studio/studio.hpp +++ b/src/nostalgia/studio/studio.hpp @@ -8,8 +8,8 @@ #pragma once -#include "lib/context.hpp" #include "lib/editor.hpp" -#include "lib/module.hpp" +#include "lib/filedialog.hpp" #include "lib/project.hpp" -#include "lib/wizard.hpp" +#include "lib/task.hpp" +#include "lib/undostack.hpp" diff --git a/src/nostalgia/studio/studioapp.cpp b/src/nostalgia/studio/studioapp.cpp new file mode 100644 index 00000000..a62420d1 --- /dev/null +++ b/src/nostalgia/studio/studioapp.cpp @@ -0,0 +1,144 @@ +/* + * Copyright 2016 - 2021 gary@drinkingtea.net + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include + +#include + +#include "lib/configio.hpp" +#include "filedialogmanager.hpp" +#include "studioapp.hpp" + +namespace nostalgia { + +struct StudioConfig { + static constexpr auto TypeName = "net.drinkingtea.nostalgia.studio.StudioConfig"; + static constexpr auto TypeVersion = 1; + ox::String projectPath; +}; + +template +constexpr ox::Error model(T *h, StudioConfig *config) noexcept { + oxReturnError(h->field("project_path", &config->projectPath)); + return OxError(0); +} + +StudioUI::StudioUI() noexcept { + m_widgets.push_back(&m_projectExplorer); + if (const auto [config, err] = studio::readConfig(); !err) { + oxIgnoreError(openProject(config.projectPath)); + } else { + oxErrf("Could not open studio config file: {}\n", err.msg); + } +} + +static ox::Result> +buildProjectTreeModel(const ox::String &name, const ox::String &path, ox::FileSystem *fs) noexcept { + ox::Vector> outChildren; + oxRequire(stat, fs->stat(path.c_str())); + if (stat.fileType == ox::FileType::Directory) { + oxRequireM(children, fs->ls(path)); + //std::sort(children.begin(), children.end()); + for (const auto &childName : children) { + if (childName[0] != '.') { + const auto childPath = ox::sfmt("{}/{}", path, childName); + oxRequireM(child, buildProjectTreeModel(childName, childPath, fs)); + outChildren.emplace_back(std::move(child)); + } + } + } + return ox::make_unique(name, std::move(outChildren)); +} + +void StudioUI::update(core::Context *ctx) noexcept { + m_taskRunner.update(ctx); +} + +void StudioUI::draw(core::Context *ctx) noexcept { + drawMenu(ctx); + drawToolbar(ctx); + for (auto &w : m_widgets) { + w->draw(ctx); + } + if (m_aboutEnabled && + ImGui::Begin("About", &m_aboutEnabled, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) { + ImGui::End(); + } +} + +void StudioUI::drawMenu(core::Context *ctx) noexcept { + if (ImGui::BeginMainMenuBar()) { + if (ImGui::BeginMenu("File")) { + if (ImGui::MenuItem("New...", "Ctrl+N")) { + } + if (ImGui::MenuItem("Open Project...", "Ctrl+O")) { + m_taskRunner.add(new FileDialogManager(this, &StudioUI::openProject)); + } + if (ImGui::MenuItem("Save", "Ctrl+S", false, m_saveEnabled)) { + } + if (ImGui::MenuItem("Quit", "Ctrl+Q")) { + core::shutdown(ctx); + } + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Module", false)) { + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Help")) { + if (ImGui::MenuItem("About")) { + m_aboutEnabled = true; + } + ImGui::EndMenu(); + } + ImGui::EndMainMenuBar(); + } +} + +void StudioUI::drawToolbar(core::Context*) noexcept { + constexpr auto BtnWidth = 96; + constexpr auto BtnHeight = 32; + const auto viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x, viewport->Pos.y + 20)); + ImGui::SetNextWindowSize(ImVec2(viewport->Size.x, 48)); + const auto flags = ImGuiWindowFlags_NoTitleBar + | ImGuiWindowFlags_NoResize + | ImGuiWindowFlags_NoMove + | ImGuiWindowFlags_NoScrollbar + | ImGuiWindowFlags_NoSavedSettings; + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0); + ImGui::Begin("MainToolbar", nullptr, flags); + ImGui::PopStyleVar(); + { + ImGui::Button("New##MainToolbar", ImVec2(BtnWidth, BtnHeight)); + ImGui::SameLine(); + if (ImGui::Button("Open Project##MainToolbar", ImVec2(BtnWidth, BtnHeight))) { + m_taskRunner.add(new FileDialogManager(this, &StudioUI::openProject)); + } + ImGui::SameLine(); + ImGui::Button("Save##MainToolbar", ImVec2(BtnWidth, BtnHeight)); + } + ImGui::End(); +} + +ox::Error StudioUI::openProject(const ox::String &path) noexcept { + m_project = ox::make_unique(path); + m_project->fileAdded.connect(this, &StudioUI::refreshProjectTreeModel); + m_project->fileDeleted.connect(this, &StudioUI::refreshProjectTreeModel); + oxReturnError(studio::editConfig([path](StudioConfig *config) { + config->projectPath = path; + })); + return refreshProjectTreeModel(); +} + +ox::Error StudioUI::refreshProjectTreeModel(const ox::String&) noexcept { + oxRequireM(model, buildProjectTreeModel("Project", "/", m_project->romFs())); + m_projectExplorer.setModel(std::move(model)); + return OxError(0); +} + +} diff --git a/src/nostalgia/studio/studioapp.hpp b/src/nostalgia/studio/studioapp.hpp new file mode 100644 index 00000000..cec6d9cb --- /dev/null +++ b/src/nostalgia/studio/studioapp.hpp @@ -0,0 +1,52 @@ +/* + * Copyright 2016 - 2021 gary@drinkingtea.net + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#pragma once + +#include +#include +#include + +#include "lib/project.hpp" +#include "lib/task.hpp" +#include "projectexplorer.hpp" +#include "projecttreemodel.hpp" + +namespace nostalgia { + +class StudioUI: public ox::SignalHandler { + friend class StudioUIDrawer; + + private: + ox::UniquePtr m_project; + studio::TaskRunner m_taskRunner; + ox::Vector m_widgets; + ox::UniquePtr m_treeModel; + ProjectExplorer m_projectExplorer; + bool m_saveEnabled = false; + bool m_aboutEnabled = false; + + public: + StudioUI() noexcept; + + void update(core::Context *ctx) noexcept; + + protected: + void draw(core::Context *ctx) noexcept; + + private: + void drawMenu(core::Context *ctx) noexcept; + + void drawToolbar(core::Context *ctx) noexcept; + + ox::Error openProject(const ox::String &path) noexcept; + + ox::Error refreshProjectTreeModel(const ox::String& = {}) noexcept; +}; + +} \ No newline at end of file diff --git a/src/nostalgia/studio/studiorsrc.qrc b/src/nostalgia/studio/studiorsrc.qrc deleted file mode 100644 index c3f571e7..00000000 --- a/src/nostalgia/studio/studiorsrc.qrc +++ /dev/null @@ -1,6 +0,0 @@ - - - nostalgia-studio.json - nostalgia-studio-dev.json - - diff --git a/src/nostalgia/world/CMakeLists.txt b/src/nostalgia/world/CMakeLists.txt index fe3ba1eb..0570d193 100644 --- a/src/nostalgia/world/CMakeLists.txt +++ b/src/nostalgia/world/CMakeLists.txt @@ -19,5 +19,5 @@ install( ) if(NOSTALGIA_BUILD_STUDIO) - add_subdirectory(studio) + #add_subdirectory(studio) endif()