diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 3a6a0257d..d31f79e0c 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -90,6 +90,7 @@ + diff --git a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs index 76e8e73e6..fd07d824f 100644 --- a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs +++ b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs @@ -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; /// @@ -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 setTextOriginal; - private readonly delegate* unmanaged getTextOriginal; + private readonly nint setTextOriginal; + private readonly nint getTextOriginal; + + [ServiceManager.ServiceDependency] + private readonly ToastGui toastGui = Service.Get(); + + private ImVectorWrapper 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)io.SetClipboardTextFn; - this.getTextOriginal = (delegate* unmanaged)io.GetClipboardTextFn; + this.setTextOriginal = io.SetClipboardTextFn; + this.getTextOriginal = io.GetClipboardTextFn; io.ClipboardUserData = GCHandle.ToIntPtr(this.clipboardUserData = GCHandle.Alloc(this)); io.SetClipboardTextFn = (nint)(delegate* unmanaged)&StaticSetClipboardTextImpl; io.GetClipboardTextFn = (nint)(delegate* unmanaged)&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 ImGuiCurrentContextClipboardHandlerData => - new((ImVector*)(ImGui.GetCurrentContext() + 0x5520)); - /// 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; } } diff --git a/Dalamud/Interface/Utility/ImVectorWrapper.ZeroTerminatedSequence.cs b/Dalamud/Interface/Utility/ImVectorWrapper.ZeroTerminatedSequence.cs deleted file mode 100644 index 507bdce20..000000000 --- a/Dalamud/Interface/Utility/ImVectorWrapper.ZeroTerminatedSequence.cs +++ /dev/null @@ -1,207 +0,0 @@ -using System.Numerics; -using System.Text; - -namespace Dalamud.Interface.Utility; - -/// -/// Utility methods for . -/// -public static partial class ImVectorWrapper -{ - /// - /// Appends from , a zero terminated sequence. - /// - /// The element type. - /// The target buffer. - /// The pointer to the zero-terminated sequence. - public static unsafe void AppendZeroTerminatedSequence(this ref ImVectorWrapper buf, T* psz) - where T : unmanaged, INumber - { - var len = 0; - while (psz[len] != default) - len++; - - buf.AddRange(new Span(psz, len)); - } - - /// - /// Sets from , a zero terminated sequence. - /// - /// The element type. - /// The target buffer. - /// The pointer to the zero-terminated sequence. - public static unsafe void SetFromZeroTerminatedSequence(this ref ImVectorWrapper buf, T* psz) - where T : unmanaged, INumber - { - buf.Clear(); - buf.AppendZeroTerminatedSequence(psz); - } - - /// - /// Trims zero terminator(s). - /// - /// The element type. - /// The buffer. - public static void TrimZeroTerminator(this ref ImVectorWrapper buf) - where T : unmanaged, INumber - { - ref var len = ref buf.LengthUnsafe; - while (len > 0 && buf[len - 1] == default) - len--; - } - - /// - /// Adds a zero terminator to the buffer, if missing. - /// - /// The element type. - /// The buffer. - public static void AddZeroTerminatorIfMissing(this ref ImVectorWrapper buf) - where T : unmanaged, INumber - { - if (buf.Length > 0 && buf[^1] == default) - return; - buf.Add(default); - } - - /// - /// Gets the codepoint at the given offset. - /// - /// The buffer containing bytes in UTF-8. - /// The offset in bytes. - /// Number of bytes occupied by the character, invalid or not. - /// The fallback character, if no valid UTF-8 character could be found. - /// The parsed codepoint, or if it could not be parsed correctly. - public static unsafe int Utf8GetCodepoint( - this in ImVectorWrapper 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; - } - - /// - /// Normalizes the given UTF-8 string.
- /// Using the default values will ensure the best interop between the game, ImGui, and Windows. - ///
- /// The buffer containing bytes in UTF-8. - /// The replacement line ending. If empty, CR LF will be used. - /// The replacement invalid character. If empty, U+FFFD REPLACEMENT CHARACTER will be used. - /// Specify whether to normalize the line endings. - /// Specify whether to replace invalid characters. - /// Specify whether to replace characters that requires the use of surrogate, when encoded in UTF-16. - /// Specify whether to make sense out of WTF-8. - public static unsafe void Utf8Normalize( - this ref ImVectorWrapper buf, - ReadOnlySpan lineEnding = default, - ReadOnlySpan 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 charsBuf = stackalloc char[2]; - Span 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; - } - } - } -} diff --git a/Dalamud/Interface/Utility/ImVectorWrapper.cs b/Dalamud/Interface/Utility/ImVectorWrapper.cs index 51524efc4..5ba1aec2f 100644 --- a/Dalamud/Interface/Utility/ImVectorWrapper.cs +++ b/Dalamud/Interface/Utility/ImVectorWrapper.cs @@ -13,7 +13,7 @@ namespace Dalamud.Interface.Utility; /// /// Utility methods for . /// -public static partial class ImVectorWrapper +public static class ImVectorWrapper { /// /// Creates a new instance of the struct, initialized with @@ -208,7 +208,7 @@ public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDi /// /// The initial capacity. /// The destroyer function to call on item removal. - public ImVectorWrapper(int initialCapacity = 0, ImGuiNativeDestroyDelegate? destroyer = null) + public ImVectorWrapper(int initialCapacity, ImGuiNativeDestroyDelegate? destroyer = null) { if (initialCapacity < 0) {