feat(keybind): cycle tabs and switch channel with pill sync

This commit is contained in:
2026-06-16 01:21:05 +02:00
parent 88491902eb
commit 3878869904
3 changed files with 55 additions and 5 deletions
+25 -5
View File
@@ -501,20 +501,40 @@ internal unsafe class KeybindManager : IDisposable
return;
Plugin.KeyState[currentBest.Item1] = false;
if (!KeybindsToIntercept.ContainsKey(currentBest.Item2))
if (!KeybindsToIntercept.TryGetValue(currentBest.Item2, out var info))
return;
// Re-surface the chat-activation entry point retired in v1.6.0: a chat-open
// keybind shows + focuses the window, restoring it from a user-hide or a
// closed state. Channel/prefill routing from the bind stays out of scope.
// closed state.
Plugin.Instance.MainWindow?.ActivateChat();
// Direct channel-switch binds (CMD_SAY/PARTY/numbered linkshells/…): switch the
// game channel AND mirror it onto the active tab so the input pill shows the
// real send target (pill-sync, Flo decision 2026-06-15). Rotation binds (REPLY /
// linkshell-cycle, Rotate != None) and the Permanent nuance stay deferred to the
// keybind-routing follow-cycle.
if (info.Channel is { } channel && info.Rotate == RotateMode.None)
{
Plugin.Instance.Functions.Chat.SetChannel(channel);
if (Plugin.Instance.MainWindow?.ActiveTab is { } activeTab)
{
activeTab.CurrentChannel.SetChannel(channel);
activeTab.CurrentChannel.TellTarget = null;
activeTab.CurrentChannel.ResetTempChannel();
}
}
// Prefill text binds (CMD_COMMAND seeds "/"): drop the token into our input.
if (info.Text is { } text)
Plugin.Instance.InputBar.SetPendingMessage(text);
}
// Tab-cycle dispatch is offline until the new chat layer surfaces a
// ChangeTabDelta entry point and pop-out input bars come back online.
// Cycle the main window's active tab. Pop-out input-bar focus-forward stays
// deferred (no focus contract yet) — main-window tabs only.
private void DispatchTabDelta(int delta)
{
_ = delta;
Plugin.Instance.MainWindow?.ChangeTabDelta(delta);
}
private static Keybind GetKeybind(string id)
+19
View File
@@ -160,6 +160,25 @@ internal sealed class MainWindow : Window
TabLifecycleHelpers.OnTabActivated(tab, previous);
}
// Tab-cycle entry point for the ChatTabForward/Backward keybinds. Empty list is a
// no-op; a null active tab seeds tabs[0]; a single-tab cycle that lands on the
// already-active tab is a no-op (ActivateTab early-returns on the same reference).
// Routes through ActivateTab so the cycle strips stale tell state + re-derives the
// channel exactly like a sidebar/top-tab click. Pop-out focus-forward stays
// deferred (no focus contract) — main-window tabs only.
internal void ChangeTabDelta(int delta)
{
var tabs = Plugin.Config.Tabs;
if (tabs.Count == 0)
return;
var idx = _activeTab is null ? 0 : tabs.IndexOf(_activeTab);
if (idx < 0)
idx = 0; // active tab not in the list (mid-strip) -> start from the first
ActivateTab(tabs[TabLifecycleHelpers.WrapTabIndex(idx, delta, tabs.Count)]);
}
// Internal accessors for self-tests so the probes can reach the live
// component without exposing them as public surface.
internal Components.Sidebar GetSidebarForSelfTest() => _sidebar;
+11
View File
@@ -83,4 +83,15 @@ internal static class TabLifecycleHelpers
tab.CurrentChannel.TellTarget = null;
tab.CurrentChannel.ResetTempChannel();
}
// Wrap-around tab index for keybind cycling. Pure so the Build-Suite can test the
// wrap math without a live window. count == 0 returns 0 (the caller dead-zones
// before activating); negative deltas wrap correctly via the double-mod.
// TEST-MIRROR: ../../Hellion Build test/_Helpers/TabLifecycleHelpersTests.cs
internal static int WrapTabIndex(int current, int delta, int count)
{
if (count <= 0)
return 0;
return ((current + delta) % count + count) % count;
}
}