[olympic] Move keel, turbine, and studio to olympic

This commit is contained in:
2023-12-11 22:48:08 -06:00
parent a60765b338
commit e2545a956b
96 changed files with 32 additions and 24 deletions

View File

@ -0,0 +1,2 @@
add_subdirectory(applib)
add_subdirectory(modlib)

View File

@ -0,0 +1 @@
add_subdirectory(src)

View File

@ -0,0 +1,15 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/string.hpp>
#include <studio/module.hpp>
namespace studio {
void registerModule(const studio::Module*) noexcept;
}

View File

@ -0,0 +1,42 @@
add_library(
StudioAppLib
aboutpopup.cpp
clawviewer.cpp
filedialogmanager.cpp
main.cpp
newmenu.cpp
projectexplorer.cpp
projecttreemodel.cpp
studioapp.cpp
)
target_compile_definitions(
StudioAppLib PUBLIC
OLYMPIC_LOAD_STUDIO_MODULES=1
OLYMPIC_APP_NAME="Studio"
)
target_link_libraries(
StudioAppLib PUBLIC
OxClArgs
OxLogConn
Studio
)
target_include_directories(
StudioAppLib PUBLIC
../include
)
install(
DIRECTORY
../include/studioapp
DESTINATION
include
)
install(
TARGETS
StudioAppLib
DESTINATION
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)

View File

@ -0,0 +1,61 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <imgui.h>
#include <studio/imguiuitl.hpp>
#include "aboutpopup.hpp"
namespace studio {
AboutPopup::AboutPopup(turbine::Context &ctx) noexcept {
m_text = ox::sfmt("{} - dev build", keelCtx(ctx).appName);
}
void AboutPopup::open() noexcept {
m_stage = Stage::Opening;
}
void AboutPopup::close() noexcept {
m_stage = Stage::Closed;
}
bool AboutPopup::isOpen() const noexcept {
return m_stage == Stage::Open;
}
void AboutPopup::draw(turbine::Context &ctx) noexcept {
switch (m_stage) {
case Stage::Closed:
break;
case Stage::Opening:
ImGui::OpenPopup("About");
m_stage = Stage::Open;
[[fallthrough]];
case Stage::Open: {
constexpr auto modalFlags =
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize;
ImGui::SetNextWindowSize(ImVec2(215, 90));
studio::ig::centerNextWindow(ctx);
auto open = true;
if (ImGui::BeginPopupModal("About", &open, modalFlags)) {
ImGui::Text("%s", m_text.c_str());
ImGui::NewLine();
ImGui::Dummy(ImVec2(148.0f, 0.0f));
ImGui::SameLine();
if (ImGui::Button("Close")) {
ImGui::CloseCurrentPopup();
open = false;
}
ImGui::EndPopup();
}
if (!open) {
m_stage = Stage::Closed;
}
break;
}
}
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/event/signal.hpp>
#include <ox/std/string.hpp>
#include <turbine/context.hpp>
#include <studio/popup.hpp>
namespace studio {
class AboutPopup: public studio::Popup {
public:
enum class Stage {
Closed,
Opening,
Open,
};
private:
Stage m_stage = Stage::Closed;
ox::String m_text;
public:
explicit AboutPopup(turbine::Context &ctx) noexcept;
void open() noexcept override;
void close() noexcept override;
[[nodiscard]]
bool isOpen() const noexcept override;
void draw(turbine::Context &ctx) noexcept override;
};
}

View File

@ -0,0 +1,186 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <imgui.h>
#include "clawviewer.hpp"
namespace studio {
ClawEditor::ClawEditor(ox::CRStringView path, ox::ModelObject obj) noexcept:
m_itemName(path),
m_itemDisplayName(pathToItemName(path)),
m_obj(std::move(obj)) {
}
ox::CStringView ClawEditor::itemName() const noexcept {
return m_itemName;
}
ox::CStringView ClawEditor::itemDisplayName() const noexcept {
return m_itemDisplayName;
}
void ClawEditor::draw(turbine::Context&) noexcept {
//const auto paneSize = ImGui::GetContentRegionAvail();
ImGui::BeginChild("PaletteEditor");
static constexpr auto flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_NoBordersInBody;
if (ImGui::BeginTable("ObjTree", 3, flags)) {
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, 100);
ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 250);
ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_NoHide);
ImGui::TableHeadersRow();
ObjPath objPath;
drawTree(&objPath, m_obj);
ImGui::EndTable();
}
ImGui::EndChild();
}
void ClawEditor::drawRow(const ox::ModelValue &value) noexcept {
using Str = ox::BasicString<100>;
Str val, type;
switch (value.type()) {
case ox::ModelValue::Type::Undefined:
val = "undefined";
type = "undefined";
break;
case ox::ModelValue::Type::Bool:
val = value.get<bool>() ? "true" : "false";
type = "bool";
break;
case ox::ModelValue::Type::UnsignedInteger8:
val = ox::sfmt<Str>("{}", value.get<uint8_t>());
type = "uint8";
break;
case ox::ModelValue::Type::UnsignedInteger16:
val = ox::sfmt<Str>("{}", value.get<uint16_t>());
type = "uint16";
break;
case ox::ModelValue::Type::UnsignedInteger32:
val = ox::sfmt<Str>("{}", value.get<uint32_t>());
type = "uint32";
break;
case ox::ModelValue::Type::UnsignedInteger64:
val = ox::sfmt<Str>("{}", value.get<uint64_t>());
type = "uint64";
break;
case ox::ModelValue::Type::SignedInteger8:
val = ox::sfmt<Str>("{}", value.get<int8_t>());
type = "int8";
break;
case ox::ModelValue::Type::SignedInteger16:
val = ox::sfmt<Str>("{}", value.get<int16_t>());
type = "int16";
break;
case ox::ModelValue::Type::SignedInteger32:
val = ox::sfmt<Str>("{}", value.get<int32_t>());
type = "int32";
break;
case ox::ModelValue::Type::SignedInteger64:
val = ox::sfmt<Str>("{}", value.get<int64_t>());
type = "int64";
break;
case ox::ModelValue::Type::String:
val = ox::sfmt<Str>("\"{}\"", value.get<ox::String>());
type = "string";
break;
case ox::ModelValue::Type::Object:
type = value.get<ox::ModelObject>().type()->typeName.c_str();
break;
case ox::ModelValue::Type::Union:
type = "union";
break;
case ox::ModelValue::Type::Vector:
type = "list";
break;
}
ImGui::TableNextColumn();
ImGui::Text("%s", type.c_str());
ImGui::TableNextColumn();
ImGui::Text("%s", val.c_str());
}
void ClawEditor::drawVar(ObjPath *path, ox::CRStringView name, const ox::ModelValue &value) noexcept {
using Str = ox::BasicString<100>;
path->push_back(name);
if (value.type() == ox::ModelValue::Type::Object) {
drawTree(path, value.get<ox::ModelObject>());
} else if (value.type() == ox::ModelValue::Type::Vector) {
const auto &vec = value.get<ox::ModelValueVector>();
const auto pathStr = ox::join<Str>("##", *path).unwrap();
const auto lbl = ox::sfmt<Str>("{}##{}", name, pathStr);
const auto flags = ImGuiTreeNodeFlags_SpanFullWidth
| ImGuiTreeNodeFlags_OpenOnArrow
| (vec.size() ? 0 : ImGuiTreeNodeFlags_Leaf)
| (false ? ImGuiTreeNodeFlags_Selected : 0);
const auto open = ImGui::TreeNodeEx(lbl.c_str(), flags);
ImGui::SameLine();
drawRow(value);
if (open) {
for (auto i = 0lu; const auto &e: vec) {
const auto iStr = ox::sfmt<Str>("{}", i);
path->push_back(iStr);
ImGui::TableNextRow(0, 5);
ImGui::TableNextColumn();
drawVar(path, ox::sfmt<Str>("[{}]", i), e);
path->pop_back();
++i;
}
ImGui::TreePop();
}
} else {
const auto pathStr = ox::join<Str>("##", *path).unwrap();
const auto lbl = ox::sfmt<Str>("{}##{}", name, pathStr);
const auto flags = ImGuiTreeNodeFlags_SpanFullWidth
| ImGuiTreeNodeFlags_OpenOnArrow
| ImGuiTreeNodeFlags_Leaf
| (false ? ImGuiTreeNodeFlags_Selected : 0);
const auto open = ImGui::TreeNodeEx(lbl.c_str(), flags);
ImGui::SameLine();
drawRow(value);
if (open) {
ImGui::TreePop();
}
}
path->pop_back();
}
void ClawEditor::drawTree(ObjPath *path, const ox::ModelObject &obj) noexcept {
using Str = ox::BasicString<100>;
for (const auto &c : obj) {
ImGui::TableNextRow(0, 5);
auto pathStr = ox::join<Str>("##", *path).unwrap();
auto lbl = ox::sfmt<Str>("{}##{}", c->name, pathStr);
const auto rowSelected = false;
const auto hasChildren = c->value.type() == ox::ModelValue::Type::Object
|| c->value.type() == ox::ModelValue::Type::Vector;
const auto flags = ImGuiTreeNodeFlags_SpanFullWidth
| ImGuiTreeNodeFlags_OpenOnArrow
| (hasChildren ? 0 : ImGuiTreeNodeFlags_Leaf)
| (rowSelected ? ImGuiTreeNodeFlags_Selected : 0);
ImGui::TableNextColumn();
if (ImGui::IsItemClicked()) {
//model()->setActiveSubsheet(*path);
}
if (ImGui::IsMouseDoubleClicked(0) && ImGui::IsItemHovered()) {
//showSubsheetEditor();
}
path->push_back(c->name);
if (c->value.type() == ox::ModelValue::Type::Object) {
const auto open = ImGui::TreeNodeEx(lbl.c_str(), flags);
ImGui::SameLine();
drawRow(c->value);
if (open) {
drawTree(path, c->value.get<ox::ModelObject>());
ImGui::TreePop();
}
} else {
drawVar(path, c->name, c->value);
}
path->pop_back();
}
}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/model/modelvalue.hpp>
#include <turbine/context.hpp>
#include <studio/editor.hpp>
namespace studio {
class ClawEditor: public studio::Editor {
private:
using ObjPath = ox::Vector<ox::StringView, 8>;
ox::String m_itemName;
ox::String m_itemDisplayName;
ox::ModelObject m_obj;
public:
ClawEditor(ox::CRStringView path, ox::ModelObject obj) noexcept;
/**
* Returns the name of item being edited.
*/
ox::CStringView itemName() const noexcept final;
ox::CStringView itemDisplayName() const noexcept final;
void draw(turbine::Context&) noexcept final;
private:
static void drawRow(const ox::ModelValue &value) noexcept;
void drawVar(ObjPath *path, ox::CRStringView name, const ox::ModelValue &value) noexcept;
void drawTree(ObjPath *path, const ox::ModelObject &obj) noexcept;
};
}

