From 507efc8cdaa4e5868ce4caff4cbff130d188905a Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 18:43:13 +0200 Subject: [PATCH 01/27] feat(tabs): nullable Tab.Icon field for custom glyph override --- HellionChat/Configuration.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/HellionChat/Configuration.cs b/HellionChat/Configuration.cs index da3b4d6..d005aa8 100755 --- a/HellionChat/Configuration.cs +++ b/HellionChat/Configuration.cs @@ -394,6 +394,12 @@ public class Tab { public string Name = Language.Tab_DefaultName; + // v1.2.0 — optionaler FontAwesome-Glyph-Name. Null bedeutet: + // Default-Mapping aus TabIconMapping greift (basiert auf Tab-Name + // oder erstem SelectedChannels-Eintrag). User können hier per + // Settings → Tabs einen eigenen Glyph setzen. + public string? Icon = null; + [Obsolete("Removed in favor of SelectedChannels")] public Dictionary ChatCodes = new(); From a2db8cb6397cf779806023b4515e88438451593d Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 18:52:47 +0200 Subject: [PATCH 02/27] feat(tabs): TabIconMapping with default-pool plus override resolver --- HellionChat/Ui/TabIconMapping.cs | 145 +++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 HellionChat/Ui/TabIconMapping.cs diff --git a/HellionChat/Ui/TabIconMapping.cs b/HellionChat/Ui/TabIconMapping.cs new file mode 100644 index 0000000..259dcb4 --- /dev/null +++ b/HellionChat/Ui/TabIconMapping.cs @@ -0,0 +1,145 @@ +using Dalamud.Interface; + +namespace HellionChat.Ui; + +/// +/// Default-Icon-Mapping für Tabs. v1.2.0 Layout-Refresh nutzt das +/// in Top-Tabs (Icon-Prefix) und Sidebar (Icon-only mit Tooltip). +/// User können in Settings → Tabs per Tab.Icon-Override eigene +/// FontAwesome-Glyphen setzen. +/// +/// Aufteilung: +/// - + +/// sind reine String-Logik und stehen aus Test-Sicht ohne +/// Dalamud-Dependency zur Verfügung (siehe Hilfsklasse +/// in derselben Datei, die ohne +/// Dalamud-Imports auskommt). +/// - liefert den FontAwesomeIcon-Enum-Wert +/// für Render-Code (T3/T5/T7) und ist daher Dalamud-abhängig. +/// +internal static class TabIconMapping +{ + /// + /// FontAwesome-Glyph-Name → Icon-Enum-Lookup. Wird für die + /// Production-Resolve-API benötigt. Enthält nur Glyphen aus + /// . + /// + private static readonly Dictionary GlyphLookup = new(StringComparer.OrdinalIgnoreCase) + { + ["comment"] = FontAwesomeIcon.Comment, + ["comments"] = FontAwesomeIcon.Comments, + ["cog"] = FontAwesomeIcon.Cog, + ["users"] = FontAwesomeIcon.Users, + ["user-friends"] = FontAwesomeIcon.UserFriends, + ["link"] = FontAwesomeIcon.Link, + ["envelope"] = FontAwesomeIcon.Envelope, + ["clock"] = FontAwesomeIcon.Clock, + ["hashtag"] = FontAwesomeIcon.Hashtag, + ["star"] = FontAwesomeIcon.Star, + ["heart"] = FontAwesomeIcon.Heart, + ["bell"] = FontAwesomeIcon.Bell, + ["bookmark"] = FontAwesomeIcon.Bookmark, + ["flag"] = FontAwesomeIcon.Flag, + ["fire"] = FontAwesomeIcon.Fire, + }; + + /// + /// Picker-Options-Pool — Pass-through zu . + /// + public static IReadOnlyList PickerOptions => TabIconGlyphResolver.PickerOptions; + + /// + /// Pass-through zu . + /// Liegt hier nochmal als Convenience-Alias, damit Aufrufer nur + /// kennen müssen. + /// + public static string ResolveGlyphName(Tab tab) => TabIconGlyphResolver.ResolveGlyphName(tab); + + /// + /// Production-Surface: liefert das Icon für einen Tab. Wrapper um + /// plus + /// Enum-Lookup. Wird von Render-Code (T3, T5, T7) verwendet. + /// + public static FontAwesomeIcon Resolve(Tab tab) + { + var glyph = TabIconGlyphResolver.ResolveGlyphName(tab); + return GlyphLookup.TryGetValue(glyph, out var icon) + ? icon + : FontAwesomeIcon.Hashtag; + } +} + +/// +/// Reine String-Resolver-Logik ohne Dalamud-Dependency. Bewusst +/// separat, damit Tests (HellionChat.Tests, Microsoft.NET.Sdk ohne +/// Dalamud-Reference) sie aufrufen können, ohne dass die JIT beim +/// Methodenaufruf die Dalamud-Assembly laden muss. +/// +internal static class TabIconGlyphResolver +{ + /// + /// Glyph-Set, das überhaupt als Override akzeptiert wird. Spiegelt + /// die Keys aus . + /// + private static readonly HashSet KnownGlyphs = new(StringComparer.OrdinalIgnoreCase) + { + "comment", "comments", "cog", "users", "user-friends", "link", + "envelope", "clock", "hashtag", "star", "heart", "bell", + "bookmark", "flag", "fire", + }; + + /// + /// Picker-Options-Pool. Wird im Settings-Tab Icon-Combobox angezeigt. + /// Reihenfolge ist die UI-Reihenfolge. + /// + public static readonly IReadOnlyList PickerOptions = + ["comment", "comments", "cog", "users", "user-friends", "link", + "envelope", "clock", "hashtag", "star", "heart", "bell", + "bookmark", "flag", "fire"]; + + /// + /// Tab-Name → Default-Glyph-Name. Tab.Name wird per Lokalisierung + /// gesetzt; wir matchen daher gegen einen Pool aus DE/EN-Synonymen. + /// + private static readonly Dictionary NameDefaults = new(StringComparer.OrdinalIgnoreCase) + { + ["allgemein"] = "comment", + ["general"] = "comment", + ["system"] = "cog", + ["free company"] = "users", + ["fc"] = "users", + ["gruppe"] = "user-friends", + ["group"] = "user-friends", + ["party"] = "user-friends", + ["linkshell"] = "link", + ["ls"] = "link", + ["cwls"] = "link", + ["tells"] = "envelope", + ["tell"] = "envelope", + }; + + /// + /// Test-Surface: Glyph-Name-Resolver ohne Dalamud-Dependency. + /// Reihenfolge: + /// 1. Tab.Icon-Override (falls gesetzt): + /// a) bekannter Glyph → diesen Glyph + /// b) unbekannter Glyph → harter Fallback "hashtag" (User hat + /// bewusst etwas gesetzt, also überstimmt das die Defaults) + /// 2. Auto-Tell-Tab → "clock" + /// 3. Tab-Name-Default (-Lookup) + /// 4. Fallback "hashtag" + /// + public static string ResolveGlyphName(Tab tab) + { + if (!string.IsNullOrEmpty(tab.Icon)) + return KnownGlyphs.Contains(tab.Icon) ? tab.Icon : "hashtag"; + + if (tab.IsTempTab) + return "clock"; + + if (NameDefaults.TryGetValue(tab.Name, out var byName)) + return byName; + + return "hashtag"; + } +} From c17f5ae5163838f01f3bf9fe844a8ec599c5af47 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 19:00:54 +0200 Subject: [PATCH 03/27] refactor(tabs): split TabIconGlyphResolver, single-source glyph pool, polish --- HellionChat/Configuration.cs | 5 +- HellionChat/Ui/TabIconGlyphResolver.cs | 78 +++++++++++++++++ HellionChat/Ui/TabIconMapping.cs | 112 ++++--------------------- 3 files changed, 94 insertions(+), 101 deletions(-) create mode 100644 HellionChat/Ui/TabIconGlyphResolver.cs diff --git a/HellionChat/Configuration.cs b/HellionChat/Configuration.cs index d005aa8..5fdb909 100755 --- a/HellionChat/Configuration.cs +++ b/HellionChat/Configuration.cs @@ -395,9 +395,8 @@ public class Tab public string Name = Language.Tab_DefaultName; // v1.2.0 — optionaler FontAwesome-Glyph-Name. Null bedeutet: - // Default-Mapping aus TabIconMapping greift (basiert auf Tab-Name - // oder erstem SelectedChannels-Eintrag). User können hier per - // Settings → Tabs einen eigenen Glyph setzen. + // Default-Mapping aus TabIconMapping greift (basiert auf Tab-Name). + // User können hier per Settings → Tabs einen eigenen Glyph setzen. public string? Icon = null; [Obsolete("Removed in favor of SelectedChannels")] diff --git a/HellionChat/Ui/TabIconGlyphResolver.cs b/HellionChat/Ui/TabIconGlyphResolver.cs new file mode 100644 index 0000000..f587c12 --- /dev/null +++ b/HellionChat/Ui/TabIconGlyphResolver.cs @@ -0,0 +1,78 @@ +namespace HellionChat.Ui; + +/// +/// Reine String-Resolver-Logik ohne Dalamud-Dependency. Bewusst in +/// eigener Datei (Dependency-Boundary auf File-Level sichtbar), damit +/// Tests (HellionChat.Tests, Microsoft.NET.Sdk ohne Dalamud-Reference) +/// sie aufrufen können, ohne dass die JIT beim Methodenaufruf die +/// Dalamud-Assembly laden muss. +/// +/// Wird im Settings-UI (T7) für die Glyph-Picker-Combobox und im +/// Render-Code indirekt über +/// verwendet. +/// +internal static class TabIconGlyphResolver +{ + /// + /// Picker-Options-Pool — Single Source of Truth für das Glyph-Set. + /// Reihenfolge ist die UI-Reihenfolge im Settings-Tab Icon-Combobox. + /// + public static readonly IReadOnlyList PickerOptions = + ["comment", "comments", "cog", "users", "user-friends", "link", + "envelope", "clock", "hashtag", "star", "heart", "bell", + "bookmark", "flag", "fire"]; + + /// + /// Glyph-Set, das überhaupt als Override akzeptiert wird. Aus + /// abgeleitet — KnownGlyphs nie + /// manuell pflegen. + /// + private static readonly HashSet KnownGlyphs = + new(PickerOptions, StringComparer.OrdinalIgnoreCase); + + /// + /// Tab-Name → Default-Glyph-Name. Tab.Name wird per Lokalisierung + /// gesetzt; wir matchen daher gegen einen Pool aus DE/EN-Synonymen. + /// + private static readonly Dictionary NameDefaults = new(StringComparer.OrdinalIgnoreCase) + { + ["allgemein"] = "comment", + ["general"] = "comment", + ["system"] = "cog", + ["free company"] = "users", + ["fc"] = "users", + ["gruppe"] = "user-friends", + ["group"] = "user-friends", + ["party"] = "user-friends", + ["linkshell"] = "link", + ["ls"] = "link", + ["cwls"] = "link", + ["tells"] = "envelope", + ["tell"] = "envelope", + }; + + /// + /// Test-Surface: Glyph-Name-Resolver ohne Dalamud-Dependency. + /// Reihenfolge: + /// 1. Tab.Icon-Override (falls gesetzt und nicht nur Whitespace): + /// a) bekannter Glyph → diesen Glyph + /// b) unbekannter Glyph → harter Fallback "hashtag" (User hat + /// bewusst etwas gesetzt, also überstimmt das die Defaults) + /// 2. Auto-Tell-Tab → "clock" + /// 3. Tab-Name-Default (-Lookup) + /// 4. Fallback "hashtag" + /// + public static string ResolveGlyphName(Tab tab) + { + if (!string.IsNullOrWhiteSpace(tab.Icon)) + return KnownGlyphs.Contains(tab.Icon) ? tab.Icon : "hashtag"; + + if (tab.IsTempTab) + return "clock"; + + if (tab.Name is { } name && NameDefaults.TryGetValue(name, out var byName)) + return byName; + + return "hashtag"; + } +} diff --git a/HellionChat/Ui/TabIconMapping.cs b/HellionChat/Ui/TabIconMapping.cs index 259dcb4..208efee 100644 --- a/HellionChat/Ui/TabIconMapping.cs +++ b/HellionChat/Ui/TabIconMapping.cs @@ -8,21 +8,24 @@ namespace HellionChat.Ui; /// User können in Settings → Tabs per Tab.Icon-Override eigene /// FontAwesome-Glyphen setzen. /// -/// Aufteilung: -/// - + -/// sind reine String-Logik und stehen aus Test-Sicht ohne -/// Dalamud-Dependency zur Verfügung (siehe Hilfsklasse -/// in derselben Datei, die ohne -/// Dalamud-Imports auskommt). -/// - liefert den FontAwesomeIcon-Enum-Wert -/// für Render-Code (T3/T5/T7) und ist daher Dalamud-abhängig. +/// Diese Klasse ist Dalamud-abhängig (FontAwesomeIcon-Enum). Die +/// reine String-Resolver-Logik liegt bewusst in +/// (eigene Datei, ohne +/// Dalamud-Imports), damit Tests sie ohne Dalamud-Reference aufrufen +/// können. /// internal static class TabIconMapping { /// /// FontAwesome-Glyph-Name → Icon-Enum-Lookup. Wird für die - /// Production-Resolve-API benötigt. Enthält nur Glyphen aus - /// . + /// Production-Resolve-API benötigt. + /// + /// INVARIANTE: Jeder Key in muss auch in + /// stehen. Wird + /// ein Glyph zu PickerOptions hinzugefügt, aber nicht hier, fällt + /// die Override-Auflösung still auf + /// zurück (degraded, kein Crash). Build-Time-Enforcement ist nicht + /// möglich, weil PickerOptions ohne Dalamud-Reference auskommt. /// private static readonly Dictionary GlyphLookup = new(StringComparer.OrdinalIgnoreCase) { @@ -43,22 +46,10 @@ internal static class TabIconMapping ["fire"] = FontAwesomeIcon.Fire, }; - /// - /// Picker-Options-Pool — Pass-through zu . - /// - public static IReadOnlyList PickerOptions => TabIconGlyphResolver.PickerOptions; - - /// - /// Pass-through zu . - /// Liegt hier nochmal als Convenience-Alias, damit Aufrufer nur - /// kennen müssen. - /// - public static string ResolveGlyphName(Tab tab) => TabIconGlyphResolver.ResolveGlyphName(tab); - /// /// Production-Surface: liefert das Icon für einen Tab. Wrapper um /// plus - /// Enum-Lookup. Wird von Render-Code (T3, T5, T7) verwendet. + /// Enum-Lookup. Wird von Render-Code (T3, T5) verwendet. /// public static FontAwesomeIcon Resolve(Tab tab) { @@ -68,78 +59,3 @@ internal static class TabIconMapping : FontAwesomeIcon.Hashtag; } } - -/// -/// Reine String-Resolver-Logik ohne Dalamud-Dependency. Bewusst -/// separat, damit Tests (HellionChat.Tests, Microsoft.NET.Sdk ohne -/// Dalamud-Reference) sie aufrufen können, ohne dass die JIT beim -/// Methodenaufruf die Dalamud-Assembly laden muss. -/// -internal static class TabIconGlyphResolver -{ - /// - /// Glyph-Set, das überhaupt als Override akzeptiert wird. Spiegelt - /// die Keys aus . - /// - private static readonly HashSet KnownGlyphs = new(StringComparer.OrdinalIgnoreCase) - { - "comment", "comments", "cog", "users", "user-friends", "link", - "envelope", "clock", "hashtag", "star", "heart", "bell", - "bookmark", "flag", "fire", - }; - - /// - /// Picker-Options-Pool. Wird im Settings-Tab Icon-Combobox angezeigt. - /// Reihenfolge ist die UI-Reihenfolge. - /// - public static readonly IReadOnlyList PickerOptions = - ["comment", "comments", "cog", "users", "user-friends", "link", - "envelope", "clock", "hashtag", "star", "heart", "bell", - "bookmark", "flag", "fire"]; - - /// - /// Tab-Name → Default-Glyph-Name. Tab.Name wird per Lokalisierung - /// gesetzt; wir matchen daher gegen einen Pool aus DE/EN-Synonymen. - /// - private static readonly Dictionary NameDefaults = new(StringComparer.OrdinalIgnoreCase) - { - ["allgemein"] = "comment", - ["general"] = "comment", - ["system"] = "cog", - ["free company"] = "users", - ["fc"] = "users", - ["gruppe"] = "user-friends", - ["group"] = "user-friends", - ["party"] = "user-friends", - ["linkshell"] = "link", - ["ls"] = "link", - ["cwls"] = "link", - ["tells"] = "envelope", - ["tell"] = "envelope", - }; - - /// - /// Test-Surface: Glyph-Name-Resolver ohne Dalamud-Dependency. - /// Reihenfolge: - /// 1. Tab.Icon-Override (falls gesetzt): - /// a) bekannter Glyph → diesen Glyph - /// b) unbekannter Glyph → harter Fallback "hashtag" (User hat - /// bewusst etwas gesetzt, also überstimmt das die Defaults) - /// 2. Auto-Tell-Tab → "clock" - /// 3. Tab-Name-Default (-Lookup) - /// 4. Fallback "hashtag" - /// - public static string ResolveGlyphName(Tab tab) - { - if (!string.IsNullOrEmpty(tab.Icon)) - return KnownGlyphs.Contains(tab.Icon) ? tab.Icon : "hashtag"; - - if (tab.IsTempTab) - return "clock"; - - if (NameDefaults.TryGetValue(tab.Name, out var byName)) - return byName; - - return "hashtag"; - } -} From a1cdae05d0e228cc8766290bbef3e3ee05210924 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 19:08:58 +0200 Subject: [PATCH 04/27] feat(sidebar): icon-only tabs with tooltip and 44px fixed width --- HellionChat/Ui/ChatLogWindow.cs | 56 ++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/HellionChat/Ui/ChatLogWindow.cs b/HellionChat/Ui/ChatLogWindow.cs index 5949104..0411add 100644 --- a/HellionChat/Ui/ChatLogWindow.cs +++ b/HellionChat/Ui/ChatLogWindow.cs @@ -1383,12 +1383,13 @@ public sealed class ChatLogWindow : Window private void DrawTabSidebar() { var currentTab = -1; - using var tabTable = ImRaii.Table("tabs-table", 2, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.Resizable); + // v1.2.0 — Sidebar fix 44 px, kein Resize. Mehr Platz fürs Chat-Log. + using var tabTable = ImRaii.Table("tabs-table", 2, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingFixedFit); if (!tabTable.Success) return; - ImGui.TableSetupColumn("tabs", ImGuiTableColumnFlags.WidthStretch, 1); - ImGui.TableSetupColumn("chat", ImGuiTableColumnFlags.WidthStretch, 4); + ImGui.TableSetupColumn("tabs", ImGuiTableColumnFlags.WidthFixed, 44f); + ImGui.TableSetupColumn("chat", ImGuiTableColumnFlags.WidthStretch, 1); ImGui.TableNextColumn(); @@ -1422,7 +1423,6 @@ public sealed class ChatLogWindow : Window } var unread = tabI == Plugin.LastTab || tab.UnreadMode == UnreadMode.None || tab.Unread == 0 ? "" : $" ({tab.Unread})"; - var selectableLabel = $"{tab.Name}{unread}###log-tab-{tabI}"; var isCurrentTab = Plugin.LastTab == tabI || Plugin.WantedTab == tabI; var showGreetedAffordance = tab.IsTempTab && Plugin.Config.AutoTellTabsShowGreetedToggle; @@ -1457,34 +1457,38 @@ public sealed class ChatLogWindow : Window ImGui.SameLine(); } - bool clicked; - if (showGreetedAffordance && tab.IsGreeted) - { - // Dim the tab name once the user marked the partner - // as greeted, so a glance at the sidebar tells them - // who still needs attention. Selectable has no idle - // background slot in ImGui, so the dim only applies - // to the selected and hovered states — the text dim - // alone signals greeted in the idle state. - var headerBase = ImGui.GetColorU32(ImGuiCol.Header); - var hoverBase = ImGui.GetColorU32(ImGuiCol.HeaderHovered); - var dimHeader = (headerBase & 0xFF000000u) | ((headerBase & 0x00FEFEFEu) >> 1); - var dimHover = (hoverBase & 0xFF000000u) | ((hoverBase & 0x00FEFEFEu) >> 1); + // v1.2.0 — Icon-only Sidebar mit Tooltip beim Hover. + // Active-Tab kriegt Akzent-Color am Icon, Greeted-Tabs + // werden auf TextDim gedimmt (löst den alten Header- + // Dim-Trick ab, da wir keine Selectable mehr nutzen). + var theme = Plugin.ThemeRegistry.Active; + var icon = TabIconMapping.Resolve(tab); + var iconColor = isCurrentTab + ? theme.Colors.Accent + : (showGreetedAffordance && tab.IsGreeted ? theme.Colors.TextDim : theme.Colors.TextPrimary); - using (ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled))) - using (ImRaii.PushColor(ImGuiCol.Header, dimHeader)) - using (ImRaii.PushColor(ImGuiCol.HeaderHovered, dimHover)) - { - clicked = ImGui.Selectable(selectableLabel, isCurrentTab); - } - } - else + bool clicked; + using (ImRaii.PushColor(ImGuiCol.Button, 0u)) + using (ImRaii.PushColor(ImGuiCol.ButtonHovered, ColourUtil.RgbaToAbgr(theme.Colors.SurfaceHover))) + using (ImRaii.PushColor(ImGuiCol.ButtonActive, ColourUtil.RgbaToAbgr(theme.Colors.Surface))) + using (ImRaii.PushColor(ImGuiCol.Text, ColourUtil.RgbaToAbgr(iconColor))) + using (Plugin.FontManager.FontAwesome.Push()) { - clicked = ImGui.Selectable(selectableLabel, isCurrentTab); + clicked = ImGui.Button($"{icon.ToIconString()}##sidebar-tab-{tabI}", new Vector2(36f, 32f)); + } + + // Tooltip mit Tab-Name + Unread-Counter beim Hover. + if (ImGui.IsItemHovered()) + { + using var tt = ImRaii.Tooltip(); + ImGui.TextUnformatted($"{tab.Name}{unread}"); } DrawTabContextMenu(tab, tabI); + if (clicked) + Plugin.WantedTab = tabI; + if (!clicked && Plugin.WantedTab != tabI) continue; From 5a9c2018b0f208ea3c098171e97a6622d3d4a5e3 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 19:09:26 +0200 Subject: [PATCH 05/27] feat(sidebar): active-tab vertical accent pill --- HellionChat/Ui/ChatLogWindow.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/HellionChat/Ui/ChatLogWindow.cs b/HellionChat/Ui/ChatLogWindow.cs index 0411add..1831587 100644 --- a/HellionChat/Ui/ChatLogWindow.cs +++ b/HellionChat/Ui/ChatLogWindow.cs @@ -1477,6 +1477,23 @@ public sealed class ChatLogWindow : Window clicked = ImGui.Button($"{icon.ToIconString()}##sidebar-tab-{tabI}", new Vector2(36f, 32f)); } + if (isCurrentTab) + { + // v1.2.0 — Vertikale Akzent-Pill an der linken Window-Kante. + // 3 px breit, halbe Tab-Höhe, vertikal zentriert. ImGui hat keine + // native Pill-API, daher direkter DrawList-Pass. + var min = ImGui.GetItemRectMin(); + var max = ImGui.GetItemRectMax(); + const float pillWidth = 3f; + var pillHeight = (max.Y - min.Y) * 0.5f; + var pillCenterY = (min.Y + max.Y) * 0.5f; + ImGui.GetWindowDrawList().AddRectFilled( + new Vector2(min.X, pillCenterY - pillHeight * 0.5f), + new Vector2(min.X + pillWidth, pillCenterY + pillHeight * 0.5f), + ColourUtil.RgbaToAbgr(theme.Colors.Accent), + 1.5f); // leichter Rounding + } + // Tooltip mit Tab-Name + Unread-Counter beim Hover. if (ImGui.IsItemHovered()) { From f663cb3c148dc96fae553895000323cc796e2731 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 19:17:59 +0200 Subject: [PATCH 06/27] feat(top-tabs): icon glyph prefix in tab label --- HellionChat/Ui/ChatLogWindow.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/HellionChat/Ui/ChatLogWindow.cs b/HellionChat/Ui/ChatLogWindow.cs index 1831587..a197d78 100644 --- a/HellionChat/Ui/ChatLogWindow.cs +++ b/HellionChat/Ui/ChatLogWindow.cs @@ -1360,7 +1360,13 @@ public sealed class ChatLogWindow : Window if (Plugin.WantedTab == tabI) flags |= ImGuiTabItemFlags.SetSelected; - using var tabItem = ImRaii.TabItem($"{tab.Name}{unread}###log-tab-{tabI}", flags); + // v1.2.0 — Icon-Glyph als Prefix vor dem Tab-Namen. + // Strategy A: Codepoint einfach in den Label-String einbauen. + // Funktioniert wenn Dalamuds Default-Font-Atlas FontAwesome- + // Glyphen mit-merged. Falls nicht (Tofu-Square sichtbar), + // wechseln wir auf Strategy B mit separatem Icon-Render. + var icon = TabIconMapping.Resolve(tab); + using var tabItem = ImRaii.TabItem($"{icon.ToIconString()} {tab.Name}{unread}###log-tab-{tabI}", flags); DrawTabContextMenu(tab, tabI); if (!tabItem.Success) From bc0f44712f820838b68a09c753b70c51dcc6d4a4 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 19:18:18 +0200 Subject: [PATCH 07/27] feat(top-tabs): active-tab accent underline pill --- HellionChat/Ui/ChatLogWindow.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/HellionChat/Ui/ChatLogWindow.cs b/HellionChat/Ui/ChatLogWindow.cs index a197d78..9a7fdb8 100644 --- a/HellionChat/Ui/ChatLogWindow.cs +++ b/HellionChat/Ui/ChatLogWindow.cs @@ -1372,6 +1372,20 @@ public sealed class ChatLogWindow : Window if (!tabItem.Success) continue; + // v1.2.0 — Active-Tab-Underline-Pill (2 px Akzent statt Background-Fill). + // Bewusst direkt nach TabItem-Setup; GetItemRectMin/Max referenziert noch + // das Tab. ImGui hat keine native Underline-API, daher direkter DrawList-Pass. + { + var theme = Plugin.ThemeRegistry.Active; + var min = ImGui.GetItemRectMin(); + var max = ImGui.GetItemRectMax(); + const float pillHeight = 2f; + ImGui.GetWindowDrawList().AddRectFilled( + new Vector2(min.X, max.Y - pillHeight), + new Vector2(max.X, max.Y), + ColourUtil.RgbaToAbgr(theme.Colors.Accent)); + } + var hasTabSwitched = Plugin.LastTab != tabI; Plugin.LastTab = tabI; From c28c972ae332321f7948e76ebbc844c582a17863 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 19:23:26 +0200 Subject: [PATCH 08/27] =?UTF-8?q?revert(top-tabs):=20drop=20icon=20prefix?= =?UTF-8?q?=20=E2=80=94=20Dalamud=20default=20font=20lacks=20FontAwesome?= =?UTF-8?q?=20codepoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tofu-squares rendered in-game (verified by Flo 2026-05-05 19:21). ImGui TabItem labels render in a single font frame; mixed-font (FontAwesome icon + default font for tab name) is not possible without Font-Atlas merging at FontManager level — out of scope for v1.2.0. Top-Tabs visual modernization is now driven by the Underline-Pill alone (T6, kept). Sidebar (icon-only) remains the use case where icons earn their keep. v1.2.0 Akzeptanzkriterium AC1 wird auf "Top-Tabs haben Pill-Underline" reduziert. --- HellionChat/Ui/ChatLogWindow.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/HellionChat/Ui/ChatLogWindow.cs b/HellionChat/Ui/ChatLogWindow.cs index 9a7fdb8..99f4083 100644 --- a/HellionChat/Ui/ChatLogWindow.cs +++ b/HellionChat/Ui/ChatLogWindow.cs @@ -1360,13 +1360,7 @@ public sealed class ChatLogWindow : Window if (Plugin.WantedTab == tabI) flags |= ImGuiTabItemFlags.SetSelected; - // v1.2.0 — Icon-Glyph als Prefix vor dem Tab-Namen. - // Strategy A: Codepoint einfach in den Label-String einbauen. - // Funktioniert wenn Dalamuds Default-Font-Atlas FontAwesome- - // Glyphen mit-merged. Falls nicht (Tofu-Square sichtbar), - // wechseln wir auf Strategy B mit separatem Icon-Render. - var icon = TabIconMapping.Resolve(tab); - using var tabItem = ImRaii.TabItem($"{icon.ToIconString()} {tab.Name}{unread}###log-tab-{tabI}", flags); + using var tabItem = ImRaii.TabItem($"{tab.Name}{unread}###log-tab-{tabI}", flags); DrawTabContextMenu(tab, tabI); if (!tabItem.Success) From e62951855040471468df2e439d9210dd49185b42 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 19:28:04 +0200 Subject: [PATCH 09/27] feat(settings): per-tab icon combobox in Tabs section --- .../Resources/HellionStrings.Designer.cs | 5 +++ HellionChat/Resources/HellionStrings.de.resx | 11 ++++++ HellionChat/Resources/HellionStrings.resx | 11 ++++++ HellionChat/Ui/SettingsTabs/Tabs.cs | 34 +++++++++++++++++++ 4 files changed, 61 insertions(+) diff --git a/HellionChat/Resources/HellionStrings.Designer.cs b/HellionChat/Resources/HellionStrings.Designer.cs index 1402a1d..784a394 100644 --- a/HellionChat/Resources/HellionStrings.Designer.cs +++ b/HellionChat/Resources/HellionStrings.Designer.cs @@ -276,6 +276,11 @@ internal class HellionStrings internal static string Tabs_Presets_Linkshell => Get(nameof(Tabs_Presets_Linkshell)); internal static string Tabs_Presets_Linkshell_Hint => Get(nameof(Tabs_Presets_Linkshell_Hint)); + // Hellion Chat — v1.2.0 per-tab icon override + internal static string Tabs_Icon_Label => Get(nameof(Tabs_Icon_Label)); + internal static string Tabs_Icon_HelpMarker => Get(nameof(Tabs_Icon_HelpMarker)); + internal static string Tabs_Icon_DefaultOption => Get(nameof(Tabs_Icon_DefaultOption)); + // Hellion Chat — v0.6.0 chat colour presets (display labels) internal static string ChatColourPresets_Default => Get(nameof(ChatColourPresets_Default)); internal static string ChatColourPresets_HighContrast => Get(nameof(ChatColourPresets_HighContrast)); diff --git a/HellionChat/Resources/HellionStrings.de.resx b/HellionChat/Resources/HellionStrings.de.resx index 9b9c9bf..6957f51 100644 --- a/HellionChat/Resources/HellionStrings.de.resx +++ b/HellionChat/Resources/HellionStrings.de.resx @@ -561,6 +561,17 @@ Wenn du mehrere Linkshells benutzt, empfiehlt der Maintainer einen Tab pro Shell für eine sauberere Übersicht. Tab duplizieren und je Kopie die Kanalauswahl einschränken. + + + + Tab-Icon + + + FontAwesome-Glyph für die Sidebar. Default greift auf Tab-Name oder Channel-Typ. + + + (Default-Mapping) + Klassik (Chat 2 Default) diff --git a/HellionChat/Resources/HellionStrings.resx b/HellionChat/Resources/HellionStrings.resx index edba1d7..67a1f16 100644 --- a/HellionChat/Resources/HellionStrings.resx +++ b/HellionChat/Resources/HellionStrings.resx @@ -561,6 +561,17 @@ If you use multiple linkshells, the maintainer recommends one tab per shell for cleaner readability. Duplicate this tab and narrow the channel selection per copy. + + + + Tab-Icon + + + FontAwesome-Glyph für die Sidebar. Default greift auf Tab-Name oder Channel-Typ. + + + (Default-Mapping) + Klassik (Chat 2 Default) diff --git a/HellionChat/Ui/SettingsTabs/Tabs.cs b/HellionChat/Ui/SettingsTabs/Tabs.cs index 8ad2bb0..75eea88 100755 --- a/HellionChat/Ui/SettingsTabs/Tabs.cs +++ b/HellionChat/Ui/SettingsTabs/Tabs.cs @@ -91,6 +91,40 @@ internal sealed class Tabs : ISettingsTab } ImGui.InputText(Language.Options_Tabs_Name, ref tab.Name, 512, ImGuiInputTextFlags.EnterReturnsTrue); + + // v1.2.0 — Per-Tab Icon-Override. Default-Mapping greift falls nichts gesetzt. + ImGui.TextUnformatted(HellionStrings.Tabs_Icon_Label); + ImGui.SameLine(); + ImGuiUtil.HelpMarker(HellionStrings.Tabs_Icon_HelpMarker); + + var iconCurrent = string.IsNullOrEmpty(tab.Icon) ? "" : tab.Icon; + var iconPreview = iconCurrent.Length == 0 + ? HellionStrings.Tabs_Icon_DefaultOption + : iconCurrent; + using (var combo = ImRaii.Combo($"##icon-{i}", iconPreview)) + { + if (combo.Success) + { + // Erste Option: Default (löscht Icon, lässt Mapping greifen). + if (ImGui.Selectable(HellionStrings.Tabs_Icon_DefaultOption, iconCurrent.Length == 0)) + { + tab.Icon = null; + } + + ImGui.Separator(); + + // Pool-Optionen aus TabIconGlyphResolver.PickerOptions (Single-Source-of-Truth). + foreach (var option in TabIconGlyphResolver.PickerOptions) + { + var isSelected = string.Equals(iconCurrent, option, StringComparison.OrdinalIgnoreCase); + if (ImGui.Selectable(option, isSelected)) + { + tab.Icon = option; + } + } + } + } + ImGui.Checkbox(Language.Options_Tabs_ShowTimestamps, ref tab.DisplayTimestamp); ImGui.Checkbox(Language.Options_Tabs_PopOut, ref tab.PopOut); if (tab.PopOut) From 985a284e7ddce286538c3c8dde48bf01e86ee289 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 19:34:27 +0200 Subject: [PATCH 10/27] feat(statusbar): cached 1Hz status-bar component with format helpers --- HellionChat/Configuration.cs | 14 ++ .../Resources/HellionStrings.Designer.cs | 4 + HellionChat/Resources/HellionStrings.de.resx | 6 + HellionChat/Resources/HellionStrings.resx | 6 + HellionChat/Ui/StatusBar.cs | 171 ++++++++++++++++++ 5 files changed, 201 insertions(+) create mode 100644 HellionChat/Ui/StatusBar.cs diff --git a/HellionChat/Configuration.cs b/HellionChat/Configuration.cs index 5fdb909..557e8cb 100755 --- a/HellionChat/Configuration.cs +++ b/HellionChat/Configuration.cs @@ -603,6 +603,20 @@ public class Tab } } + /// + /// Aktuelle Anzahl der gespeicherten Messages. Lock-acquire pro Read + /// ist OK für 1×/sec Status-Bar-Polling (v1.2.0). + /// + public int Count + { + get + { + LockSlim.Wait(-1); + try { return Messages.Count; } + finally { LockSlim.Release(); } + } + } + /// /// Returns an array copy of the message list for usage outside of main thread /// diff --git a/HellionChat/Resources/HellionStrings.Designer.cs b/HellionChat/Resources/HellionStrings.Designer.cs index 784a394..bec7fff 100644 --- a/HellionChat/Resources/HellionStrings.Designer.cs +++ b/HellionChat/Resources/HellionStrings.Designer.cs @@ -315,4 +315,8 @@ internal class HellionStrings internal static string ChatTwoConflictTitle => Get(nameof(ChatTwoConflictTitle)); internal static string ChatTwoConflictBody => Get(nameof(ChatTwoConflictBody)); internal static string ChatTwoConflictAction => Get(nameof(ChatTwoConflictAction)); + + // Hellion Chat — v1.2.0 Bottom-Status-Bar Privacy-Badge labels + internal static string StatusBar_Privacy_Enabled => Get(nameof(StatusBar_Privacy_Enabled)); + internal static string StatusBar_Privacy_Open => Get(nameof(StatusBar_Privacy_Open)); } diff --git a/HellionChat/Resources/HellionStrings.de.resx b/HellionChat/Resources/HellionStrings.de.resx index 6957f51..5adeb67 100644 --- a/HellionChat/Resources/HellionStrings.de.resx +++ b/HellionChat/Resources/HellionStrings.de.resx @@ -716,4 +716,10 @@ Behalten + + Privacy-First + + + Offen + diff --git a/HellionChat/Resources/HellionStrings.resx b/HellionChat/Resources/HellionStrings.resx index 67a1f16..86d829d 100644 --- a/HellionChat/Resources/HellionStrings.resx +++ b/HellionChat/Resources/HellionStrings.resx @@ -716,4 +716,10 @@ Keep current + + Privacy-First + + + Open + diff --git a/HellionChat/Ui/StatusBar.cs b/HellionChat/Ui/StatusBar.cs new file mode 100644 index 0000000..77a6811 --- /dev/null +++ b/HellionChat/Ui/StatusBar.cs @@ -0,0 +1,171 @@ +using System.Globalization; +using System.Numerics; +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; +using HellionChat.Code; +using HellionChat.Resources; +using HellionChat.Util; + +namespace HellionChat.Ui; + +/// +/// Bottom-Status-Bar (v1.2.0). Fix 22 px hoch, BorderTop als Trenner. +/// Slots links → rechts: Channel-Indicator (Color-Dot + Channel-Name), +/// Privacy-Badge (Lock-Icon + Privacy-Label), Counts (Tabs + Msgs), +/// Tells (Auto-Tell-Counter, hidden bei 0), Version (rechtsbündig, muted). +/// +/// Update-Frequenz: 1×/Sekunde. Format-Strings werden zwischen Updates +/// gecached, damit kein Per-Frame-Format-Allocation entsteht. +/// +internal sealed class StatusBar +{ + public const float Height = 22f; + private const long UpdateIntervalMs = 1000; + + // Cache-State — initial outdated, damit der erste Frame frisch berechnet. + private long _lastUpdateMs = -UpdateIntervalMs; + private string _cachedCountsText = string.Empty; + private string _cachedTellsText = string.Empty; + + /// + /// Reine String-Logik — testbar ohne ImGui-Init. + /// + public static string FormatCounts(int tabs, int messages) + { + // InvariantCulture: User-System-Locale darf das Format nicht + // verändern (de_DE würde sonst "1,2k" statt "1.2k" liefern). + var msgPart = messages >= 1000 + ? string.Format(CultureInfo.InvariantCulture, "{0:0.0}k msg", messages / 1000.0) + : $"{messages} msg"; + var tabsPart = $"{tabs} {(tabs == 1 ? "tab" : "tabs")}"; + return $"{tabsPart} · {msgPart}"; + } + + /// + /// Reine String-Logik — testbar ohne ImGui-Init. + /// 0 Tells → Leerstring (Slot wird ausgeblendet). + /// + public static string FormatTells(int count) + { + if (count <= 0) return string.Empty; + return $"{count} {(count == 1 ? "tell" : "tells")}"; + } + + /// + /// Test-Hook: Cache-Logic ohne reale Time-Source verifizieren. + /// Nicht für Production-Render. + /// + internal (string counts, string tells) SnapshotForTest(long now, int tabs, int messages, int tells) + { + UpdateCacheIfDue(now, tabs, messages, tells); + return (_cachedCountsText, _cachedTellsText); + } + + private void UpdateCacheIfDue(long now, int tabs, int messages, int tells) + { + if (now - _lastUpdateMs < UpdateIntervalMs) + return; + _cachedCountsText = FormatCounts(tabs, messages); + _cachedTellsText = FormatTells(tells); + _lastUpdateMs = now; + } + + /// + /// Render-Pfad. Aufrufer pusht bereits den HellionStyle/Theme; + /// wir lesen nur die aktiven Theme-Farben und zeichnen. + /// + public void Draw(Plugin plugin) + { + var theme = plugin.ThemeRegistry.Active; + var now = Environment.TickCount64; + + // Counts pro Frame berechnen ist günstig (List<>.Count, kleine + // Sums); Format-String wird gecached. + var tabs = Plugin.Config.Tabs.Count; + var messages = Plugin.Config.Tabs.Sum(t => t.Messages.Count); + var tells = Plugin.Config.Tabs.Count(t => t.IsTempTab); + UpdateCacheIfDue(now, tabs, messages, tells); + + // BorderTop als Trenner — DrawList-Line, ImGui-Separator hat zu viel Padding. + var cursorY = ImGui.GetCursorScreenPos().Y; + var winLeft = ImGui.GetWindowPos().X; + var winRight = winLeft + ImGui.GetWindowSize().X; + ImGui.GetWindowDrawList().AddLine( + new Vector2(winLeft, cursorY), + new Vector2(winRight, cursorY), + ColourUtil.RgbaToAbgr(theme.Colors.Border), + 1f); + + ImGui.Dummy(new Vector2(0, 2)); // BorderTop-Spacing + + using var group = ImRaii.Group(); + + // Slot 1: Active-Channel-Indicator + var inputCh = plugin.CurrentTab?.CurrentChannel?.Channel ?? InputChannel.Invalid; + var hasChannel = inputCh != InputChannel.Invalid; + var chatType = inputCh.ToChatType(); + var channelName = hasChannel ? chatType.Name() : "—"; + var channelColor = hasChannel + ? (plugin.Functions.Chat.GetChannelColor(chatType) ?? theme.Colors.TextMuted) + : theme.Colors.TextMuted; + DrawDot(channelColor); + ImGui.SameLine(); + ImGui.TextUnformatted(channelName); + + // Slot 2: Privacy-Badge — abgeleitet aus PrivacyFilterEnabled. + ImGui.SameLine(); + DrawSeparator(); + ImGui.SameLine(); + using (plugin.FontManager.FontAwesome.Push()) + { + ImGui.TextUnformatted(FontAwesomeIcon.Lock.ToIconString()); + } + ImGui.SameLine(); + var privacyLabel = Plugin.Config.PrivacyFilterEnabled + ? HellionStrings.StatusBar_Privacy_Enabled + : HellionStrings.StatusBar_Privacy_Open; + ImGui.TextUnformatted(privacyLabel); + + // Slot 3: Counts + ImGui.SameLine(); + DrawSeparator(); + ImGui.SameLine(); + ImGui.TextUnformatted(_cachedCountsText); + + // Slot 4: Tells (nur wenn > 0) + if (!string.IsNullOrEmpty(_cachedTellsText)) + { + ImGui.SameLine(); + DrawSeparator(); + ImGui.SameLine(); + ImGui.TextUnformatted(_cachedTellsText); + } + + // Slot 5: Version (rechtsbündig, muted) + var versionText = $"v{Plugin.Interface.Manifest.AssemblyVersion} · Hellion"; + var versionWidth = ImGui.CalcTextSize(versionText).X; + var rightCursor = ImGui.GetWindowSize().X - versionWidth - ImGui.GetStyle().WindowPadding.X; + ImGui.SameLine(rightCursor); + using (ImRaii.PushColor(ImGuiCol.Text, ColourUtil.RgbaToAbgr(theme.Colors.TextMuted))) + { + ImGui.TextUnformatted(versionText); + } + } + + private static void DrawDot(uint rgba) + { + var pos = ImGui.GetCursorScreenPos(); + const float radius = 4f; + ImGui.GetWindowDrawList().AddCircleFilled( + new Vector2(pos.X + radius, pos.Y + ImGui.GetTextLineHeight() / 2f), + radius, + ColourUtil.RgbaToAbgr(rgba)); + ImGui.Dummy(new Vector2(radius * 2 + 4, ImGui.GetTextLineHeight())); + } + + private static void DrawSeparator() + { + ImGui.TextDisabled("·"); + } +} From a11c8bc6e971ec0bab2fe5fcae99eae604167783 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 19:35:52 +0200 Subject: [PATCH 11/27] feat(statusbar): wire status bar into ChatLogWindow render pipeline --- HellionChat/Plugin.cs | 3 +++ HellionChat/Ui/ChatLogWindow.cs | 17 ++++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index 6cb47f5..2b58150 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -64,6 +64,7 @@ public sealed class Plugin : IDalamudPlugin internal TypingIpc TypingIpc { get; } internal FontManager FontManager { get; } internal Themes.ThemeRegistry ThemeRegistry { get; private set; } = null!; + internal Ui.StatusBar StatusBar { get; private set; } = null!; internal int DeferredSaveFrames = -1; @@ -296,6 +297,8 @@ public sealed class Plugin : IDalamudPlugin ThemeRegistry = new Themes.ThemeRegistry(customThemesDir); ThemeRegistry.Switch(Config.Theme); + StatusBar = new Ui.StatusBar(); + MessageManager = new MessageManager(this); // Does it require UI? // Hellion Chat — Auto-Tell-Tabs service. Subscribes to the diff --git a/HellionChat/Ui/ChatLogWindow.cs b/HellionChat/Ui/ChatLogWindow.cs index 99f4083..1c826cf 100644 --- a/HellionChat/Ui/ChatLogWindow.cs +++ b/HellionChat/Ui/ChatLogWindow.cs @@ -375,6 +375,9 @@ public sealed class ChatLogWindow : Window // weil der Cursor schon weiter unten steht — kein eigener Abzug. height -= ImGui.GetFrameHeightWithSpacing(); + // v1.2.0 — Status-Bar am Window-Boden reserviert 22 px + 2 px Spacing. + height -= StatusBar.Height + 2; + return height; } @@ -790,13 +793,17 @@ public sealed class ChatLogWindow : Window if (ImGui.IsWindowHovered(ImGuiHoveredFlags.ChildWindows)) LastActivityTime = FrameTime; - if (!showNovice) - return; + if (showNovice) + { + ImGui.SameLine(); - ImGui.SameLine(); + if (ImGuiUtil.IconButton(FontAwesomeIcon.Leaf)) + GameFunctions.GameFunctions.ClickNoviceNetworkButton(); + } - if (ImGuiUtil.IconButton(FontAwesomeIcon.Leaf)) - GameFunctions.GameFunctions.ClickNoviceNetworkButton(); + // v1.2.0 — Bottom-Status-Bar. Letzter Render-Step in DrawChatLog, + // damit alle Zeilen-Operationen davor keine Layout-Sprünge auslösen. + Plugin.StatusBar.Draw(Plugin); } internal Dictionary GetValidChannels() From b48684ce5aa2dc3f5214b91bf0cd93ba304b4b6a Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 19:42:57 +0200 Subject: [PATCH 12/27] feat(settings): compact density toggle in Appearance --- HellionChat/Resources/HellionStrings.Designer.cs | 4 ++++ HellionChat/Resources/HellionStrings.de.resx | 6 ++++++ HellionChat/Resources/HellionStrings.resx | 6 ++++++ HellionChat/Ui/SettingsTabs/Appearance.cs | 5 +++++ 4 files changed, 21 insertions(+) diff --git a/HellionChat/Resources/HellionStrings.Designer.cs b/HellionChat/Resources/HellionStrings.Designer.cs index bec7fff..bfbead7 100644 --- a/HellionChat/Resources/HellionStrings.Designer.cs +++ b/HellionChat/Resources/HellionStrings.Designer.cs @@ -319,4 +319,8 @@ internal class HellionStrings // Hellion Chat — v1.2.0 Bottom-Status-Bar Privacy-Badge labels internal static string StatusBar_Privacy_Enabled => Get(nameof(StatusBar_Privacy_Enabled)); internal static string StatusBar_Privacy_Open => Get(nameof(StatusBar_Privacy_Open)); + + // Hellion Chat — v1.2.0 Appearance / Compact-Density toggle + internal static string Appearance_UseCompactDensity_Name => Get(nameof(Appearance_UseCompactDensity_Name)); + internal static string Appearance_UseCompactDensity_Description => Get(nameof(Appearance_UseCompactDensity_Description)); } diff --git a/HellionChat/Resources/HellionStrings.de.resx b/HellionChat/Resources/HellionStrings.de.resx index 5adeb67..eea3036 100644 --- a/HellionChat/Resources/HellionStrings.de.resx +++ b/HellionChat/Resources/HellionStrings.de.resx @@ -722,4 +722,10 @@ Offen + + Kompakte Dichte + + + Schaltet das Message-Layout vom Card-Row-Default zurück auf einzeilige `[HH:mm] Sender: Text` Zeilen. + diff --git a/HellionChat/Resources/HellionStrings.resx b/HellionChat/Resources/HellionStrings.resx index 86d829d..7616d54 100644 --- a/HellionChat/Resources/HellionStrings.resx +++ b/HellionChat/Resources/HellionStrings.resx @@ -722,4 +722,10 @@ Open + + Compact Density + + + Schaltet das Message-Layout vom Card-Row-Default zurück auf einzeilige `[HH:mm] Sender: Text` Zeilen. + diff --git a/HellionChat/Ui/SettingsTabs/Appearance.cs b/HellionChat/Ui/SettingsTabs/Appearance.cs index ce09f4c..5cde2ee 100644 --- a/HellionChat/Ui/SettingsTabs/Appearance.cs +++ b/HellionChat/Ui/SettingsTabs/Appearance.cs @@ -356,6 +356,11 @@ internal sealed class Appearance : ISettingsTab ImGui.Checkbox(Language.Options_MoreCompactPretty_Name, ref Mutable.MoreCompactPretty); ImGuiUtil.HelpMarker(Language.Options_MoreCompactPretty_Description); + // v1.2.0 — Card-Rows als Default. Compact-Density schaltet auf den + // klassischen Single-Line-Mode `[HH:mm] Sender: Text` zurück. + ImGui.Checkbox(HellionStrings.Appearance_UseCompactDensity_Name, ref Mutable.UseCompactDensity); + ImGuiUtil.HelpMarker(HellionStrings.Appearance_UseCompactDensity_Description); + ImGui.Checkbox(Language.Options_HideSameTimestamps_Name, ref Mutable.HideSameTimestamps); ImGuiUtil.HelpMarker(Language.Options_HideSameTimestamps_Description); } From d485f5ea1fad1f952cff6797d3f7308d3fed3e98 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 19:44:37 +0200 Subject: [PATCH 13/27] feat(messages): card-row default render with compact-density opt-out --- HellionChat/Ui/ChatLogWindow.cs | 64 ++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 9 deletions(-) diff --git a/HellionChat/Ui/ChatLogWindow.cs b/HellionChat/Ui/ChatLogWindow.cs index 1c826cf..9a83e00 100644 --- a/HellionChat/Ui/ChatLogWindow.cs +++ b/HellionChat/Ui/ChatLogWindow.cs @@ -1323,17 +1323,63 @@ public sealed class ChatLogWindow : Window ImGui.TableNextColumn(); var lineWidth = ImGui.GetContentRegionAvail().X; - if (message.Sender.Count > 0) - { - DrawChunks(message.Sender, true, handler, lineWidth); - ImGui.SameLine(); - } - // We need to draw something otherwise the item visibility check below won't work. - if (message.Content.Count == 0) - DrawChunks([new TextChunk(ChunkSource.Content, null, " ")], true, handler, lineWidth); + // v1.2.0 — Card-Rows als Default, Compact-Density als Opt-Out. + // Card-Mode: Sender-Header in Channel-Color auf eigener Zeile, + // dann Body, dann subtile Border-Bottom als Card-Trenner. + // Compact-Mode: bisheriges Verhalten — Sender + Space + Content + // auf einer Zeile via SameLine. + var useCard = !Plugin.Config.UseCompactDensity; + if (useCard) + { + if (message.Sender.Count > 0) + { + var theme = Plugin.ThemeRegistry.Active; + var senderColor = Plugin.Functions.Chat.GetChannelColor(message.Code.Type) + ?? theme.Colors.TextPrimary; + using (ImRaii.PushColor(ImGuiCol.Text, ColourUtil.RgbaToAbgr(senderColor))) + { + DrawChunks(message.Sender, true, handler, lineWidth); + } + // KEIN SameLine — Body landet auf eigener Zeile. + } + + // We need to draw something otherwise the item visibility check below won't work. + if (message.Content.Count == 0) + DrawChunks([new TextChunk(ChunkSource.Content, null, " ")], true, handler, lineWidth); + else + DrawChunks(message.Content, true, handler, lineWidth); + + // Subtile Border-Bottom als Card-Trenner. Border-Farbe mit + // reduzierter Alpha (RGBA → 0x33) für dezente Trennung. + { + var theme = Plugin.ThemeRegistry.Active; + var rowEndY = ImGui.GetCursorScreenPos().Y; + var winLeft = ImGui.GetWindowPos().X; + var winRight = winLeft + ImGui.GetWindowSize().X; + var borderRgba = (theme.Colors.Border & 0xFFFFFF00u) | 0x33u; + ImGui.GetWindowDrawList().AddLine( + new Vector2(winLeft + 4, rowEndY - 1), + new Vector2(winRight - 4, rowEndY - 1), + ColourUtil.RgbaToAbgr(borderRgba), + 1f); + ImGui.Dummy(new Vector2(0, 2)); + } + } else - DrawChunks(message.Content, true, handler, lineWidth); + { + if (message.Sender.Count > 0) + { + DrawChunks(message.Sender, true, handler, lineWidth); + ImGui.SameLine(); + } + + // We need to draw something otherwise the item visibility check below won't work. + if (message.Content.Count == 0) + DrawChunks([new TextChunk(ChunkSource.Content, null, " ")], true, handler, lineWidth); + else + DrawChunks(message.Content, true, handler, lineWidth); + } message.IsVisible[tab.Identifier] = ImGui.IsItemVisible(); } From af5f4d380a7c684de8e2ee26a10fb79b8ce164f3 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 19:51:29 +0200 Subject: [PATCH 14/27] =?UTF-8?q?feat(config):=20migration=20v14=20?= =?UTF-8?q?=E2=86=92=20v15,=20removed=20legacy=20theme=20fields=20and=20Ap?= =?UTF-8?q?pearance=20bindings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HellionChat/Configuration.cs | 19 +--------- HellionChat/Plugin.cs | 19 ++++++++-- HellionChat/Ui/SettingsTabs/Appearance.cs | 42 ++++------------------- 3 files changed, 23 insertions(+), 57 deletions(-) diff --git a/HellionChat/Configuration.cs b/HellionChat/Configuration.cs index 557e8cb..d874a17 100755 --- a/HellionChat/Configuration.cs +++ b/HellionChat/Configuration.cs @@ -34,7 +34,7 @@ public class ConfigKeyBind [Serializable] public class Configuration : IPluginConfiguration { - private const int LatestVersion = 14; + private const int LatestVersion = 15; public int Version { get; set; } = LatestVersion; @@ -80,19 +80,6 @@ public class Configuration : IPluginConfiguration // ChatTwo users skip it because the v6→v7 migration sets the flag. public bool FirstRunCompleted; - // Hellion Chat global ImGui theme — applied to every plugin window in - // Plugin.Draw. Default ON; users who prefer the upstream Dalamud look - // can flip this off in the Privacy tab. - [Obsolete("Replaced by Theme slug + WindowOpacity in v14")] - public bool HellionThemeEnabled = true; - - // Window background opacity, 0.5–1.0. Lower values make the plugin - // panes more glass-like so the game shines through. Default 0.5 - // matches the maintainer's daily-driver preference; users who want - // a less translucent look bump it up in Aussehen → Theme. - [Obsolete("Replaced by WindowOpacity in v14")] - public float HellionThemeWindowOpacity = 0.5f; - // Use the bundled Exo 2 font (OFL-1.1) for the regular plugin font // instead of whatever GlobalFontV2.FontId points at. Default ON so a // fresh install gets the Hellion typography out-of-the-box; flip OFF @@ -336,10 +323,6 @@ public class Configuration : IPluginConfiguration RetentionLastRunAt = other.RetentionLastRunAt; FirstRunCompleted = other.FirstRunCompleted; -#pragma warning disable CS0612, CS0618 // Obsolete-Felder bleiben bis v1.2.0 als JSON-Safety-Net erhalten - HellionThemeEnabled = other.HellionThemeEnabled; - HellionThemeWindowOpacity = other.HellionThemeWindowOpacity; -#pragma warning restore CS0612, CS0618 UseHellionFont = other.UseHellionFont; // v1.1.0 theme engine fields diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index 2b58150..8a2f153 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -247,9 +247,8 @@ public sealed class Plugin : IDalamudPlugin if (Config.Version < 14) { Config.Theme = "hellion-arctic"; - #pragma warning disable CS0612, CS0618 // Obsolete: HellionThemeWindowOpacity bleibt readable bis v1.2.0 - Config.WindowOpacity = Config.HellionThemeWindowOpacity; - #pragma warning restore CS0612, CS0618 + // v1.2.0: alter Opacity-Wert wird nicht mehr migriert (Field entfernt). + // User die direkt v13 → v15 springen bekommen den Default 0.85. Config.ReduceMotion = false; Config.UseCompactDensity = false; Config.ShowThemeQuickPicker = false; @@ -260,6 +259,20 @@ public sealed class Plugin : IDalamudPlugin "pick chat2-classic in Settings → Themes for the upstream look"); } + if (Config.Version < 15) + { + // v1.2.0 — keine Datenmigration nötig. Removal der deprecated + // Theme-Felder ist reine Schema-Bereinigung (System.Text.Json + // ignoriert unbekannte Felder im JSON, daher kein Crash bei + // Configs die noch HellionThemeEnabled/HellionThemeWindowOpacity + // serialisiert haben — die Werte verfallen einfach). + Config.Version = 15; + SaveConfig(); + Log.Information( + "Migrated config v14 → v15: legacy theme fields removed " + + "(HellionThemeEnabled, HellionThemeWindowOpacity)"); + } + // Hellion v1.0.0 default tab layout. Five thematically separated // tabs: General catches the immediate-surroundings public chat // (Say/Yell/Shout) only; System absorbs the rest of the technical diff --git a/HellionChat/Ui/SettingsTabs/Appearance.cs b/HellionChat/Ui/SettingsTabs/Appearance.cs index 5cde2ee..ba07ee3 100644 --- a/HellionChat/Ui/SettingsTabs/Appearance.cs +++ b/HellionChat/Ui/SettingsTabs/Appearance.cs @@ -45,32 +45,11 @@ internal sealed class Appearance : ISettingsTab using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) { - // v1.1.0 — Diese Settings-UI wird in Phase J durch den dedizierten - // Themes-Tab ersetzt. Bis dahin bleiben die alten Toggles erhalten, - // damit die Settings-Seite kompiliert; sie schreiben in die mit - // [Obsolete] markierten Felder, die bis v1.2.0 als JSON-Safety-Net - // bestehen bleiben. Das pragma unterdrückt die CS0612-Warnungen - // gezielt für diesen Übergangs-Block. -#pragma warning disable CS0612, CS0618 - ImGui.Checkbox(HellionStrings.Theme_Enabled_Name, ref Mutable.HellionThemeEnabled); - ImGuiUtil.HelpMarker(HellionStrings.Theme_Enabled_Description); - - // Clamp 0.5–1.0 stays consistent with Privacy.cs which already - // shipped this slider; lower values would let chat windows - // disappear behind game UI. - using (ImRaii.Disabled(!Mutable.HellionThemeEnabled)) - { - ImGui.SetNextItemWidth(200f * ImGuiHelpers.GlobalScale); - var opacity = Mutable.HellionThemeWindowOpacity; - if (ImGui.SliderFloat($"{HellionStrings.Theme_WindowOpacity_Label}##theme-opacity", ref opacity, 0.5f, 1.0f, "%.2f")) - { - Mutable.HellionThemeWindowOpacity = Math.Clamp(opacity, 0.5f, 1.0f); - } - ImGuiUtil.HelpMarker(HellionStrings.Theme_WindowOpacity_Help); - } - - ImGui.Spacing(); - + // v1.2.0 — Legacy HellionThemeEnabled/HellionThemeWindowOpacity-Bindings + // entfernt. Theme-Auswahl + globale Window-Opacity leben jetzt in + // Settings → Themes (eingeführt mit v1.1.0). Hier verbleibt nur der + // klassische OverrideStyle-Toggle plus der Bestand-WindowAlpha-Slider + // für das Chat-Log-Fenster. ImGui.Checkbox(Language.Options_OverrideStyle_Name, ref Mutable.OverrideStyle); ImGuiUtil.HelpMarker(Language.Options_OverrideStyle_Name_Desc); @@ -79,16 +58,7 @@ internal sealed class Appearance : ISettingsTab DrawStyleCombo(); } - // The Bestand-Slider WindowAlpha targets the chat log window's - // background only. The Hellion theme opacity above already covers - // every plugin window globally, so the two sliders fight each - // other when the theme is active. Disable the legacy slider in - // that case to make Hellion theme the single source of truth. - using (ImRaii.Disabled(Mutable.HellionThemeEnabled)) - { - ImGuiUtil.DragFloatVertical(Language.Options_WindowOpacity_Name, ref Mutable.WindowAlpha, .25f, 0f, 100f, $"{Mutable.WindowAlpha:N2}%%", ImGuiSliderFlags.AlwaysClamp); - } -#pragma warning restore CS0612, CS0618 + ImGuiUtil.DragFloatVertical(Language.Options_WindowOpacity_Name, ref Mutable.WindowAlpha, .25f, 0f, 100f, $"{Mutable.WindowAlpha:N2}%%", ImGuiSliderFlags.AlwaysClamp); } } From 3e98b9103f8c68442a8671f638722a648bfa63b0 Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 19:54:56 +0200 Subject: [PATCH 15/27] =?UTF-8?q?chore(release):=20bump=20to=20v1.2.0=20?= =?UTF-8?q?=E2=80=94=20layout=20refresh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HellionChat/HellionChat.csproj | 2 +- HellionChat/HellionChat.yaml | 48 ++++++++++++++++++++++++++++++++++ docs/CHANGELOG.md | 18 +++++++++++++ repo.json | 10 +++---- 4 files changed, 72 insertions(+), 6 deletions(-) diff --git a/HellionChat/HellionChat.csproj b/HellionChat/HellionChat.csproj index 1771783..96cfba1 100644 --- a/HellionChat/HellionChat.csproj +++ b/HellionChat/HellionChat.csproj @@ -4,7 +4,7 @@ 0.1.0 is our bootstrap release; the underlying Chat 2 base is called out in the yaml changelog so users can see what it derives from. --> - 1.1.0 + 1.2.0 enable enable