using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.ComTypes;
using System.Text;
using Dalamud.Bindings.ImGui;
using Dalamud.Utility;
using Serilog;
namespace Dalamud.Interface.DragDrop;
/// Implements the IDropTarget interface to interact with external drag and dropping.
internal partial class DragDropManager : DragDropManager.IDropTarget
{
private int lastUpdateFrame = -1;
private DragDropInterop.ModifierKeys lastKeyState = DragDropInterop.ModifierKeys.MK_NONE;
/// Create the drag and drop formats we accept.
private FORMATETC formatEtc =
new()
{
cfFormat = (short)DragDropInterop.ClipboardFormat.CF_HDROP,
ptd = nint.Zero,
dwAspect = DVASPECT.DVASPECT_CONTENT,
lindex = -1,
tymed = TYMED.TYMED_HGLOBAL,
};
///
/// Invoked whenever a drag and drop process drags files into any FFXIV-related viewport.
///
/// The drag and drop data.
/// The mouse button used to drag as well as key modifiers.
/// The global cursor position.
/// Effects that can be used with this drag and drop process.
public void DragEnter(IDataObject pDataObj, uint grfKeyState, POINTL pt, ref uint pdwEffect)
{
this.IsDragging = true;
this.lastKeyState = UpdateIo((DragDropInterop.ModifierKeys)grfKeyState, true);
if (pDataObj.QueryGetData(ref this.formatEtc) != 0)
{
pdwEffect = 0;
}
else
{
pdwEffect &= (uint)DragDropInterop.DropEffects.Copy;
(this.Files, this.Directories) = this.GetPaths(pDataObj);
this.Extensions = this.Files.Select(Path.GetExtension).Where(p => !p.IsNullOrEmpty()).Distinct().ToHashSet();
}
Log.Debug("[DragDrop] Entering external Drag and Drop with {KeyState} at {PtX}, {PtY} and with {N} files.", (DragDropInterop.ModifierKeys)grfKeyState, pt.X, pt.Y, this.Files.Count + this.Directories.Count);
}
/// Invoked every windows update-frame as long as the drag and drop process keeps hovering over an FFXIV-related viewport.
/// The mouse button used to drag as well as key modifiers.
/// The global cursor position.
/// Effects that can be used with this drag and drop process.
/// Can be invoked more often than once a XIV frame, so we are keeping track of frames to skip unnecessary updates.
public void DragOver(uint grfKeyState, POINTL pt, ref uint pdwEffect)
{
var frame = ImGui.GetFrameCount();
if (frame != this.lastUpdateFrame)
{
this.lastUpdateFrame = frame;
this.lastKeyState = UpdateIo((DragDropInterop.ModifierKeys)grfKeyState, false);
pdwEffect &= (uint)DragDropInterop.DropEffects.Copy;
Log.Verbose("[DragDrop] External Drag and Drop with {KeyState} at {PtX}, {PtY}.", (DragDropInterop.ModifierKeys)grfKeyState, pt.X, pt.Y);
}
}
/// Invoked whenever a drag and drop process that hovered over any FFXIV-related viewport leaves all FFXIV-related viewports.
public void DragLeave()
{
this.IsDragging = false;
this.Files = Array.Empty();
this.Directories = Array.Empty();
this.Extensions = new HashSet();
MouseDrop(this.lastKeyState);
Log.Debug("[DragDrop] Leaving external Drag and Drop.");
}
/// Invoked whenever a drag process ends by dropping over any FFXIV-related viewport.
/// The drag and drop data.
/// The mouse button used to drag as well as key modifiers.
/// The global cursor position.
/// Effects that can be used with this drag and drop process.
public void Drop(IDataObject pDataObj, uint grfKeyState, POINTL pt, ref uint pdwEffect)
{
MouseDrop(this.lastKeyState);
this.lastDropFrame = ImGui.GetFrameCount();
this.IsDragging = false;
if (this.HasPaths)
{
pdwEffect &= (uint)DragDropInterop.DropEffects.Copy;
}
else
{
pdwEffect = 0;
}
Log.Debug("[DragDrop] Dropping {N} files with {KeyState} at {PtX}, {PtY}.", this.Files.Count + this.Directories.Count, (DragDropInterop.ModifierKeys)grfKeyState, pt.X, pt.Y);
}
private static DragDropInterop.ModifierKeys UpdateIo(DragDropInterop.ModifierKeys keys, bool entering)
{
var io = ImGui.GetIO();
void UpdateMouse(int mouseIdx)
{
if (entering)
{
io.MouseDownDuration[mouseIdx] = 1f;
}
io.MouseDown[mouseIdx] = true;
io.AddMouseButtonEvent(mouseIdx, true);
}
if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_LBUTTON))
{
UpdateMouse(0);
}
if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_RBUTTON))
{
UpdateMouse(1);
}
if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_MBUTTON))
{
UpdateMouse(2);
}
if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_CONTROL))
{
io.KeyCtrl = true;
io.AddKeyEvent(ImGuiKey.LeftCtrl, true);
}
else
{
io.KeyCtrl = false;
io.AddKeyEvent(ImGuiKey.LeftCtrl, false);
}
if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_ALT))
{
io.KeyAlt = true;
io.AddKeyEvent(ImGuiKey.LeftAlt, true);
}
else
{
io.KeyAlt = false;
io.AddKeyEvent(ImGuiKey.LeftAlt, false);
}
if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_SHIFT))
{
io.KeyShift = true;
io.AddKeyEvent(ImGuiKey.LeftShift, true);
}
else
{
io.KeyShift = false;
io.AddKeyEvent(ImGuiKey.LeftShift, false);
}
return keys;
}
private static void MouseDrop(DragDropInterop.ModifierKeys keys)
{
var io = ImGui.GetIO();
void UpdateMouse(int mouseIdx)
{
io.AddMouseButtonEvent(mouseIdx, false);
io.MouseDown[mouseIdx] = false;
}
if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_LBUTTON))
{
UpdateMouse(0);
}
if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_RBUTTON))
{
UpdateMouse(1);
}
if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_MBUTTON))
{
UpdateMouse(2);
}
}
private (string[] Files, string[] Directories) GetPaths(IDataObject data)
{
if (!this.IsDragging)
{
return (Array.Empty(), Array.Empty());
}
try
{
data.GetData(ref this.formatEtc, out var stgMedium);
var numFiles = DragDropInterop.DragQueryFileW(stgMedium.unionmember, uint.MaxValue, new StringBuilder(), 0);
var files = new string[numFiles];
var sb = new StringBuilder(1024);
var directoryCount = 0;
var fileCount = 0;
for (var i = 0u; i < numFiles; ++i)
{
sb.Clear();
var ret = DragDropInterop.DragQueryFileW(stgMedium.unionmember, i, sb, sb.Capacity);
if (ret >= sb.Capacity)
{
sb.Capacity = ret + 1;
ret = DragDropInterop.DragQueryFileW(stgMedium.unionmember, i, sb, sb.Capacity);
}
if (ret > 0 && ret < sb.Capacity)
{
var s = sb.ToString();
if (Directory.Exists(s))
{
files[^(++directoryCount)] = s;
}
else
{
files[fileCount++] = s;
}
}
}
var fileArray = fileCount > 0 ? files.Take(fileCount).ToArray() : Array.Empty();
var directoryArray = directoryCount > 0 ? files.TakeLast(directoryCount).Reverse().ToArray() : Array.Empty();
return (fileArray, directoryArray);
}
catch (Exception ex)
{
Log.Error($"Error obtaining data from drag & drop:\n{ex}");
}
return (Array.Empty(), Array.Empty());
}
}