feat(ui): add optional regex filter per tab
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dalamud;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Configuration;
|
||||
using Dalamud.Game.ClientState.Keys;
|
||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
using Dalamud.Interface.FontIdentifier;
|
||||
using HellionChat._Helpers;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.GameFunctions.Types;
|
||||
using HellionChat.Resources;
|
||||
@@ -459,6 +462,10 @@ public class Tab
|
||||
public bool EnableNotificationSound;
|
||||
public uint NotificationSoundId = 1;
|
||||
|
||||
// UI-8: optional regex applied on top of the channel filter. Null or
|
||||
// empty means no filter, today's behaviour unchanged.
|
||||
public string? MessageRegex;
|
||||
|
||||
[NonSerialized]
|
||||
public uint Unread;
|
||||
|
||||
@@ -512,11 +519,32 @@ public class Tab
|
||||
[NonSerialized]
|
||||
internal float _cardHoverAlpha;
|
||||
|
||||
// UI-8: compiled-regex cache. Recompiled only when MessageRegex changes;
|
||||
// _compiledRegexKey is the validation key (mirrors the _cachedTint* keys).
|
||||
[NonSerialized]
|
||||
private Regex? _compiledRegex;
|
||||
|
||||
[NonSerialized]
|
||||
private string? _compiledRegexKey;
|
||||
|
||||
public bool Matches(Message message)
|
||||
{
|
||||
if (!message.Matches(SelectedChannels, ExtraChatAll, ExtraChatChannels))
|
||||
return false;
|
||||
|
||||
// UI-8: optional regex content filter, AND-constraint after the channel
|
||||
// check. An empty pattern or an invalid one (cached regex is null) lets
|
||||
// every message through, so existing tabs behave unchanged.
|
||||
if (!string.IsNullOrEmpty(MessageRegex))
|
||||
{
|
||||
var text = string.Join(
|
||||
"",
|
||||
message.Content.OfType<TextChunk>().Select(c => c.Content)
|
||||
);
|
||||
if (!RegexRouteMatcher.IsMatch(GetCompiledRegex(), text))
|
||||
return false;
|
||||
}
|
||||
|
||||
// Temp tabs are bound to a single conversation partner — other tells
|
||||
// matching the channel filter must not land here.
|
||||
if (IsTempTab && TellTarget?.IsSet() == true)
|
||||
@@ -525,6 +553,20 @@ public class Tab
|
||||
return true;
|
||||
}
|
||||
|
||||
// UI-8: lazily compiles MessageRegex and caches the result. Recompiles only
|
||||
// when the pattern string changed (validation-key pattern, same idea as the
|
||||
// _cachedTint* caches). An invalid pattern caches a null regex.
|
||||
private Regex? GetCompiledRegex()
|
||||
{
|
||||
if (_compiledRegexKey != MessageRegex)
|
||||
{
|
||||
(_compiledRegex, _) = RegexRouteMatcher.Compile(MessageRegex);
|
||||
_compiledRegexKey = MessageRegex;
|
||||
}
|
||||
|
||||
return _compiledRegex;
|
||||
}
|
||||
|
||||
public void AddMessage(Message message, bool unread = true)
|
||||
{
|
||||
Messages.AddPrune(message, MessageManager.MessageDisplayLimit);
|
||||
@@ -579,6 +621,7 @@ public class Tab
|
||||
TellTarget = TellTarget.Clone(),
|
||||
EnableNotificationSound = EnableNotificationSound,
|
||||
NotificationSoundId = NotificationSoundId,
|
||||
MessageRegex = MessageRegex,
|
||||
IsGreeted = IsGreeted,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using HellionChat._Helpers;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Util;
|
||||
@@ -238,6 +240,25 @@ internal sealed class Tabs : ISettingsTab
|
||||
}
|
||||
}
|
||||
}
|
||||
// UI-8: optional regex filter for this tab.
|
||||
var regexBuffer = tab.MessageRegex ?? string.Empty;
|
||||
if (ImGui.InputText($"{HellionStrings.Settings_Tabs_MessageRegex_Name}##regex-{i}", ref regexBuffer, 256))
|
||||
tab.MessageRegex = string.IsNullOrEmpty(regexBuffer) ? null : regexBuffer;
|
||||
ImGuiUtil.HelpMarker(HellionStrings.Settings_Tabs_MessageRegex_Description);
|
||||
|
||||
// Validity feedback: the settings window is not a hot path, so a
|
||||
// per-frame Compile is fine. An invalid pattern shows a red line and
|
||||
// the tab behaves as if no regex were set.
|
||||
if (!string.IsNullOrEmpty(tab.MessageRegex))
|
||||
{
|
||||
var (_, regexError) = RegexRouteMatcher.Compile(tab.MessageRegex);
|
||||
if (regexError is not null)
|
||||
ImGui.TextColored(
|
||||
ImGuiColors.DalamudRed,
|
||||
string.Format(HellionStrings.Settings_Tabs_MessageRegex_Invalid, regexError)
|
||||
);
|
||||
}
|
||||
|
||||
ImGui.Checkbox(Language.Options_Tabs_PopOut, ref tab.PopOut);
|
||||
if (tab.PopOut)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace HellionChat._Helpers;
|
||||
|
||||
// UI-8 pure decision helper for the optional per-tab regex filter. Compiling a
|
||||
// user pattern can throw (invalid syntax) and matching a pathological pattern
|
||||
// can hang (catastrophic backtracking), so both are contained here. Kept free
|
||||
// of Dalamud types so the Build Suite can test compile/match/timeout in
|
||||
// isolation.
|
||||
// TEST-MIRROR: ../../../Hellion Build test/Ui/RegexRouteMatcherTests.cs
|
||||
public static class RegexRouteMatcher
|
||||
{
|
||||
// A match must never block the message-routing hot path. 100ms is far
|
||||
// beyond any sane pattern yet short enough that a runaway pattern fails
|
||||
// fast instead of freezing the chat.
|
||||
public static readonly TimeSpan MatchTimeout = TimeSpan.FromMilliseconds(100);
|
||||
|
||||
// Compiles a user pattern. Null/empty pattern -> (null, null): "no filter".
|
||||
// Invalid pattern -> (null, errorMessage): the tab then behaves as if no
|
||||
// regex were set (see IsMatch).
|
||||
public static (Regex? Regex, string? Error) Compile(string? pattern)
|
||||
{
|
||||
if (string.IsNullOrEmpty(pattern))
|
||||
return (null, null);
|
||||
|
||||
try
|
||||
{
|
||||
var regex = new Regex(pattern, RegexOptions.IgnoreCase, MatchTimeout);
|
||||
return (regex, null);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
return (null, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
// Runs the filter. A null regex (no filter, or an invalid pattern) returns
|
||||
// true so nothing is filtered out. A timeout on a pathological pattern is
|
||||
// treated as "no match" rather than a crash.
|
||||
public static bool IsMatch(Regex? regex, string text)
|
||||
{
|
||||
if (regex is null)
|
||||
return true;
|
||||
|
||||
try
|
||||
{
|
||||
return regex.IsMatch(text);
|
||||
}
|
||||
catch (RegexMatchTimeoutException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user