Gary Talent bd2e88cd88
All checks were successful
Build / build (push) Successful in 2m33s
[olympic,nostalgia] Cleanup with StringParam
2024-08-30 20:47:43 -05:00

416 lines
13 KiB
C++

/*
* Copyright 2016 - 2024 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <algorithm>
#include <filesystem>
#include <imgui.h>
#include <keel/media.hpp>
#include <glutils/glutils.hpp>
#include <turbine/turbine.hpp>
#include <studio/configio.hpp>
#include "clawviewer.hpp"
#include "filedialogmanager.hpp"
#include "studioapp.hpp"
namespace studio {
static ox::Vector<studio::Module const*> modules;
void registerModule(studio::Module const*mod) noexcept {
modules.emplace_back(mod);
}
struct StudioConfig {
static constexpr auto TypeName = "net.drinkingtea.studio.StudioConfig";
static constexpr auto TypeVersion = 1;
ox::String projectPath;
ox::String activeTabItemName;
ox::Vector<ox::String> openFiles;
bool showProjectExplorer = true;
};
oxModelBegin(StudioConfig)
oxModelFieldRename(activeTabItemName, active_tab_item_name)
oxModelFieldRename(projectPath, project_path)
oxModelFieldRename(openFiles, open_files)
oxModelFieldRename(showProjectExplorer, show_project_explorer)
oxModelEnd()
StudioUI::StudioUI(turbine::Context &ctx, ox::StringParam projectDataDir) noexcept:
m_sctx(*this, ctx),
m_ctx(ctx),
m_projectDataDir(std::move(projectDataDir)),
m_projectExplorer(m_ctx),
m_newProject(m_projectDataDir),
m_aboutPopup(m_ctx) {
turbine::setApplicationData(m_ctx, &m_sctx);
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<StudioConfig>(keelCtx(m_ctx));
m_showProjectExplorer = config.showProjectExplorer;
if (!err) {
auto const openProjErr = openProjectPath(config.projectPath);
if (!openProjErr) {
for (auto const&f: config.openFiles) {
auto openFileErr = openFileActiveTab(f, config.activeTabItemName == f);
if (openFileErr) {
oxErrorf("\nCould not open editor for file:\n\t{}\nReason:\n\t{}\n", f, toStr(openFileErr));
}
}
}
} else {
if constexpr(!ox::defines::Debug) {
oxErrf("Could not open studio config file: {}: {}\n", err.errCode, toStr(err));
} else {
oxErrf(
"Could not open studio config file: {}: {} ({}:{})\n",
err.errCode, toStr(err), err.file, err.line);
}
}
}
void StudioUI::handleKeyEvent(turbine::Key key, bool down) noexcept {
for (auto p : m_popups) {
if (p->isOpen()) {
if (key == turbine::Key::Escape) {
p->close();
}
return;
}
}
if (m_activeEditor && !ImGui::IsKeyDown(ImGuiKey_ModCtrl)) {
m_activeEditor->keyStateChanged(key, down);
}
}
void StudioUI::draw() noexcept {
glutils::clearScreen();
drawMenu();
auto const viewport = ImGui::GetMainViewport();
constexpr auto menuHeight = 18;
ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x, viewport->Pos.y + menuHeight));
ImGui::SetNextWindowSize(ImVec2(viewport->Size.x, viewport->Size.y - menuHeight));
constexpr auto windowFlags = ImGuiWindowFlags_NoTitleBar
| ImGuiWindowFlags_NoResize
| ImGuiWindowFlags_NoMove
| ImGuiWindowFlags_NoScrollbar
| ImGuiWindowFlags_NoSavedSettings;
ImGui::Begin("MainWindow##Studio", nullptr, windowFlags);
{
if (m_showProjectExplorer) {
m_projectExplorer.draw(m_sctx);
ImGui::SameLine();
}
drawTabBar();
for (auto &w: m_widgets) {
w->draw(m_sctx);
}
for (auto p: m_popups) {
p->draw(m_sctx);
}
}
ImGui::End();
handleKeyInput();
m_taskRunner.update(m_ctx);
}
void StudioUI::drawMenu() noexcept {
if (ImGui::BeginMainMenuBar()) {
if (ImGui::BeginMenu("File")) {
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<FileDialogManager>(this, &StudioUI::openProjectPath));
}
if (ImGui::MenuItem("Save", "Ctrl+S", false, m_activeEditor && m_activeEditor->unsavedChanges())) {
m_activeEditor->save();
}
if (ImGui::MenuItem("Quit", "Ctrl+Q")) {
turbine::requestShutdown(m_ctx);
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Edit")) {
auto undoStack = m_activeEditor ? m_activeEditor->undoStack() : nullptr;
if (ImGui::MenuItem("Undo", "Ctrl+Z", false, undoStack && undoStack->canUndo())) {
oxLogError(undoStack->undo());
}
if (ImGui::MenuItem("Redo", "Ctrl+Y", false, undoStack && undoStack->canRedo())) {
oxLogError(undoStack->redo());
}
ImGui::Separator();
if (ImGui::MenuItem("Copy", "Ctrl+C", false, m_activeEditor && m_activeEditor->copyEnabled())) {
m_activeEditor->copy();
}
if (ImGui::MenuItem("Cut", "Ctrl+X", false, m_activeEditor && m_activeEditor->cutEnabled())) {
m_activeEditor->cut();
}
if (ImGui::MenuItem("Paste", "Ctrl+V", false, m_activeEditor && m_activeEditor->pasteEnabled())) {
m_activeEditor->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_aboutPopup.open();
}
ImGui::EndMenu();
}
ImGui::EndMainMenuBar();
}
}
void StudioUI::drawTabBar() noexcept {
auto const viewport = ImGui::GetContentRegionAvail();
ImGui::BeginChild("TabWindow##MainWindow##Studio", ImVec2(viewport.x, viewport.y));
constexpr auto tabBarFlags = ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_TabListPopupButton;
if (ImGui::BeginTabBar("TabBar##TabWindow##MainWindow##Studio", tabBarFlags)) {
drawTabs();
ImGui::EndTabBar();
}
ImGui::EndChild();
}
void StudioUI::drawTabs() noexcept {
for (auto it = m_editors.begin(); it != m_editors.end();) {
auto const &e = *it;
auto open = true;
auto const unsavedChanges = e->unsavedChanges() ? ImGuiTabItemFlags_UnsavedDocument : 0;
auto const selected = m_activeEditorUpdatePending == e.get() ? ImGuiTabItemFlags_SetSelected : 0;
auto const flags = unsavedChanges | selected;
if (ImGui::BeginTabItem(e->itemDisplayName().c_str(), &open, flags)) {
if (m_activeEditor != e.get()) {
m_activeEditor = e.get();
studio::editConfig<StudioConfig>(keelCtx(m_ctx), [&](StudioConfig &config) {
config.activeTabItemName = m_activeEditor->itemPath();
});
}
if (m_activeEditorUpdatePending == e.get()) {
m_activeEditorUpdatePending = nullptr;
}
if (m_activeEditorOnLastDraw != e.get()) [[unlikely]] {
m_activeEditor->onActivated();
}
e->draw(m_sctx);
m_activeEditorOnLastDraw = e.get();
ImGui::EndTabItem();
}
if (!open) {
e->close();
if (m_activeEditor == (*it).get()) {
m_activeEditor = nullptr;
}
try {
oxThrowError(m_editors.erase(it).moveTo(it));
} catch (ox::Exception const&ex) {
oxErrf("Editor tab deletion failed: {} ({}:{})\n", ex.what(), ex.file, ex.line);
} catch (std::exception const&ex) {
oxErrf("Editor tab deletion failed: {}\n", ex.what());
}
} else {
++it;
}
}
}
void StudioUI::loadEditorMaker(studio::EditorMaker const&editorMaker) noexcept {
for (auto const&ext : editorMaker.fileTypes) {
m_editorMakers[ext] = editorMaker.make;
}
}
void StudioUI::loadModule(studio::Module const*mod) noexcept {
for (auto const&editorMaker : mod->editors(m_sctx)) {
loadEditorMaker(editorMaker);
}
for (auto &im : mod->itemMakers(m_sctx)) {
m_newMenu.addItemMaker(std::move(im));
}
}
void StudioUI::loadModules() noexcept {
for (auto const mod : modules) {
loadModule(mod);
}
}
void StudioUI::toggleProjectExplorer() noexcept {
m_showProjectExplorer = !m_showProjectExplorer;
studio::editConfig<StudioConfig>(keelCtx(m_ctx), [&](StudioConfig &config) {
config.showProjectExplorer = m_showProjectExplorer;
});
}
void StudioUI::redo() noexcept {
auto undoStack = m_activeEditor ? m_activeEditor->undoStack() : nullptr;
if (undoStack && undoStack->canRedo()) {
oxLogError(m_activeEditor->undoStack()->redo());
}
}
void StudioUI::undo() noexcept {
auto undoStack = m_activeEditor ? m_activeEditor->undoStack() : nullptr;
if (undoStack && undoStack->canUndo()) {
oxLogError(m_activeEditor->undoStack()->undo());
}
}
void StudioUI::save() noexcept {
if (m_activeEditor && m_activeEditor->unsavedChanges()) {
m_activeEditor->save();
}
}
void StudioUI::handleKeyInput() noexcept {
if (ImGui::IsKeyDown(ImGuiKey_ModCtrl)) {
if (ImGui::IsKeyPressed(ImGuiKey_C)) {
if (m_activeEditor && m_activeEditor->copyEnabled()) {
m_activeEditor->copy();
}
} else if (ImGui::IsKeyPressed(ImGuiKey_N)) {
if (turbine::buttonDown(m_ctx, turbine::Key::Mod_Shift)) {
m_newProject.open();
} else {
m_newMenu.open();
}
} else if (ImGui::IsKeyPressed(ImGuiKey_O)) {
m_taskRunner.add(*ox::make<FileDialogManager>(this, &StudioUI::openProjectPath));
} else if (ImGui::IsKeyPressed(ImGuiKey_S)) {
save();
} else if (ImGui::IsKeyPressed(ImGuiKey_Q)) {
turbine::requestShutdown(m_ctx);
} else if (ImGui::IsKeyPressed(ImGuiKey_V)) {
if (m_activeEditor && m_activeEditor->pasteEnabled()) {
m_activeEditor->paste();
}
} else if (ImGui::IsKeyPressed(ImGuiKey_X)) {
if (m_activeEditor && m_activeEditor->cutEnabled()) {
m_activeEditor->cut();
}
} else if (ImGui::IsKeyPressed(ImGuiKey_Y)) {
auto const undoStack = m_activeEditor ? m_activeEditor->undoStack() : nullptr;
if (undoStack) { oxLogError(undoStack->redo()); }
} else if (ImGui::IsKeyPressed(ImGuiKey_Z)) {
auto const undoStack = m_activeEditor ? m_activeEditor->undoStack() : nullptr;
if (undoStack) { oxLogError(undoStack->undo()); }
} else if (ImGui::IsKeyPressed(ImGuiKey_1)) {
toggleProjectExplorer();
}
}
}
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->writeTypeStore();
}
ox::Error StudioUI::openProjectPath(ox::StringParam path) noexcept {
oxRequireM(fs, keel::loadRomFs(path.view()));
oxReturnError(keel::setRomFs(keelCtx(m_ctx), std::move(fs)));
oxReturnError(
ox::make_unique_catch<studio::Project>(keelCtx(m_ctx), std::move(path), m_projectDataDir)
.moveTo(m_project));
auto const sctx = applicationData<studio::StudioContext>(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<StudioConfig>(keelCtx(m_ctx), [&](StudioConfig &config) {
config.projectPath = ox::String(m_project->projectPath());
config.openFiles.clear();
});
return m_projectExplorer.refreshProjectTreeModel();
}
ox::Error StudioUI::openFile(ox::CRStringView path) noexcept {
return openFileActiveTab(path, true);
}
ox::Error StudioUI::openFileActiveTab(ox::CRStringView path, bool makeActiveTab) noexcept {
if (!m_project) {
return OxError(1, "No project open to open a file from");
}
if (m_openFiles.contains(path)) {
for (auto &e : m_editors) {
if (makeActiveTab && e->itemPath() == path) {
m_activeEditor = e.get();
m_activeEditorUpdatePending = e.get();
break;
}
}
return {};
}
oxRequire(ext, studio::fileExt(path));
// create Editor
studio::BaseEditor *editor = nullptr;
if (!m_editorMakers.contains(ext)) {
auto [obj, err] = m_project->loadObj<ox::ModelObject>(path);
if (err) {
return OxError(1, "There is no editor for this file extension");
}
editor = ox::make<ClawEditor>(path, std::move(obj));
} else {
auto const err = m_editorMakers[ext](path).moveTo(editor);
if (err) {
if constexpr(!ox::defines::Debug) {
oxErrf("Could not open Editor: {}\n", toStr(err));
} else {
oxErrf("Could not open Editor: {} ({}:{})\n", err.errCode, err.file, err.line);
}
return err;
}
}
editor->closed.connect(this, &StudioUI::closeFile);
m_editors.emplace_back(editor);
m_openFiles.emplace_back(path);
if (makeActiveTab) {
m_activeEditor = m_editors.back().value->get();
m_activeEditorUpdatePending = editor;
}
// save to config
studio::editConfig<StudioConfig>(keelCtx(m_ctx), [&path](StudioConfig &config) {
if (!config.openFiles.contains(path)) {
config.openFiles.emplace_back(path);
}
});
return {};
}
ox::Error StudioUI::closeFile(ox::CRStringView path) noexcept {
if (!m_openFiles.contains(path)) {
return {};
}
std::ignore = m_openFiles.erase(std::remove(m_openFiles.begin(), m_openFiles.end(), path));
// save to config
studio::editConfig<StudioConfig>(keelCtx(m_ctx), [&](StudioConfig &config) {
std::ignore = config.openFiles.erase(std::remove(config.openFiles.begin(), config.openFiles.end(), path));
});
return {};
}
}