From 1df4e7808497dfdb88d039fd7d9d2028b2a5d9b0 Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Thu, 28 Dec 2023 23:44:25 -0600 Subject: [PATCH] [olympic/studio] Add new project menu, make file creation open file --- src/olympic/studio/applib/src/CMakeLists.txt | 1 + src/olympic/studio/applib/src/newmenu.cpp | 4 +- src/olympic/studio/applib/src/newproject.cpp | 95 +++++++++++++++++++ src/olympic/studio/applib/src/newproject.hpp | 58 +++++++++++ src/olympic/studio/applib/src/studioapp.cpp | 36 +++++-- src/olympic/studio/applib/src/studioapp.hpp | 7 +- .../modlib/include/studio/itemmaker.hpp | 14 ++- .../studio/modlib/include/studio/project.hpp | 21 ++-- src/olympic/studio/modlib/src/project.cpp | 19 ++++ 9 files changed, 228 insertions(+), 27 deletions(-) create mode 100644 src/olympic/studio/applib/src/newproject.cpp create mode 100644 src/olympic/studio/applib/src/newproject.hpp diff --git a/src/olympic/studio/applib/src/CMakeLists.txt b/src/olympic/studio/applib/src/CMakeLists.txt index fc402178..2dfce8d5 100644 --- a/src/olympic/studio/applib/src/CMakeLists.txt +++ b/src/olympic/studio/applib/src/CMakeLists.txt @@ -5,6 +5,7 @@ add_library( filedialogmanager.cpp main.cpp newmenu.cpp + newproject.cpp projectexplorer.cpp projecttreemodel.cpp studioapp.cpp diff --git a/src/olympic/studio/applib/src/newmenu.cpp b/src/olympic/studio/applib/src/newmenu.cpp index 8b57dd8d..f9cd84a1 100644 --- a/src/olympic/studio/applib/src/newmenu.cpp +++ b/src/olympic/studio/applib/src/newmenu.cpp @@ -115,12 +115,12 @@ void NewMenu::drawLastPageButtons(turbine::Context &ctx) noexcept { } void NewMenu::finish(turbine::Context &ctx) noexcept { - auto const err = m_types[static_cast(m_selectedType)]->write(ctx, m_itemName); + auto const [path, err] = m_types[static_cast(m_selectedType)]->write(ctx, m_itemName); if (err) { oxLogError(err); return; } - finished.emit(""); + finished.emit(path); m_stage = Stage::Closed; } diff --git a/src/olympic/studio/applib/src/newproject.cpp b/src/olympic/studio/applib/src/newproject.cpp new file mode 100644 index 00000000..2310721d --- /dev/null +++ b/src/olympic/studio/applib/src/newproject.cpp @@ -0,0 +1,95 @@ +/* + * Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved. + */ + +#include + +#include +#include + +#include "filedialogmanager.hpp" +#include "newproject.hpp" + +namespace studio { + +NewProject::NewProject(ox::String projectDatadir) noexcept: m_projectDataDir(std::move(projectDatadir)) { + setTitle(ox::String("New Project")); + setSize({230, 140}); +} + +void NewProject::open() noexcept { + m_stage = Stage::Opening; + m_projectPath = ""; + m_projectName = ""; +} + +void NewProject::close() noexcept { + m_stage = Stage::Closed; + m_open = false; +} + +bool NewProject::isOpen() const noexcept { + return m_open; +} + +void NewProject::draw(turbine::Context &ctx) noexcept { + switch (m_stage) { + case Stage::Opening: + ImGui::OpenPopup(title().c_str()); + m_stage = Stage::NewItemName; + m_open = true; + [[fallthrough]]; + case Stage::NewItemName: + drawNewProjectName(ctx); + break; + case Stage::Closed: + m_open = false; + break; + } +} + +void NewProject::drawNewProjectName(turbine::Context &ctx) noexcept { + drawWindow(ctx, &m_open, [this, &ctx] { + ImGui::InputText("Name", m_projectName.data(), m_projectName.cap()); + ImGui::Text("Path: %s", m_projectPath.c_str()); + if (ImGui::Button("Browse")) { + oxLogError(studio::chooseDirectory().moveTo(m_projectPath)); + } + drawLastPageButtons(ctx); + }); +} + +void NewProject::drawFirstPageButtons() noexcept { + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - 130); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ImGui::GetContentRegionAvail().y - 20); + auto const btnSz = ImVec2(60, 20); + if (ImGui::Button("Next", btnSz)) { + m_stage = Stage::NewItemName; + } + ImGui::SameLine(); + if (ImGui::Button("Cancel", btnSz)) { + ImGui::CloseCurrentPopup(); + m_stage = Stage::Closed; + } +} + +void NewProject::drawLastPageButtons(turbine::Context&) noexcept { + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - 95); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ImGui::GetContentRegionAvail().y - 20); + if (ImGui::Button("Finish")) { + finish(); + } + ImGui::SameLine(); + if (ImGui::Button("Quit")) { + ImGui::CloseCurrentPopup(); + m_stage = Stage::Closed; + } +} + +void NewProject::finish() noexcept { + auto projectPath = ox::sfmt("{}/{}", m_projectPath, m_projectName); + finished.emit(projectPath); + m_stage = Stage::Closed; +} + +} diff --git a/src/olympic/studio/applib/src/newproject.hpp b/src/olympic/studio/applib/src/newproject.hpp new file mode 100644 index 00000000..f3bd42bc --- /dev/null +++ b/src/olympic/studio/applib/src/newproject.hpp @@ -0,0 +1,58 @@ +/* + * Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved. + */ + +#pragma once + +#include +#include +#include + +#include +#include + +namespace studio { + +class NewProject: public studio::Popup { + public: + enum class Stage { + Closed, + Opening, + NewItemName, + }; + + // emits path parameter + ox::Signal finished; + + private: + Stage m_stage = Stage::Closed; + ox::String const m_projectDataDir; + ox::String m_projectPath; + ox::BString<255> m_projectName; + ox::Vector> m_types; + bool m_open = false; + + public: + NewProject(ox::String projectDatadir) noexcept; + + void open() noexcept override; + + void close() noexcept override; + + [[nodiscard]] + bool isOpen() const noexcept override; + + void draw(turbine::Context &ctx) noexcept override; + + private: + void drawNewProjectName(turbine::Context &ctx) noexcept; + + void drawFirstPageButtons() noexcept; + + void drawLastPageButtons(turbine::Context &ctx) noexcept; + + void finish() noexcept; + +}; + +} diff --git a/src/olympic/studio/applib/src/studioapp.cpp b/src/olympic/studio/applib/src/studioapp.cpp index 52530878..5061f226 100644 --- a/src/olympic/studio/applib/src/studioapp.cpp +++ b/src/olympic/studio/applib/src/studioapp.cpp @@ -2,6 +2,8 @@ * Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved. */ +#include + #include #include @@ -42,15 +44,18 @@ StudioUI::StudioUI(turbine::Context &ctx, ox::StringView projectDataDir) noexcep m_ctx(ctx), m_projectDataDir(projectDataDir), m_projectExplorer(m_ctx), + m_newProject(ox::String(projectDataDir)), m_aboutPopup(m_ctx) { m_projectExplorer.fileChosen.connect(this, &StudioUI::openFile); + m_newProject.finished.connect(this, &StudioUI::createOpenProject); + m_newMenu.finished.connect(this, &StudioUI::openFile); ImGui::GetIO().IniFilename = nullptr; loadModules(); // open project and files auto const [config, err] = studio::readConfig(keelCtx(m_ctx)); m_showProjectExplorer = config.showProjectExplorer; if (!err) { - auto const openProjErr = openProject(config.projectPath); + auto const openProjErr = openProjectPath(config.projectPath); if (!openProjErr) { for (auto const&f: config.openFiles) { auto openFileErr = openFileActiveTab(f, config.activeTabItemName == f); @@ -95,10 +100,14 @@ void StudioUI::handleKeyEvent(turbine::Key key, bool down) noexcept { } break; case turbine::Key::Alpha_N: - m_newMenu.open(); + if (turbine::buttonDown(m_ctx, turbine::Key::Mod_Shift)) { + m_newProject.open(); + } else { + m_newMenu.open(); + } break; case turbine::Key::Alpha_O: - m_taskRunner.add(*ox::make(this, &StudioUI::openProject)); + m_taskRunner.add(*ox::make(this, &StudioUI::openProjectPath)); break; case turbine::Key::Alpha_Q: turbine::requestShutdown(m_ctx); @@ -168,8 +177,11 @@ void StudioUI::drawMenu() noexcept { if (ImGui::MenuItem("New...", "Ctrl+N")) { m_newMenu.open(); } + if (ImGui::MenuItem("New Project...", "Ctrl+Shift+N")) { + m_newProject.open(); + } if (ImGui::MenuItem("Open Project...", "Ctrl+O")) { - m_taskRunner.add(*ox::make(this, &StudioUI::openProject)); + m_taskRunner.add(*ox::make(this, &StudioUI::openProjectPath)); } if (ImGui::MenuItem("Save", "Ctrl+S", false, m_activeEditor && m_activeEditor->unsavedChanges())) { m_activeEditor->save(); @@ -317,19 +329,27 @@ void StudioUI::save() noexcept { } } -ox::Error StudioUI::openProject(ox::CRStringView path) noexcept { +ox::Error StudioUI::createOpenProject(ox::CRStringView path) noexcept { + std::error_code ec; + std::filesystem::create_directories(toStdStringView(path), ec); + oxReturnError(OxError(ec.value() != 0, "Could not create project directory")); + oxReturnError(openProjectPath(path)); + return m_project->writeAllTypeDescriptors(); +} + +ox::Error StudioUI::openProjectPath(ox::CRStringView path) noexcept { oxRequireM(fs, keel::loadRomFs(path)); oxReturnError(keel::setRomFs(keelCtx(m_ctx), std::move(fs))); - turbine::setWindowTitle(m_ctx, ox::sfmt("{} - {}", keelCtx(m_ctx).appName, path)); m_project = ox::make_unique(keelCtx(m_ctx), ox::String(path), m_projectDataDir); - auto sctx = applicationData(m_ctx); + auto const sctx = applicationData(m_ctx); sctx->project = m_project.get(); + turbine::setWindowTitle(m_ctx, ox::sfmt("{} - {}", keelCtx(m_ctx).appName, m_project->projectPath())); m_project->fileAdded.connect(&m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel); m_project->fileDeleted.connect(&m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel); m_openFiles.clear(); m_editors.clear(); studio::editConfig(keelCtx(m_ctx), [&](StudioConfig *config) { - config->projectPath = ox::String(path); + config->projectPath = ox::String(m_project->projectPath()); config->openFiles.clear(); }); return m_projectExplorer.refreshProjectTreeModel(); diff --git a/src/olympic/studio/applib/src/studioapp.hpp b/src/olympic/studio/applib/src/studioapp.hpp index 97be957e..ff433c9a 100644 --- a/src/olympic/studio/applib/src/studioapp.hpp +++ b/src/olympic/studio/applib/src/studioapp.hpp @@ -14,6 +14,7 @@ #include #include "aboutpopup.hpp" #include "newmenu.hpp" +#include "newproject.hpp" #include "projectexplorer.hpp" #include "projecttreemodel.hpp" @@ -36,9 +37,11 @@ class StudioUI: public ox::SignalHandler { studio::BaseEditor *m_activeEditor = nullptr; studio::BaseEditor *m_activeEditorUpdatePending = nullptr; NewMenu m_newMenu; + NewProject m_newProject; AboutPopup m_aboutPopup; ox::Array const m_popups = { &m_newMenu, + &m_newProject, &m_aboutPopup }; bool m_showProjectExplorer = true; @@ -79,7 +82,9 @@ class StudioUI: public ox::SignalHandler { void save() noexcept; - ox::Error openProject(ox::CRStringView path) noexcept; + ox::Error createOpenProject(ox::CRStringView path) noexcept; + + ox::Error openProjectPath(ox::CRStringView path) noexcept; ox::Error openFile(ox::CRStringView path) noexcept; diff --git a/src/olympic/studio/modlib/include/studio/itemmaker.hpp b/src/olympic/studio/modlib/include/studio/itemmaker.hpp index 88225247..dd7bb513 100644 --- a/src/olympic/studio/modlib/include/studio/itemmaker.hpp +++ b/src/olympic/studio/modlib/include/studio/itemmaker.hpp @@ -24,7 +24,14 @@ class ItemMaker { fileExt(pFileExt) { } virtual ~ItemMaker() noexcept = default; - virtual ox::Error write(turbine::Context &ctx, ox::CRStringView pName) const noexcept = 0; + + /** + * Returns path of the file created. + * @param ctx + * @param pName + * @return path of file or error in Result + */ + virtual ox::Result write(turbine::Context &ctx, ox::CRStringView pName) const noexcept = 0; }; template @@ -61,11 +68,12 @@ class ItemMakerT: public ItemMaker { m_item(std::move(pItem)), m_fmt(pFmt) { } - ox::Error write(turbine::Context &ctx, ox::CRStringView pName) const noexcept override { + ox::Result write(turbine::Context &ctx, ox::CRStringView pName) const noexcept override { auto const path = ox::sfmt("/{}/{}.{}", parentDir, pName, fileExt); auto sctx = turbine::applicationData(ctx); keel::createUuidMapping(keelCtx(ctx), path, ox::UUID::generate().unwrap()); - return sctx->project->writeObj(path, m_item, m_fmt); + oxReturnError(sctx->project->writeObj(path, m_item, m_fmt)); + return path; } }; diff --git a/src/olympic/studio/modlib/include/studio/project.hpp b/src/olympic/studio/modlib/include/studio/project.hpp index e242c1da..d54313e4 100644 --- a/src/olympic/studio/modlib/include/studio/project.hpp +++ b/src/olympic/studio/modlib/include/studio/project.hpp @@ -50,6 +50,7 @@ class Project { keel::Context &m_ctx; ox::String m_path; ox::String m_projectDataDir; + ox::String m_typeDescPath; mutable keel::TypeStore m_typeStore; ox::FileSystem &m_fs; ox::HashMap> m_fileExtFileMap; @@ -59,6 +60,9 @@ class Project { ox::Error create() noexcept; + [[nodiscard]] + ox::String const&projectPath() const noexcept; + [[nodiscard]] ox::FileSystem *romFs() noexcept; @@ -87,6 +91,8 @@ class Project { [[nodiscard]] ox::Vector const&fileList(ox::CRStringView ext) noexcept; + ox::Error writeAllTypeDescriptors() noexcept; + private: void buildFileIndex() noexcept; @@ -123,21 +129,10 @@ ox::Error Project::writeObj(ox::CRStringView path, T const&obj, ox::ClawFormat f if (m_typeStore.get().error) { oxReturnError(ox::buildTypeDef(&m_typeStore, &obj)); } - // write out type store - auto const descPath = ox::sfmt("/{}/type_descriptors", m_projectDataDir); oxRequire(desc, m_typeStore.get()); - auto const descExists = m_fs.stat(ox::sfmt("{}/{}", descPath, buildTypeId(*desc))).error != 0; + auto const descExists = m_fs.stat(ox::sfmt("{}/{}", m_typeDescPath, buildTypeId(*desc))).error != 0; if (!descExists || ox::defines::Debug) { - // write all descriptors because we don't know which types T depends on - oxReturnError(mkdir(descPath)); - for (auto const &t: m_typeStore.typeList()) { - oxRequireM(typeOut, ox::writeClaw(*t, ox::ClawFormat::Organic)); - // replace garbage last character with new line - *typeOut.back().value = '\n'; - // write to FS - auto const typePath = ox::sfmt("/{}/{}", descPath, buildTypeId(*t)); - oxReturnError(writeBuff(typePath, typeOut)); - } + oxReturnError(writeAllTypeDescriptors()); } oxReturnError(keel::setAsset(m_ctx, path, obj)); fileUpdated.emit(path); diff --git a/src/olympic/studio/modlib/src/project.cpp b/src/olympic/studio/modlib/src/project.cpp index 0a87fd75..76ee9b36 100644 --- a/src/olympic/studio/modlib/src/project.cpp +++ b/src/olympic/studio/modlib/src/project.cpp @@ -30,6 +30,7 @@ Project::Project(keel::Context &ctx, ox::String path, ox::CRStringView projectDa m_ctx(ctx), m_path(std::move(path)), m_projectDataDir(projectDataDir), + m_typeDescPath(ox::sfmt("/{}/type_descriptors", m_projectDataDir)), m_typeStore(*m_ctx.rom, ox::sfmt("/{}/type_descriptors", projectDataDir)), m_fs(*m_ctx.rom) { oxTracef("studio", "Project: {}", m_path); @@ -43,6 +44,10 @@ ox::Error Project::create() noexcept { return OxError(static_cast(ec.value()), "PassThroughFS: mkdir failed"); } +ox::String const&Project::projectPath() const noexcept { + return m_path; +} + ox::FileSystem *Project::romFs() noexcept { return &m_fs; } @@ -65,6 +70,20 @@ ox::Vector const&Project::fileList(ox::CRStringView ext) noexcept { return m_fileExtFileMap[ext]; } +ox::Error Project::writeAllTypeDescriptors() noexcept { + // write all descriptors because we don't know which types T depends on + oxReturnError(mkdir(m_typeDescPath)); + for (auto const &t: m_typeStore.typeList()) { + oxRequireM(typeOut, ox::writeClaw(*t, ox::ClawFormat::Organic)); + // replace garbage last character with new line + *typeOut.back().value = '\n'; + // write to FS + auto const typePath = ox::sfmt("{}/{}", m_typeDescPath, buildTypeId(*t)); + oxReturnError(writeBuff(typePath, typeOut)); + } + return {}; +} + void Project::buildFileIndex() noexcept { auto [files, err] = listFiles(); if (err) {