From ad743565b2778381813446e7f7008f27ea42c322 Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Sun, 31 Oct 2021 13:31:12 -0500 Subject: [PATCH] [nostalgia] Update Studio to handle tabs and open directory dialog on Mac, Update core::init --- src/nostalgia/core/color.hpp | 16 ++ src/nostalgia/core/context.hpp | 10 +- src/nostalgia/core/core.hpp | 3 +- src/nostalgia/core/gba/core.cpp | 5 +- src/nostalgia/core/gba/gfx.cpp | 3 +- src/nostalgia/player/app.cpp | 4 +- src/nostalgia/player/app.hpp | 4 +- src/nostalgia/player/main.cpp | 4 +- src/nostalgia/studio/CMakeLists.txt | 1 + src/nostalgia/studio/builtinmodules.hpp | 24 +++ src/nostalgia/studio/lib/CMakeLists.txt | 13 +- src/nostalgia/studio/lib/configio.hpp | 54 +++-- src/nostalgia/studio/lib/editor.cpp | 12 +- src/nostalgia/studio/lib/editor.hpp | 9 +- .../lib/{filedialog.mm => filedialog_mac.mm} | 0 src/nostalgia/studio/lib/module.cpp | 11 +- src/nostalgia/studio/lib/module.hpp | 26 +-- src/nostalgia/studio/lib/project.cpp | 4 +- src/nostalgia/studio/lib/project.hpp | 4 +- src/nostalgia/studio/lib/task.cpp | 12 +- src/nostalgia/studio/main.cpp | 27 ++- src/nostalgia/studio/projectexplorer.cpp | 43 +++- src/nostalgia/studio/projectexplorer.hpp | 14 ++ src/nostalgia/studio/projecttreemodel.cpp | 26 ++- src/nostalgia/studio/projecttreemodel.hpp | 14 +- src/nostalgia/studio/studio.hpp | 2 + src/nostalgia/studio/studioapp.cpp | 194 +++++++++++++----- src/nostalgia/studio/studioapp.hpp | 32 ++- 28 files changed, 416 insertions(+), 155 deletions(-) create mode 100644 src/nostalgia/studio/builtinmodules.hpp rename src/nostalgia/studio/lib/{filedialog.mm => filedialog_mac.mm} (100%) diff --git a/src/nostalgia/core/color.hpp b/src/nostalgia/core/color.hpp index 12f636d3..ea23324d 100644 --- a/src/nostalgia/core/color.hpp +++ b/src/nostalgia/core/color.hpp @@ -107,6 +107,22 @@ constexpr float bluef(Color16 c) noexcept { } +[[nodiscard]] +constexpr float redf(Color32 c) noexcept { + return static_cast(red32(c)) / 255.f; +} + +[[nodiscard]] +constexpr float greenf(Color32 c) noexcept { + return static_cast(green32(c)) / 255.f; +} + +[[nodiscard]] +constexpr float bluef(Color32 c) noexcept { + return static_cast(blue32(c)) / 255.f; +} + + [[nodiscard]] constexpr Color16 color16(uint8_t r, uint8_t g, uint8_t b) noexcept { return r | (g << 5) | (b << 10); diff --git a/src/nostalgia/core/context.hpp b/src/nostalgia/core/context.hpp index 366cf7f5..01cb2543 100644 --- a/src/nostalgia/core/context.hpp +++ b/src/nostalgia/core/context.hpp @@ -10,6 +10,8 @@ #include +#include "assetmanager.hpp" + namespace nostalgia::core { class Context; @@ -23,15 +25,19 @@ class Drawer { // User Input Output class Context { public: - ox::FileSystem *rom = nullptr; + ox::UniquePtr rom; ox::Vector drawers; + const char *appName = "Nostalgia"; +#ifndef OX_BARE_METAL + AssetManager assetManager; +#endif private: void *m_customData = nullptr; void *m_windowerData = nullptr; void *m_rendererData = nullptr; public: - constexpr Context() noexcept = default; + Context() noexcept = default; Context(Context &other) noexcept = delete; Context(const Context &other) noexcept = delete; diff --git a/src/nostalgia/core/core.hpp b/src/nostalgia/core/core.hpp index 41902de0..a5064d94 100644 --- a/src/nostalgia/core/core.hpp +++ b/src/nostalgia/core/core.hpp @@ -10,6 +10,7 @@ #include +#include "assetmanager.hpp" #include "clipboard.hpp" #include "consts.hpp" #include "gfx.hpp" @@ -20,7 +21,7 @@ namespace nostalgia::core { using event_handler = int(*)(Context*); -ox::Result> init(ox::FileSystem *fs) noexcept; +ox::Result> init(ox::UniquePtr fs, const char *appName = "Nostalgia") noexcept; ox::Error run(Context *ctx) noexcept; diff --git a/src/nostalgia/core/gba/core.cpp b/src/nostalgia/core/gba/core.cpp index 17e16891..dd1678d1 100644 --- a/src/nostalgia/core/gba/core.cpp +++ b/src/nostalgia/core/gba/core.cpp @@ -40,9 +40,10 @@ static void initTimer() noexcept { REG_IE = REG_IE | Int_timer0; } -ox::Result> init(ox::FileSystem *fs) noexcept { +ox::Result> init(ox::UniquePtr fs, const char *appName) noexcept { auto ctx = ox::make_unique(); - ctx->rom = fs; + ctx->rom = std::move(fs); + ctx->appName = std::move(appName); oxReturnError(initGfx(ctx.get())); initTimer(); initIrq(); diff --git a/src/nostalgia/core/gba/gfx.cpp b/src/nostalgia/core/gba/gfx.cpp index 76d5ca0d..ff2f5e9e 100644 --- a/src/nostalgia/core/gba/gfx.cpp +++ b/src/nostalgia/core/gba/gfx.cpp @@ -162,7 +162,8 @@ ox::Error initConsole(Context *ctx) noexcept { ctx = new (ox_alloca(sizeof(Context))) Context(); oxRequire(rom, loadRom()); ox::FileStore32 fs(rom, 32 * ox::units::MB); - ctx->rom = new (ox_alloca(sizeof(ox::FileSystem32))) ox::FileSystem32(fs); + auto romFs = new (ox_alloca(sizeof(ox::FileSystem32))) ox::FileSystem32(fs); + new (&ctx->rom) ox::UniquePtr(romFs); } return loadBgTileSheet(ctx, 0, TilesheetAddr, PaletteAddr); } diff --git a/src/nostalgia/player/app.cpp b/src/nostalgia/player/app.cpp index ab674729..ae76bf32 100644 --- a/src/nostalgia/player/app.cpp +++ b/src/nostalgia/player/app.cpp @@ -32,8 +32,8 @@ static int eventHandler(core::Context *ctx) noexcept { return 16; } -ox::Error run(ox::FileSystem *fs) noexcept { - oxRequireM(ctx, core::init(fs)); +ox::Error run(ox::UniquePtr fs) noexcept { + oxRequireM(ctx, core::init(std::move(fs))); constexpr auto TileSheetAddr = "/TileSheets/Charset.ng"; constexpr auto PaletteAddr = "/Palettes/Charset.npal"; oxReturnError(core::loadSpriteTileSheet(ctx.get(), 0, TileSheetAddr, PaletteAddr)); diff --git a/src/nostalgia/player/app.hpp b/src/nostalgia/player/app.hpp index b06bfd00..737f3e40 100644 --- a/src/nostalgia/player/app.hpp +++ b/src/nostalgia/player/app.hpp @@ -6,4 +6,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -class ox::Error run(class ox::FileSystem *fs) noexcept; +#include + +class ox::Error run(ox::UniquePtr fs) noexcept; diff --git a/src/nostalgia/player/main.cpp b/src/nostalgia/player/main.cpp index be2a7160..e3a55560 100644 --- a/src/nostalgia/player/main.cpp +++ b/src/nostalgia/player/main.cpp @@ -17,8 +17,8 @@ static ox::Error run(int argc, const char **argv) noexcept { return OxError(1); } const auto path = argv[1]; - oxRequire(fs, nostalgia::core::loadRomFs(path)); - return run(fs.get()); + oxRequireM(fs, nostalgia::core::loadRomFs(path)); + return run(std::move(fs)); } int main(int argc, const char **argv) { diff --git a/src/nostalgia/studio/CMakeLists.txt b/src/nostalgia/studio/CMakeLists.txt index 83cfd526..0a8e9bcc 100644 --- a/src/nostalgia/studio/CMakeLists.txt +++ b/src/nostalgia/studio/CMakeLists.txt @@ -13,6 +13,7 @@ target_link_libraries( nostalgia-studio OxClArgs OxFS + NostalgiaCore-Studio NostalgiaCore-Userspace NostalgiaStudio NostalgiaPack diff --git a/src/nostalgia/studio/builtinmodules.hpp b/src/nostalgia/studio/builtinmodules.hpp new file mode 100644 index 00000000..c5419ce7 --- /dev/null +++ b/src/nostalgia/studio/builtinmodules.hpp @@ -0,0 +1,24 @@ +/* + * 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 { + +//[[maybe_unused]] // GCC warns about the existence of this "unused" constexpr list in a header file... +//constexpr auto BuiltinModules = { +// [] { +// return ox::make_unique(); +// }, +//}; + +} \ No newline at end of file diff --git a/src/nostalgia/studio/lib/CMakeLists.txt b/src/nostalgia/studio/lib/CMakeLists.txt index 03b526be..cb889eeb 100644 --- a/src/nostalgia/studio/lib/CMakeLists.txt +++ b/src/nostalgia/studio/lib/CMakeLists.txt @@ -11,15 +11,20 @@ add_library( NostalgiaStudio configio.cpp editor.cpp + module.cpp project.cpp task.cpp undostack.cpp widget.cpp window.cpp filedialog_gtk.cpp - $<$:filedialog.mm> + $<$:filedialog_mac.mm> ) +if(NOT MSVC) + target_compile_options(NostalgiaStudio PUBLIC -Wsign-conversion) +endif() + install(TARGETS NostalgiaStudio LIBRARY DESTINATION ${NOSTALGIA_DIST_LIB}/nostalgia) @@ -46,11 +51,15 @@ target_link_libraries( install( FILES + configio.hpp editor.hpp - filedialog.hpp + filedialog.hpp + module.hpp project.hpp task.hpp undostack.hpp + widget.hpp + window.hpp ${CMAKE_CURRENT_BINARY_DIR}/nostalgiastudio_export.h DESTINATION include/nostalgia/studio/lib diff --git a/src/nostalgia/studio/lib/configio.hpp b/src/nostalgia/studio/lib/configio.hpp index 9e60b753..ff4253d1 100644 --- a/src/nostalgia/studio/lib/configio.hpp +++ b/src/nostalgia/studio/lib/configio.hpp @@ -19,18 +19,20 @@ #include #include +#include + namespace nostalgia::studio { constexpr auto ConfigDir = [] { switch (ox::defines::OS) { case ox::defines::OS::Darwin: - return "{}/Library/Preferences/NostalgiaStudio"; + return "{}/Library/Preferences/{}"; 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"; + return "{}/.config/{}"; case ox::defines::OS::BareMetal: case ox::defines::OS::Windows: return ""; @@ -38,13 +40,14 @@ constexpr auto ConfigDir = [] { }(); template -ox::Result readConfig(const ox::String &name = ox::getModelTypeName()) noexcept { +ox::Result readConfig(core::Context *ctx, const ox::String &name = ox::getModelTypeName()) noexcept { + oxAssert(name != "", "Config type has no TypeName"); const auto homeDir = std::getenv("HOME"); - const auto configPath = ox::sfmt(ConfigDir, homeDir).toStdString(); + const auto configPath = ox::sfmt(ConfigDir, homeDir, ctx->appName).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); + oxErrf("Could not find config file: {}\n", path); return OxError(1, "Could not find config file"); } try { @@ -54,15 +57,16 @@ ox::Result readConfig(const ox::String &name = ox::getModelTypeName()) noe file.read(buff.data(), size); return ox::readOC(buff); } catch (const std::ios_base::failure &e) { - oxErrorf("Could not read config file: {}", e.what()); + oxErrf("Could not read config file: {}\n", e.what()); return OxError(2, "Could not read config file"); } } template -ox::Error writeConfig(const ox::String &name, T *data) noexcept { +ox::Error writeConfig(core::Context *ctx, const ox::String &name, T *data) noexcept { + oxAssert(name != "", "Config type has no TypeName"); const auto homeDir = std::getenv("HOME"); - const auto configPath = ox::sfmt(ConfigDir, homeDir).toStdString(); + const auto configPath = ox::sfmt(ConfigDir, homeDir, ctx->appName).toStdString(); const auto path = ox::sfmt("{}/{}.json", configPath, name).toStdString(); std::error_code ec; std::filesystem::create_directory(configPath, ec); @@ -71,7 +75,7 @@ ox::Error writeConfig(const ox::String &name, T *data) noexcept { } std::ofstream file(path, std::ios::binary | std::ios::ate); if (!file.good()) { - oxErrorf("Could not find config file: {}", path); + oxErrf("Could not find config file: {}\n", path); return OxError(1, "Could not find config file"); } oxRequireM(buff, ox::writeOC(data)); @@ -80,26 +84,42 @@ ox::Error writeConfig(const ox::String &name, T *data) noexcept { 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()); + oxErrf("Could not read config file: {}\n", e.what()); return OxError(2, "Could not read config file"); } } template -ox::Error writeConfig(T *data) noexcept { - return writeConfig(ox::getModelTypeName(), data); +ox::Error writeConfig(core::Context *ctx, T *data) noexcept { + const auto TypeName = ox::getModelTypeName(); + return writeConfig(ctx, TypeName, data); } template -ox::Error editConfig(const ox::String &name, Func f) noexcept { - auto c = readConfig(name); +void openConfig(core::Context *ctx, const ox::String &name, Func f) noexcept { + oxAssert(name != "", "Config type has no TypeName"); + const auto c = readConfig(ctx, name); f(&c.value); - return writeConfig(name, &c.value); } template -ox::Error editConfig(Func f) noexcept { - return editConfig(ox::getModelTypeName(), f); +void openConfig(core::Context *ctx, Func f) noexcept { + const auto TypeName = ox::getModelTypeName(); + openConfig(ctx, TypeName, f); +} + +template +void editConfig(core::Context *ctx, const ox::String &name, Func f) noexcept { + oxAssert(name != "", "Config type has no TypeName"); + auto c = readConfig(ctx, name); + f(&c.value); + oxLogError(writeConfig(ctx, name, &c.value)); +} + +template +void editConfig(core::Context *ctx, Func f) noexcept { + const auto TypeName = ox::getModelTypeName(); + editConfig(ctx, TypeName, f); } } diff --git a/src/nostalgia/studio/lib/editor.cpp b/src/nostalgia/studio/lib/editor.cpp index 5cc7603d..558be286 100644 --- a/src/nostalgia/studio/lib/editor.cpp +++ b/src/nostalgia/studio/lib/editor.cpp @@ -12,6 +12,10 @@ namespace nostalgia::studio { +ox::String Editor::itemDisplayName() const { + return itemName(); +} + void Editor::cut() { } @@ -24,9 +28,13 @@ void Editor::paste() { void Editor::exportFile() { } +void Editor::close() { + this->closed.emit(itemName()); +} + void Editor::save() { - saveItem(); - setUnsavedChanges(false); + saveItem(); + setUnsavedChanges(false); } void Editor::setUnsavedChanges(bool uc) { diff --git a/src/nostalgia/studio/lib/editor.hpp b/src/nostalgia/studio/lib/editor.hpp index 79253461..42897acc 100644 --- a/src/nostalgia/studio/lib/editor.hpp +++ b/src/nostalgia/studio/lib/editor.hpp @@ -11,12 +11,13 @@ #include #include "undostack.hpp" +#include "widget.hpp" #include "nostalgiastudio_export.h" namespace nostalgia::studio { -class NOSTALGIASTUDIO_EXPORT Editor: public ox::SignalHandler { +class NOSTALGIASTUDIO_EXPORT Editor: public Widget { private: UndoStack m_cmdStack; @@ -35,6 +36,8 @@ class NOSTALGIASTUDIO_EXPORT Editor: public ox::SignalHandler { [[nodiscard]] virtual ox::String itemName() const = 0; + virtual ox::String itemDisplayName() const; + virtual void cut(); virtual void copy(); @@ -43,6 +46,8 @@ class NOSTALGIASTUDIO_EXPORT Editor: public ox::SignalHandler { virtual void exportFile(); + void close(); + /** * Save changes to item being edited. */ @@ -89,12 +94,14 @@ class NOSTALGIASTUDIO_EXPORT Editor: public ox::SignalHandler { */ virtual void saveItem(); + // slots public: ox::Signal unsavedChangesChanged; ox::Signal exportableChanged; ox::Signal cutEnabledChanged; ox::Signal copyEnabledChanged; ox::Signal pasteEnabledChanged; + ox::Signal closed; }; diff --git a/src/nostalgia/studio/lib/filedialog.mm b/src/nostalgia/studio/lib/filedialog_mac.mm similarity index 100% rename from src/nostalgia/studio/lib/filedialog.mm rename to src/nostalgia/studio/lib/filedialog_mac.mm diff --git a/src/nostalgia/studio/lib/module.cpp b/src/nostalgia/studio/lib/module.cpp index c289eee4..38282eb5 100644 --- a/src/nostalgia/studio/lib/module.cpp +++ b/src/nostalgia/studio/lib/module.cpp @@ -6,20 +6,11 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -#include "context.hpp" #include "module.hpp" namespace nostalgia::studio { -QVector Module::newWizards(const Context*) { - return {}; -} - -QVector Module::importWizards(const Context*) { - return {}; -} - -QVector Module::editors(const Context*) { +ox::Vector Module::editors(core::Context*) { return {}; } diff --git a/src/nostalgia/studio/lib/module.hpp b/src/nostalgia/studio/lib/module.hpp index ca54c3cf..83fa31ca 100644 --- a/src/nostalgia/studio/lib/module.hpp +++ b/src/nostalgia/studio/lib/module.hpp @@ -10,33 +10,25 @@ #include -#include -#include +#include +#include -#include "wizard.hpp" +#include namespace nostalgia::studio { struct EditorMaker { - QStringList fileTypes; - std::function make; + using Func = std::function; + ox::Vector fileTypes; + Func make; }; class Module { - public: - virtual ~Module() = default; + virtual ~Module() noexcept = default; - virtual QVector newWizards(const class Context *ctx); - - virtual QVector importWizards(const Context *ctx); - - virtual QVector editors(const class Context *ctx); + virtual ox::Vector editors(core::Context *ctx); }; -} - -#define PluginInterface_iid "net.drinkingtea.nostalgia.studio.Module" - -Q_DECLARE_INTERFACE(nostalgia::studio::Module, PluginInterface_iid) +} \ No newline at end of file diff --git a/src/nostalgia/studio/lib/project.cpp b/src/nostalgia/studio/lib/project.cpp index e7792ca8..6857e321 100644 --- a/src/nostalgia/studio/lib/project.cpp +++ b/src/nostalgia/studio/lib/project.cpp @@ -21,7 +21,7 @@ ox::String filePathToName(const ox::String &path, const ox::String &prefix, cons } -Project::Project(const ox::String &path) noexcept: m_fs(ox::make_unique(path.c_str())) { +Project::Project(ox::FileSystem *fs, const ox::String &path) noexcept: m_fs(fs) { oxTracef("nostalgia::studio", "Project: {}", path); m_path = path; } @@ -33,7 +33,7 @@ ox::Error Project::create() noexcept { } ox::FileSystem *Project::romFs() noexcept { - return m_fs.get(); + return m_fs; } ox::Error Project::mkdir(const ox::String &path) const noexcept { diff --git a/src/nostalgia/studio/lib/project.hpp b/src/nostalgia/studio/lib/project.hpp index 6f9dd3aa..7f40b58c 100644 --- a/src/nostalgia/studio/lib/project.hpp +++ b/src/nostalgia/studio/lib/project.hpp @@ -39,10 +39,10 @@ ox::String filePathToName(const ox::String &path, const ox::String &prefix, cons class NOSTALGIASTUDIO_EXPORT Project { private: ox::String m_path = ""; - mutable ox::UniquePtr m_fs; + mutable ox::FileSystem *m_fs = nullptr; public: - explicit Project(const ox::String &path) noexcept; + explicit Project(ox::FileSystem *fs, const ox::String &path) noexcept; ox::Error create() noexcept; diff --git a/src/nostalgia/studio/lib/task.cpp b/src/nostalgia/studio/lib/task.cpp index 85842db5..2b801988 100644 --- a/src/nostalgia/studio/lib/task.cpp +++ b/src/nostalgia/studio/lib/task.cpp @@ -6,20 +6,20 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include + #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--)); - } - } + oxIgnoreError(m_tasks.erase(std::remove_if(m_tasks.begin(), m_tasks.end(), [&](auto &t) { + return t->update(ctx) == TaskState::Done; + }))); } void TaskRunner::add(Task *task) noexcept { m_tasks.emplace_back(task); } -} \ No newline at end of file +} diff --git a/src/nostalgia/studio/main.cpp b/src/nostalgia/studio/main.cpp index 11c10af6..fed824b5 100644 --- a/src/nostalgia/studio/main.cpp +++ b/src/nostalgia/studio/main.cpp @@ -21,24 +21,29 @@ class StudioUIDrawer: public core::Drawer { explicit StudioUIDrawer(StudioUI *ui) noexcept: m_ui(ui) { } protected: - void draw(core::Context *ctx) noexcept final { - m_ui->draw(ctx); + void draw(core::Context*) noexcept final { + m_ui->draw(); } }; static int eventHandler(core::Context *ctx) noexcept { auto ui = ctx->customData(); - ui->update(ctx); + ui->update(); return 16; } -static ox::Error run(ox::FileSystem *fs) noexcept { - oxRequireM(ctx, core::init(fs)); +static ox::Error run(ox::UniquePtr fs) noexcept { + oxRequireM(ctx, core::init(std::move(fs), "NostalgiaStudio")); core::setWindowTitle(ctx.get(), "Nostalgia Studio"); core::setEventHandler(ctx.get(), eventHandler); - StudioUI ui; - StudioUIDrawer drawer(&ui); - ctx->setCustomData(&ui); + ox::UniquePtr ui; + try { + ui = ox::make_unique(ctx.get()); + } catch (ox::Exception &ex) { + return ex.toError(); + } + StudioUIDrawer drawer(ui.get()); + ctx->setCustomData(ui.get()); ctx->drawers.emplace_back(&drawer); return core::run(ctx.get()); } @@ -47,10 +52,10 @@ static ox::Error run(int argc, const char **argv) noexcept { ox::trace::init(); if (argc >= 2) { const auto path = argv[1]; - oxRequire(fs, core::loadRomFs(path)); - return run(fs.get()); + oxRequireM(fs, core::loadRomFs(path)); + return run(std::move(fs)); } else { - return run(nullptr); + return run(ox::UniquePtr(nullptr)); } } diff --git a/src/nostalgia/studio/projectexplorer.cpp b/src/nostalgia/studio/projectexplorer.cpp index bec07759..ed85541d 100644 --- a/src/nostalgia/studio/projectexplorer.cpp +++ b/src/nostalgia/studio/projectexplorer.cpp @@ -6,6 +6,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include + #include #include @@ -14,18 +16,41 @@ namespace nostalgia { +static ox::Result> +buildProjectTreeModel(ProjectExplorer *explorer, const ox::String &name, const ox::String &path, ProjectTreeModel *parent) noexcept { + const auto fs = explorer->romFs(); + oxRequire(stat, fs->stat(path.c_str())); + auto out = ox::make_unique(explorer, name, parent); + if (stat.fileType == ox::FileType::Directory) { + oxRequireM(children, fs->ls(path)); + std::sort(children.begin(), children.end()); + ox::Vector> outChildren; + for (const auto &childName : children) { + if (childName[0] != '.') { + const auto childPath = ox::sfmt("{}/{}", path, childName); + oxRequireM(child, buildProjectTreeModel(explorer, childName, childPath, out.get())); + outChildren.emplace_back(std::move(child)); + } + } + out->setChildren(std::move(outChildren)); + } + return out; +} + +ProjectExplorer::ProjectExplorer(core::Context *ctx) noexcept { + m_ctx = ctx; +} + 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); + | ImGuiWindowFlags_NoResize + | ImGuiWindowFlags_NoMove + | ImGuiWindowFlags_NoScrollbar + | ImGuiWindowFlags_NoSavedSettings; ImGui::Begin("ProjectExplorer", nullptr, flags); - ImGui::PopStyleVar(); ImGui::SetNextTreeNodeOpen(true); if (m_treeModel) { m_treeModel->draw(ctx); @@ -37,5 +62,11 @@ void ProjectExplorer::setModel(ox::UniquePtr model) noexcept { m_treeModel = std::move(model); } +ox::Error ProjectExplorer::refreshProjectTreeModel(const ox::String&) noexcept { + oxRequireM(model, buildProjectTreeModel(this, "Project", "/", nullptr)); + setModel(std::move(model)); + return OxError(0); +} + } diff --git a/src/nostalgia/studio/projectexplorer.hpp b/src/nostalgia/studio/projectexplorer.hpp index a117c855..d483a04d 100644 --- a/src/nostalgia/studio/projectexplorer.hpp +++ b/src/nostalgia/studio/projectexplorer.hpp @@ -8,6 +8,7 @@ #pragma once +#include #include #include "lib/widget.hpp" @@ -18,10 +19,23 @@ namespace nostalgia { class ProjectExplorer: public studio::Widget { private: ox::UniquePtr m_treeModel; + core::Context *m_ctx = nullptr; public: + explicit ProjectExplorer(core::Context *ctx) noexcept; + void draw(core::Context *ctx) noexcept override; void setModel(ox::UniquePtr model) noexcept; + + ox::Error refreshProjectTreeModel(const ox::String& = {}) noexcept; + + constexpr ox::FileSystem *romFs() noexcept { + return m_ctx->rom.get(); + } + + // slots + public: + ox::Signal fileChosen; }; } \ No newline at end of file diff --git a/src/nostalgia/studio/projecttreemodel.cpp b/src/nostalgia/studio/projecttreemodel.cpp index 48192172..adc470a7 100644 --- a/src/nostalgia/studio/projecttreemodel.cpp +++ b/src/nostalgia/studio/projecttreemodel.cpp @@ -8,14 +8,16 @@ #include +#include "projectexplorer.hpp" #include "projecttreemodel.hpp" namespace nostalgia { -ProjectTreeModel::ProjectTreeModel(const ox::String &name, - ox::Vector> children) noexcept { +ProjectTreeModel::ProjectTreeModel(ProjectExplorer *explorer, const ox::String &name, + ProjectTreeModel *parent) noexcept { + m_explorer = explorer; m_name = name; - m_children = std::move(children); + m_parent = parent; } ProjectTreeModel::ProjectTreeModel(ProjectTreeModel &&other) noexcept { @@ -32,11 +34,23 @@ void ProjectTreeModel::draw(core::Context *ctx) noexcept { } ImGui::TreePop(); } - } else { - if (ImGui::TreeNodeEx(m_name.c_str(), ImGuiTreeNodeFlags_Leaf)) { - ImGui::TreePop(); + } else if (auto path = fullPath(); ImGui::TreeNodeEx(ox::sfmt("{}##{}", m_name, path).c_str(), ImGuiTreeNodeFlags_Leaf)) { + if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) { + m_explorer->fileChosen.emit(path); } + ImGui::TreePop(); } } +void ProjectTreeModel::setChildren(ox::Vector> children) noexcept { + m_children = std::move(children); +} + +ox::String ProjectTreeModel::fullPath() noexcept { + if (m_parent) { + return m_parent->fullPath() + "/" + m_name; + } + return ""; +} + } \ No newline at end of file diff --git a/src/nostalgia/studio/projecttreemodel.hpp b/src/nostalgia/studio/projecttreemodel.hpp index 848c9752..bc3ca7ae 100644 --- a/src/nostalgia/studio/projecttreemodel.hpp +++ b/src/nostalgia/studio/projecttreemodel.hpp @@ -17,15 +17,23 @@ namespace nostalgia { class ProjectTreeModel { private: + class ProjectExplorer *m_explorer = nullptr; + ProjectTreeModel *m_parent = nullptr; ox::String m_name; - ox::Vector > m_children; + ox::Vector> m_children; public: - explicit ProjectTreeModel(const ox::String &name, - ox::Vector > children) noexcept; + explicit ProjectTreeModel(class ProjectExplorer *explorer, const ox::String &name, + ProjectTreeModel *parent = nullptr) noexcept; ProjectTreeModel(ProjectTreeModel &&other) noexcept; void draw(core::Context *ctx) noexcept; + + void setChildren(ox::Vector> children) noexcept; + + private: + [[nodiscard]] + ox::String fullPath() noexcept; }; } \ No newline at end of file diff --git a/src/nostalgia/studio/studio.hpp b/src/nostalgia/studio/studio.hpp index 351c4e8e..8667a2c5 100644 --- a/src/nostalgia/studio/studio.hpp +++ b/src/nostalgia/studio/studio.hpp @@ -10,6 +10,8 @@ #include "lib/editor.hpp" #include "lib/filedialog.hpp" +#include "lib/module.hpp" #include "lib/project.hpp" #include "lib/task.hpp" #include "lib/undostack.hpp" +#include "lib/widget.hpp" diff --git a/src/nostalgia/studio/studioapp.cpp b/src/nostalgia/studio/studioapp.cpp index a62420d1..08406eeb 100644 --- a/src/nostalgia/studio/studioapp.cpp +++ b/src/nostalgia/studio/studioapp.cpp @@ -11,6 +11,7 @@ #include #include "lib/configio.hpp" +#include "builtinmodules.hpp" #include "filedialogmanager.hpp" #include "studioapp.hpp" @@ -20,58 +21,62 @@ struct StudioConfig { static constexpr auto TypeName = "net.drinkingtea.nostalgia.studio.StudioConfig"; static constexpr auto TypeVersion = 1; ox::String projectPath; + ox::Vector openFiles; }; template constexpr ox::Error model(T *h, StudioConfig *config) noexcept { + h->template setTypeInfo(); oxReturnError(h->field("project_path", &config->projectPath)); + oxReturnError(h->field("open_files", &config->openFiles)); return OxError(0); } -StudioUI::StudioUI() noexcept { - m_widgets.push_back(&m_projectExplorer); - if (const auto [config, err] = studio::readConfig(); !err) { +StudioUI::StudioUI(core::Context *ctx) noexcept { + m_ctx = ctx; + m_projectExplorer = new ProjectExplorer(m_ctx); + m_widgets.emplace_back(m_projectExplorer); + m_projectExplorer->fileChosen.connect(this, &StudioUI::openFile); + loadModules(); + // open project and files + const auto [config, err] = studio::readConfig(ctx); + if (!err) { oxIgnoreError(openProject(config.projectPath)); + for (const auto &f : config.openFiles) { + oxLogError(openFile(f)); + } } 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)); - } + if (err.msg) { + oxErrf("Could not open studio config file: {}\n", err.msg); + } else { + oxErrf("Could not open studio config file: {} ({}:{})\n", err.errCode, err.file, err.line); } } - return ox::make_unique(name, std::move(outChildren)); } -void StudioUI::update(core::Context *ctx) noexcept { - m_taskRunner.update(ctx); +void StudioUI::update() noexcept { + m_taskRunner.update(m_ctx); } -void StudioUI::draw(core::Context *ctx) noexcept { - drawMenu(ctx); - drawToolbar(ctx); +void StudioUI::draw() noexcept { + drawMenu(); + drawToolbar(); + drawTabs(); for (auto &w : m_widgets) { - w->draw(ctx); + w->draw(m_ctx); } + constexpr auto aboutFlags = ImGuiWindowFlags_NoCollapse + | ImGuiWindowFlags_NoResize + | ImGuiWindowFlags_Modal; if (m_aboutEnabled && - ImGui::Begin("About", &m_aboutEnabled, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) { + ImGui::Begin("About", &m_aboutEnabled, aboutFlags)) { + ImGui::SetWindowSize(ImVec2(400, 250)); + ImGui::Text("Nostalgia Studio - dev build"); ImGui::End(); } } -void StudioUI::drawMenu(core::Context *ctx) noexcept { +void StudioUI::drawMenu() noexcept { if (ImGui::BeginMainMenuBar()) { if (ImGui::BeginMenu("File")) { if (ImGui::MenuItem("New...", "Ctrl+N")) { @@ -82,7 +87,7 @@ void StudioUI::drawMenu(core::Context *ctx) noexcept { if (ImGui::MenuItem("Save", "Ctrl+S", false, m_saveEnabled)) { } if (ImGui::MenuItem("Quit", "Ctrl+Q")) { - core::shutdown(ctx); + core::shutdown(m_ctx); } ImGui::EndMenu(); } @@ -99,45 +104,134 @@ void StudioUI::drawMenu(core::Context *ctx) noexcept { } } -void StudioUI::drawToolbar(core::Context*) noexcept { +void StudioUI::drawToolbar() 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)); + ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x + 1, viewport->Pos.y + 21)); + ImGui::SetNextWindowSize(ImVec2(viewport->Size.x - 1, 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::Begin("MainToolbar##MainWindow", nullptr, flags); { - ImGui::Button("New##MainToolbar", ImVec2(BtnWidth, BtnHeight)); + ImGui::Button("New##MainToolbar##MainWindow", ImVec2(BtnWidth, BtnHeight)); ImGui::SameLine(); - if (ImGui::Button("Open Project##MainToolbar", ImVec2(BtnWidth, BtnHeight))) { + if (ImGui::Button("Open Project##MainToolbar##MainWindow", ImVec2(BtnWidth, BtnHeight))) { m_taskRunner.add(new FileDialogManager(this, &StudioUI::openProject)); } ImGui::SameLine(); - ImGui::Button("Save##MainToolbar", ImVec2(BtnWidth, BtnHeight)); + ImGui::Button("Save##MainToolbar##MainWindow", 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(); +void StudioUI::drawTabs() noexcept { + const auto viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x + 316, viewport->Pos.y + 71)); + ImGui::SetNextWindowSize(ImVec2(viewport->Size.x - 316, viewport->Size.y - 71)); + constexpr auto windowFlags = ImGuiWindowFlags_NoTitleBar + | ImGuiWindowFlags_NoResize + | ImGuiWindowFlags_NoMove + | ImGuiWindowFlags_NoScrollbar + | ImGuiWindowFlags_NoSavedSettings; + ImGui::Begin("TabWindow##MainWindow", nullptr, windowFlags); + constexpr auto tabBarFlags = ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_TabListPopupButton; + if (ImGui::BeginTabBar("TabBar##TabWindow##MainWindow", tabBarFlags)) { + for (auto &e : m_editors) { + bool open = true; + if (ImGui::BeginTabItem(e->itemDisplayName().c_str(), &open)) { + e->draw(m_ctx); + ImGui::EndTabItem(); + } + if (!open) { + e->close(); + try { + oxThrowError(m_editors.erase(e)); + } catch (const std::exception &e) { + oxErrf("Editor tab deletion failed: {}", e.what()); + } + } + } + ImGui::EndTabBar(); + } + ImGui::End(); } -ox::Error StudioUI::refreshProjectTreeModel(const ox::String&) noexcept { - oxRequireM(model, buildProjectTreeModel("Project", "/", m_project->romFs())); - m_projectExplorer.setModel(std::move(model)); +void StudioUI::loadEditorMaker(const studio::EditorMaker &editorMaker) noexcept { + for (auto &ext : editorMaker.fileTypes) { + m_editorMakers[ext] = editorMaker.make; + } +} + +void StudioUI::loadModule(studio::Module *module) noexcept { + for (auto &editorMaker : module->editors(m_ctx)) { + loadEditorMaker(editorMaker); + } +} + +void StudioUI::loadModules() noexcept { + //for (auto &moduleMaker : BuiltinModules) { + // const auto module = moduleMaker(); + // loadModule(module.get()); + //} +} + +ox::Error StudioUI::openProject(const ox::String &path) noexcept { + oxRequireM(fs, core::loadRomFs(path.c_str())); + m_ctx->rom = std::move(fs); + core::setWindowTitle(m_ctx, ox::sfmt("Nostalgia Studio - {}", path).c_str()); + m_project = ox::make_unique(m_ctx->rom.get(), path); + m_project->fileAdded.connect(m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel); + m_project->fileDeleted.connect(m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel); + studio::editConfig(m_ctx, [&](StudioConfig *config) { + config->projectPath = path; + }); + return m_projectExplorer->refreshProjectTreeModel(); +} + +ox::Error StudioUI::openFile(const ox::String &path) noexcept { + if (m_openFiles.contains(path)) { + return OxError(0); + } + // find file extension + const auto extStart = std::find(path.crbegin(), path.crend(), '.').offset(); + if (!extStart) { + return OxError(1, "Cannot open a file without valid extension."); + } + const auto ext = path.substr(extStart + 1); + // create Editor + if (!m_editorMakers.contains(ext)) { + return OxError(1, "There is no editor for this file extension"); + } + try { + auto editor = m_editorMakers[ext](path); + editor->closed.connect(this, &StudioUI::closeFile); + m_editors.emplace_back(editor); + } catch (const ox::Exception &ex) { + return ex.toError(); + } + m_openFiles.emplace_back(path); + // save to config + studio::editConfig(m_ctx, [&](StudioConfig *config) { + if (!config->openFiles.contains((path))) { + config->openFiles.emplace_back(path); + } + }); + return OxError(0); +} + +ox::Error StudioUI::closeFile(const ox::String &path) noexcept { + if (!m_openFiles.contains(path)) { + return OxError(0); + } + oxIgnoreError(m_openFiles.erase(std::remove(m_openFiles.begin(), m_openFiles.end(), path))); + // save to config + studio::editConfig(m_ctx, [&](StudioConfig *config) { + oxIgnoreError(config->openFiles.erase(std::remove(config->openFiles.begin(), config->openFiles.end(), path))); + }); return OxError(0); } diff --git a/src/nostalgia/studio/studioapp.hpp b/src/nostalgia/studio/studioapp.hpp index cec6d9cb..349d784e 100644 --- a/src/nostalgia/studio/studioapp.hpp +++ b/src/nostalgia/studio/studioapp.hpp @@ -12,6 +12,7 @@ #include #include +#include "lib/module.hpp" #include "lib/project.hpp" #include "lib/task.hpp" #include "projectexplorer.hpp" @@ -25,28 +26,41 @@ class StudioUI: public ox::SignalHandler { private: ox::UniquePtr m_project; studio::TaskRunner m_taskRunner; - ox::Vector m_widgets; - ox::UniquePtr m_treeModel; - ProjectExplorer m_projectExplorer; + core::Context *m_ctx = nullptr; + ox::Vector> m_editors; + ox::Vector> m_widgets; + ox::HashMap m_editorMakers; + ProjectExplorer *m_projectExplorer = nullptr; + ox::Vector m_openFiles; bool m_saveEnabled = false; bool m_aboutEnabled = false; public: - StudioUI() noexcept; + explicit StudioUI(core::Context *ctx) noexcept; - void update(core::Context *ctx) noexcept; + void update() noexcept; protected: - void draw(core::Context *ctx) noexcept; + void draw() noexcept; private: - void drawMenu(core::Context *ctx) noexcept; + void drawMenu() noexcept; - void drawToolbar(core::Context *ctx) noexcept; + void drawToolbar() noexcept; + + void drawTabs() noexcept; + + void loadEditorMaker(const studio::EditorMaker &editorMaker) noexcept; + + void loadModule(studio::Module *module) noexcept; + + void loadModules() noexcept; ox::Error openProject(const ox::String &path) noexcept; - ox::Error refreshProjectTreeModel(const ox::String& = {}) noexcept; + ox::Error openFile(const ox::String &path) noexcept; + + ox::Error closeFile(const ox::String &path) noexcept; }; } \ No newline at end of file