View File

@ -0,0 +1,37 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <turbine/gfx.hpp>
#include "filedialogmanager.hpp"
namespace studio {
studio::TaskState FileDialogManager::update(turbine::Context &ctx) noexcept {
switch (m_state) {
case UpdateProjectPathState::EnableSystemCursor: {
// switch to system cursor in this update and open file dialog in the next
// the cursor state has to be set before the ImGui frame starts
m_state = UpdateProjectPathState::RunFileDialog;
break;
}
case UpdateProjectPathState::RunFileDialog: {
// switch to system cursor
auto [path, err] = studio::chooseDirectory();
// Mac file dialog doesn't restore focus to main window when closed...
turbine::focusWindow(ctx);
if (!err) {
err = pathChosen.emitCheckError(path);
oxAssert(err, "Path chosen response failed");
}
m_state = UpdateProjectPathState::None;
return studio::TaskState::Done;
}
case UpdateProjectPathState::None:
break;
}
return studio::TaskState::Running;
}
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/event/signal.hpp>
#include <ox/std/string.hpp>
#include <turbine/context.hpp>
#include <studio/filedialog.hpp>
#include <studio/task.hpp>
namespace studio {
class FileDialogManager : public studio::Task {
private:
enum class UpdateProjectPathState {
None,
EnableSystemCursor,
RunFileDialog,
Start = EnableSystemCursor,
} m_state = UpdateProjectPathState::Start;
public:
FileDialogManager() noexcept = default;
template<class... Args>
explicit FileDialogManager(Args ...args) noexcept {
pathChosen.connect(args...);
}
~FileDialogManager() noexcept override = default;
studio::TaskState update(turbine::Context &ctx) noexcept final;
// signals
ox::Signal<ox::Error(const ox::String &)> pathChosen;
};
}

View File

@ -0,0 +1,95 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ctime>
#include <ox/logconn/logconn.hpp>
#include <ox/logconn/def.hpp>
#include <ox/std/trace.hpp>
#include <ox/std/uuid.hpp>
#include <keel/media.hpp>
#include <turbine/turbine.hpp>
#include <studio/context.hpp>
#include <studioapp/studioapp.hpp>
#include "studioapp.hpp"
namespace studio {
class StudioUIDrawer: public turbine::gl::Drawer {
private:
StudioUI &m_ui;
public:
explicit StudioUIDrawer(StudioUI &ui) noexcept: m_ui(ui) {
}
protected:
void draw(turbine::Context&) noexcept final {
m_ui.draw();
}
};
static int updateHandler(turbine::Context &ctx) noexcept {
auto sctx = turbine::applicationData<studio::StudioContext>(ctx);
auto ui = dynamic_cast<StudioUI*>(sctx->ui);
ui->update();
return 16;
}
static void keyEventHandler(turbine::Context &ctx, turbine::Key key, bool down) noexcept {
auto sctx = turbine::applicationData<studio::StudioContext>(ctx);
auto ui = dynamic_cast<StudioUI*>(sctx->ui);
ui->handleKeyEvent(key, down);
}
static ox::Error runApp(
ox::CRStringView appName,
ox::CRStringView projectDataDir,
ox::UniquePtr<ox::FileSystem> fs) noexcept {
oxRequireM(ctx, turbine::init(std::move(fs), appName));
turbine::setWindowTitle(*ctx, keelCtx(*ctx).appName);
turbine::setUpdateHandler(*ctx, updateHandler);
turbine::setKeyEventHandler(*ctx, keyEventHandler);
turbine::setConstantRefresh(*ctx, false);
studio::StudioContext studioCtx;
turbine::setApplicationData(*ctx, &studioCtx);
StudioUI ui(ctx.get(), projectDataDir);
studioCtx.ui = &ui;
StudioUIDrawer drawer(ui);
turbine::gl::addDrawer(*ctx, &drawer);
const auto err = turbine::run(*ctx);
turbine::gl::removeDrawer(*ctx, &drawer);
return err;
}
ox::Error run(
ox::CRStringView appName,
ox::CRStringView projectDataDir,
int,
const char**) {
// seed UUID generator
const auto time = std::time(nullptr);
ox::UUID::seedGenerator({
static_cast<uint64_t>(time),
static_cast<uint64_t>(time << 1)
});
// run app
const auto err = runApp(appName, projectDataDir, ox::UniquePtr<ox::FileSystem>(nullptr));
oxAssert(err, "Something went wrong...");
return err;
}
}
namespace olympic {
ox::Error run(
ox::StringView project,
ox::StringView appName,
ox::StringView projectDataDir,
int argc,
const char **argv) noexcept {
return studio::run(ox::sfmt("{} {}", project, appName), projectDataDir, argc, argv);
}
}

View File

@ -0,0 +1,125 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <algorithm>
#include <imgui.h>
#include <studio/imguiuitl.hpp>
#include "newmenu.hpp"
namespace studio {
NewMenu::NewMenu() noexcept {
setTitle(ox::String("New Item"));
setSize({230, 140});
}
void NewMenu::open() noexcept {
m_stage = Stage::Opening;
m_selectedType = 0;
}
void NewMenu::close() noexcept {
m_stage = Stage::Closed;
m_open = false;
}
bool NewMenu::isOpen() const noexcept {
return m_open;
}
void NewMenu::draw(turbine::Context &ctx) noexcept {
switch (m_stage) {
case Stage::Opening:
ImGui::OpenPopup(title().c_str());
m_stage = Stage::NewItemType;
m_open = true;
[[fallthrough]];
case Stage::NewItemType:
drawNewItemType(ctx);
break;
case Stage::NewItemName:
drawNewItemName(ctx);
break;
case Stage::Closed:
m_open = false;
break;
}
}
void NewMenu::addItemMaker(ox::UniquePtr<studio::ItemMaker> im) noexcept {
m_types.emplace_back(std::move(im));
std::sort(
m_types.begin(), m_types.end(),
[](ox::UPtr<ItemMaker> const&im1, ox::UPtr<ItemMaker> const&im2) {
return im1->name < im2->name;
});
}
void NewMenu::drawNewItemType(turbine::Context &ctx) noexcept {
drawWindow(ctx, &m_open, [this] {
auto items = ox_malloca(m_types.size() * sizeof(const char*), const char*, nullptr);
for (auto i = 0u; const auto &im : m_types) {
items.get()[i] = im->name.c_str();
++i;
}
ImGui::ListBox("Item Type", &m_selectedType, items.get(), static_cast<int>(m_types.size()));
drawFirstPageButtons();
});
}
void NewMenu::drawNewItemName(turbine::Context &ctx) noexcept {
drawWindow(ctx, &m_open, [this, &ctx] {
const auto typeIdx = static_cast<std::size_t>(m_selectedType);
if (typeIdx < m_types.size()) {
ImGui::InputText("Name", m_itemName.data(), m_itemName.cap());
}
drawLastPageButtons(ctx);
});
}
void NewMenu::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 NewMenu::drawLastPageButtons(turbine::Context &ctx) noexcept {
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - 138);
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ImGui::GetContentRegionAvail().y - 20);
if (ImGui::Button("Back")) {
m_stage = Stage::NewItemType;
}
ImGui::SameLine();
if (ImGui::Button("Finish")) {
finish(ctx);
}
ImGui::SameLine();
if (ImGui::Button("Quit")) {
ImGui::CloseCurrentPopup();
m_stage = Stage::Closed;
}
}
void NewMenu::finish(turbine::Context &ctx) noexcept {
const auto err = m_types[static_cast<std::size_t>(m_selectedType)]->write(ctx, m_itemName);
if (err) {
oxLogError(err);
return;
}
finished.emit("");
m_stage = Stage::Closed;
}
}

