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); +}