diff --git a/GlitchyEditor/src/Assets/AssetHierarchy.bf b/GlitchyEditor/src/Assets/AssetHierarchy.bf index 627bb2c..5d13be3 100644 --- a/GlitchyEditor/src/Assets/AssetHierarchy.bf +++ b/GlitchyEditor/src/Assets/AssetHierarchy.bf @@ -646,4 +646,101 @@ class AssetHierarchy public delegate void FileRenamedFunc(AssetNode node, StringView oldName); public Event OnFileRenamed ~ _.Dispose(); + + public enum CopyError + { + TargetDirectoryIsNotDirectory, + SourceDoesntExist, + TargetExists + } + + public enum OverwriteMode + { + None, + Overwrite, + KeepBoth, + Combine + } + + public Result CopyExternFileToNode(TreeNode targetDirectory, String sourcePath, OverwriteMode overwrite) + { + if (!targetDirectory->IsDirectory) + return .Err(.TargetDirectoryIsNotDirectory); + + if (!File.Exists(sourcePath)) + return .Err(.SourceDoesntExist); + + + FileInfo sourceInfo = scope FileInfo(sourcePath); + + String fileName = scope .(); + Path.GetFileName(sourcePath, fileName); + + String targetPath = scope .(); + Path.Combine(targetPath, targetDirectory->Path, fileName); + + FileInfo targetInfo = scope FileInfo(targetPath); + + if (sourceInfo.IsDirectory) + { + bool forceOverwrite = false; + if (targetInfo.Exists) + { + switch (overwrite) + { + case .KeepBoth: + String fileExtension = scope .(); + Path.GetExtension(targetPath, fileExtension); + FindFreePath(targetDirectory->Path, fileName.Substring(0..<^fileExtension.Length), fileExtension, targetPath..Clear()); + case .Overwrite: + forceOverwrite = true; + default: + return .Err(.TargetExists); + } + } + } + else + { + bool forceOverwrite = false; + if (targetInfo.Exists) + { + switch (overwrite) + { + case .KeepBoth: + String fileExtension = scope .(); + Path.GetExtension(targetPath, fileExtension); + FindFreePath(targetDirectory->Path, fileName.Substring(0..<^fileExtension.Length), fileExtension, targetPath..Clear()); + case .Overwrite: + forceOverwrite = true; + default: + return .Err(.TargetExists); + } + } + + File.Copy(sourcePath, targetPath, forceOverwrite); + } + + return .Ok; + } + + public void FindFreePath(StringView directory, StringView wantedName, StringView fileExtension, String outFreePath) + { + int fileNumber = 0; + + String currentFileName = scope $"{wantedName}{fileExtension}"; + while (true) + { + Path.Combine(outFreePath..Clear(), directory, currentFileName); + + FileInfo targetInfo = scope FileInfo(outFreePath); + + if (!targetInfo.Exists) + { + break; + } + + fileNumber++; + currentFileName..Clear().AppendF($"{wantedName} ({fileNumber}){fileExtension}"); + } + } } diff --git a/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf b/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf index 53bb485..ea0e3f0 100644 --- a/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf +++ b/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf @@ -174,7 +174,9 @@ namespace GlitchyEditor.EditWindows if (ImGui.BeginChild("Files")) { - DrawCurrentDirectory(); + TreeNode currentDirectoryNode = TrySilent!(_manager.AssetHierarchy.GetNodeFromPath(_currentDirectory)); + + DrawCurrentDirectory(currentDirectoryNode); // Context menu when clicking on the background. if (ImGui.BeginPopupContextWindow()) @@ -184,28 +186,11 @@ namespace GlitchyEditor.EditWindows } ImGui.EndChild(); + + FileDropTarget(currentDirectoryNode, .External); } ImGui.EndTable(); - - if (ImGui.BeginDragDropTarget()) - { - ImGui.Payload* peekPayload = ImGui.AcceptDragDropPayload(.ExternFiles, .AcceptBeforeDelivery); - - if (peekPayload != null) - { - if (peekPayload.IsDelivery()) - { - Log.EngineLogger.Info("Buup!"); - } - else - { - EditorLayer.SetDropEffect(.Copy); - } - } - - ImGui.EndDragDropTarget(); - } } ImGui.PopStyleVar(1); @@ -213,6 +198,28 @@ namespace GlitchyEditor.EditWindows ImGui.End(); } + /*private void ExternalFileDropTarget() + { + if (ImGui.BeginDragDropTarget()) + { + ImGui.Payload* peekPayload = ImGui.AcceptDragDropPayload(.ExternFiles, .AcceptBeforeDelivery); + + if (peekPayload != null) + { + if (peekPayload.IsDelivery()) + { + Log.EngineLogger.Info("Buup!"); + } + else + { + EditorLayer.SetDropEffect(.Copy); + } + } + + ImGui.EndDragDropTarget(); + } + }*/ + private void SelectFile(StringView fullFileName) { if (_selectedFile == fullFileName) @@ -399,6 +406,8 @@ namespace GlitchyEditor.EditWindows bool isOpen = ImGui.TreeNodeEx(name, flags, $"{name}"); + FileDropTarget(tree, .Internal | .External); + if (!ImGui.IsItemToggledOpen() && ImGui.IsItemClicked(.Left)) { _currentDirectory.Set(tree->Path); @@ -419,8 +428,8 @@ namespace GlitchyEditor.EditWindows const float2 padding = .(24, 24); - /// Renders the contents of _currentDirectory. - private void DrawCurrentDirectory() + /// Renders the contents of _currentDirectory. Returns the node of the current directory, or null if the browser isn't in a directory. + private void DrawCurrentDirectory(TreeNode currentDirectoryNode) { List> files = null; @@ -443,28 +452,21 @@ namespace GlitchyEditor.EditWindows } else { - if (_currentDirectory.IsEmpty) - return; - - // Get the node of the current directory. - var currentDirectoryNode = _manager.AssetHierarchy.GetNodeFromPath(_currentDirectory); - - if (currentDirectoryNode case .Err) + if (currentDirectoryNode == null) { - Log.EngineLogger.Error($"No node exists for {_currentDirectory}."); ImGui.TextUnformatted("Failed to display contents of directory."); return; } - files = currentDirectoryNode->Children; - + files = currentDirectoryNode.Children; + // show back button (".."-File) - if (currentDirectoryNode->Parent != _manager.AssetHierarchy.RootNode) + if (currentDirectoryNode.Parent != _manager.AssetHierarchy.RootNode) { ImGui.PushID("Back"); - DrawBackButton(currentDirectoryNode->Parent); - + DrawBackButton(currentDirectoryNode.Parent); + // X-Coordinate of the right side of the current entry. float currentButtonRight = ImGui.GetItemRectMax().x; // Expected right-Coordinate if next entry was on the same line. @@ -506,7 +508,7 @@ namespace GlitchyEditor.EditWindows ShowNewFile(); ImGui.PopID(); - } + } } float _zoom = 1.0f; @@ -543,7 +545,7 @@ namespace GlitchyEditor.EditWindows ImGui.PopStyleColor(); - FileDropTarget(entry); + FileDropTarget(entry, .Internal | .External); if (ImGui.IsItemHovered() && ImGui.IsMouseClicked(.Left)) { @@ -564,34 +566,71 @@ namespace GlitchyEditor.EditWindows ImGui.EndChild(); } - /// Makes a drop target for the given asset node that files and directories can be dropped on, so that Files can be moved - private void FileDropTarget(TreeNode dropTarget) + enum DropTargetMode { - if (dropTarget->IsDirectory && ImGui.BeginDragDropTarget()) + Internal = 1, + External = 2 + } + + /// Makes a drop target for the given asset node that files and directories can be dropped on, so that Files can be moved + private void FileDropTarget(TreeNode dropTarget, DropTargetMode mode) + { + if (dropTarget != null && dropTarget->IsDirectory && ImGui.BeginDragDropTarget()) { bool allowDrop = false; - ImGui.Payload* peekPayload = ImGui.AcceptDragDropPayload(.ContentBrowserItem, .AcceptPeekOnly); - - if (peekPayload != null) + if (mode.HasFlag(.Internal)) { - StringView assetIdentifier = .((char8*)peekPayload.Data, (int)peekPayload.DataSize); + ImGui.Payload* peekPayload = ImGui.AcceptDragDropPayload(.ContentBrowserItem, .AcceptBeforeDelivery); - allowDrop = assetIdentifier != dropTarget->Identifier; - } - - if (allowDrop) - { - ImGui.Payload* payload = ImGui.AcceptDragDropPayload(.ContentBrowserItem); - - if (payload != null) + if (peekPayload != null) { - StringView movedChild = .((char8*)payload.Data, (int)payload.DataSize); + StringView assetIdentifier = .((char8*)peekPayload.Data, (int)peekPayload.DataSize); - _manager.AssetHierarchy.MoveFileToNode(movedChild, dropTarget); + allowDrop = assetIdentifier != dropTarget->Identifier; + + if (allowDrop && peekPayload.IsDelivery()) + { + _manager.AssetHierarchy.MoveFileToNode(assetIdentifier, dropTarget); + } } } + if (mode.HasFlag(.External)) + { + ImGui.Payload* peekPayload = ImGui.AcceptDragDropPayload(.ExternFiles, .AcceptBeforeDelivery); + + if (peekPayload != null) + { + if (peekPayload.IsDelivery()) + { + Log.EngineLogger.Info($"Dropping into {dropTarget->Path}"); + + List droppedFiles = (.)Internal.UnsafeCastToObject(*(void**)peekPayload.Data); + + for (String path in droppedFiles) + { + Log.EngineLogger.Info($"Dropping file {path}"); + + _manager.AssetHierarchy.CopyExternFileToNode(dropTarget, path, .None); + } + } + else + { + EditorLayer.SetDropEffect(.Copy); + + if (dropTarget->Path == _currentDirectory) + { + ImGui.SetTooltip($"Copy here ({dropTarget->Name})"); + } + else + { + ImGui.SetTooltip($"Copy to {dropTarget->Name}"); + } + } + } + } + ImGui.EndDragDropTarget(); } } @@ -643,7 +682,7 @@ namespace GlitchyEditor.EditWindows ImGui.EndDragDropSource(); } - FileDropTarget(entry); + FileDropTarget(entry, .Internal | .External); if (ImGui.IsItemHovered() && ImGui.IsMouseClicked(.Left)) { diff --git a/GlitchyEditor/src/EditorLayer.bf b/GlitchyEditor/src/EditorLayer.bf index 7862061..acfb8bf 100644 --- a/GlitchyEditor/src/EditorLayer.bf +++ b/GlitchyEditor/src/EditorLayer.bf @@ -140,9 +140,10 @@ namespace GlitchyEditor return .Ok; } - public override Result OnDrop(int2 cursorPosition) + public override Result OnDrop(int2 cursorPosition, List fileNames) { DragDropEvent event = scope DragDropEvent(.Drop, cursorPosition); + event.FileNames = fileNames; Application.Instance.OnEvent(event); return event.OutDropEffect; } @@ -1204,16 +1205,29 @@ namespace GlitchyEditor ImGui.Viewport* viewport = ImGui.GetMainViewport(); ImGui.DockSpaceOverViewport(viewport); - if (_isDraggingFromOutside) + if (_isDraggingFromOutside not case .None) { // Reset drop effect, so the drop targets have to set it themselves (and don't "leak" it). SetDropEffect(.None); if (ImGui.BeginDragDropSource(.SourceExtern | .SourceAutoExpirePayload | .SourceNoPreviewTooltip)) { - ImGui.SetDragDropPayload(.ExternFiles, null, 0); + void* ptr = Internal.UnsafeCastToPtr(_droppedFiles); + ImGui.SetDragDropPayload(.ExternFiles, &ptr, sizeof(int)); + ImGui.EndDragDropSource(); } + + if (_isDraggingFromOutside case .Dropping) + { + _isDraggingFromOutside = .None; + } + } + else + { + // TODO: Can Imgui handle this, somehow? + DeleteContainerAndItems!(_droppedFiles); + _droppedFiles = null; } DrawMainMenuBar(); @@ -1671,14 +1685,23 @@ namespace GlitchyEditor return false; } - private bool _isDraggingFromOutside; + enum DragDropState + { + None, + Dragging, + Dropping + } + + private DragDropState _isDraggingFromOutside; private DropEffect _effectWhenDropping = .None; private static EditorLayer _layer; + private List _droppedFiles; + public static void SetDropEffect(DropEffect effect) { - if (!_layer._isDraggingFromOutside) + if (_layer._isDraggingFromOutside == .None) { Log.EngineLogger.Warning("Setting drop effect, eventhou the user is currently not dragging."); } @@ -1688,27 +1711,46 @@ namespace GlitchyEditor private bool OnDragDrop(DragDropEvent e) { - Log.EngineLogger.Info($"D'n'D: {e.DragDropType} {e.CursorPosition.X} {e.CursorPosition.Y}"); - if (all(e.CursorPosition != .(-1,-1))) { ImGui.GetIO().MousePos = .(e.CursorPosition.X, e.CursorPosition.Y); } + if (e.FileNames == null) + { + DeleteContainerAndItems!(_droppedFiles); + _droppedFiles = null; + } + else + { + if (_droppedFiles == null) + { + _droppedFiles = new List(); + } + + for (String fileName in e.FileNames) + { + if (!_droppedFiles.Contains(fileName, .Ordinal)) + { + _droppedFiles.Add(new String(fileName)); + } + } + } + switch (e.DragDropType) { case .Enter: - _isDraggingFromOutside = true; + _isDraggingFromOutside = .Dragging; ImGui.GetIO().MouseDown[0] = true; case .Over: case .Leave: - _isDraggingFromOutside = false; + _isDraggingFromOutside = .None; ImGui.ClearDragDrop(); case .Drop: - _isDraggingFromOutside = false; + _isDraggingFromOutside = .Dropping; ImGui.GetIO().MouseDown[0] = false; } - + e.OutDropEffect = _effectWhenDropping; return true; diff --git a/GlitchyEditor/src/Platform/Windows/DropTarget.bf b/GlitchyEditor/src/Platform/Windows/DropTarget.bf index 2842902..f4dd7b7 100644 --- a/GlitchyEditor/src/Platform/Windows/DropTarget.bf +++ b/GlitchyEditor/src/Platform/Windows/DropTarget.bf @@ -279,7 +279,7 @@ abstract class IDropTargetImplBase : RefCounted public abstract Result OnDragEnter(int2 cursorPosition); public abstract Result OnDragOver(int2 cursorPosition); public abstract Result OnDragLeave(); - public abstract Result OnDrop(int2 cursorPosition); + public abstract Result OnDrop(int2 cursorPosition, List fileNames); [CallingConvention(.Stdcall)] private static HResult DragEnterImpl(IDropTarget* self, /*IDataObject*/ IUnknown* dataObject, uint32 grfKeyState, int2 point, ref DropEffect effect) @@ -338,11 +338,16 @@ abstract class IDropTargetImplBase : RefCounted tymed = (.)TYMED.HGLOBAL }; + List files = null; + defer {DeleteContainerAndItems!(files);} + if (dataObject.GetData(ref format, var stgm).Succeeded) { HDROP hdrop = (HDROP)stgm.hGlobal; uint32 fileCount = DragQueryFileW(hdrop, 0xFFFFFFFF, null, 0); - + + files = new List(fileCount); + List buffer = scope List(); buffer.Resize(256); @@ -354,15 +359,15 @@ abstract class IDropTargetImplBase : RefCounted uint32 retrievedSize = DragQueryFileW(hdrop, i, buffer.Ptr, (.)buffer.Count); if (retrievedSize > 0 && retrievedSize < buffer.Count) { - String str = scope String(buffer.Ptr); - Log.EngineLogger.Info($"Dropped File: {str}"); + String str = new String(buffer.Ptr); + files.Add(str); } } ReleaseStgMedium(ref stgm); } - Result result = instance.OnDrop(point); + Result result = instance.OnDrop(point, files); if (result case .Ok(out effect)) return .S_OK; @@ -425,6 +430,9 @@ public class DragDropEvent : Event, IEvent public DropEffect OutDropEffect { get; set; } + // Do not keep references to this list or any of it's items outside of the event handler, they will not survive it. + public List FileNames { get; set; } + // TODO: Keys, or perhaps just make the listeners query them... public this(DragDropType dragDropType, int2 cursorPosition)