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