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