mirror of
https://github.com/goatcorp/Dalamud.git
synced 2026-01-01 05:13: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 System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
using Dalamud.Interface.Utility;
|
||||||
|
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
|
|
||||||
namespace Dalamud.Interface.Internal;
|
namespace Dalamud.Interface.Internal;
|
||||||
|
|
@ -21,60 +26,209 @@ namespace Dalamud.Interface.Internal;
|
||||||
/// works for both ImGui and XIV.
|
/// works for both ImGui and XIV.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
internal static class ImGuiClipboardConfig
|
[ServiceManager.EarlyLoadedService]
|
||||||
|
internal sealed unsafe class ImGuiClipboardConfig : IServiceType, IDisposable
|
||||||
{
|
{
|
||||||
private delegate void SetClipboardTextDelegate(IntPtr userData, string text);
|
private readonly nint clipboardUserDataOriginal;
|
||||||
private delegate string GetClipboardTextDelegate();
|
private readonly delegate* unmanaged<nint, byte*, void> setTextOriginal;
|
||||||
|
private readonly delegate* unmanaged<nint, byte*> getTextOriginal;
|
||||||
|
|
||||||
private static SetClipboardTextDelegate? _setTextOriginal = null;
|
[ServiceManager.ServiceConstructor]
|
||||||
private static GetClipboardTextDelegate? _getTextOriginal = null;
|
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
|
var io = ImGui.GetIO();
|
||||||
private static SetClipboardTextDelegate? _setText = null;
|
this.setTextOriginal = (delegate* unmanaged<nint, byte*, void>)io.SetClipboardTextFn;
|
||||||
private static GetClipboardTextDelegate? _getText = null;
|
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();
|
var io = ImGui.GetIO();
|
||||||
if (_setTextOriginal == null)
|
if (io.ClipboardUserData == default)
|
||||||
{
|
return;
|
||||||
_setTextOriginal =
|
|
||||||
Marshal.GetDelegateForFunctionPointer<SetClipboardTextDelegate>(io.SetClipboardTextFn);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_getTextOriginal == null)
|
GCHandle.FromIntPtr(io.ClipboardUserData).Free();
|
||||||
{
|
io.SetClipboardTextFn = (nint)this.setTextOriginal;
|
||||||
_getTextOriginal =
|
io.GetClipboardTextFn = (nint)this.getTextOriginal;
|
||||||
Marshal.GetDelegateForFunctionPointer<GetClipboardTextDelegate>(io.GetClipboardTextFn);
|
io.ClipboardUserData = this.clipboardUserDataOriginal;
|
||||||
}
|
|
||||||
|
|
||||||
_setText = new SetClipboardTextDelegate(SetClipboardText);
|
|
||||||
_getText = new GetClipboardTextDelegate(GetClipboardText);
|
|
||||||
|
|
||||||
io.SetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_setText);
|
|
||||||
io.GetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_getText);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Unapply()
|
private void SetClipboardTextImpl(byte* text)
|
||||||
{
|
{
|
||||||
var io = ImGui.GetIO();
|
var buffer = ImGuiCurrentContextClipboardHandlerData;
|
||||||
if (_setTextOriginal != null)
|
Utf8Utils.SetFromNullTerminatedBytes(ref buffer, text);
|
||||||
{
|
Utf8Utils.Normalize(ref buffer);
|
||||||
io.SetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_setTextOriginal);
|
Utf8Utils.AddNullTerminatorIfMissing(ref buffer);
|
||||||
}
|
this.setTextOriginal(this.clipboardUserDataOriginal, buffer.Data);
|
||||||
if (_getTextOriginal != null)
|
|
||||||
{
|
|
||||||
io.GetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_getTextOriginal);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -240,7 +240,6 @@ internal class InterfaceManager : IDisposable, IServiceType
|
||||||
this.processMessageHook?.Dispose();
|
this.processMessageHook?.Dispose();
|
||||||
}).Wait();
|
}).Wait();
|
||||||
|
|
||||||
ImGuiClipboardConfig.Unapply();
|
|
||||||
this.scene?.Dispose();
|
this.scene?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -629,7 +628,6 @@ internal class InterfaceManager : IDisposable, IServiceType
|
||||||
ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale;
|
ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale;
|
||||||
|
|
||||||
this.SetupFonts();
|
this.SetupFonts();
|
||||||
ImGuiClipboardConfig.Apply();
|
|
||||||
|
|
||||||
if (!configuration.IsDocking)
|
if (!configuration.IsDocking)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue