mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 18:27:23 +01:00
Improve Drag & Drop interface.
This commit is contained in:
parent
c76cbbd518
commit
96bb94b9d5
6 changed files with 283 additions and 22 deletions
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/// <summary> Implements interop enums and function calls to interact with external drag and drop. </summary>
|
||||
internal partial class DragDropManager
|
||||
{
|
||||
private static class DragDropInterop
|
||||
|
|
|
|||
|
|
@ -1,18 +1,47 @@
|
|||
using ImGuiNET;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using ImGuiNET;
|
||||
using Serilog;
|
||||
|
||||
namespace Dalamud.Interface.DragDrop;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal partial class DragDropManager : IDisposable, IDragDropManager
|
||||
{
|
||||
private readonly UiBuilder uiBuilder;
|
||||
|
||||
private int lastDropFrame = -2;
|
||||
private int lastTooltipFrame = -1;
|
||||
|
||||
/// <summary> Initializes a new instance of the <see cref="DragDropManager"/> class.</summary>
|
||||
/// <param name="uiBuilder">The parent <see cref="UiBuilder"/> instance.</param>
|
||||
public DragDropManager(UiBuilder uiBuilder)
|
||||
=> this.uiBuilder = uiBuilder;
|
||||
|
||||
/// <summary> Gets a value indicating whether external drag and drop is available at all. </summary>
|
||||
public bool ServiceAvailable { get; private set; }
|
||||
|
||||
/// <summary> Gets a value indicating whether a valid external drag and drop is currently active and hovering over any FFXIV-related viewport. </summary>
|
||||
public bool IsDragging { get; private set; }
|
||||
|
||||
/// <summary> Gets a value indicating whether there are any files or directories currently being dragged. </summary>
|
||||
public bool HasPaths { get; private set; }
|
||||
|
||||
/// <summary> Gets the list of file paths currently being dragged from an external application over any FFXIV-related viewport. </summary>
|
||||
public IReadOnlyList<string> Files { get; private set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary> Gets a set of all extensions available in the paths currently being dragged from an external application over any FFXIV-related viewport. </summary>
|
||||
public IReadOnlySet<string> Extensions { get; private set; } = new HashSet<string>();
|
||||
|
||||
/// <summary> Gets the list of directory paths currently being dragged from an external application over any FFXIV-related viewport. </summary>
|
||||
public IReadOnlyList<string> Directories { get; private set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary> Enable external drag and drop. </summary>
|
||||
public void Enable()
|
||||
{
|
||||
if (this.ServiceAvailable)
|
||||
|
|
@ -32,31 +61,42 @@ internal partial class DragDropManager : IDisposable, IDragDropManager
|
|||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
/// <summary> Disable external drag and drop. </summary>
|
||||
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<string> Files { get; private set; } = Array.Empty<string>();
|
||||
|
||||
public IReadOnlySet<string> Extensions { get; private set; } = new HashSet<string>();
|
||||
|
||||
public IReadOnlyList<string> Directories { get; private set; } = Array.Empty<string>();
|
||||
/// <inheritdoc cref="Disable"/>
|
||||
public void Dispose()
|
||||
=> this.Disable();
|
||||
|
||||
/// <inheritdoc cref="IDragDropManager.CreateImGuiSource(string, Func{IDragDropManager, bool}, Func{IDragDropManager, bool})"/>
|
||||
public void CreateImGuiSource(string label, Func<IDragDropManager, bool> validityCheck, Func<IDragDropManager, bool> 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<string>();
|
||||
directories = Array.Empty<string>();
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/// <summary> Implements the IDropTarget interface to interact with external drag and dropping. </summary>
|
||||
internal partial class DragDropManager : IDropTarget
|
||||
{
|
||||
/// <summary> Create the drag and drop formats we accept. </summary>
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Invoked whenever a drag and drop process drags files into any FFXIV-related viewport.
|
||||
/// </summary>
|
||||
/// <param name="pDataObj"> The drag and drop data. </param>
|
||||
/// <param name="grfKeyState"> The mouse button used to drag as well as key modifiers. </param>
|
||||
/// <param name="pt"> The global cursor position. </param>
|
||||
/// <param name="pdwEffect"> Effects that can be used with this drag and drop process. </param>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Invoked every windows update-frame as long as the drag and drop process keeps hovering over an FFXIV-related viewport. </summary>
|
||||
/// <param name="grfKeyState"> The mouse button used to drag as well as key modifiers. </param>
|
||||
/// <param name="pt"> The global cursor position. </param>
|
||||
/// <param name="pdwEffect"> Effects that can be used with this drag and drop process. </param>
|
||||
/// <remarks> Can be invoked more often than once a XIV frame, can also be less often (?). </remarks>
|
||||
public void DragOver(uint grfKeyState, POINTL pt, ref uint pdwEffect)
|
||||
{
|
||||
UpdateIo((DragDropInterop.ModifierKeys)grfKeyState, false);
|
||||
pdwEffect &= (uint)DragDropInterop.DropEffects.Copy;
|
||||
}
|
||||
|
||||
/// <summary> Invoked whenever a drag and drop process that hovered over any FFXIV-related viewport leaves all FFXIV-related viewports. </summary>
|
||||
public void DragLeave()
|
||||
{
|
||||
this.IsDragging = false;
|
||||
this.Files = Array.Empty<string>();
|
||||
this.Directories = Array.Empty<string>();
|
||||
this.Extensions = new HashSet<string>();
|
||||
}
|
||||
|
||||
/// <summary> Invoked whenever a drag process ends by dropping over any FFXIV-related viewport. </summary>
|
||||
/// <param name="pDataObj"> The drag and drop data. </param>
|
||||
/// <param name="grfKeyState"> The mouse button used to drag as well as key modifiers. </param>
|
||||
/// <param name="pt"> The global cursor position. </param>
|
||||
/// <param name="pdwEffect"> Effects that can be used with this drag and drop process. </param>
|
||||
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<string>(), Array.Empty<string>());
|
||||
}
|
||||
|
||||
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<string>();
|
||||
var directoryArray = directoryCount > 0 ? files.TakeLast(directoryCount).Reverse().ToArray() : Array.Empty<string>();
|
||||
|
||||
return (fileArray, directoryArray);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error($"Error obtaining data from drag & drop:\n{ex}");
|
||||
}
|
||||
|
||||
return (Array.Empty<string>(), Array.Empty<string>());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,8 @@ public interface IDragDropManager
|
|||
|
||||
/// <summary> Create an ImGui drag & drop target on the last ImGui object. </summary>
|
||||
/// <param name="label">The label used for the drag & drop payload.</param>
|
||||
/// <param name="files">On success, contains the files dropped onto the target.</param>
|
||||
/// <param name="files">On success, contains the list of file paths dropped onto the target.</param>
|
||||
/// <param name="directories">On success, contains the list of directory paths dropped onto the target.</param>
|
||||
/// <returns>True if items were dropped onto the target this frame, false otherwise.</returns>
|
||||
public bool CreateImGuiTarget(string label, out IReadOnlyList<string> files);
|
||||
public bool CreateImGuiTarget(string label, out IReadOnlyList<string> files, out IReadOnlyList<string> directories);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue