/* * Copyright 2016 - 2022 Gary Talent (gary@drinkingtea.net). All rights reserved. */ #include #include #include "lib/configio.hpp" #include "builtinmodules.hpp" #include "filedialogmanager.hpp" #include "nostalgia/core/input.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; ox::Vector openFiles; bool showProjectExplorer = true; }; oxModelBegin(StudioConfig) oxModelFieldRename(project_path, projectPath) oxModelFieldRename(open_files, openFiles) oxModelFieldRename(show_project_explorer, showProjectExplorer) oxModelEnd() StudioUI::StudioUI(core::Context *ctx) noexcept { m_ctx = ctx; m_projectExplorer = new ProjectExplorer(m_ctx); m_projectExplorer->fileChosen.connect(this, &StudioUI::openFile); loadModules(); // open project and files const auto [config, err] = studio::readConfig(ctx); m_showProjectExplorer = config.showProjectExplorer; if (!err) { oxIgnoreError(openProject(config.projectPath)); for (const auto &f : config.openFiles) { auto openFileErr = openFile(f); if (openFileErr) { oxErrorf("Could not open editor: {}\n", toStr(openFileErr)); } } } else { if (toStr(err)) { oxErrf("Could not open studio config file: {}\n", toStr(err)); } else { oxErrf("Could not open studio config file: {} ({}:{})\n", err.errCode, err.file, err.line); } } } void StudioUI::update() noexcept { m_taskRunner.update(m_ctx); } void StudioUI::handleKeyEvent(core::Key key, bool down) noexcept { if (down && core::buttonDown(m_ctx, core::Key::Mod_Ctrl)) { switch (key) { case core::Key::Num_1: toggleProjectExplorer(); break; case core::Key::Alpha_C: m_acitveEditor->copy(); break; case core::Key::Alpha_O: m_taskRunner.add(new FileDialogManager(this, &StudioUI::openProject)); break; case core::Key::Alpha_Q: core::shutdown(m_ctx); break; case core::Key::Alpha_S: save(); break; case core::Key::Alpha_V: m_acitveEditor->paste(); break; case core::Key::Alpha_X: m_acitveEditor->cut(); break; case core::Key::Alpha_Y: redo(); break; case core::Key::Alpha_Z: undo(); break; default: break; } } } void StudioUI::draw() noexcept { drawMenu(); drawTabBar(); if (m_showProjectExplorer) { m_projectExplorer->draw(m_ctx); } for (auto &w : m_widgets) { w->draw(m_ctx); } if (m_aboutEnabled) { ImGui::OpenPopup("About"); } drawAboutPopup(); } void StudioUI::drawMenu() 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_acitveEditor && m_acitveEditor->unsavedChanges())) { m_acitveEditor->save(); } if (ImGui::MenuItem("Quit", "Ctrl+Q")) { core::shutdown(m_ctx); } ImGui::EndMenu(); } if (ImGui::BeginMenu("Edit")) { auto undoStack = m_acitveEditor ? m_acitveEditor->undoStack() : nullptr; if (ImGui::MenuItem("Undo", "Ctrl+Z", false, undoStack && undoStack->canUndo())) { m_acitveEditor->undoStack()->undo(); } if (ImGui::MenuItem("Redo", "Ctrl+Y", false, undoStack && undoStack->canRedo())) { m_acitveEditor->undoStack()->redo(); } ImGui::Separator(); if (ImGui::MenuItem("Copy", "Ctrl+C")) { m_acitveEditor->copy(); } if (ImGui::MenuItem("Cut", "Ctrl+X")) { m_acitveEditor->cut(); } if (ImGui::MenuItem("Paste", "Ctrl+V")) { m_acitveEditor->paste(); } ImGui::EndMenu(); } if (ImGui::BeginMenu("View")) { if (ImGui::MenuItem("Project Explorer", "Ctrl+1", m_showProjectExplorer)) { toggleProjectExplorer(); } ImGui::EndMenu(); } if (ImGui::BeginMenu("Help")) { if (ImGui::MenuItem("About")) { m_aboutEnabled = true; } ImGui::EndMenu(); } ImGui::EndMainMenuBar(); } } void StudioUI::drawTabBar() noexcept { const auto viewport = ImGui::GetMainViewport(); const auto mod = m_showProjectExplorer ? 316.f : 0.f; ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x + mod, viewport->Pos.y + 20)); ImGui::SetNextWindowSize(ImVec2(viewport->Size.x - mod, viewport->Size.y - 20)); 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)) { drawTabs(); ImGui::EndTabBar(); } ImGui::End(); } void StudioUI::drawTabs() noexcept { for (auto it = m_editors.begin(); it != m_editors.end();) { auto const &e = *it; auto open = true; const auto unsavedChanges = e->unsavedChanges() ? ImGuiTabItemFlags_UnsavedDocument : 0; const auto selected = m_activeEditorUpdatePending && e.get() == m_acitveEditor ? ImGuiTabItemFlags_SetSelected : 0; const auto flags = unsavedChanges | selected; if (ImGui::BeginTabItem(e->itemDisplayName().c_str(), &open, flags)) { if (selected) { m_acitveEditor = e.get(); m_activeEditorUpdatePending = false; } e->draw(m_ctx); ImGui::EndTabItem(); } if (!open) { e->close(); try { oxThrowError(m_editors.erase(it).moveTo(&it)); } catch (const ox::Exception &ex) { oxErrf("Editor tab deletion failed: {} ({}:{})\n", ex.what(), ex.file, ex.line); } catch (const std::exception &ex) { oxErrf("Editor tab deletion failed: {}\n", ex.what()); } } else { ++it; } } } void StudioUI::drawAboutPopup() noexcept { constexpr auto modalFlags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize; if (ImGui::BeginPopupModal("About", &m_aboutEnabled, modalFlags)) { ImGui::Text("Nostalgia Studio - dev build"); ImGui::SetNextWindowPos(ImVec2(0.5f, 0.5f), ImGuiCond_Always, ImVec2(0.5f,0.5f)); if (ImGui::Button("Close")) { ImGui::CloseCurrentPopup(); m_aboutEnabled = false; } ImGui::EndPopup(); } } void StudioUI::loadEditorMaker(const studio::EditorMaker &editorMaker) noexcept { for (const 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()); } } void StudioUI::toggleProjectExplorer() noexcept { m_showProjectExplorer = !m_showProjectExplorer; studio::editConfig(m_ctx, [&](StudioConfig *config) { config->showProjectExplorer = m_showProjectExplorer; }); } void StudioUI::redo() noexcept { auto undoStack = m_acitveEditor ? m_acitveEditor->undoStack() : nullptr; if (undoStack && undoStack->canRedo()) { m_acitveEditor->undoStack()->redo(); } } void StudioUI::undo() noexcept { auto undoStack = m_acitveEditor ? m_acitveEditor->undoStack() : nullptr; if (undoStack && undoStack->canUndo()) { m_acitveEditor->undoStack()->undo(); } } void StudioUI::save() noexcept { if (m_acitveEditor && m_acitveEditor->unsavedChanges()) { m_acitveEditor->save(); } } 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); auto sctx = applicationData(m_ctx); sctx->project = m_project.get(); m_project->fileAdded.connect(m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel); m_project->fileDeleted.connect(m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel); m_openFiles.clear(); m_editors.clear(); studio::editConfig(m_ctx, [&](StudioConfig *config) { config->projectPath = path; config->openFiles.clear(); }); return m_projectExplorer->refreshProjectTreeModel(); } ox::Error StudioUI::openFile(const ox::String &path) noexcept { if (m_openFiles.contains(path)) { for (auto &e : m_editors) { if (e->itemName() == path) { m_acitveEditor = e.get(); m_activeEditorUpdatePending = true; break; } } return OxError(0); } oxRequire(ext, studio::fileExt(path)); // create Editor if (!m_editorMakers.contains(ext)) { return OxError(1, "There is no editor for this file extension"); } auto [editor, err] = m_editorMakers[ext](path); if (err) { oxErrorf("Could not open Editor: {} ({}:{})", err.msg, err.file, err.line); return err; } editor->closed.connect(this, &StudioUI::closeFile); m_editors.emplace_back(editor); m_openFiles.emplace_back(path); m_acitveEditor = m_editors.back().value.get(); m_activeEditorUpdatePending = true; // 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); } }