diff --git a/.editorconfig b/.editorconfig
index 66e123f53..141e8c9c9 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -104,13 +104,14 @@ resharper_can_use_global_alias = false
resharper_csharp_align_multiline_parameter = true
resharper_csharp_align_multiple_declaration = true
resharper_csharp_empty_block_style = multiline
-resharper_csharp_int_align_comments = true
+resharper_csharp_int_align_comments = false
resharper_csharp_new_line_before_while = true
resharper_csharp_wrap_after_declaration_lpar = true
resharper_csharp_wrap_after_invocation_lpar = true
resharper_csharp_wrap_arguments_style = chop_if_long
resharper_enforce_line_ending_style = true
resharper_instance_members_qualify_declared_in = this_class, base_class
+resharper_int_align = false
resharper_member_can_be_private_global_highlighting = none
resharper_member_can_be_private_local_highlighting = none
resharper_new_line_before_finally = true
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/ImGuiClipboardConfig.cs b/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs
deleted file mode 100644
index b3302add4..000000000
--- a/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs
+++ /dev/null
@@ -1,80 +0,0 @@
-using System.Runtime.InteropServices;
-using ImGuiNET;
-
-namespace Dalamud.Interface.Internal;
-
-///
-/// Configures the ImGui clipboard behaviour to work nicely with XIV.
-///
-///
-///
-/// XIV uses '\r' for line endings and will truncate all text after a '\n' character.
-/// This means that copy/pasting multi-line text from ImGui to XIV will only copy the first line.
-///
-///
-/// ImGui uses '\n' for line endings and will ignore '\r' entirely.
-/// This means that copy/pasting multi-line text from XIV to ImGui will copy all the text
-/// without line breaks.
-///
-///
-/// To fix this we normalize all clipboard line endings entering/exiting ImGui to '\r\n' which
-/// works for both ImGui and XIV.
-///
-///
-internal static class ImGuiClipboardConfig
-{
- private delegate void SetClipboardTextDelegate(IntPtr userData, string text);
- private delegate string GetClipboardTextDelegate();
-
- private static SetClipboardTextDelegate? _setTextOriginal = null;
- private static GetClipboardTextDelegate? _getTextOriginal = null;
-
- // These must exist as variables to prevent them from being GC'd
- private static SetClipboardTextDelegate? _setText = null;
- private static GetClipboardTextDelegate? _getText = null;
-
- public static void Apply()
- {
- var io = ImGui.GetIO();
- if (_setTextOriginal == null)
- {
- _setTextOriginal =
- Marshal.GetDelegateForFunctionPointer(io.SetClipboardTextFn);
- }
-
- if (_getTextOriginal == null)
- {
- _getTextOriginal =
- Marshal.GetDelegateForFunctionPointer(io.GetClipboardTextFn);
- }
-
- _setText = new SetClipboardTextDelegate(SetClipboardText);
- _getText = new GetClipboardTextDelegate(GetClipboardText);
-
- io.SetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_setText);
- io.GetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_getText);
- }
-
- public static void Unapply()
- {
- var io = ImGui.GetIO();
- if (_setTextOriginal != null)
- {
- io.SetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_setTextOriginal);
- }
- if (_getTextOriginal != null)
- {
- io.GetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_getTextOriginal);
- }
- }
-
- private static void SetClipboardText(IntPtr userData, string text)
- {
- _setTextOriginal!(userData, text.ReplaceLineEndings("\r\n"));
- }
-
- private static string GetClipboardText()
- {
- return _getTextOriginal!().ReplaceLineEndings("\r\n");
- }
-}
diff --git a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs
new file mode 100644
index 000000000..fd07d824f
--- /dev/null
+++ b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs
@@ -0,0 +1,199 @@
+using System.Diagnostics;
+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;
+
+///
+/// Configures the ImGui clipboard behaviour to work nicely with XIV.
+///
+///
+///
+/// XIV uses '\r' for line endings and will truncate all text after a '\n' character.
+/// This means that copy/pasting multi-line text from ImGui to XIV will only copy the first line.
+///
+///
+/// ImGui uses '\n' for line endings and will ignore '\r' entirely.
+/// This means that copy/pasting multi-line text from XIV to ImGui will copy all the text
+/// without line breaks.
+///
+///
+/// To fix this we normalize all clipboard line endings entering/exiting ImGui to '\r\n' which
+/// works for both ImGui and XIV.
+///
+///
+[ServiceManager.EarlyLoadedService]
+internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDisposable
+{
+ private static readonly ModuleLog Log = new(nameof(ImGuiClipboardFunctionProvider));
+ private readonly nint clipboardUserDataOriginal;
+ private readonly nint setTextOriginal;
+ private readonly nint getTextOriginal;
+
+ [ServiceManager.ServiceDependency]
+ private readonly ToastGui toastGui = Service.Get();
+
+ private ImVectorWrapper clipboardData;
+ private GCHandle clipboardUserData;
+
+ [ServiceManager.ServiceConstructor]
+ private ImGuiClipboardFunctionProvider(InterfaceManager.InterfaceManagerWithScene imws)
+ {
+ // Effectively waiting for ImGui to become available.
+ _ = imws;
+ Debug.Assert(ImGuiHelpers.IsImGuiInitialized, "IMWS initialized but IsImGuiInitialized is false?");
+
+ var io = ImGui.GetIO();
+ this.clipboardUserDataOriginal = io.ClipboardUserData;
+ 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]
+ static void StaticSetClipboardTextImpl(nint userData, byte* text) =>
+ ((ImGuiClipboardFunctionProvider)GCHandle.FromIntPtr(userData).Target)!.SetClipboardTextImpl(text);
+
+ [UnmanagedCallersOnly]
+ static byte* StaticGetClipboardTextImpl(nint userData) =>
+ ((ImGuiClipboardFunctionProvider)GCHandle.FromIntPtr(userData).Target)!.GetClipboardTextImpl();
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (!this.clipboardUserData.IsAllocated)
+ return;
+
+ var io = ImGui.GetIO();
+ 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)
+ {
+ 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.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;
+ }
+
+ 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/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs
index 7d164c01f..1b12fd853 100644
--- a/Dalamud/Interface/Internal/InterfaceManager.cs
+++ b/Dalamud/Interface/Internal/InterfaceManager.cs
@@ -240,7 +240,6 @@ internal class InterfaceManager : IDisposable, IServiceType
this.processMessageHook?.Dispose();
}).Wait();
- ImGuiClipboardConfig.Unapply();
this.scene?.Dispose();
}
@@ -629,7 +628,6 @@ internal class InterfaceManager : IDisposable, IServiceType
ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale;
this.SetupFonts();
- ImGuiClipboardConfig.Apply();
if (!configuration.IsDocking)
{
diff --git a/Dalamud/Interface/Utility/ImVectorWrapper.cs b/Dalamud/Interface/Utility/ImVectorWrapper.cs
index 67b002179..5ba1aec2f 100644
--- a/Dalamud/Interface/Utility/ImVectorWrapper.cs
+++ b/Dalamud/Interface/Utility/ImVectorWrapper.cs
@@ -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)
{
@@ -394,7 +394,7 @@ public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDi
}
///
- public void AddRange(Span items)
+ public void AddRange(ReadOnlySpan items)
{
this.EnsureCapacityExponential(this.LengthUnsafe + items.Length);
foreach (var item in items)
@@ -466,7 +466,7 @@ public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDi
/// The minimum capacity to ensure.
/// Whether the capacity has been changed.
public bool EnsureCapacityExponential(int capacity)
- => this.EnsureCapacity(1 << ((sizeof(int) * 8) - BitOperations.LeadingZeroCount((uint)this.LengthUnsafe)));
+ => this.EnsureCapacity(1 << ((sizeof(int) * 8) - BitOperations.LeadingZeroCount((uint)capacity)));
///
/// Resizes the underlying array and fills with zeroes if grown.
@@ -519,10 +519,11 @@ public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDi
if (index < 0 || index > this.LengthUnsafe)
throw new IndexOutOfRangeException();
- this.EnsureCapacityExponential(this.CapacityUnsafe + 1);
+ this.EnsureCapacityExponential(this.LengthUnsafe + 1);
var num = this.LengthUnsafe - index;
Buffer.MemoryCopy(this.DataUnsafe + index, this.DataUnsafe + index + 1, num * sizeof(T), num * sizeof(T));
this.DataUnsafe[index] = item;
+ this.LengthUnsafe += 1;
}
///
@@ -535,6 +536,7 @@ public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDi
Buffer.MemoryCopy(this.DataUnsafe + index, this.DataUnsafe + index + count, num * sizeof(T), num * sizeof(T));
foreach (var item in items)
this.DataUnsafe[index++] = item;
+ this.LengthUnsafe += count;
}
else
{
@@ -543,14 +545,15 @@ public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDi
}
}
- ///
- public void InsertRange(int index, Span items)
+ ///
+ public void InsertRange(int index, ReadOnlySpan items)
{
this.EnsureCapacityExponential(this.LengthUnsafe + items.Length);
var num = this.LengthUnsafe - index;
Buffer.MemoryCopy(this.DataUnsafe + index, this.DataUnsafe + index + items.Length, num * sizeof(T), num * sizeof(T));
foreach (var item in items)
this.DataUnsafe[index++] = item;
+ this.LengthUnsafe += items.Length;
}
///
@@ -558,15 +561,7 @@ public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDi
///
/// The index.
/// Whether to skip calling the destroyer function.
- public void RemoveAt(int index, bool skipDestroyer = false)
- {
- this.EnsureIndex(index);
- var num = this.LengthUnsafe - index - 1;
- if (!skipDestroyer)
- this.destroyer?.Invoke(&this.DataUnsafe[index]);
-
- Buffer.MemoryCopy(this.DataUnsafe + index + 1, this.DataUnsafe + index, num * sizeof(T), num * sizeof(T));
- }
+ public void RemoveAt(int index, bool skipDestroyer = false) => this.RemoveRange(index, 1, skipDestroyer);
///
void IList.RemoveAt(int index) => this.RemoveAt(index);
@@ -574,6 +569,73 @@ public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDi
///
void IList.RemoveAt(int index) => this.RemoveAt(index);
+ ///
+ /// Removes elements at the given index.
+ ///
+ /// The index of the first item to remove.
+ /// Number of items to remove.
+ /// Whether to skip calling the destroyer function.
+ public void RemoveRange(int index, int count, bool skipDestroyer = false)
+ {
+ this.EnsureIndex(index);
+ if (count < 0)
+ throw new ArgumentOutOfRangeException(nameof(count), count, "Must be positive.");
+ if (count == 0)
+ return;
+
+ if (!skipDestroyer && this.destroyer is { } d)
+ {
+ for (var i = 0; i < count; i++)
+ d(this.DataUnsafe + index + i);
+ }
+
+ var numItemsToMove = this.LengthUnsafe - index - count;
+ var numBytesToMove = numItemsToMove * sizeof(T);
+ Buffer.MemoryCopy(this.DataUnsafe + index + count, this.DataUnsafe + index, numBytesToMove, numBytesToMove);
+ this.LengthUnsafe -= count;
+ }
+
+ ///
+ /// Replaces a sequence at given offset of items with
+ /// .
+ ///
+ /// The index of the first item to be replaced.
+ /// The number of items to be replaced.
+ /// The replacement.
+ /// Whether to skip calling the destroyer function.
+ public void ReplaceRange(int index, int count, ReadOnlySpan replacement, bool skipDestroyer = false)
+ {
+ this.EnsureIndex(index);
+ if (count < 0)
+ throw new ArgumentOutOfRangeException(nameof(count), count, "Must be positive.");
+ if (count == 0)
+ return;
+
+ // Ensure the capacity first, so that we can safely destroy the items first.
+ this.EnsureCapacityExponential((this.LengthUnsafe + replacement.Length) - count);
+
+ if (!skipDestroyer && this.destroyer is { } d)
+ {
+ for (var i = 0; i < count; i++)
+ d(this.DataUnsafe + index + i);
+ }
+
+ if (count == replacement.Length)
+ {
+ replacement.CopyTo(this.DataSpan[index..]);
+ }
+ else if (count > replacement.Length)
+ {
+ replacement.CopyTo(this.DataSpan[index..]);
+ this.RemoveRange(index + replacement.Length, count - replacement.Length);
+ }
+ else
+ {
+ replacement[..count].CopyTo(this.DataSpan[index..]);
+ this.InsertRange(index + count, replacement[count..]);
+ }
+ }
+
///
/// Sets the capacity exactly as requested.
///
@@ -611,9 +673,6 @@ public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDi
if (!oldSpan.IsEmpty && !newSpan.IsEmpty)
oldSpan[..this.LengthUnsafe].CopyTo(newSpan);
-// #if DEBUG
-// new Span(newAlloc + this.LengthUnsafe, sizeof(T) * (capacity - this.LengthUnsafe)).Fill(0xCC);
-// #endif
if (oldAlloc != null)
ImGuiNative.igMemFree(oldAlloc);