338 lines
9.8 KiB
C++
338 lines
9.8 KiB
C++
/*
|
|
* Copyright 2016 - 2022 Gary Talent (gary@drinkingtea.net). All rights reserved.
|
|
*/
|
|
|
|
#include <imgui.h>
|
|
|
|
#include <nostalgia/core/core.hpp>
|
|
|
|
#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<ox::String> 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<StudioConfig>(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<StudioConfig>(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<studio::Project>(m_ctx->rom.get(), path);
|
|
auto sctx = applicationData<studio::StudioContext>(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<StudioConfig>(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<StudioConfig>(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<StudioConfig>(m_ctx, [&](StudioConfig *config) {
|
|
oxIgnoreError(config->openFiles.erase(std::remove(config->openFiles.begin(), config->openFiles.end(), path)));
|
|
});
|
|
return OxError(0);
|
|
}
|
|
|
|
}
|