Basic copy for drag'n'drop

This commit is contained in:
Simon Lübeß
2025-06-20 21:51:46 +02:00
parent 68745f0515
commit 6af2e49387
4 changed files with 257 additions and 71 deletions
@@ -646,4 +646,101 @@ class AssetHierarchy
public delegate void FileRenamedFunc(AssetNode node, StringView oldName); public delegate void FileRenamedFunc(AssetNode node, StringView oldName);
public Event<FileRenamedFunc> OnFileRenamed ~ _.Dispose(); public Event<FileRenamedFunc> OnFileRenamed ~ _.Dispose();
public enum CopyError
{
TargetDirectoryIsNotDirectory,
SourceDoesntExist,
TargetExists
}
public enum OverwriteMode
{
None,
Overwrite,
KeepBoth,
Combine
}
public Result<void, CopyError> CopyExternFileToNode(TreeNode<AssetNode> 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}");
}
}
} }
@@ -174,7 +174,9 @@ namespace GlitchyEditor.EditWindows
if (ImGui.BeginChild("Files")) if (ImGui.BeginChild("Files"))
{ {
DrawCurrentDirectory(); TreeNode<AssetNode> currentDirectoryNode = TrySilent!(_manager.AssetHierarchy.GetNodeFromPath(_currentDirectory));
DrawCurrentDirectory(currentDirectoryNode);
// Context menu when clicking on the background. // Context menu when clicking on the background.
if (ImGui.BeginPopupContextWindow()) if (ImGui.BeginPopupContextWindow())
@@ -184,10 +186,20 @@ namespace GlitchyEditor.EditWindows
} }
ImGui.EndChild(); ImGui.EndChild();
FileDropTarget(currentDirectoryNode, .External);
} }
ImGui.EndTable(); ImGui.EndTable();
}
ImGui.PopStyleVar(1);
ImGui.End();
}
/*private void ExternalFileDropTarget()
{
if (ImGui.BeginDragDropTarget()) if (ImGui.BeginDragDropTarget())
{ {
ImGui.Payload* peekPayload = ImGui.AcceptDragDropPayload(.ExternFiles, .AcceptBeforeDelivery); ImGui.Payload* peekPayload = ImGui.AcceptDragDropPayload(.ExternFiles, .AcceptBeforeDelivery);
@@ -206,12 +218,7 @@ namespace GlitchyEditor.EditWindows
ImGui.EndDragDropTarget(); ImGui.EndDragDropTarget();
} }
} }*/
ImGui.PopStyleVar(1);
ImGui.End();
}
private void SelectFile(StringView fullFileName) private void SelectFile(StringView fullFileName)
{ {
@@ -399,6 +406,8 @@ namespace GlitchyEditor.EditWindows
bool isOpen = ImGui.TreeNodeEx(name, flags, $"{name}"); bool isOpen = ImGui.TreeNodeEx(name, flags, $"{name}");
FileDropTarget(tree, .Internal | .External);
if (!ImGui.IsItemToggledOpen() && ImGui.IsItemClicked(.Left)) if (!ImGui.IsItemToggledOpen() && ImGui.IsItemClicked(.Left))
{ {
_currentDirectory.Set(tree->Path); _currentDirectory.Set(tree->Path);
@@ -419,8 +428,8 @@ namespace GlitchyEditor.EditWindows
const float2 padding = .(24, 24); const float2 padding = .(24, 24);
/// Renders the contents of _currentDirectory. /// 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() private void DrawCurrentDirectory(TreeNode<AssetNode> currentDirectoryNode)
{ {
List<TreeNode<AssetNode>> files = null; List<TreeNode<AssetNode>> files = null;
@@ -443,27 +452,20 @@ namespace GlitchyEditor.EditWindows
} }
else else
{ {
if (_currentDirectory.IsEmpty) if (currentDirectoryNode == null)
return;
// Get the node of the current directory.
var currentDirectoryNode = _manager.AssetHierarchy.GetNodeFromPath(_currentDirectory);
if (currentDirectoryNode case .Err)
{ {
Log.EngineLogger.Error($"No node exists for {_currentDirectory}.");
ImGui.TextUnformatted("Failed to display contents of directory."); ImGui.TextUnformatted("Failed to display contents of directory.");
return; return;
} }
files = currentDirectoryNode->Children; files = currentDirectoryNode.Children;
// show back button (".."-File) // show back button (".."-File)
if (currentDirectoryNode->Parent != _manager.AssetHierarchy.RootNode) if (currentDirectoryNode.Parent != _manager.AssetHierarchy.RootNode)
{ {
ImGui.PushID("Back"); ImGui.PushID("Back");
DrawBackButton(currentDirectoryNode->Parent); DrawBackButton(currentDirectoryNode.Parent);
// X-Coordinate of the right side of the current entry. // X-Coordinate of the right side of the current entry.
float currentButtonRight = ImGui.GetItemRectMax().x; float currentButtonRight = ImGui.GetItemRectMax().x;
@@ -543,7 +545,7 @@ namespace GlitchyEditor.EditWindows
ImGui.PopStyleColor(); ImGui.PopStyleColor();
FileDropTarget(entry); FileDropTarget(entry, .Internal | .External);
if (ImGui.IsItemHovered() && ImGui.IsMouseClicked(.Left)) if (ImGui.IsItemHovered() && ImGui.IsMouseClicked(.Left))
{ {
@@ -564,31 +566,68 @@ namespace GlitchyEditor.EditWindows
ImGui.EndChild(); 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 enum DropTargetMode
private void FileDropTarget(TreeNode<AssetNode> dropTarget)
{ {
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<AssetNode> dropTarget, DropTargetMode mode)
{
if (dropTarget != null && dropTarget->IsDirectory && ImGui.BeginDragDropTarget())
{ {
bool allowDrop = false; bool allowDrop = false;
ImGui.Payload* peekPayload = ImGui.AcceptDragDropPayload(.ContentBrowserItem, .AcceptPeekOnly); if (mode.HasFlag(.Internal))
{
ImGui.Payload* peekPayload = ImGui.AcceptDragDropPayload(.ContentBrowserItem, .AcceptBeforeDelivery);
if (peekPayload != null) if (peekPayload != null)
{ {
StringView assetIdentifier = .((char8*)peekPayload.Data, (int)peekPayload.DataSize); StringView assetIdentifier = .((char8*)peekPayload.Data, (int)peekPayload.DataSize);
allowDrop = assetIdentifier != dropTarget->Identifier; allowDrop = assetIdentifier != dropTarget->Identifier;
if (allowDrop && peekPayload.IsDelivery())
{
_manager.AssetHierarchy.MoveFileToNode(assetIdentifier, dropTarget);
}
}
} }
if (allowDrop) if (mode.HasFlag(.External))
{ {
ImGui.Payload* payload = ImGui.AcceptDragDropPayload(.ContentBrowserItem); ImGui.Payload* peekPayload = ImGui.AcceptDragDropPayload(.ExternFiles, .AcceptBeforeDelivery);
if (payload != null) if (peekPayload != null)
{ {
StringView movedChild = .((char8*)payload.Data, (int)payload.DataSize); if (peekPayload.IsDelivery())
{
Log.EngineLogger.Info($"Dropping into {dropTarget->Path}");
_manager.AssetHierarchy.MoveFileToNode(movedChild, dropTarget); List<String> 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}");
}
}
} }
} }
@@ -643,7 +682,7 @@ namespace GlitchyEditor.EditWindows
ImGui.EndDragDropSource(); ImGui.EndDragDropSource();
} }
FileDropTarget(entry); FileDropTarget(entry, .Internal | .External);
if (ImGui.IsItemHovered() && ImGui.IsMouseClicked(.Left)) if (ImGui.IsItemHovered() && ImGui.IsMouseClicked(.Left))
{ {
+52 -10
View File
@@ -140,9 +140,10 @@ namespace GlitchyEditor
return .Ok; return .Ok;
} }
public override Result<DropEffect> OnDrop(int2 cursorPosition) public override Result<DropEffect> OnDrop(int2 cursorPosition, List<String> fileNames)
{ {
DragDropEvent event = scope DragDropEvent(.Drop, cursorPosition); DragDropEvent event = scope DragDropEvent(.Drop, cursorPosition);
event.FileNames = fileNames;
Application.Instance.OnEvent(event); Application.Instance.OnEvent(event);
return event.OutDropEffect; return event.OutDropEffect;
} }
@@ -1204,16 +1205,29 @@ namespace GlitchyEditor
ImGui.Viewport* viewport = ImGui.GetMainViewport(); ImGui.Viewport* viewport = ImGui.GetMainViewport();
ImGui.DockSpaceOverViewport(viewport); 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). // Reset drop effect, so the drop targets have to set it themselves (and don't "leak" it).
SetDropEffect(.None); SetDropEffect(.None);
if (ImGui.BeginDragDropSource(.SourceExtern | .SourceAutoExpirePayload | .SourceNoPreviewTooltip)) if (ImGui.BeginDragDropSource(.SourceExtern | .SourceAutoExpirePayload | .SourceNoPreviewTooltip))
{ {
ImGui.SetDragDropPayload(.ExternFiles, null, 0); void* ptr = Internal.UnsafeCastToPtr(_droppedFiles);
ImGui.SetDragDropPayload(.ExternFiles, &ptr, sizeof(int));
ImGui.EndDragDropSource(); ImGui.EndDragDropSource();
} }
if (_isDraggingFromOutside case .Dropping)
{
_isDraggingFromOutside = .None;
}
}
else
{
// TODO: Can Imgui handle this, somehow?
DeleteContainerAndItems!(_droppedFiles);
_droppedFiles = null;
} }
DrawMainMenuBar(); DrawMainMenuBar();
@@ -1671,14 +1685,23 @@ namespace GlitchyEditor
return false; return false;
} }
private bool _isDraggingFromOutside; enum DragDropState
{
None,
Dragging,
Dropping
}
private DragDropState _isDraggingFromOutside;
private DropEffect _effectWhenDropping = .None; private DropEffect _effectWhenDropping = .None;
private static EditorLayer _layer; private static EditorLayer _layer;
private List<String> _droppedFiles;
public static void SetDropEffect(DropEffect effect) 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."); Log.EngineLogger.Warning("Setting drop effect, eventhou the user is currently not dragging.");
} }
@@ -1688,24 +1711,43 @@ namespace GlitchyEditor
private bool OnDragDrop(DragDropEvent e) 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))) if (all(e.CursorPosition != .(-1,-1)))
{ {
ImGui.GetIO().MousePos = .(e.CursorPosition.X, e.CursorPosition.Y); ImGui.GetIO().MousePos = .(e.CursorPosition.X, e.CursorPosition.Y);
} }
if (e.FileNames == null)
{
DeleteContainerAndItems!(_droppedFiles);
_droppedFiles = null;
}
else
{
if (_droppedFiles == null)
{
_droppedFiles = new List<String>();
}
for (String fileName in e.FileNames)
{
if (!_droppedFiles.Contains(fileName, .Ordinal))
{
_droppedFiles.Add(new String(fileName));
}
}
}
switch (e.DragDropType) switch (e.DragDropType)
{ {
case .Enter: case .Enter:
_isDraggingFromOutside = true; _isDraggingFromOutside = .Dragging;
ImGui.GetIO().MouseDown[0] = true; ImGui.GetIO().MouseDown[0] = true;
case .Over: case .Over:
case .Leave: case .Leave:
_isDraggingFromOutside = false; _isDraggingFromOutside = .None;
ImGui.ClearDragDrop(); ImGui.ClearDragDrop();
case .Drop: case .Drop:
_isDraggingFromOutside = false; _isDraggingFromOutside = .Dropping;
ImGui.GetIO().MouseDown[0] = false; ImGui.GetIO().MouseDown[0] = false;
} }
@@ -279,7 +279,7 @@ abstract class IDropTargetImplBase : RefCounted
public abstract Result<DropEffect> OnDragEnter(int2 cursorPosition); public abstract Result<DropEffect> OnDragEnter(int2 cursorPosition);
public abstract Result<DropEffect> OnDragOver(int2 cursorPosition); public abstract Result<DropEffect> OnDragOver(int2 cursorPosition);
public abstract Result<void> OnDragLeave(); public abstract Result<void> OnDragLeave();
public abstract Result<DropEffect> OnDrop(int2 cursorPosition); public abstract Result<DropEffect> OnDrop(int2 cursorPosition, List<String> fileNames);
[CallingConvention(.Stdcall)] [CallingConvention(.Stdcall)]
private static HResult DragEnterImpl(IDropTarget* self, /*IDataObject*/ IUnknown* dataObject, uint32 grfKeyState, int2 point, ref DropEffect effect) 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 tymed = (.)TYMED.HGLOBAL
}; };
List<String> files = null;
defer {DeleteContainerAndItems!(files);}
if (dataObject.GetData(ref format, var stgm).Succeeded) if (dataObject.GetData(ref format, var stgm).Succeeded)
{ {
HDROP hdrop = (HDROP)stgm.hGlobal; HDROP hdrop = (HDROP)stgm.hGlobal;
uint32 fileCount = DragQueryFileW(hdrop, 0xFFFFFFFF, null, 0); uint32 fileCount = DragQueryFileW(hdrop, 0xFFFFFFFF, null, 0);
files = new List<String>(fileCount);
List<char16> buffer = scope List<char16>(); List<char16> buffer = scope List<char16>();
buffer.Resize(256); buffer.Resize(256);
@@ -354,15 +359,15 @@ abstract class IDropTargetImplBase : RefCounted
uint32 retrievedSize = DragQueryFileW(hdrop, i, buffer.Ptr, (.)buffer.Count); uint32 retrievedSize = DragQueryFileW(hdrop, i, buffer.Ptr, (.)buffer.Count);
if (retrievedSize > 0 && retrievedSize < buffer.Count) if (retrievedSize > 0 && retrievedSize < buffer.Count)
{ {
String str = scope String(buffer.Ptr); String str = new String(buffer.Ptr);
Log.EngineLogger.Info($"Dropped File: {str}"); files.Add(str);
} }
} }
ReleaseStgMedium(ref stgm); ReleaseStgMedium(ref stgm);
} }
Result<DropEffect> result = instance.OnDrop(point); Result<DropEffect> result = instance.OnDrop(point, files);
if (result case .Ok(out effect)) if (result case .Ok(out effect))
return .S_OK; return .S_OK;
@@ -425,6 +430,9 @@ public class DragDropEvent : Event, IEvent
public DropEffect OutDropEffect { get; set; } 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<String> FileNames { get; set; }
// TODO: Keys, or perhaps just make the listeners query them... // TODO: Keys, or perhaps just make the listeners query them...
public this(DragDropType dragDropType, int2 cursorPosition) public this(DragDropType dragDropType, int2 cursorPosition)