[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> regenerateUuidHeader(ox::Buffer &buff) noexcept; | ||||
|  | ||||
| ox::Error writeUuidHeader(ox::Writer_c auto &writer, ox::UUID const&uuid) noexcept { | ||||
| 	OX_RETURN_ERROR(write(writer, "K1;")); | ||||
| 	OX_RETURN_ERROR(uuid.toString(writer)); | ||||
|   | ||||
| @@ -8,15 +8,25 @@ namespace keel { | ||||
|  | ||||
| ox::Result<ox::UUID> readUuidHeader(ox::BufferView buff) noexcept { | ||||
| 	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;"; | ||||
| 	if (k1Hdr != ox::StringView(buff.data(), k1Hdr.bytes())) [[unlikely]] { | ||||
| 		return ox::Error(2, "No Keel asset header data"); | ||||
| 	if (k1Hdr != ox::StringView{buff.data(), k1Hdr.bytes()}) [[unlikely]] { | ||||
| 		return ox::Error{2, "No Keel asset header data"}; | ||||
| 	} | ||||
| 	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 { | ||||
| 	std::size_t offset = 0; | ||||
| 	if (!readUuidHeader(buff).error) { | ||||
|   | ||||
| @@ -5,6 +5,7 @@ add_library( | ||||
| 		deleteconfirmation.cpp | ||||
| 		filedialogmanager.cpp | ||||
| 		main.cpp | ||||
| 		makecopypopup.cpp | ||||
| 		newdir.cpp | ||||
| 		newmenu.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")) { | ||||
| 			renameItem.emit(path); | ||||
| 		} | ||||
| 		if (ImGui::MenuItem("Make Copy")) { | ||||
| 			makeCopy.emit(path); | ||||
| 		} | ||||
| 		ImGui::EndPopup(); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -20,6 +20,7 @@ class ProjectExplorer final: public FileExplorer { | ||||
| 		ox::Signal<ox::Error(ox::StringViewCR)> addDir; | ||||
| 		ox::Signal<ox::Error(ox::StringViewCR)> deleteItem; | ||||
| 		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)> moveItem; | ||||
|  | ||||
|   | ||||
| @@ -59,6 +59,7 @@ StudioUI::StudioUI(turbine::Context &ctx, ox::StringParam projectDataDir) noexce | ||||
| 	m_projectExplorer.addItem.connect(this, &StudioUI::addFile); | ||||
| 	m_projectExplorer.deleteItem.connect(this, &StudioUI::deleteFile); | ||||
| 	m_projectExplorer.renameItem.connect(this, &StudioUI::renameFile); | ||||
| 	m_projectExplorer.makeCopy.connect(this, &StudioUI::makeCopyDlg); | ||||
| 	m_projectExplorer.moveDir.connect(this, &StudioUI::queueDirMove); | ||||
| 	m_projectExplorer.moveItem.connect(this, &StudioUI::queueFileMove); | ||||
| 	m_renameFile.moveFile.connect(this, &StudioUI::queueFileMove); | ||||
| @@ -136,6 +137,7 @@ void StudioUI::draw() noexcept { | ||||
| 			p->draw(m_sctx); | ||||
| 		} | ||||
| 		m_closeFileConfirm.draw(m_sctx); | ||||
| 		m_copyFilePopup.draw(m_sctx, {250, 0}); | ||||
| 	} | ||||
| 	ImGui::End(); | ||||
| 	handleKeyInput(); | ||||
| @@ -451,6 +453,7 @@ ox::Error StudioUI::openProjectPath(ox::StringParam path) noexcept { | ||||
| 	m_sctx.project = m_project.get(); | ||||
| 	turbine::setWindowTitle(m_tctx, ox::sfmt("{} - {}", keelCtx(m_tctx).appName, m_project->projectPath())); | ||||
| 	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_project->dirAdded.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 {}; | ||||
| } | ||||
|  | ||||
| ox::Error StudioUI::makeCopyDlg(ox::StringViewCR path) noexcept { | ||||
| 	return m_copyFilePopup.open(path); | ||||
| } | ||||
|  | ||||
| ox::Error StudioUI::handleCloseFileResponse(ig::PopupResponse const response) noexcept { | ||||
| 	if (response == ig::PopupResponse::OK && m_activeEditor) { | ||||
| 		return closeCurrentFile(); | ||||
|   | ||||
| @@ -16,6 +16,7 @@ | ||||
|  | ||||
| #include "aboutpopup.hpp" | ||||
| #include "deleteconfirmation.hpp" | ||||
| #include "makecopypopup.hpp" | ||||
| #include "newdir.hpp" | ||||
| #include "newmenu.hpp" | ||||
| #include "newproject.hpp" | ||||
| @@ -48,6 +49,7 @@ class StudioUI: public ox::SignalHandler { | ||||
| 		DeleteConfirmation m_deleteConfirmation; | ||||
| 		NewDir m_newDirDialog; | ||||
| 		ig::QuestionPopup m_closeFileConfirm{"Close File?", "This file has unsaved changes. Close?"}; | ||||
| 		MakeCopyPopup m_copyFilePopup; | ||||
| 		RenameFile m_renameFile; | ||||
| 		NewProject m_newProject; | ||||
| 		AboutPopup m_aboutPopup; | ||||
| @@ -117,6 +119,8 @@ class StudioUI: public ox::SignalHandler { | ||||
|  | ||||
| 		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 closeCurrentFile() noexcept; | ||||
|   | ||||
| @@ -92,6 +92,8 @@ class Project: public ox::SignalHandler { | ||||
|  | ||||
| 		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 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); | ||||
| } | ||||
|  | ||||
| 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_RETURN_ERROR(m_fs.move(src, dest)); | ||||
| 	OX_RETURN_ERROR(keel::updatePath(m_kctx, src, dest)); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user