feat(tabs): add IsPinned with separate pool and 5-tab cap
Tester-Request from Jin (2026-05-03): TempTabs should be pinnable so a
key conversation partner survives a relog. Right-click a TempTab and
choose Pin Tab / Unpin Tab / Promote to permanent.
Pool semantics:
- AutoTellTabsLimit (15) still gates the auto-managed unpinned pool.
- Pinned TempTabs live in their own pool, hard-capped at 5.
- The 6th pin attempt fails with a notification; users can unpin first
or promote to permanent.
- Unpinning into a full unpinned pool drops the oldest unpinned (no
user friction).
Mechanics:
- Tab.IsPinned (default false); Tab.Clone() carries it.
- Migration v16 -> v17 (additive; existing tabs default to unpinned).
- Three strip-sites synchronised through TabLifecycleHelpers:
Plugin.cs load-time, Plugin.SaveConfig, Configuration.UpdateFrom.
- AutoTellTabsService:
* MaxPinnedTempTabs constant.
* F2.1 _activeTempTabCount counter retired — ActiveTempTabCount is
now Tabs.Count(predicate). Pin/Unpin/Promote transitions are
cold-path and don't need lock-free reads.
* DropOldestTempTab filters on IsInUnpinnedPool so pinned tabs are
never drop candidates.
* OnLogout strips only the unpinned pool; pinned popouts and the
active-tab switch behave correspondingly.
* TryPin / Unpin / PromoteToPermanent service methods.
- ChatLogWindow tab context menu: Pin / Unpin / Promote with disabled-
state at-cap tooltip + Promote tooltip explaining the channel-filter
side effect.
- HellionStrings (EN+DE) for menu labels, tooltips, the limit warning.
- AutoTellTabsLimit slider description now flags the separate pinned
pool so users aren't surprised by 18 tabs when the limit reads 15.
This commit is contained in:
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.GameFunctions.Types;
|
||||
using HellionChat.Resources;
|
||||
@@ -20,13 +21,11 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
private readonly MessageStore _store;
|
||||
private readonly object _tempTabsLock = new();
|
||||
|
||||
// F2.1: lock-free counter mirrors Config.Tabs.Count(IsTempTab) so the
|
||||
// hot-path getter doesn't contend with HandleTell on every render frame.
|
||||
// Bumped from inside the existing mutation paths so it stays consistent
|
||||
// with the underlying list — see SpawnTempTab, DropOldestTempTab, OnLogout
|
||||
// and ResyncTempTabCounter (used by Plugin.cs snapshot-restore).
|
||||
// TEST-MIRROR: ../../Hellion Build test/_Helpers/TempTabCounterTests.cs
|
||||
private int _activeTempTabCount;
|
||||
// Hard cap on pinned TempTabs so the sidebar doesn't inflate over years
|
||||
// of usage. Separate pool from AutoTellTabsLimit (15) — pinned tabs live
|
||||
// in their own bucket. A configurable cap is a vault-backlog anchor for
|
||||
// a later cycle if tester feedback demands it.
|
||||
internal const int MaxPinnedTempTabs = 5;
|
||||
|
||||
private bool _initialized;
|
||||
|
||||
@@ -37,7 +36,14 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
_store = store;
|
||||
}
|
||||
|
||||
internal int ActiveTempTabCount => Volatile.Read(ref _activeTempTabCount);
|
||||
// Derived from the tab list on read. Pin/Unpin/Promote/Logout simply
|
||||
// mutate IsPinned or remove tabs — the count adapts automatically.
|
||||
// Replaces the F2.1 Interlocked counter because the new pin-state
|
||||
// transitions are cold-path and don't need lock-free reads.
|
||||
internal int ActiveTempTabCount =>
|
||||
Plugin.Config.Tabs.Count(TabLifecycleHelpers.IsInUnpinnedPool);
|
||||
|
||||
internal int PinnedTempTabCount => Plugin.Config.Tabs.Count(TabLifecycleHelpers.IsInPinnedPool);
|
||||
|
||||
internal void Initialize()
|
||||
{
|
||||
@@ -46,25 +52,11 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
// Seed the counter from the persisted Tabs list so a config that already
|
||||
// contains TempTabs from a prior session starts in sync. Plugin.cs:168
|
||||
// crash-recovery has already dropped TempTabs by the time we get here,
|
||||
// so the snapshot reflects post-recovery reality.
|
||||
Interlocked.Exchange(ref _activeTempTabCount, Plugin.Config.Tabs.Count(t => t.IsTempTab));
|
||||
|
||||
_messageManager.MessageProcessed += HandleTell;
|
||||
Plugin.ClientState.Logout += OnLogout;
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
// F2.1: callable from outside paths that mutate Config.Tabs directly
|
||||
// (Plugin.cs snapshot-restore). Atomically re-pegs the counter to the
|
||||
// live IsTempTab count.
|
||||
internal void ResyncTempTabCounter()
|
||||
{
|
||||
Interlocked.Exchange(ref _activeTempTabCount, Plugin.Config.Tabs.Count(t => t.IsTempTab));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_initialized)
|
||||
@@ -170,12 +162,14 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
);
|
||||
}
|
||||
|
||||
private void DropOldestTempTab()
|
||||
internal void DropOldestTempTab()
|
||||
{
|
||||
// Prioritize greeted tabs for drop; within each bucket, drop by oldest LastActivity
|
||||
// Pinned tabs live in their own bucket (MaxPinnedTempTabs) and are
|
||||
// never drop candidates. They leave the bucket only via Unpin or
|
||||
// PromoteToPermanent.
|
||||
var victim = Plugin
|
||||
.Config.Tabs.Select((tab, idx) => (Tab: tab, Index: idx))
|
||||
.Where(t => t.Tab.IsTempTab)
|
||||
.Where(t => TabLifecycleHelpers.IsInUnpinnedPool(t.Tab))
|
||||
.OrderByDescending(t => t.Tab.IsGreeted)
|
||||
.ThenBy(t => t.Tab.LastActivity)
|
||||
.FirstOrDefault();
|
||||
@@ -198,7 +192,6 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
}
|
||||
|
||||
Plugin.Config.Tabs.RemoveAt(victim.Index);
|
||||
Interlocked.Decrement(ref _activeTempTabCount);
|
||||
|
||||
// Re-anchor active tab to avoid silent switch when tab is dropped
|
||||
if (victim.Index <= _plugin.LastTab)
|
||||
@@ -223,7 +216,6 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
}
|
||||
|
||||
Plugin.Config.Tabs.Add(tab);
|
||||
Interlocked.Increment(ref _activeTempTabCount);
|
||||
}
|
||||
|
||||
private static Tab BuildTempTab(string playerName, uint worldRowId)
|
||||
@@ -354,14 +346,16 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
{
|
||||
lock (_tempTabsLock)
|
||||
{
|
||||
// Snapshot active tab index before mutating list
|
||||
// Pinned TempTabs must survive char-switch — that's the whole point
|
||||
// of pinning. Only unpinned ones get stripped.
|
||||
var lastIndex = _plugin.LastTab;
|
||||
var lastIndexValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count;
|
||||
var currentWasTempTab = lastIndexValid && Plugin.Config.Tabs[lastIndex].IsTempTab;
|
||||
var currentWasUnpinnedTempTab =
|
||||
lastIndexValid
|
||||
&& TabLifecycleHelpers.IsInUnpinnedPool(Plugin.Config.Tabs[lastIndex]);
|
||||
|
||||
// Clean up pop-out windows before removing temp tabs
|
||||
var poppedTempTabIds = Plugin
|
||||
.Config.Tabs.Where(t => t.IsTempTab && t.PopOut)
|
||||
.Config.Tabs.Where(t => TabLifecycleHelpers.IsInUnpinnedPool(t) && t.PopOut)
|
||||
.Select(t => t.Identifier)
|
||||
.ToList();
|
||||
if (poppedTempTabIds.Count > 0)
|
||||
@@ -377,15 +371,68 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
var removed = Plugin.Config.Tabs.RemoveAll(t => t.IsTempTab);
|
||||
Interlocked.Add(ref _activeTempTabCount, -removed);
|
||||
Plugin.Config.Tabs.RemoveAll(TabLifecycleHelpers.IsInUnpinnedPool);
|
||||
|
||||
// Force switch to tab 0 if active tab was temp or index is now out of range
|
||||
// Force switch to tab 0 if active tab was an unpinned temp tab or
|
||||
// index is now out of range. Pinned tabs survive — no switch needed.
|
||||
var stillValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count;
|
||||
if (currentWasTempTab || !stillValid)
|
||||
if (currentWasUnpinnedTempTab || !stillValid)
|
||||
{
|
||||
_plugin.WantedTab = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal bool TryPin(Tab tab)
|
||||
{
|
||||
if (!tab.IsTempTab || tab.IsPinned)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (PinnedTempTabCount >= MaxPinnedTempTabs)
|
||||
{
|
||||
WrapperUtil.AddNotification(
|
||||
string.Format(HellionStrings.PinTab_LimitReached, MaxPinnedTempTabs),
|
||||
NotificationType.Warning
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
tab.IsPinned = true;
|
||||
_plugin.SaveConfig();
|
||||
return true;
|
||||
}
|
||||
|
||||
internal void Unpin(Tab tab)
|
||||
{
|
||||
if (!tab.IsPinned)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If the unpinned pool is already full, dropping the oldest before
|
||||
// flipping the flag avoids counting the just-unpinned tab as a drop
|
||||
// candidate.
|
||||
if (ActiveTempTabCount >= Plugin.Config.AutoTellTabsLimit)
|
||||
{
|
||||
DropOldestTempTab();
|
||||
}
|
||||
|
||||
tab.IsPinned = false;
|
||||
_plugin.SaveConfig();
|
||||
}
|
||||
|
||||
internal void PromoteToPermanent(Tab tab)
|
||||
{
|
||||
if (!tab.IsTempTab)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
tab.IsTempTab = false;
|
||||
tab.IsPinned = false;
|
||||
tab.TellTarget = TellTarget.Empty();
|
||||
_plugin.SaveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ public class ConfigKeyBind
|
||||
[Serializable]
|
||||
public class Configuration : IPluginConfiguration
|
||||
{
|
||||
private const int LatestVersion = 16;
|
||||
private const int LatestVersion = 17;
|
||||
|
||||
public int Version { get; set; } = LatestVersion;
|
||||
|
||||
@@ -278,16 +278,17 @@ public class Configuration : IPluginConfiguration
|
||||
ColorSelectedInputChannelButton = other.ColorSelectedInputChannelButton;
|
||||
|
||||
// Keep live temp tabs alive across UpdateFrom — a settings save must
|
||||
// not destroy open tell conversations. For persistent tabs, capture
|
||||
// the live MessageList and LastSendUnread by Identifier before the
|
||||
// replace and restore them onto the freshly cloned tabs; new tabs
|
||||
// get an empty MessageList, deleted tabs lose their history (intended).
|
||||
var liveTempTabs = Tabs.Where(t => t.IsTempTab).ToList();
|
||||
var livePersistentSession = Tabs.Where(t => !t.IsTempTab)
|
||||
// not destroy open tell conversations. Pinned TempTabs are persistent
|
||||
// and come through `other` like regular tabs; unpinned TempTabs are
|
||||
// session-only and held from the local state. For persistent tabs
|
||||
// (incl. pinned), capture live MessageList + LastSendUnread by
|
||||
// Identifier and restore them onto the freshly cloned tabs.
|
||||
var liveUnpinnedTempTabs = Tabs.Where(TabLifecycleHelpers.IsInUnpinnedPool).ToList();
|
||||
var livePersistentSession = Tabs.Where(t => !TabLifecycleHelpers.IsInUnpinnedPool(t))
|
||||
.ToDictionary(t => t.Identifier, t => (t.Messages, t.LastSendUnread));
|
||||
|
||||
Tabs = other
|
||||
.Tabs.Where(t => !t.IsTempTab)
|
||||
.Tabs.Where(t => !t.IsTempTab || t.IsPinned)
|
||||
.Select(t =>
|
||||
{
|
||||
var clone = t.Clone();
|
||||
@@ -299,7 +300,7 @@ public class Configuration : IPluginConfiguration
|
||||
return clone;
|
||||
})
|
||||
.ToList();
|
||||
Tabs.AddRange(liveTempTabs);
|
||||
Tabs.AddRange(liveUnpinnedTempTabs);
|
||||
|
||||
ChatTabForward = other.ChatTabForward;
|
||||
ChatTabBackward = other.ChatTabBackward;
|
||||
@@ -404,6 +405,11 @@ public class Tab
|
||||
public bool HideWhenInactive;
|
||||
|
||||
public bool IsTempTab;
|
||||
|
||||
// Pinned TempTabs survive plugin reload and logout — tester feedback from
|
||||
// Jin (v1.4.7). Pinned tabs live in their own pool (MaxPinnedTempTabs)
|
||||
// separate from the AutoTellTabsLimit bucket.
|
||||
public bool IsPinned;
|
||||
public bool AllSenderMessages;
|
||||
public TellTarget TellTarget = TellTarget.Empty();
|
||||
|
||||
@@ -511,6 +517,7 @@ public class Tab
|
||||
HideInBattle = HideInBattle,
|
||||
HideWhenInactive = HideWhenInactive,
|
||||
IsTempTab = IsTempTab,
|
||||
IsPinned = IsPinned,
|
||||
AllSenderMessages = AllSenderMessages,
|
||||
TellTarget = TellTarget.Clone(),
|
||||
IsGreeted = IsGreeted,
|
||||
|
||||
+18
-20
@@ -169,20 +169,22 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
PlatformUtil = new DalamudPlatformUtil();
|
||||
LogProxy = new DalamudPluginLogProxy(Log);
|
||||
|
||||
// Schema gate: v1.4.x requires config v16. Users on older schemas
|
||||
// must install v1.4.2 first to run the migration chain.
|
||||
// Schema gate: v1.4.x requires config v16+. Users on older schemas
|
||||
// must install v1.4.2 first to run the migration chain. v17 adds
|
||||
// Tab.IsPinned (additive, no data migration needed) so v16 configs
|
||||
// load cleanly and get their Version stamp bumped after the gate.
|
||||
if (Config.Version < 16)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"HellionChat v1.4.6 requires config schema v16, got v{Config.Version}. "
|
||||
+ "Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.6."
|
||||
$"HellionChat v1.4.7 requires config schema v16, got v{Config.Version}. "
|
||||
+ "Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.7."
|
||||
);
|
||||
}
|
||||
Config.Version = 17;
|
||||
|
||||
// Session-only tabs are stripped on every load; AutoTellTabsService.Initialize
|
||||
// then re-pegs TempTabCounter from the stripped list, not the pre-strip snapshot.
|
||||
// TEST-MIRROR: ../../Hellion Build test/_Helpers/TempTabCounterTests.cs
|
||||
Config.Tabs.RemoveAll(t => t.IsTempTab);
|
||||
// Unpinned TempTabs are session-only and dropped on every load. Pinned
|
||||
// TempTabs survive reload — Jin's tester feedback (v1.4.7).
|
||||
Config.Tabs.RemoveAll(TabLifecycleHelpers.ShouldStripOnLoad);
|
||||
|
||||
LanguageChanged(Interface.UiLanguage);
|
||||
ImGuiUtil.Initialize(this);
|
||||
@@ -652,21 +654,17 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
|
||||
internal void SaveConfig()
|
||||
{
|
||||
// Session-only Auto-Tell-Tabs aren't persisted, so they move aside
|
||||
// before serialization and re-attach after. Cloning only the temp
|
||||
// subset keeps the allocation proportional to AutoTellTabsLimit
|
||||
// (<=15) instead of the full tab list.
|
||||
var tempTabs = Config.Tabs.Where(t => t.IsTempTab).ToList();
|
||||
Config.Tabs.RemoveAll(t => t.IsTempTab);
|
||||
// Only unpinned TempTabs are session-only — they move aside before
|
||||
// serialization and re-attach after. Pinned TempTabs stay in
|
||||
// Config.Tabs across the save so JSON includes them. Cloning only the
|
||||
// unpinned subset keeps the allocation proportional to
|
||||
// AutoTellTabsLimit (<=15) instead of the full tab list.
|
||||
var unpinnedTempTabs = Config.Tabs.Where(TabLifecycleHelpers.IsInUnpinnedPool).ToList();
|
||||
Config.Tabs.RemoveAll(TabLifecycleHelpers.ShouldStripOnSave);
|
||||
|
||||
Interface.SavePluginConfig(Config);
|
||||
|
||||
Config.Tabs.AddRange(tempTabs);
|
||||
|
||||
// F2.1: the mid-step RemoveAll bypasses AutoTellTabsService, so
|
||||
// re-peg the counter. Null-conditional because SaveConfig can fire
|
||||
// before Phase-2 init.
|
||||
AutoTellTabsService?.ResyncTempTabCounter();
|
||||
Config.Tabs.AddRange(unpinnedTempTabs);
|
||||
}
|
||||
|
||||
internal void LanguageChanged(string langCode)
|
||||
|
||||
@@ -170,6 +170,12 @@ internal class HellionStrings
|
||||
internal static string AutoTellTabs_HistoryLoadError => Get(nameof(AutoTellTabs_HistoryLoadError));
|
||||
internal static string AutoTellTabs_GreetedTooltip => Get(nameof(AutoTellTabs_GreetedTooltip));
|
||||
internal static string AutoTellTabs_UnGreetedTooltip => Get(nameof(AutoTellTabs_UnGreetedTooltip));
|
||||
internal static string PinTab_MenuPin => Get(nameof(PinTab_MenuPin));
|
||||
internal static string PinTab_MenuUnpin => Get(nameof(PinTab_MenuUnpin));
|
||||
internal static string PinTab_MenuPromote => Get(nameof(PinTab_MenuPromote));
|
||||
internal static string PinTab_PromoteTooltip => Get(nameof(PinTab_PromoteTooltip));
|
||||
internal static string PinTab_LimitReached => Get(nameof(PinTab_LimitReached));
|
||||
internal static string PinTab_PinnedTooltip => Get(nameof(PinTab_PinnedTooltip));
|
||||
|
||||
// Hellion Chat — Auto-Tell-Tabs Chat settings tab
|
||||
internal static string ChatLog_AutoTellTabs_Section_Title => Get(nameof(ChatLog_AutoTellTabs_Section_Title));
|
||||
|
||||
@@ -383,6 +383,24 @@
|
||||
<data name="AutoTellTabs_UnGreetedTooltip" xml:space="preserve">
|
||||
<value>Als begrüßt markieren.</value>
|
||||
</data>
|
||||
<data name="PinTab_MenuPin" xml:space="preserve">
|
||||
<value>Tab anpinnen</value>
|
||||
</data>
|
||||
<data name="PinTab_MenuUnpin" xml:space="preserve">
|
||||
<value>Tab lösen</value>
|
||||
</data>
|
||||
<data name="PinTab_MenuPromote" xml:space="preserve">
|
||||
<value>Dauerhaft behalten</value>
|
||||
</data>
|
||||
<data name="PinTab_PromoteTooltip" xml:space="preserve">
|
||||
<value>„Dauerhaft behalten" macht aus dem Tab einen regulären Tab, der zu allen channel-gefilterten Nachrichten passt — bei Bedarf danach umbenennen.</value>
|
||||
</data>
|
||||
<data name="PinTab_LimitReached" xml:space="preserve">
|
||||
<value>Maximal {0} angepinnte Tell-Tabs erreicht. Erst einen lösen oder dauerhaft behalten.</value>
|
||||
</data>
|
||||
<data name="PinTab_PinnedTooltip" xml:space="preserve">
|
||||
<value>Angepinnt — überlebt Relog.</value>
|
||||
</data>
|
||||
|
||||
<!-- Hellion Chat — Auto-Tell-Tabs (Chat-Einstellungstab) -->
|
||||
<data name="ChatLog_AutoTellTabs_Section_Title" xml:space="preserve">
|
||||
@@ -398,7 +416,7 @@
|
||||
<value>Maximale Anzahl der Auto-Tell-Tabs</value>
|
||||
</data>
|
||||
<data name="ChatLog_AutoTellTabs_Limit_Description" xml:space="preserve">
|
||||
<value>Beim Erreichen werden begrüßte Tabs mit der ältesten Aktivität zuerst geschlossen. Änderungen greifen beim nächsten /tell.</value>
|
||||
<value>Beim Erreichen werden begrüßte Tabs mit der ältesten Aktivität zuerst geschlossen. Änderungen greifen beim nächsten /tell. Diese Grenze gilt nur für den automatisch verwalteten Pool. Angepinnte Tell-Tabs (Rechtsklick → Tab anpinnen) leben in einem separaten Pool von bis zu 5 Tabs und überleben Relog.</value>
|
||||
</data>
|
||||
<data name="ChatLog_AutoTellTabs_Compact_Name" xml:space="preserve">
|
||||
<value>Kompakte Anzeige</value>
|
||||
|
||||
@@ -383,6 +383,24 @@
|
||||
<data name="AutoTellTabs_UnGreetedTooltip" xml:space="preserve">
|
||||
<value>Mark as greeted.</value>
|
||||
</data>
|
||||
<data name="PinTab_MenuPin" xml:space="preserve">
|
||||
<value>Pin Tab</value>
|
||||
</data>
|
||||
<data name="PinTab_MenuUnpin" xml:space="preserve">
|
||||
<value>Unpin Tab</value>
|
||||
</data>
|
||||
<data name="PinTab_MenuPromote" xml:space="preserve">
|
||||
<value>Promote to permanent</value>
|
||||
</data>
|
||||
<data name="PinTab_PromoteTooltip" xml:space="preserve">
|
||||
<value>Promote turns this into a regular tab matching all channel-filtered messages — rename afterwards if needed.</value>
|
||||
</data>
|
||||
<data name="PinTab_LimitReached" xml:space="preserve">
|
||||
<value>Maximum of {0} pinned tell tabs reached. Unpin one first, or use Promote to permanent.</value>
|
||||
</data>
|
||||
<data name="PinTab_PinnedTooltip" xml:space="preserve">
|
||||
<value>Pinned — survives relog.</value>
|
||||
</data>
|
||||
|
||||
<!-- Hellion Chat — Auto-Tell-Tabs (Chat settings tab) -->
|
||||
<data name="ChatLog_AutoTellTabs_Section_Title" xml:space="preserve">
|
||||
@@ -398,7 +416,7 @@
|
||||
<value>Maximum number of auto-tell tabs</value>
|
||||
</data>
|
||||
<data name="ChatLog_AutoTellTabs_Limit_Description" xml:space="preserve">
|
||||
<value>When the limit is reached, greeted tabs with the oldest activity are closed first. Changes take effect on the next /tell.</value>
|
||||
<value>When the limit is reached, greeted tabs with the oldest activity are closed first. Changes take effect on the next /tell. This limit applies to the auto-managed pool. Pinned tell tabs (right-click → Pin Tab) live in a separate pool of up to 5 and survive relog.</value>
|
||||
</data>
|
||||
<data name="ChatLog_AutoTellTabs_Compact_Name" xml:space="preserve">
|
||||
<value>Compact display</value>
|
||||
|
||||
@@ -2124,10 +2124,56 @@ public sealed class ChatLogWindow : Window
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
if (tab.IsTempTab)
|
||||
{
|
||||
ImGui.Separator();
|
||||
DrawPinControls(tab);
|
||||
}
|
||||
|
||||
if (anyChanged)
|
||||
Plugin.SaveConfig();
|
||||
}
|
||||
|
||||
private void DrawPinControls(Tab tab)
|
||||
{
|
||||
var svc = Plugin.AutoTellTabsService;
|
||||
if (svc == null)
|
||||
return;
|
||||
|
||||
if (tab.IsPinned)
|
||||
{
|
||||
if (ImGui.MenuItem(HellionStrings.PinTab_MenuUnpin))
|
||||
{
|
||||
svc.Unpin(tab);
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var atCap = svc.PinnedTempTabCount >= AutoTellTabsService.MaxPinnedTempTabs;
|
||||
if (ImGui.MenuItem(HellionStrings.PinTab_MenuPin, enabled: !atCap))
|
||||
{
|
||||
if (svc.TryPin(tab))
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
if (atCap && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
||||
ImGui.SetTooltip(
|
||||
string.Format(
|
||||
HellionStrings.PinTab_LimitReached,
|
||||
AutoTellTabsService.MaxPinnedTempTabs
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (ImGui.MenuItem(HellionStrings.PinTab_MenuPromote))
|
||||
{
|
||||
svc.PromoteToPermanent(tab);
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip(HellionStrings.PinTab_PromoteTooltip);
|
||||
}
|
||||
|
||||
internal readonly List<bool> PopOutDocked = [];
|
||||
internal readonly HashSet<Guid> PopOutWindows = [];
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace HellionChat.Util;
|
||||
|
||||
// Pure predicates for the TempTab pin lifecycle. Extracted from the strip
|
||||
// sites in Plugin.cs and Configuration.cs so they stay in lockstep — a
|
||||
// load-time strip that disagrees with the save-time strip is exactly how
|
||||
// pinned tabs would silently fall out of the JSON.
|
||||
internal static class TabLifecycleHelpers
|
||||
{
|
||||
public static bool IsInUnpinnedPool(Tab t) => t.IsTempTab && !t.IsPinned;
|
||||
|
||||
public static bool IsInPinnedPool(Tab t) => t.IsTempTab && t.IsPinned;
|
||||
|
||||
public static bool ShouldStripOnLoad(Tab t) => IsInUnpinnedPool(t);
|
||||
|
||||
public static bool ShouldStripOnSave(Tab t) => IsInUnpinnedPool(t);
|
||||
}
|
||||
Reference in New Issue
Block a user