[olympic] Move keel, turbine, and studio to olympic
This commit is contained in:
1
src/olympic/studio/applib/CMakeLists.txt
Normal file
1
src/olympic/studio/applib/CMakeLists.txt
Normal file
@ -0,0 +1 @@
|
||||
add_subdirectory(src)
|
15
src/olympic/studio/applib/include/studioapp/studioapp.hpp
Normal file
15
src/olympic/studio/applib/include/studioapp/studioapp.hpp
Normal 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;
|
||||
|
||||
}
|
42
src/olympic/studio/applib/src/CMakeLists.txt
Normal file
42
src/olympic/studio/applib/src/CMakeLists.txt
Normal 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
|
||||
)
|
61
src/olympic/studio/applib/src/aboutpopup.cpp
Normal file
61
src/olympic/studio/applib/src/aboutpopup.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
42
src/olympic/studio/applib/src/aboutpopup.hpp
Normal file
42
src/olympic/studio/applib/src/aboutpopup.hpp
Normal 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;
|
||||
|
||||
};
|
||||
|
||||
}
|
186
src/olympic/studio/applib/src/clawviewer.cpp
Normal file
186
src/olympic/studio/applib/src/clawviewer.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
40
src/olympic/studio/applib/src/clawviewer.hpp
Normal file
40
src/olympic/studio/applib/src/clawviewer.hpp
Normal 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;
|
||||
};
|
||||
|
||||
}
|
37
src/olympic/studio/applib/src/filedialogmanager.cpp
Normal file
37
src/olympic/studio/applib/src/filedialogmanager.cpp
Normal 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;
|
||||
}
|
||||
|
||||
}
|
43
src/olympic/studio/applib/src/filedialogmanager.hpp
Normal file
43
src/olympic/studio/applib/src/filedialogmanager.hpp
Normal 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;
|
||||
|
||||
};
|
||||
|
||||
}
|
95
src/olympic/studio/applib/src/main.cpp
Normal file
95
src/olympic/studio/applib/src/main.cpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
125
src/olympic/studio/applib/src/newmenu.cpp
Normal file
125
src/olympic/studio/applib/src/newmenu.cpp
Normal 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;
|
||||
}
|
||||
|
||||
}
|
79
src/olympic/studio/applib/src/newmenu.hpp
Normal file
79
src/olympic/studio/applib/src/newmenu.hpp
Normal 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));
|
||||
}
|
||||
|
||||
}
|
57
src/olympic/studio/applib/src/projectexplorer.cpp
Normal file
57
src/olympic/studio/applib/src/projectexplorer.cpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
38
src/olympic/studio/applib/src/projectexplorer.hpp
Normal file
38
src/olympic/studio/applib/src/projectexplorer.hpp
Normal 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;
|
||||
};
|
||||
|
||||
}
|
58
src/olympic/studio/applib/src/projecttreemodel.cpp
Normal file
58
src/olympic/studio/applib/src/projecttreemodel.cpp
Normal 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 {};
|
||||
}
|
||||
|
||||
}
|
35
src/olympic/studio/applib/src/projecttreemodel.hpp
Normal file
35
src/olympic/studio/applib/src/projecttreemodel.hpp
Normal 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;
|
||||
};
|
||||
|
||||
}
|
389
src/olympic/studio/applib/src/studioapp.cpp
Normal file
389
src/olympic/studio/applib/src/studioapp.cpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
91
src/olympic/studio/applib/src/studioapp.hpp
Normal file
91
src/olympic/studio/applib/src/studioapp.hpp
Normal 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;
|
||||
};
|
||||
|
||||
}
|
Reference in New Issue
Block a user