@@ -4379,6 +4379,7 @@ function Database:PopulateDynamicTalents()
43794379 end
43804380 end
43814381 end
4382+ self :InjectTalentSwapRows ()
43824383 return true
43834384end
43844385
@@ -4588,6 +4589,156 @@ function Stored.WalkPlayerBags()
45884589 return itemMap , order , slotsSeen
45894590end
45904591
4592+ -- Spec switching and saved talent loadouts as Talents-category rows: typing
4593+ -- "holy" or "spec" swaps specialization, a loadout name loads it. Both row
4594+ -- kinds carry stable IDs (specSetIndex / loadoutConfigID) so they dispatch
4595+ -- field-based like window commands, staying pinnable and bindable. Names and
4596+ -- icons come localized from the APIs.
4597+ local TALENT_SPEC_KW = { " spec" , " specialization" , " swap" , " switch" , " talents" }
4598+ local TALENT_LOADOUT_KW = { " loadout" , " loadouts" , " build" , " talents" , " swap" , " switch" }
4599+
4600+ function Database :InjectTalentSwapRows ()
4601+ local specPath = { _G [" TALENTS" ] or " Talents" }
4602+ local getNumSpecs = (C_SpecializationInfo and C_SpecializationInfo .GetNumSpecializations )
4603+ or GetNumSpecializations
4604+ local getSpec = (C_SpecializationInfo and C_SpecializationInfo .GetSpecialization )
4605+ or GetSpecialization
4606+ local getSpecInfo = (C_SpecializationInfo and C_SpecializationInfo .GetSpecializationInfo )
4607+ or GetSpecializationInfo
4608+ if not (getNumSpecs and getSpecInfo ) then return end
4609+
4610+ local activeSpec = getSpec and getSpec ()
4611+ -- Green matching the outfit rows' currently-equipped tint (0.3, 1, 0.3).
4612+ local activeLabel = " |cff4dff4d" .. L [" TALENT_CURRENTLY_ACTIVE" ] .. " |r"
4613+ for i = 1 , getNumSpecs () or 0 do
4614+ local ok , specID , specName , _ , specIcon = pcall (getSpecInfo , i )
4615+ if ok and specID and specName and specName ~= " " then
4616+ local isActive = (i == activeSpec ) or nil
4617+ uiSearchData [# uiSearchData + 1 ] = {
4618+ name = specName ,
4619+ nameLower = slower (specName ),
4620+ icon = specIcon ,
4621+ category = " Talent" ,
4622+ path = specPath ,
4623+ keywords = TALENT_SPEC_KW ,
4624+ keywordsLower = TALENT_SPEC_KW ,
4625+ specSetIndex = i ,
4626+ specIsActive = isActive ,
4627+ searchCommandDesc = isActive and activeLabel
4628+ or L [" TALENT_SPEC_SUBTEXT" ],
4629+ }
4630+ end
4631+ end
4632+
4633+ if not (C_ClassTalents and C_ClassTalents .GetConfigIDsBySpecID
4634+ and C_Traits and C_Traits .GetConfigInfo ) then
4635+ return
4636+ end
4637+ -- ONLY the active spec's loadouts. Loading one auto-commits (measured:
4638+ -- LoadConfig starts the ~5s commit cast itself), so each row is a clean
4639+ -- one-click action. Another spec's loadout would need a spec swap plus a
4640+ -- gated second commit; the spec rows above cover that -- swap first, and
4641+ -- the new spec's loadouts appear (the spec-change event re-dirties this
4642+ -- provider).
4643+ if not activeSpec then return end
4644+ local okID , specID , specName , _ , specIcon = pcall (getSpecInfo , activeSpec )
4645+ if not (okID and specID ) then return end
4646+ local okCfg , configIDs = pcall (C_ClassTalents .GetConfigIDsBySpecID , specID )
4647+ if not (okCfg and type (configIDs ) == " table" ) then return end
4648+ -- The loadout the talent UI considers selected, i.e. the one the player
4649+ -- is "in". Same record LoadTalentConfig maintains after every load.
4650+ local lastSelected
4651+ if C_ClassTalents .GetLastSelectedSavedConfigID then
4652+ local okSel , sel = pcall (C_ClassTalents .GetLastSelectedSavedConfigID , specID )
4653+ if okSel then lastSelected = sel end
4654+ end
4655+ for i = 1 , # configIDs do
4656+ local loadoutID = configIDs [i ]
4657+ local infoOk , info = pcall (C_Traits .GetConfigInfo , loadoutID )
4658+ if infoOk and type (info ) == " table" and info .name and info .name ~= " " then
4659+ -- Keywords: the shared loadout words plus the spec's name, so
4660+ -- "frost loadout" finds it.
4661+ local kw = { slower (specName or " " ) }
4662+ for k = 1 , # TALENT_LOADOUT_KW do kw [# kw + 1 ] = TALENT_LOADOUT_KW [k ] end
4663+ local isActive = (loadoutID == lastSelected ) or nil
4664+ uiSearchData [# uiSearchData + 1 ] = {
4665+ name = info .name ,
4666+ nameLower = slower (info .name ),
4667+ icon = specIcon ,
4668+ category = " Talent" ,
4669+ path = specPath ,
4670+ keywords = kw ,
4671+ keywordsLower = kw ,
4672+ loadoutConfigID = loadoutID ,
4673+ loadoutIsActive = isActive ,
4674+ searchCommandDesc = isActive and activeLabel
4675+ or L [" TALENT_LOADOUT_SUBTEXT" ],
4676+ }
4677+ end
4678+ end
4679+ end
4680+
4681+ -- LoadConfig applies the BUILD but not which saved loadout the talent UI
4682+ -- considers selected; with the frame closed nothing else updates it, so the
4683+ -- frame later shows the wrong loadout ("Default Loadout") and treats the live
4684+ -- tree as a staged diff (red Apply Changes after a reload). Blizzard's talent
4685+ -- frame pairs every load with UpdateLastSelectedSavedConfigID(specID,
4686+ -- configID); the call is VERIFIED via the getter and falls back to the
4687+ -- single-argument form, so a signature drift surfaces as a wrong dropdown at
4688+ -- worst, never silently.
4689+ local function UpdateLastSelectedLoadout (configID )
4690+ local CT = C_ClassTalents
4691+ if not (CT and CT .UpdateLastSelectedSavedConfigID ) then return end
4692+ local getSpec = (C_SpecializationInfo and C_SpecializationInfo .GetSpecialization )
4693+ or GetSpecialization
4694+ local getSpecInfo = (C_SpecializationInfo and C_SpecializationInfo .GetSpecializationInfo )
4695+ or GetSpecializationInfo
4696+ local specIndex = getSpec and getSpec ()
4697+ local specID = specIndex and getSpecInfo and getSpecInfo (specIndex )
4698+ local function selectedNow ()
4699+ if not (CT .GetLastSelectedSavedConfigID and specID ) then return nil end
4700+ local ok , id = pcall (CT .GetLastSelectedSavedConfigID , specID )
4701+ if ok then return id end
4702+ return nil
4703+ end
4704+ if specID then
4705+ pcall (CT .UpdateLastSelectedSavedConfigID , specID , configID )
4706+ local sel = selectedNow ()
4707+ if sel == configID or sel == nil then return end
4708+ end
4709+ pcall (CT .UpdateLastSelectedSavedConfigID , configID )
4710+ end
4711+
4712+ local function LoadTalentConfig (configID )
4713+ if not (C_ClassTalents and C_ClassTalents .LoadConfig ) then return false end
4714+ local ok = (xpcall (C_ClassTalents .LoadConfig , Utils .ErrorHandler , configID , true ))
4715+ if ok then UpdateLastSelectedLoadout (configID ) end
4716+ return ok
4717+ end
4718+
4719+ -- Executes a spec/loadout row. Combat is a hard veto for both APIs; the
4720+ -- localized combat error keeps the refusal visible instead of silent.
4721+ -- Loadouts are always the active spec's (rows only inject for it), and
4722+ -- LoadConfig auto-commits, so both actions are single calls.
4723+ function ns .RunTalentSwap (specIndex , loadoutConfigID )
4724+ if InCombatLockdown () then
4725+ if EasyFind and EasyFind .Print then
4726+ EasyFind :Print (_G [" ERR_NOT_IN_COMBAT" ] or " " )
4727+ end
4728+ return false
4729+ end
4730+ if specIndex then
4731+ local setSpec = (C_SpecializationInfo and C_SpecializationInfo .SetSpecialization )
4732+ or SetSpecialization
4733+ if setSpec then
4734+ return (xpcall (setSpec , Utils .ErrorHandler , specIndex ))
4735+ end
4736+ elseif loadoutConfigID then
4737+ return LoadTalentConfig (loadoutConfigID )
4738+ end
4739+ return false
4740+ end
4741+
45914742function Database :PopulateDynamicBags ()
45924743 local isRealEquipLoc = Utils .IsRealEquipLoc
45934744 local getEquipLoc = Utils .GetItemEquipLoc
0 commit comments