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;