diff --git a/Dalamud/Interface/DragDrop/DragDropInterop.cs b/Dalamud/Interface/DragDrop/DragDropInterop.cs
new file mode 100644
index 000000000..28a2644a5
--- /dev/null
+++ b/Dalamud/Interface/DragDrop/DragDropInterop.cs
@@ -0,0 +1,107 @@
+using System;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.ComTypes;
+using System.Text;
+
+// ReSharper disable UnusedMember.Local
+// ReSharper disable IdentifierTypo
+// ReSharper disable InconsistentNaming
+namespace Dalamud.Interface.DragDrop;
+
+#pragma warning disable SA1600 // Elements should be documented
+/// Implements interop enums and function calls to interact with external drag and drop.
+internal partial class DragDropManager
+{
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ [Guid("00000122-0000-0000-C000-000000000046")]
+ [ComImport]
+ public interface IDropTarget
+ {
+ [MethodImpl(MethodImplOptions.InternalCall)]
+ void DragEnter([MarshalAs(UnmanagedType.Interface), In] IDataObject pDataObj, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.DWORD"), In] uint grfKeyState, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.POINTL"), In] POINTL pt, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.DWORD"), In, Out] ref uint pdwEffect);
+
+ [MethodImpl(MethodImplOptions.InternalCall)]
+ void DragOver([ComAliasName("Microsoft.VisualStudio.OLE.Interop.DWORD"), In] uint grfKeyState, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.POINTL"), In] POINTL pt, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.DWORD"), In, Out] ref uint pdwEffect);
+
+ [MethodImpl(MethodImplOptions.InternalCall)]
+ void DragLeave();
+
+ [MethodImpl(MethodImplOptions.InternalCall)]
+ void Drop([MarshalAs(UnmanagedType.Interface), In] IDataObject pDataObj, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.DWORD"), In] uint grfKeyState, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.POINTL"), In] POINTL pt, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.DWORD"), In, Out] ref uint pdwEffect);
+ }
+
+ internal struct POINTL
+ {
+ [ComAliasName("Microsoft.VisualStudio.OLE.Interop.LONG")]
+ public int x;
+ [ComAliasName("Microsoft.VisualStudio.OLE.Interop.LONG")]
+ public int y;
+ }
+
+ private static class DragDropInterop
+ {
+ [Flags]
+ public enum ModifierKeys
+ {
+ MK_NONE = 0x00,
+ MK_LBUTTON = 0x01,
+ MK_RBUTTON = 0x02,
+ MK_SHIFT = 0x04,
+ MK_CONTROL = 0x08,
+ MK_MBUTTON = 0x10,
+ MK_ALT = 0x20,
+ }
+
+ public enum ClipboardFormat
+ {
+ CF_TEXT = 1,
+ CF_BITMAP = 2,
+ CF_DIB = 3,
+ CF_UNICODETEXT = 13,
+ CF_HDROP = 15,
+ }
+
+ [Flags]
+ public enum DVAspect
+ {
+ DVASPECT_CONTENT = 0x01,
+ DVASPECT_THUMBNAIL = 0x02,
+ DVASPECT_ICON = 0x04,
+ DVASPECT_DOCPRINT = 0x08,
+ }
+
+ [Flags]
+ public enum TYMED
+ {
+ TYMED_NULL = 0x00,
+ TYMED_HGLOBAL = 0x01,
+ TYMED_FILE = 0x02,
+ TYMED_ISTREAM = 0x04,
+ TYMED_ISTORAGE = 0x08,
+ TYMED_GDI = 0x10,
+ TYMED_MFPICT = 0x20,
+ TYMED_ENHMF = 0x40,
+ }
+
+ [Flags]
+ public enum DropEffects : uint
+ {
+ None = 0x00_0000_00,
+ Copy = 0x00_0000_01,
+ Move = 0x00_0000_02,
+ Link = 0x00_0000_04,
+ Scroll = 0x80_0000_00,
+ }
+
+ [DllImport("ole32.dll")]
+ public static extern int RegisterDragDrop(nint hwnd, IDropTarget pDropTarget);
+
+ [DllImport("ole32.dll")]
+ public static extern int RevokeDragDrop(nint hwnd);
+
+ [DllImport("shell32.dll")]
+ public static extern int DragQueryFile(IntPtr hDrop, uint iFile, StringBuilder lpszFile, int cch);
+ }
+}
+#pragma warning restore SA1600 // Elements should be documented
diff --git a/Dalamud/Interface/DragDrop/DragDropManager.cs b/Dalamud/Interface/DragDrop/DragDropManager.cs
new file mode 100644
index 000000000..34f1296e1
--- /dev/null
+++ b/Dalamud/Interface/DragDrop/DragDropManager.cs
@@ -0,0 +1,156 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+
+using Dalamud.Interface.Internal;
+using Dalamud.IoC;
+using Dalamud.IoC.Internal;
+using ImGuiNET;
+using Serilog;
+
+namespace Dalamud.Interface.DragDrop;
+
+///
+/// A manager that keeps state of external windows drag and drop events,
+/// and can be used to create ImGui drag and drop sources and targets for those external events.
+///
+[PluginInterface]
+[ServiceManager.EarlyLoadedService]
+[ResolveVia]
+internal partial class DragDropManager : IDisposable, IDragDropManager, IServiceType
+{
+ [ServiceManager.ServiceDependency]
+ private readonly InterfaceManager.InterfaceManagerWithScene interfaceManager = Service.Get();
+
+ private int lastDropFrame = -2;
+ private int lastTooltipFrame = -1;
+
+ [ServiceManager.ServiceConstructor]
+ private DragDropManager()
+ => this.Enable();
+
+ /// Gets a value indicating whether external drag and drop is available at all.
+ public bool ServiceAvailable { get; private set; }
+
+ /// Gets a value indicating whether a valid external drag and drop is currently active and hovering over any FFXIV-related viewport.
+ public bool IsDragging { get; private set; }
+
+ /// Gets a value indicating whether there are any files or directories currently being dragged, or stored from the last drop.
+ public bool HasPaths
+ => this.Files.Count + this.Directories.Count > 0;
+
+ /// Gets the list of file paths currently being dragged from an external application over any FFXIV-related viewport, or stored from the last drop.
+ public IReadOnlyList Files { get; private set; } = Array.Empty();
+
+ /// Gets a set of all extensions available in the paths currently being dragged from an external application over any FFXIV-related viewport or stored from the last drop.
+ public IReadOnlySet Extensions { get; private set; } = new HashSet();
+
+ /// Gets the list of directory paths currently being dragged from an external application over any FFXIV-related viewport or stored from the last drop.
+ public IReadOnlyList Directories { get; private set; } = Array.Empty();
+
+ /// Enable external drag and drop.
+ public void Enable()
+ {
+ if (this.ServiceAvailable)
+ {
+ return;
+ }
+
+ try
+ {
+ var ret = DragDropInterop.RegisterDragDrop(this.interfaceManager.Manager.WindowHandlePtr, this);
+ Log.Information($"[DragDrop] Registered window 0x{this.interfaceManager.Manager.WindowHandlePtr:X} for external drag and drop operations. ({ret})");
+ Marshal.ThrowExceptionForHR(ret);
+ this.ServiceAvailable = true;
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"Could not create windows drag and drop utility for window 0x{this.interfaceManager.Manager.WindowHandlePtr:X}:\n{ex}");
+ }
+ }
+
+ /// Disable external drag and drop.
+ public void Disable()
+ {
+ if (!this.ServiceAvailable)
+ {
+ return;
+ }
+
+ try
+ {
+ var ret = DragDropInterop.RevokeDragDrop(this.interfaceManager.Manager.WindowHandlePtr);
+ Log.Information($"[DragDrop] Disabled external drag and drop operations for window 0x{this.interfaceManager.Manager.WindowHandlePtr:X}. ({ret})");
+ Marshal.ThrowExceptionForHR(ret);
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"Could not disable windows drag and drop utility for window 0x{this.interfaceManager.Manager.WindowHandlePtr:X}:\n{ex}");
+ }
+
+ this.ServiceAvailable = false;
+ }
+
+ ///
+ public void Dispose()
+ => this.Disable();
+
+ ///
+ public void CreateImGuiSource(string label, Func validityCheck, Func tooltipBuilder)
+ {
+ if (!this.IsDragging && !this.IsDropping())
+ {
+ return;
+ }
+
+ if (!validityCheck(this) || !ImGui.BeginDragDropSource(ImGuiDragDropFlags.SourceExtern))
+ {
+ return;
+ }
+
+ ImGui.SetDragDropPayload(label, nint.Zero, 0);
+ if (this.CheckTooltipFrame(out var frame) && tooltipBuilder(this))
+ {
+ this.lastTooltipFrame = frame;
+ }
+
+ ImGui.EndDragDropSource();
+ }
+
+ ///
+ public bool CreateImGuiTarget(string label, out IReadOnlyList files, out IReadOnlyList directories)
+ {
+ files = Array.Empty();
+ directories = Array.Empty();
+ if (!this.HasPaths || !ImGui.BeginDragDropTarget())
+ {
+ return false;
+ }
+
+ unsafe
+ {
+ if (ImGui.AcceptDragDropPayload(label, ImGuiDragDropFlags.AcceptBeforeDelivery).NativePtr != null && this.IsDropping())
+ {
+ this.lastDropFrame = -2;
+ files = this.Files;
+ directories = this.Directories;
+ return true;
+ }
+ }
+
+ ImGui.EndDragDropTarget();
+ return false;
+ }
+
+ private bool CheckTooltipFrame(out int frame)
+ {
+ frame = ImGui.GetFrameCount();
+ return this.lastTooltipFrame < frame;
+ }
+
+ private bool IsDropping()
+ {
+ var frame = ImGui.GetFrameCount();
+ return this.lastDropFrame == frame || this.lastDropFrame == frame - 1;
+ }
+}
diff --git a/Dalamud/Interface/DragDrop/DragDropTarget.cs b/Dalamud/Interface/DragDrop/DragDropTarget.cs
new file mode 100644
index 000000000..05e5599f9
--- /dev/null
+++ b/Dalamud/Interface/DragDrop/DragDropTarget.cs
@@ -0,0 +1,247 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices.ComTypes;
+using System.Text;
+
+using Dalamud.Utility;
+using ImGuiNET;
+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();
+ 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.DragQueryFile(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.DragQueryFile(stgMedium.unionmember, i, sb, sb.Capacity);
+ if (ret >= sb.Capacity)
+ {
+ sb.Capacity = ret + 1;
+ ret = DragDropInterop.DragQueryFile(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());
+ }
+}
diff --git a/Dalamud/Interface/DragDrop/IDragDropManager.cs b/Dalamud/Interface/DragDrop/IDragDropManager.cs
new file mode 100644
index 000000000..736c8af24
--- /dev/null
+++ b/Dalamud/Interface/DragDrop/IDragDropManager.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+
+namespace Dalamud.Interface.DragDrop;
+
+///
+/// A service to handle external drag and drop from WinAPI.
+///
+public interface IDragDropManager
+{
+ /// Gets a value indicating whether Drag and Drop functionality is available at all.
+ public bool ServiceAvailable { get; }
+
+ /// Gets a value indicating whether anything is being dragged from an external application and over any of the games viewports.
+ public bool IsDragging { get; }
+
+ /// Gets the list of files currently being dragged from an external application over any of the games viewports.
+ public IReadOnlyList Files { get; }
+
+ /// Gets the set of file types by extension currently being dragged from an external application over any of the games viewports.
+ public IReadOnlySet Extensions { get; }
+
+ /// Gets the list of directories currently being dragged from an external application over any of the games viewports.
+ public IReadOnlyList Directories { get; }
+
+ /// Create an ImGui drag & drop source that is active only if anything is being dragged from an external source.
+ /// The label used for the drag & drop payload.
+ /// A function returning whether the current status is relevant for this source. Checked before creating the source but only if something is being dragged.
+ public void CreateImGuiSource(string label, Func validityCheck)
+ => this.CreateImGuiSource(label, validityCheck, _ => false);
+
+ /// Create an ImGui drag & drop source that is active only if anything is being dragged from an external source.
+ /// The label used for the drag & drop payload.
+ /// A function returning whether the current status is relevant for this source. Checked before creating the source but only if something is being dragged.
+ /// Executes ImGui functions to build a tooltip. Should return true if it creates any tooltip and false otherwise. If multiple sources are active, only the first non-empty tooltip type drawn in a frame will be used.
+ public void CreateImGuiSource(string label, Func validityCheck, Func tooltipBuilder);
+
+ /// Create an ImGui drag & drop target on the last ImGui object.
+ /// The label used for the drag & drop payload.
+ /// On success, contains the list of file paths dropped onto the target.
+ /// On success, contains the list of directory paths dropped onto the target.
+ /// True if items were dropped onto the target this frame, false otherwise.
+ public bool CreateImGuiTarget(string label, out IReadOnlyList files, out IReadOnlyList directories);
+}