[studio] Add support for adding and deleting directories
This commit is contained in:
		@@ -5,6 +5,7 @@ add_library(
 | 
			
		||||
		deleteconfirmation.cpp
 | 
			
		||||
		filedialogmanager.cpp
 | 
			
		||||
		main.cpp
 | 
			
		||||
		newdir.cpp
 | 
			
		||||
		newmenu.cpp
 | 
			
		||||
		newproject.cpp
 | 
			
		||||
		projectexplorer.cpp
 | 
			
		||||
 
 | 
			
		||||
@@ -43,7 +43,7 @@ void DeleteConfirmation::draw(StudioContext &ctx) noexcept {
 | 
			
		||||
		case Stage::Open:
 | 
			
		||||
			drawWindow(ctx.tctx, m_open, [this] {
 | 
			
		||||
				ImGui::Text("Are you sure you want to delete %s?", m_path.c_str());
 | 
			
		||||
				if (ig::PopupControlsOkCancel(m_open) != ig::PopupResponse::None) {
 | 
			
		||||
				if (ig::PopupControlsOkCancel(m_open, "Yes", "No") != ig::PopupResponse::None) {
 | 
			
		||||
					deleteFile.emit(m_path);
 | 
			
		||||
					close();
 | 
			
		||||
				}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										59
									
								
								src/olympic/studio/applib/src/newdir.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/olympic/studio/applib/src/newdir.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
#include <studio/imguiutil.hpp>
 | 
			
		||||
 | 
			
		||||
#include "newdir.hpp"
 | 
			
		||||
 | 
			
		||||
namespace studio {
 | 
			
		||||
 | 
			
		||||
NewDir::NewDir() noexcept {
 | 
			
		||||
	setTitle("New Directory");
 | 
			
		||||
	setSize({280, 0});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void NewDir::openPath(ox::StringViewCR path) noexcept {
 | 
			
		||||
	open();
 | 
			
		||||
	m_path = path;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void NewDir::open() noexcept {
 | 
			
		||||
	m_path = "";
 | 
			
		||||
	m_stage = Stage::Opening;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void NewDir::close() noexcept {
 | 
			
		||||
	m_stage = Stage::Closed;
 | 
			
		||||
	m_open = false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool NewDir::isOpen() const noexcept {
 | 
			
		||||
	return m_open;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void NewDir::draw(StudioContext &ctx) noexcept {
 | 
			
		||||
	switch (m_stage) {
 | 
			
		||||
		case Stage::Closed:
 | 
			
		||||
			break;
 | 
			
		||||
		case Stage::Opening:
 | 
			
		||||
			ImGui::OpenPopup(title().c_str());
 | 
			
		||||
			m_open = true;
 | 
			
		||||
		[[fallthrough]];
 | 
			
		||||
		case Stage::Open:
 | 
			
		||||
			drawWindow(ctx.tctx, m_open, [this] {
 | 
			
		||||
				if (m_stage == Stage::Opening) {
 | 
			
		||||
					ImGui::SetKeyboardFocusHere();
 | 
			
		||||
				}
 | 
			
		||||
				ig::InputText("Name", m_str);
 | 
			
		||||
				if (ig::PopupControlsOkCancel(m_open) == ig::PopupResponse::OK) {
 | 
			
		||||
					newDir.emit(m_path + "/" + m_str);
 | 
			
		||||
					close();
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
			m_stage = Stage::Open;
 | 
			
		||||
			break;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										49
									
								
								src/olympic/studio/applib/src/newdir.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/olympic/studio/applib/src/newdir.hpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <ox/std/string.hpp>
 | 
			
		||||
 | 
			
		||||
#include <studio/context.hpp>
 | 
			
		||||
#include <studio/popup.hpp>
 | 
			
		||||
 | 
			
		||||
namespace studio {
 | 
			
		||||
 | 
			
		||||
class NewDir final: public Popup {
 | 
			
		||||
 	private:
 | 
			
		||||
		enum class Stage {
 | 
			
		||||
			Closed,
 | 
			
		||||
			Opening,
 | 
			
		||||
			Open,
 | 
			
		||||
		};
 | 
			
		||||
		Stage m_stage = Stage::Closed;
 | 
			
		||||
		bool m_open{};
 | 
			
		||||
		ox::String m_path;
 | 
			
		||||
		ox::IString<255> m_str;
 | 
			
		||||
 | 
			
		||||
	public:
 | 
			
		||||
		ox::Signal<ox::Error(ox::StringViewCR path)> newDir;
 | 
			
		||||
 | 
			
		||||
		NewDir() noexcept;
 | 
			
		||||
 | 
			
		||||
		void openPath(ox::StringViewCR path) noexcept;
 | 
			
		||||
 | 
			
		||||
		void open() noexcept override;
 | 
			
		||||
 | 
			
		||||
		void close() noexcept override;
 | 
			
		||||
 | 
			
		||||
		[[nodiscard]]
 | 
			
		||||
		bool isOpen() const noexcept override;
 | 
			
		||||
 | 
			
		||||
		void draw(StudioContext &ctx) noexcept override;
 | 
			
		||||
 | 
			
		||||
		[[nodiscard]]
 | 
			
		||||
		constexpr ox::CStringView value() const noexcept {
 | 
			
		||||
			return m_str;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -27,13 +27,13 @@ ProjectTreeModel::ProjectTreeModel(ProjectTreeModel &&other) noexcept:
 | 
			
		||||
	m_children(std::move(other.m_children)) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ProjectTreeModel::draw(turbine::Context &ctx) const noexcept {
 | 
			
		||||
void ProjectTreeModel::draw(turbine::Context &tctx) const noexcept {
 | 
			
		||||
	constexpr auto dirFlags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick;
 | 
			
		||||
	if (!m_children.empty()) {
 | 
			
		||||
		if (ImGui::TreeNodeEx(m_name.c_str(), dirFlags)) {
 | 
			
		||||
            drawDirContextMenu();
 | 
			
		||||
			for (auto const&child : m_children) {
 | 
			
		||||
				child->draw(ctx);
 | 
			
		||||
				child->draw(tctx);
 | 
			
		||||
			}
 | 
			
		||||
			ImGui::TreePop();
 | 
			
		||||
		} else {
 | 
			
		||||
@@ -54,6 +54,9 @@ void ProjectTreeModel::draw(turbine::Context &ctx) const noexcept {
 | 
			
		||||
				ImGui::EndPopup();
 | 
			
		||||
			}
 | 
			
		||||
			ImGui::TreePop();
 | 
			
		||||
			ig::dragDropSource([this] {
 | 
			
		||||
				ImGui::Text("%s", m_name.c_str());
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -70,13 +73,16 @@ void ProjectTreeModel::drawDirContextMenu() const noexcept {
 | 
			
		||||
        if (ImGui::MenuItem("Add Directory")) {
 | 
			
		||||
            m_explorer.addDir.emit(fullPath());
 | 
			
		||||
        }
 | 
			
		||||
		if (ImGui::MenuItem("Delete")) {
 | 
			
		||||
			m_explorer.deleteItem.emit(fullPath());
 | 
			
		||||
		}
 | 
			
		||||
        ImGui::EndPopup();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ox::BasicString<255> ProjectTreeModel::fullPath() const noexcept {
 | 
			
		||||
	if (m_parent) {
 | 
			
		||||
		return m_parent->fullPath() + "/" + ox::StringView(m_name);
 | 
			
		||||
		return m_parent->fullPath() + "/" + m_name;
 | 
			
		||||
	}
 | 
			
		||||
	return {};
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@ class ProjectTreeModel {
 | 
			
		||||
 | 
			
		||||
		ProjectTreeModel(ProjectTreeModel &&other) noexcept;
 | 
			
		||||
 | 
			
		||||
		void draw(turbine::Context &ctx) const noexcept;
 | 
			
		||||
		void draw(turbine::Context &tctx) const noexcept;
 | 
			
		||||
 | 
			
		||||
		void setChildren(ox::Vector<ox::UPtr<ProjectTreeModel>> children) noexcept;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -52,6 +52,7 @@ StudioUI::StudioUI(turbine::Context &ctx, ox::StringParam projectDataDir) noexce
 | 
			
		||||
		m_aboutPopup(m_tctx) {
 | 
			
		||||
	turbine::setApplicationData(m_tctx, &m_sctx);
 | 
			
		||||
	m_projectExplorer.fileChosen.connect(this, &StudioUI::openFile);
 | 
			
		||||
	m_projectExplorer.addDir.connect(this, &StudioUI::addDir);
 | 
			
		||||
	m_projectExplorer.addItem.connect(this, &StudioUI::addFile);
 | 
			
		||||
	m_projectExplorer.deleteItem.connect(this, &StudioUI::deleteFile);
 | 
			
		||||
	m_newProject.finished.connect(this, &StudioUI::createOpenProject);
 | 
			
		||||
@@ -344,6 +345,11 @@ void StudioUI::handleKeyInput() noexcept {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ox::Error StudioUI::addDir(ox::StringViewCR path) noexcept {
 | 
			
		||||
	m_newDirDialog.openPath(path);
 | 
			
		||||
	return {};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ox::Error StudioUI::addFile(ox::StringViewCR path) noexcept {
 | 
			
		||||
	m_newMenu.openPath(path);
 | 
			
		||||
	return {};
 | 
			
		||||
@@ -369,8 +375,10 @@ ox::Error StudioUI::openProjectPath(ox::StringParam path) noexcept {
 | 
			
		||||
			ox::make_unique_catch<Project>(keelCtx(m_tctx), std::move(path), m_projectDataDir)
 | 
			
		||||
			        .moveTo(m_project));
 | 
			
		||||
	m_sctx.project = m_project.get();
 | 
			
		||||
	m_deleteConfirmation.deleteFile.connect(m_sctx.project, &Project::deleteItem);
 | 
			
		||||
	turbine::setWindowTitle(m_tctx, ox::sfmt("{} - {}", keelCtx(m_tctx).appName, m_project->projectPath()));
 | 
			
		||||
	m_deleteConfirmation.deleteFile.connect(m_sctx.project, &Project::deleteItem);
 | 
			
		||||
	m_newDirDialog.newDir.connect(m_sctx.project, &Project::mkdir);
 | 
			
		||||
	m_project->dirAdded.connect(&m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel);
 | 
			
		||||
	m_project->fileAdded.connect(&m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel);
 | 
			
		||||
	m_project->fileDeleted.connect(&m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel);
 | 
			
		||||
	m_openFiles.clear();
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@
 | 
			
		||||
 | 
			
		||||
#include "aboutpopup.hpp"
 | 
			
		||||
#include "deleteconfirmation.hpp"
 | 
			
		||||
#include "newdir.hpp"
 | 
			
		||||
#include "newmenu.hpp"
 | 
			
		||||
#include "newproject.hpp"
 | 
			
		||||
#include "projectexplorer.hpp"
 | 
			
		||||
@@ -41,13 +42,15 @@ class StudioUI: public ox::SignalHandler {
 | 
			
		||||
		BaseEditor *m_activeEditorUpdatePending = nullptr;
 | 
			
		||||
		NewMenu m_newMenu;
 | 
			
		||||
		DeleteConfirmation m_deleteConfirmation;
 | 
			
		||||
		NewDir m_newDirDialog;
 | 
			
		||||
		NewProject m_newProject;
 | 
			
		||||
		AboutPopup m_aboutPopup;
 | 
			
		||||
		ox::Array<Popup*, 4> const m_popups = {
 | 
			
		||||
		ox::Array<Popup*, 5> const m_popups = {
 | 
			
		||||
			&m_newMenu,
 | 
			
		||||
			&m_newProject,
 | 
			
		||||
			&m_aboutPopup,
 | 
			
		||||
			&m_deleteConfirmation,
 | 
			
		||||
			&m_newDirDialog,
 | 
			
		||||
		};
 | 
			
		||||
		bool m_showProjectExplorer = true;
 | 
			
		||||
 | 
			
		||||
@@ -87,6 +90,8 @@ class StudioUI: public ox::SignalHandler {
 | 
			
		||||
 | 
			
		||||
		void handleKeyInput() noexcept;
 | 
			
		||||
 | 
			
		||||
		ox::Error addDir(ox::StringViewCR path) noexcept;
 | 
			
		||||
 | 
			
		||||
		ox::Error addFile(ox::StringViewCR path) noexcept;
 | 
			
		||||
 | 
			
		||||
		ox::Error deleteFile(ox::StringViewCR path) noexcept;
 | 
			
		||||
 
 | 
			
		||||
@@ -175,9 +175,16 @@ enum class PopupResponse {
 | 
			
		||||
	Cancel,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
PopupResponse PopupControlsOkCancel(float popupWidth, bool &popupOpen);
 | 
			
		||||
PopupResponse PopupControlsOkCancel(
 | 
			
		||||
	float popupWidth,
 | 
			
		||||
	bool &popupOpen,
 | 
			
		||||
	ox::CStringViewCR ok = "OK",
 | 
			
		||||
	ox::CStringViewCR cancel = "Cancel");
 | 
			
		||||
 | 
			
		||||
PopupResponse PopupControlsOkCancel(bool &popupOpen);
 | 
			
		||||
PopupResponse PopupControlsOkCancel(
 | 
			
		||||
	bool &popupOpen,
 | 
			
		||||
	ox::CStringViewCR ok = "OK",
 | 
			
		||||
	ox::CStringViewCR cancel = "Cancel");
 | 
			
		||||
 | 
			
		||||
[[nodiscard]]
 | 
			
		||||
bool BeginPopup(turbine::Context &ctx, ox::CStringViewCR popupName, bool &show, ImVec2 const&sz = {285, 0});
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,7 @@ namespace studio {
 | 
			
		||||
 | 
			
		||||
enum class ProjectEvent {
 | 
			
		||||
	None,
 | 
			
		||||
	DirAdded,
 | 
			
		||||
	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.
 | 
			
		||||
@@ -120,6 +121,7 @@ class Project: public ox::SignalHandler {
 | 
			
		||||
	// signals
 | 
			
		||||
	public:
 | 
			
		||||
		ox::Signal<ox::Error(ProjectEvent, ox::StringViewCR)> fileEvent;
 | 
			
		||||
		ox::Signal<ox::Error(ox::StringViewCR)> dirAdded;
 | 
			
		||||
		ox::Signal<ox::Error(ox::StringViewCR)> fileAdded;
 | 
			
		||||
		// FileRecognized is triggered for all matching files upon a new
 | 
			
		||||
		// subscription to a section of the project and upon the addition of a
 | 
			
		||||
@@ -177,6 +179,9 @@ ox::Error Project::subscribe(ProjectEvent e, ox::SignalHandler *tgt, Functor &&s
 | 
			
		||||
	switch (e) {
 | 
			
		||||
		case ProjectEvent::None:
 | 
			
		||||
			break;
 | 
			
		||||
		case ProjectEvent::DirAdded:
 | 
			
		||||
			connect(this, &Project::dirAdded, tgt, slot);
 | 
			
		||||
			break;
 | 
			
		||||
		case ProjectEvent::FileAdded:
 | 
			
		||||
			connect(this, &Project::fileAdded, tgt, slot);
 | 
			
		||||
			break;
 | 
			
		||||
 
 | 
			
		||||
@@ -54,18 +54,22 @@ bool PushButton(ox::CStringViewCR lbl, ImVec2 const&btnSz) noexcept {
 | 
			
		||||
	return ImGui::Button(lbl.c_str(), btnSz);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PopupResponse PopupControlsOkCancel(float popupWidth, bool &popupOpen) {
 | 
			
		||||
PopupResponse PopupControlsOkCancel(
 | 
			
		||||
	float popupWidth,
 | 
			
		||||
	bool &popupOpen,
 | 
			
		||||
	ox::CStringViewCR ok,
 | 
			
		||||
	ox::CStringViewCR cancel) {
 | 
			
		||||
	auto out = PopupResponse::None;
 | 
			
		||||
	constexpr auto btnSz = ImVec2{50, BtnSz.y};
 | 
			
		||||
	ImGui::Separator();
 | 
			
		||||
	ImGui::SetCursorPosX(popupWidth - 118);
 | 
			
		||||
	if (ImGui::Button("OK", btnSz)) {
 | 
			
		||||
	if (ImGui::Button(ok.c_str(), btnSz)) {
 | 
			
		||||
		ImGui::CloseCurrentPopup();
 | 
			
		||||
		popupOpen = false;
 | 
			
		||||
		out = PopupResponse::OK;
 | 
			
		||||
	}
 | 
			
		||||
	ImGui::SameLine();
 | 
			
		||||
	if (ImGui::IsKeyDown(ImGuiKey_Escape) || ImGui::Button("Cancel", btnSz)) {
 | 
			
		||||
	if (ImGui::IsKeyDown(ImGuiKey_Escape) || ImGui::Button(cancel.c_str(), btnSz)) {
 | 
			
		||||
		ImGui::CloseCurrentPopup();
 | 
			
		||||
		popupOpen = false;
 | 
			
		||||
		out = PopupResponse::Cancel;
 | 
			
		||||
@@ -73,8 +77,11 @@ PopupResponse PopupControlsOkCancel(float popupWidth, bool &popupOpen) {
 | 
			
		||||
	return out;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PopupResponse PopupControlsOkCancel(bool &popupOpen) {
 | 
			
		||||
	return PopupControlsOkCancel(ImGui::GetContentRegionAvail().x + 17, popupOpen);
 | 
			
		||||
PopupResponse PopupControlsOkCancel(
 | 
			
		||||
	bool &popupOpen,
 | 
			
		||||
	ox::CStringViewCR ok,
 | 
			
		||||
	ox::CStringViewCR cancel) {
 | 
			
		||||
	return PopupControlsOkCancel(ImGui::GetContentRegionAvail().x + 17, popupOpen, ok, cancel);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool BeginPopup(turbine::Context &ctx, ox::CStringViewCR popupName, bool &show, ImVec2 const&sz) {
 | 
			
		||||
 
 | 
			
		||||
@@ -59,10 +59,10 @@ ox::Error Project::mkdir(ox::StringViewCR path) const noexcept {
 | 
			
		||||
	auto const [stat, err] = m_fs.stat(path);
 | 
			
		||||
	if (err) {
 | 
			
		||||
		OX_RETURN_ERROR(m_fs.mkdir(path, true));
 | 
			
		||||
		fileUpdated.emit(path, {});
 | 
			
		||||
		dirAdded.emit(path);
 | 
			
		||||
	}
 | 
			
		||||
	return stat.fileType == ox::FileType::Directory ?
 | 
			
		||||
		ox::Error{} : ox::Error(1, "path exists as normal file");
 | 
			
		||||
		ox::Error{} : ox::Error{1, "path exists as normal file"};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ox::Result<ox::FileStat> Project::stat(ox::StringViewCR path) const noexcept {
 | 
			
		||||
@@ -70,11 +70,28 @@ ox::Result<ox::FileStat> Project::stat(ox::StringViewCR path) const noexcept {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ox::Error Project::deleteItem(ox::StringViewCR path) noexcept {
 | 
			
		||||
	auto const err = m_fs.remove(path);
 | 
			
		||||
	if (!err) {
 | 
			
		||||
		fileDeleted.emit(path);
 | 
			
		||||
	OX_REQUIRE(stat, m_fs.stat(path));
 | 
			
		||||
	if (stat.fileType == ox::FileType::Directory) {
 | 
			
		||||
		bool partialRemoval{};
 | 
			
		||||
		OX_REQUIRE(members, m_fs.ls(path));
 | 
			
		||||
		for (auto const&p : members) {
 | 
			
		||||
			partialRemoval = m_fs.remove(ox::sfmt("{}/{}", path, p)) || partialRemoval;
 | 
			
		||||
		}
 | 
			
		||||
		if (partialRemoval) {
 | 
			
		||||
			return ox::Error{1, "failed to remove one or more directory members"};
 | 
			
		||||
		}
 | 
			
		||||
		auto const err = m_fs.remove(path);
 | 
			
		||||
		if (!err) {
 | 
			
		||||
			fileDeleted.emit(path);
 | 
			
		||||
		}
 | 
			
		||||
		return err;
 | 
			
		||||
	} else {
 | 
			
		||||
		auto const err = m_fs.remove(path);
 | 
			
		||||
		if (!err) {
 | 
			
		||||
			fileDeleted.emit(path);
 | 
			
		||||
		}
 | 
			
		||||
		return err;
 | 
			
		||||
	}
 | 
			
		||||
	return err;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool Project::exists(ox::StringViewCR path) const noexcept {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user