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:
+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)
|
||||
|
||||
Reference in New Issue
Block a user