View File

@ -0,0 +1,79 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/claw/claw.hpp>
#include <ox/event/signal.hpp>
#include <ox/std/string.hpp>
#include <studio/itemmaker.hpp>
#include <studio/popup.hpp>
namespace studio {
class NewMenu: public studio::Popup {
public:
enum class Stage {
Closed,
Opening,
NewItemType,
NewItemName,
};
// emits path parameter
ox::Signal<ox::Error(ox::StringView)> finished;
private:
Stage m_stage = Stage::Closed;
ox::String m_typeName;
ox::BString<255> m_itemName;
ox::Vector<ox::UniquePtr<studio::ItemMaker>> m_types;
int m_selectedType = 0;
bool m_open = false;
public:
NewMenu() noexcept;
void open() noexcept override;
void close() noexcept override;
[[nodiscard]]
bool isOpen() const noexcept override;
void draw(turbine::Context &ctx) noexcept override;
template<typename T>
void addItemType(ox::String name, ox::String parentDir, ox::String fileExt, T itemTempl, ox::ClawFormat pFmt = ox::ClawFormat::Metal) noexcept;
template<typename T>
void addItemType(ox::String name, ox::String parentDir, ox::String fileExt, ox::ClawFormat pFmt = ox::ClawFormat::Metal) noexcept;
void addItemMaker(ox::UniquePtr<studio::ItemMaker> im) noexcept;
private:
void drawNewItemType(turbine::Context &ctx) noexcept;
void drawNewItemName(turbine::Context &ctx) noexcept;
void drawFirstPageButtons() noexcept;
void drawLastPageButtons(turbine::Context &ctx) noexcept;
void finish(turbine::Context &ctx) noexcept;
};
template<typename T>
void NewMenu::addItemType(ox::String displayName, ox::String parentDir, ox::String fileExt, T itemTempl, ox::ClawFormat pFmt) noexcept {
m_types.emplace_back(ox::make<studio::ItemMakerT<T>>(std::move(displayName), std::move(parentDir), std::move(fileExt), std::move(itemTempl), pFmt));
}
template<typename T>
void NewMenu::addItemType(ox::String displayName, ox::String parentDir, ox::String fileExt, ox::ClawFormat pFmt) noexcept {
m_types.emplace_back(ox::make<studio::ItemMakerT<T>>(std::move(displayName), std::move(parentDir), std::move(fileExt), pFmt));
}
}

View File

