35e8d3a7fe
Plugin.cs:937 only pushed RegularFont when Config.FontsEnabled was true.
FontsAndColours.cs:50 forces FontsEnabled=false whenever UseHellionFont is
enabled (to hide the chooser UI), so the bundled-font path was silently
dead and the FFXIV Axis game-font took over. Exo 2 looked "almost right"
because it overlaps Axis on basic Latin, so the regression went unnoticed
for the entire v1.5.x series.
The fix routes RegularFont through draw whenever either FontsEnabled or
UseHellionFont is on. First-frame HITCH dropped from ~74 ms to ~20 ms
median (5-reload Linux/Wine sample 17.9-23.6 ms) as a side effect — the
v1.5.1 "too optimistic" defer-pattern hypothesis was actually a symptom
of this bug, not bad math.
Font-stack overhaul on top:
- Inter Light (Static 18pt-Light, 343 KB, SIL OFL 1.1) replaces Exo 2 as
the bundled font. Inter ships full Latin Extended-A/B, Greek polytonic
and Cyrillic Supplement coverage.
- NotoSansCjkRegular added as a third merge layer for Hangul,
Simplified-Chinese-specific Han glyphs, and CJK fallbacks the FFXIV
Japanese font does not ship.
- Two new ExtraGlyphRanges flags (LatinExtended, Greek) implemented via
AddChar pair lists in SetUpRanges.
- Settings.Apply auto-activates the matching ExtraGlyphRanges flag on
language change. Plugin.LoadAsync runs a one-shot migration that ORs
in the required flag for an already-selected language.
- ExtraGlyphRanges CollapsingHeader reachable regardless of
UseHellionFont (was hidden in the early-return branch).
- New WarningText below the language combo: FFXIV's chat engine only
fully supports EN/DE/FR/JA. Other scripts render in the HellionChat
UI but may garble in in-game chat input/send.
Localisation wave (originally a FR-only cycle):
- 24 selectable UI languages. LanguageOverride enum gains 10 new locales
plus 3 previously commented-out (Italian, Korean, Norwegian with ISO
code `nb` instead of `no`). All new values append to keep existing
user-config integer serialisation stable.
- Resource bundle split: HellionStrings.resx (24 locales, 328 keys) for
fork-added strings, Language.resx (24 locales, 456 keys) for the
ChatTwo-Crowdin-heritage. 4 post-sync Crowdin keys backfilled into
13 legacy locales with per-key AI-assisted comment marker.
- Em-dash sweep on EN source plus 18 translations. Russian and Ukrainian
keep their typographic norm.
Old HellionFont.ttf + HellionFont-OFL.txt removed; Inter-Light.ttf +
Inter-OFL.txt take their place. Configuration field UseHellionFont keeps
its name for backwards-compat. Migration v17 stays.
310 lines
10 KiB
C#
Executable File
310 lines
10 KiB
C#
Executable File
using System.Numerics;
|
|
using Dalamud.Bindings.ImGui;
|
|
using Dalamud.Interface.Utility.Raii;
|
|
using Dalamud.Interface.Windowing;
|
|
using Dalamud.Utility;
|
|
using HellionChat.Resources;
|
|
using HellionChat.Ui.SettingsTabs;
|
|
using HellionChat.Util;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace HellionChat.Ui;
|
|
|
|
internal enum SettingsView
|
|
{
|
|
Overview,
|
|
Detail,
|
|
}
|
|
|
|
public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
|
{
|
|
internal readonly Plugin Plugin;
|
|
|
|
private Configuration Mutable { get; }
|
|
private List<ISettingsTab> Tabs { get; }
|
|
private int CurrentTab;
|
|
private SettingsView View = SettingsView.Overview;
|
|
private readonly SettingsOverview Overview;
|
|
|
|
internal SettingsWindow(Plugin plugin, ILoggerFactory loggerFactory)
|
|
: base($"{Language.Settings_Title.Format(Plugin.PluginName)}###chat2-settings")
|
|
{
|
|
Flags = ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse;
|
|
|
|
SizeCondition = ImGuiCond.FirstUseEver;
|
|
SizeConstraints = new WindowSizeConstraints
|
|
{
|
|
MinimumSize = new Vector2(475, 600),
|
|
MaximumSize = new Vector2(float.MaxValue, float.MaxValue),
|
|
};
|
|
|
|
Plugin = plugin;
|
|
Mutable = new Configuration();
|
|
|
|
Overview = new SettingsOverview(this);
|
|
|
|
Tabs =
|
|
[
|
|
new General(Plugin, Mutable),
|
|
new ThemeAndLayout(Plugin, Mutable, loggerFactory.CreateLogger<ThemeAndLayout>()),
|
|
new FontsAndColours(Plugin, Mutable, loggerFactory.CreateLogger<FontsAndColours>()),
|
|
new SettingsTabs.Window(Plugin, Mutable),
|
|
new Chat(Plugin, Mutable),
|
|
new SettingsTabs.Tabs(Plugin, Mutable),
|
|
new SettingsTabs.Privacy(Plugin, Mutable),
|
|
new DataManagement(Plugin, Mutable, loggerFactory.CreateLogger<DataManagement>()),
|
|
new SettingsTabs.Integrations(Plugin, Mutable),
|
|
new Information(Mutable),
|
|
];
|
|
|
|
RespectCloseHotkey = false;
|
|
DisableWindowSounds = true;
|
|
|
|
Initialise();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
// Slash-command + OpenConfigUi tear-down moved to Plugin.TearDownCommands.
|
|
}
|
|
|
|
private void Initialise()
|
|
{
|
|
Mutable.UpdateFrom(Plugin.Config, false);
|
|
}
|
|
|
|
public override void Draw()
|
|
{
|
|
if (ImGui.IsWindowAppearing())
|
|
{
|
|
Initialise();
|
|
View = SettingsView.Overview;
|
|
}
|
|
|
|
// ESC in Detail view returns to Overview. Window focus check is
|
|
// required so ESC doesn't fire when the user targets a different window.
|
|
if (
|
|
View == SettingsView.Detail
|
|
&& ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows)
|
|
&& ImGui.IsKeyPressed(ImGuiKey.Escape)
|
|
)
|
|
{
|
|
View = SettingsView.Overview;
|
|
return;
|
|
}
|
|
|
|
if (View == SettingsView.Overview)
|
|
Overview.Draw();
|
|
else
|
|
DrawDetail();
|
|
|
|
ImGui.Separator();
|
|
DrawSaveButtons();
|
|
}
|
|
|
|
internal void OpenSection(int tabIndex)
|
|
{
|
|
CurrentTab = tabIndex;
|
|
View = SettingsView.Detail;
|
|
}
|
|
|
|
internal void OpenOverview()
|
|
{
|
|
View = SettingsView.Overview;
|
|
}
|
|
|
|
private void DrawDetail()
|
|
{
|
|
// Breadcrumb header -- accent cyan, clickable, returns to Overview.
|
|
using (ImRaii.PushColor(ImGuiCol.Text, 0xFF00BED2u))
|
|
using (ImRaii.PushColor(ImGuiCol.Button, 0u))
|
|
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, 0x33FFFFFFu))
|
|
using (ImRaii.PushColor(ImGuiCol.ButtonActive, 0x55FFFFFFu))
|
|
{
|
|
if (ImGui.SmallButton("<- Settings"))
|
|
{
|
|
View = SettingsView.Overview;
|
|
return;
|
|
}
|
|
}
|
|
ImGui.SameLine();
|
|
ImGui.TextUnformatted("·");
|
|
ImGui.SameLine();
|
|
ImGui.TextUnformatted(Tabs[CurrentTab].Name.Split("###")[0]);
|
|
|
|
ImGui.Spacing();
|
|
ImGui.Separator();
|
|
ImGui.Spacing();
|
|
|
|
// Section content fills full width. Navigation back to another
|
|
// section goes via the breadcrumb or ESC.
|
|
var style = ImGui.GetStyle();
|
|
var height =
|
|
ImGui.GetContentRegionAvail().Y
|
|
- style.FramePadding.Y * 2
|
|
- style.ItemSpacing.Y
|
|
- style.ItemInnerSpacing.Y * 2
|
|
- ImGui.CalcTextSize("A").Y;
|
|
|
|
using var child = ImRaii.Child("##chat2-settings-detail", new Vector2(-1, height));
|
|
if (child.Success)
|
|
Tabs[CurrentTab].Draw(false);
|
|
}
|
|
|
|
private void DrawSaveButtons()
|
|
{
|
|
var save = ImGui.Button(Language.Settings_Save);
|
|
|
|
ImGui.SameLine();
|
|
|
|
if (ImGui.Button(Language.Settings_SaveAndClose))
|
|
{
|
|
save = true;
|
|
IsOpen = false;
|
|
}
|
|
|
|
ImGui.SameLine();
|
|
|
|
if (ImGui.Button(Language.Settings_Discard))
|
|
IsOpen = false;
|
|
|
|
const string buttonLabel = "Anna's Ko-fi";
|
|
const string buttonLabel2 = "Infi's Ko-fi";
|
|
|
|
using (ImRaii.PushColor(ImGuiCol.Button, ColourUtil.RgbaToAbgr(0xFF5E5BFF)))
|
|
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, ColourUtil.RgbaToAbgr(0xFF7775FF)))
|
|
using (ImRaii.PushColor(ImGuiCol.ButtonActive, ColourUtil.RgbaToAbgr(0xFF4542FF)))
|
|
using (ImRaii.PushColor(ImGuiCol.Text, 0xFFFFFFFF))
|
|
{
|
|
var buttonWidth =
|
|
ImGui.CalcTextSize(buttonLabel).X + ImGui.GetStyle().FramePadding.X * 2;
|
|
var buttonWidth2 =
|
|
ImGui.CalcTextSize(buttonLabel2).X + ImGui.GetStyle().FramePadding.X * 2;
|
|
ImGui.SameLine(
|
|
ImGui.GetContentRegionAvail().X
|
|
- buttonWidth
|
|
- buttonWidth2
|
|
- ImGui.GetStyle().ItemSpacing.X
|
|
);
|
|
|
|
if (ImGui.Button(buttonLabel2))
|
|
Plugin.PlatformUtil.OpenLink("https://ko-fi.com/infiii");
|
|
|
|
ImGui.SameLine();
|
|
|
|
if (ImGui.Button(buttonLabel))
|
|
Plugin.PlatformUtil.OpenLink("https://ko-fi.com/lojewalo");
|
|
}
|
|
|
|
if (!save)
|
|
return;
|
|
|
|
var hideChanged = !Mutable.HideChat && Mutable.HideChat != Plugin.Config.HideChat;
|
|
var languageChanged = Mutable.LanguageOverride != Plugin.Config.LanguageOverride;
|
|
|
|
// v1.5.3: Auto-enable the ExtraGlyphRanges flag matching the new
|
|
// locale so non-Latin scripts render immediately. Without this,
|
|
// a user switching to Korean would see "===" until they manually
|
|
// tick the Korean range in Fonts & Colours.
|
|
if (languageChanged)
|
|
{
|
|
var required = Mutable.LanguageOverride.RequiredGlyphRanges();
|
|
if (required != 0)
|
|
Mutable.ExtraGlyphRanges |= required;
|
|
}
|
|
|
|
var fontChanged =
|
|
Mutable.GlobalFontV2 != Plugin.Config.GlobalFontV2
|
|
|| Mutable.JapaneseFontV2 != Plugin.Config.JapaneseFontV2
|
|
|| Mutable.ItalicFontV2 != Plugin.Config.ItalicFontV2
|
|
|| Mutable.ExtraGlyphRanges != Plugin.Config.ExtraGlyphRanges
|
|
|| Mutable.UseHellionFont != Plugin.Config.UseHellionFont;
|
|
var fontSizeChanged =
|
|
Math.Abs(Mutable.SymbolsFontSizeV2 - Plugin.Config.SymbolsFontSizeV2) > 0.001
|
|
|| Math.Abs(Mutable.FontSizeV2 - Plugin.Config.FontSizeV2) > 0.001;
|
|
var italicStateChanged = Mutable.ItalicEnabled != Plugin.Config.ItalicEnabled;
|
|
|
|
// Only refilter when filter-relevant settings changed. Clear+Refilter
|
|
// reloads from the DB and silently drops in-session messages that
|
|
// weren't persisted (Privacy-First blocks most channels). Cosmetic
|
|
// changes (theme, icons, layout) skip the cycle.
|
|
var filtersChanged = HasFilterRelevantChanges();
|
|
|
|
Plugin.Config.UpdateFrom(Mutable, true);
|
|
|
|
// Defer save by 60 frames to avoid committing changes that cause a crash.
|
|
Plugin.DeferredSaveFrames = 60;
|
|
if (filtersChanged)
|
|
{
|
|
Plugin.MessageManager.ClearAllTabs();
|
|
Plugin.MessageManager.FilterAllTabsAsync();
|
|
}
|
|
|
|
if (fontChanged || fontSizeChanged || italicStateChanged)
|
|
Plugin.FontManager.RebuildDelegateFonts();
|
|
|
|
if (languageChanged)
|
|
Plugin.LanguageChanged(Plugin.Interface.UiLanguage);
|
|
|
|
if (hideChanged)
|
|
GameFunctions.GameFunctions.SetChatInteractable(true);
|
|
|
|
if (Plugin.Config.ShowEmotes)
|
|
_ = EmoteCache.LoadData();
|
|
|
|
Initialise();
|
|
}
|
|
|
|
// Returns true if any filter-relevant setting changed between Plugin.Config
|
|
// and the Mutable copy. Gates Clear+Refilter on Save so cosmetic changes
|
|
// don't wipe in-session chat history.
|
|
private bool HasFilterRelevantChanges()
|
|
{
|
|
if (Mutable.PrivacyFilterEnabled != Plugin.Config.PrivacyFilterEnabled)
|
|
return true;
|
|
if (Mutable.PrivacyPersistUnknownChannels != Plugin.Config.PrivacyPersistUnknownChannels)
|
|
return true;
|
|
if (!Mutable.PrivacyPersistChannels.SetEquals(Plugin.Config.PrivacyPersistChannels))
|
|
return true;
|
|
|
|
// FilterIncludePreviousSessions changes the GetMostRecentMessages
|
|
// window and is filter-relevant even outside the Privacy block.
|
|
if (Mutable.FilterIncludePreviousSessions != Plugin.Config.FilterIncludePreviousSessions)
|
|
return true;
|
|
|
|
// Compare persistent tabs only -- TempTabs are never refiltered.
|
|
var origPersistent = Plugin.Config.Tabs.Where(t => !t.IsTempTab).ToList();
|
|
var newPersistent = Mutable.Tabs.Where(t => !t.IsTempTab).ToList();
|
|
|
|
if (origPersistent.Count != newPersistent.Count)
|
|
return true;
|
|
|
|
for (var i = 0; i < origPersistent.Count; i++)
|
|
{
|
|
var orig = origPersistent[i];
|
|
var neu = newPersistent[i];
|
|
|
|
// Identifier mismatch means reorder or slot swap -- treat as filter-relevant.
|
|
if (orig.Identifier != neu.Identifier)
|
|
return true;
|
|
|
|
if (orig.ExtraChatAll != neu.ExtraChatAll)
|
|
return true;
|
|
if (!orig.ExtraChatChannels.SetEquals(neu.ExtraChatChannels))
|
|
return true;
|
|
|
|
if (orig.SelectedChannels.Count != neu.SelectedChannels.Count)
|
|
return true;
|
|
foreach (var pair in orig.SelectedChannels)
|
|
{
|
|
if (!neu.SelectedChannels.TryGetValue(pair.Key, out var nv))
|
|
return true;
|
|
if (!pair.Value.Equals(nv))
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|