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 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()), new FontsAndColours(Plugin, Mutable, loggerFactory.CreateLogger()), 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()), 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; } }