feat(layout): add top-tabs layout mode and shared channel resolver
This commit is contained in:
@@ -201,9 +201,13 @@ internal static class PluginHostFactory
|
|||||||
sp.GetRequiredService<ThemeRegistry>(),
|
sp.GetRequiredService<ThemeRegistry>(),
|
||||||
sp.GetRequiredService<FontManager>()
|
sp.GetRequiredService<FontManager>()
|
||||||
));
|
));
|
||||||
|
services.AddSingleton(sp => new Ui.Components.TopTabBar(
|
||||||
|
sp.GetRequiredService<Ui.Windows.ChannelPopoutPool>()
|
||||||
|
));
|
||||||
services.AddSingleton(sp => new Ui.Windows.MainWindow(
|
services.AddSingleton(sp => new Ui.Windows.MainWindow(
|
||||||
sp.GetRequiredService<Ui.Components.HonorificHeader>(),
|
sp.GetRequiredService<Ui.Components.HonorificHeader>(),
|
||||||
sp.GetRequiredService<Ui.Components.Sidebar>(),
|
sp.GetRequiredService<Ui.Components.Sidebar>(),
|
||||||
|
sp.GetRequiredService<Ui.Components.TopTabBar>(),
|
||||||
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>(),
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
|
||||||
|
|
||||||
namespace HellionChat.Ui.Components.Settings.Tabs;
|
namespace HellionChat.Ui.Components.Settings.Tabs;
|
||||||
|
|
||||||
@@ -16,11 +15,16 @@ internal sealed class WindowTab
|
|||||||
{
|
{
|
||||||
if (ImGui.CollapsingHeader("Layout mode", ImGuiTreeNodeFlags.DefaultOpen))
|
if (ImGui.CollapsingHeader("Layout mode", ImGuiTreeNodeFlags.DefaultOpen))
|
||||||
{
|
{
|
||||||
// Sidebar is the v1.7.0 default; TopTabs is a v1.8.0 teaser.
|
var mode = Plugin.Config.MainWindowLayoutMode;
|
||||||
ImGui.RadioButton("Sidebar", true);
|
if (ImGui.RadioButton("Sidebar", mode == MainWindowLayoutMode.Sidebar))
|
||||||
using (ImRaii.Disabled(true))
|
|
||||||
{
|
{
|
||||||
ImGui.RadioButton("Top tabs (lands in v1.8.0)", false);
|
Plugin.Config.MainWindowLayoutMode = MainWindowLayoutMode.Sidebar;
|
||||||
|
_plugin.SaveConfig();
|
||||||
|
}
|
||||||
|
if (ImGui.RadioButton("Top tabs", mode == MainWindowLayoutMode.TopTabs))
|
||||||
|
{
|
||||||
|
Plugin.Config.MainWindowLayoutMode = MainWindowLayoutMode.TopTabs;
|
||||||
|
_plugin.SaveConfig();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ internal sealed class Sidebar
|
|||||||
if (ImGui.IsItemClicked())
|
if (ImGui.IsItemClicked())
|
||||||
{
|
{
|
||||||
activeTab = tab;
|
activeTab = tab;
|
||||||
EnsureCurrentChannel(tab);
|
TabLifecycleHelpers.EnsureCurrentChannel(tab);
|
||||||
}
|
}
|
||||||
|
|
||||||
dl.DrawHoverSheen(
|
dl.DrawHoverSheen(
|
||||||
@@ -252,23 +252,4 @@ internal sealed class Sidebar
|
|||||||
ChatType.CustomEmote or ChatType.StandardEmote => FontAwesomeIcon.Comments,
|
ChatType.CustomEmote or ChatType.StandardEmote => FontAwesomeIcon.Comments,
|
||||||
_ => FontAwesomeIcon.Comment,
|
_ => FontAwesomeIcon.Comment,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Pick a sensible input channel for the tab if it has none yet —
|
|
||||||
// walking SelectedChannels for the first key with a ToInputChannel
|
|
||||||
// mapping lets the channel pill render the tab's actual channel
|
|
||||||
// instead of falling back to "—" on first activation.
|
|
||||||
private static void EnsureCurrentChannel(Tab tab)
|
|
||||||
{
|
|
||||||
if (tab.CurrentChannel.Channel != InputChannel.Invalid)
|
|
||||||
return;
|
|
||||||
|
|
||||||
foreach (var chatType in tab.SelectedChannels.Keys)
|
|
||||||
{
|
|
||||||
if (chatType.ToInputChannel() is { } input)
|
|
||||||
{
|
|
||||||
tab.CurrentChannel.SetChannel(input);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
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;
|
||||||
|
|
||||||
|
public TopTabBar(Windows.ChannelPopoutPool pool)
|
||||||
|
{
|
||||||
|
_pool = pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Draw(IList<Tab> tabs, ref Tab? activeTab)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < tabs.Count; i++)
|
||||||
|
{
|
||||||
|
var tab = tabs[i];
|
||||||
|
if (i > 0)
|
||||||
|
ImGui.SameLine();
|
||||||
|
|
||||||
|
var selected = ReferenceEquals(tab, activeTab);
|
||||||
|
if (
|
||||||
|
ImGui.Selectable(
|
||||||
|
$"{tab.Name}###hellion_toptab_{i}",
|
||||||
|
selected,
|
||||||
|
ImGuiSelectableFlags.None,
|
||||||
|
new Vector2(0, 0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
activeTab = tab;
|
||||||
|
TabLifecycleHelpers.EnsureCurrentChannel(tab);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.BeginPopupContextItem($"toptab_ctx_{i}"))
|
||||||
|
{
|
||||||
|
if (ImGui.MenuItem("Pop Out"))
|
||||||
|
_pool.TryOpen(tab);
|
||||||
|
ImGui.EndPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Separator();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ internal sealed class MainWindow : Window
|
|||||||
|
|
||||||
private readonly Components.HonorificHeader _honorific;
|
private readonly Components.HonorificHeader _honorific;
|
||||||
private readonly Components.Sidebar _sidebar;
|
private readonly Components.Sidebar _sidebar;
|
||||||
|
private readonly Components.TopTabBar _topTabs;
|
||||||
private readonly Components.MessageList _messages;
|
private readonly Components.MessageList _messages;
|
||||||
private readonly Components.InputBar _input;
|
private readonly Components.InputBar _input;
|
||||||
private readonly Components.StatusBar _status;
|
private readonly Components.StatusBar _status;
|
||||||
@@ -39,6 +40,7 @@ internal sealed class MainWindow : Window
|
|||||||
public MainWindow(
|
public MainWindow(
|
||||||
Components.HonorificHeader honorific,
|
Components.HonorificHeader honorific,
|
||||||
Components.Sidebar sidebar,
|
Components.Sidebar sidebar,
|
||||||
|
Components.TopTabBar topTabs,
|
||||||
Components.MessageList messages,
|
Components.MessageList messages,
|
||||||
Components.InputBar input,
|
Components.InputBar input,
|
||||||
Components.StatusBar status,
|
Components.StatusBar status,
|
||||||
@@ -48,6 +50,7 @@ internal sealed class MainWindow : Window
|
|||||||
{
|
{
|
||||||
_honorific = honorific;
|
_honorific = honorific;
|
||||||
_sidebar = sidebar;
|
_sidebar = sidebar;
|
||||||
|
_topTabs = topTabs;
|
||||||
_messages = messages;
|
_messages = messages;
|
||||||
_input = input;
|
_input = input;
|
||||||
_status = status;
|
_status = status;
|
||||||
@@ -121,6 +124,17 @@ internal sealed class MainWindow : Window
|
|||||||
var bodyWidth = ImGui.GetContentRegionAvail().X;
|
var bodyWidth = ImGui.GetContentRegionAvail().X;
|
||||||
_honorific.Draw(bodyWidth);
|
_honorific.Draw(bodyWidth);
|
||||||
|
|
||||||
|
if (Plugin.Config.MainWindowLayoutMode == MainWindowLayoutMode.TopTabs)
|
||||||
|
{
|
||||||
|
_topTabs.Draw(Plugin.Config.Tabs, ref _activeTab);
|
||||||
|
using (ImRaii.Group())
|
||||||
|
{
|
||||||
|
DrawMainArea();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sidebar layout (default).
|
||||||
using (ImRaii.Group())
|
using (ImRaii.Group())
|
||||||
{
|
{
|
||||||
_sidebar.Draw(bodyWidth, Plugin.Config.Tabs, ref _activeTab);
|
_sidebar.Draw(bodyWidth, Plugin.Config.Tabs, ref _activeTab);
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using HellionChat.Code;
|
||||||
|
|
||||||
namespace HellionChat.Util;
|
namespace HellionChat.Util;
|
||||||
|
|
||||||
// Pure predicates for the TempTab pin lifecycle. Extracted from the strip
|
// Pure predicates for the TempTab pin lifecycle. Extracted from the strip
|
||||||
@@ -13,4 +15,21 @@ internal static class TabLifecycleHelpers
|
|||||||
public static bool ShouldStripOnLoad(Tab t) => IsInUnpinnedPool(t);
|
public static bool ShouldStripOnLoad(Tab t) => IsInUnpinnedPool(t);
|
||||||
|
|
||||||
public static bool ShouldStripOnSave(Tab t) => IsInUnpinnedPool(t);
|
public static bool ShouldStripOnSave(Tab t) => IsInUnpinnedPool(t);
|
||||||
|
|
||||||
|
// Shared by the click paths (Sidebar, TopTabBar) and the keybind tab-cycle
|
||||||
|
// path so every entry point resolves a tab's channel identically (no drift).
|
||||||
|
internal static void EnsureCurrentChannel(Tab tab)
|
||||||
|
{
|
||||||
|
if (tab.CurrentChannel.Channel != InputChannel.Invalid)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var chatType in tab.SelectedChannels.Keys)
|
||||||
|
{
|
||||||
|
if (chatType.ToInputChannel() is { } input)
|
||||||
|
{
|
||||||
|
tab.CurrentChannel.SetChannel(input);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user