@ -0,0 +1,57 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <algorithm>
#include <imgui.h>
#include "projectexplorer.hpp"
namespace studio {
static ox::Result<ox::UniquePtr<ProjectTreeModel>>
buildProjectTreeModel(ProjectExplorer *explorer, ox::StringView name, ox::CRStringView path, ProjectTreeModel *parent) noexcept {
const auto fs = explorer->romFs();
oxRequire(stat, fs->stat(path));
auto out = ox::make_unique<ProjectTreeModel>(explorer, ox::String(name), parent);
if (stat.fileType == ox::FileType::Directory) {
oxRequireM(children, fs->ls(path));
std::sort(children.begin(), children.end());
ox::Vector<ox::UniquePtr<ProjectTreeModel>> 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(turbine::Context &ctx) noexcept: m_ctx(ctx) {
}
void ProjectExplorer::draw(turbine::Context &ctx) noexcept {
const auto viewport = ImGui::GetContentRegionAvail();
ImGui::BeginChild("ProjectExplorer", ImVec2(300, viewport.y), true);
ImGui::SetNextItemOpen(true);
if (m_treeModel) {
m_treeModel->draw(ctx);
}
ImGui::EndChild();
}
void ProjectExplorer::setModel(ox::UniquePtr<ProjectTreeModel> model) noexcept {
m_treeModel = std::move(model);
}
ox::Error ProjectExplorer::refreshProjectTreeModel(ox::CRStringView) noexcept {
oxRequireM(model, buildProjectTreeModel(this, "Project", "/", nullptr));
setModel(std::move(model));
return OxError(0);
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/event/signal.hpp>
#include <ox/std/memory.hpp>
#include <studio/widget.hpp>
#include "projecttreemodel.hpp"
namespace studio {
class ProjectExplorer: public studio::Widget {
private:
ox::UniquePtr<ProjectTreeModel> m_treeModel;
turbine::Context &m_ctx;
public:
explicit ProjectExplorer(turbine::Context &ctx) noexcept;
void draw(turbine::Context &ctx) noexcept override;
void setModel(ox::UniquePtr<ProjectTreeModel> model) noexcept;
ox::Error refreshProjectTreeModel(ox::CRStringView = {}) noexcept;
[[nodiscard]]
inline ox::FileSystem *romFs() noexcept {
return rom(m_ctx);
}
// slots
public:
ox::Signal<ox::Error(const ox::StringView&)> fileChosen;
};
}

View File

@ -0,0 +1,58 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <imgui.h>
#include "projectexplorer.hpp"
#include "projecttreemodel.hpp"
namespace studio {
ProjectTreeModel::ProjectTreeModel(ProjectExplorer *explorer, ox::String name,
ProjectTreeModel *parent) noexcept:
m_explorer(explorer),
m_parent(parent),
m_name(std::move(name)) {
}
ProjectTreeModel::ProjectTreeModel(ProjectTreeModel &&other) noexcept:
m_explorer(other.m_explorer),
m_parent(other.m_parent),
m_name(std::move(other.m_name)),
m_children(std::move(other.m_children)) {
}
void ProjectTreeModel::draw(turbine::Context &ctx) const noexcept {
constexpr auto dirFlags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick;
if (!m_children.empty()) {
if (ImGui::TreeNodeEx(m_name.c_str(), dirFlags)) {
for (const auto &child : m_children) {
child->draw(ctx);
}
ImGui::TreePop();
}
} else {
const auto path = fullPath();
const auto name = ox::sfmt<ox::BasicString<255>>("{}##{}", m_name, path);
if (ImGui::TreeNodeEx(name.c_str(), ImGuiTreeNodeFlags_Leaf)) {
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) {
m_explorer->fileChosen.emit(path);
}
ImGui::TreePop();
}
}
}
void ProjectTreeModel::setChildren(ox::Vector<ox::UniquePtr<ProjectTreeModel>> children) noexcept {
m_children = std::move(children);
}
ox::BasicString<255> ProjectTreeModel::fullPath() const noexcept {
if (m_parent) {
return m_parent->fullPath() + "/" + ox::StringView(m_name);
}
return {};
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/memory.hpp>
#include <ox/std/string.hpp>
#include <turbine/context.hpp>
namespace studio {
class ProjectTreeModel {
private:
class ProjectExplorer *m_explorer = nullptr;
ProjectTreeModel *m_parent = nullptr;
ox::String m_name;
ox::Vector<ox::UniquePtr<ProjectTreeModel>> m_children;
public:
explicit ProjectTreeModel(class ProjectExplorer *explorer, ox::String name,
ProjectTreeModel *parent = nullptr) noexcept;
ProjectTreeModel(ProjectTreeModel &&other) noexcept;
void draw(turbine::Context &ctx) const noexcept;
void setChildren(ox::Vector<ox::UniquePtr<ProjectTreeModel>> children) noexcept;
private:
[[nodiscard]]
ox::BasicString<255> fullPath() const noexcept;
};
}

View File

@ -0,0 +1,389 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#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 {
ox::Vector<const studio::Module*> modules;
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(active_tab_item_name, activeTabItemName)
oxModelFieldRename(project_path, projectPath)
oxModelFieldRename(open_files, openFiles)
oxModelFieldRename(show_project_explorer, showProjectExplorer)
oxModelEnd()
StudioUI::StudioUI(turbine::Context *ctx, ox::StringView projectDir) noexcept:
m_ctx(*ctx),
m_projectDir(projectDir),
m_projectExplorer(ox::make_unique<ProjectExplorer>(m_ctx)),
m_aboutPopup(*ctx) {
m_projectExplorer->fileChosen.connect(this, &StudioUI::openFile);
ImGui::GetIO().IniFilename = nullptr;
loadModules();
// open project and files
const auto [config, err] = studio::readConfig<StudioConfig>(keelCtx(*ctx));
m_showProjectExplorer = config.showProjectExplorer;
if (!err) {
oxIgnoreError(openProject(config.projectPath));
for (const auto &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::update() noexcept {
m_taskRunner.update(m_ctx);
}
void StudioUI::handleKeyEvent(turbine::Key key, bool down) noexcept {
const auto ctrlDown = turbine::buttonDown(m_ctx, turbine::Key::Mod_Ctrl);
for (auto p : m_popups) {
if (p->isOpen()) {
if (key == turbine::Key::Escape) {
p->close();
}
return;
}
}
if (down && ctrlDown) {
switch (key) {
case turbine::Key::Num_1:
toggleProjectExplorer();
break;
case turbine::Key::Alpha_C:
m_activeEditor->copy();
break;
case turbine::Key::Alpha_N:
m_newMenu.open();
break;
case turbine::Key::Alpha_O:
m_taskRunner.add(*ox::make<FileDialogManager>(this, &StudioUI::openProject));
break;
case turbine::Key::Alpha_Q:
turbine::requestShutdown(m_ctx);
break;
case turbine::Key::Alpha_S:
save();
break;
case turbine::Key::Alpha_V:
m_activeEditor->paste();
break;
case turbine::Key::Alpha_X:
m_activeEditor->cut();
break;
case turbine::Key::Alpha_Y:
redo();
break;
case turbine::Key::Alpha_Z:
undo();
break;
default:
if (m_activeEditor) {
m_activeEditor->keyStateChanged(key, down);
}
break;
}
} else if (m_activeEditor && !ctrlDown) {
m_activeEditor->keyStateChanged(key, down);
}
}
void StudioUI::draw() noexcept {
glutils::clearScreen();
drawMenu();
const auto 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_ctx);
ImGui::SameLine();
}
drawTabBar();
for (auto &w: m_widgets) {
w->draw(m_ctx);
}
for (auto p: m_popups) {
p->draw(m_ctx);
}
}
ImGui::End();
}
void StudioUI::drawMenu() noexcept {
if (ImGui::BeginMainMenuBar()) {
if (ImGui::BeginMenu("File")) {
if (ImGui::MenuItem("New...", "Ctrl+N")) {
m_newMenu.open();
}
if (ImGui::MenuItem("Open Project...", "Ctrl+O")) {
m_taskRunner.add(*ox::make<FileDialogManager>(this, &StudioUI::openProject));
}
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())) {
m_activeEditor->undoStack()->undo();
}
if (ImGui::MenuItem("Redo", "Ctrl+Y", false, undoStack && undoStack->canRedo())) {
m_activeEditor->undoStack()->redo();
}
ImGui::Separator();
if (ImGui::MenuItem("Copy", "Ctrl+C")) {
m_activeEditor->copy();
}
if (ImGui::MenuItem("Cut", "Ctrl+X")) {
m_activeEditor->cut();
}
if (ImGui::MenuItem("Paste", "Ctrl+V")) {
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 {
const auto 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;
const auto unsavedChanges = e->unsavedChanges() ? ImGuiTabItemFlags_UnsavedDocument : 0;
const auto selected = m_activeEditorUpdatePending == e.get() ? ImGuiTabItemFlags_SetSelected : 0;
const auto 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->itemName();
});
}
if (m_activeEditorUpdatePending == e.get()) {
m_activeEditorUpdatePending = nullptr;
}
if (m_activeEditorOnLastDraw != e.get()) [[unlikely]] {
m_activeEditor->onActivated();
turbine::setConstantRefresh(m_ctx, m_activeEditor->requiresConstantRefresh());
}
e->draw(m_ctx);
m_activeEditorOnLastDraw = e.get();
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::loadEditorMaker(studio::EditorMaker const&editorMaker) noexcept {
for (auto const&ext : editorMaker.fileTypes) {
m_editorMakers[ext] = editorMaker.make;
}
}
void StudioUI::loadModule(const studio::Module *mod) noexcept {
for (const auto &editorMaker : mod->editors(m_ctx)) {
loadEditorMaker(editorMaker);
}
for (auto &im : mod->itemMakers(m_ctx)) {
m_newMenu.addItemMaker(std::move(im));
}
}
void StudioUI::loadModules() noexcept {
for (auto &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()) {
m_activeEditor->undoStack()->redo();
}
}
void StudioUI::undo() noexcept {
auto undoStack = m_activeEditor ? m_activeEditor->undoStack() : nullptr;
if (undoStack && undoStack->canUndo()) {
m_activeEditor->undoStack()->undo();
}
}
void StudioUI::save() noexcept {
if (m_activeEditor && m_activeEditor->unsavedChanges()) {
m_activeEditor->save();
}
}
ox::Error StudioUI::openProject(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<studio::Project>(keelCtx(m_ctx), ox::String(path), m_projectDir);
auto sctx = applicationData<studio::StudioContext>(m_ctx);
sctx->project = m_project.get();
m_project->fileAdded.connect(m_projectExplorer.get(), &ProjectExplorer::refreshProjectTreeModel);
m_project->fileDeleted.connect(m_projectExplorer.get(), &ProjectExplorer::refreshProjectTreeModel);
m_openFiles.clear();
m_editors.clear();
studio::editConfig<StudioConfig>(keelCtx(m_ctx), [&](StudioConfig *config) {
config->projectPath = ox::String(path);
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_openFiles.contains(path)) {
for (auto &e : m_editors) {
if (makeActiveTab && e->itemName() == path) {
m_activeEditor = e.get();
m_activeEditorUpdatePending = e.get();
break;
}
}
return OxError(0);
}
oxRequire(ext, studio::fileExt(path).to<ox::String>([](auto const&v) {return ox::String(v);}));
// 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 {
const auto 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), [&](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 OxError(0);
}
oxIgnoreError(m_openFiles.erase(std::remove(m_openFiles.begin(), m_openFiles.end(), path)));
// save to config
studio::editConfig<StudioConfig>(keelCtx(m_ctx), [&](StudioConfig *config) {
oxIgnoreError(config->openFiles.erase(std::remove(config->openFiles.begin(), config->openFiles.end(), path)));
});
return OxError(0);
}
void registerModule(const studio::Module *mod) noexcept {
modules.emplace_back(mod);
}
}

View File

@ -0,0 +1,91 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/event/signal.hpp>
#include <ox/std/memory.hpp>
#include <ox/std/string.hpp>
#include <studio/editor.hpp>
#include <studio/module.hpp>
#include <studio/project.hpp>
#include <studio/task.hpp>
#include "aboutpopup.hpp"
#include "newmenu.hpp"
#include "projectexplorer.hpp"
#include "projecttreemodel.hpp"
namespace studio {
class StudioUI: public ox::SignalHandler {
friend class StudioUIDrawer;
private:
turbine::Context &m_ctx;
ox::String m_projectDir;
ox::UniquePtr<studio::Project> m_project;
studio::TaskRunner m_taskRunner;
ox::Vector<ox::UniquePtr<studio::BaseEditor>> m_editors;
ox::Vector<ox::UniquePtr<studio::Widget>> m_widgets;
ox::HashMap<ox::String, studio::EditorMaker::Func> m_editorMakers;
ox::UniquePtr<ProjectExplorer> m_projectExplorer;
ox::Vector<ox::String> m_openFiles;
studio::BaseEditor *m_activeEditorOnLastDraw = nullptr;
studio::BaseEditor *m_activeEditor = nullptr;
studio::BaseEditor *m_activeEditorUpdatePending = nullptr;
NewMenu m_newMenu;
AboutPopup m_aboutPopup;
const ox::Array<studio::Popup*, 2> m_popups = {
&m_newMenu,
&m_aboutPopup
};
bool m_showProjectExplorer = true;
public:
explicit StudioUI(turbine::Context *ctx, ox::StringView projectDir) noexcept;
void update() noexcept;
void handleKeyEvent(turbine::Key, bool down) noexcept;
[[nodiscard]]
constexpr studio::Project *project() noexcept {
return m_project.get();
}
protected:
void draw() noexcept;
private:
void drawMenu() noexcept;
void drawTabBar() noexcept;
void drawTabs() noexcept;
void loadEditorMaker(studio::EditorMaker const&editorMaker) noexcept;
void loadModule(const studio::Module *mod) noexcept;
void loadModules() noexcept;
void toggleProjectExplorer() noexcept;
void redo() noexcept;
void undo() noexcept;
void save() noexcept;
ox::Error openProject(ox::CRStringView path) noexcept;
ox::Error openFile(ox::CRStringView path) noexcept;
ox::Error openFileActiveTab(ox::CRStringView path, bool makeActiveTab) noexcept;
ox::Error closeFile(ox::CRStringView path) noexcept;
};
}

View File

@ -0,0 +1 @@
add_subdirectory(src)

View File

@ -0,0 +1,95 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/fs/fs.hpp>
#include <ox/model/typenamecatcher.hpp>
#include <ox/oc/oc.hpp>
#include <ox/std/buffer.hpp>
#include <ox/std/defines.hpp>
#include <ox/std/fmt.hpp>
#include <ox/std/trace.hpp>
#include <ox/std/string.hpp>
#include <keel/context.hpp>
namespace studio {
[[nodiscard]]
ox::String configPath(keel::Context const&ctx) noexcept;
template<typename T>
ox::Result<T> readConfig(keel::Context &ctx, ox::CRStringView name) noexcept {
oxAssert(name != "", "Config type has no TypeName");
const auto path = ox::sfmt("/{}.json", name);
ox::PassThroughFS fs(configPath(ctx));
const auto [buff, err] = fs.read(path);
if (err) {
oxErrf("Could not read config file: {}\n", toStr(err));
return err;
}
return ox::readOC<T>(buff);
}
template<typename T>
ox::Result<T> readConfig(keel::Context &ctx) noexcept {
constexpr auto TypeName = ox::requireModelTypeName<T>();
return readConfig<T>(ctx, TypeName);
}
template<typename T>
ox::Error writeConfig(keel::Context &ctx, ox::CRStringView name, T *data) noexcept {
oxAssert(name != "", "Config type has no TypeName");
const auto path = ox::sfmt("/{}.json", name);
ox::PassThroughFS fs(configPath(ctx));
if (const auto err = fs.mkdir("/", true)) {
oxErrf("Could not create config directory: {}\n", toStr(err));
return err;
}
oxRequireM(buff, ox::writeOC(*data));
*buff.back().value = '\n';
if (const auto err = fs.write(path, buff.data(), buff.size())) {
oxErrf("Could not read config file: {}\n", toStr(err));
return OxError(2, "Could not read config file");
}
return OxError(0);
}
template<typename T>
ox::Error writeConfig(keel::Context &ctx, T *data) noexcept {
constexpr auto TypeName = ox::requireModelTypeName<T>();
return writeConfig(ctx, TypeName, data);
}
template<typename T, typename Func>
void openConfig(keel::Context &ctx, ox::CRStringView name, Func f) noexcept {
oxAssert(name != "", "Config type has no TypeName");
const auto [c, err] = readConfig<T>(ctx, name);
oxLogError(err);
f(&c);
}
template<typename T, typename Func>
void openConfig(keel::Context &ctx, Func f) noexcept {
constexpr auto TypeName = ox::requireModelTypeName<T>();
openConfig<T>(ctx, TypeName, f);
}
template<typename T, typename Func>
void editConfig(keel::Context &ctx, ox::CRStringView name, Func f) noexcept {
oxAssert(name != "", "Config type has no TypeName");
auto [c, err] = readConfig<T>(ctx, name);
oxLogError(err);
f(&c);
oxLogError(writeConfig(ctx, name, &c));
}
template<typename T, typename Func>
void editConfig(keel::Context &ctx, Func f) noexcept {
constexpr auto TypeName = ox::requireModelTypeName<T>();
editConfig<T>(ctx, TypeName, f);
}
}

View File

@ -0,0 +1,18 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/event/signal.hpp>
#include "project.hpp"
namespace studio {
struct StudioContext {
ox::SignalHandler *ui = nullptr;
Project *project = nullptr;
};
}

View File

@ -0,0 +1,136 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/event/signal.hpp>
#include "undostack.hpp"
#include "widget.hpp"
namespace studio {
class StudioUI;
class BaseEditor: public Widget {
friend StudioUI;
private:
bool m_unsavedChanges = false;
bool m_exportable = false;
bool m_cutEnabled = false;
bool m_copyEnabled = false;
bool m_pasteEnabled = false;
bool m_requiresConstantRefresh = false;
public:
~BaseEditor() override = default;
/**
* Returns the name of item being edited.
*/
[[nodiscard]]
virtual ox::CStringView itemName() const noexcept = 0;
[[nodiscard]]
virtual ox::CStringView itemDisplayName() const noexcept;
virtual void cut();
virtual void copy();
virtual void paste();
virtual void exportFile();
virtual void keyStateChanged(turbine::Key key, bool down);
virtual void onActivated() noexcept;
[[nodiscard]]
bool requiresConstantRefresh() const noexcept;
void close() const;
/**
* Save changes to item being edited.
*/
void save() noexcept;
/**
* Sets indication of item being edited has unsaved changes. Also emits
* unsavedChangesChanged signal.
*/
void setUnsavedChanges(bool);
[[nodiscard]]
bool unsavedChanges() const noexcept;
void setExportable(bool);
[[nodiscard]]
bool exportable() const;
void setCutEnabled(bool);
[[nodiscard]]
bool cutEnabled() const;
void setCopyEnabled(bool);
[[nodiscard]]
bool copyEnabled() const;
void setPasteEnabled(bool);
[[nodiscard]]
bool pasteEnabled() const;
protected:
/**
* Save changes to item being edited.
*/
virtual ox::Error saveItem() noexcept;
/**
* Returns the undo stack holding changes to the item being edited.
*/
[[nodiscard]]
virtual UndoStack *undoStack() noexcept {
return nullptr;
}
static ox::StringView pathToItemName(ox::CRStringView path) noexcept;
void setRequiresConstantRefresh(bool value) noexcept;
// signals
public:
ox::Signal<ox::Error(bool)> unsavedChangesChanged;
ox::Signal<ox::Error(bool)> exportableChanged;
ox::Signal<ox::Error(bool)> cutEnabledChanged;
ox::Signal<ox::Error(bool)> copyEnabledChanged;
ox::Signal<ox::Error(bool)> pasteEnabledChanged;
ox::Signal<ox::Error(ox::StringView)> closed;
};
class Editor: public studio::BaseEditor {
private:
studio::UndoStack m_undoStack;
public:
Editor() noexcept;
UndoStack *undoStack() noexcept final {
return &m_undoStack;
}
private:
ox::Error markUnsavedChanges(const UndoCommand*) noexcept;
};
}

View File

@ -0,0 +1,27 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/defines.hpp>
namespace studio {
struct FDFilterItem {
#ifdef OX_OS_Windows
using String = ox::Vector<wchar_t>;
#else
using String = ox::Vector<char>;
#endif
String name{};
String spec{};
constexpr FDFilterItem() noexcept = default;
FDFilterItem(ox::CRStringView pName, ox::CRStringView pSpec) noexcept;
};
ox::Result<ox::String> saveFile(ox::Vector<FDFilterItem> const&exts) noexcept;
ox::Result<ox::String> chooseDirectory() noexcept;
}

View File

@ -0,0 +1,13 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <imgui.h>
#include <turbine/context.hpp>
namespace studio::ig {
void centerNextWindow(turbine::Context &ctx) noexcept;
}

View File

@ -0,0 +1,72 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/claw/claw.hpp>
#include <keel/media.hpp>
#include <turbine/context.hpp>
#include "context.hpp"
namespace studio {
class ItemMaker {
public:
const ox::String name;
const ox::String parentDir;
const ox::String fileExt;
constexpr explicit ItemMaker(ox::StringView pName, ox::StringView pParentDir, ox::CRStringView pFileExt) noexcept:
name(pName),
parentDir(pParentDir),
fileExt(pFileExt) {
}
virtual ~ItemMaker() noexcept = default;
virtual ox::Error write(turbine::Context &ctx, ox::CRStringView pName) const noexcept = 0;
};
template<typename T>
class ItemMakerT: public ItemMaker {
private:
const T item;
const ox::ClawFormat fmt;
public:
constexpr ItemMakerT(
ox::StringView pDisplayName,
ox::StringView pParentDir,
ox::StringView fileExt,
ox::ClawFormat pFmt = ox::ClawFormat::Metal) noexcept:
ItemMaker(pDisplayName, pParentDir, fileExt),
fmt(pFmt) {
}
constexpr ItemMakerT(
ox::StringView pDisplayName,
ox::StringView pParentDir,
ox::StringView fileExt,
T pItem,
ox::ClawFormat pFmt) noexcept:
ItemMaker(pDisplayName, pParentDir, fileExt),
item(pItem),
fmt(pFmt) {
}
constexpr ItemMakerT(
ox::StringView pDisplayName,
ox::StringView pParentDir,
ox::StringView fileExt,
T &&pItem,
ox::ClawFormat pFmt) noexcept:
ItemMaker(pDisplayName, pParentDir, fileExt),
item(std::move(pItem)),
fmt(pFmt) {
}
ox::Error write(turbine::Context &ctx, ox::CRStringView pName) const noexcept override {
const auto path = ox::sfmt("/{}/{}.{}", parentDir, pName, fileExt);
auto sctx = turbine::applicationData<studio::StudioContext>(ctx);
keel::createUuidMapping(keelCtx(ctx), path, ox::UUID::generate().unwrap());
return sctx->project->writeObj(path, item, fmt);
}
};
}

View File

@ -0,0 +1,47 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <functional>
#include <ox/std/string.hpp>
#include <ox/std/vector.hpp>
#include <turbine/context.hpp>
#include <studio/itemmaker.hpp>
namespace studio {
class ItemMaker;
struct EditorMaker {
using Func = std::function<ox::Result<class BaseEditor*>(ox::CRStringView)>;
ox::Vector<ox::String> fileTypes;
Func make;
};
class Module {
public:
virtual ~Module() noexcept = default;
virtual ox::Vector<EditorMaker> editors(turbine::Context &ctx) const;
virtual ox::Vector<ox::UPtr<ItemMaker>> itemMakers(turbine::Context&) const;
};
template<typename Editor>
[[nodiscard]]
studio::EditorMaker editorMaker(turbine::Context &ctx, ox::CRStringView ext) noexcept {
return {
{ox::String(ext)},
[&ctx](ox::CRStringView path) -> ox::Result<studio::BaseEditor*> {
return ox::makeCatch<Editor>(ctx, path);
}
};
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <functional>
#include <ox/event/signal.hpp>
#include <ox/std/string.hpp>
#include <ox/std/vec.hpp>
#include <turbine/context.hpp>
namespace studio {
class Popup {
private:
ox::Vec2 m_size;
ox::String m_title;
public:
// emits path parameter
ox::Signal<ox::Error(const ox::String&)> finished;
virtual ~Popup() noexcept = default;
virtual void open() noexcept = 0;
virtual void close() noexcept = 0;
[[nodiscard]]
virtual bool isOpen() const noexcept = 0;
virtual void draw(turbine::Context &ctx) noexcept = 0;
protected:
constexpr void setSize(ox::Size sz) noexcept {
m_size = {static_cast<float>(sz.width), static_cast<float>(sz.height)};
}
constexpr void setTitle(ox::String title) noexcept {
m_title = std::move(title);
}
constexpr const ox::String &title() const noexcept {
return m_title;
}
void drawWindow(turbine::Context &ctx, bool *open, std::function<void()> const&drawContents);
};
}

View File

@ -0,0 +1,169 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/claw/read.hpp>
#include <ox/claw/write.hpp>
#include <ox/event/signal.hpp>
#include <ox/fs/fs.hpp>
#include <ox/mc/mc.hpp>
#include <ox/model/descwrite.hpp>
#include <ox/std/hashmap.hpp>
#include <keel/typestore.hpp>
#include <keel/media.hpp>
namespace studio {
enum class ProjectEvent {
None,
FileAdded,
// FileRecognized is triggered for all matching files upon a new
// subscription to a section of the project and upon the addition of a file.
FileRecognized,
FileDeleted,
FileUpdated,
};
[[nodiscard]]
constexpr ox::Result<ox::StringView> fileExt(ox::CRStringView path) noexcept {
const auto extStart = ox::find(path.crbegin(), path.crend(), '.').offset();
if (!extStart) {
return OxError(1, "Cannot open a file without valid extension.");
}
return substr(path, extStart + 1);
}
class Project {
private:
keel::Context &m_ctx;
ox::String m_path;
ox::String m_projectDataDir;
mutable keel::TypeStore m_typeStore;
ox::FileSystem &m_fs;
ox::HashMap<ox::String, ox::Vector<ox::String>> m_fileExtFileMap;
public:
explicit Project(keel::Context &ctx, ox::String path, ox::CRStringView projectDataDir) noexcept;
ox::Error create() noexcept;
[[nodiscard]]
ox::FileSystem *romFs() noexcept;
ox::Error mkdir(ox::CRStringView path) const noexcept;
/**
* Writes a MetalClaw object to the project at the given path.
*/
template<typename T>
ox::Error writeObj(
ox::CRStringView path,
T const&obj,
ox::ClawFormat fmt = ox::ClawFormat::Metal) noexcept;
template<typename T>
ox::Result<T> loadObj(ox::CRStringView path) const noexcept;
ox::Result<ox::FileStat> stat(ox::CRStringView path) const noexcept;
[[nodiscard]]
bool exists(ox::CRStringView path) const noexcept;
template<typename Functor>
ox::Error subscribe(ProjectEvent e, ox::SignalHandler *tgt, Functor &&slot) const noexcept;
[[nodiscard]]
const ox::Vector<ox::String> &fileList(ox::CRStringView ext) noexcept;
private:
void buildFileIndex() noexcept;
void indexFile(ox::CRStringView path) noexcept;
ox::Error writeBuff(ox::CRStringView path, ox::Buffer const&buff) noexcept;
ox::Result<ox::Buffer> loadBuff(ox::CRStringView path) const noexcept;
ox::Error lsProcDir(ox::Vector<ox::String> *paths, ox::CRStringView path) const noexcept;
ox::Result<ox::Vector<ox::String>> listFiles(ox::CRStringView path = "") const noexcept;
// signals
public:
ox::Signal<ox::Error(ProjectEvent, ox::CRStringView)> fileEvent;
ox::Signal<ox::Error(ox::CRStringView)> fileAdded;
// FileRecognized is triggered for all matching files upon a new
// subscription to a section of the project and upon the addition of a
// file.
ox::Signal<ox::Error(ox::CRStringView)> fileRecognized;
ox::Signal<ox::Error(ox::CRStringView)> fileDeleted;
ox::Signal<ox::Error(ox::CRStringView)> fileUpdated;
};
template<typename T>
ox::Error Project::writeObj(ox::CRStringView path, T const&obj, ox::ClawFormat fmt) noexcept {
oxRequireM(buff, ox::writeClaw(obj, fmt));
// write to FS
oxReturnError(writeBuff(path, buff));
// write type descriptor
if (m_typeStore.get<T>().error) {
oxReturnError(ox::buildTypeDef(&m_typeStore, &obj));
}
// write out type store
const auto descPath = ox::sfmt("/{}/type_descriptors", m_projectDataDir);
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
const auto typePath = ox::sfmt("/{}/{}", descPath, buildTypeId(*t));
oxReturnError(writeBuff(typePath, typeOut));
}
oxReturnError(keel::setAsset(m_ctx, path, obj));
fileUpdated.emit(path);
return {};
}
template<typename T>
ox::Result<T> Project::loadObj(ox::CRStringView path) const noexcept {
oxRequire(buff, loadBuff(path));
if constexpr(ox::is_same_v<T, ox::ModelObject>) {
return keel::readAsset(&m_typeStore, buff);
} else {
return keel::readAsset<T>(buff);
}
}
template<typename Functor>
ox::Error Project::subscribe(ProjectEvent e, ox::SignalHandler *tgt, Functor &&slot) const noexcept {
switch (e) {
case ProjectEvent::None:
break;
case ProjectEvent::FileAdded:
connect(this, &Project::fileAdded, tgt, slot);
break;
case ProjectEvent::FileRecognized:
{
oxRequire(files, listFiles());
for (auto const&f : files) {
slot(f);
}
connect(this, &Project::fileRecognized, tgt, slot);
break;
}
case ProjectEvent::FileDeleted:
connect(this, &Project::fileDeleted, tgt, slot);
break;
case ProjectEvent::FileUpdated:
connect(this, &Project::fileUpdated, tgt, slot);
break;
}
return {};
}
}

View File

@ -0,0 +1,17 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <studio/context.hpp>
#include <studio/editor.hpp>
#include <studio/filedialog.hpp>
#include <studio/imguiuitl.hpp>
#include <studio/module.hpp>
#include <studio/itemmaker.hpp>
#include <studio/popup.hpp>
#include <studio/project.hpp>
#include <studio/task.hpp>
#include <studio/undostack.hpp>
#include <studio/widget.hpp>

View File

@ -0,0 +1,33 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/event/signal.hpp>
#include <turbine/context.hpp>
namespace studio {
enum class TaskState {
Running,
Done
};
class Task: public ox::SignalHandler {
public:
ox::Signal<ox::Error()> finished;
~Task() noexcept override = default;
virtual TaskState update(turbine::Context &ctx) noexcept = 0;
};
class TaskRunner {
private:
ox::Vector<ox::UniquePtr<studio::Task>> m_tasks;
public:
void update(turbine::Context &ctx) noexcept;
void add(Task &task) noexcept;
};
}

View File

@ -0,0 +1,51 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/event/signal.hpp>
#include <ox/std/error.hpp>
#include <ox/std/memory.hpp>
#include <ox/std/vector.hpp>
namespace studio {
class UndoCommand {
public:
virtual ~UndoCommand() noexcept = default;
virtual void redo() noexcept = 0;
virtual void undo() noexcept = 0;
[[nodiscard]]
virtual int commandId() const noexcept = 0;
virtual bool mergeWith(const UndoCommand *cmd) noexcept;
};
class UndoStack {
private:
ox::Vector<ox::UPtr<UndoCommand>> m_stack;
std::size_t m_stackIdx = 0;
public:
void push(ox::UPtr<UndoCommand> &&cmd) noexcept;
void redo() noexcept;
void undo() noexcept;
[[nodiscard]]
constexpr bool canRedo() const noexcept {
return m_stackIdx < m_stack.size();
}
[[nodiscard]]
constexpr bool canUndo() const noexcept {
return m_stackIdx;
}
ox::Signal<ox::Error(const studio::UndoCommand*)> redoTriggered;
ox::Signal<ox::Error(const studio::UndoCommand*)> undoTriggered;
ox::Signal<ox::Error(const studio::UndoCommand*)> changeTriggered;
};
}

View File

@ -0,0 +1,19 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/event/signal.hpp>
#include <turbine/context.hpp>
namespace studio {
class Widget: public ox::SignalHandler {
public:
~Widget() noexcept override = default;
virtual void draw(turbine::Context&) noexcept = 0;
};
}

View File

@ -0,0 +1,46 @@
add_library(
Studio
configio.cpp
editor.cpp
imguiutil.cpp
module.cpp
popup.cpp
project.cpp
task.cpp
undostack.cpp
widget.cpp
filedialog_nfd.cpp
)
target_include_directories(
Studio PUBLIC
../include
)
include_directories(
SYSTEM
${GTK3_INCLUDE_DIRS}
)
target_link_libraries(
Studio PUBLIC
nfd
OxEvent
GlUtils
Turbine
)
install(
TARGETS
Studio
DESTINATION
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)
install(
DIRECTORY
../include/studio
DESTINATION
include/studio/
)

View File

@ -0,0 +1,31 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <studio/configio.hpp>
namespace studio {
constexpr auto ConfigDir = [] {
switch (ox::defines::OS) {
case ox::OS::Darwin:
return "{}/Library/Preferences/{}";
case ox::OS::DragonFlyBSD:
case ox::OS::FreeBSD:
case ox::OS::Linux:
case ox::OS::NetBSD:
case ox::OS::OpenBSD:
return "{}/.config/{}";
case ox::OS::Windows:
return R"({}/AppData/Local/{})";
case ox::OS::BareMetal:
return "";
}
}();
ox::String configPath(const keel::Context &ctx) noexcept {
const auto homeDir = std::getenv(ox::defines::OS == ox::OS::Windows ? "USERPROFILE" : "HOME");
return ox::sfmt(ConfigDir, homeDir, ctx.appName);
}
}

View File

@ -0,0 +1,124 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <algorithm>
#include <ox/std/string.hpp>
#include <studio/editor.hpp>
namespace studio {
ox::CStringView BaseEditor::itemDisplayName() const noexcept {
return itemName();
}
void BaseEditor::cut() {
}
void BaseEditor::copy() {
}
void BaseEditor::paste() {
}
void BaseEditor::exportFile() {
}
void BaseEditor::keyStateChanged(turbine::Key, bool) {
}
void BaseEditor::onActivated() noexcept {
}
bool BaseEditor::requiresConstantRefresh() const noexcept {
return m_requiresConstantRefresh;
}
void BaseEditor::close() const {
this->closed.emit(itemName());
}
void BaseEditor::save() noexcept {
const auto err = saveItem();
if (!err) {
setUnsavedChanges(false);
} else {
if constexpr(ox::defines::Debug) {
oxErrorf("Could not save file {}: {} ({}:{})", itemName(), toStr(err), err.file, err.line);
} else {
oxErrorf("Could not save file {}: {}", itemName(), toStr(err));
}
}
}
void BaseEditor::setUnsavedChanges(bool uc) {
m_unsavedChanges = uc;
unsavedChangesChanged.emit(uc);
}
bool BaseEditor::unsavedChanges() const noexcept {
return m_unsavedChanges;
}
void BaseEditor::setExportable(bool exportable) {
m_exportable = exportable;
exportableChanged.emit(exportable);
}
bool BaseEditor::exportable() const {
return m_exportable;
}
void BaseEditor::setCutEnabled(bool v) {
m_cutEnabled = v;
cutEnabledChanged.emit(v);
}
bool BaseEditor::cutEnabled() const {
return m_cutEnabled;
}
void BaseEditor::setCopyEnabled(bool v) {
m_copyEnabled = v;
copyEnabledChanged.emit(v);
}
bool BaseEditor::copyEnabled() const {
return m_copyEnabled;
}
void BaseEditor::setPasteEnabled(bool v) {
m_pasteEnabled = v;
pasteEnabledChanged.emit(v);
}
bool BaseEditor::pasteEnabled() const {
return m_pasteEnabled;
}
ox::Error BaseEditor::saveItem() noexcept {
return OxError(0);
}
ox::StringView BaseEditor::pathToItemName(ox::CRStringView path) noexcept {
const auto lastSlash = std::find(path.rbegin(), path.rend(), '/').offset();
return substr(path, lastSlash + 1);
}
void BaseEditor::setRequiresConstantRefresh(bool value) noexcept {
m_requiresConstantRefresh = value;
}
Editor::Editor() noexcept {
m_undoStack.changeTriggered.connect(this, &Editor::markUnsavedChanges);
}
ox::Error Editor::markUnsavedChanges(const UndoCommand*) noexcept {
setUnsavedChanges(true);
return OxError(0);
}
}

View File

@ -0,0 +1,57 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <nfd.hpp>
#include <ox/std/error.hpp>
#include <ox/std/string.hpp>
#include <studio/filedialog.hpp>
namespace studio {
FDFilterItem::FDFilterItem(ox::CRStringView pName, ox::CRStringView pSpec) noexcept {
name.resize(pName.len() + 1);
ox_strncpy(name.data(), pName.data(), pName.len());
spec.resize(pSpec.len() + 1);
ox_strncpy(spec.data(), pSpec.data(), pSpec.len());
}
static ox::Result<ox::String> toResult(nfdresult_t r, NFD::UniquePathN const&path) noexcept {
switch (r) {
case NFD_OKAY: {
ox::String out;
for (auto i = 0u; path.get()[i]; ++i) {
const auto c = static_cast<char>(path.get()[i]);
oxIgnoreError(out.append(&c, 1));
}
return out;
}
case NFD_CANCEL:
return OxError(1, "Operation cancelled");
default:
return OxError(2, NFD::GetError());
}
}
ox::Result<ox::String> saveFile(ox::Vector<FDFilterItem> const&filters) noexcept {
NFD::Guard const guard;
NFD::UniquePathN path;
ox::Vector<nfdnfilteritem_t, 5> filterItems(filters.size());
for (auto i = 0u; const auto &f : filters) {
filterItems[i].name = f.name.data();
filterItems[i].spec = f.spec.data();
++i;
}
auto const filterItemsCnt = static_cast<nfdfiltersize_t>(filterItems.size());
return toResult(NFD::SaveDialog(path, filterItems.data(), filterItemsCnt), path);
}
ox::Result<ox::String> chooseDirectory() noexcept {
const NFD::Guard guard;
NFD::UniquePathN path;
return toResult(NFD::PickFolder(path), path);
}
}

View File

@ -0,0 +1,19 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <imgui.h>
#include <turbine/gfx.hpp>
namespace studio::ig {
void centerNextWindow(turbine::Context &ctx) noexcept {
const auto sz = turbine::getScreenSize(ctx);
const auto screenW = static_cast<float>(sz.width);
const auto screenH = static_cast<float>(sz.height);
const auto mod = ImGui::GetWindowDpiScale() * 2;
ImGui::SetNextWindowPos(ImVec2(screenW / mod, screenH / mod), ImGuiCond_Always, ImVec2(0.5f, 0.5f));
}
}

View File

@ -0,0 +1,17 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <studio/module.hpp>
namespace studio {
ox::Vector<EditorMaker> Module::editors(turbine::Context&) const {
return {};
}
ox::Vector<ox::UPtr<ItemMaker>> Module::itemMakers(turbine::Context&) const {
return {};
}
}

View File

@ -0,0 +1,20 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <studio/imguiuitl.hpp>
#include <studio/popup.hpp>
namespace studio {
void Popup::drawWindow(turbine::Context &ctx, bool *open, std::function<void()> const&drawContents) {
studio::ig::centerNextWindow(ctx);
ImGui::SetNextWindowSize(static_cast<ImVec2>(m_size));
constexpr auto modalFlags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize;
if (ImGui::BeginPopupModal(m_title.c_str(), open, modalFlags)) {
drawContents();
ImGui::EndPopup();
}
}
}

View File

@ -0,0 +1,136 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <algorithm>
#include <filesystem>
#include <ox/std/std.hpp>
#include <keel/module.hpp>
#include <studio/project.hpp>
namespace studio {
static void generateTypes(ox::TypeStore &ts) noexcept {
for (const auto mod : keel::modules()) {
for (auto gen : mod->types()) {
oxLogError(gen(ts));
}
}
}
Project::Project(keel::Context &ctx, ox::String path, ox::CRStringView projectDataDir) noexcept:
m_ctx(ctx),
m_path(std::move(path)),
m_projectDataDir(projectDataDir),
m_typeStore(*m_ctx.rom, ox::sfmt("/{}/type_descriptors", projectDataDir)),
m_fs(*m_ctx.rom) {
oxTracef("studio", "Project: {}", m_path);
generateTypes(m_typeStore);
buildFileIndex();
}
ox::Error Project::create() noexcept {
std::error_code ec;
std::filesystem::create_directory(m_path.toStdString(), ec);
return OxError(static_cast<ox::ErrorCode>(ec.value()), "PassThroughFS: mkdir failed");
}
ox::FileSystem *Project::romFs() noexcept {
return &m_fs;
}
ox::Error Project::mkdir(ox::CRStringView path) const noexcept {
oxReturnError(m_fs.mkdir(path, true));
fileUpdated.emit(path);
return {};
}
ox::Result<ox::FileStat> Project::stat(ox::CRStringView path) const noexcept {
return m_fs.stat(path);
}
bool Project::exists(ox::CRStringView path) const noexcept {
return m_fs.stat(path).error == 0;
}
const ox::Vector<ox::String> &Project::fileList(ox::CRStringView ext) noexcept {
return m_fileExtFileMap[ext];
}
void Project::buildFileIndex() noexcept {
auto [files, err] = listFiles();
if (err) {
oxLogError(err);
return;
}
m_fileExtFileMap.clear();
std::sort(files.begin(), files.end());
for (const auto &file : files) {
if (!beginsWith(file, ox::sfmt("/.{}/", m_projectDataDir))) {
indexFile(file);
}
}
}
void Project::indexFile(ox::CRStringView path) noexcept {
const auto [ext, err] = fileExt(path);
if (err) {
return;
}
m_fileExtFileMap[ext].emplace_back(path);
}
ox::Error Project::writeBuff(ox::CRStringView path, ox::Buffer const&buff) noexcept {
constexpr auto HdrSz = 40;
ox::Buffer outBuff;
outBuff.reserve(buff.size() + HdrSz);
ox::BufferWriter writer(&outBuff);
const auto [uuid, err] = m_ctx.pathToUuid.at(path);
if (!err) {
oxReturnError(keel::writeUuidHeader(writer, *uuid));
}
oxReturnError(writer.write(buff.data(), buff.size()));
const auto newFile = m_fs.stat(path).error != 0;
oxReturnError(m_fs.write(path, outBuff.data(), outBuff.size(), ox::FileType::NormalFile));
if (newFile) {
fileAdded.emit(path);
indexFile(path);
} else {
fileUpdated.emit(path);
}
return {};
}
ox::Result<ox::Buffer> Project::loadBuff(ox::CRStringView path) const noexcept {
return m_fs.read(path);
}
ox::Error Project::lsProcDir(ox::Vector<ox::String> *paths, ox::CRStringView path) const noexcept {
oxRequire(files, m_fs.ls(path));
for (const auto &name : files) {
auto fullPath = ox::sfmt("{}/{}", path, name);
oxRequire(stat, m_fs.stat(ox::StringView(fullPath)));
switch (stat.fileType) {
case ox::FileType::NormalFile:
paths->emplace_back(std::move(fullPath));
break;
case ox::FileType::Directory:
oxReturnError(lsProcDir(paths, fullPath));
break;
case ox::FileType::None:
break;
}
}
return {};
}
ox::Result<ox::Vector<ox::String>> Project::listFiles(ox::CRStringView path) const noexcept {
ox::Vector<ox::String> paths;
oxReturnError(lsProcDir(&paths, path));
return paths;
}
}

View File

@ -0,0 +1,25 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <algorithm>
#include <studio/task.hpp>
namespace studio {
void TaskRunner::update(turbine::Context &ctx) noexcept {
oxIgnoreError(m_tasks.erase(std::remove_if(m_tasks.begin(), m_tasks.end(), [&](ox::UPtr<studio::Task> &t) {
const auto done = t->update(ctx) == TaskState::Done;
if (done) {
t->finished.emit();
}
return done;
})));
}
void TaskRunner::add(Task &task) noexcept {
m_tasks.emplace_back(&task);
}
}

View File

@ -0,0 +1,44 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <studio/undostack.hpp>
namespace studio {
bool UndoCommand::mergeWith(const UndoCommand*) noexcept {
return false;
}
void UndoStack::push(ox::UPtr<UndoCommand> &&cmd) noexcept {
for (const auto i = m_stackIdx; i < m_stack.size();) {
oxIgnoreError(m_stack.erase(i));
}
cmd->redo();
redoTriggered.emit(cmd.get());
changeTriggered.emit(cmd.get());
if (m_stack.empty() || !(*m_stack.back().value)->mergeWith(cmd.get())) {
m_stack.emplace_back(std::move(cmd));
++m_stackIdx;
}
}
void UndoStack::redo() noexcept {
if (m_stackIdx < m_stack.size()) {
auto &c = m_stack[m_stackIdx++];
c->redo();
redoTriggered.emit(c.get());
changeTriggered.emit(c.get());
}
}
void UndoStack::undo() noexcept {
if (m_stackIdx) {
auto &c = m_stack[--m_stackIdx];
c->undo();
undoTriggered.emit(c.get());
changeTriggered.emit(c.get());
}
}
}

View File

@ -0,0 +1,9 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <studio/widget.hpp>
namespace studio {
}