[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 @@
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;
};
}