Implement emotes part 2
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<Version>1.23.5</Version>
|
||||
<Version>1.24.0</Version>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
@@ -40,6 +40,9 @@ internal class Configuration : IPluginConfiguration
|
||||
public bool KeepInputFocus = true;
|
||||
public int MaxLinesToRender = 10_000;
|
||||
|
||||
public bool ShowEmotes = true;
|
||||
public HashSet<string> BlockedEmotes = [];
|
||||
|
||||
public bool FontsEnabled = true;
|
||||
public ExtraGlyphRanges ExtraGlyphRanges = 0;
|
||||
public float FontSize = 17f;
|
||||
@@ -88,6 +91,8 @@ internal class Configuration : IPluginConfiguration
|
||||
PlaySounds = other.PlaySounds;
|
||||
KeepInputFocus = other.KeepInputFocus;
|
||||
MaxLinesToRender = other.MaxLinesToRender;
|
||||
ShowEmotes = other.ShowEmotes;
|
||||
BlockedEmotes = other.BlockedEmotes;
|
||||
FontsEnabled = other.FontsEnabled;
|
||||
ExtraGlyphRanges = other.ExtraGlyphRanges;
|
||||
FontSize = other.FontSize;
|
||||
|
||||
@@ -39,7 +39,7 @@ public static class EmoteCache
|
||||
|
||||
private static readonly Dictionary<string, Emote> Cache = new();
|
||||
|
||||
private static readonly string[] EmoteCodeArray = [];
|
||||
public static readonly string[] EmoteCodeArray = [];
|
||||
|
||||
static EmoteCache()
|
||||
{
|
||||
|
||||
+3
-2
@@ -72,7 +72,8 @@ internal class Message
|
||||
internal Dictionary<Guid, float?> Height { get; } = new();
|
||||
internal Dictionary<Guid, bool> IsVisible { get; } = new();
|
||||
|
||||
internal Message(ulong receiver, ulong contentId, ChatCode code, List<Chunk> sender, List<Chunk> content, SeString senderSource, SeString contentSource) {
|
||||
internal Message(ulong receiver, ulong contentId, ChatCode code, List<Chunk> sender, List<Chunk> content, SeString senderSource, SeString contentSource)
|
||||
{
|
||||
Receiver = receiver;
|
||||
ContentId = contentId;
|
||||
Date = DateTimeOffset.UtcNow;
|
||||
@@ -191,7 +192,7 @@ internal class Message
|
||||
var builder = new StringBuilder();
|
||||
foreach (var word in text.Content.Split(" "))
|
||||
{
|
||||
if (EmoteCache.Exists(word))
|
||||
if (Plugin.Config.ShowEmotes && EmoteCache.Exists(word) && !Plugin.Config.BlockedEmotes.Contains(word))
|
||||
{
|
||||
// We add all the previous collected text parts
|
||||
AddContentAfterURLCheck(builder.ToString(), text, chunk);
|
||||
|
||||
@@ -207,7 +207,7 @@ public sealed class PayloadHandler {
|
||||
|
||||
internal void Click(Chunk chunk, Payload? payload, ImGuiMouseButton button)
|
||||
{
|
||||
if (LogWindow.Plugin.Config.PlaySounds)
|
||||
if (Plugin.Config.PlaySounds)
|
||||
UIModule.PlaySound(PopupSfx);
|
||||
|
||||
switch (button)
|
||||
@@ -230,7 +230,7 @@ public sealed class PayloadHandler {
|
||||
DoHover(() => HoverStatus(status), hoverSize);
|
||||
break;
|
||||
case ItemPayload item:
|
||||
if (LogWindow.Plugin.Config.NativeItemTooltips)
|
||||
if (Plugin.Config.NativeItemTooltips)
|
||||
{
|
||||
if (!_handleTooltips || _hoveredItem != item.RawItemId)
|
||||
{
|
||||
@@ -302,7 +302,7 @@ public sealed class PayloadHandler {
|
||||
|
||||
var x = isLeft ? window.X : LogWindow.LastWindowPos.X - atkSize.X;
|
||||
var y = Math.Clamp(window.Y - atkSize.Y, 0, float.MaxValue);
|
||||
y -= isTop ? 0 : LogWindow.Plugin.Config.TooltipOffset; // offset to prevent cut-off on the bottom
|
||||
y -= isTop ? 0 : Plugin.Config.TooltipOffset; // offset to prevent cut-off on the bottom
|
||||
|
||||
atk->SetPosition((short) x, (short) y);
|
||||
}
|
||||
|
||||
+2
-1
@@ -40,6 +40,8 @@ public sealed class Plugin : IDalamudPlugin
|
||||
[PluginService] internal static INotificationManager Notification { get; private set; } = null!;
|
||||
[PluginService] internal static IAddonLifecycle AddonLifecycle { get; private set; } = null!;
|
||||
|
||||
internal static Configuration Config = null!;
|
||||
|
||||
public readonly WindowSystem WindowSystem = new(PluginName);
|
||||
public SettingsWindow SettingsWindow { get; }
|
||||
public ChatLogWindow ChatLogWindow { get; }
|
||||
@@ -48,7 +50,6 @@ public sealed class Plugin : IDalamudPlugin
|
||||
public DebuggerWindow DebuggerWindow { get; }
|
||||
internal LegacyMessageImporterWindow LegacyMessageImporterWindow { get; }
|
||||
|
||||
internal Configuration Config { get; }
|
||||
internal Commands Commands { get; }
|
||||
internal XivCommonBase Common { get; }
|
||||
internal TextureCache TextureCache { get; }
|
||||
|
||||
Generated
+45
@@ -1868,6 +1868,33 @@ namespace ChatTwo.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Blocked emotes.
|
||||
/// </summary>
|
||||
internal static string Options_Emote_BlockedEmotes {
|
||||
get {
|
||||
return ResourceManager.GetString("Options_Emote_BlockedEmotes", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Emote.
|
||||
/// </summary>
|
||||
internal static string Options_Emote_EmoteTable {
|
||||
get {
|
||||
return ResourceManager.GetString("Options_Emote_EmoteTable", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Emotes.
|
||||
/// </summary>
|
||||
internal static string Options_Emote_Tab {
|
||||
get {
|
||||
return ResourceManager.GetString("Options_Emote_Tab", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Extra glyphs can be added to {0}'s global font by enabling the checkboxes below. This will likely require increasing Dalamud's font atlas size..
|
||||
/// </summary>
|
||||
@@ -2309,6 +2336,24 @@ namespace ChatTwo.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Replaces words with their emote version, currently supports BetterTTV.
|
||||
/// </summary>
|
||||
internal static string Options_ShowEmotes_Desc {
|
||||
get {
|
||||
return ResourceManager.GetString("Options_ShowEmotes_Desc", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Show emotes.
|
||||
/// </summary>
|
||||
internal static string Options_ShowEmotes_Name {
|
||||
get {
|
||||
return ResourceManager.GetString("Options_ShowEmotes_Name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Show the Novice Network join button next to the settings button if logged in as a mentor..
|
||||
/// </summary>
|
||||
|
||||
@@ -1018,4 +1018,19 @@
|
||||
<data name="Options_KeepInputFocus_Description" xml:space="preserve">
|
||||
<value>Keeps the input focus, even if you enter battle or do other actions</value>
|
||||
</data>
|
||||
<data name="Options_ShowEmotes_Name" xml:space="preserve">
|
||||
<value>Show emotes</value>
|
||||
</data>
|
||||
<data name="Options_ShowEmotes_Desc" xml:space="preserve">
|
||||
<value>Replaces words with their emote version, currently supports BetterTTV</value>
|
||||
</data>
|
||||
<data name="Options_Emote_Tab" xml:space="preserve">
|
||||
<value>Emotes</value>
|
||||
</data>
|
||||
<data name="Options_Emote_BlockedEmotes" xml:space="preserve">
|
||||
<value>Blocked emotes</value>
|
||||
</data>
|
||||
<data name="Options_Emote_EmoteTable" xml:space="preserve">
|
||||
<value>Emote</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -31,7 +31,7 @@ public class CommandHelpWindow : Window {
|
||||
var width = 350;
|
||||
var scaledWidth = width * ImGuiHelpers.GlobalScale;
|
||||
var pos = LogWindow.LastWindowPos;
|
||||
switch (LogWindow.Plugin.Config.CommandHelpSide) {
|
||||
switch (Plugin.Config.CommandHelpSide) {
|
||||
case CommandHelpSide.Right:
|
||||
pos.X += LogWindow.LastWindowSize.X;
|
||||
break;
|
||||
|
||||
@@ -39,15 +39,16 @@ internal class Popout : Window
|
||||
|
||||
public override void PreDraw()
|
||||
{
|
||||
if (ChatLogWindow.Plugin.Config is { OverrideStyle: true, ChosenStyle: not null })
|
||||
StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == ChatLogWindow.Plugin.Config.ChosenStyle)?.Push();
|
||||
if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null })
|
||||
StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == Plugin.Config.ChosenStyle)?.Push();
|
||||
|
||||
Flags = ImGuiWindowFlags.None;
|
||||
if (!ChatLogWindow.Plugin.Config.ShowPopOutTitleBar)
|
||||
if (!Plugin.Config.ShowPopOutTitleBar)
|
||||
Flags |= ImGuiWindowFlags.NoTitleBar;
|
||||
|
||||
if (!ChatLogWindow.PopOutDocked[Idx]) {
|
||||
var alpha = Tab.IndependentOpacity ? Tab.Opacity : ChatLogWindow.Plugin.Config.WindowAlpha;
|
||||
if (!ChatLogWindow.PopOutDocked[Idx])
|
||||
{
|
||||
var alpha = Tab.IndependentOpacity ? Tab.Opacity : Plugin.Config.WindowAlpha;
|
||||
BgAlpha = alpha / 100f;
|
||||
}
|
||||
}
|
||||
@@ -56,7 +57,7 @@ internal class Popout : Window
|
||||
{
|
||||
using var id = ImRaii.PushId($"popout-{Tab.Identifier}");
|
||||
|
||||
if (!ChatLogWindow.Plugin.Config.ShowPopOutTitleBar)
|
||||
if (!Plugin.Config.ShowPopOutTitleBar)
|
||||
{
|
||||
ImGui.TextUnformatted(Tab.Name);
|
||||
ImGui.Separator();
|
||||
@@ -70,8 +71,8 @@ internal class Popout : Window
|
||||
{
|
||||
ChatLogWindow.PopOutDocked[Idx] = ImGui.IsWindowDocked();
|
||||
|
||||
if (ChatLogWindow.Plugin.Config is { OverrideStyle: true, ChosenStyle: not null })
|
||||
StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == ChatLogWindow.Plugin.Config.ChosenStyle)?.Pop();
|
||||
if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null })
|
||||
StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == Plugin.Config.ChosenStyle)?.Pop();
|
||||
}
|
||||
|
||||
public override void OnClose()
|
||||
|
||||
@@ -35,6 +35,7 @@ public sealed class SettingsWindow : Window
|
||||
{
|
||||
new Display(Mutable),
|
||||
new ChatLog(Plugin, Mutable),
|
||||
new Emote(Plugin, Mutable),
|
||||
new Ui.SettingsTabs.Fonts(Mutable),
|
||||
new ChatColours(Plugin, Mutable),
|
||||
new Tabs(Plugin, Mutable),
|
||||
|
||||
@@ -49,5 +49,7 @@ internal sealed class Display : ISettingsTab
|
||||
|
||||
ImGuiUtil.OptionCheckbox(ref Mutable.CollapseDuplicateMessages, Language.Options_CollapseDuplicateMessages_Name, Language.Options_CollapseDuplicateMessages_Description);
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGui.PopTextWrapPos();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
using System.Numerics;
|
||||
using ChatTwo.Resources;
|
||||
using ChatTwo.Util;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using ImGuiNET;
|
||||
|
||||
namespace ChatTwo.Ui.SettingsTabs;
|
||||
|
||||
internal sealed class Emote : ISettingsTab
|
||||
{
|
||||
private readonly Plugin Plugin;
|
||||
private Configuration Mutable { get; }
|
||||
|
||||
public string Name => Language.Options_Emote_Tab + "###tabs-emote";
|
||||
|
||||
private static SearchSelector.SelectorPopupOptions WordPopupOptions = null!;
|
||||
|
||||
internal Emote(Plugin plugin, Configuration mutable)
|
||||
{
|
||||
Plugin = plugin;
|
||||
Mutable = mutable;
|
||||
|
||||
WordPopupOptions = new SearchSelector.SelectorPopupOptions
|
||||
{
|
||||
FilteredSheet = EmoteCache.EmoteCodeArray.Where(w => !Mutable.BlockedEmotes.Contains(w))
|
||||
};
|
||||
}
|
||||
|
||||
public void Draw(bool changed)
|
||||
{
|
||||
ImGui.PushTextWrapPos();
|
||||
|
||||
ImGuiUtil.OptionCheckbox(ref Mutable.ShowEmotes, Language.Options_ShowEmotes_Name, Language.Options_ShowEmotes_Desc);
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGui.TextUnformatted(Language.Options_Emote_BlockedEmotes);
|
||||
ImGui.Spacing();
|
||||
|
||||
var buttonWidth = ImGui.GetContentRegionAvail().X / 3;
|
||||
using (Plugin.FontManager.FontAwesome.Push())
|
||||
ImGui.Button(FontAwesomeIcon.Plus.ToIconString(), new Vector2(buttonWidth, 0));
|
||||
|
||||
if (SearchSelector.SelectorPopup("WordAddPopup", out var newWord, WordPopupOptions))
|
||||
Mutable.BlockedEmotes.Add(newWord);
|
||||
|
||||
using var table = ImRaii.Table("##BlockedWords", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInner);
|
||||
if (table)
|
||||
{
|
||||
ImGui.TableSetupColumn(Language.Options_Emote_EmoteTable);
|
||||
ImGui.TableSetupColumn("##Del", 0, 0.07f);
|
||||
|
||||
ImGui.TableHeadersRow();
|
||||
|
||||
var copiedList = Mutable.BlockedEmotes.ToArray();
|
||||
foreach (var word in copiedList)
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(word);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if (ImGuiUtil.Button($"##{word}Del", FontAwesomeIcon.Trash, !ImGui.GetIO().KeyCtrl))
|
||||
Mutable.BlockedEmotes.Remove(word);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.PopTextWrapPos();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using System.Text;
|
||||
using Dalamud.Game.ClientState.Keys;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Components;
|
||||
using Dalamud.Interface.Style;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
@@ -260,6 +261,15 @@ internal static class ImGuiUtil
|
||||
return r;
|
||||
}
|
||||
|
||||
public static bool Button(string id, FontAwesomeIcon icon, bool disabled)
|
||||
{
|
||||
var clicked = false;
|
||||
using (ImRaii.Disabled(disabled))
|
||||
clicked = ImGuiComponents.IconButton(id, icon);
|
||||
|
||||
return clicked;
|
||||
}
|
||||
|
||||
internal static bool CtrlShiftButton(string label, string tooltip = "")
|
||||
{
|
||||
var ctrlShiftHeld = ImGui.GetIO() is { KeyCtrl: true, KeyShift: true };
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
using System.Numerics;
|
||||
using Dalamud.Interface.Utility;
|
||||
using ImGuiNET;
|
||||
using System.Collections;
|
||||
|
||||
namespace ChatTwo.Util;
|
||||
|
||||
// Modified from: https://github.com/UnknownX7/Hypostasis/blob/master/ImGui/ExcelSheet.cs
|
||||
public static class SearchSelector
|
||||
{
|
||||
private static string[]? FilteredSearchSheet;
|
||||
|
||||
private static string SheetSearchText = null!;
|
||||
private static string PrevSearchId = null!;
|
||||
private static Type PrevSearchType = null!;
|
||||
|
||||
|
||||
public record SelectorOptions
|
||||
{
|
||||
public Func<string, string> FormatRow { get; init; } = row => row.ToString();
|
||||
public Func<string, string, bool>? SearchPredicate { get; init; } = null;
|
||||
public Func<string, bool, bool>? DrawSelectable { get; init; } = null;
|
||||
public IEnumerable<string> FilteredSheet { get; init; } = [];
|
||||
public Vector2? Size { get; init; } = null;
|
||||
}
|
||||
|
||||
public record SelectorPopupOptions: SelectorOptions
|
||||
{
|
||||
public ImGuiPopupFlags PopupFlags { get; init; } = ImGuiPopupFlags.None;
|
||||
public bool CloseOnSelection { get; init; } = false;
|
||||
public Func<string, bool> IsSelected { get; init; } = _ => false;
|
||||
}
|
||||
|
||||
private static void SearchInput(string id, IEnumerable<string> filteredSheet, Func<string, string, bool> searchPredicate)
|
||||
{
|
||||
if (ImGui.IsWindowAppearing() && ImGui.IsWindowFocused() && !ImGui.IsAnyItemActive())
|
||||
{
|
||||
if (id != PrevSearchId)
|
||||
{
|
||||
if (typeof(string) != PrevSearchType)
|
||||
{
|
||||
SheetSearchText = string.Empty;
|
||||
PrevSearchType = typeof(string);
|
||||
}
|
||||
|
||||
FilteredSearchSheet = null;
|
||||
PrevSearchId = id;
|
||||
}
|
||||
|
||||
ImGui.SetKeyboardFocusHere(0);
|
||||
}
|
||||
|
||||
if (ImGui.InputTextWithHint("##ExcelSheetSearch", "Search", ref SheetSearchText, 128, ImGuiInputTextFlags.AutoSelectAll))
|
||||
FilteredSearchSheet = null;
|
||||
|
||||
FilteredSearchSheet ??= filteredSheet.Where(s => searchPredicate(s, SheetSearchText)).ToArray();
|
||||
}
|
||||
|
||||
public static bool SelectorPopup(string id, out string selected, SelectorPopupOptions? options = null, bool close = false)
|
||||
{
|
||||
|
||||
options ??= new SelectorPopupOptions();
|
||||
var sheet = options.FilteredSheet;
|
||||
selected = string.Empty;
|
||||
|
||||
if (close)
|
||||
return false;
|
||||
|
||||
ImGui.SetNextWindowSize(options.Size ?? new Vector2(0, 250 * ImGuiHelpers.GlobalScale));
|
||||
if (!ImGui.BeginPopupContextItem(id, options.PopupFlags))
|
||||
return false;
|
||||
|
||||
SearchInput(id, sheet, options.SearchPredicate ?? ((row, s) => options.FormatRow(row).Contains(s, StringComparison.CurrentCultureIgnoreCase)));
|
||||
|
||||
ImGui.BeginChild("SearchList", Vector2.Zero, true);
|
||||
|
||||
var ret = false;
|
||||
var drawSelectable = options.DrawSelectable ?? ((row, selected) => ImGui.Selectable(options.FormatRow(row), selected));
|
||||
using (var clipper = new ListClipper(FilteredSearchSheet!.Length))
|
||||
{
|
||||
foreach (var i in clipper.Rows)
|
||||
{
|
||||
var searched = FilteredSearchSheet[i];
|
||||
ImGui.PushID(id);
|
||||
if (!drawSelectable(searched, options.IsSelected(searched))) continue;
|
||||
selected = searched;
|
||||
ret = true;
|
||||
ImGui.PopID();
|
||||
}
|
||||
}
|
||||
|
||||
// ImGui issue #273849, children keep popups from closing automatically
|
||||
if (ret && options.CloseOnSelection)
|
||||
ImGui.CloseCurrentPopup();
|
||||
|
||||
ImGui.EndChild();
|
||||
ImGui.EndPopup();
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe class ListClipper : IEnumerable<(int, int)>, IDisposable
|
||||
{
|
||||
private ImGuiListClipperPtr Clipper;
|
||||
private readonly int CurrentRows;
|
||||
private readonly int CurrentColumns;
|
||||
private readonly bool TwoDimensional;
|
||||
private readonly int ItemRemainder;
|
||||
|
||||
public int FirstRow { get; private set; } = -1;
|
||||
public int CurrentRow { get; private set; }
|
||||
public int DisplayEnd => Clipper.DisplayEnd;
|
||||
|
||||
public IEnumerable<int> Rows
|
||||
{
|
||||
get
|
||||
{
|
||||
while (Clipper.Step()) // Supposedly this calls End()
|
||||
{
|
||||
if (Clipper.ItemsHeight > 0 && FirstRow < 0)
|
||||
FirstRow = (int)(ImGui.GetScrollY() / Clipper.ItemsHeight);
|
||||
|
||||
for (var i = Clipper.DisplayStart; i < Clipper.DisplayEnd; i++)
|
||||
{
|
||||
CurrentRow = i;
|
||||
yield return TwoDimensional ? i : i * CurrentColumns;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<int> Columns
|
||||
{
|
||||
get
|
||||
{
|
||||
var cols = (ItemRemainder == 0 || CurrentRows != DisplayEnd || CurrentRow != DisplayEnd - 1) ? CurrentColumns : ItemRemainder;
|
||||
for (var j = 0; j < cols; j++)
|
||||
yield return j;
|
||||
}
|
||||
}
|
||||
|
||||
public ListClipper(int items, int cols = 1, bool twoD = false, float itemHeight = 0)
|
||||
{
|
||||
TwoDimensional = twoD;
|
||||
CurrentColumns = cols;
|
||||
CurrentRows = TwoDimensional ? items : (int)MathF.Ceiling((float)items / CurrentColumns);
|
||||
ItemRemainder = !TwoDimensional ? items % CurrentColumns : 0;
|
||||
Clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper());
|
||||
Clipper.Begin(CurrentRows, itemHeight);
|
||||
}
|
||||
|
||||
public IEnumerator<(int, int)> GetEnumerator() => (from i in Rows from j in Columns select (i, j)).GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Clipper.Destroy(); // This also calls End() but I'm calling it anyway just in case
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user