Files
HellionChat/HellionChat/Util/ImGuiUtil.cs
T
JonKazama-Hellion 3c33acf6d7 fix(util): pin operator precedence in DrawArrows IconButton id
`id + 1.ToString()` resolves as `id.ToString() + "1"`, producing "01"
instead of "1" for the ArrowRight button. The single live caller
(DbViewer page navigation) still produced unique IDs by accident, but
the semantics were wrong. Explicit parentheses fix it.
2026-05-13 08:08:52 +02:00

847 lines
28 KiB
C#
Executable File

using System.Buffers;
using System.Numerics;
using System.Text;
using Dalamud.Bindings.ImGui;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Interface;
using Dalamud.Interface.Components;
using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.ImGuiFontChooserDialog;
using Dalamud.Interface.Style;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using HellionChat.Code;
using HellionChat.GameFunctions.Types;
using HellionChat.Resources;
namespace HellionChat.Util;
internal static class ImGuiUtil
{
private static Plugin Plugin = null!;
public static void Initialize(Plugin plugin)
{
Plugin = plugin;
}
private static readonly ImGuiMouseButton[] Buttons =
[
ImGuiMouseButton.Left,
ImGuiMouseButton.Middle,
ImGuiMouseButton.Right,
];
private static Payload? Hovered;
private static Payload? LastLink;
private static readonly List<(Vector2, Vector2)> PayloadBounds = [];
internal static void PostPayload(Chunk chunk, PayloadHandler? handler)
{
var payload = chunk.Link;
if (payload != null && ImGui.IsItemHovered())
{
Hovered = payload;
ImGui.SetMouseCursor(ImGuiMouseCursor.Hand);
handler?.Hover(payload);
}
else if (!ReferenceEquals(Hovered, payload))
{
Hovered = null;
}
if (handler == null)
return;
foreach (var button in Buttons)
if (ImGui.IsItemClicked(button))
handler.Click(chunk, payload, button);
}
// Ceiling on the byte buffer for a single rendered line. UTF-8 takes at
// most 4 bytes per char; ImGui's internal ImString limit is well below
// this and FFXIV's chat lines top out around a few hundred chars in
// practice. The cap prevents an unbounded ArrayPool rent if a caller
// ever feeds in a degenerate input.
private const int MaxLineByteCount = 16 * 1024;
internal static void WrapText(
string csText,
Chunk chunk,
PayloadHandler? handler,
Vector4 defaultText,
float lineWidth
)
{
if (csText.Length == 0)
return;
foreach (var part in csText.Split(["\r\n", "\r", "\n"], StringSplitOptions.None))
{
if (part.Length == 0)
{
ImGui.TextUnformatted("");
continue;
}
// Allocate against the encoder's own MaxByteCount so the buffer
// we hand to ImGui is sized by us. The actual byte count
// returned by GetBytes is then validated against that ceiling
// before any pointer arithmetic touches it; CodeQL recognises
// that comparison as a sanitiser for the
// cs/unvalidated-local-pointer-arithmetic taint flow.
var maxBytes = Encoding.UTF8.GetMaxByteCount(part.Length);
if (maxBytes <= 0 || maxBytes > MaxLineByteCount)
{
ImGui.TextUnformatted("");
continue;
}
var buffer = ArrayPool<byte>.Shared.Rent(maxBytes);
try
{
var written = Encoding.UTF8.GetBytes(part, 0, part.Length, buffer, 0);
if (written <= 0 || written > maxBytes)
{
ImGui.TextUnformatted("");
continue;
}
WrapEncodedLine(buffer.AsSpan(0, written), chunk, handler, defaultText, lineWidth);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
}
private static unsafe void WrapEncodedLine(
ReadOnlySpan<byte> bytes,
Chunk chunk,
PayloadHandler? handler,
Vector4 defaultText,
float lineWidth
)
{
var byteCount = bytes.Length;
if (byteCount == 0)
{
ImGui.TextUnformatted("");
return;
}
fixed (byte* basePtr = bytes)
{
var widthLeft = ImGui.GetContentRegionAvail().X;
var endPrev = CalcWordWrap(basePtr, 0, byteCount, widthLeft);
if (endPrev < 0)
return;
var firstSpace = FindFirstSpace(bytes, 0, byteCount);
var properBreak = firstSpace <= endPrev;
if (properBreak)
{
DrawText(basePtr, 0, endPrev, chunk, handler, defaultText);
}
else if (lineWidth == 0f)
{
ImGui.TextUnformatted("");
}
else
{
// Check whether the next chunk would wrap at or past the
// first space. If yes, force a line break.
var wrapPos = CalcWordWrap(basePtr, 0, firstSpace, lineWidth);
if (wrapPos >= firstSpace)
ImGui.TextUnformatted("");
}
widthLeft = ImGui.GetContentRegionAvail().X;
var lineStart = 0;
while (endPrev < byteCount)
{
if (properBreak)
lineStart = endPrev;
// Skip a leading space at the start of a wrapped line.
if (lineStart < byteCount && bytes[lineStart] == (byte)' ')
lineStart++;
var newEnd = CalcWordWrap(basePtr, lineStart, byteCount, widthLeft);
if (properBreak && newEnd == endPrev)
break;
if (newEnd < 0)
{
ImGui.TextUnformatted("");
ImGui.TextUnformatted("");
break;
}
endPrev = newEnd;
DrawText(basePtr, lineStart, endPrev, chunk, handler, defaultText);
if (!properBreak)
{
properBreak = true;
widthLeft = ImGui.GetContentRegionAvail().X;
}
}
}
}
private static unsafe int CalcWordWrap(byte* basePtr, int start, int end, float width)
{
var result = ImGuiNative.CalcWordWrapPositionA(
ImGui.GetFont().Handle,
ImGuiHelpers.GlobalScale,
basePtr + start,
basePtr + end,
width
);
if (result == null)
return -1;
return (int)(result - basePtr);
}
private static unsafe void DrawText(
byte* basePtr,
int start,
int end,
Chunk chunk,
PayloadHandler? handler,
Vector4 defaultText
)
{
var oldPos = ImGui.GetCursorScreenPos();
ImGuiNative.TextUnformatted(basePtr + start, basePtr + end);
PostPayload(chunk, handler);
if (!ReferenceEquals(LastLink, chunk.Link))
PayloadBounds.Clear();
LastLink = chunk.Link;
if (Hovered != null && ReferenceEquals(Hovered, chunk.Link))
{
defaultText.W = 0.25f;
var actualCol = ColourUtil.Vector4ToAbgr(defaultText);
ImGui
.GetWindowDrawList()
.AddRectFilled(oldPos, oldPos + ImGui.GetItemRectSize(), actualCol);
foreach (var (boundsStart, boundsSize) in PayloadBounds)
ImGui
.GetWindowDrawList()
.AddRectFilled(boundsStart, boundsStart + boundsSize, actualCol);
PayloadBounds.Clear();
}
if (Hovered == null && chunk.Link != null)
PayloadBounds.Add((oldPos, ImGui.GetItemRectSize()));
}
private static int FindFirstSpace(ReadOnlySpan<byte> bytes, int start, int end)
{
for (var i = start; i < end; i++)
if (char.IsWhiteSpace((char)bytes[i]))
return i;
return end;
}
// ---------------------------------------------------------------
// Inspired by ChatTwo upstream f35b7d3 (Infiziert90, 2026-05-12).
// Upstream dropped the width parameter (no callers there); we keep
// it because two ChatLogWindow header buttons size themselves to
// match the ChannelIcon button's frame. The actual bug is the
// manual size = width - 2 * CellPadding.X subtraction: CellPadding
// scales with HUD scale, the raw int does not, so the button
// shrank under high HUD scales. ImGui.Button already handles its
// own frame padding internally — pass the measured width straight
// through.
// ---------------------------------------------------------------
internal static bool IconButton(
FontAwesomeIcon icon,
string? id = null,
string? tooltip = null,
int width = 0
)
{
var label = icon.ToIconString();
if (id != null)
label += $"##{id}";
bool ret;
using (Plugin.FontManager.FontAwesome.Push())
{
var size = width > 0 ? new Vector2(width, 0f) : Vector2.Zero;
ret = ImGui.Button(label, size);
}
if (
!string.IsNullOrEmpty(tooltip)
&& ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)
)
Tooltip(tooltip);
return ret;
}
internal static bool OptionCheckbox(ref bool value, string label, string? description = null)
{
var ret = ImGui.Checkbox(label, ref value);
if (!string.IsNullOrEmpty(description))
HelpText(description);
return ret;
}
internal static void HelpText(string text)
{
using (ImRaii.TextWrapPos(0.0f))
using (ImRaii.PushColor(ImGuiCol.Text, ImGui.GetStyle().Colors[(int)ImGuiCol.TextDisabled]))
ImGui.TextUnformatted(text);
}
// Hellion Chat — compact help affordance: a dimmed "(?)" glyph rendered
// on the same line as the previous item, with the long-form description
// tucked into a hover tooltip. Lets us keep the settings panes scannable
// instead of stacking a wall of HelpText paragraphs under every option.
internal static void HelpMarker(string description)
{
ImGui.SameLine();
using (ImRaii.PushColor(ImGuiCol.Text, ImGui.GetStyle().Colors[(int)ImGuiCol.TextDisabled]))
ImGui.TextUnformatted("(?)");
// AllowWhenDisabled — ohne das Flag liefert IsItemHovered bei
// ausgegrauten Settings false, der User könnte nicht mehr lesen
// warum eine Option nicht aktiv ist. Genau dann braucht er den
// Hover-Tooltip aber am dringendsten.
if (!ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
return;
using var tooltip = ImRaii.Tooltip();
using (ImRaii.TextWrapPos(35.0f * ImGui.GetFontSize()))
ImGui.TextUnformatted(description);
}
internal static void WarningText(string text, bool wrap = true)
{
var style = StyleModel.GetConfiguredStyle() ?? StyleModel.GetFromCurrent();
var dalamudOrange = style.BuiltInColors?.DalamudOrange;
using (ImRaii.TextWrapPos(wrap ? 0.0f : ImGui.GetFontSize() * 35.0f))
using (
ImRaii.PushColor(ImGuiCol.Text, dalamudOrange ?? Vector4.Zero, dalamudOrange != null)
)
ImGui.TextUnformatted(text);
}
internal static ImRaii.ComboDisposable BeginComboVertical(
string label,
string previewValue,
ImGuiComboFlags flags = ImGuiComboFlags.None
)
{
ImGui.TextUnformatted(label);
ImGui.SetNextItemWidth(-1);
return ImRaii.Combo($"##{label}", previewValue, flags);
}
internal static bool DragFloatVertical(
string label,
ref float value,
float vSpeed = 1.0f,
float vMin = float.MinValue,
float vMax = float.MaxValue,
string? format = null,
ImGuiSliderFlags flags = ImGuiSliderFlags.None
)
{
ImGui.TextUnformatted(label);
ImGui.SetNextItemWidth(-1);
return ImGui.DragFloat($"##{label}", ref value, vSpeed, vMin, vMax, format, flags);
}
internal static bool DragFloatVertical(
string label,
string description,
ref float value,
float vSpeed = 1.0f,
float vMin = float.MinValue,
float vMax = float.MaxValue,
string? format = null,
ImGuiSliderFlags flags = ImGuiSliderFlags.None
)
{
ImGui.TextUnformatted(label);
ImGui.SetNextItemWidth(-1);
var r = ImGui.DragFloat($"##{label}", ref value, vSpeed, vMin, vMax, format, flags);
HelpText(description);
return r;
}
internal static bool InputIntVertical(
string label,
string description,
ref int value,
int step = 1,
int stepFast = 100,
ImGuiInputTextFlags flags = ImGuiInputTextFlags.None
)
{
ImGui.TextUnformatted(label);
ImGui.SetNextItemWidth(-1);
var r = ImGui.InputInt($"##{label}", ref value, step, stepFast, flags: flags);
HelpText(description);
return r;
}
internal static void Tooltip(string tooltip)
{
using (ImRaii.Tooltip())
using (ImRaii.TextWrapPos(ImGui.GetFontSize() * 35.0f))
ImGui.TextUnformatted(tooltip);
}
public static SingleFontChooserDialog? FontChooser(
string label,
SingleFontSpec font,
bool checkbox,
ref bool checkboxValue,
Predicate<IFontFamilyId>? exclusion = null,
string? preview = null
)
{
using var id = ImRaii.PushId(label);
ImGui.TextUnformatted(label);
if (checkbox)
{
ImGui.Checkbox("##enabled", ref checkboxValue);
ImGui.SameLine();
}
var fontFamily = font.FontId.Family.EnglishName;
var fontStyle = font.FontId.EnglishName;
fontStyle = fontStyle.Equals(fontFamily) ? "" : $" - {fontStyle}";
var buttonText = $"{fontFamily}{fontStyle} ({font.SizePt}pt)";
if (!ImGui.Button($"{buttonText}##{label}"))
return null;
var chooser = SingleFontChooserDialog.CreateAuto((UiBuilder)Plugin.Interface.UiBuilder);
chooser.SelectedFont = font;
if (exclusion is not null)
chooser.FontFamilyExcludeFilter = exclusion;
if (preview is not null)
chooser.PreviewText = preview;
return chooser;
}
public static void FontSizeCombo(string label, ref float currentSize)
{
ImGui.TextUnformatted(label);
ImGui.SetNextItemWidth(-1);
using var combo = ImRaii.Combo($"##{label}", $"{currentSize:###.##}pt");
if (!combo.Success)
return;
foreach (var size in FontManager.AxisFontSizeList)
if (ImGui.Selectable($"{size:###.##}pt", currentSize.Equals(size)))
currentSize = size;
}
public static bool Button(string id, FontAwesomeIcon icon, bool disabled)
{
using (ImRaii.Disabled(disabled))
return ImGuiComponents.IconButton(id, icon);
}
internal static bool CtrlShiftButton(string label, string tooltip = "")
{
var ctrlShiftHeld = ImGui.GetIO() is { KeyCtrl: true, KeyShift: true };
bool ret;
using (ImRaii.Disabled(!ctrlShiftHeld))
ret = ImGui.Button(label) && ctrlShiftHeld;
if (tooltip.Length != 0 && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
Tooltip(tooltip);
return ret;
}
internal static void KeybindInput(string id, ref ConfigKeyBind? keybind)
{
var idUint = ImGui.GetID(id);
using var pushedId = ImRaii.PushId(id);
if (ImGui.GetStateStorage().GetBool(idUint))
{
var io = ImGui.GetIO();
var currentMods = ModifierFlag.None;
var modString = "";
if (io.KeyCtrl)
{
currentMods |= ModifierFlag.Ctrl;
modString += Language.Keybind_Modifier_Ctrl + " + ";
}
if (io.KeyShift)
{
currentMods |= ModifierFlag.Shift;
modString += Language.Keybind_Modifier_Shift + " + ";
}
if (io.KeyAlt)
{
currentMods |= ModifierFlag.Alt;
modString += Language.Keybind_Modifier_Alt + " + ";
}
var text = $"{modString}... ({Language.Keybind_EscToClear})";
using (ImRaii.PushColor(ImGuiCol.TextSelectedBg, Vector4.Zero))
{
ImGui.SetKeyboardFocusHere();
ImGui.InputText(id + "##keybind", ref text, 0, ImGuiInputTextFlags.ReadOnly);
}
if (ImGui.IsKeyPressed(ImGuiKey.Escape))
{
keybind = null;
ImGui.GetStateStorage().SetBool(idUint, false);
return;
}
foreach (var vk in Enum.GetValues<VirtualKey>())
{
if (
vk
is VirtualKey.NO_KEY
or VirtualKey.CONTROL
or VirtualKey.LCONTROL
or VirtualKey.RCONTROL
or VirtualKey.SHIFT
or VirtualKey.LSHIFT
or VirtualKey.RSHIFT
or VirtualKey.MENU
or VirtualKey.LMENU
or VirtualKey.RMENU
)
continue;
if (!vk.TryToImGui(out var imKey) || !ImGui.IsKeyPressed(imKey))
continue;
keybind = new ConfigKeyBind { Modifier = currentMods, Key = vk };
ImGui.GetStateStorage().SetBool(idUint, false);
return;
}
}
else
{
var text = $"({Language.Keybind_None})";
if (keybind != null)
text = keybind.ToString();
if (ImGui.Button(text, new Vector2(-1, 0)))
ImGui.GetStateStorage().SetBool(idUint, true);
}
}
public static void DrawArrows(
ref int selected,
int min,
int max,
float spacing,
int id = 0,
string? tooltipLeft = null,
string? tooltipRight = null
)
{
// Prevents changing values from triggering EndDisable
var isMin = selected == min;
var isMax = selected == max;
ImGui.SameLine(0, spacing);
using (ImRaii.Disabled(isMin))
{
if (IconButton(FontAwesomeIcon.ArrowLeft, id.ToString()))
selected--;
}
if (tooltipLeft != null && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
ImGui.SetTooltip(tooltipLeft);
ImGui.SameLine(0, spacing);
using (ImRaii.Disabled(isMax))
{
// Parentheses pin the operator precedence: without them this resolves as
// id.ToString() + "1" (e.g. "01" instead of "1").
if (IconButton(FontAwesomeIcon.ArrowRight, (id + 1).ToString()))
selected++;
}
if (tooltipRight != null && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
ImGui.SetTooltip(tooltipRight);
}
public static void WrappedTextWithColor(Vector4 color, string text)
{
using (ImRaii.PushColor(ImGuiCol.Text, color))
ImGui.TextWrapped(text);
}
public static void CenterText(string text, float indent = 0.0f)
{
indent *= ImGuiHelpers.GlobalScale;
ImGui.SameLine(
((ImGui.GetContentRegionAvail().X - ImGui.CalcTextSize(text).X) * 0.5f) + indent
);
ImGui.TextUnformatted(text);
}
internal static bool TryToImGui(this VirtualKey key, out ImGuiKey result)
{
result = key switch
{
VirtualKey.NO_KEY => ImGuiKey.None,
VirtualKey.BACK => ImGuiKey.Backspace,
VirtualKey.TAB => ImGuiKey.Tab,
VirtualKey.RETURN => ImGuiKey.Enter,
VirtualKey.SHIFT => ImGuiKey.ModShift,
VirtualKey.CONTROL => ImGuiKey.ModCtrl,
VirtualKey.MENU => ImGuiKey.ModAlt,
VirtualKey.PAUSE => ImGuiKey.Pause,
VirtualKey.CAPITAL => ImGuiKey.CapsLock,
VirtualKey.ESCAPE => ImGuiKey.Escape,
VirtualKey.SPACE => ImGuiKey.Space,
VirtualKey.PRIOR => ImGuiKey.PageUp,
VirtualKey.NEXT => ImGuiKey.PageDown,
VirtualKey.END => ImGuiKey.End,
VirtualKey.HOME => ImGuiKey.Home,
VirtualKey.LEFT => ImGuiKey.LeftArrow,
VirtualKey.UP => ImGuiKey.UpArrow,
VirtualKey.RIGHT => ImGuiKey.RightArrow,
VirtualKey.DOWN => ImGuiKey.DownArrow,
VirtualKey.SNAPSHOT => ImGuiKey.PrintScreen,
VirtualKey.INSERT => ImGuiKey.Insert,
VirtualKey.DELETE => ImGuiKey.Delete,
VirtualKey.KEY_0 => ImGuiKey.Key0,
VirtualKey.KEY_1 => ImGuiKey.Key1,
VirtualKey.KEY_2 => ImGuiKey.Key2,
VirtualKey.KEY_3 => ImGuiKey.Key3,
VirtualKey.KEY_4 => ImGuiKey.Key4,
VirtualKey.KEY_5 => ImGuiKey.Key5,
VirtualKey.KEY_6 => ImGuiKey.Key6,
VirtualKey.KEY_7 => ImGuiKey.Key7,
VirtualKey.KEY_8 => ImGuiKey.Key8,
VirtualKey.KEY_9 => ImGuiKey.Key9,
VirtualKey.A => ImGuiKey.A,
VirtualKey.B => ImGuiKey.B,
VirtualKey.C => ImGuiKey.C,
VirtualKey.D => ImGuiKey.D,
VirtualKey.E => ImGuiKey.E,
VirtualKey.F => ImGuiKey.F,
VirtualKey.G => ImGuiKey.G,
VirtualKey.H => ImGuiKey.H,
VirtualKey.I => ImGuiKey.I,
VirtualKey.J => ImGuiKey.J,
VirtualKey.K => ImGuiKey.K,
VirtualKey.L => ImGuiKey.L,
VirtualKey.M => ImGuiKey.M,
VirtualKey.N => ImGuiKey.N,
VirtualKey.O => ImGuiKey.O,
VirtualKey.P => ImGuiKey.P,
VirtualKey.Q => ImGuiKey.Q,
VirtualKey.R => ImGuiKey.R,
VirtualKey.S => ImGuiKey.S,
VirtualKey.T => ImGuiKey.T,
VirtualKey.U => ImGuiKey.U,
VirtualKey.V => ImGuiKey.V,
VirtualKey.W => ImGuiKey.W,
VirtualKey.X => ImGuiKey.X,
VirtualKey.Y => ImGuiKey.Y,
VirtualKey.Z => ImGuiKey.Z,
VirtualKey.LWIN => ImGuiKey.LeftSuper,
VirtualKey.RWIN => ImGuiKey.RightSuper,
VirtualKey.NUMPAD0 => ImGuiKey.Keypad0,
VirtualKey.NUMPAD1 => ImGuiKey.Keypad1,
VirtualKey.NUMPAD2 => ImGuiKey.Keypad2,
VirtualKey.NUMPAD3 => ImGuiKey.Keypad3,
VirtualKey.NUMPAD4 => ImGuiKey.Keypad4,
VirtualKey.NUMPAD5 => ImGuiKey.Keypad5,
VirtualKey.NUMPAD6 => ImGuiKey.Keypad6,
VirtualKey.NUMPAD7 => ImGuiKey.Keypad7,
VirtualKey.NUMPAD8 => ImGuiKey.Keypad8,
VirtualKey.NUMPAD9 => ImGuiKey.Keypad9,
VirtualKey.MULTIPLY => ImGuiKey.KeypadMultiply,
VirtualKey.ADD => ImGuiKey.KeypadAdd,
VirtualKey.SUBTRACT => ImGuiKey.KeypadSubtract,
VirtualKey.DECIMAL => ImGuiKey.KeypadDecimal,
VirtualKey.DIVIDE => ImGuiKey.KeypadDivide,
VirtualKey.F1 => ImGuiKey.F1,
VirtualKey.F2 => ImGuiKey.F2,
VirtualKey.F3 => ImGuiKey.F3,
VirtualKey.F4 => ImGuiKey.F4,
VirtualKey.F5 => ImGuiKey.F5,
VirtualKey.F6 => ImGuiKey.F6,
VirtualKey.F7 => ImGuiKey.F7,
VirtualKey.F8 => ImGuiKey.F8,
VirtualKey.F9 => ImGuiKey.F9,
VirtualKey.F10 => ImGuiKey.F10,
VirtualKey.F11 => ImGuiKey.F11,
VirtualKey.F12 => ImGuiKey.F12,
VirtualKey.NUMLOCK => ImGuiKey.NumLock,
VirtualKey.SCROLL => ImGuiKey.ScrollLock,
VirtualKey.OEM_NEC_EQUAL => ImGuiKey.KeypadEqual,
VirtualKey.LSHIFT => ImGuiKey.LeftShift,
VirtualKey.RSHIFT => ImGuiKey.RightShift,
VirtualKey.LCONTROL => ImGuiKey.LeftCtrl,
VirtualKey.RCONTROL => ImGuiKey.RightCtrl,
VirtualKey.LMENU => ImGuiKey.LeftAlt,
VirtualKey.RMENU => ImGuiKey.RightAlt,
VirtualKey.OEM_1 => ImGuiKey.Semicolon,
VirtualKey.OEM_PLUS => ImGuiKey.Equal,
VirtualKey.OEM_COMMA => ImGuiKey.Comma,
VirtualKey.OEM_MINUS => ImGuiKey.Minus,
VirtualKey.OEM_PERIOD => ImGuiKey.Period,
VirtualKey.OEM_2 => ImGuiKey.Slash,
VirtualKey.OEM_3 => ImGuiKey.GraveAccent,
VirtualKey.OEM_4 => ImGuiKey.LeftBracket,
VirtualKey.OEM_5 => ImGuiKey.Backslash,
VirtualKey.OEM_6 => ImGuiKey.RightBracket,
VirtualKey.OEM_7 => ImGuiKey.Apostrophe,
_ => 0,
};
return result != 0 || key == VirtualKey.NO_KEY;
}
public static void ChannelSelector(
string headerText,
Dictionary<ChatType, (ChatSource Source, ChatSource Target)> chatCodes
)
{
var spacing = 3.0f * ImGuiHelpers.GlobalScale;
using var channelNode = ImRaii.TreeNode(headerText);
if (!channelNode.Success)
return;
foreach (var (header, types) in ChatTypeExt.SortOrder)
{
using var pushedId = ImRaii.PushId(header);
if (ImGuiComponents.IconButton(FontAwesomeIcon.Check))
{
foreach (var type in types)
chatCodes.TryAdd(type, (ChatSourceExt.All, ChatSourceExt.All));
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip(Language.ChannelSelector_Select);
ImGui.SameLine(0, spacing);
if (ImGuiComponents.IconButton(FontAwesomeIcon.Times))
{
foreach (var type in types)
chatCodes.Remove(type);
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip(Language.ChannelSelector_Unselect);
ImGui.SameLine(0, spacing);
using var headerNode = ImRaii.TreeNode(header);
if (!headerNode.Success)
continue;
foreach (var type in types)
{
if (type.IsGm())
continue;
var enabled = chatCodes.ContainsKey(type);
if (ImGui.Checkbox($"##{type.Name()}", ref enabled))
{
if (enabled)
chatCodes[type] = (ChatSourceExt.All, ChatSourceExt.All);
else
chatCodes.Remove(type);
}
ImGui.SameLine();
if (!type.HasSource())
{
ImGui.TextUnformatted(type.Name());
continue;
}
using var typeNode = ImRaii.TreeNode($"{type.Name()}");
if (!typeNode.Success)
continue;
ImGui.Text(Language.ImGuiUtil_ChannelSelector_Source);
ImGui.SameLine(400.0f * ImGuiHelpers.GlobalScale);
ImGui.Text(Language.ImGuiUtil_ChannelSelector_Target);
chatCodes.TryGetValue(type, out var sourcesEnum);
var sources = (uint)sourcesEnum.Source;
var targets = (uint)sourcesEnum.Target;
foreach (var kind in Enum.GetValues<ChatSource>().Where(s => s != ChatSource.None))
{
if (ImGui.CheckboxFlags($"{kind.Name()}##source", ref sources, (uint)kind))
chatCodes[type] = ((ChatSource)sources, sourcesEnum.Target);
ImGui.SameLine(400.0f * ImGuiHelpers.GlobalScale);
if (ImGui.CheckboxFlags($"{kind.Name()}##target", ref targets, (uint)kind))
chatCodes[type] = (sourcesEnum.Source, (ChatSource)targets);
}
}
}
}
public static void ExtraChatSelector(
string headerText,
ref bool all,
HashSet<Guid> extraChatChannels
)
{
if (Plugin.ExtraChat.ChannelNames.Count <= 0)
return;
using var extraTree = ImRaii.TreeNode(headerText);
if (!extraTree.Success)
return;
ImGui.Checkbox(Language.Options_Tabs_ExtraChatAll, ref all);
ImGui.Separator();
using var _ = ImRaii.Disabled(all);
foreach (var (id, name) in Plugin.ExtraChat.ChannelNames)
{
var enabled = extraChatChannels.Contains(id);
if (!ImGui.Checkbox($"{name}##ec-{id}", ref enabled))
continue;
if (enabled)
extraChatChannels.Add(id);
else
extraChatChannels.Remove(id);
}
}
}