diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs index 206c578c2..d352ad2c8 100644 --- a/Dalamud.CorePlugin/PluginImpl.cs +++ b/Dalamud.CorePlugin/PluginImpl.cs @@ -1,13 +1,12 @@ 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/Interface/DragDrop/DragDropInterop.cs b/Dalamud/Interface/DragDrop/DragDropInterop.cs index d25400834..b3befac07 100644 --- a/Dalamud/Interface/DragDrop/DragDropInterop.cs +++ b/Dalamud/Interface/DragDrop/DragDropInterop.cs @@ -1,10 +1,15 @@ using System; using System.Runtime.InteropServices; using System.Text; -using System.Windows.Forms; +using Microsoft.VisualStudio.OLE.Interop; + +// ReSharper disable UnusedMember.Local +// ReSharper disable IdentifierTypo +// ReSharper disable InconsistentNaming namespace Dalamud.Interface.DragDrop; +/// Implements interop enums and function calls to interact with external drag and drop. internal partial class DragDropManager { private static class DragDropInterop diff --git a/Dalamud/Interface/DragDrop/DragDropManager.cs b/Dalamud/Interface/DragDrop/DragDropManager.cs index ab838dc45..c27149153 100644 --- a/Dalamud/Interface/DragDrop/DragDropManager.cs +++ b/Dalamud/Interface/DragDrop/DragDropManager.cs @@ -1,18 +1,47 @@ -using ImGuiNET; using System; using System.Collections.Generic; using System.Runtime.InteropServices; + +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. +/// internal partial class DragDropManager : IDisposable, IDragDropManager { private readonly UiBuilder uiBuilder; + private int lastDropFrame = -2; + private int lastTooltipFrame = -1; + + /// Initializes a new instance of the class. + /// The parent instance. public DragDropManager(UiBuilder uiBuilder) => this.uiBuilder = uiBuilder; + /// 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. + public bool HasPaths { get; private set; } + + /// Gets the list of file paths currently being dragged from an external application over any FFXIV-related viewport. + 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. + 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. + public IReadOnlyList Directories { get; private set; } = Array.Empty(); + + /// Enable external drag and drop. public void Enable() { if (this.ServiceAvailable) @@ -32,31 +61,42 @@ internal partial class DragDropManager : IDisposable, IDragDropManager } } - public void Dispose() + /// Disable external drag and drop. + public void Disable() { if (!this.ServiceAvailable) { return; } + try + { + DragDropInterop.RevokeDragDrop(this.uiBuilder.WindowHandlePtr); + } + catch (Exception ex) + { + Log.Error($"Could not disable windows drag and drop utility:\n{ex}"); + } + this.ServiceAvailable = false; } - 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 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; + if (!this.HasPaths && !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)) @@ -72,7 +112,10 @@ internal partial class DragDropManager : IDisposable, IDragDropManager { files = Array.Empty(); directories = Array.Empty(); - if (!this.IsDragging || !ImGui.BeginDragDropTarget()) return false; + if (!this.IsDragging || !ImGui.BeginDragDropTarget()) + { + return false; + } unsafe { @@ -89,9 +132,6 @@ internal partial class DragDropManager : IDisposable, IDragDropManager return false; } - private int lastDropFrame = -2; - private int lastTooltipFrame = -1; - private bool CheckTooltipFrame(out int frame) { frame = ImGui.GetFrameCount(); diff --git a/Dalamud/Interface/DragDrop/DragDropTarget.cs b/Dalamud/Interface/DragDrop/DragDropTarget.cs index cff97a987..79a20cff0 100644 --- a/Dalamud/Interface/DragDrop/DragDropTarget.cs +++ b/Dalamud/Interface/DragDrop/DragDropTarget.cs @@ -1,23 +1,238 @@ using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +using Dalamud.Utility; +using ImGuiNET; using Microsoft.VisualStudio.OLE.Interop; +using Serilog; namespace Dalamud.Interface.DragDrop; +/// Implements the IDropTarget interface to interact with external drag and dropping. internal partial class DragDropManager : IDropTarget { + /// Create the drag and drop formats we accept. + private static readonly FORMATETC[] FormatEtc = + { + new() + { + cfFormat = (ushort)DragDropInterop.ClipboardFormat.CF_HDROP, + ptd = nint.Zero, + dwAspect = (uint)DragDropInterop.DVAspect.DVASPECT_CONTENT, + lindex = -1, + tymed = (uint)DragDropInterop.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; + UpdateIo((DragDropInterop.ModifierKeys)grfKeyState, true); + + if (pDataObj.QueryGetData(FormatEtc) != 0) + { + pdwEffect = 0; + } + else + { + pdwEffect &= (uint)DragDropInterop.DropEffects.Copy; + (this.Files, this.Directories) = this.GetPaths(pDataObj); + this.HasPaths = this.Files.Count + this.Directories.Count > 0; + this.Extensions = this.Files.Select(Path.GetExtension).Where(p => !p.IsNullOrEmpty()).Distinct().ToHashSet(); + } } + /// 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, can also be less often (?). public void DragOver(uint grfKeyState, POINTL pt, ref uint pdwEffect) { + UpdateIo((DragDropInterop.ModifierKeys)grfKeyState, false); + pdwEffect &= (uint)DragDropInterop.DropEffects.Copy; } + /// 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(); } + /// 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((DragDropInterop.ModifierKeys)grfKeyState); + this.lastDropFrame = ImGui.GetFrameCount(); + this.IsDragging = false; + if (this.Files.Count > 0 || this.Directories.Count > 0) + { + pdwEffect &= (uint)DragDropInterop.DropEffects.Copy; + } + else + { + pdwEffect = 0; + } + } + + private static void 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); + } + } + + 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 + { + var stgMedium = new STGMEDIUM[] + { + default, + }; + data.GetData(FormatEtc, stgMedium); + var numFiles = DragDropInterop.DragQueryFile(stgMedium[0].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[0].unionmember, i, sb, sb.Capacity); + if (ret >= sb.Capacity) + { + sb.Capacity = ret + 1; + ret = DragDropInterop.DragQueryFile(stgMedium[0].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 index 2a0e7bdd9..736c8af24 100644 --- a/Dalamud/Interface/DragDrop/IDragDropManager.cs +++ b/Dalamud/Interface/DragDrop/IDragDropManager.cs @@ -37,7 +37,8 @@ public interface IDragDropManager /// 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. + /// 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); + public bool CreateImGuiTarget(string label, out IReadOnlyList files, out IReadOnlyList directories); } diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 79541648b..a421d17ba 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -50,6 +50,7 @@ public sealed class UiBuilder : IDisposable this.hitchDetector = new HitchDetector($"UiBuilder({namespaceName})", this.configuration.UiBuilderHitch); this.namespaceName = namespaceName; this.dragDropManager = new DragDropManager(this); + this.dragDropManager.Enable(); this.interfaceManager.Draw += this.OnDraw; this.interfaceManager.BuildFonts += this.OnBuildFonts;