Reported by Carla: a tell arriving while you are typing pulls the focus away. The interruption is the visible half. The sharp half is that the input buffer belongs to the WINDOW while the send target is read off whatever tab is active at Enter -- so a line typed at one person could leave addressed to whoever just wrote, and in this game losing the keyboard means the next sentence walks the character around. Nothing is revealed now while any chat surface is mid-sentence, in any mode. The tab still appears and still carries its unread mark. The check lives in its own file because the answer has to be identical everywhere: it started inside the reveal plan, and a second pop-out path walked straight past it -- AutoTellTabsService opened windows off its own flag, at tab creation, a tick before the router was ever asked. Those two paths are one now. AutoTellTabsOpenAsPopout and TellAutoOpenMode were two settings for one decision, and the older one won every race, which is why the other looked inert. Config schema 28 carries the old flag forward so nobody's behaviour changes. "Off" went with it: it never stopped the tab from being created -- that is the auto-tell switch -- it only stopped the jump to it, which is what the switch below it does. Also in here, all from the same corner of the code: - Closing a tab was lost in the v2.0.0 rebuild. The trash entry lived in the retired ChatLogWindow menu, and the rebuilt one restored rename, sound, pop-out and pinning but not this. For tell tabs that left no way out at all: IsEditable keeps them out of the settings editor on purpose and points at the context menu, which could not close them either. Pinned tell tabs stay disabled with a tooltip rather than absent. - Re-anchoring the active tab used an unconditional Tabs[0] in three places, and Tabs[0] can be popped out -- so it ran OnTabActivated over a tab live in its own window and stripped its tell binding. With every tab popped, the seed and the re-anchor also fought each other every frame. - PinTab_LimitReached still pointed at "Promote to permanent", removed in May. Spanish said "Desija", which is not a word; Greek left "tell tabs" untranslated; pt-PT broke its own unpin verb. - Pop Out was a hardcoded English literal despite the key existing in all 25 languages since v1.5.6, and the tell-open modes were the last English display names in the plugin. - Segmented setting rows measured 200px flat, which cut German labels in half. They size to their longest label now. - Metrics.Scale still called GlobalScaleSafe. It is an alias for GlobalScale in current Dalamud, and dropping it clears the last compiler warning in the project.
190 lines
7.0 KiB
C#
190 lines
7.0 KiB
C#
using System.Numerics;
|
|
using Dalamud.Bindings.ImGui;
|
|
using HellionChat.Themes;
|
|
using HellionChat.Ui.StyleEngine;
|
|
using HellionChat.Ui.StyleEngine.Widgets;
|
|
using HellionChat.Util;
|
|
|
|
namespace HellionChat.Ui.Components;
|
|
|
|
// Horizontal tab strip — the alternative MainWindow layout to the Sidebar.
|
|
// Selection drives the same shared EnsureCurrentChannel path; pop-out is the
|
|
// same pool.TryOpen affordance as the sidebar (right-click context menu).
|
|
internal sealed class TopTabBar
|
|
{
|
|
private readonly Windows.ChannelPopoutPool _pool;
|
|
private readonly ThemeRegistry _themes;
|
|
private readonly WidgetPalette _palette;
|
|
|
|
// Slightly smaller than the sidebar default: the strip is horizontal, so
|
|
// every pixel of badge width costs a tab.
|
|
private static readonly BadgeStyle TabBadge = new() { Height = 12f, PaddingX = 3f };
|
|
|
|
// Render observability. At most one underline per frame, except in the frame
|
|
// a click lands: a tab drawn before the clicked one was still the active tab
|
|
// when it was painted, so that frame legitimately shows two.
|
|
internal int LastRenderedUnderlineCount { get; private set; }
|
|
|
|
public TopTabBar(Windows.ChannelPopoutPool pool, ThemeRegistry themes, TokenResolver resolver)
|
|
{
|
|
_pool = pool;
|
|
_themes = themes;
|
|
_palette = new WidgetPalette(resolver);
|
|
}
|
|
|
|
public void Draw(IReadOnlyList<Tab> tabs, ref Tab? activeTab)
|
|
{
|
|
LastRenderedUnderlineCount = 0;
|
|
|
|
var colors = _themes.Active.Colors;
|
|
var surfaceActive = _palette.Abgr(Token.SurfaceActive, colors);
|
|
var surfaceHover = _palette.Abgr(Token.SurfaceHover, colors);
|
|
var accent = _palette.Abgr(Token.AccentPrimary, colors);
|
|
var textAbgr = _palette.Abgr(Token.Text, colors);
|
|
var mutedAbgr = _palette.Abgr(Token.TextMuted, colors);
|
|
var borderAbgr = _palette.Abgr(Token.Border, colors);
|
|
|
|
var height = Metrics.TopTabHeight;
|
|
var padX = Metrics.TopTabPaddingX;
|
|
var dl = ImGui.GetWindowDrawList();
|
|
|
|
var firstDrawn = true;
|
|
for (var i = 0; i < tabs.Count; i++)
|
|
{
|
|
var tab = tabs[i];
|
|
// A popped-out tab is owned by its pop-out window, not the main
|
|
// strip (1.5.6 exclusivity). Gate on the pool, not Tab.PopOut (stale flag).
|
|
if (_pool.IsOpen(tab.Identifier))
|
|
continue;
|
|
|
|
if (!firstDrawn)
|
|
ImGui.SameLine();
|
|
firstDrawn = false;
|
|
|
|
var selected = ReferenceEquals(tab, activeTab);
|
|
var origin = ImGui.GetCursorScreenPos();
|
|
|
|
// The badge has to be part of the width, not painted over it. Padding
|
|
// alone is 10px and the badge is at least 14 wide, so placing it in
|
|
// the trailing padding covered the label on every tab that had one.
|
|
var showUnread =
|
|
!ReferenceEquals(tab, activeTab)
|
|
&& tab.UnreadMode != UnreadMode.None
|
|
&& tab.Unread > 0;
|
|
// Resolved once: measuring one string and drawing another would size
|
|
// every tab wrong the moment screenshot mode is on.
|
|
var label = Util.TabDisplayName.Resolve(
|
|
tab.Name,
|
|
tab.NameCameFromPartner,
|
|
Plugin.Config.ScreenshotMode
|
|
);
|
|
|
|
var unread = showUnread ? (int)Math.Min(tab.Unread, int.MaxValue) : 0;
|
|
var badgeSize = showUnread ? Badge.CalcSize(unread, TabBadge) : Vector2.Zero;
|
|
|
|
var width =
|
|
ImGui.CalcTextSize(label).X
|
|
+ padX * 2f
|
|
+ (showUnread ? badgeSize.X + Metrics.TopTabUnreadInset : 0f);
|
|
var size = WidgetGeometry.IconButton(width, height);
|
|
|
|
// The ### keeps the ImGui id stable across a rename; without it the
|
|
// context menu loses its binding the moment the label changes. Built
|
|
// once and reused for the hover key, since GetID hashes the same
|
|
// string the button registers under.
|
|
var buttonId = $"###hellion_toptab_{tab.Identifier}";
|
|
var hoverId = ImGui.GetID(buttonId);
|
|
var pressed = ImGui.InvisibleButton(buttonId, size);
|
|
var hovered = ImGui.IsItemHovered();
|
|
var hoverAmount = HoverState.Query(hoverId, hovered);
|
|
|
|
if (pressed)
|
|
{
|
|
var previous = activeTab;
|
|
activeTab = tab;
|
|
TabLifecycleHelpers.OnTabActivated(tab, previous);
|
|
selected = true;
|
|
}
|
|
|
|
DrawTab(
|
|
dl,
|
|
origin,
|
|
size,
|
|
label,
|
|
selected,
|
|
hoverAmount,
|
|
surfaceActive,
|
|
surfaceHover,
|
|
accent,
|
|
selected || hovered ? textAbgr : mutedAbgr,
|
|
padX
|
|
);
|
|
|
|
if (selected)
|
|
LastRenderedUnderlineCount++;
|
|
|
|
// Clicking a tab clears its marker in the same frame, like the
|
|
// sidebar: showUnread was resolved before the click was handled, so
|
|
// re-check against the post-click selection.
|
|
if (showUnread && !ReferenceEquals(tab, activeTab))
|
|
Badge.Draw(
|
|
new Vector2(
|
|
origin.X + size.X - badgeSize.X - Metrics.TopTabUnreadInset,
|
|
origin.Y + MetricsMath.CenterY(size.Y, badgeSize.Y)
|
|
),
|
|
unread,
|
|
_palette.Abgr(Token.AccentEmber, colors),
|
|
textAbgr,
|
|
TabBadge
|
|
);
|
|
|
|
TabContextMenu.Draw(tab, $"toptab_ctx_{tab.Identifier}", _pool, tabs);
|
|
}
|
|
|
|
LineDivider.Draw(null, borderAbgr, mutedAbgr);
|
|
}
|
|
|
|
// Keeps the fill Selectable used to provide (ImGuiCol.Header) and adds the
|
|
// underline on top. Dropping the fill for the underline alone would make the
|
|
// active tab harder to spot, not easier.
|
|
private static void DrawTab(
|
|
ImDrawListPtr dl,
|
|
Vector2 origin,
|
|
Vector2 size,
|
|
string label,
|
|
bool selected,
|
|
float hoverAmount,
|
|
uint surfaceActive,
|
|
uint surfaceHover,
|
|
uint accent,
|
|
uint labelAbgr,
|
|
float padX
|
|
)
|
|
{
|
|
var max = origin + size;
|
|
|
|
if (selected)
|
|
dl.AddRectFilled(origin, max, surfaceActive);
|
|
|
|
if (hoverAmount > 0f)
|
|
{
|
|
var a = (uint)
|
|
Math.Clamp(MathF.Round(((surfaceHover >> 24) & 0xFF) * hoverAmount), 0f, 255f);
|
|
dl.AddRectFilled(origin, max, (surfaceHover & 0x00FFFFFFu) | (a << 24));
|
|
}
|
|
|
|
if (selected)
|
|
{
|
|
var thickness = Metrics.TopTabUnderline;
|
|
dl.AddRectFilled(new Vector2(origin.X, max.Y - thickness), max, accent);
|
|
}
|
|
|
|
var textSize = ImGui.CalcTextSize(label);
|
|
dl.AddText(
|
|
new Vector2(origin.X + padX, origin.Y + MetricsMath.CenterY(size.Y, textSize.Y)),
|
|
labelAbgr,
|
|
label
|
|
);
|
|
}
|
|
}
|