mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 18:27:23 +01:00
Just use win32 APIs
This commit is contained in:
parent
f6d16d5624
commit
06938509e7
4 changed files with 125 additions and 231 deletions
|
|
@ -90,6 +90,7 @@
|
|||
<PackageReference Include="System.Reactive" Version="5.0.0" />
|
||||
<PackageReference Include="System.Reflection.MetadataLoadContext" Version="7.0.0" />
|
||||
<PackageReference Include="System.Resources.Extensions" Version="7.0.0" />
|
||||
<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.22621.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Dalamud.Common\Dalamud.Common.csproj" />
|
||||
|
|
|
|||
|
|
@ -1,11 +1,19 @@
|
|||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
using CheapLoc;
|
||||
|
||||
using Dalamud.Game.Gui.Toast;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Logging.Internal;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
using TerraFX.Interop.Windows;
|
||||
|
||||
using static TerraFX.Interop.Windows.Windows;
|
||||
|
||||
namespace Dalamud.Interface.Internal;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -29,9 +37,15 @@ namespace Dalamud.Interface.Internal;
|
|||
[ServiceManager.EarlyLoadedService]
|
||||
internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDisposable
|
||||
{
|
||||
private static readonly ModuleLog Log = new(nameof(ImGuiClipboardFunctionProvider));
|
||||
private readonly nint clipboardUserDataOriginal;
|
||||
private readonly delegate* unmanaged<nint, byte*, void> setTextOriginal;
|
||||
private readonly delegate* unmanaged<nint, byte*> getTextOriginal;
|
||||
private readonly nint setTextOriginal;
|
||||
private readonly nint getTextOriginal;
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly ToastGui toastGui = Service<ToastGui>.Get();
|
||||
|
||||
private ImVectorWrapper<byte> clipboardData;
|
||||
private GCHandle clipboardUserData;
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
|
|
@ -43,11 +57,13 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis
|
|||
|
||||
var io = ImGui.GetIO();
|
||||
this.clipboardUserDataOriginal = io.ClipboardUserData;
|
||||
this.setTextOriginal = (delegate* unmanaged<nint, byte*, void>)io.SetClipboardTextFn;
|
||||
this.getTextOriginal = (delegate* unmanaged<nint, byte*>)io.GetClipboardTextFn;
|
||||
this.setTextOriginal = io.SetClipboardTextFn;
|
||||
this.getTextOriginal = io.GetClipboardTextFn;
|
||||
io.ClipboardUserData = GCHandle.ToIntPtr(this.clipboardUserData = GCHandle.Alloc(this));
|
||||
io.SetClipboardTextFn = (nint)(delegate* unmanaged<nint, byte*, void>)&StaticSetClipboardTextImpl;
|
||||
io.GetClipboardTextFn = (nint)(delegate* unmanaged<nint, byte*>)&StaticGetClipboardTextImpl;
|
||||
|
||||
this.clipboardData = new(0);
|
||||
return;
|
||||
|
||||
[UnmanagedCallersOnly]
|
||||
|
|
@ -59,10 +75,6 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis
|
|||
((ImGuiClipboardFunctionProvider)GCHandle.FromIntPtr(userData).Target)!.GetClipboardTextImpl();
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "AssignNullToNotNullAttribute", Justification = "If it's null, it's crashworthy")]
|
||||
private static ImVectorWrapper<byte> ImGuiCurrentContextClipboardHandlerData =>
|
||||
new((ImVector*)(ImGui.GetCurrentContext() + 0x5520));
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
|
|
@ -70,30 +82,118 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis
|
|||
return;
|
||||
|
||||
var io = ImGui.GetIO();
|
||||
io.SetClipboardTextFn = (nint)this.setTextOriginal;
|
||||
io.GetClipboardTextFn = (nint)this.getTextOriginal;
|
||||
io.SetClipboardTextFn = this.setTextOriginal;
|
||||
io.GetClipboardTextFn = this.getTextOriginal;
|
||||
io.ClipboardUserData = this.clipboardUserDataOriginal;
|
||||
|
||||
this.clipboardUserData.Free();
|
||||
this.clipboardData.Dispose();
|
||||
}
|
||||
|
||||
private bool OpenClipboardOrShowError()
|
||||
{
|
||||
if (!OpenClipboard(default))
|
||||
{
|
||||
this.toastGui.ShowError(
|
||||
Loc.Localize(
|
||||
"ImGuiClipboardFunctionProviderClipboardInUse",
|
||||
"Some other application is using the clipboard. Try again later."));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void SetClipboardTextImpl(byte* text)
|
||||
{
|
||||
var buffer = ImGuiCurrentContextClipboardHandlerData;
|
||||
buffer.SetFromZeroTerminatedSequence(text);
|
||||
buffer.Utf8Normalize();
|
||||
buffer.AddZeroTerminatorIfMissing();
|
||||
this.setTextOriginal(this.clipboardUserDataOriginal, buffer.Data);
|
||||
if (!this.OpenClipboardOrShowError())
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var len = 0;
|
||||
while (text[len] != 0)
|
||||
len++;
|
||||
var str = Encoding.UTF8.GetString(text, len);
|
||||
str = str.ReplaceLineEndings("\r\n");
|
||||
var hMem = GlobalAlloc(GMEM.GMEM_MOVEABLE, (nuint)((str.Length + 1) * 2));
|
||||
if (hMem == 0)
|
||||
throw new OutOfMemoryException();
|
||||
|
||||
var ptr = (char*)GlobalLock(hMem);
|
||||
if (ptr == null)
|
||||
{
|
||||
throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error())
|
||||
?? throw new InvalidOperationException($"{nameof(GlobalLock)} failed.");
|
||||
}
|
||||
|
||||
str.AsSpan().CopyTo(new(ptr, str.Length));
|
||||
ptr[str.Length] = default;
|
||||
GlobalUnlock(hMem);
|
||||
|
||||
SetClipboardData(CF.CF_UNICODETEXT, hMem);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, $"Error in {nameof(this.SetClipboardTextImpl)}");
|
||||
this.toastGui.ShowError(
|
||||
Loc.Localize(
|
||||
"ImGuiClipboardFunctionProviderErrorCopy",
|
||||
"Failed to copy. See logs for details."));
|
||||
}
|
||||
finally
|
||||
{
|
||||
CloseClipboard();
|
||||
}
|
||||
}
|
||||
|
||||
private byte* GetClipboardTextImpl()
|
||||
{
|
||||
_ = this.getTextOriginal(this.clipboardUserDataOriginal);
|
||||
this.clipboardData.Clear();
|
||||
|
||||
var formats = stackalloc uint[] { CF.CF_UNICODETEXT, CF.CF_TEXT };
|
||||
if (GetPriorityClipboardFormat(formats, 2) < 1 || !this.OpenClipboardOrShowError())
|
||||
{
|
||||
this.clipboardData.Add(0);
|
||||
return this.clipboardData.Data;
|
||||
}
|
||||
|
||||
var buffer = ImGuiCurrentContextClipboardHandlerData;
|
||||
buffer.TrimZeroTerminator();
|
||||
buffer.Utf8Normalize();
|
||||
buffer.AddZeroTerminatorIfMissing();
|
||||
return buffer.Data;
|
||||
try
|
||||
{
|
||||
var hMem = (HGLOBAL)GetClipboardData(CF.CF_UNICODETEXT);
|
||||
if (hMem != default)
|
||||
{
|
||||
var ptr = (char*)GlobalLock(hMem);
|
||||
if (ptr == null)
|
||||
{
|
||||
throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error())
|
||||
?? throw new InvalidOperationException($"{nameof(GlobalLock)} failed.");
|
||||
}
|
||||
|
||||
var str = new string(ptr);
|
||||
str = str.ReplaceLineEndings("\r\n");
|
||||
this.clipboardData.Resize(Encoding.UTF8.GetByteCount(str) + 1);
|
||||
Encoding.UTF8.GetBytes(str, this.clipboardData.DataSpan);
|
||||
this.clipboardData[^1] = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.clipboardData.Add(0);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, $"Error in {nameof(this.GetClipboardTextImpl)}");
|
||||
this.toastGui.ShowError(
|
||||
Loc.Localize(
|
||||
"ImGuiClipboardFunctionProviderErrorPaste",
|
||||
"Failed to paste. See logs for details."));
|
||||
}
|
||||
finally
|
||||
{
|
||||
CloseClipboard();
|
||||
}
|
||||
|
||||
return this.clipboardData.Data;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,207 +0,0 @@
|
|||
using System.Numerics;
|
||||
using System.Text;
|
||||
|
||||
namespace Dalamud.Interface.Utility;
|
||||
|
||||
/// <summary>
|
||||
/// Utility methods for <see cref="ImVectorWrapper{T}"/>.
|
||||
/// </summary>
|
||||
public static partial class ImVectorWrapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Appends <paramref name="buf"/> from <paramref name="psz"/>, a zero terminated sequence.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The element type.</typeparam>
|
||||
/// <param name="buf">The target buffer.</param>
|
||||
/// <param name="psz">The pointer to the zero-terminated sequence.</param>
|
||||
public static unsafe void AppendZeroTerminatedSequence<T>(this ref ImVectorWrapper<T> buf, T* psz)
|
||||
where T : unmanaged, INumber<T>
|
||||
{
|
||||
var len = 0;
|
||||
while (psz[len] != default)
|
||||
len++;
|
||||
|
||||
buf.AddRange(new Span<T>(psz, len));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets <paramref name="buf"/> from <paramref name="psz"/>, a zero terminated sequence.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The element type.</typeparam>
|
||||
/// <param name="buf">The target buffer.</param>
|
||||
/// <param name="psz">The pointer to the zero-terminated sequence.</param>
|
||||
public static unsafe void SetFromZeroTerminatedSequence<T>(this ref ImVectorWrapper<T> buf, T* psz)
|
||||
where T : unmanaged, INumber<T>
|
||||
{
|
||||
buf.Clear();
|
||||
buf.AppendZeroTerminatedSequence(psz);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trims zero terminator(s).
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The element type.</typeparam>
|
||||
/// <param name="buf">The buffer.</param>
|
||||
public static void TrimZeroTerminator<T>(this ref ImVectorWrapper<T> buf)
|
||||
where T : unmanaged, INumber<T>
|
||||
{
|
||||
ref var len = ref buf.LengthUnsafe;
|
||||
while (len > 0 && buf[len - 1] == default)
|
||||
len--;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a zero terminator to the buffer, if missing.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The element type.</typeparam>
|
||||
/// <param name="buf">The buffer.</param>
|
||||
public static void AddZeroTerminatorIfMissing<T>(this ref ImVectorWrapper<T> buf)
|
||||
where T : unmanaged, INumber<T>
|
||||
{
|
||||
if (buf.Length > 0 && buf[^1] == default)
|
||||
return;
|
||||
buf.Add(default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the codepoint at the given offset.
|
||||
/// </summary>
|
||||
/// <param name="buf">The buffer containing bytes in UTF-8.</param>
|
||||
/// <param name="offset">The offset in bytes.</param>
|
||||
/// <param name="numBytes">Number of bytes occupied by the character, invalid or not.</param>
|
||||
/// <param name="invalid">The fallback character, if no valid UTF-8 character could be found.</param>
|
||||
/// <returns>The parsed codepoint, or <paramref name="invalid"/> if it could not be parsed correctly.</returns>
|
||||
public static unsafe int Utf8GetCodepoint(
|
||||
this in ImVectorWrapper<byte> buf,
|
||||
int offset,
|
||||
out int numBytes,
|
||||
int invalid = 0xFFFD)
|
||||
{
|
||||
var cb = buf.LengthUnsafe - offset;
|
||||
if (cb <= 0)
|
||||
{
|
||||
numBytes = 0;
|
||||
return invalid;
|
||||
}
|
||||
|
||||
numBytes = 1;
|
||||
|
||||
var b = buf.DataUnsafe + offset;
|
||||
if ((b[0] & 0x80) == 0)
|
||||
return b[0];
|
||||
|
||||
if (cb < 2 || (b[1] & 0xC0) != 0x80)
|
||||
return invalid;
|
||||
if ((b[0] & 0xE0) == 0xC0)
|
||||
{
|
||||
numBytes = 2;
|
||||
return ((b[0] & 0x1F) << 6) | (b[1] & 0x3F);
|
||||
}
|
||||
|
||||
if (cb < 3 || (b[2] & 0xC0) != 0x80)
|
||||
return invalid;
|
||||
if ((b[0] & 0xF0) == 0xE0)
|
||||
{
|
||||
numBytes = 3;
|
||||
return ((b[0] & 0x0F) << 12) | ((b[1] & 0x3F) << 6) | (b[2] & 0x3F);
|
||||
}
|
||||
|
||||
if (cb < 4 || (b[3] & 0xC0) != 0x80)
|
||||
return invalid;
|
||||
if ((b[0] & 0xF8) == 0xF0)
|
||||
{
|
||||
numBytes = 4;
|
||||
return ((b[0] & 0x07) << 18) | ((b[1] & 0x3F) << 12) | ((b[2] & 0x3F) << 6) | (b[3] & 0x3F);
|
||||
}
|
||||
|
||||
return invalid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes the given UTF-8 string.<br />
|
||||
/// Using the default values will ensure the best interop between the game, ImGui, and Windows.
|
||||
/// </summary>
|
||||
/// <param name="buf">The buffer containing bytes in UTF-8.</param>
|
||||
/// <param name="lineEnding">The replacement line ending. If empty, CR LF will be used.</param>
|
||||
/// <param name="invalidChar">The replacement invalid character. If empty, U+FFFD REPLACEMENT CHARACTER will be used.</param>
|
||||
/// <param name="normalizeLineEndings">Specify whether to normalize the line endings.</param>
|
||||
/// <param name="sanitizeInvalidCharacters">Specify whether to replace invalid characters.</param>
|
||||
/// <param name="sanitizeNonUcs2Characters">Specify whether to replace characters that requires the use of surrogate, when encoded in UTF-16.</param>
|
||||
/// <param name="sanitizeSurrogates">Specify whether to make sense out of WTF-8.</param>
|
||||
public static unsafe void Utf8Normalize(
|
||||
this ref ImVectorWrapper<byte> buf,
|
||||
ReadOnlySpan<byte> lineEnding = default,
|
||||
ReadOnlySpan<byte> invalidChar = default,
|
||||
bool normalizeLineEndings = true,
|
||||
bool sanitizeInvalidCharacters = true,
|
||||
bool sanitizeNonUcs2Characters = true,
|
||||
bool sanitizeSurrogates = true)
|
||||
{
|
||||
if (lineEnding.IsEmpty)
|
||||
lineEnding = "\r\n"u8;
|
||||
if (invalidChar.IsEmpty)
|
||||
invalidChar = "\uFFFD"u8;
|
||||
|
||||
// Ensure an implicit null after the end of the string.
|
||||
buf.EnsureCapacity(buf.Length + 1);
|
||||
buf.StorageSpan[buf.Length] = 0;
|
||||
|
||||
Span<char> charsBuf = stackalloc char[2];
|
||||
Span<byte> bytesBuf = stackalloc byte[4];
|
||||
for (var i = 0; i < buf.Length;)
|
||||
{
|
||||
var c1 = buf.Utf8GetCodepoint(i, out var cb, -1);
|
||||
switch (c1)
|
||||
{
|
||||
// Note that buf.Data[i + 1] is always defined. See the beginning of the function.
|
||||
case '\r' when buf.Data[i + 1] == '\n':
|
||||
// If it's already CR LF, it passes all filters.
|
||||
i += 2;
|
||||
break;
|
||||
|
||||
case >= 0xD800 and <= 0xDFFF when sanitizeSurrogates:
|
||||
{
|
||||
var c2 = buf.Utf8GetCodepoint(i + cb, out var cb2);
|
||||
if (c1 is < 0xD800 or >= 0xDC00)
|
||||
goto case -2;
|
||||
if (c2 is < 0xDC00 or >= 0xE000)
|
||||
goto case -2;
|
||||
charsBuf[0] = unchecked((char)c1);
|
||||
charsBuf[1] = unchecked((char)c2);
|
||||
var bytesLen = Encoding.UTF8.GetBytes(charsBuf, bytesBuf);
|
||||
buf.ReplaceRange(i, cb + cb2, bytesBuf[..bytesLen]);
|
||||
// Do not alter i; now that the WTF-8 has been dealt with, apply other filters.
|
||||
break;
|
||||
}
|
||||
|
||||
case -2:
|
||||
case -1 or 0xFFFE or 0xFFFF when sanitizeInvalidCharacters:
|
||||
case >= 0xD800 and <= 0xDFFF when sanitizeInvalidCharacters:
|
||||
case > char.MaxValue when sanitizeNonUcs2Characters:
|
||||
{
|
||||
buf.ReplaceRange(i, cb, invalidChar);
|
||||
i += invalidChar.Length;
|
||||
break;
|
||||
}
|
||||
|
||||
// See String.Manipulation.cs: IndexOfNewlineChar.
|
||||
// CR; Carriage Return
|
||||
// LF; Line Feed
|
||||
// FF; Form Feed
|
||||
// NEL; Next Line
|
||||
// LS; Line Separator
|
||||
// PS; Paragraph Separator
|
||||
case '\r' or '\n' or '\f' or '\u0085' or '\u2028' or '\u2029' when normalizeLineEndings:
|
||||
{
|
||||
buf.ReplaceRange(i, cb, lineEnding);
|
||||
i += lineEnding.Length;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
i += cb;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ namespace Dalamud.Interface.Utility;
|
|||
/// <summary>
|
||||
/// Utility methods for <see cref="ImVectorWrapper{T}"/>.
|
||||
/// </summary>
|
||||
public static partial class ImVectorWrapper
|
||||
public static class ImVectorWrapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="ImVectorWrapper{T}"/> struct, initialized with
|
||||
|
|
@ -208,7 +208,7 @@ public unsafe struct ImVectorWrapper<T> : IList<T>, IList, IReadOnlyList<T>, IDi
|
|||
/// </summary>
|
||||
/// <param name="initialCapacity">The initial capacity.</param>
|
||||
/// <param name="destroyer">The destroyer function to call on item removal.</param>
|
||||
public ImVectorWrapper(int initialCapacity = 0, ImGuiNativeDestroyDelegate? destroyer = null)
|
||||
public ImVectorWrapper(int initialCapacity, ImGuiNativeDestroyDelegate? destroyer = null)
|
||||
{
|
||||
if (initialCapacity < 0)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue