[olympic] Move keel, turbine, and studio to olympic
This commit is contained in:
		
							
								
								
									
										2
									
								
								src/olympic/studio/CMakeLists.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/olympic/studio/CMakeLists.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| add_subdirectory(applib) | ||||
| add_subdirectory(modlib) | ||||
							
								
								
									
										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; | ||||
| }; | ||||
|  | ||||
| } | ||||
							
								
								
									
										1
									
								
								src/olympic/studio/modlib/CMakeLists.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/olympic/studio/modlib/CMakeLists.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| add_subdirectory(src) | ||||
							
								
								
									
										95
									
								
								src/olympic/studio/modlib/include/studio/configio.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								src/olympic/studio/modlib/include/studio/configio.hpp
									
									
									
									
									
										Normal 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); | ||||
| } | ||||
|  | ||||
| } | ||||
							
								
								
									
										18
									
								
								src/olympic/studio/modlib/include/studio/context.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/olympic/studio/modlib/include/studio/context.hpp
									
									
									
									
									
										Normal 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; | ||||
| }; | ||||
|  | ||||
| } | ||||
							
								
								
									
										136
									
								
								src/olympic/studio/modlib/include/studio/editor.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								src/olympic/studio/modlib/include/studio/editor.hpp
									
									
									
									
									
										Normal 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; | ||||
| }; | ||||
|  | ||||
|  | ||||
| } | ||||
							
								
								
									
										27
									
								
								src/olympic/studio/modlib/include/studio/filedialog.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/olympic/studio/modlib/include/studio/filedialog.hpp
									
									
									
									
									
										Normal 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; | ||||
|  | ||||
| } | ||||
							
								
								
									
										13
									
								
								src/olympic/studio/modlib/include/studio/imguiuitl.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/olympic/studio/modlib/include/studio/imguiuitl.hpp
									
									
									
									
									
										Normal 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; | ||||
|  | ||||
| } | ||||
							
								
								
									
										72
									
								
								src/olympic/studio/modlib/include/studio/itemmaker.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								src/olympic/studio/modlib/include/studio/itemmaker.hpp
									
									
									
									
									
										Normal 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); | ||||
| 		} | ||||
| }; | ||||
|  | ||||
| } | ||||
							
								
								
									
										47
									
								
								src/olympic/studio/modlib/include/studio/module.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/olympic/studio/modlib/include/studio/module.hpp
									
									
									
									
									
										Normal 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); | ||||
| 			} | ||||
| 	}; | ||||
| } | ||||
|  | ||||
| } | ||||
							
								
								
									
										55
									
								
								src/olympic/studio/modlib/include/studio/popup.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/olympic/studio/modlib/include/studio/popup.hpp
									
									
									
									
									
										Normal 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); | ||||
|  | ||||
| }; | ||||
|  | ||||
|  | ||||
| } | ||||
							
								
								
									
										169
									
								
								src/olympic/studio/modlib/include/studio/project.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								src/olympic/studio/modlib/include/studio/project.hpp
									
									
									
									
									
										Normal 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 {}; | ||||
| } | ||||
|  | ||||
| } | ||||
							
								
								
									
										17
									
								
								src/olympic/studio/modlib/include/studio/studio.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/olympic/studio/modlib/include/studio/studio.hpp
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										33
									
								
								src/olympic/studio/modlib/include/studio/task.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/olympic/studio/modlib/include/studio/task.hpp
									
									
									
									
									
										Normal 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; | ||||
| }; | ||||
|  | ||||
| } | ||||
							
								
								
									
										51
									
								
								src/olympic/studio/modlib/include/studio/undostack.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/olympic/studio/modlib/include/studio/undostack.hpp
									
									
									
									
									
										Normal 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; | ||||
| }; | ||||
|  | ||||
| } | ||||
							
								
								
									
										19
									
								
								src/olympic/studio/modlib/include/studio/widget.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/olympic/studio/modlib/include/studio/widget.hpp
									
									
									
									
									
										Normal 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; | ||||
| }; | ||||
|  | ||||
| } | ||||
							
								
								
									
										46
									
								
								src/olympic/studio/modlib/src/CMakeLists.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/olympic/studio/modlib/src/CMakeLists.txt
									
									
									
									
									
										Normal 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/ | ||||
| ) | ||||
							
								
								
									
										31
									
								
								src/olympic/studio/modlib/src/configio.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/olympic/studio/modlib/src/configio.cpp
									
									
									
									
									
										Normal 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); | ||||
| } | ||||
|  | ||||
| } | ||||
							
								
								
									
										124
									
								
								src/olympic/studio/modlib/src/editor.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								src/olympic/studio/modlib/src/editor.cpp
									
									
									
									
									
										Normal 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); | ||||
| } | ||||
|  | ||||
| } | ||||
							
								
								
									
										57
									
								
								src/olympic/studio/modlib/src/filedialog_nfd.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/olympic/studio/modlib/src/filedialog_nfd.cpp
									
									
									
									
									
										Normal 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); | ||||
| } | ||||
|  | ||||
| } | ||||
							
								
								
									
										19
									
								
								src/olympic/studio/modlib/src/imguiutil.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/olympic/studio/modlib/src/imguiutil.cpp
									
									
									
									
									
										Normal 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)); | ||||
| } | ||||
|  | ||||
| } | ||||
							
								
								
									
										17
									
								
								src/olympic/studio/modlib/src/module.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/olympic/studio/modlib/src/module.cpp
									
									
									
									
									
										Normal 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 {}; | ||||
| } | ||||
|  | ||||
| } | ||||
							
								
								
									
										20
									
								
								src/olympic/studio/modlib/src/popup.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/olympic/studio/modlib/src/popup.cpp
									
									
									
									
									
										Normal 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(); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| } | ||||
							
								
								
									
										136
									
								
								src/olympic/studio/modlib/src/project.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								src/olympic/studio/modlib/src/project.cpp
									
									
									
									
									
										Normal 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; | ||||
| } | ||||
|  | ||||
| } | ||||
							
								
								
									
										25
									
								
								src/olympic/studio/modlib/src/task.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/olympic/studio/modlib/src/task.cpp
									
									
									
									
									
										Normal 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); | ||||
| } | ||||
|  | ||||
| } | ||||
							
								
								
									
										44
									
								
								src/olympic/studio/modlib/src/undostack.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/olympic/studio/modlib/src/undostack.cpp
									
									
									
									
									
										Normal 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()); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| } | ||||
							
								
								
									
										9
									
								
								src/olympic/studio/modlib/src/widget.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/olympic/studio/modlib/src/widget.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| /* | ||||
|  * Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved. | ||||
|  */ | ||||
|  | ||||
| #include <studio/widget.hpp> | ||||
|  | ||||
| namespace studio { | ||||
|  | ||||
| } | ||||
		Reference in New Issue
	
	Block a user