Restore pop-out exclusivity: hide popped tabs from main window + keybind/unread parity
This commit is contained in:
@@ -231,7 +231,8 @@ internal static class PluginHostFactory
|
|||||||
sp.GetRequiredService<Ui.Components.MessageList>(),
|
sp.GetRequiredService<Ui.Components.MessageList>(),
|
||||||
sp.GetRequiredService<Ui.Components.InputBar>(),
|
sp.GetRequiredService<Ui.Components.InputBar>(),
|
||||||
sp.GetRequiredService<Ui.Components.StatusBar>(),
|
sp.GetRequiredService<Ui.Components.StatusBar>(),
|
||||||
sp.GetRequiredService<Lender<PayloadHandler>>()
|
sp.GetRequiredService<Lender<PayloadHandler>>(),
|
||||||
|
sp.GetRequiredService<Ui.Windows.ChannelPopoutPool>()
|
||||||
));
|
));
|
||||||
services.AddSingleton(sp => new Integrations.FailedTellNotifier(
|
services.AddSingleton(sp => new Integrations.FailedTellNotifier(
|
||||||
sp.GetRequiredService<ILogger<Integrations.FailedTellNotifier>>()
|
sp.GetRequiredService<ILogger<Integrations.FailedTellNotifier>>()
|
||||||
|
|||||||
@@ -134,7 +134,10 @@ internal sealed class Sidebar
|
|||||||
// the tab list itself stays untouched and every row keeps its
|
// the tab list itself stays untouched and every row keeps its
|
||||||
// ORIGINAL list index for PushID, so an open context-menu popup
|
// ORIGINAL list index for PushID, so an open context-menu popup
|
||||||
// stays bound to its tab when sectioning moves it visually.
|
// stays bound to its tab when sectioning moves it visually.
|
||||||
var renderOrder = BuildRenderOrder(tabs);
|
var renderOrder = TabLifecycleHelpers.BuildRenderOrder(
|
||||||
|
tabs,
|
||||||
|
t => _pool.IsOpen(t.Identifier)
|
||||||
|
);
|
||||||
var pinnedHeaderRendered = false;
|
var pinnedHeaderRendered = false;
|
||||||
var unpinnedHeaderRendered = false;
|
var unpinnedHeaderRendered = false;
|
||||||
foreach (var i in renderOrder)
|
foreach (var i in renderOrder)
|
||||||
@@ -186,30 +189,6 @@ internal sealed class Sidebar
|
|||||||
LastDrawnSectionHeaderCount++;
|
LastDrawnSectionHeaderCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mirror of 1.5.6's BuildSidebarRenderOrder: returns indices into the
|
|
||||||
// live tab list grouped by section, so the list order itself is never
|
|
||||||
// mutated and headers gate on the first tab actually reached per pool
|
|
||||||
// (an empty pool draws neither separator nor header).
|
|
||||||
private static List<int> BuildRenderOrder(IList<Tab> tabs)
|
|
||||||
{
|
|
||||||
var persistent = new List<int>(tabs.Count);
|
|
||||||
var pinned = new List<int>();
|
|
||||||
var unpinned = new List<int>();
|
|
||||||
for (var i = 0; i < tabs.Count; i++)
|
|
||||||
{
|
|
||||||
if (TabLifecycleHelpers.IsInPinnedPool(tabs[i]))
|
|
||||||
pinned.Add(i);
|
|
||||||
else if (TabLifecycleHelpers.IsInUnpinnedPool(tabs[i]))
|
|
||||||
unpinned.Add(i);
|
|
||||||
else
|
|
||||||
persistent.Add(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
persistent.AddRange(pinned);
|
|
||||||
persistent.AddRange(unpinned);
|
|
||||||
return persistent;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawRow(
|
private void DrawRow(
|
||||||
Tab tab,
|
Tab tab,
|
||||||
int index,
|
int index,
|
||||||
|
|||||||
@@ -18,11 +18,18 @@ internal sealed class TopTabBar
|
|||||||
|
|
||||||
public void Draw(IList<Tab> tabs, ref Tab? activeTab)
|
public void Draw(IList<Tab> tabs, ref Tab? activeTab)
|
||||||
{
|
{
|
||||||
|
var firstDrawn = true;
|
||||||
for (var i = 0; i < tabs.Count; i++)
|
for (var i = 0; i < tabs.Count; i++)
|
||||||
{
|
{
|
||||||
var tab = tabs[i];
|
var tab = tabs[i];
|
||||||
if (i > 0)
|
// POP-1b: 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();
|
ImGui.SameLine();
|
||||||
|
firstDrawn = false;
|
||||||
|
|
||||||
var selected = ReferenceEquals(tab, activeTab);
|
var selected = ReferenceEquals(tab, activeTab);
|
||||||
// Size the selectable to its own label width. A zero width makes ImGui
|
// Size the selectable to its own label width. A zero width makes ImGui
|
||||||
|
|||||||
@@ -111,6 +111,11 @@ internal sealed class ChannelPopoutWindow : Window, IFocusableChatWindow
|
|||||||
if (Bound is null)
|
if (Bound is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// POP-1f: the bound tab is live-visible in this pop-out, so it carries no
|
||||||
|
// unread badge — mirror MainWindow's per-frame zero for the active tab.
|
||||||
|
// View-state reset only (tab.Messages store is untouched).
|
||||||
|
Bound.Unread = 0;
|
||||||
|
|
||||||
var inputHeight = InputBar.Height;
|
var inputHeight = InputBar.Height;
|
||||||
using (
|
using (
|
||||||
var body = ImRaii.Child(
|
var body = ImRaii.Child(
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ internal sealed class MainWindow : Window, IFocusableChatWindow
|
|||||||
private readonly Components.InputBar _input;
|
private readonly Components.InputBar _input;
|
||||||
private readonly Components.StatusBar _status;
|
private readonly Components.StatusBar _status;
|
||||||
private readonly Lender<PayloadHandler> _handlerLender;
|
private readonly Lender<PayloadHandler> _handlerLender;
|
||||||
|
private readonly ChannelPopoutPool _pool;
|
||||||
|
|
||||||
private Tab? _activeTab;
|
private Tab? _activeTab;
|
||||||
|
|
||||||
@@ -51,7 +52,8 @@ internal sealed class MainWindow : Window, IFocusableChatWindow
|
|||||||
Components.MessageList messages,
|
Components.MessageList messages,
|
||||||
Components.InputBar input,
|
Components.InputBar input,
|
||||||
Components.StatusBar status,
|
Components.StatusBar status,
|
||||||
Lender<PayloadHandler> handlerLender
|
Lender<PayloadHandler> handlerLender,
|
||||||
|
ChannelPopoutPool pool
|
||||||
)
|
)
|
||||||
: base($"{Plugin.PluginName}###hellion-main")
|
: base($"{Plugin.PluginName}###hellion-main")
|
||||||
{
|
{
|
||||||
@@ -62,6 +64,7 @@ internal sealed class MainWindow : Window, IFocusableChatWindow
|
|||||||
_input = input;
|
_input = input;
|
||||||
_status = status;
|
_status = status;
|
||||||
_handlerLender = handlerLender;
|
_handlerLender = handlerLender;
|
||||||
|
_pool = pool;
|
||||||
|
|
||||||
Size = new Vector2(DefaultWidth, DefaultHeight);
|
Size = new Vector2(DefaultWidth, DefaultHeight);
|
||||||
SizeCondition = ImGuiCond.FirstUseEver;
|
SizeCondition = ImGuiCond.FirstUseEver;
|
||||||
@@ -176,7 +179,13 @@ internal sealed class MainWindow : Window, IFocusableChatWindow
|
|||||||
if (idx < 0)
|
if (idx < 0)
|
||||||
idx = 0; // active tab not in the list (mid-strip) -> start from the first
|
idx = 0; // active tab not in the list (mid-strip) -> start from the first
|
||||||
|
|
||||||
ActivateTab(tabs[TabLifecycleHelpers.WrapTabIndex(idx, delta, tabs.Count)]);
|
var nextIndex = TabLifecycleHelpers.NextMainTabIndex(
|
||||||
|
idx,
|
||||||
|
delta,
|
||||||
|
tabs,
|
||||||
|
t => _pool.IsOpen(t.Identifier)
|
||||||
|
);
|
||||||
|
ActivateTab(tabs[nextIndex]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Internal accessors for self-tests so the probes can reach the live
|
// Internal accessors for self-tests so the probes can reach the live
|
||||||
@@ -270,6 +279,25 @@ internal sealed class MainWindow : Window, IFocusableChatWindow
|
|||||||
TabLifecycleHelpers.OnTabActivated(reseed, active);
|
TabLifecycleHelpers.OnTabActivated(reseed, active);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POP-1c: a popped-out tab must not stay the main window's active surface
|
||||||
|
// (1.5.6 exclusivity). Re-anchor to the first non-popped tab the moment the
|
||||||
|
// active one is popped; null when every tab is popped (POP-1d guards Draw).
|
||||||
|
// Runs post-seed, before the sidebar/top-tab draw, so the popped tab never
|
||||||
|
// renders. Idempotent: PickMainActiveTab returns the same reference once
|
||||||
|
// settled, so OnTabActivated fires only on the pop frame.
|
||||||
|
var visibleActive = TabLifecycleHelpers.PickMainActiveTab(
|
||||||
|
_activeTab,
|
||||||
|
Plugin.Config.Tabs,
|
||||||
|
t => _pool.IsOpen(t.Identifier)
|
||||||
|
);
|
||||||
|
if (!ReferenceEquals(visibleActive, _activeTab))
|
||||||
|
{
|
||||||
|
var previousActive = _activeTab;
|
||||||
|
_activeTab = visibleActive;
|
||||||
|
if (visibleActive is not null)
|
||||||
|
TabLifecycleHelpers.OnTabActivated(visibleActive, previousActive);
|
||||||
|
}
|
||||||
|
|
||||||
// The active tab's messages are on screen, so it carries no unread badge
|
// The active tab's messages are on screen, so it carries no unread badge
|
||||||
// (1.5.6 convention: zero the current tab every frame so the dot only ever
|
// (1.5.6 convention: zero the current tab every frame so the dot only ever
|
||||||
// shows on tabs you are NOT looking at).
|
// shows on tabs you are NOT looking at).
|
||||||
@@ -336,8 +364,8 @@ internal sealed class MainWindow : Window, IFocusableChatWindow
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (messages.Success)
|
if (messages.Success && _activeTab is not null)
|
||||||
_messages.Draw(_activeTab!);
|
_messages.Draw(_activeTab);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inside-mode inline render: measure first so PreviewHeight is fresh
|
// Inside-mode inline render: measure first so PreviewHeight is fresh
|
||||||
|
|||||||
@@ -107,4 +107,82 @@ internal static class TabLifecycleHelpers
|
|||||||
return 0;
|
return 0;
|
||||||
return ((current + delta) % count + count) % count;
|
return ((current + delta) % count + count) % count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sectioned sidebar render order (1.5.6 parity): persistent → pinned TempTabs →
|
||||||
|
// unpinned TempTabs. Returns indices into the live tab list so the list order is
|
||||||
|
// never mutated and DrawRow keeps each tab's ORIGINAL index for PushID. POP-1a:
|
||||||
|
// isPoppedOut excludes tabs bound to a pop-out window — an excluded tab draws no
|
||||||
|
// row and its pool's section header gates on the first tab actually reached. Pure
|
||||||
|
// + Dalamud-free so the Build-Suite can pin it.
|
||||||
|
// TEST-MIRROR: ../../../Hellion Build test/_Helpers/SidebarRenderOrderTests.cs
|
||||||
|
internal static List<int> BuildRenderOrder(IList<Tab> tabs, Func<Tab, bool> isPoppedOut)
|
||||||
|
{
|
||||||
|
var persistent = new List<int>(tabs.Count);
|
||||||
|
var pinned = new List<int>();
|
||||||
|
var unpinned = new List<int>();
|
||||||
|
for (var i = 0; i < tabs.Count; i++)
|
||||||
|
{
|
||||||
|
if (isPoppedOut(tabs[i]))
|
||||||
|
continue;
|
||||||
|
if (IsInPinnedPool(tabs[i]))
|
||||||
|
pinned.Add(i);
|
||||||
|
else if (IsInUnpinnedPool(tabs[i]))
|
||||||
|
unpinned.Add(i);
|
||||||
|
else
|
||||||
|
persistent.Add(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
persistent.AddRange(pinned);
|
||||||
|
persistent.AddRange(unpinned);
|
||||||
|
return persistent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// POP-1c: returns the tab the main window should display — the current tab if it
|
||||||
|
// is not popped out, else the FIRST non-popped tab in list order, else null when
|
||||||
|
// every tab is popped. NOTE: deliberately NOT ResetActiveTabIfRemoved's
|
||||||
|
// unconditional Tabs[0] — Tabs[0] may itself be popped and would re-trigger the
|
||||||
|
// re-anchor every frame. Pure + Dalamud-free.
|
||||||
|
// TEST-MIRROR: ../../../Hellion Build test/_Helpers/PickMainActiveTabTests.cs
|
||||||
|
internal static Tab? PickMainActiveTab(
|
||||||
|
Tab? current,
|
||||||
|
IList<Tab> tabs,
|
||||||
|
Func<Tab, bool> isPoppedOut
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (current is not null && !isPoppedOut(current))
|
||||||
|
return current;
|
||||||
|
foreach (var tab in tabs)
|
||||||
|
if (!isPoppedOut(tab))
|
||||||
|
return tab;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// POP-1e: popout-aware sibling of WrapTabIndex for the ChatTabForward/Backward
|
||||||
|
// keybind. Steps from current by delta's sign (±1), wrapping, and returns the
|
||||||
|
// first index whose tab is NOT popped out within tabs.Count steps; returns current
|
||||||
|
// when every other tab is popped (no-op), the list is empty, or a single tab.
|
||||||
|
// Skipping popped tabs here is what stops the POP-1c re-anchor from making the
|
||||||
|
// cycle stick (CYCLE-1). Pure + Dalamud-free.
|
||||||
|
// TEST-MIRROR: ../../../Hellion Build test/_Helpers/NextMainTabIndexTests.cs
|
||||||
|
internal static int NextMainTabIndex(
|
||||||
|
int current,
|
||||||
|
int delta,
|
||||||
|
IList<Tab> tabs,
|
||||||
|
Func<Tab, bool> isPoppedOut
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (tabs.Count == 0)
|
||||||
|
return current;
|
||||||
|
var step = delta >= 0 ? 1 : -1;
|
||||||
|
var idx = current;
|
||||||
|
for (var n = 0; n < tabs.Count; n++)
|
||||||
|
{
|
||||||
|
idx = ((idx + step) % tabs.Count + tabs.Count) % tabs.Count;
|
||||||
|
if (idx == current)
|
||||||
|
break;
|
||||||
|
if (!isPoppedOut(tabs[idx]))
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user