mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-30 20:33:40 +01:00
Reimplement clipboard text normalizer to use the correct buffers
This commit is contained in:
parent
0c3ebd4b5b
commit
8967174cd9
2 changed files with 193 additions and 41 deletions
|
|
@ -1,4 +1,9 @@
|
|||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using Dalamud.Interface.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.Internal;
|
||||
|
|
@ -21,60 +26,209 @@ namespace Dalamud.Interface.Internal;
|
|||
/// works for both ImGui and XIV.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal static class ImGuiClipboardConfig
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal sealed unsafe class ImGuiClipboardConfig : IServiceType, IDisposable
|
||||
{
|
||||
private delegate void SetClipboardTextDelegate(IntPtr userData, string text);
|
||||
private delegate string GetClipboardTextDelegate();
|
||||
private readonly nint clipboardUserDataOriginal;
|
||||
private readonly delegate* unmanaged<nint, byte*, void> setTextOriginal;
|
||||
private readonly delegate* unmanaged<nint, byte*> getTextOriginal;
|
||||
|
||||
private static SetClipboardTextDelegate? _setTextOriginal = null;
|
||||
private static GetClipboardTextDelegate? _getTextOriginal = null;
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private ImGuiClipboardConfig(InterfaceManager.InterfaceManagerWithScene imws)
|
||||
{
|
||||
// Effectively waiting for ImGui to become available.
|
||||
_ = imws;
|
||||
Debug.Assert(ImGuiHelpers.IsImGuiInitialized, "IMWS initialized but IsImGuiInitialized is false?");
|
||||
|
||||
// These must exist as variables to prevent them from being GC'd
|
||||
private static SetClipboardTextDelegate? _setText = null;
|
||||
private static GetClipboardTextDelegate? _getText = null;
|
||||
var io = ImGui.GetIO();
|
||||
this.setTextOriginal = (delegate* unmanaged<nint, byte*, void>)io.SetClipboardTextFn;
|
||||
this.getTextOriginal = (delegate* unmanaged<nint, byte*>)io.GetClipboardTextFn;
|
||||
this.clipboardUserDataOriginal = io.ClipboardUserData;
|
||||
io.SetClipboardTextFn = (nint)(delegate* unmanaged<nint, byte*, void>)(&StaticSetClipboardTextImpl);
|
||||
io.GetClipboardTextFn = (nint)(delegate* unmanaged<nint, byte*>)&StaticGetClipboardTextImpl;
|
||||
io.ClipboardUserData = GCHandle.ToIntPtr(GCHandle.Alloc(this));
|
||||
return;
|
||||
|
||||
public static void Apply()
|
||||
[UnmanagedCallersOnly]
|
||||
static void StaticSetClipboardTextImpl(nint userData, byte* text) =>
|
||||
((ImGuiClipboardConfig)GCHandle.FromIntPtr(userData).Target)!.SetClipboardTextImpl(text);
|
||||
|
||||
[UnmanagedCallersOnly]
|
||||
static byte* StaticGetClipboardTextImpl(nint userData) =>
|
||||
((ImGuiClipboardConfig)GCHandle.FromIntPtr(userData).Target)!.GetClipboardTextImpl();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes an instance of the <see cref="ImGuiClipboardConfig"/> class.
|
||||
/// </summary>
|
||||
~ImGuiClipboardConfig() => this.ReleaseUnmanagedResources();
|
||||
|
||||
[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()
|
||||
{
|
||||
this.ReleaseUnmanagedResources();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void ReleaseUnmanagedResources()
|
||||
{
|
||||
var io = ImGui.GetIO();
|
||||
if (_setTextOriginal == null)
|
||||
{
|
||||
_setTextOriginal =
|
||||
Marshal.GetDelegateForFunctionPointer<SetClipboardTextDelegate>(io.SetClipboardTextFn);
|
||||
}
|
||||
if (io.ClipboardUserData == default)
|
||||
return;
|
||||
|
||||
if (_getTextOriginal == null)
|
||||
{
|
||||
_getTextOriginal =
|
||||
Marshal.GetDelegateForFunctionPointer<GetClipboardTextDelegate>(io.GetClipboardTextFn);
|
||||
}
|
||||
|
||||
_setText = new SetClipboardTextDelegate(SetClipboardText);
|
||||
_getText = new GetClipboardTextDelegate(GetClipboardText);
|
||||
|
||||
io.SetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_setText);
|
||||
io.GetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_getText);
|
||||
GCHandle.FromIntPtr(io.ClipboardUserData).Free();
|
||||
io.SetClipboardTextFn = (nint)this.setTextOriginal;
|
||||
io.GetClipboardTextFn = (nint)this.getTextOriginal;
|
||||
io.ClipboardUserData = this.clipboardUserDataOriginal;
|
||||
}
|
||||
|
||||
public static void Unapply()
|
||||
private void SetClipboardTextImpl(byte* text)
|
||||
{
|
||||
var io = ImGui.GetIO();
|
||||
if (_setTextOriginal != null)
|
||||
{
|
||||
io.SetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_setTextOriginal);
|
||||
}
|
||||
if (_getTextOriginal != null)
|
||||
{
|
||||
io.GetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_getTextOriginal);
|
||||
}
|
||||
var buffer = ImGuiCurrentContextClipboardHandlerData;
|
||||
Utf8Utils.SetFromNullTerminatedBytes(ref buffer, text);
|
||||
Utf8Utils.Normalize(ref buffer);
|
||||
Utf8Utils.AddNullTerminatorIfMissing(ref buffer);
|
||||
this.setTextOriginal(this.clipboardUserDataOriginal, buffer.Data);
|
||||
}
|
||||
|
||||
private static void SetClipboardText(IntPtr userData, string text)
|
||||
private byte* GetClipboardTextImpl()
|
||||
{
|
||||
_setTextOriginal!(userData, text.ReplaceLineEndings("\r\n"));
|
||||
_ = this.getTextOriginal(this.clipboardUserDataOriginal);
|
||||
|
||||
var buffer = ImGuiCurrentContextClipboardHandlerData;
|
||||
Utf8Utils.TrimNullTerminator(ref buffer);
|
||||
Utf8Utils.Normalize(ref buffer);
|
||||
Utf8Utils.AddNullTerminatorIfMissing(ref buffer);
|
||||
return buffer.Data;
|
||||
}
|
||||
|
||||
private static string GetClipboardText()
|
||||
private static class Utf8Utils
|
||||
{
|
||||
return _getTextOriginal!().ReplaceLineEndings("\r\n");
|
||||
/// <summary>
|
||||
/// Sets <paramref name="buf"/> from <paramref name="psz"/>, a null terminated UTF-8 string.
|
||||
/// </summary>
|
||||
/// <param name="buf">The target buffer. It will not contain a null terminator.</param>
|
||||
/// <param name="psz">The pointer to the null-terminated UTF-8 string.</param>
|
||||
public static void SetFromNullTerminatedBytes(ref ImVectorWrapper<byte> buf, byte* psz)
|
||||
{
|
||||
var len = 0;
|
||||
while (psz[len] != 0)
|
||||
len++;
|
||||
|
||||
buf.Clear();
|
||||
buf.AddRange(new Span<byte>(psz, len));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the null terminator.
|
||||
/// </summary>
|
||||
/// <param name="buf">The UTF-8 string buffer.</param>
|
||||
public static void TrimNullTerminator(ref ImVectorWrapper<byte> buf)
|
||||
{
|
||||
while (buf.Length > 0 && buf[^1] == 0)
|
||||
buf.LengthUnsafe--;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a null terminator to the buffer.
|
||||
/// </summary>
|
||||
/// <param name="buf">The buffer.</param>
|
||||
public static void AddNullTerminatorIfMissing(ref ImVectorWrapper<byte> buf)
|
||||
{
|
||||
if (buf.Length > 0 && buf[^1] == 0)
|
||||
return;
|
||||
buf.Add(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Counts the number of bytes for the UTF-8 character.
|
||||
/// </summary>
|
||||
/// <param name="b">The bytes.</param>
|
||||
/// <param name="avail">Available number of bytes.</param>
|
||||
/// <returns>Number of bytes taken, or -1 if the byte was invalid.</returns>
|
||||
public static int CountBytes(byte* b, int avail)
|
||||
{
|
||||
if (avail <= 0)
|
||||
return 0;
|
||||
if ((b[0] & 0x80) == 0)
|
||||
return 1;
|
||||
if ((b[0] & 0xE0) == 0xC0 && avail >= 2)
|
||||
return (b[1] & 0xC0) == 0x80 ? 2 : -1;
|
||||
if ((b[0] & 0xF0) == 0xE0 && avail >= 3)
|
||||
return (b[1] & 0xC0) == 0x80 && (b[2] & 0xC0) == 0x80 ? 3 : -1;
|
||||
if ((b[0] & 0xF8) == 0xF0 && avail >= 4)
|
||||
return (b[1] & 0xC0) == 0x80 && (b[2] & 0xC0) == 0x80 && (b[3] & 0xC0) == 0x80 ? 4 : -1;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the codepoint.
|
||||
/// </summary>
|
||||
/// <param name="b">The bytes.</param>
|
||||
/// <param name="cb">The result from <see cref="CountBytes"/>.</param>
|
||||
/// <returns>The codepoint, or \xFFFD replacement character if failed.</returns>
|
||||
public static int GetCodepoint(byte* b, int cb) => cb switch
|
||||
{
|
||||
1 => b[0],
|
||||
2 => ((b[0] & 0x8F) << 6) | (b[1] & 0x3F),
|
||||
3 => ((b[0] & 0x0F) << 12) | ((b[1] & 0x3F) << 6) | (b[2] & 0x3F),
|
||||
4 => ((b[0] & 0x0F) << 18) | ((b[1] & 0x3F) << 12) | ((b[2] & 0x3F) << 6) | (b[3] & 0x3F),
|
||||
_ => 0xFFFD,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Normalize the given text for our use case.
|
||||
/// </summary>
|
||||
/// <param name="buf">The buffer.</param>
|
||||
public static void Normalize(ref ImVectorWrapper<byte> buf)
|
||||
{
|
||||
for (var i = 0; i < buf.Length;)
|
||||
{
|
||||
// Already correct?
|
||||
if (buf[i] is 0x0D && buf[i + 1] is 0x0A)
|
||||
{
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
var cb = CountBytes(buf.Data + i, buf.Length - i);
|
||||
var currInt = GetCodepoint(buf.Data + i, cb);
|
||||
switch (currInt)
|
||||
{
|
||||
case 0xFFFF: // Simply invalid
|
||||
case > char.MaxValue: // ImWchar is same size with char; does not support
|
||||
case >= 0xD800 and <= 0xDBFF: // UTF-16 surrogate; does not support
|
||||
// Replace with \uFFFD in UTF-8: EF BF BD
|
||||
buf[i++] = 0xEF;
|
||||
buf.Insert(i++, 0xBF);
|
||||
buf.Insert(i++, 0xBD);
|
||||
break;
|
||||
|
||||
// See String.Manipulation.cs: IndexOfNewlineChar.
|
||||
case '\r': // CR; Carriage Return
|
||||
case '\n': // LF; Line Feed
|
||||
case '\f': // FF; Form Feed
|
||||
buf[i++] = 0x0D;
|
||||
buf.Insert(i++, 0x0A);
|
||||
break;
|
||||
|
||||
case '\u0085': // NEL; Next Line
|
||||
case '\u2028': // LS; Line Separator
|
||||
case '\u2029': // PS; Paragraph Separator
|
||||
buf[i++] = 0x0D;
|
||||
buf[i++] = 0x0A;
|
||||
break;
|
||||
|
||||
default:
|
||||
// Not a newline char.
|
||||
i += cb;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue