[keel,studio] Add Make Copy option to ProjectExplorer
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				Build / build (push) Successful in 3m46s
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	Build / build (push) Successful in 3m46s
				
			This commit is contained in:
		| @@ -15,6 +15,8 @@ constexpr auto K1HdrSz = 40; | |||||||
|  |  | ||||||
| ox::Result<ox::UUID> readUuidHeader(ox::BufferView buff) noexcept; | ox::Result<ox::UUID> readUuidHeader(ox::BufferView buff) noexcept; | ||||||
|  |  | ||||||
|  | ox::Result<ox::UUID> regenerateUuidHeader(ox::Buffer &buff) noexcept; | ||||||
|  |  | ||||||
| ox::Error writeUuidHeader(ox::Writer_c auto &writer, ox::UUID const&uuid) noexcept { | ox::Error writeUuidHeader(ox::Writer_c auto &writer, ox::UUID const&uuid) noexcept { | ||||||
| 	OX_RETURN_ERROR(write(writer, "K1;")); | 	OX_RETURN_ERROR(write(writer, "K1;")); | ||||||
| 	OX_RETURN_ERROR(uuid.toString(writer)); | 	OX_RETURN_ERROR(uuid.toString(writer)); | ||||||
|   | |||||||
| @@ -8,15 +8,25 @@ namespace keel { | |||||||
|  |  | ||||||
| ox::Result<ox::UUID> readUuidHeader(ox::BufferView buff) noexcept { | ox::Result<ox::UUID> readUuidHeader(ox::BufferView buff) noexcept { | ||||||
| 	if (buff.size() < K1HdrSz) [[unlikely]] { | 	if (buff.size() < K1HdrSz) [[unlikely]] { | ||||||
| 		return ox::Error(1, "Insufficient data to contain complete Keel header"); | 		return ox::Error{1, "Insufficient data to contain complete Keel header"}; | ||||||
| 	} | 	} | ||||||
| 	constexpr ox::StringView k1Hdr = "K1;"; | 	constexpr ox::StringView k1Hdr = "K1;"; | ||||||
| 	if (k1Hdr != ox::StringView(buff.data(), k1Hdr.bytes())) [[unlikely]] { | 	if (k1Hdr != ox::StringView{buff.data(), k1Hdr.bytes()}) [[unlikely]] { | ||||||
| 		return ox::Error(2, "No Keel asset header data"); | 		return ox::Error{2, "No Keel asset header data"}; | ||||||
| 	} | 	} | ||||||
| 	return ox::UUID::fromString(ox::StringView(&buff[k1Hdr.bytes()], 36)); | 	return ox::UUID::fromString(ox::StringView(&buff[k1Hdr.bytes()], 36)); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | ox::Result<ox::UUID> regenerateUuidHeader(ox::Buffer &buff) noexcept { | ||||||
|  | 	OX_RETURN_ERROR(readUuidHeader(buff)); | ||||||
|  | 	OX_REQUIRE(id, ox::UUID::generate()); | ||||||
|  | 	auto const str = id.toString(); | ||||||
|  | 	for (size_t i = 0; i < ox::UUIDStr::cap(); ++i) { | ||||||
|  | 		buff[i + 3] = str[i]; | ||||||
|  | 	} | ||||||
|  | 	return id; | ||||||
|  | } | ||||||
|  |  | ||||||
| ox::Result<ox::ModelObject> readAsset(ox::TypeStore &ts, ox::BufferView buff) noexcept { | ox::Result<ox::ModelObject> readAsset(ox::TypeStore &ts, ox::BufferView buff) noexcept { | ||||||
| 	std::size_t offset = 0; | 	std::size_t offset = 0; | ||||||
| 	if (!readUuidHeader(buff).error) { | 	if (!readUuidHeader(buff).error) { | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ add_library( | |||||||
| 		deleteconfirmation.cpp | 		deleteconfirmation.cpp | ||||||
| 		filedialogmanager.cpp | 		filedialogmanager.cpp | ||||||
| 		main.cpp | 		main.cpp | ||||||
|  | 		makecopypopup.cpp | ||||||
| 		newdir.cpp | 		newdir.cpp | ||||||
| 		newmenu.cpp | 		newmenu.cpp | ||||||
| 		newproject.cpp | 		newproject.cpp | ||||||
|   | |||||||
							
								
								
									
										70
									
								
								src/olympic/studio/applib/src/makecopypopup.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/olympic/studio/applib/src/makecopypopup.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | #include "makecopypopup.hpp" | ||||||
|  |  | ||||||
|  | namespace studio { | ||||||
|  |  | ||||||
|  | ox::Error MakeCopyPopup::open(ox::StringViewCR path) noexcept { | ||||||
|  | 	m_stage = Stage::Opening; | ||||||
|  | 	OX_REQUIRE(idx, ox::findIdx(path.rbegin(), path.rend(), '/')); | ||||||
|  | 	m_srcPath = path; | ||||||
|  | 	m_dirPath = substr(path, 0, idx + 1); | ||||||
|  | 	m_title = sfmt("Copy {}", path); | ||||||
|  | 	m_fileName = ""; | ||||||
|  | 	return {}; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void MakeCopyPopup::close() noexcept { | ||||||
|  | 	m_stage = Stage::Closed; | ||||||
|  | 	m_open = false; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool MakeCopyPopup::isOpen() const noexcept { | ||||||
|  | 	return m_open; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void MakeCopyPopup::draw(StudioContext const &ctx, ImVec2 const &sz) noexcept { | ||||||
|  | 	switch (m_stage) { | ||||||
|  | 		case Stage::Closed: | ||||||
|  | 			break; | ||||||
|  | 		case Stage::Opening: | ||||||
|  | 			ImGui::OpenPopup(m_title.c_str()); | ||||||
|  | 			m_stage = Stage::Open; | ||||||
|  | 			m_open = true; | ||||||
|  | 		[[fallthrough]]; | ||||||
|  | 		case Stage::Open: | ||||||
|  | 			ig::centerNextWindow(ctx.tctx); | ||||||
|  | 			ImGui::SetNextWindowSize(sz); | ||||||
|  | 			constexpr auto modalFlags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize; | ||||||
|  | 			if (ImGui::BeginPopupModal(m_title.c_str(), &m_open, modalFlags)) { | ||||||
|  | 				if (ImGui::IsWindowAppearing()) { | ||||||
|  | 					ImGui::SetKeyboardFocusHere(); | ||||||
|  | 				} | ||||||
|  | 				ig::InputText("Name", m_fileName); | ||||||
|  | 				bool open = true; | ||||||
|  | 				switch (ig::PopupControlsOkCancel(open)) { | ||||||
|  | 					case ig::PopupResponse::None: | ||||||
|  | 						break; | ||||||
|  | 					case ig::PopupResponse::OK: | ||||||
|  | 						{ | ||||||
|  | 							auto const p = sfmt("{}{}", m_dirPath, m_fileName); | ||||||
|  | 							if (!ctx.project->exists(p)) { | ||||||
|  | 								makeCopy.emit(m_srcPath, p); | ||||||
|  | 								close(); | ||||||
|  | 							} | ||||||
|  | 						} | ||||||
|  | 						break; | ||||||
|  | 					case ig::PopupResponse::Cancel: | ||||||
|  | 						close(); | ||||||
|  | 						break; | ||||||
|  | 				} | ||||||
|  | 				ImGui::EndPopup(); | ||||||
|  | 			} | ||||||
|  | 			break; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										42
									
								
								src/olympic/studio/applib/src/makecopypopup.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/olympic/studio/applib/src/makecopypopup.hpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include <ox/std/string.hpp> | ||||||
|  |  | ||||||
|  | #include <studio/context.hpp> | ||||||
|  | #include <studio/imguiutil.hpp> | ||||||
|  |  | ||||||
|  | namespace studio { | ||||||
|  |  | ||||||
|  | class MakeCopyPopup { | ||||||
|  |  	private: | ||||||
|  | 		enum class Stage { | ||||||
|  | 			Closed, | ||||||
|  | 			Opening, | ||||||
|  | 			Open, | ||||||
|  | 		}; | ||||||
|  | 		Stage m_stage = Stage::Closed; | ||||||
|  | 		bool m_open{}; | ||||||
|  | 		ox::String m_title{"Copy File"}; | ||||||
|  | 		ox::String m_srcPath; | ||||||
|  | 		ox::String m_dirPath; | ||||||
|  | 		ox::IString<255> m_fileName; | ||||||
|  |  | ||||||
|  | 	public: | ||||||
|  | 		ox::Signal<ox::Error(ox::StringViewCR src, ox::StringViewCR dst)> makeCopy; | ||||||
|  |  | ||||||
|  | 		ox::Error open(ox::StringViewCR path) noexcept; | ||||||
|  |  | ||||||
|  | 		void close() noexcept; | ||||||
|  |  | ||||||
|  | 		[[nodiscard]] | ||||||
|  | 		bool isOpen() const noexcept; | ||||||
|  |  | ||||||
|  | 		void draw(StudioContext const &ctx, ImVec2 const &sz = {}) noexcept; | ||||||
|  |  | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -42,6 +42,9 @@ void ProjectExplorer::fileContextMenu(ox::StringViewCR path) const noexcept { | |||||||
| 		if (ImGui::MenuItem("Rename")) { | 		if (ImGui::MenuItem("Rename")) { | ||||||
| 			renameItem.emit(path); | 			renameItem.emit(path); | ||||||
| 		} | 		} | ||||||
|  | 		if (ImGui::MenuItem("Make Copy")) { | ||||||
|  | 			makeCopy.emit(path); | ||||||
|  | 		} | ||||||
| 		ImGui::EndPopup(); | 		ImGui::EndPopup(); | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -20,6 +20,7 @@ class ProjectExplorer final: public FileExplorer { | |||||||
| 		ox::Signal<ox::Error(ox::StringViewCR)> addDir; | 		ox::Signal<ox::Error(ox::StringViewCR)> addDir; | ||||||
| 		ox::Signal<ox::Error(ox::StringViewCR)> deleteItem; | 		ox::Signal<ox::Error(ox::StringViewCR)> deleteItem; | ||||||
| 		ox::Signal<ox::Error(ox::StringViewCR)> renameItem; | 		ox::Signal<ox::Error(ox::StringViewCR)> renameItem; | ||||||
|  | 		ox::Signal<ox::Error(ox::StringViewCR)> makeCopy; | ||||||
| 		ox::Signal<ox::Error(ox::StringViewCR src, ox::StringViewCR dst)> moveDir; | 		ox::Signal<ox::Error(ox::StringViewCR src, ox::StringViewCR dst)> moveDir; | ||||||
| 		ox::Signal<ox::Error(ox::StringViewCR src, ox::StringViewCR dst)> moveItem; | 		ox::Signal<ox::Error(ox::StringViewCR src, ox::StringViewCR dst)> moveItem; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -59,6 +59,7 @@ StudioUI::StudioUI(turbine::Context &ctx, ox::StringParam projectDataDir) noexce | |||||||
| 	m_projectExplorer.addItem.connect(this, &StudioUI::addFile); | 	m_projectExplorer.addItem.connect(this, &StudioUI::addFile); | ||||||
| 	m_projectExplorer.deleteItem.connect(this, &StudioUI::deleteFile); | 	m_projectExplorer.deleteItem.connect(this, &StudioUI::deleteFile); | ||||||
| 	m_projectExplorer.renameItem.connect(this, &StudioUI::renameFile); | 	m_projectExplorer.renameItem.connect(this, &StudioUI::renameFile); | ||||||
|  | 	m_projectExplorer.makeCopy.connect(this, &StudioUI::makeCopyDlg); | ||||||
| 	m_projectExplorer.moveDir.connect(this, &StudioUI::queueDirMove); | 	m_projectExplorer.moveDir.connect(this, &StudioUI::queueDirMove); | ||||||
| 	m_projectExplorer.moveItem.connect(this, &StudioUI::queueFileMove); | 	m_projectExplorer.moveItem.connect(this, &StudioUI::queueFileMove); | ||||||
| 	m_renameFile.moveFile.connect(this, &StudioUI::queueFileMove); | 	m_renameFile.moveFile.connect(this, &StudioUI::queueFileMove); | ||||||
| @@ -136,6 +137,7 @@ void StudioUI::draw() noexcept { | |||||||
| 			p->draw(m_sctx); | 			p->draw(m_sctx); | ||||||
| 		} | 		} | ||||||
| 		m_closeFileConfirm.draw(m_sctx); | 		m_closeFileConfirm.draw(m_sctx); | ||||||
|  | 		m_copyFilePopup.draw(m_sctx, {250, 0}); | ||||||
| 	} | 	} | ||||||
| 	ImGui::End(); | 	ImGui::End(); | ||||||
| 	handleKeyInput(); | 	handleKeyInput(); | ||||||
| @@ -451,6 +453,7 @@ ox::Error StudioUI::openProjectPath(ox::StringParam path) noexcept { | |||||||
| 	m_sctx.project = m_project.get(); | 	m_sctx.project = m_project.get(); | ||||||
| 	turbine::setWindowTitle(m_tctx, ox::sfmt("{} - {}", keelCtx(m_tctx).appName, m_project->projectPath())); | 	turbine::setWindowTitle(m_tctx, ox::sfmt("{} - {}", keelCtx(m_tctx).appName, m_project->projectPath())); | ||||||
| 	m_deleteConfirmation.deleteFile.connect(m_sctx.project, &Project::deleteItem); | 	m_deleteConfirmation.deleteFile.connect(m_sctx.project, &Project::deleteItem); | ||||||
|  | 	m_copyFilePopup.makeCopy.connect(m_sctx.project, &Project::copyItem); | ||||||
| 	m_newDirDialog.newDir.connect(m_sctx.project, &Project::mkdir); | 	m_newDirDialog.newDir.connect(m_sctx.project, &Project::mkdir); | ||||||
| 	m_project->dirAdded.connect(&m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel); | 	m_project->dirAdded.connect(&m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel); | ||||||
| 	m_project->fileAdded.connect(&m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel); | 	m_project->fileAdded.connect(&m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel); | ||||||
| @@ -513,6 +516,10 @@ ox::Error StudioUI::openFileActiveTab(ox::StringViewCR path, bool const makeActi | |||||||
| 	return {}; | 	return {}; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | ox::Error StudioUI::makeCopyDlg(ox::StringViewCR path) noexcept { | ||||||
|  | 	return m_copyFilePopup.open(path); | ||||||
|  | } | ||||||
|  |  | ||||||
| ox::Error StudioUI::handleCloseFileResponse(ig::PopupResponse const response) noexcept { | ox::Error StudioUI::handleCloseFileResponse(ig::PopupResponse const response) noexcept { | ||||||
| 	if (response == ig::PopupResponse::OK && m_activeEditor) { | 	if (response == ig::PopupResponse::OK && m_activeEditor) { | ||||||
| 		return closeCurrentFile(); | 		return closeCurrentFile(); | ||||||
|   | |||||||
| @@ -16,6 +16,7 @@ | |||||||
|  |  | ||||||
| #include "aboutpopup.hpp" | #include "aboutpopup.hpp" | ||||||
| #include "deleteconfirmation.hpp" | #include "deleteconfirmation.hpp" | ||||||
|  | #include "makecopypopup.hpp" | ||||||
| #include "newdir.hpp" | #include "newdir.hpp" | ||||||
| #include "newmenu.hpp" | #include "newmenu.hpp" | ||||||
| #include "newproject.hpp" | #include "newproject.hpp" | ||||||
| @@ -48,6 +49,7 @@ class StudioUI: public ox::SignalHandler { | |||||||
| 		DeleteConfirmation m_deleteConfirmation; | 		DeleteConfirmation m_deleteConfirmation; | ||||||
| 		NewDir m_newDirDialog; | 		NewDir m_newDirDialog; | ||||||
| 		ig::QuestionPopup m_closeFileConfirm{"Close File?", "This file has unsaved changes. Close?"}; | 		ig::QuestionPopup m_closeFileConfirm{"Close File?", "This file has unsaved changes. Close?"}; | ||||||
|  | 		MakeCopyPopup m_copyFilePopup; | ||||||
| 		RenameFile m_renameFile; | 		RenameFile m_renameFile; | ||||||
| 		NewProject m_newProject; | 		NewProject m_newProject; | ||||||
| 		AboutPopup m_aboutPopup; | 		AboutPopup m_aboutPopup; | ||||||
| @@ -117,6 +119,8 @@ class StudioUI: public ox::SignalHandler { | |||||||
|  |  | ||||||
| 		ox::Error openFileActiveTab(ox::StringViewCR path, bool makeActiveTab) noexcept; | 		ox::Error openFileActiveTab(ox::StringViewCR path, bool makeActiveTab) noexcept; | ||||||
|  |  | ||||||
|  | 		ox::Error makeCopyDlg(ox::StringViewCR path) noexcept; | ||||||
|  |  | ||||||
| 		ox::Error handleCloseFileResponse(ig::PopupResponse response) noexcept; | 		ox::Error handleCloseFileResponse(ig::PopupResponse response) noexcept; | ||||||
|  |  | ||||||
| 		ox::Error closeCurrentFile() noexcept; | 		ox::Error closeCurrentFile() noexcept; | ||||||
|   | |||||||
| @@ -92,6 +92,8 @@ class Project: public ox::SignalHandler { | |||||||
|  |  | ||||||
| 		ox::Result<ox::FileStat> stat(ox::StringViewCR path) const noexcept; | 		ox::Result<ox::FileStat> stat(ox::StringViewCR path) const noexcept; | ||||||
|  |  | ||||||
|  | 		ox::Error copyItem(ox::StringViewCR src, ox::StringViewCR dest) noexcept; | ||||||
|  |  | ||||||
| 		ox::Error moveItem(ox::StringViewCR src, ox::StringViewCR dest) noexcept; | 		ox::Error moveItem(ox::StringViewCR src, ox::StringViewCR dest) noexcept; | ||||||
|  |  | ||||||
| 		ox::Error moveDir(ox::StringViewCR src, ox::StringViewCR dest) noexcept; | 		ox::Error moveDir(ox::StringViewCR src, ox::StringViewCR dest) noexcept; | ||||||
|   | |||||||
| @@ -97,6 +97,14 @@ ox::Result<ox::FileStat> Project::stat(ox::StringViewCR path) const noexcept { | |||||||
| 	return m_fs.stat(path); | 	return m_fs.stat(path); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | ox::Error Project::copyItem(ox::StringViewCR src, ox::StringViewCR dest) noexcept { | ||||||
|  | 	OX_REQUIRE_M(buff, loadBuff(src)); | ||||||
|  | 	OX_REQUIRE(id, keel::regenerateUuidHeader(buff)); | ||||||
|  | 	OX_RETURN_ERROR(writeBuff(dest, buff)); | ||||||
|  | 	createUuidMapping(m_kctx, dest, id); | ||||||
|  | 	return {}; | ||||||
|  | } | ||||||
|  |  | ||||||
| ox::Error Project::moveItem(ox::StringViewCR src, ox::StringViewCR dest) noexcept { | ox::Error Project::moveItem(ox::StringViewCR src, ox::StringViewCR dest) noexcept { | ||||||
| 	OX_RETURN_ERROR(m_fs.move(src, dest)); | 	OX_RETURN_ERROR(m_fs.move(src, dest)); | ||||||
| 	OX_RETURN_ERROR(keel::updatePath(m_kctx, src, dest)); | 	OX_RETURN_ERROR(keel::updatePath(m_kctx, src, dest)); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user