diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs index d352ad2c8..206c578c2 100644 --- a/Dalamud.CorePlugin/PluginImpl.cs +++ b/Dalamud.CorePlugin/PluginImpl.cs @@ -1,12 +1,13 @@ using System; using System.IO; - +using System.Windows.Forms; using Dalamud.Configuration.Internal; using Dalamud.Game.Command; using Dalamud.Interface.Windowing; using Dalamud.Logging; using Dalamud.Plugin; using Dalamud.Utility; +using PInvoke; namespace Dalamud.CorePlugin { diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 61730b5ca..0f259ee31 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -82,6 +82,7 @@ + diff --git a/Dalamud/Interface/DragDrop/DragDropInterop.cs b/Dalamud/Interface/DragDrop/DragDropInterop.cs new file mode 100644 index 000000000..d25400834 --- /dev/null +++ b/Dalamud/Interface/DragDrop/DragDropInterop.cs @@ -0,0 +1,74 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; +using System.Windows.Forms; + +namespace Dalamud.Interface.DragDrop; + +internal partial class DragDropManager +{ + 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); + } +} diff --git a/Dalamud/Interface/DragDrop/DragDropManager.cs b/Dalamud/Interface/DragDrop/DragDropManager.cs new file mode 100644 index 000000000..ab838dc45 --- /dev/null +++ b/Dalamud/Interface/DragDrop/DragDropManager.cs @@ -0,0 +1,106 @@ +using ImGuiNET; +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Serilog; + +namespace Dalamud.Interface.DragDrop; + +internal partial class DragDropManager : IDisposable, IDragDropManager +{ + private readonly UiBuilder uiBuilder; + + public DragDropManager(UiBuilder uiBuilder) + => this.uiBuilder = uiBuilder; + + public void Enable() + { + if (this.ServiceAvailable) + { + return; + } + + try + { + var ret2 = DragDropInterop.RegisterDragDrop(this.uiBuilder.WindowHandlePtr, this); + Marshal.ThrowExceptionForHR(ret2); + this.ServiceAvailable = true; + } + catch (Exception ex) + { + Log.Error($"Could not create windows drag and drop utility:\n{ex}"); + } + } + + public void Dispose() + { + if (!this.ServiceAvailable) + { + return; + } + + + } + + public bool ServiceAvailable { get; internal set; } + + public bool IsDragging { get; private set; } + + public IReadOnlyList Files { get; private set; } = Array.Empty(); + + public IReadOnlySet Extensions { get; private set; } = new HashSet(); + + public IReadOnlyList Directories { get; private set; } = Array.Empty(); + + /// + 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.IsDragging || !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 int lastDropFrame = -2; + private int lastTooltipFrame = -1; + + 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..cff97a987 --- /dev/null +++ b/Dalamud/Interface/DragDrop/DragDropTarget.cs @@ -0,0 +1,23 @@ +using System; +using Microsoft.VisualStudio.OLE.Interop; + +namespace Dalamud.Interface.DragDrop; + +internal partial class DragDropManager : IDropTarget +{ + public void DragEnter(IDataObject pDataObj, uint grfKeyState, POINTL pt, ref uint pdwEffect) + { + } + + public void DragOver(uint grfKeyState, POINTL pt, ref uint pdwEffect) + { + } + + public void DragLeave() + { + } + + public void Drop(IDataObject pDataObj, uint grfKeyState, POINTL pt, ref uint pdwEffect) + { + } +} diff --git a/Dalamud/Interface/DragDrop/IDragDropManager.cs b/Dalamud/Interface/DragDrop/IDragDropManager.cs new file mode 100644 index 000000000..2a0e7bdd9 --- /dev/null +++ b/Dalamud/Interface/DragDrop/IDragDropManager.cs @@ -0,0 +1,43 @@ +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 files dropped onto the target. + /// True if items were dropped onto the target this frame, false otherwise. + public bool CreateImGuiTarget(string label, out IReadOnlyList files); +} diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index da4f241ac..79541648b 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -7,6 +7,7 @@ using Dalamud.Configuration.Internal; using Dalamud.Game; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.Gui; +using Dalamud.Interface.DragDrop; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.ManagedAsserts; @@ -30,6 +31,7 @@ public sealed class UiBuilder : IDisposable private readonly string namespaceName; private readonly InterfaceManager interfaceManager = Service.Get(); private readonly GameFontManager gameFontManager = Service.Get(); + private readonly DragDropManager dragDropManager; private bool hasErrorWindow = false; private bool lastFrameUiHideState = false; @@ -47,6 +49,7 @@ public sealed class UiBuilder : IDisposable this.stopwatch = new Stopwatch(); this.hitchDetector = new HitchDetector($"UiBuilder({namespaceName})", this.configuration.UiBuilderHitch); this.namespaceName = namespaceName; + this.dragDropManager = new DragDropManager(this); this.interfaceManager.Draw += this.OnDraw; this.interfaceManager.BuildFonts += this.OnBuildFonts; @@ -100,6 +103,11 @@ public sealed class UiBuilder : IDisposable /// public event Action HideUi; + /// + /// Gets the manager for external, WinAPI-based drag and drop functionality. + /// + public IDragDropManager DragDropManager => this.dragDropManager; + /// /// Gets the default Dalamud font based on Noto Sans CJK Medium in 17pt - supporting all game languages and icons. /// @@ -397,6 +405,7 @@ public sealed class UiBuilder : IDisposable /// void IDisposable.Dispose() { + this.dragDropManager.Dispose(); this.interfaceManager.Draw -= this.OnDraw; this.interfaceManager.BuildFonts -= this.OnBuildFonts; this.interfaceManager.ResizeBuffers -= this.OnResizeBuffers;