[studio] Add ability to move directories
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				Build / build (push) Successful in 3m29s
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	Build / build (push) Successful in 3m29s
				
			This commit is contained in:
		@@ -238,6 +238,9 @@ class AssetManager {
 | 
				
			|||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				ox::Error updateAssetId(ox::StringViewCR oldId, ox::StringViewCR newId) noexcept final {
 | 
									ox::Error updateAssetId(ox::StringViewCR oldId, ox::StringViewCR newId) noexcept final {
 | 
				
			||||||
 | 
										if (!m_cache.contains(oldId)) {
 | 
				
			||||||
 | 
											return {};
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
					auto &o = m_cache[oldId];
 | 
										auto &o = m_cache[oldId];
 | 
				
			||||||
					auto &n = m_cache[newId];
 | 
										auto &n = m_cache[newId];
 | 
				
			||||||
					n = std::move(o);
 | 
										n = std::move(o);
 | 
				
			||||||
@@ -246,11 +249,14 @@ class AssetManager {
 | 
				
			|||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				void gc() noexcept final {
 | 
									void gc() noexcept final {
 | 
				
			||||||
					for (auto const&ack : m_cache.keys()) {
 | 
										for (size_t i = 0; i < m_cache.keys().size();) {
 | 
				
			||||||
 | 
											auto const &ack = m_cache.keys()[i];
 | 
				
			||||||
						auto &ac = m_cache[ack];
 | 
											auto &ac = m_cache[ack];
 | 
				
			||||||
						if (!ac->references()) {
 | 
											if (!ac->references()) {
 | 
				
			||||||
							m_cache.erase(ack);
 | 
												m_cache.erase(ack);
 | 
				
			||||||
 | 
												continue;
 | 
				
			||||||
						}
 | 
											}
 | 
				
			||||||
 | 
											++i;
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -309,6 +315,9 @@ class AssetManager {
 | 
				
			|||||||
			if (m_fileUpdated.contains(newId)) {
 | 
								if (m_fileUpdated.contains(newId)) {
 | 
				
			||||||
				return ox::Error{1, "new asset ID already has an entry"};
 | 
									return ox::Error{1, "new asset ID already has an entry"};
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
								if (!m_fileUpdated.contains(oldId)) {
 | 
				
			||||||
 | 
									return {};
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
			auto &o = m_fileUpdated[oldId];
 | 
								auto &o = m_fileUpdated[oldId];
 | 
				
			||||||
			auto &n = m_fileUpdated[newId];
 | 
								auto &n = m_fileUpdated[newId];
 | 
				
			||||||
			n = std::move(o);
 | 
								n = std::move(o);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,6 +30,10 @@ void ProjectExplorer::fileMoved(ox::StringViewCR src, ox::StringViewCR dst) cons
 | 
				
			|||||||
	moveItem.emit(src, dst);
 | 
						moveItem.emit(src, dst);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void ProjectExplorer::dirMoved(ox::StringViewCR src, ox::StringViewCR dst) const noexcept {
 | 
				
			||||||
 | 
						moveDir.emit(src, dst);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void ProjectExplorer::fileContextMenu(ox::StringViewCR path) const noexcept {
 | 
					void ProjectExplorer::fileContextMenu(ox::StringViewCR path) const noexcept {
 | 
				
			||||||
	if (ImGui::BeginPopupContextItem("FileMenu", ImGuiPopupFlags_MouseButtonRight)) {
 | 
						if (ImGui::BeginPopupContextItem("FileMenu", ImGuiPopupFlags_MouseButtonRight)) {
 | 
				
			||||||
		if (ImGui::MenuItem("Delete")) {
 | 
							if (ImGui::MenuItem("Delete")) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		explicit ProjectExplorer(keel::Context &kctx) noexcept;
 | 
							explicit ProjectExplorer(keel::Context &kctx) noexcept;
 | 
				
			||||||
@@ -33,6 +34,8 @@ class ProjectExplorer final: public FileExplorer {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		void fileMoved(ox::StringViewCR src, ox::StringViewCR dst) const noexcept override;
 | 
							void fileMoved(ox::StringViewCR src, ox::StringViewCR dst) const noexcept override;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							void dirMoved(ox::StringViewCR src, ox::StringViewCR dst) const noexcept override;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		void fileContextMenu(ox::StringViewCR path) const noexcept override;
 | 
							void fileContextMenu(ox::StringViewCR path) const noexcept override;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		void dirContextMenu(ox::StringViewCR path) const noexcept override;
 | 
							void dirContextMenu(ox::StringViewCR path) const noexcept override;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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.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);
 | 
				
			||||||
	m_newProject.finished.connect(this, &StudioUI::createOpenProject);
 | 
						m_newProject.finished.connect(this, &StudioUI::createOpenProject);
 | 
				
			||||||
@@ -486,6 +487,11 @@ ox::Error StudioUI::closeFile(ox::StringViewCR path) noexcept {
 | 
				
			|||||||
	return {};
 | 
						return {};
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ox::Error StudioUI::queueDirMove(ox::StringParam src, ox::StringParam dst) noexcept {
 | 
				
			||||||
 | 
						m_queuedDirMoves.emplace_back(std::move(src), std::move(dst));
 | 
				
			||||||
 | 
						return {};
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ox::Error StudioUI::queueFileMove(ox::StringParam src, ox::StringParam dst) noexcept {
 | 
					ox::Error StudioUI::queueFileMove(ox::StringParam src, ox::StringParam dst) noexcept {
 | 
				
			||||||
	m_queuedMoves.emplace_back(std::move(src), std::move(dst));
 | 
						m_queuedMoves.emplace_back(std::move(src), std::move(dst));
 | 
				
			||||||
	return {};
 | 
						return {};
 | 
				
			||||||
@@ -496,6 +502,10 @@ void StudioUI::procFileMoves() noexcept {
 | 
				
			|||||||
		oxLogError(m_project->moveItem(m.a, m.b));
 | 
							oxLogError(m_project->moveItem(m.a, m.b));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	m_queuedMoves.clear();
 | 
						m_queuedMoves.clear();
 | 
				
			||||||
 | 
						for (auto const &m : m_queuedDirMoves) {
 | 
				
			||||||
 | 
							oxLogError(m_project->moveDir(m.a, m.b));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						m_queuedDirMoves.clear();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -40,7 +40,8 @@ class StudioUI: public ox::SignalHandler {
 | 
				
			|||||||
		BaseEditor *m_activeEditorOnLastDraw = nullptr;
 | 
							BaseEditor *m_activeEditorOnLastDraw = nullptr;
 | 
				
			||||||
		BaseEditor *m_activeEditor = nullptr;
 | 
							BaseEditor *m_activeEditor = nullptr;
 | 
				
			||||||
		BaseEditor *m_activeEditorUpdatePending = nullptr;
 | 
							BaseEditor *m_activeEditorUpdatePending = nullptr;
 | 
				
			||||||
		ox::Vector<ox::Pair<ox::String, ox::String>> m_queuedMoves;
 | 
							ox::Vector<ox::Pair<ox::String>> m_queuedMoves;
 | 
				
			||||||
 | 
							ox::Vector<ox::Pair<ox::String>> m_queuedDirMoves;
 | 
				
			||||||
		NewMenu m_newMenu{keelCtx(m_tctx)};
 | 
							NewMenu m_newMenu{keelCtx(m_tctx)};
 | 
				
			||||||
		DeleteConfirmation m_deleteConfirmation;
 | 
							DeleteConfirmation m_deleteConfirmation;
 | 
				
			||||||
		NewDir m_newDirDialog;
 | 
							NewDir m_newDirDialog;
 | 
				
			||||||
@@ -113,6 +114,8 @@ class StudioUI: public ox::SignalHandler {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		ox::Error closeFile(ox::StringViewCR path) noexcept;
 | 
							ox::Error closeFile(ox::StringViewCR path) noexcept;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							ox::Error queueDirMove(ox::StringParam src, ox::StringParam dst) noexcept;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		ox::Error queueFileMove(ox::StringParam src, ox::StringParam dst) noexcept;
 | 
							ox::Error queueFileMove(ox::StringParam src, ox::StringParam dst) noexcept;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		void procFileMoves() noexcept;
 | 
							void procFileMoves() noexcept;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,10 +13,12 @@ struct FileRef {
 | 
				
			|||||||
	static constexpr auto TypeName = "net.drinkingtea.studio.FileRef";
 | 
						static constexpr auto TypeName = "net.drinkingtea.studio.FileRef";
 | 
				
			||||||
	static constexpr auto TypeVersion = 1;
 | 
						static constexpr auto TypeVersion = 1;
 | 
				
			||||||
	ox::String path;
 | 
						ox::String path;
 | 
				
			||||||
 | 
						bool isDir{};
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
OX_MODEL_BEGIN(FileRef)
 | 
					OX_MODEL_BEGIN(FileRef)
 | 
				
			||||||
	OX_MODEL_FIELD(path)
 | 
						OX_MODEL_FIELD(path)
 | 
				
			||||||
 | 
						OX_MODEL_FIELD(isDir)
 | 
				
			||||||
OX_MODEL_END()
 | 
					OX_MODEL_END()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -47,6 +47,8 @@ class FileExplorer: public ox::SignalHandler {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		virtual void fileMoved(ox::StringViewCR src, ox::StringViewCR dst) const noexcept;
 | 
							virtual void fileMoved(ox::StringViewCR src, ox::StringViewCR dst) const noexcept;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							virtual void dirMoved(ox::StringViewCR src, ox::StringViewCR dst) const noexcept;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		void drawFileContextMenu(ox::CStringViewCR path) const noexcept;
 | 
							void drawFileContextMenu(ox::CStringViewCR path) const noexcept;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		void drawDirContextMenu(ox::CStringViewCR path) const noexcept;
 | 
							void drawDirContextMenu(ox::CStringViewCR path) const noexcept;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -94,6 +94,8 @@ class Project: public ox::SignalHandler {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		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 deleteItem(ox::StringViewCR path) noexcept;
 | 
							ox::Error deleteItem(ox::StringViewCR path) noexcept;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		[[nodiscard]]
 | 
							[[nodiscard]]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -43,6 +43,8 @@ void FileExplorer::fileDeleted(ox::StringViewCR) const noexcept {}
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
void FileExplorer::fileMoved(ox::StringViewCR, ox::StringViewCR) const noexcept {}
 | 
					void FileExplorer::fileMoved(ox::StringViewCR, ox::StringViewCR) const noexcept {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void FileExplorer::dirMoved(ox::StringViewCR, ox::StringViewCR) const noexcept {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void FileExplorer::drawFileContextMenu(ox::CStringViewCR path) const noexcept {
 | 
					void FileExplorer::drawFileContextMenu(ox::CStringViewCR path) const noexcept {
 | 
				
			||||||
	ig::IDStackItem const idStackItem{path};
 | 
						ig::IDStackItem const idStackItem{path};
 | 
				
			||||||
	fileContextMenu(path);
 | 
						fileContextMenu(path);
 | 
				
			||||||
@@ -93,13 +95,23 @@ void FileTreeModel::draw(turbine::Context &tctx) const noexcept {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
		ig::IDStackItem const idStackItem{m_name};
 | 
							ig::IDStackItem const idStackItem{m_name};
 | 
				
			||||||
		m_explorer.drawDirContextMenu(m_fullPath);
 | 
							m_explorer.drawDirContextMenu(m_fullPath);
 | 
				
			||||||
 | 
							if (m_explorer.fileDraggable()) {
 | 
				
			||||||
 | 
								std::ignore = ig::dragDropSource([this] {
 | 
				
			||||||
 | 
									ImGui::Text("%s", m_name.c_str());
 | 
				
			||||||
 | 
									return ig::setDragDropPayload("FileRef", FileRef{ox::String{m_fullPath}, true});
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		if (ig::DragDropTarget const dragDropTarget; dragDropTarget) {
 | 
							if (ig::DragDropTarget const dragDropTarget; dragDropTarget) {
 | 
				
			||||||
			auto const [ref, err] = ig::getDragDropPayload<FileRef>("FileRef");
 | 
								auto const [ref, err] = ig::getDragDropPayload<FileRef>("FileRef");
 | 
				
			||||||
			if (!err) {
 | 
								if (!err) {
 | 
				
			||||||
				auto const name = substr(ref.path, find(ref.path.rbegin(), ref.path.rend(), '/').offset() + 1);
 | 
									auto const name = substr(ref.path, find(ref.path.rbegin(), ref.path.rend(), '/').offset() + 1);
 | 
				
			||||||
 | 
									if (ref.isDir) {
 | 
				
			||||||
 | 
										m_explorer.dirMoved(ref.path, sfmt("{}/{}", m_fullPath, name));
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
					m_explorer.fileMoved(ref.path, sfmt("{}/{}", m_fullPath, name));
 | 
										m_explorer.fileMoved(ref.path, sfmt("{}/{}", m_fullPath, name));
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		if (nodeOpen) {
 | 
							if (nodeOpen) {
 | 
				
			||||||
			for (auto const&child : m_children) {
 | 
								for (auto const&child : m_children) {
 | 
				
			||||||
				child->draw(tctx);
 | 
									child->draw(tctx);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,6 +26,26 @@ static void generateTypes(ox::TypeStore &ts) noexcept {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static ox::Result<ox::Vector<ox::String>> listAllRecursive(ox::FileSystem &fs, ox::StringViewCR path) {
 | 
				
			||||||
 | 
						// there really isn't much recourse if this function fails, just log it and move on
 | 
				
			||||||
 | 
						auto [out, outErr] = fs.ls(path);
 | 
				
			||||||
 | 
						oxLogError(outErr);
 | 
				
			||||||
 | 
						for (auto const &p : out) {
 | 
				
			||||||
 | 
							auto const [stat, statErr] = fs.stat(path);
 | 
				
			||||||
 | 
							if (statErr) {
 | 
				
			||||||
 | 
								oxLogError(statErr);
 | 
				
			||||||
 | 
								continue;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if (stat.fileType == ox::FileType::Directory) {
 | 
				
			||||||
 | 
								OX_REQUIRE_M(l, listAllRecursive(fs, sfmt("{}/{}", path, p)));
 | 
				
			||||||
 | 
								for (auto &c : l) {
 | 
				
			||||||
 | 
									out.emplace_back(std::move(c));
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return std::move(out);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Project::Project(keel::Context &ctx, ox::String path, ox::StringViewCR projectDataDir):
 | 
					Project::Project(keel::Context &ctx, ox::String path, ox::StringViewCR projectDataDir):
 | 
				
			||||||
	m_kctx(ctx),
 | 
						m_kctx(ctx),
 | 
				
			||||||
@@ -78,6 +98,21 @@ ox::Error Project::moveItem(ox::StringViewCR src, ox::StringViewCR dest) noexcep
 | 
				
			|||||||
	return {};
 | 
						return {};
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ox::Error Project::moveDir(ox::StringViewCR src, ox::StringViewCR dest) noexcept {
 | 
				
			||||||
 | 
						OX_REQUIRE(files, listAllRecursive(m_fs, src));
 | 
				
			||||||
 | 
						OX_RETURN_ERROR(m_fs.move(src, dest));
 | 
				
			||||||
 | 
						fileMoved.emit(src, dest, ox::UUID{});
 | 
				
			||||||
 | 
						for (auto const &op : files) {
 | 
				
			||||||
 | 
							auto const name =
 | 
				
			||||||
 | 
								substr(op, ox::find(op.rbegin(), op.rend(), '/').offset() + 1);
 | 
				
			||||||
 | 
							auto const np = sfmt("{}/{}", dest, name);
 | 
				
			||||||
 | 
							OX_RETURN_ERROR(keel::updatePath(m_kctx, op, np));
 | 
				
			||||||
 | 
							OX_REQUIRE(uuid, keel::pathToUuid(m_kctx, dest));
 | 
				
			||||||
 | 
							fileMoved.emit(src, dest, uuid);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return {};
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ox::Error Project::deleteItem(ox::StringViewCR path) noexcept {
 | 
					ox::Error Project::deleteItem(ox::StringViewCR path) noexcept {
 | 
				
			||||||
	OX_REQUIRE(stat, m_fs.stat(path));
 | 
						OX_REQUIRE(stat, m_fs.stat(path));
 | 
				
			||||||
	if (stat.fileType == ox::FileType::Directory) {
 | 
						if (stat.fileType == ox::FileType::Directory) {
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user