diff --git a/EllesmereUILocales/_keys.txt b/EllesmereUILocales/_keys.txt index 67d60c51e..0e47324fe 100644 --- a/EllesmereUILocales/_keys.txt +++ b/EllesmereUILocales/_keys.txt @@ -1,6 +1,6 @@ # Auto-generated by .tools/extract-locale-keys.sh -- do not edit by hand. # Canonical list of translatable English keys passed as string literals -# (791 unique). Regenerate after wrapping new strings. Keys passed as +# (800 unique). Regenerate after wrapping new strings. Keys passed as # variables are not listed here -- use the in-game /euiloc harvester for # the complete runtime set. (Raid) diff --git a/EllesmereUIOptions/EUI_PlayerAuraBars_ManagerPages.lua b/EllesmereUIOptions/EUI_PlayerAuraBars_ManagerPages.lua index 32038ee3f..7f366f670 100644 --- a/EllesmereUIOptions/EUI_PlayerAuraBars_ManagerPages.lua +++ b/EllesmereUIOptions/EUI_PlayerAuraBars_ManagerPages.lua @@ -328,8 +328,8 @@ local function DefaultBuffBarName(cfg) end -- Weapon-enchants-only is a fundamentally different shape of bar -- at most --- three cells (main hand / off hand / ranged, see EUI_UnitFrames_ --- WeaponEnchants.lua's SLOTS, which matches Blizzard's own +-- three cells (main hand / off hand / ranged, the engine's own +-- AuraContainerItemEnchantmentSlot set, which matches Blizzard's -- UpdateTemporaryEnchantmentBuffs) instead of a wrapping buff grid. Resize the -- grid to fit on the way in and restore it on the way out. -- @@ -570,7 +570,7 @@ local function BuildAssignedBuffsFields(frame, fontPath, sy, cfg, apply, isDefau -- alone, so the row would be a dead switch on custom buff bars. if isDefault then items[#items + 1] = { key = PAB_WEAPON_ENCH_KEY, label = "Weapon Enchants", - tooltip = "Show weapon oil and imbue icons at the front of this bar. They are weapon enchants rather than auras, so they show independently of the options above -- and the aura grid is shifted inward to make room for them, with every row shifting over by the same amount." } + tooltip = "Show weapon oil and imbue icons at the front of this bar. They are weapon enchants rather than auras, so they show independently of the options above." } end items[#items + 1] = { isHeader = true, label = "Show", rightLabel = "Hide" } for i = 1, #filters do diff --git a/EllesmereUIUnitFrames/EUI_UnitFrames_AuraContainers.lua b/EllesmereUIUnitFrames/EUI_UnitFrames_AuraContainers.lua index 386454742..6ec43a9f6 100644 --- a/EllesmereUIUnitFrames/EUI_UnitFrames_AuraContainers.lua +++ b/EllesmereUIUnitFrames/EUI_UnitFrames_AuraContainers.lua @@ -1213,11 +1213,9 @@ local function AnchorContainer(container, frame, unit, base, s, buffContainer) local mergedBuff = merged and isBuff if mergedBuff then anchor = s.debuffAnchor end if anchor == "none" then - -- Player buffs hidden: retire the weapon-enchant lead strip too. - if unit == "player" and isBuff and ns._weaponEnchUF then - ns._weaponEnchUF = nil - if ns.WeaponEnchants_Layout then ns.WeaponEnchants_Layout() end - end + -- Player buffs hidden: the weapon enchants ride this container (see + -- the declaration below), so ApplyGroupConfig's SetShown(false) takes + -- them down with it -- nothing to retire here. return anchor end @@ -1308,36 +1306,38 @@ local function AnchorContainer(container, frame, unit, base, s, buffContainer) container:SetPoint(ia, frame, fp, ox + offX, oy + cbOff + offY) AK.SetContainerAnchor(container, ia) if unit == "player" and isBuff then - -- Weapon enchant lead icons (oils/imbues are not auras; see - -- EUI_UnitFrames_WeaponEnchants.lua): ride the SAME resolved - -- anchor as the player's buff container so the strip leads it. - -- Published only while the broad-content mode admits generic - -- duration buffs (All Buffs or Has Duration -- the catch-all - -- gate) AND the buff display itself is on; renders with the - -- container's live style so customizations follow. - local broad = s.buffShowAll ~= false or s.buffHasDuration == true - local shownBuffs = (s.showBuffs ~= false) - or (s.debuffAnchorBuffs == true and (s.debuffAnchor or "none") ~= "none") - if broad and shownBuffs then - ns._weaponEnchUF = { frame = frame, ia = ia, fp = fp, - x = ox + offX, y = oy + cbOff + offY, gX = gX, - pad = EllesmereUI.PP.FromPixels(s.buffSpacingX or 1), - styleKey = StyleKey("player", "HELPFUL") } - -- Shift the engine run inward past the enchant cells (main - -- hand adjacent to the run; zero enchants = zero shift). - local n = (ns.WeaponEnchants_Count and ns.WeaponEnchants_Count()) or 0 - if n > 0 then - local st = AK.styles[StyleKey("player", "HELPFUL")] - local w = (st and st.width) or 22 - local sign = (gX == "RIGHT") and 1 or -1 - local shift = sign * n * (w + EllesmereUI.PP.FromPixels(s.buffSpacingX or 1)) - container:ClearAllPoints() - container:SetPoint(ia, frame, fp, ox + offX + shift, oy + cbOff + offY) - end - else - ns._weaponEnchUF = nil - end - if ns.WeaponEnchants_Layout then ns.WeaponEnchants_Layout() end + -- Weapon enchants are not auras: the engine's own item- + -- enchantment source renders them as a layout group flowed AHEAD + -- of the aura groups on this container (see + -- AK.AddItemEnchantmentsToContainer), so nothing is anchored and + -- no cells are reserved. Declared lazily and never removed (no + -- addon-facing unregister), so they follow this container's + -- visibility instead of the old broad-content gate. + local PP = EllesmereUI.PP + local w, h = ElementSize(unit, base, s) + local gap = PP.FromPixels(s.buffSpacingX or 1) + local lineGap = PP.FromPixels(s.buffSpacingY or 1) + local placement = CustomAuraContainerItemEnchantmentPlacement + local sortMethods = AuraContainerItemEnchantmentSortMethod + local sortDirs = AuraContainerSortDirection + AK.AddItemEnchantmentsToContainer(container, { + style = StyleKey("player", "HELPFUL"), + hidePermanent = true, + -- REVERSE keeps main hand adjacent to the aura run: the + -- engine puts the group's first element at the leading edge, + -- and Slot order is main hand, off hand, ranged. + sortMethod = sortMethods and sortMethods.Slot, + sortDirection = sortDirs and sortDirs.Reverse, + layout = { + elementWidth = w, + elementHeight = h, + elementSpacing = gap, + lineSpacing = lineGap, + -- No groupSpacing, see the aura groups' layout below. + groupLineSpacing = lineGap, + placement = placement and placement.BeforeAuraGroups, + }, + }) end end AK.SetContainerGrowth(container, FlowDir(gX), FlowDir(gY)) @@ -1424,6 +1424,10 @@ local function ApplyGroupConfig(container, unit, base, s, chain, declared) cand.excludeSpellIDs = ex end + -- No groupSpacing: the engine trails every element with elementSpacing, + -- the last one of a group included, so a group boundary -- the seam + -- between the weapon-enchant cells and the first aura -- already sits one + -- spX away, and groupSpacing would double it. local layout = { elementWidth = size, elementHeight = h, elementSpacing = spX, lineSpacing = spY } -- Active set = "all" (a non-player BUFF element with no classes enabled) diff --git a/EllesmereUIUnitFrames/EUI_UnitFrames_WeaponEnchants.lua b/EllesmereUIUnitFrames/EUI_UnitFrames_WeaponEnchants.lua deleted file mode 100644 index 540f49841..000000000 --- a/EllesmereUIUnitFrames/EUI_UnitFrames_WeaponEnchants.lua +++ /dev/null @@ -1,507 +0,0 @@ -if EUI_CLIENT_BLOCKED then return end -- pre-12.1 client failsafe (EllesmereUI_ClientGate.lua) --- EUI_UnitFrames_WeaponEnchants.lua --- Weapon oils/imbues are temporary weapon ENCHANTS, not auras: nothing with --- a spell ID ever exists on the player (the API struct carries an enchantID, --- an item-enchant identifier), so the engine aura containers behind the --- Player Aura Bars Buffs bar and the player unit frame's buffs can never --- render them -- containers only render auras that exist in the aura stream. --- Blizzard's own BuffFrame hand-injects "TempEnchant" buttons in Lua for the --- same reason (and may read secrets untainted; we cannot copy that path). --- --- Integration model -- first-class citizens of BOTH displays: --- * IN-GRID CELLS: while N enchants are active, the display's engine --- container is SHIFTED inward by N cells (the producers own that shift; --- this file exposes ns.WeaponEnchants_Count and pokes them on change), --- and the enchant buttons occupy the bar's actual first cells -- main --- hand adjacent to the run, Blizzard's temp-enchants-first ordering. --- No oils = zero shift = byte-identical layout. --- * LIVE STYLE: buttons render from the display's own AK.styles table and --- speak both style dialects (PAB BuildStyle and the unit-frame --- BuildStyle: texCoord rect vs iconCrop/iconZoom, cdText* vs duration*, --- stackPos vs stackPoint, noTooltips) -- customizations follow --- automatically. --- * FILTER SEMANTICS: producers publish a record only while their --- broad-content mode admits generic duration buffs (All Buffs OR Has --- Duration on); curated-only configs render and shift nothing. --- * TOOLTIP + CANCEL: the inventory-item tooltip mirrors Blizzard's --- TempEnchant button (GameTooltip:SetInventoryItem -- item data only --- GameTooltip can render, same pattern as Bags). PAB buttons cancel on --- right-click through the SECURE path: CancelTemporaryEnchantment is --- hard-protected for addon code (ADDON_ACTION_FORBIDDEN), so the PAB --- trio are SecureActionButtons with type2="cancelaura" + --- target-slot2=, which SECURE_ACTIONS.cancelaura resolves --- to the protected call engine-side. UF buttons never cancel (spec). --- --- COMBAT MODEL (the secure trio cannot be moved/shown/hidden/re-attributed --- in lockdown): each PAB button is SLOT-FIXED (attributes set once at --- creation), pre-warmed shown at alpha 0 out of combat, and in-combat --- transitions touch only alpha + content (icon/cooldown/texts -- all legal --- on protected frames). Geometry, mouse flags and the producers' container --- shifts are deferred to PLAYER_REGEN_ENABLED, then one full pass repacks. --- --- Secrecy: GetTemporaryEnchantmentInfo's returns carry NO Secret* flags in --- the generated API docs (equipment state, not unit-aura data) -- plain --- numbers in restricted combat too. --- --- Cost model: WEAPON_ENCHANT_CHANGED / WEAPON_SLOT_CHANGED (the pair --- Blizzard's BuffFrame registers) are the only wake-ups, registered only --- while a display publishes a record; the duration text runs on a shared --- 0.5s ticker that exists ONLY while an enchant button is visible. --- --- Producer records (nil = display inactive/filtered), poked via --- ns.WeaponEnchants_Layout(): --- ns._weaponEnchPAB = { parent, corner, dir, pad, styleKey, canCancel } --- ns._weaponEnchUF = { frame, ia, fp, x, y, gX, pad, styleKey } - -local _, ns = ... -local EllesmereUI = _G.EllesmereUI - -local SLOTS = { INVSLOT_MAINHAND or 16, INVSLOT_OFFHAND or 17, INVSLOT_RANGED or 18 } - -local hosts = {} -- "pab"/"uf" -> { frame, buttons = {}, rec } -local evFrame, regenFrame, textTicker -local activeInfos, activeCount = {}, 0 -- packed active list (MH first) -local activeBySlot = {} -- slot -> info - --- Raw active-enchant count. Producers gate on their own record/filters and --- shift their container inward by this many cells. -function ns.WeaponEnchants_Count() - return activeCount -end - -local function Style(rec) - local AK = EllesmereUI.AuraKit - return (AK and AK.styles and rec and AK.styles[rec.styleKey]) or nil -end - -local function OnButtonEnter(self) - if self._noTooltip or not self._slot then return end - GameTooltip:SetOwner(self, "ANCHOR_BOTTOMLEFT") - GameTooltip:SetInventoryItem("player", self._slot) -end - -local function OnButtonLeave() - GameTooltip:Hide() -end - --- host.secure: SecureActionButton trio, one per SLOT, cancel wired once at --- creation (attribute writes are combat-blocked, so nothing is ever --- re-attributed). Insecure hosts get plain frames. -local function MakeButton(host, index) - local b - if host.secure then - b = CreateFrame("Button", nil, host.frame, "SecureActionButtonTemplate") - b:SetAttribute("type2", "cancelaura") - b:SetAttribute("target-slot2", SLOTS[index]) - -- BOTH phases: with the press-and-hold cvar (ActionButtonUseKeyDown, - -- default on) the secure handler executes on the DOWN click and - -- ignores the up -- an up-only registration never fires at all. The - -- handler runs exactly one cvar-selected phase, so no double-fire. - b:RegisterForClicks("RightButtonDown", "RightButtonUp") - else - b = CreateFrame("Frame", nil, host.frame) - end - local icon = b:CreateTexture(nil, "ARTWORK") - icon:SetAllPoints() - b.icon = icon - local cd = CreateFrame("Cooldown", nil, b, "CooldownFrameTemplate") - cd:SetAllPoints() - cd:SetHideCountdownNumbers(true) -- duration text is ours, styled below - b.cd = cd - -- Text rides its own frame ABOVE the cooldown: FontStrings on the button - -- itself render UNDER the Cooldown child frame's swipe. - local tf = CreateFrame("Frame", nil, b) - tf:SetAllPoints() - tf:SetFrameLevel(cd:GetFrameLevel() + 5) - b.duration = tf:CreateFontString(nil, "OVERLAY") - b.charges = tf:CreateFontString(nil, "OVERLAY") - b:SetScript("OnEnter", OnButtonEnter) - b:SetScript("OnLeave", OnButtonLeave) - return b -end - --- Duration swipe gate. SetShown alone does not stick: PaintContent's --- SetCooldown implicitly re-Shows the frame (documented in AK's --- ApplyStyleToRegions), and a cooldown started while it was hidden comes back --- as a swipe that never advances -- a frozen wedge on some passes, nothing on --- others. SetDrawSwipe is persistent cooldown STYLE (SetCooldown never resets --- it, AllowedWhenTainted) and alpha survives any re-show; the texts ride their --- own frame above the cooldown, so the alpha misses them. Show/Hide stays in --- ApplyStyle: it is combat-blocked on the secure trio's descendants. -local function ApplySwipeGate(b, hide) - if b._hideSwipe == hide then return end - b._hideSwipe = hide - if b.cd.SetDrawSwipe then b.cd:SetDrawSwipe(not hide) end - b.cd:SetAlpha(hide and 0 or 1) -end - --- Mirrors the display's live style onto one of our buttons; the texcoord --- cascade is AK ApplyStyleToRegions' exact order. Called out of combat only --- for the secure trio (SetSize is geometry). -local function ApplyStyle(b, style) - local w = style.width or 32 - b:SetSize(w, style.height or w) - local tc = style.texCoord - if tc then - b.icon:SetTexCoord(tc[1], tc[2], tc[3], tc[4]) - elseif style.iconCrop then - local z = style.iconZoom or 0.07 - b.icon:SetTexCoord(z, 1 - z, z, 1 - z) - else - b.icon:SetTexCoord(0, 1, 0, 1) - end - local PP = EllesmereUI.PP - local bd = style.border - if bd and PP then - if not b._bdr then - b._bdr = CreateFrame("Frame", nil, b) - b._bdr:SetAllPoints() - b._bdr:EnableMouse(false) - end - b._bdr:SetFrameLevel(bd.behind - and math.max(0, b:GetFrameLevel() - 1) - or (b.cd:GetFrameLevel() + 1)) - EllesmereUI.ApplyBorderStyle(b._bdr, bd.textureSize or bd.size or 1, - bd[1] or 0, bd[2] or 0, bd[3] or 0, bd[4] or 1, - bd.texture or "solid", bd.offsetX, bd.offsetY, bd.shiftX, bd.shiftY, - bd.addonKey or "unitframes", bd.sizeKey or bd.size or 1) - elseif b._bdr then - if EllesmereUI.HideBorderStyle then EllesmereUI.HideBorderStyle(b._bdr) end - b._bdr:Hide() - end - b.cd:SetReverse(style.cooldownReverse ~= false) - b.cd:SetDrawEdge(style.cooldownDrawEdge == true) - local hideSwipe = style.hideSwipe == true - ApplySwipeGate(b, hideSwipe) - b.cd:SetShown(not hideSwipe) - - local path = style.fontPath or STANDARD_TEXT_FONT - local flag = style.fontFlag or "OUTLINE" - if style.hideDurationText then - b.duration:Hide() - else - if EllesmereUI.PrimeFontShadow then EllesmereUI.PrimeFontShadow(b.duration, flag == "") end - b.duration:SetFont(path, style.durationFontSize or style.cdTextSize or 11, flag) - b.duration:ClearAllPoints() - b.duration:SetPoint(style.durationPoint or "CENTER", b, - style.durationRelPoint or "CENTER", - style.durationX or style.cdOffX or 0, - style.durationY or style.cdOffY or 0) - local c = style.durationColor or style.cdTextColor - b.duration:SetTextColor(c and c.r or 1, c and c.g or 1, c and c.b or 1) - b.duration:Show() - end - if EllesmereUI.PrimeFontShadow then EllesmereUI.PrimeFontShadow(b.charges, flag == "") end - b.charges:SetFont(path, style.stackFontSize or style.stackSize or 11, flag) - b.charges:ClearAllPoints() - local sp = style.stackPoint or style.stackPos or "BOTTOMRIGHT" - b.charges:SetPoint(sp, b, sp, - style.stackX or style.stackOffX or 0, - style.stackY or style.stackOffY or 0) - local sc = style.stackColor - b.charges:SetTextColor(sc and sc.r or 1, sc and sc.g or 1, sc and sc.b or 1) -end - --- Compact house duration format matching the engine aura text: bare seconds --- under a minute (with the unit when the style's Show S for Seconds is on), --- m/h above. -local function FormatRemaining(sec, showS) - if sec >= 3600 then return string.format("%dh", math.floor(sec / 3600 + 0.5)) end - if sec >= 60 then return string.format("%dm", math.floor(sec / 60 + 0.5)) end - return string.format(showS and "%ds" or "%d", math.floor(sec + 0.5)) -end - -local function UpdateTexts() - local now = GetTime() - local anyShown = false - for _, host in pairs(hosts) do - local st = Style(host.rec) - local showS = st and st.durationShowSeconds - for _, b in pairs(host.buttons) do - if b:IsShown() and b:GetAlpha() > 0 then - anyShown = true - if b._expire and b.duration:IsShown() then - local rem = b._expire - now - if rem > 0 then - b.duration:SetText(FormatRemaining(rem, showS)) - else - b.duration:SetText("") - end - end - end - end - end - if not anyShown and textTicker then - textTicker:Cancel() - textTicker = nil - end -end - -local function EnsureTicker(want) - if want and not textTicker then - textTicker = C_Timer.NewTicker(0.5, UpdateTexts) - elseif not want and textTicker then - textTicker:Cancel() - textTicker = nil - end -end - -local function ReadEnchants() - local n = 0 - for k in pairs(activeBySlot) do activeBySlot[k] = nil end - for i = 1, #SLOTS do - local info = C_PaperDollInfo and C_PaperDollInfo.GetTemporaryEnchantmentInfo - and C_PaperDollInfo.GetTemporaryEnchantmentInfo(SLOTS[i]) - if info and info.hasExpirationTime then - n = n + 1 - activeInfos[n] = { slot = SLOTS[i], slotIndex = i, info = info } - activeBySlot[SLOTS[i]] = info - end - end - for i = n + 1, #activeInfos do activeInfos[i] = nil end - activeCount = n -end - --- Fills one button's CONTENT (legal on protected frames in combat). -local function PaintContent(b, slot, info, hideSwipe) - b.icon:SetTexture(GetInventoryItemTexture("player", slot)) - local remaining = (info.remainingTimeMs or 0) / 1000 - b._expire = GetTime() + remaining - b.cd:SetCooldown(GetTime(), remaining) - -- Combat-legal half of the gate: ApplyStyle cannot run for the secure - -- trio in lockdown, so a swipe toggle flipped there lands here. - ApplySwipeGate(b, hideSwipe) - local ch = info.chargesRemaining or 0 - if ch > 1 then b.charges:SetText(ch); b.charges:Show() - else b.charges:Hide() end - b._slot = slot -end - -local function Paint() - local n = activeCount - local combat = InCombatLockdown() - local anyShown = false - for _, host in pairs(hosts) do - local rec = host.rec - local style = Style(rec) - if host.secure then - -- Slot-fixed secure trio. The host frame and every button stay - -- PERMANENTLY SHOWN once created (Show/Hide on the buttons OR on - -- any ancestor of them is combat-blocked); visibility rides - -- button alpha exclusively. Out of combat: full geometry/style - -- pass; in combat: alpha + content only. Cells pack active slots - -- with MAIN HAND adjacent to the engine run. - local active = rec and style or nil - local cellIndex = {} - if active then - local ci = n - 1 - for i = 1, #SLOTS do - if activeBySlot[SLOTS[i]] then - cellIndex[i] = ci - ci = ci - 1 - end - end - end - local usedCells = {} - for i = 1, #SLOTS do - local b = host.buttons[i] - if not b and not combat and active then - b = MakeButton(host, i) - b:Show() -- pre-warmed forever; visibility rides alpha - host.buttons[i] = b - end - if b then - local info = active and activeBySlot[SLOTS[i]] or nil - if not combat then - if active then - local w = style.width or 32 - local cell = w + (rec.pad or 0) - ApplyStyle(b, style) - local idx = cellIndex[i] or 0 - local dir = rec.dir or "LEFT" - local dx, dy = 0, 0 - if dir == "RIGHT" then dx = 1 elseif dir == "LEFT" then dx = -1 - elseif dir == "UP" then dy = 1 elseif dir == "DOWN" then dy = -1 end - b:ClearAllPoints() - local point = rec.point or rec.corner - b:SetPoint(point, rec.anchorTo or rec.parent, rec.relativePoint or rec.corner or point, - (rec.x or 0) + dx * idx * cell, - (rec.y or 0) + dy * idx * cell) - b._cell = idx - b._noTooltip = style.noTooltips == true - end - b:EnableMouse(info and true or false) - end - if info then - PaintContent(b, SLOTS[i], info, style.hideSwipe == true) - -- In combat positions are frozen: a button whose - -- last-packed cell now belongs to the shifted engine - -- run (or to another enchant) goes alpha 0 instead - -- of overlapping; the regen pass repacks everything. - local cellBad = combat and (b._cell == nil - or b._cell > n - 1 or usedCells[b._cell]) - if cellBad then - b:SetAlpha(0) - else - if combat then usedCells[b._cell] = true end - b:SetAlpha(1) - anyShown = true - end - else - b:SetAlpha(0) - end - end - end - elseif rec and style and n > 0 then - -- Insecure host (UF): packed dynamic buttons, plain frames. - local w = style.width or 32 - local cell = w + (rec.pad or 0) - for i = 1, n do - local b = host.buttons[i] - if not b then b = MakeButton(host, i); host.buttons[i] = b end - ApplyStyle(b, style) - local idx = n - i -- main hand (i=1) adjacent to the run - local sign = (rec.gX == "RIGHT") and 1 or -1 - b:ClearAllPoints() - b:SetPoint(rec.ia, rec.frame, rec.fp, - rec.x + sign * idx * cell, rec.y) - PaintContent(b, activeInfos[i].slot, activeInfos[i].info, - style.hideSwipe == true) - b._noTooltip = style.noTooltips == true - b:EnableMouse(not b._noTooltip) - b:SetAlpha(1) - b:Show() - anyShown = true - end - for i = n + 1, #host.buttons do host.buttons[i]:Hide() end - host.frame:Show() - elseif host.frame then - host.frame:Hide() - end - end - EnsureTicker(anyShown) - if anyShown then UpdateTexts() end -end - --- Count changes re-anchor the displays (their containers shift inward by --- the new count). Container re-seating is combat-legal and runs IMMEDIATELY --- (the engine run repositions live); only the SECURE trio's geometry is --- lockdown-blocked, so in combat PAB takes the minimal container-only path --- (the full ApplyLiveConfig would trip the anchor-poisoned parent:SetSize) --- and a regen pass repacks the buttons afterward. -local function PokeProducers(combat) - if combat then - if ns.PAB_ReShiftEnchants then ns.PAB_ReShiftEnchants() end - else - if ns.PAB_ApplyLiveConfig then ns.PAB_ApplyLiveConfig(true) end - end - if ns.UF_ReloadAllAuraContainers then ns.UF_ReloadAllAuraContainers() end -end - -local function OnRegen(self) - self:UnregisterEvent("PLAYER_REGEN_ENABLED") - ReadEnchants() - PokeProducers(false) - Paint() -end - -local function OnEnchantEvent() - local prev = activeCount - ReadEnchants() - if activeCount ~= prev then - local combat = InCombatLockdown() - PokeProducers(combat) - if combat then - -- Secure-button geometry can't follow until regen: repack then. - if not regenFrame then - regenFrame = CreateFrame("Frame") - regenFrame:SetScript("OnEvent", OnRegen) - end - regenFrame:RegisterEvent("PLAYER_REGEN_ENABLED") - end - end - Paint() -end - -local function LayoutHost(key, rec) - local host = hosts[key] - if not rec then - -- Secure host: never Hide (protected descendants make even the - -- insecure ancestor's Hide combat-blocked, and the always-shown - -- model needs no visibility ops at all -- buttons go alpha 0). - if host then - host.rec = nil - if not host.secure then host.frame:Hide() end - end - return - end - if not host then - host = { frame = CreateFrame("Frame", nil, UIParent), buttons = {}, - secure = (key == "pab") } - host.frame:Show() - hosts[key] = host - end - host.rec = rec - if host.secure and InCombatLockdown() then return end - -- Parent from rec.parent ONLY -- rec.anchorTo can be an engine aura - -- container, which carries forbidden aspects - -- (UntrustedLayoutScriptExecution): SetParent into it hard-errors for - -- the secure host ("child object would inherit forbidden aspects"). - -- Anchoring is legal, so the host RIDES the container's rect while - -- living in the plain bar frame's tree. - local function DoAnchor() - local r = host.rec - -- Cleared-rec guard, plus the outer skip mirrored: AuraKit jobs are - -- combat-runnable, so a job enqueued OOC can drain in lockdown and - -- these writes on the SECURE host would be blocked actions there. - -- The rec stays, and the regen relayout heals it like the old skip. - if not r or (host.secure and InCombatLockdown()) then return end - host.frame:SetParent(r.parent or r.frame) - host.frame:ClearAllPoints() - host.frame:SetAllPoints(r.anchorTo or r.parent or r.frame) - end - if host.secure then - -- Some callers (e.g. Unlock Mode's grow-direction dropdown) reach - -- here from a live, insecure OnClick handler; anchoring the secure - -- host straight onto an engine aura container from that stack throws - -- UntrustedLayoutScriptExecution. AuraKit's own job queue gives the - -- anchor write a fresh, untainted execution context instead. - local AK = EllesmereUI.AuraKit - if AK and AK.QueueLiveBuildJob then - AK.QueueLiveBuildJob(DoAnchor, "uf:weaponench-anchor") - else - DoAnchor() - end - else - DoAnchor() - end -end - -local function EnsureEvents() - local want = false - for _, host in pairs(hosts) do - if host.rec then want = true; break end - end - if want then - if not evFrame then - evFrame = CreateFrame("Frame") - evFrame:SetScript("OnEvent", OnEnchantEvent) - end - evFrame:RegisterEvent("WEAPON_ENCHANT_CHANGED") - evFrame:RegisterEvent("WEAPON_SLOT_CHANGED") - elseif evFrame then - evFrame:UnregisterAllEvents() - end -end - -function ns.WeaponEnchants_Layout() - LayoutHost("pab", ns._weaponEnchPAB) - LayoutHost("uf", ns._weaponEnchUF) - EnsureEvents() - ReadEnchants() - Paint() -end - --- Producers that loaded (and published) before this file get picked up now; --- later publishes arrive via their own Layout pokes. -ns.WeaponEnchants_Layout() diff --git a/EllesmereUIUnitFrames/EllesmereUIUnitFrames.toc b/EllesmereUIUnitFrames/EllesmereUIUnitFrames.toc index a49620c86..80c38b23b 100644 --- a/EllesmereUIUnitFrames/EllesmereUIUnitFrames.toc +++ b/EllesmereUIUnitFrames/EllesmereUIUnitFrames.toc @@ -14,6 +14,5 @@ EUI_UnitFrames_Engine.lua EllesmereUIUnitFrames.lua EllesmereUIUnitFrames_PlayerAuraBars.lua EUI_UnitFrames_AuraContainers.lua -EUI_UnitFrames_WeaponEnchants.lua # Options diff --git a/EllesmereUIUnitFrames/EllesmereUIUnitFrames_PlayerAuraBars.lua b/EllesmereUIUnitFrames/EllesmereUIUnitFrames_PlayerAuraBars.lua index 1707c200b..bb264fa66 100644 --- a/EllesmereUIUnitFrames/EllesmereUIUnitFrames_PlayerAuraBars.lua +++ b/EllesmereUIUnitFrames/EllesmereUIUnitFrames_PlayerAuraBars.lua @@ -689,9 +689,11 @@ local STYLE_DEBUFFS = "playerAuraBars_debuffs" -- borderTexture ("solid" or a built-in/LibSharedMedia key), optional -- borderTextureOffset/OffsetY/ShiftX/ShiftY, and borderBehind; -- padding (single scalar -> all 4 sides); rowSpacing (optional row gap: feeds --- lineSpacing/groupLineSpacing only, nil falls back to `padding`; elementSpacing/ --- groupSpacing, icon-to-icon within a row, always stay tied to `padding`); maxTotal --- (overall icon cap); iconsPerRow (row width in columns); maxRows (row cap; with +-- lineSpacing/groupLineSpacing only, nil falls back to `padding`; elementSpacing, +-- icon-to-icon within a row AND across a group seam, stays tied to `padding` +-- while groupSpacing is held at 0, see BuildGroupLayout); maxTotal +-- (overall icon cap, weapon-enchant cells included while that row is on); +-- iconsPerRow (row width in columns); maxRows (row cap; with -- iconsPerRow also bounds maxTotal, see ComputeGrid()); growDirection ("LEFT"/"RIGHT"/ -- "CENTER_HORIZONTAL"/"CENTER_VERTICAL"/"UP"/"DOWN", default LEFT). -- Buff bars (default AND custom, one model): filters ([filterId]=true, shared PAB @@ -1291,7 +1293,13 @@ local function BuildGroupLayout(cfg, gap, rowGap, size) elementHeight = size, elementSpacing = gap, lineSpacing = rowGap, - groupSpacing = gap, + -- ZERO on purpose: the engine's flow layout advances the cursor by + -- elementSpacing AFTER every element, the last one of a group + -- included, so a group boundary already carries one `gap`. Adding + -- groupSpacing on top doubled it -- visible as a wider seam between + -- the weapon-enchant cells and the first buff, and between the + -- catch-all and spells groups. + groupSpacing = 0, groupLineSpacing = rowGap, } end @@ -1299,13 +1307,13 @@ end local function ApplyGroupConfig(container, chain, declaredSet, styleKey, effectiveMax, gap, rowGap, cfg, extraCand) local sortMethod = ResolveSortMethod(cfg) local sortDirection = ResolveSortDirection(cfg) - -- elementSpacing = icon-to-icon gap in a row; lineSpacing = gap between wrapped - -- rows within a group; group*Spacing = gap to the NEXT group on the same - -- container. elementSpacing/groupSpacing stay tied to `gap` (padding); - -- lineSpacing/groupLineSpacing use `rowGap` (defaults to `gap`) so row-to-row - -- distance is overridable independently of icon-to-icon spacing (cfg.rowSpacing in - -- the Settings Schema comment). Container-level padding is a THIRD, unrelated - -- concept: the OUTER edge inset, fixed at 0 elsewhere and never affected by either. + -- elementSpacing = icon-to-icon gap in a row, and it also separates two + -- groups on one line (see BuildGroupLayout's zero groupSpacing); + -- lineSpacing = gap between wrapped rows within a group, tied to `rowGap` + -- (defaults to `gap`) so row-to-row distance is overridable independently of + -- icon-to-icon spacing (cfg.rowSpacing in the Settings Schema comment). + -- Container-level padding is a THIRD, unrelated concept: the OUTER edge inset, + -- fixed at 0 elsewhere and never affected by either. local active = {} for i = 1, #chain do local link = chain[i] @@ -1457,6 +1465,12 @@ local function MaxIconSizeFor(isBuff, cfg) return PabSnap(shaped), shaped end +-- Engine-declared weapon-enchant slots (AuraContainerItemEnchantmentSlot: +-- main hand, off hand, ranged). Only the first two are reachable in current +-- retail content, but the engine declares all three, so anything reserving +-- space for them must tolerate three. +local ENCH_SLOT_COUNT = 3 + local function ComputeGrid(isBuff, cfg) local iconSize, rawIconSize = MaxIconSizeFor(isBuff, cfg) local pad = cfg.padding or 5 @@ -1469,6 +1483,14 @@ local function ComputeGrid(isBuff, cfg) local effectiveMax = math.min(configuredMax, rows * cols) -- Actual rows needed for the effective cap, never more than the row limit local usedRows = math.min(rows, math.max(1, math.ceil(effectiveMax / cols))) + -- How many weapon-enchant slots may be DECLARED on this bar. A grid under + -- three cells declares only what it can hold (main hand first), so the + -- cells can never spill out of it. This is not a budget: an inactive slot + -- reserves nothing, only a cell that actually shows costs one (BuffAuraMax). + local enchSlots = 0 + if isBuff and cfg.showWeaponEnchants == true then + enchSlots = math.min(ENCH_SLOT_COUNT, effectiveMax) + end -- `lineExtent` is the icons' own extent on the line axis (iconsPerRow -- icons of iconSize + gaps); `crossExtent` is the other axis (lines actually used). -- Horizontal growth: a "line" is a row, so lineExtent -> width. Vertical growth @@ -1498,7 +1520,10 @@ local function ComputeGrid(isBuff, cfg) local legacyLine = cols * legacyIcon + (cols - 1) * legacyPad local legacyCross = usedRows * legacyIcon + (usedRows - 1) * legacyRowGap return { + -- The bar's WHOLE icon budget (Max Icons), weapon-enchant cells + -- included -- BuffAuraMax takes the showing ones off it. effectiveMax = effectiveMax, + enchSlots = enchSlots, rowWidth = rowWidth, width = width, height = height, @@ -2329,85 +2354,108 @@ local function SyncNativeAuras() end end --- Shifts the default Buffs container inward by the active weapon-enchant --- count (see EUI_UnitFrames_WeaponEnchants.lua): the enchant buttons occupy --- the bar's first cells and the engine run starts after them -- Blizzard's --- temp-enchants-first ordering. Zero enchants (or a filtered-out record) --- leaves the anchor byte-identical. Full rows overflow the reserved grid by --- the shift while an oil is up -- accepted; the shift is transient. -local function ShiftBuffsForEnchants(container, parent, cfg, grid) - local n = (cfg.showWeaponEnchants == true and ns.WeaponEnchants_Count and ns.WeaponEnchants_Count()) or 0 - local containerAnchor = BuildContainerSpec(parent, cfg, grid) - local dir = cfg.growDirection or "LEFT" - local cell = PabSnap(cfg.iconSize or 32) + PabSnap(cfg.padding or 5) - container:ClearAllPoints() - -- Centered modes: the enchant cells must hug the RUN's moving edge, which - -- only the container's live rect knows. rec.parent must stay the PLAIN bar - -- frame -- the container carries forbidden aspects - -- (UntrustedLayoutScriptExecution), and the consumer's secure host frame - -- hard-errors on SetParent into that subtree ("child object would inherit - -- forbidden aspects"). rec.anchorTo carries the container for ANCHORING - -- only (SetPoint relative-to does not reparent), which is the same trust - -- shape as the buttons' existing anchors into insecurely-positioned frames. - if dir == "CENTER_HORIZONTAL" then - local span = n * cell - container:SetPoint("CENTER", parent, "CENTER", span / 2, 0) - if ns._weaponEnchPAB then - ns._weaponEnchPAB.parent = parent - ns._weaponEnchPAB.anchorTo = container - ns._weaponEnchPAB.corner = nil - ns._weaponEnchPAB.point = "LEFT" - ns._weaponEnchPAB.relativePoint = "LEFT" - ns._weaponEnchPAB.x = -span - ns._weaponEnchPAB.y = 0 - ns._weaponEnchPAB.dir = "RIGHT" +-- Weapon enchants are not auras, so only the engine's own item-enchantment +-- source renders them (see AK.AddItemEnchantmentsToContainer): a LEADING +-- layout group on the Buffs container, flowed ahead of the aura run in every +-- grow direction and in combat. Opt-in per bar (showWeaponEnchants, the +-- pinned "Weapon Enchants" Filters row), a content source of its own and so +-- never gated on the broad-content modes. +local function BuildEnchantSpec(cfg, pad, rowGap, maxSlots) + local layout = BuildGroupLayout(cfg, pad, rowGap) + local placement = CustomAuraContainerItemEnchantmentPlacement + if placement then layout.placement = placement.BeforeAuraGroups end + local sortMethods = AuraContainerItemEnchantmentSortMethod + local sortDirs = AuraContainerSortDirection + return { + style = STYLE_BUFFS, + layout = layout, + hidePermanent = true, + maxSlots = maxSlots, + -- REVERSE keeps MAIN HAND adjacent to the aura run: the engine puts + -- the group's first element at the leading edge, and Slot order is + -- main hand, off hand, ranged. + sortMethod = sortMethods and sortMethods.Slot, + sortDirection = sortDirs and sortDirs.Reverse, + } +end + +-- Declares them on `container` (idempotent) and re-applies their layout, so +-- a live padding/icon-size change follows. Turning the row OFF is served by +-- the content signature below instead: the engine has no addon-facing +-- unregister, so the container has to be rebuilt for that. +-- Wrapped in do...end: this file sits at Lua 5.1's 200-locals-per-chunk +-- limit, so only the two helpers used below stay chunk-level locals. +local BuffAuraMax, SyncEnchantEvents +do + -- Inventory slots behind AuraContainerItemEnchantmentSlot, in the same order. + local ENCH_INV_SLOTS = { INVSLOT_MAINHAND or 16, INVSLOT_OFFHAND or 17, INVSLOT_RANGED or 18 } + + -- Weapon enchants showing right now. Only duration-bearing ones render + -- (hidePermanent in BuildEnchantSpec), so an empty or permanently enchanted + -- slot costs nothing. Equipment state, not aura data: these returns carry no + -- secret flags, in restricted combat either. + local function ActiveEnchantCount(slots) + local api = C_PaperDollInfo and C_PaperDollInfo.GetTemporaryEnchantmentInfo + if not api then return 0 end + local n = 0 + for i = 1, math.min(slots or 0, #ENCH_INV_SLOTS) do + local info = api(ENCH_INV_SLOTS[i]) + if info and info.hasExpirationTime then n = n + 1 end end - return - end - if dir == "CENTER_VERTICAL" then - local span = n * cell - container:SetPoint("CENTER", parent, "CENTER", 0, -span / 2) - if ns._weaponEnchPAB then - ns._weaponEnchPAB.parent = parent - ns._weaponEnchPAB.anchorTo = container - ns._weaponEnchPAB.corner = nil - ns._weaponEnchPAB.point = "BOTTOM" - ns._weaponEnchPAB.relativePoint = "TOP" - ns._weaponEnchPAB.x = 0 - ns._weaponEnchPAB.y = math.max(0, n - 1) * cell + PabSnap(cfg.padding or 5) - ns._weaponEnchPAB.dir = "DOWN" + return n + end + + -- Aura cap for the Buffs bar: "Max Icons" is the whole bar's budget, so the + -- cells actually showing come off it and the rendered total stays at the + -- configured number. Nothing is reserved for a slot that is not enchanted. + function BuffAuraMax(grid) + local slots = grid.enchSlots or 0 + if slots <= 0 then return grid.effectiveMax end + return math.max(0, grid.effectiveMax - ActiveEnchantCount(slots)) + end + + -- The budget moves with the enchants, so an applied or expired oil re-applies + -- it. Registered ONLY while the row is on, and the count is change-guarded: + -- WEAPON_ENCHANT_CHANGED also fires for charge ticks, which leave the cell + -- count alone. + local enchEventFrame, lastEnchCount + function SyncEnchantEvents(want) + if want then + if not enchEventFrame then + enchEventFrame = CreateFrame("Frame") + enchEventFrame:SetScript("OnEvent", function() + local n = ActiveEnchantCount(#ENCH_INV_SLOTS) + if n == lastEnchCount then return end + lastEnchCount = n + if ns.PAB_ApplyLiveConfig then ns.PAB_ApplyLiveConfig(true) end + end) + end + lastEnchCount = ActiveEnchantCount(#ENCH_INV_SLOTS) + enchEventFrame:RegisterEvent("WEAPON_ENCHANT_CHANGED") + enchEventFrame:RegisterEvent("WEAPON_SLOT_CHANGED") + elseif enchEventFrame then + enchEventFrame:UnregisterAllEvents() + lastEnchCount = nil end - return end +end - local dx, dy = 0, 0 - if dir == "RIGHT" then dx = 1 elseif dir == "LEFT" then dx = -1 - elseif dir == "UP" then dy = 1 elseif dir == "DOWN" then dy = -1 end - container:SetPoint(containerAnchor, parent, containerAnchor, dx * n * cell, dy * n * cell) - if ns._weaponEnchPAB then - ns._weaponEnchPAB.parent = parent - ns._weaponEnchPAB.anchorTo = nil - ns._weaponEnchPAB.corner = containerAnchor - ns._weaponEnchPAB.point = nil - ns._weaponEnchPAB.relativePoint = nil - ns._weaponEnchPAB.x = nil - ns._weaponEnchPAB.y = nil - ns._weaponEnchPAB.dir = dir - end -end - --- Combat-path re-shift for enchant count changes: re-seating the CONTAINER --- is combat-legal (plain SetPoint, same class as the merged-debuff ride), --- but the full ApplyLiveConfig is not -- the secure enchant trio anchors --- into the bar frame's family, which blocks the bar's own SetSize in --- lockdown. Recomputes the live grid and re-seats ONLY the container, --- INCLUDING the shift-to-zero reset when the last oil expires. -function ns.PAB_ReShiftEnchants() - local s = PAB() - if not (AK and s and buffsContainer and buffsParent) then return end - local cfg = DefaultBuffsCfg(s) - local grid = ComputeGrid(true, cfg) - ShiftBuffsForEnchants(buffsContainer, buffsParent, cfg, grid) +local function ApplyEnchants(container, cfg, pad, grid) + SyncEnchantEvents(cfg ~= nil and cfg.showWeaponEnchants == true) + if not (container and cfg and grid and cfg.showWeaponEnchants == true) then return end + if (grid.enchSlots or 0) <= 0 then return end + AK.AddItemEnchantmentsToContainer(container, + BuildEnchantSpec(cfg, pad, grid.rowGap, grid.enchSlots)) +end + +-- Buffs content signature: the resolved spell set PLUS the number of declared +-- weapon-enchant slots. A group's candidateFilters are fixed at declaration +-- and an item enchantment cannot be undeclared at all, so a change in either +-- releases the container and builds a fresh one -- including a grid shrunk +-- below three cells, which declares fewer slots than before. +local function BuffsContentSig(cfg, spells, enchSlots) + enchSlots = (cfg.showWeaponEnchants == true) and (enchSlots or 0) or 0 + return table.concat(spells, ",") .. (enchSlots > 0 and ("|e" .. enchSlots) or "") end local function CreateBars() @@ -2506,31 +2554,9 @@ local function CreateBars() local buffPad = buffCfg.padding or 5 local debuffPad = debuffCfg.padding or 5 - local buffCorner, buffSpec = BuildContainerSpec(buffsParent, buffCfg, buffGrid) + local _, buffSpec = BuildContainerSpec(buffsParent, buffCfg, buffGrid) local _, debuffSpec = BuildContainerSpec(debuffsParent, debuffCfg, debuffGrid) - -- Weapon enchant lead icons (oils/imbues are not auras; see - -- EUI_UnitFrames_WeaponEnchants.lua): opt-in (showWeaponEnchants, default - -- off -- the cell shift offsets the aura grid), exposed as the "Weapon - -- Enchants" pinned row in the bar's Filters dropdown. A content source of - -- its own, NOT gated on the broad-content modes: enchants are not auras - -- and never come from the catch-all group, so checking the row alone - -- shows just the enchant cells. They render with the bar's live style, so - -- every customization follows automatically. - if buffCfg.showWeaponEnchants == true and buffCfg.enabled ~= false then - ns._weaponEnchPAB = { parent = buffsParent, corner = buffCorner, - dir = buffCfg.growDirection or "LEFT", - -- Snapped like the shift's own cell stride above: the buttons add - -- this to an already-snapped style.width, so a raw gap would place - -- them off the engine's grid at a non-native UI scale. - pad = PabSnap(buffPad), styleKey = STYLE_BUFFS, canCancel = true } - else - ns._weaponEnchPAB = nil - end - if buffCfg.growDirection ~= "CENTER_HORIZONTAL" and buffCfg.growDirection ~= "CENTER_VERTICAL" and ns.WeaponEnchants_Layout then - ns.WeaponEnchants_Layout() - end - -- Groups are declared additively right after creation (not via spec.groups) so -- the same ApplyGroupConfig path handles both initial creation and every later -- live settings change. @@ -2557,14 +2583,20 @@ local function CreateBars() -- the sig-diffing. local buffAllChain = BuffBarChain(buffCfg) local buffSpells = ns.PAB_ResolveSpells(buffCfg) - buffsSlotSig = table.concat(buffSpells, ",") + buffsSlotSig = BuffsContentSig(buffCfg, buffSpells, buffGrid.enchSlots) + -- This runs more than once per session (master re-enable, Use Blizzard + -- Buffs off, a profile swap whose content signature moved), and a container + -- can never be destroyed: retire the previous pair first or the orphans stay + -- shown on the same parent and every aura -- and every enchant cell -- renders + -- twice. + RetireContainer(buffsContainer, declared.buffs) + RetireContainer(debuffsContainer, declared.debuffs) AK.RequestContainer(buffsParent, "player", buffSpec, function(container) buffsContainer = container ApplyContainerAnchorAndGrowth(container, buffsParent, buffCfg, buffGrid) - ShiftBuffsForEnchants(container, buffsParent, buffCfg, buffGrid) - if ns.WeaponEnchants_Layout then ns.WeaponEnchants_Layout() end + ApplyEnchants(container, buffCfg, buffPad, buffGrid) declared.buffs = {} - ApplyGroupConfig(container, buffAllChain, declared.buffs, STYLE_BUFFS, buffGrid.effectiveMax, buffPad, buffGrid.rowGap, buffCfg, BuffCandidateExtras(buffCfg)) + ApplyGroupConfig(container, buffAllChain, declared.buffs, STYLE_BUFFS, BuffAuraMax(buffGrid), buffPad, buffGrid.rowGap, buffCfg, BuffCandidateExtras(buffCfg)) if #buffSpells > 0 then local includeMap = {} for i = 1, #buffSpells do includeMap[buffSpells[i]] = true end @@ -2572,7 +2604,7 @@ local function CreateBars() key = "spells", filter = { "HELPFUL" }, style = STYLE_BUFFS, - maxFrameCount = buffGrid.effectiveMax, + maxFrameCount = BuffAuraMax(buffGrid), candidateFilters = MergeCandidateFilters({ includeSpellIDs = includeMap }, BuffCandidateExtras(buffCfg)), sortMethod = ResolveSortMethod(buffCfg), sortDirection = ResolveSortDirection(buffCfg), @@ -2745,12 +2777,6 @@ local function RestyleBars() AK.styles[STYLE_DEBUFFS] = BuildStyle(false, DefaultDebuffsCfg(s)) AK.RestyleSoon(STYLE_BUFFS) AK.RestyleSoon(STYLE_DEBUFFS) - -- RestyleSoon only reaches ENGINE buttons. The weapon-enchant cells - -- carry the bar's style too but repaint only from their own Paint, so - -- the callers that restyle without ApplyLiveConfig (global font/outline - -- changes, profile and spec-override swaps through the - -- _EUF_ReloadFrames tail) would leave them on the previous style. - if ns.WeaponEnchants_Layout then ns.WeaponEnchants_Layout() end SyncCancelCVar() end ns.PAB_Restyle = RestyleBars @@ -2842,17 +2868,14 @@ local function ApplyLiveConfig(isBuff) local cfg = isBuff and DefaultBuffsCfg(s) or DefaultDebuffsCfg(s) -- Enable toggle: a disabled bar hides its parent and skips every live - -- apply below (geometry, enchant publish, group work) -- re-enabling runs + -- apply below (geometry, enchant layout, group work) -- re-enabling runs -- the full pass. Same shape as the custom bars' early return. Use -- Blizzard Buffs and the MASTER disable stand the default bars down the - -- same way (weapon-enchant events still reach this while disabled -- - -- their registration outlives the module). + -- same way. Weapon enchants ride the container, so they stand down with + -- the bar's parent -- nothing extra to tear down here. ApplyDefaultBarShown(isBuff) if s.enabled ~= true or cfg.enabled == false or s.useBlizzardBuffs == true then - if isBuff then - ns._weaponEnchPAB = nil - if ns.WeaponEnchants_Layout then ns.WeaponEnchants_Layout() end - end + if isBuff then SyncEnchantEvents(false) end return end local grid = ComputeGrid(isBuff, cfg) @@ -2925,21 +2948,6 @@ local function ApplyLiveConfig(isBuff) ApplyContainerAnchorAndGrowth(container, parent, cfg, grid) if isBuff then - -- Keep the weapon-enchant cells riding the bar's live geometry and - -- filter state (opt-in only -- an independent content source, see the - -- publish in CreateBars), then shift the engine run inward past them. - if cfg.showWeaponEnchants == true then - local liveCorner = BuildContainerSpec(parent, cfg, grid) - ns._weaponEnchPAB = { parent = parent, corner = liveCorner, - dir = cfg.growDirection or "LEFT", - -- Snapped, as in CreateBars' publish above. - pad = PabSnap(pad), styleKey = STYLE_BUFFS, canCancel = true } - else - ns._weaponEnchPAB = nil - end - ShiftBuffsForEnchants(container, parent, cfg, grid) - if ns.WeaponEnchants_Layout then ns.WeaponEnchants_Layout() end - -- Unlock mode bakes the mover's label at registration, so a Filters -- change would leave the old name on it until the next CreateBars. -- Re-register only when the name actually changed -- this function runs @@ -2952,7 +2960,7 @@ local function ApplyLiveConfig(isBuff) if isBuff then local spells = ns.PAB_ResolveSpells(cfg) - local sig = table.concat(spells, ",") + local sig = BuffsContentSig(cfg, spells, grid.enchSlots) local allChain = BuffBarChain(cfg) if sig ~= buffsSlotSig then -- Safe to fully release+rebuild: the default Buffs container holds only the @@ -2960,16 +2968,16 @@ local function ApplyLiveConfig(isBuff) -- container's anchor/growth/rowWidth come from `spec` below -- the live -- SetContainerAnchor/etc calls above ran against the OLD container and are -- harmless overhead. A group's candidateFilters is fixed at declaration, so a - -- spell-list change requires this release+rebuild. + -- spell-list change requires this release+rebuild -- and so does the + -- weapon-enchant row, which the engine cannot undeclare at all. RetireContainer(container, declared.buffs) local _, spec = BuildContainerSpec(parent, cfg, grid) AK.RequestContainer(parent, "player", spec, function(newContainer) buffsContainer = newContainer ApplyContainerAnchorAndGrowth(newContainer, parent, cfg, grid) - ShiftBuffsForEnchants(newContainer, parent, cfg, grid) - if ns.WeaponEnchants_Layout then ns.WeaponEnchants_Layout() end + ApplyEnchants(newContainer, cfg, pad, grid) declared.buffs = {} - ApplyGroupConfig(newContainer, allChain, declared.buffs, STYLE_BUFFS, grid.effectiveMax, pad, grid.rowGap, cfg, BuffCandidateExtras(cfg)) + ApplyGroupConfig(newContainer, allChain, declared.buffs, STYLE_BUFFS, BuffAuraMax(grid), pad, grid.rowGap, cfg, BuffCandidateExtras(cfg)) if #spells > 0 then local includeMap = {} for i = 1, #spells do includeMap[spells[i]] = true end @@ -2977,7 +2985,7 @@ local function ApplyLiveConfig(isBuff) key = "spells", filter = { "HELPFUL" }, style = STYLE_BUFFS, - maxFrameCount = grid.effectiveMax, + maxFrameCount = BuffAuraMax(grid), candidateFilters = MergeCandidateFilters({ includeSpellIDs = includeMap }, BuffCandidateExtras(cfg)), sortMethod = ResolveSortMethod(cfg), sortDirection = ResolveSortDirection(cfg), @@ -2993,9 +3001,14 @@ local function ApplyLiveConfig(isBuff) -- zeroes the catch-all when `allChain` is empty, so one call covers on and -- off. The spells group isn't part of that chain path, so its -- maxFrameCount/layout/sort are refreshed here directly. - ApplyGroupConfig(container, allChain, declared.buffs, STYLE_BUFFS, grid.effectiveMax, pad, grid.rowGap, cfg, BuffCandidateExtras(cfg)) + -- + -- The enchant layout rides this branch (not the pass above): on the + -- rebuild path the container here is the one about to be retired, + -- and declaring three engine frames on it would leak them. + ApplyEnchants(container, cfg, pad, grid) + ApplyGroupConfig(container, allChain, declared.buffs, STYLE_BUFFS, BuffAuraMax(grid), pad, grid.rowGap, cfg, BuffCandidateExtras(cfg)) if declared.buffs.spells then - container:SetAuraGroupMaxFrameCount("spells", grid.effectiveMax) + container:SetAuraGroupMaxFrameCount("spells", BuffAuraMax(grid)) container:SetAuraGroupLayout("spells", BuildGroupLayout(cfg, pad, grid.rowGap)) local liveIncludeMap = {} for i = 1, #spells do liveIncludeMap[spells[i]] = true end @@ -4576,11 +4589,11 @@ local function PreviewSpellIcon(spellID) end -- Weapon-enchant preview cells, leading the bar exactly like the live ones --- (EUI_UnitFrames_WeaponEnchants.lua publishes them ahead of the engine run). +-- (the engine flows its item-enchantment group ahead of the aura groups). -- Main hand + off hand only: those are the two slots reachable in current --- retail content, even though both that module's SLOTS and Blizzard's --- UpdateTemporaryEnchantmentBuffs still poll a third (ranged) -- so a bar sized --- for three enchants shows one placeholder cell of genuine spare capacity here, +-- retail content, even though the engine declares a third (ranged), as +-- Blizzard's own UpdateTemporaryEnchantmentBuffs does -- so a bar sized for +-- three enchants shows one placeholder cell of genuine spare capacity here, -- which is what the live bar would do too. -- -- Paints the player's OWN equipped weapon icons rather than an invented sample: @@ -4595,9 +4608,12 @@ local PREVIEW_ENCHANT_SLOTS = { INVSLOT_MAINHAND or 16, INVSLOT_OFFHAND or 17 } -- unknown-icon question mark there would advertise a cell the player can never -- fill. An unarmed character still gets the single main-hand cell, so ticking -- the option always previews as something rather than silently nothing. +-- Walked in REVERSE, mirroring the live group's sortDirection: the engine puts +-- the first element at the leading edge, so main hand ends up adjacent to the +-- aura run (see BuildEnchantSpec), off hand out at the corner. local function PreviewEnchantSlots() local out = {} - for i = 1, #PREVIEW_ENCHANT_SLOTS do + for i = #PREVIEW_ENCHANT_SLOTS, 1, -1 do local slot = PREVIEW_ENCHANT_SLOTS[i] if GetInventoryItemTexture("player", slot) then out[#out + 1] = slot end end @@ -4996,28 +5012,22 @@ local function BuildPreviewSlots(isBuff, cfg, list, listLen, count) -- appear, not just their order. Truncate to `count` on the stable, sort- -- independent mixed order FIRST, then sort that fixed selection for display. -- Weapon enchants take the LEADING cells and are never sorted into the aura - -- content: they are not auras. They are also ADDITIVE, not a slice of the - -- bar's capacity -- the live container keeps its full maxFrameCount and is - -- shifted past them wholesale (ShiftBuffsForEnchants), so an enchant never - -- costs an aura its slot. + -- content: they are not auras. They do take a cell each, though: Max Icons + -- counts the whole bar, so the aura slots below are what is left after them. local numEnch, enchSlots = 0, nil if isBuff and cfg.showWeaponEnchants == true then enchSlots = PreviewEnchantSlots() numEnch = #enchSlots end - -- The two modes are genuinely different shapes and the preview mirrors both: - -- * alongside auras -- the container keeps its full maxFrameCount and is - -- shifted past the enchants wholesale, so they cost no aura its slot and - -- the first row overflows the reserved grid by the shift. - -- * enchants-only -- the grid was auto-sized FOR the enchants - -- (SyncWeaponEnchantsGrid) and the container holds no groups, so the - -- cells sit INSIDE that reserved width. The leftover cells stay as - -- placeholders on purpose: they are what explains where the bar's width - -- comes from, and dropping them made the 3-wide frame look arbitrary. - local avail = count - if isBuff and ns.PAB_IsWeaponEnchantsOnly(cfg) then - avail = math.max(0, count - numEnch) - end + -- Both modes render exactly `count` cells, which is what Max Icons promises: + -- the enchant cells lead, the aura slots take the rest. The enchants-only + -- bar keeps its leftovers as placeholders on purpose -- they are what + -- explains where the bar's width comes from. + -- + -- The live bar counts the enchants ACTUALLY up (BuffAuraMax); the preview + -- counts the weapon slots that could carry one, so an unenchanted character + -- still sees the shape the option produces. + local avail = math.max(0, count - numEnch) local mixed = isBuff and DedupeByIcon(BuildMixedRealSpells(cfg)) or nil local extraIDs @@ -5186,11 +5196,12 @@ local function RenderPreviewIcons(box, icons, isBuff, cfg, fontPath, pool) -- selects the fixed filler slice from this stable, shuffled-once order FIRST, sorts after. local list = (pool and #pool > 0 and pool) or (isBuff and PREVIEW_BUFF_SPELLS or PREVIEW_DEBUFF_SPELLS) local listLen = #list - local slots, numEnch = BuildPreviewSlots(isBuff, cfg, list, listLen, count) + -- Enchant cells are the leading `slots` entries and carry their own kind, + -- so the packing below needs no separate count of them. + local slots = BuildPreviewSlots(isBuff, cfg, list, listLen, count) -- Enchants are additive leading cells, so the rendered total exceeds the -- bar's aura capacity by however many are showing. local total = #slots - local auraCount = total - numEnch -- Icon Effects Per-Filter preview (debuffs only): deliberately NOT tied to the -- bar's own active Base Filters/Show All Debuffs state -- requiring a matching @@ -5222,7 +5233,9 @@ local function RenderPreviewIcons(box, icons, isBuff, cfg, fontPath, pool) end end - local rows = math.max(1, math.ceil(auraCount / cols)) + -- Enchant cells share the line with the auras (one engine flow, see the + -- packing block below), so they count toward the wrap. + local rows = math.max(1, math.ceil(total / cols)) -- Real per-icon flow packing: each slot's OWN actual render size (its fx Size -- override, or the bar's base iconSize) drives its own footprint directly, so @@ -5240,26 +5253,17 @@ local function RenderPreviewIcons(box, icons, isBuff, cfg, fontPath, pool) local rowWidth, rowHeight, colOffset, rowYOffset = {}, {}, {}, {} do - -- Weapon enchants lead row 0 at the bar's own corner, and the aura block - -- starts past them on EVERY row -- ShiftBuffsForEnchants moves the whole - -- container, not just its first line, so lower rows stay indented by the - -- same amount and the first row overflows the reserved grid by the - -- shift. That asymmetry is the real bar's behavior; packing enchants as - -- plain leading members of one uniform flow would wrap row 2 back to the - -- bar's edge and misrepresent it. - local enchShift = 0 - for k = 1, numEnch do - colOffset[k] = enchShift - enchShift = enchShift + slotSize[k] + pad - rowHeight[0] = math.max(rowHeight[0] or 0, slotSize[k]) - end - + -- ONE uniform flow, enchants first: the engine lays the weapon-enchant + -- frames out as their own layout group placed BEFORE the aura groups + -- on the same container, so they lead row 0 as plain members of the + -- line and row 2 wraps back to the bar's own edge, no per-row indent. + -- Their cells come out of the aura cap, so the box always fits. local runningX, runningY = {}, 0 - for r = 0, rows - 1 do runningX[r] = enchShift end - for i = numEnch + 1, total do - local r = math.floor((i - numEnch - 1) / cols) - colOffset[i] = runningX[r] - runningX[r] = runningX[r] + slotSize[i] + pad + for r = 0, rows - 1 do runningX[r] = 0 end + for i = 1, total do + local r = math.floor((i - 1) / cols) + colOffset[i] = runningX[r] or 0 + runningX[r] = (runningX[r] or 0) + slotSize[i] + pad rowHeight[r] = math.max(rowHeight[r] or 0, slotSize[i]) end for r = 0, rows - 1 do @@ -5292,10 +5296,7 @@ local function RenderPreviewIcons(box, icons, isBuff, cfg, fontPath, pool) icons[i] = btn end - -- Enchant cells all live on row 0; aura slots index into their own - -- block, which starts after them (see the packing block above). - local row = (i <= numEnch) and 0 - or math.floor((i - numEnch - 1) / cols) + local row = math.floor((i - 1) / cols) local withinLineStep = colOffset[i] local acrossLinesStep = rowYOffset[row] -- btn's own anchor point is `corner` (matching growDirection/ @@ -5755,8 +5756,7 @@ function ns.PAB_SetEnabled(v) if debuffsParent then debuffsParent:Hide() end for _, parent in pairs(customBuffParents) do parent:Hide() end for _, parent in pairs(customDebuffParents) do parent:Hide() end - ns._weaponEnchPAB = nil - if ns.WeaponEnchants_Layout then ns.WeaponEnchants_Layout() end + SyncEnchantEvents(false) -- The re-hide hooks release once the master is off: hand Blizzard's -- native display back live. ShowBlizzardPlayerAuras() @@ -5788,6 +5788,20 @@ function ns.PAB_ProfileResync() CreateBars() return end + -- Content that can only change by rebuilding the container (the resolved + -- spell set, and the weapon-enchant row the engine cannot undeclare) does + -- not reach ApplyLiveConfig on a swap, so the new profile's signature is + -- reconciled here. CreateBars is the safe lane for it: it applies the new + -- profile's sizes and positions outright instead of running + -- ApplyLiveConfig's size-rebase against the OLD profile's lastSize. + if s and s.enabled == true and s.useBlizzardBuffs ~= true and buffsParent then + local buffCfg = DefaultBuffsCfg(s) + local buffGrid = ComputeGrid(true, buffCfg) + if BuffsContentSig(buffCfg, ns.PAB_ResolveSpells(buffCfg), buffGrid.enchSlots) ~= buffsSlotSig then + CreateBars() + return + end + end ApplyDefaultBarShown(true) ApplyDefaultBarShown(false) SyncNativeAuras() @@ -5804,8 +5818,7 @@ function ns.PAB_ApplyUseBlizzard() if s.useBlizzardBuffs == true then ApplyDefaultBarShown(true) ApplyDefaultBarShown(false) - ns._weaponEnchPAB = nil - if ns.WeaponEnchants_Layout then ns.WeaponEnchants_Layout() end + SyncEnchantEvents(false) ShowBlizzardPlayerAuras() RegisterPABUnlock() else diff --git a/EllesmereUI_AuraKit.lua b/EllesmereUI_AuraKit.lua index a783d992b..62b6d396a 100644 --- a/EllesmereUI_AuraKit.lua +++ b/EllesmereUI_AuraKit.lua @@ -1362,6 +1362,67 @@ function AK.AddSlotToContainer(container, s) return f end +------------------------------------------------------------------------------ +-- Item enchantments (temporary weapon enchants: oils, imbues, stones) +-- +-- Not auras (an enchantID, no spell ID) but engine display sources since +-- 12.1: the container builds the frame through the SAME initializer as an +-- aura group, resolves the icon off the inventory slot, shows the item +-- tooltip, and runs the protected CancelTemporaryEnchantment on a cancel +-- click (style.cancelButtons feeds SetCancelAuraButtons, as for auras). +AK.ITEM_ENCH_SLOTS = { "MainHand", "OffHand", "Ranged" } + +-- Their position is LAYOUT data: they are a flow group of their own, and +-- `placement`/`layoutIndex` decides whether they lead or trail the aura +-- groups. Nothing may be anchored to a container (forbidden aspects). +function AK.SetContainerItemEnchantmentLayout(container, layout) + if not (container and layout and container.SetItemEnchantmentLayout) then return end + container:SetItemEnchantmentLayout(layout) +end + +-- BOTH halves required: the inbound setter validates before it defaults, so a +-- method without a direction is a hard error engine-side. +function AK.SetContainerItemEnchantmentSort(container, sortMethod, sortDirection) + if not (container and sortMethod and sortDirection + and container.SetItemEnchantmentSortMethod) then return end + container:SetItemEnchantmentSortMethod(sortMethod, sortDirection) +end + +-- e = { style, extraInit, hidePermanent (default true: duration-bearing +-- only), maxSlots (declare only the first N of ITEM_ENCH_SLOTS, for a +-- display too small to hold all three), layout, sortMethod, sortDirection }. +-- ONE-WAY: with no addon-facing unregister, a consumer that must stop +-- showing them releases the container and builds a fresh one. Re-calling is +-- idempotent and re-applies layout and sort only. +function AK.AddItemEnchantmentsToContainer(container, e) + if not (container and e and container.AddItemEnchantment) then return end + local slotEnum = AuraContainerItemEnchantmentSlot + if not slotEnum then return end + local cd = containerData[container] + if cd and not cd.itemEnchFrames then cd.itemEnchFrames = {} end + local tracked = cd and cd.itemEnchFrames + local names = AK.ITEM_ENCH_SLOTS + local last = math.min(e.maxSlots or #names, #names) + for i = 1, last do + local name = names[i] + local slot = slotEnum[name] + -- HasItemEnchantment is engine-private and a second declaration for + -- the same slot asserts, so the declared set is ours to keep. pcall'd + -- anyway: ReleaseContainer drops the whole containerData entry, so a + -- container that is released and then re-declared (no consumer does + -- that today) would hit that assert with the tracking gone. + if slot ~= nil and not (tracked and tracked[name]) then + local ok, frame = pcall(container.AddItemEnchantment, container, slot, { + initializeFrame = AK.MakeInitializer(e.style, e.extraInit), + hidePermanent = e.hidePermanent ~= false, + }) + if ok and tracked then tracked[name] = frame or true end + end + end + AK.SetContainerItemEnchantmentLayout(container, e.layout) + AK.SetContainerItemEnchantmentSort(container, e.sortMethod, e.sortDirection) +end + -- Unit LAST: unit assignment re-evaluates event registrations, and those -- are gated on the container having groups/slots. Setting the unit before -- declaring content leaves UNIT_AURA unregistered (the Blizzard reference @@ -1545,17 +1606,23 @@ end function AK.ReleaseContainer(container) if not container then return end local data = containerData[container] - if data and data.slotFrames then - for _, slotButton in pairs(data.slotFrames) do - local d = bd[slotButton] - if d then - if d.styleKey and styleButtons[d.styleKey] then - styleButtons[d.styleKey][slotButton] = nil - end - bd[slotButton] = nil + local function Untrack(button) + local d = type(button) == "table" and bd[button] + if d then + if d.styleKey and styleButtons[d.styleKey] then + styleButtons[d.styleKey][button] = nil end + bd[button] = nil end end + if data and data.slotFrames then + for _, slotButton in pairs(data.slotFrames) do Untrack(slotButton) end + end + -- Item enchantment frames come back as handles on declaration, so unlike + -- group buttons they can be untracked instead of left as restyle zombies. + if data and data.itemEnchFrames then + for _, enchButton in pairs(data.itemEnchFrames) do Untrack(enchButton) end + end containerData[container] = nil container:Hide() end