Just use win32 APIs

This commit is contained in:
Soreepeong 2023-12-09 15:25:50 +09:00
parent f6d16d5624
commit 06938509e7
4 changed files with 125 additions and 231 deletions

View file

@ -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" />

View file

@ -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 buffer = ImGuiCurrentContextClipboardHandlerData;
buffer.TrimZeroTerminator();
buffer.Utf8Normalize();
buffer.AddZeroTerminatorIfMissing();
return buffer.Data;
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;
}
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;
}
}

View file

@ -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;
}
}
}
}

View file

@ -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)
{