-------------------------------------------------------------------------------- -- Nanami-UI: ActionBars -- -- DESIGN: Blizzard's ActionButton_GetPagedID() identifies action slots by -- checking button:GetParent():GetName(). We MUST NOT reparent multi-bar -- buttons. Instead we reposition the original bar FRAMES and style buttons -- in-place. Only ActionButton1-12 are safely reparented (page-based calc). -- -- ShapeshiftBarFrame and PetActionBarFrame are children of MainMenuBar, so -- they become invisible when we hide MainMenuBar. We reparent them to -- UIParent before hiding. -------------------------------------------------------------------------------- SFrames.ActionBars = {} local AB = SFrames.ActionBars local DEFAULTS = { enable = true, buttonSize = 36, buttonGap = 2, smallBarSize = 27, scale = 1.0, alpha = 1.0, barCount = 3, showHotkey = true, showMacroName = false, rangeColoring = true, showPetBar = true, showStanceBar = true, showRightBars = true, alwaysShowGrid = false, buttonRounded = false, buttonInnerShadow = false, hideGryphon = true, gryphonStyle = "dragonflight", gryphonOnTop = false, gryphonWidth = 64, gryphonHeight = 64, gryphonOffsetX = 30, gryphonOffsetY = 0, bottomOffsetX = 0, bottomOffsetY = 2, rightOffsetX = -4, rightOffsetY = -80, rightBar1PerRow = 1, rightBar2PerRow = 1, bottomBar1PerRow = 12, bottomBar2PerRow = 12, bottomBar3PerRow = 12, } local BUTTONS_PER_ROW = 12 -- 狮鹫样式定义:每种样式包含联盟和部落纹理路径 local GRYPHON_STYLES = { { key = "dragonflight", label = "巨龙时代", alliance = "Interface\\AddOns\\Nanami-UI\\img\\df-gryphon", horde = "Interface\\AddOns\\Nanami-UI\\img\\df-wyvern" }, { key = "dragonflight_beta", label = "巨龙时代 (Beta)", alliance = "Interface\\AddOns\\Nanami-UI\\img\\df-gryphon-beta", horde = "Interface\\AddOns\\Nanami-UI\\img\\df-gryphon-beta" }, { key = "classic", label = "经典", alliance = "Interface\\MainMenuBar\\UI-MainMenuBar-EndCap-Human", horde = "Interface\\MainMenuBar\\UI-MainMenuBar-EndCap-Human" }, { key = "cat", label = "猫", alliance = "Interface\\AddOns\\Nanami-UI\\img\\cat", horde = "Interface\\AddOns\\Nanami-UI\\img\\cat" }, } local function GetGryphonTexPath(styleKey) local faction = UnitFactionGroup and UnitFactionGroup("player") or "Alliance" for _, s in ipairs(GRYPHON_STYLES) do if s.key == styleKey then return (faction == "Horde") and s.horde or s.alliance end end return GRYPHON_STYLES[1].alliance end -------------------------------------------------------------------------------- -- Layout helpers -------------------------------------------------------------------------------- local function LayoutRow(buttons, parent, size, gap) for i, b in ipairs(buttons) do b:SetWidth(size) b:SetHeight(size) b:ClearAllPoints() if i == 1 then b:SetPoint("BOTTOMLEFT", parent, "BOTTOMLEFT", 0, 0) else b:SetPoint("LEFT", buttons[i - 1], "RIGHT", gap, 0) end end end local function LayoutColumn(buttons, parent, size, gap) for i, b in ipairs(buttons) do b:SetWidth(size) b:SetHeight(size) b:ClearAllPoints() if i == 1 then b:SetPoint("TOPRIGHT", parent, "TOPRIGHT", 0, 0) else b:SetPoint("TOP", buttons[i - 1], "BOTTOM", 0, -gap) end end end local function LayoutGrid(buttons, parent, size, gap, perRow) local count = table.getn(buttons) if count == 0 then return end local numCols = perRow local numRows = math.ceil(count / perRow) parent:SetWidth(numCols * size + math.max(numCols - 1, 0) * gap) parent:SetHeight(numRows * size + math.max(numRows - 1, 0) * gap) for i, b in ipairs(buttons) do b:SetWidth(size) b:SetHeight(size) b:ClearAllPoints() local col = math.fmod(i - 1, perRow) local row = math.floor((i - 1) / perRow) b:SetPoint("TOPLEFT", parent, "TOPLEFT", col * (size + gap), -row * (size + gap)) end end -------------------------------------------------------------------------------- -- DB -------------------------------------------------------------------------------- function AB:GetDB() if not SFramesDB then SFramesDB = {} end if type(SFramesDB.ActionBars) ~= "table" then SFramesDB.ActionBars = {} end local db = SFramesDB.ActionBars for k, v in pairs(DEFAULTS) do if db[k] == nil then db[k] = v end end return db end -------------------------------------------------------------------------------- -- Style helpers -------------------------------------------------------------------------------- local styledButtons = {} local function HideNormalTexture(nt) if not nt then return end if nt.SetAlpha then nt:SetAlpha(0) end if nt.SetWidth then nt:SetWidth(0) end if nt.SetHeight then nt:SetHeight(0) end end local function StyleButton(b) if not b or styledButtons[b] then return end styledButtons[b] = true local nt = _G[b:GetName() .. "NormalTexture"] HideNormalTexture(nt) b.SetNormalTexture = function() end -- pfUI approach: backdrop on a SEPARATE child frame at lower frame level -- so it renders behind the button's own textures (Icon etc.) if b:GetBackdrop() then b:SetBackdrop(nil) end local level = b:GetFrameLevel() local bd = CreateFrame("Frame", nil, b) bd:SetFrameLevel(level > 0 and (level - 1) or 0) bd:SetAllPoints(b) SFrames:CreateBackdrop(bd) b.sfBackdrop = bd local icon = _G[b:GetName() .. "Icon"] if icon then icon:ClearAllPoints() icon:SetPoint("TOPLEFT", b, "TOPLEFT", 2, -2) icon:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", -2, 2) icon:SetTexCoord(0.07, 0.93, 0.07, 0.93) end local cd = _G[b:GetName() .. "Cooldown"] if cd then cd:ClearAllPoints() cd:SetPoint("TOPLEFT", b, "TOPLEFT", 2, -2) cd:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", -2, 2) end local hotkey = _G[b:GetName() .. "HotKey"] if hotkey then hotkey:SetFont(SFrames:GetFont(), 9, "OUTLINE") hotkey:ClearAllPoints() hotkey:SetPoint("TOPRIGHT", b, "TOPRIGHT", -2, -2) end local count = _G[b:GetName() .. "Count"] if count then count:SetFont(SFrames:GetFont(), 9, "OUTLINE") count:ClearAllPoints() count:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", -2, 2) end local macroName = _G[b:GetName() .. "Name"] if macroName then macroName:SetFont(SFrames:GetFont(), 8, "OUTLINE") macroName:ClearAllPoints() macroName:SetPoint("BOTTOM", b, "BOTTOM", 0, 2) end local floatingBG = _G[b:GetName() .. "FloatingBG"] if floatingBG then floatingBG:SetAlpha(0) end local border = _G[b:GetName() .. "Border"] if border then border:SetAlpha(0) end end local function KillPetNormalTextures(b) local name = b:GetName() -- Pet buttons use NormalTexture AND NormalTexture2 for _, suffix in ipairs({"NormalTexture", "NormalTexture2"}) do local nt = _G[name .. suffix] if nt and nt.SetTexture then nt:SetTexture(nil) end if nt and nt.SetAlpha then nt:SetAlpha(0) end if nt and nt.Hide then nt:Hide() end end local gnt = b.GetNormalTexture and b:GetNormalTexture() if gnt and gnt.SetTexture then gnt:SetTexture(nil) end if gnt and gnt.SetAlpha then gnt:SetAlpha(0) end end local function StylePetButton(b) if not b or styledButtons[b] then return end styledButtons[b] = true KillPetNormalTextures(b) b.SetNormalTexture = function() end -- pfUI approach: backdrop on separate child frame at lower frame level if b.GetBackdrop and b:GetBackdrop() then b:SetBackdrop(nil) end local level = b:GetFrameLevel() local bd = CreateFrame("Frame", nil, b) bd:SetFrameLevel(level > 0 and (level - 1) or 0) bd:SetAllPoints(b) SFrames:CreateBackdrop(bd) b.sfBackdrop = bd local icon = _G[b:GetName() .. "Icon"] if icon then icon:ClearAllPoints() icon:SetPoint("TOPLEFT", b, "TOPLEFT", 2, -2) icon:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", -2, 2) icon:SetTexCoord(0.07, 0.93, 0.07, 0.93) end local cd = _G[b:GetName() .. "Cooldown"] if cd then cd:ClearAllPoints() cd:SetPoint("TOPLEFT", b, "TOPLEFT", 2, -2) cd:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", -2, 2) end local ab = _G[b:GetName() .. "AutoCastable"] if ab then ab:ClearAllPoints() ab:SetPoint("TOPLEFT", b, "TOPLEFT", -4, 4) ab:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", 4, -4) end local hotkey = _G[b:GetName() .. "HotKey"] if hotkey then hotkey:SetFont(SFrames:GetFont(), 9, "OUTLINE") hotkey:ClearAllPoints() hotkey:SetPoint("TOPRIGHT", b, "TOPRIGHT", -2, -2) end local floatingBG = _G[b:GetName() .. "FloatingBG"] if floatingBG then floatingBG:SetAlpha(0) end end -------------------------------------------------------------------------------- -- Button visual effects (rounded corners + inner shadow) -------------------------------------------------------------------------------- local function CreateInnerShadow(btn) if btn.sfInnerShadow then return btn.sfInnerShadow end local shadow = {} local thickness = 4 local top = btn:CreateTexture(nil, "OVERLAY") top:SetTexture("Interface\\Buttons\\WHITE8X8") top:SetHeight(thickness) top:SetGradientAlpha("VERTICAL", 0, 0, 0, 0, 0, 0, 0, 0.5) shadow.top = top local bot = btn:CreateTexture(nil, "OVERLAY") bot:SetTexture("Interface\\Buttons\\WHITE8X8") bot:SetHeight(thickness) bot:SetGradientAlpha("VERTICAL", 0, 0, 0, 0.5, 0, 0, 0, 0) shadow.bottom = bot local left = btn:CreateTexture(nil, "OVERLAY") left:SetTexture("Interface\\Buttons\\WHITE8X8") left:SetWidth(thickness) left:SetGradientAlpha("HORIZONTAL", 0, 0, 0, 0.5, 0, 0, 0, 0) shadow.left = left local right = btn:CreateTexture(nil, "OVERLAY") right:SetTexture("Interface\\Buttons\\WHITE8X8") right:SetWidth(thickness) right:SetGradientAlpha("HORIZONTAL", 0, 0, 0, 0, 0, 0, 0, 0.5) shadow.right = right btn.sfInnerShadow = shadow return shadow end local function ApplyButtonVisuals(btn, rounded, shadow) local bd = btn.sfBackdrop if not bd then return end local inset = rounded and 3 or 2 btn.sfIconInset = inset if rounded then bd:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 12, insets = { left = 3, right = 3, top = 3, bottom = 3 } }) else bd:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", tile = false, tileSize = 0, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } }) end local A = SFrames.ActiveTheme if A and A.panelBg then bd:SetBackdropColor(A.panelBg[1], A.panelBg[2], A.panelBg[3], A.panelBg[4] or 0.9) bd:SetBackdropBorderColor(A.panelBorder[1], A.panelBorder[2], A.panelBorder[3], A.panelBorder[4] or 1) else bd:SetBackdropColor(0.1, 0.1, 0.1, 0.9) bd:SetBackdropBorderColor(0, 0, 0, 1) end local name = btn:GetName() if name then local icon = _G[name .. "Icon"] if icon then icon:ClearAllPoints() icon:SetPoint("TOPLEFT", btn, "TOPLEFT", inset, -inset) icon:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -inset, inset) end local cd = _G[name .. "Cooldown"] if cd then cd:ClearAllPoints() cd:SetPoint("TOPLEFT", btn, "TOPLEFT", inset, -inset) cd:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -inset, inset) end end if btn.sfRangeOverlay then btn.sfRangeOverlay:ClearAllPoints() btn.sfRangeOverlay:SetPoint("TOPLEFT", btn, "TOPLEFT", inset, -inset) btn.sfRangeOverlay:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -inset, inset) end if shadow then if not btn.sfInnerShadow then CreateInnerShadow(btn) end local s = btn.sfInnerShadow s.top:ClearAllPoints() s.top:SetPoint("TOPLEFT", btn, "TOPLEFT", inset, -inset) s.top:SetPoint("TOPRIGHT", btn, "TOPRIGHT", -inset, -inset) s.bottom:ClearAllPoints() s.bottom:SetPoint("BOTTOMLEFT", btn, "BOTTOMLEFT", inset, inset) s.bottom:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -inset, inset) s.left:ClearAllPoints() s.left:SetPoint("TOPLEFT", btn, "TOPLEFT", inset, -inset) s.left:SetPoint("BOTTOMLEFT", btn, "BOTTOMLEFT", inset, inset) s.right:ClearAllPoints() s.right:SetPoint("TOPRIGHT", btn, "TOPRIGHT", -inset, -inset) s.right:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -inset, inset) s.top:Show(); s.bottom:Show(); s.left:Show(); s.right:Show() else if btn.sfInnerShadow then local s = btn.sfInnerShadow s.top:Hide(); s.bottom:Hide(); s.left:Hide(); s.right:Hide() end end end -------------------------------------------------------------------------------- -- Hide Blizzard chrome -------------------------------------------------------------------------------- function AB:HideBlizzardBars() SHOW_MULTI_ACTIONBAR_1 = 1 SHOW_MULTI_ACTIONBAR_2 = 1 SHOW_MULTI_ACTIONBAR_3 = 1 SHOW_MULTI_ACTIONBAR_4 = 1 -- Reparent stance/pet bars BEFORE hiding MainMenuBar (they are children of it) if ShapeshiftBarFrame then ShapeshiftBarFrame:SetParent(UIParent) end if PetActionBarFrame then PetActionBarFrame:SetParent(UIParent) end if MultiActionBar_Update then MultiActionBar_Update() end if MainMenuBar then MainMenuBar:UnregisterAllEvents() MainMenuBar:Hide() MainMenuBar.Show = function() end end local hideArt = { "MainMenuBarArtFrame", "MainMenuExpBar", "ReputationWatchBar", } for _, name in ipairs(hideArt) do local f = _G[name] if f then f:Hide() if f.SetHeight then f:SetHeight(0) end f.Show = function() end end end -- BonusActionBarFrame: keep functional for druid form switching, just hide art if BonusActionBarFrame then BonusActionBarFrame:SetParent(UIParent) if BonusActionBarFrame.SetBackdrop then BonusActionBarFrame:SetBackdrop(nil) end for i = 0, 4 do local tex = _G["BonusActionBarTexture" .. i] if tex then tex:SetAlpha(0) end end end if MultiBarBottomLeftArtFrame then MultiBarBottomLeftArtFrame:Hide() end if MultiBarBottomRightArtFrame then MultiBarBottomRightArtFrame:Hide() end -- 隐藏原版狮鹫端帽(Texture 对象,非 Frame) local endcaps = { "MainMenuBarLeftEndCap", "MainMenuBarRightEndCap" } for _, name in ipairs(endcaps) do local f = _G[name] if f then f:Hide() f.Show = function() end end end if MainMenuBarBackpackButton then MainMenuBarBackpackButton:Hide() end for slot = 0, 3 do local b = _G["CharacterBag" .. slot .. "Slot"] if b then b:Hide() end end if KeyRingButton then KeyRingButton:Hide() end end -------------------------------------------------------------------------------- -- Create bar structure (once) -------------------------------------------------------------------------------- function AB:CreateBars() local db = self:GetDB() local size = db.buttonSize local gap = db.buttonGap local bpr1 = db.bottomBar1PerRow or 12 local bpr1Cols = bpr1 local bpr1Rows = math.ceil(BUTTONS_PER_ROW / bpr1) local bar1W = bpr1Cols * size + math.max(bpr1Cols - 1, 0) * gap local bar1H = bpr1Rows * size + math.max(bpr1Rows - 1, 0) * gap -- === BOTTOM BARS === local anchor = CreateFrame("Frame", "SFramesActionBarAnchor", UIParent) anchor:SetWidth(bar1W) anchor:SetHeight(bar1H) local abPos = SFramesDB and SFramesDB.Positions and SFramesDB.Positions["ActionBarBottom"] if abPos and abPos.point and abPos.relativePoint then anchor:SetPoint(abPos.point, UIParent, abPos.relativePoint, abPos.xOfs or 0, abPos.yOfs or 0) else anchor:SetPoint("BOTTOM", UIParent, "BOTTOM", db.bottomOffsetX, db.bottomOffsetY) end anchor:SetScale(db.scale) self.anchor = anchor -- Row 1: ActionButton1-12 (safe to reparent, uses page calc) local row1 = CreateFrame("Frame", "SFramesMainBar", anchor) row1:SetWidth(bar1W) row1:SetHeight(bar1H) row1:SetPoint("BOTTOMLEFT", anchor, "BOTTOMLEFT", 0, 0) self.row1 = row1 self.mainButtons = {} for i = 1, BUTTONS_PER_ROW do local b = _G["ActionButton" .. i] if b then b:SetParent(row1) StyleButton(b) table.insert(self.mainButtons, b) end end -- === BONUS ACTION BAR (druid forms, warrior stances, etc.) === self.bonusButtons = {} if BonusActionBarFrame then BonusActionBarFrame:SetParent(row1) BonusActionBarFrame:ClearAllPoints() BonusActionBarFrame:SetPoint("BOTTOMLEFT", row1, "BOTTOMLEFT", 0, 0) BonusActionBarFrame:SetWidth(bar1W) BonusActionBarFrame:SetHeight(bar1H) BonusActionBarFrame:SetFrameLevel(row1:GetFrameLevel() + 5) for i = 1, BUTTONS_PER_ROW do local b = _G["BonusActionButton" .. i] if b then StyleButton(b) table.insert(self.bonusButtons, b) end end BonusActionBarFrame:SetScript("OnShow", function() for i = 1, BUTTONS_PER_ROW do local b = _G["BonusActionButton" .. i] if b and BonusActionButton_Update then BonusActionButton_Update(b) end end local db = AB:GetDB() local s = db.buttonSize local g = db.buttonGap local bpr = db.bottomBar1PerRow or 12 LayoutGrid(AB.bonusButtons, BonusActionBarFrame, s, g, bpr) local btnLevel = BonusActionBarFrame:GetFrameLevel() + 1 for _, btn in ipairs(AB.bonusButtons) do btn:EnableMouse(true) btn:SetFrameLevel(btnLevel) if btn.sfBackdrop then btn.sfBackdrop:SetFrameLevel(btnLevel - 1) end end end) end -- === PAGE INDICATOR === local pi = row1:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") pi:SetPoint("RIGHT", row1, "LEFT", -4, 0) pi:SetTextColor(0.9, 0.8, 0.2, 0.9) self.pageIndicator = pi -- Row 2: independent anchor for MultiBarBottomLeft local bpr2 = db.bottomBar2PerRow or 12 local bpr2Cols = bpr2 local bpr2Rows = math.ceil(BUTTONS_PER_ROW / bpr2) local bar2W = bpr2Cols * size + math.max(bpr2Cols - 1, 0) * gap local bar2H = bpr2Rows * size + math.max(bpr2Rows - 1, 0) * gap local row2Anchor = CreateFrame("Frame", "SFramesRow2Anchor", UIParent) row2Anchor:SetWidth(bar2W) row2Anchor:SetHeight(bar2H) row2Anchor:SetScale(db.scale) self.row2Anchor = row2Anchor local r2Pos = SFramesDB and SFramesDB.Positions and SFramesDB.Positions["ActionBarRow2"] if r2Pos and r2Pos.point and r2Pos.relativePoint then row2Anchor:SetPoint(r2Pos.point, UIParent, r2Pos.relativePoint, r2Pos.xOfs or 0, r2Pos.yOfs or 0) else row2Anchor:SetPoint("BOTTOMLEFT", anchor, "TOPLEFT", 0, gap) end if MultiBarBottomLeft then MultiBarBottomLeft:SetParent(row2Anchor) MultiBarBottomLeft:ClearAllPoints() MultiBarBottomLeft:SetPoint("BOTTOMLEFT", row2Anchor, "BOTTOMLEFT", 0, 0) MultiBarBottomLeft:SetWidth(bar2W) MultiBarBottomLeft:SetHeight(bar2H) MultiBarBottomLeft:Show() end self.row2 = MultiBarBottomLeft self.bar2Buttons = {} for i = 1, BUTTONS_PER_ROW do local b = _G["MultiBarBottomLeftButton" .. i] if b then b:Show() StyleButton(b) table.insert(self.bar2Buttons, b) end end -- Row 3: independent anchor for MultiBarBottomRight local bpr3 = db.bottomBar3PerRow or 12 local bpr3Cols = bpr3 local bpr3Rows = math.ceil(BUTTONS_PER_ROW / bpr3) local bar3W = bpr3Cols * size + math.max(bpr3Cols - 1, 0) * gap local bar3H = bpr3Rows * size + math.max(bpr3Rows - 1, 0) * gap local row3Anchor = CreateFrame("Frame", "SFramesRow3Anchor", UIParent) row3Anchor:SetWidth(bar3W) row3Anchor:SetHeight(bar3H) row3Anchor:SetScale(db.scale) self.row3Anchor = row3Anchor local r3Pos = SFramesDB and SFramesDB.Positions and SFramesDB.Positions["ActionBarRow3"] if r3Pos and r3Pos.point and r3Pos.relativePoint then row3Anchor:SetPoint(r3Pos.point, UIParent, r3Pos.relativePoint, r3Pos.xOfs or 0, r3Pos.yOfs or 0) else row3Anchor:SetPoint("BOTTOMLEFT", row2Anchor, "TOPLEFT", 0, gap) end if MultiBarBottomRight then MultiBarBottomRight:SetParent(row3Anchor) MultiBarBottomRight:ClearAllPoints() MultiBarBottomRight:SetPoint("BOTTOMLEFT", row3Anchor, "BOTTOMLEFT", 0, 0) MultiBarBottomRight:SetWidth(bar3W) MultiBarBottomRight:SetHeight(bar3H) MultiBarBottomRight:Show() end self.row3 = MultiBarBottomRight self.bar3Buttons = {} for i = 1, BUTTONS_PER_ROW do local b = _G["MultiBarBottomRightButton" .. i] if b then b:Show() StyleButton(b) table.insert(self.bar3Buttons, b) end end -- === 狮鹫端帽 === local srcL = MainMenuBarLeftEndCap local capW = (srcL and srcL.GetWidth) and srcL:GetWidth() or 128 local capH = (srcL and srcL.GetHeight) and srcL:GetHeight() or 76 local gryphonTex = GetGryphonTexPath(db.gryphonStyle) local leftCap = CreateFrame("Frame", "SFramesGryphonLeft", UIParent) leftCap:SetWidth(capW) leftCap:SetHeight(capH) local leftImg = leftCap:CreateTexture(nil, "ARTWORK") leftImg:SetAllPoints() leftImg:SetTexture(gryphonTex) leftCap._sfTex = leftImg self.gryphonLeft = leftCap local rightCap = CreateFrame("Frame", "SFramesGryphonRight", UIParent) rightCap:SetWidth(capW) rightCap:SetHeight(capH) local rightImg = rightCap:CreateTexture(nil, "ARTWORK") rightImg:SetAllPoints() rightImg:SetTexture(gryphonTex) rightImg:SetTexCoord(1, 0, 0, 1) rightCap._sfTex = rightImg self.gryphonRight = rightCap -- === RIGHT-SIDE BARS (independent holders) === -- Migrate legacy "ActionBarRight" position to "RightBar1" if SFramesDB and SFramesDB.Positions then if SFramesDB.Positions["ActionBarRight"] and not SFramesDB.Positions["RightBar1"] then SFramesDB.Positions["RightBar1"] = SFramesDB.Positions["ActionBarRight"] end end local perRow1 = db.rightBar1PerRow or 1 local perRow2 = db.rightBar2PerRow or 1 local numCols1 = perRow1 local numRows1 = math.ceil(BUTTONS_PER_ROW / perRow1) local rb1W = numCols1 * size + math.max(numCols1 - 1, 0) * gap local rb1H = numRows1 * size + math.max(numRows1 - 1, 0) * gap local numCols2 = perRow2 local numRows2 = math.ceil(BUTTONS_PER_ROW / perRow2) local rb2W = numCols2 * size + math.max(numCols2 - 1, 0) * gap local rb2H = numRows2 * size + math.max(numRows2 - 1, 0) * gap -- RightBar1: MultiBarRight local rb1Holder = CreateFrame("Frame", "SFramesRightBar1Holder", UIParent) rb1Holder:SetWidth(rb1W) rb1Holder:SetHeight(rb1H) rb1Holder:SetScale(db.scale) self.rightBar1Holder = rb1Holder local rb1Pos = SFramesDB and SFramesDB.Positions and SFramesDB.Positions["RightBar1"] if rb1Pos and rb1Pos.point and rb1Pos.relativePoint then rb1Holder:SetPoint(rb1Pos.point, UIParent, rb1Pos.relativePoint, rb1Pos.xOfs or 0, rb1Pos.yOfs or 0) else rb1Holder:SetPoint("RIGHT", UIParent, "RIGHT", db.rightOffsetX, db.rightOffsetY) end if MultiBarRight then MultiBarRight:SetParent(rb1Holder) MultiBarRight:ClearAllPoints() MultiBarRight:SetPoint("TOPLEFT", rb1Holder, "TOPLEFT", 0, 0) MultiBarRight:Show() end self.rightButtons = {} for i = 1, BUTTONS_PER_ROW do local b = _G["MultiBarRightButton" .. i] if b then b:Show() StyleButton(b) table.insert(self.rightButtons, b) end end -- RightBar2: MultiBarLeft local rb2Holder = CreateFrame("Frame", "SFramesRightBar2Holder", UIParent) rb2Holder:SetWidth(rb2W) rb2Holder:SetHeight(rb2H) rb2Holder:SetScale(db.scale) self.rightBar2Holder = rb2Holder local rb2Pos = SFramesDB and SFramesDB.Positions and SFramesDB.Positions["RightBar2"] if rb2Pos and rb2Pos.point and rb2Pos.relativePoint then rb2Holder:SetPoint(rb2Pos.point, UIParent, rb2Pos.relativePoint, rb2Pos.xOfs or 0, rb2Pos.yOfs or 0) else rb2Holder:SetPoint("TOPRIGHT", rb1Holder, "TOPLEFT", -gap, 0) end if MultiBarLeft then MultiBarLeft:SetParent(rb2Holder) MultiBarLeft:ClearAllPoints() MultiBarLeft:SetPoint("TOPLEFT", rb2Holder, "TOPLEFT", 0, 0) MultiBarLeft:Show() end self.leftButtons = {} for i = 1, BUTTONS_PER_ROW do local b = _G["MultiBarLeftButton" .. i] if b then b:Show() StyleButton(b) table.insert(self.leftButtons, b) end end -- Legacy compat: keep rightHolder reference pointing to rb1Holder self.rightHolder = rb1Holder -- === STANCE BAR === local stanceHolder = CreateFrame("Frame", "SFramesStanceHolder", UIParent) stanceHolder:SetWidth(bar1W) stanceHolder:SetHeight(size) stanceHolder:SetScale(db.scale) self.stanceHolder = stanceHolder self.stanceButtons = {} for i = 1, 10 do local b = _G["ShapeshiftButton" .. i] if b then b:SetParent(stanceHolder) StyleButton(b) table.insert(self.stanceButtons, b) end end -- === PET BAR === local petHolder = CreateFrame("Frame", "SFramesPetHolder", UIParent) petHolder:SetWidth(bar1W) petHolder:SetHeight(size) petHolder:SetScale(db.scale) self.petHolder = petHolder self.petButtons = {} for i = 1, 10 do local b = _G["PetActionButton" .. i] if b then b:SetParent(petHolder) StylePetButton(b) table.insert(self.petButtons, b) end end end -------------------------------------------------------------------------------- -- Adaptive text sizing: scale hotkey / count / macro-name fonts to button size -------------------------------------------------------------------------------- local function UpdateButtonTexts(buttons, btnSize, showHotkey, showMacroName) local fontSize = math.max(6, math.floor(btnSize * 0.25 + 0.5)) local nameSize = math.max(6, math.floor(btnSize * 0.22 + 0.5)) local font = SFrames:GetFont() for _, b in ipairs(buttons) do local bname = b:GetName() local hotkey = _G[bname .. "HotKey"] if hotkey then hotkey:SetFont(font, fontSize, "OUTLINE") if showHotkey then hotkey:Show() else hotkey:Hide() end end local count = _G[bname .. "Count"] if count then count:SetFont(font, fontSize, "OUTLINE") end local mn = _G[bname .. "Name"] if mn then mn:SetFont(font, nameSize, "OUTLINE") if showMacroName then mn:Show() else mn:Hide() end end end end -------------------------------------------------------------------------------- -- Apply config -------------------------------------------------------------------------------- function AB:ApplyConfig() if not self.anchor then return end local db = self:GetDB() local size = db.buttonSize local gap = db.buttonGap -- Bottom bar 1 dimensions local bpr1 = db.bottomBar1PerRow or 12 local bpr1Cols = bpr1 local bpr1Rows = math.ceil(BUTTONS_PER_ROW / bpr1) local bar1W = bpr1Cols * size + math.max(bpr1Cols - 1, 0) * gap local bar1H = bpr1Rows * size + math.max(bpr1Rows - 1, 0) * gap -- Bottom bars anchor (row1 only) self.anchor:SetScale(db.scale) self.anchor:SetWidth(bar1W) self.anchor:SetHeight(bar1H) -- Row 1 self.row1:SetWidth(bar1W) self.row1:SetHeight(bar1H) LayoutGrid(self.mainButtons, self.row1, size, gap, bpr1) -- Bonus bar (druid forms) — same layout as row 1, overlays when active if self.bonusButtons and BonusActionBarFrame then BonusActionBarFrame:SetWidth(bar1W) BonusActionBarFrame:SetHeight(bar1H) LayoutGrid(self.bonusButtons, BonusActionBarFrame, size, gap, bpr1) end -- Bottom bar 2 dimensions local bpr2 = db.bottomBar2PerRow or 12 local bpr2Cols = bpr2 local bpr2Rows = math.ceil(BUTTONS_PER_ROW / bpr2) local bar2W = bpr2Cols * size + math.max(bpr2Cols - 1, 0) * gap local bar2H = bpr2Rows * size + math.max(bpr2Rows - 1, 0) * gap -- Row 2 (independent anchor) if self.row2Anchor then self.row2Anchor:SetScale(db.scale) self.row2Anchor:SetWidth(bar2W) self.row2Anchor:SetHeight(bar2H) end if self.row2 then self.row2:SetWidth(bar2W) self.row2:SetHeight(bar2H) self.row2:ClearAllPoints() self.row2:SetPoint("BOTTOMLEFT", self.row2Anchor or self.anchor, "BOTTOMLEFT", 0, 0) LayoutGrid(self.bar2Buttons, self.row2, size, gap, bpr2) if db.barCount >= 2 then if self.row2Anchor then self.row2Anchor:Show() end self.row2:Show() else if self.row2Anchor then self.row2Anchor:Hide() end self.row2:Hide() end end -- Bottom bar 3 dimensions local bpr3 = db.bottomBar3PerRow or 12 local bpr3Cols = bpr3 local bpr3Rows = math.ceil(BUTTONS_PER_ROW / bpr3) local bar3W = bpr3Cols * size + math.max(bpr3Cols - 1, 0) * gap local bar3H = bpr3Rows * size + math.max(bpr3Rows - 1, 0) * gap -- Row 3 (independent anchor) if self.row3Anchor then self.row3Anchor:SetScale(db.scale) self.row3Anchor:SetWidth(bar3W) self.row3Anchor:SetHeight(bar3H) end if self.row3 then self.row3:SetWidth(bar3W) self.row3:SetHeight(bar3H) self.row3:ClearAllPoints() self.row3:SetPoint("BOTTOMLEFT", self.row3Anchor or self.anchor, "BOTTOMLEFT", 0, 0) LayoutGrid(self.bar3Buttons, self.row3, size, gap, bpr3) if db.barCount >= 3 then if self.row3Anchor then self.row3Anchor:Show() end self.row3:Show() else if self.row3Anchor then self.row3Anchor:Hide() end self.row3:Hide() end end -- Right-side bar 1 (MultiBarRight, grid layout) if self.rightBar1Holder then self.rightBar1Holder:SetScale(db.scale) local perRow1 = db.rightBar1PerRow or 1 local numCols1 = perRow1 local numRows1 = math.ceil(BUTTONS_PER_ROW / perRow1) local rb1W = numCols1 * size + math.max(numCols1 - 1, 0) * gap local rb1H = numRows1 * size + math.max(numRows1 - 1, 0) * gap self.rightBar1Holder:SetWidth(rb1W) self.rightBar1Holder:SetHeight(rb1H) if MultiBarRight then MultiBarRight:SetWidth(rb1W) MultiBarRight:SetHeight(rb1H) LayoutGrid(self.rightButtons, MultiBarRight, size, gap, perRow1) MultiBarRight:ClearAllPoints() MultiBarRight:SetPoint("TOPLEFT", self.rightBar1Holder, "TOPLEFT", 0, 0) end if db.showRightBars then self.rightBar1Holder:Show() else self.rightBar1Holder:Hide() end end -- Right-side bar 2 (MultiBarLeft, grid layout) if self.rightBar2Holder then self.rightBar2Holder:SetScale(db.scale) local perRow2 = db.rightBar2PerRow or 1 local numCols2 = perRow2 local numRows2 = math.ceil(BUTTONS_PER_ROW / perRow2) local rb2W = numCols2 * size + math.max(numCols2 - 1, 0) * gap local rb2H = numRows2 * size + math.max(numRows2 - 1, 0) * gap self.rightBar2Holder:SetWidth(rb2W) self.rightBar2Holder:SetHeight(rb2H) if MultiBarLeft then MultiBarLeft:SetWidth(rb2W) MultiBarLeft:SetHeight(rb2H) LayoutGrid(self.leftButtons, MultiBarLeft, size, gap, perRow2) MultiBarLeft:ClearAllPoints() MultiBarLeft:SetPoint("TOPLEFT", self.rightBar2Holder, "TOPLEFT", 0, 0) end if db.showRightBars then self.rightBar2Holder:Show() else self.rightBar2Holder:Hide() end end -- Alpha local alpha = db.alpha or 1 if alpha < 0.1 then alpha = 0.1 end if alpha > 1 then alpha = 1 end if self.anchor then self.anchor:SetAlpha(alpha) end if self.row2Anchor then self.row2Anchor:SetAlpha(alpha) end if self.row3Anchor then self.row3Anchor:SetAlpha(alpha) end if self.rightBar1Holder then self.rightBar1Holder:SetAlpha(alpha) end if self.rightBar2Holder then self.rightBar2Holder:SetAlpha(alpha) end if self.stanceHolder then self.stanceHolder:SetAlpha(alpha) end if self.petHolder then self.petHolder:SetAlpha(alpha) end -- Hotkey / macro name(使用缓存表,避免每次 ApplyConfig 都分配临时表) if not self.allButtonsCache then self.allButtonsCache = {} for _, b in ipairs(self.mainButtons) do table.insert(self.allButtonsCache, b) end if self.bonusButtons then for _, b in ipairs(self.bonusButtons) do table.insert(self.allButtonsCache, b) end end for _, b in ipairs(self.bar2Buttons) do table.insert(self.allButtonsCache, b) end for _, b in ipairs(self.bar3Buttons) do table.insert(self.allButtonsCache, b) end for _, b in ipairs(self.rightButtons) do table.insert(self.allButtonsCache, b) end for _, b in ipairs(self.leftButtons) do table.insert(self.allButtonsCache, b) end for _, b in ipairs(self.stanceButtons) do table.insert(self.allButtonsCache, b) end for _, b in ipairs(self.petButtons) do table.insert(self.allButtonsCache, b) end end -- Hotkey / macro name — per-group with adaptive font size local showHK = db.showHotkey local showMN = db.showMacroName local smallSize = db.smallBarSize UpdateButtonTexts(self.mainButtons, size, showHK, showMN) if self.bonusButtons then UpdateButtonTexts(self.bonusButtons, size, showHK, showMN) end UpdateButtonTexts(self.bar2Buttons, size, showHK, showMN) UpdateButtonTexts(self.bar3Buttons, size, showHK, showMN) UpdateButtonTexts(self.rightButtons, size, showHK, showMN) UpdateButtonTexts(self.leftButtons, size, showHK, showMN) UpdateButtonTexts(self.stanceButtons, smallSize, showHK, showMN) UpdateButtonTexts(self.petButtons, smallSize, showHK, showMN) -- Button visual style (rounded corners, inner shadow) local isRounded = db.buttonRounded local isShadow = db.buttonInnerShadow for _, b in ipairs(self.allButtonsCache) do ApplyButtonVisuals(b, isRounded, isShadow) end -- 始终显示动作条:强制 showgrid,让空格子也显示背景框 self:ApplyAlwaysShowGrid() self:ApplyPosition() self:ApplyStanceBar() self:ApplyPetBar() self:ApplyGryphon() self:UpdateBonusBar() end -------------------------------------------------------------------------------- -- Always-show-grid: 对当前所有已显示行中的空格子,强制显示背景框和格子材质 -- 不控制哪些行显示/隐藏,只控制空格子是否绘制背景 -------------------------------------------------------------------------------- function AB:ApplyAlwaysShowGrid() local db = self:GetDB() -- 收集所有当前参与布局的按钮(已样式化的按钮) local allBars = { self.mainButtons, self.bar2Buttons, self.bar3Buttons, self.rightButtons, self.leftButtons, } if db.alwaysShowGrid then for _, bar in ipairs(allBars) do if bar then for _, b in ipairs(bar) do if styledButtons[b] then -- showgrid > 0 时 Blizzard 不会隐藏空格按钮 b.showgrid = 1 b:Show() -- 背景子帧始终随按钮显示 if b.sfBackdrop then b.sfBackdrop:Show() end end end end end else for _, bar in ipairs(allBars) do if bar then for _, b in ipairs(bar) do if styledButtons[b] then b.showgrid = 0 -- 恢复 Blizzard 默认:无技能的格子隐藏 local ok, action = pcall(ActionButton_GetPagedID, b) if ok and action and not HasAction(action) then b:Hide() end end end end end end if BonusActionBarFrame and BonusActionBarFrame:IsShown() then for _, b in ipairs(self.mainButtons) do b:Hide() end end end -------------------------------------------------------------------------------- -- Stance bar -------------------------------------------------------------------------------- function AB:GetTopRowAnchor() local db = self:GetDB() if db.barCount >= 3 and self.row3Anchor then return self.row3Anchor end if db.barCount >= 2 and self.row2Anchor then return self.row2Anchor end return self.anchor end function AB:ApplyStanceBar() local db = self:GetDB() local size = db.smallBarSize local gap = db.buttonGap local numForms = GetNumShapeshiftForms and GetNumShapeshiftForms() or 0 if not db.showStanceBar or numForms == 0 then if self.stanceHolder then self.stanceHolder:Hide() end return end self.stanceHolder:SetScale(db.scale) local totalW = numForms * size + (numForms - 1) * gap self.stanceHolder:SetWidth(totalW) self.stanceHolder:SetHeight(size) self.stanceHolder:ClearAllPoints() local positions = SFramesDB and SFramesDB.Positions local pos = positions and positions["StanceBar"] if pos and pos.point and pos.relativePoint then self.stanceHolder:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0) else self.stanceHolder:SetPoint("BOTTOMLEFT", self:GetTopRowAnchor(), "TOPLEFT", 0, gap) end self.stanceHolder:Show() for i, b in ipairs(self.stanceButtons) do if i <= numForms then b:SetWidth(size) b:SetHeight(size) b:ClearAllPoints() if i == 1 then b:SetPoint("BOTTOMLEFT", self.stanceHolder, "BOTTOMLEFT", 0, 0) else b:SetPoint("LEFT", self.stanceButtons[i - 1], "RIGHT", gap, 0) end b:Show() else b:Hide() end end end -------------------------------------------------------------------------------- -- Pet bar -------------------------------------------------------------------------------- function AB:ApplyPetBar() local db = self:GetDB() local size = db.smallBarSize local gap = db.buttonGap local hasPet = HasPetUI and HasPetUI() if not db.showPetBar or not hasPet then if self.petHolder then self.petHolder:Hide() end return end self.petHolder:SetScale(db.scale) local numPet = 10 local totalW = numPet * size + (numPet - 1) * gap self.petHolder:SetWidth(totalW) self.petHolder:SetHeight(size) self.petHolder:ClearAllPoints() local positions = SFramesDB and SFramesDB.Positions local pos = positions and positions["PetBar"] if pos and pos.point and pos.relativePoint then self.petHolder:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0) else local numForms = GetNumShapeshiftForms and GetNumShapeshiftForms() or 0 if db.showStanceBar and numForms > 0 and self.stanceHolder and self.stanceHolder:IsShown() then self.petHolder:SetPoint("BOTTOMLEFT", self.stanceHolder, "TOPLEFT", 0, gap) else self.petHolder:SetPoint("BOTTOMLEFT", self:GetTopRowAnchor(), "TOPLEFT", 0, gap) end end self.petHolder:Show() for i, b in ipairs(self.petButtons) do b:SetWidth(size) b:SetHeight(size) b:ClearAllPoints() if i == 1 then b:SetPoint("BOTTOMLEFT", self.petHolder, "BOTTOMLEFT", 0, 0) else b:SetPoint("LEFT", self.petButtons[i - 1], "RIGHT", gap, 0) end b:Show() end end -------------------------------------------------------------------------------- -- Gryphon end-caps -------------------------------------------------------------------------------- function AB:ApplyGryphon() if not self.gryphonLeft or not self.gryphonRight then return end if not self.anchor then return end local db = self:GetDB() if db.hideGryphon then self.gryphonLeft:Hide() self.gryphonRight:Hide() return end -- 切换纹理样式 local texPath = GetGryphonTexPath(db.gryphonStyle) if self.gryphonLeft._sfTex then self.gryphonLeft._sfTex:SetTexture(texPath) end if self.gryphonRight._sfTex then self.gryphonRight._sfTex:SetTexture(texPath) self.gryphonRight._sfTex:SetTexCoord(1, 0, 0, 1) end local scale = db.scale or 1.0 local gw = db.gryphonWidth or 64 local gh = db.gryphonHeight or 64 self.gryphonLeft:SetScale(scale) self.gryphonRight:SetScale(scale) self.gryphonLeft:SetWidth(gw) self.gryphonLeft:SetHeight(gh) self.gryphonRight:SetWidth(gw) self.gryphonRight:SetHeight(gh) if db.gryphonOnTop then self.gryphonLeft:SetFrameLevel(self.anchor:GetFrameLevel() + 10) self.gryphonRight:SetFrameLevel(self.anchor:GetFrameLevel() + 10) else self.gryphonLeft:SetFrameLevel(0) self.gryphonRight:SetFrameLevel(0) end local ox = db.gryphonOffsetX or 30 local oy = db.gryphonOffsetY or 0 local positions = SFramesDB and SFramesDB.Positions self.gryphonLeft:ClearAllPoints() local posL = positions and positions["GryphonLeft"] if posL and posL.point and posL.relativePoint then self.gryphonLeft:SetPoint(posL.point, UIParent, posL.relativePoint, posL.xOfs or 0, posL.yOfs or 0) else self.gryphonLeft:SetPoint("BOTTOMRIGHT", self.anchor, "BOTTOMLEFT", ox, oy) end self.gryphonRight:ClearAllPoints() local posR = positions and positions["GryphonRight"] if posR and posR.point and posR.relativePoint then self.gryphonRight:SetPoint(posR.point, UIParent, posR.relativePoint, posR.xOfs or 0, posR.yOfs or 0) else self.gryphonRight:SetPoint("BOTTOMLEFT", self.anchor, "BOTTOMRIGHT", -ox, oy) end self.gryphonLeft:Show() self.gryphonRight:Show() end -------------------------------------------------------------------------------- -- Layout presets -------------------------------------------------------------------------------- AB.PRESETS = { { id = 1, name = "经典", desc = "底部堆叠 + 右侧竖栏" }, { id = 2, name = "宽屏", desc = "左4x3 + 底部堆叠 + 右4x3" }, { id = 3, name = "堆叠", desc = "全部堆叠于底部中央" }, } function AB:ApplyPreset(presetId) if not self.anchor then return end local db = self:GetDB() if not SFramesDB.Positions then SFramesDB.Positions = {} end local positions = SFramesDB.Positions local size = db.buttonSize local gap = db.buttonGap local smSize = db.smallBarSize local rowW = (size + gap) * BUTTONS_PER_ROW - gap local bottomY = db.bottomOffsetY or 2 local step = size + gap local leftX = -rowW / 2 local clearKeys = { "ActionBarBottom", "ActionBarRow2", "ActionBarRow3", "RightBar1", "RightBar2", "StanceBar", "PetBar", "PlayerFrame", "TargetFrame", "PetFrame", "FocusFrame", "ToTFrame", } for _, key in ipairs(clearKeys) do positions[key] = nil end local numForms = GetNumShapeshiftForms and GetNumShapeshiftForms() or 0 local hasStance = db.showStanceBar and numForms > 0 local stanceH = hasStance and (smSize + gap) or 0 db.bottomBar1PerRow = 12 db.bottomBar2PerRow = 12 db.bottomBar3PerRow = 12 if presetId == 1 then -- Classic: stacked bottom bars, vertical right side bars db.rightBar1PerRow = 1 db.rightBar2PerRow = 1 db.showRightBars = true local rx = db.rightOffsetX or -4 local ry = db.rightOffsetY or -80 positions["RightBar1"] = { point = "RIGHT", relativePoint = "RIGHT", xOfs = rx, yOfs = ry, } positions["RightBar2"] = { point = "RIGHT", relativePoint = "RIGHT", xOfs = rx - size - gap, yOfs = ry, } positions["StanceBar"] = { point = "BOTTOMLEFT", relativePoint = "BOTTOM", xOfs = leftX, yOfs = bottomY + step * 3, } positions["PetBar"] = { point = "BOTTOMLEFT", relativePoint = "BOTTOM", xOfs = leftX, yOfs = bottomY + step * 3 + stanceH, } elseif presetId == 2 then -- Widescreen: left 4x3, center stack, right 4x3 db.rightBar1PerRow = 4 db.rightBar2PerRow = 4 db.showRightBars = true positions["RightBar1"] = { point = "BOTTOMRIGHT", relativePoint = "BOTTOM", xOfs = -(rowW / 2 + gap * 2), yOfs = bottomY, } positions["RightBar2"] = { point = "BOTTOMLEFT", relativePoint = "BOTTOM", xOfs = rowW / 2 + gap * 2, yOfs = bottomY, } positions["StanceBar"] = { point = "BOTTOMLEFT", relativePoint = "BOTTOM", xOfs = leftX, yOfs = bottomY + step * 3, } positions["PetBar"] = { point = "BOTTOMLEFT", relativePoint = "BOTTOM", xOfs = leftX, yOfs = bottomY + step * 3 + stanceH, } elseif presetId == 3 then -- Stacked: all stacked at bottom center db.rightBar1PerRow = 12 db.rightBar2PerRow = 12 db.showRightBars = true local barH3 = 3 * step positions["RightBar1"] = { point = "BOTTOM", relativePoint = "BOTTOM", xOfs = 0, yOfs = bottomY + barH3, } positions["RightBar2"] = { point = "BOTTOM", relativePoint = "BOTTOM", xOfs = 0, yOfs = bottomY + barH3 + step, } positions["StanceBar"] = { point = "BOTTOMLEFT", relativePoint = "BOTTOM", xOfs = leftX, yOfs = bottomY + barH3 + step * 2, } positions["PetBar"] = { point = "BOTTOMLEFT", relativePoint = "BOTTOM", xOfs = leftX, yOfs = bottomY + barH3 + step * 2 + stanceH, } local upShift = step * 2 positions["PlayerFrame"] = { point = "CENTER", relativePoint = "CENTER", xOfs = -200, yOfs = -100 + upShift, } positions["TargetFrame"] = { point = "CENTER", relativePoint = "CENTER", xOfs = 200, yOfs = -100 + upShift, } end -- Castbar: centered above PetBar, explicit UIParent coords (avoids layout-engine timing issues) local petBarPos = positions["PetBar"] if petBarPos then local petTopY = petBarPos.yOfs + smSize positions["PlayerCastbar"] = { point = "BOTTOM", relativePoint = "BOTTOM", xOfs = 0, yOfs = petTopY + 6, } end -- Calculate Pet/Focus positions relative to Player/Target local playerPos = positions["PlayerFrame"] local px = playerPos and playerPos.xOfs or -200 local py = playerPos and playerPos.yOfs or -100 local targetPos = positions["TargetFrame"] local tx = targetPos and targetPos.xOfs or 200 local ty = targetPos and targetPos.yOfs or -100 local pf = _G["SFramesPlayerFrame"] local pfScale = pf and (pf:GetEffectiveScale() / UIParent:GetEffectiveScale()) or 1 local pfW = ((pf and pf:GetWidth()) or 220) * pfScale local pfH = ((pf and pf:GetHeight()) or 50) * pfScale local tf = _G["SFramesTargetFrame"] local tfScale = tf and (tf:GetEffectiveScale() / UIParent:GetEffectiveScale()) or 1 local tfW = ((tf and tf:GetWidth()) or 220) * tfScale local tfH = ((tf and tf:GetHeight()) or 50) * tfScale local petGap, focGap if presetId == 1 then petGap, focGap = 75, 71 elseif presetId == 2 then petGap, focGap = 62, 58 else petGap, focGap = 52, 42 end positions["PetFrame"] = { point = "TOPLEFT", relativePoint = "CENTER", xOfs = px - pfW / 2, yOfs = py - pfH / 2 - petGap, } positions["FocusFrame"] = { point = "TOPLEFT", relativePoint = "CENTER", xOfs = tx - tfW / 2, yOfs = ty - tfH / 2 - focGap, } positions["ToTFrame"] = { point = "BOTTOMLEFT", relativePoint = "CENTER", xOfs = tx + tfW / 2 + 5, yOfs = ty - tfH / 2, } db.layoutPreset = presetId self:ApplyConfig() if SFrames.Movers then local reg = SFrames.Movers:GetRegistry() for _, key in ipairs({"PlayerFrame", "TargetFrame", "PetFrame", "FocusFrame", "ToTFrame"}) do local entry = reg[key] if entry and entry.frame then SFrames.Movers:ApplyPosition(key, entry.frame, entry.defaultPoint, entry.defaultRelativeTo, entry.defaultRelPoint, entry.defaultX, entry.defaultY) end end end if SFrames.Player and SFrames.Player.ApplyCastbarPosition then SFrames.Player:ApplyCastbarPosition() end if SFrames.Movers and SFrames.Movers:IsLayoutMode() then for name, entry in pairs(SFrames.Movers:GetRegistry()) do local frame = entry and entry.frame local shouldSync = entry.alwaysShowInLayout or (frame and frame.IsShown and frame:IsShown()) if shouldSync then SFrames.Movers:SyncMoverToFrame(name) end end end SFrames:Print("|cff66eeff[布局预设]|r 已应用方案: " .. (self.PRESETS[presetId] and self.PRESETS[presetId].name or tostring(presetId))) end -------------------------------------------------------------------------------- -- Slider-based position update -------------------------------------------------------------------------------- function AB:ApplyPosition() local db = self:GetDB() local positions = SFramesDB and SFramesDB.Positions local gap = db.buttonGap if self.anchor then self.anchor:ClearAllPoints() local pos = positions and positions["ActionBarBottom"] if pos and pos.point and pos.relativePoint then self.anchor:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0) else self.anchor:SetPoint("BOTTOM", UIParent, "BOTTOM", db.bottomOffsetX, db.bottomOffsetY) end end if self.row2Anchor then self.row2Anchor:ClearAllPoints() local pos = positions and positions["ActionBarRow2"] if pos and pos.point and pos.relativePoint then self.row2Anchor:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0) else self.row2Anchor:SetPoint("BOTTOMLEFT", self.anchor, "TOPLEFT", 0, gap) end end if self.row3Anchor then self.row3Anchor:ClearAllPoints() local pos = positions and positions["ActionBarRow3"] if pos and pos.point and pos.relativePoint then self.row3Anchor:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0) else self.row3Anchor:SetPoint("BOTTOMLEFT", self.row2Anchor or self.anchor, "TOPLEFT", 0, gap) end end if self.rightBar1Holder then self.rightBar1Holder:ClearAllPoints() local pos = positions and positions["RightBar1"] if pos and pos.point and pos.relativePoint then self.rightBar1Holder:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0) else self.rightBar1Holder:SetPoint("RIGHT", UIParent, "RIGHT", db.rightOffsetX, db.rightOffsetY) end end if self.rightBar2Holder then self.rightBar2Holder:ClearAllPoints() local pos = positions and positions["RightBar2"] if pos and pos.point and pos.relativePoint then self.rightBar2Holder:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0) else self.rightBar2Holder:SetPoint("TOPRIGHT", self.rightBar1Holder or UIParent, "TOPLEFT", -gap, 0) end end end -------------------------------------------------------------------------------- -- Page indicator: shows which page / bonus offset the main bar is displaying -------------------------------------------------------------------------------- function AB:UpdatePageIndicator() if not self.pageIndicator then return end local page = CURRENT_ACTIONBAR_PAGE or 1 local offset = GetBonusBarOffset and GetBonusBarOffset() or 0 if offset > 0 and page == 1 then self.pageIndicator:SetTextColor(1.0, 0.5, 0.0) self.pageIndicator:SetText("B" .. offset) else self.pageIndicator:SetTextColor(0.9, 0.8, 0.2) self.pageIndicator:SetText(tostring(page)) end end -------------------------------------------------------------------------------- -- Bonus bar show/hide (warriors, druids, rogues — any class with stance/form) -- Blizzard's ShowBonusActionBar() is triggered in MainMenuBar's OnEvent, but we -- unregistered MainMenuBar events, so we must manage this ourselves. -------------------------------------------------------------------------------- function AB:UpdateBonusBar() if not BonusActionBarFrame then return end local offset = GetBonusBarOffset and GetBonusBarOffset() or 0 local page = CURRENT_ACTIONBAR_PAGE or 1 if page == 1 and offset > 0 then for _, b in ipairs(self.mainButtons) do b:Hide() end BonusActionBarFrame:Show() for i = 0, 4 do local tex = _G["BonusActionBarTexture" .. i] if tex then tex:SetAlpha(0) end end local db = self:GetDB() local size = db.buttonSize local gap = db.buttonGap local bpr = db.bottomBar1PerRow or 12 local numC = bpr local numR = math.ceil(BUTTONS_PER_ROW / bpr) local bW = numC * size + math.max(numC - 1, 0) * gap local bH = numR * size + math.max(numR - 1, 0) * gap BonusActionBarFrame:ClearAllPoints() BonusActionBarFrame:SetPoint("BOTTOMLEFT", self.row1, "BOTTOMLEFT", 0, 0) BonusActionBarFrame:SetWidth(bW) BonusActionBarFrame:SetHeight(bH) BonusActionBarFrame:SetFrameLevel(self.row1:GetFrameLevel() + 5) LayoutGrid(self.bonusButtons, BonusActionBarFrame, size, gap, bpr) local btnLevel = BonusActionBarFrame:GetFrameLevel() + 1 for _, b in ipairs(self.bonusButtons) do b:EnableMouse(true) b:SetFrameLevel(btnLevel) if b.sfBackdrop then b.sfBackdrop:SetFrameLevel(btnLevel - 1) end end else BonusActionBarFrame:Hide() for _, b in ipairs(self.mainButtons) do b:Show() end end self:UpdatePageIndicator() end -------------------------------------------------------------------------------- -- Range coloring - uses a separate red overlay; NEVER touches icon VertexColor -------------------------------------------------------------------------------- local function GetOrCreateRangeOverlay(b) if b.sfRangeOverlay then return b.sfRangeOverlay end local inset = b.sfIconInset or 2 local ov = b:CreateTexture(nil, "OVERLAY") ov:SetTexture("Interface\\Buttons\\WHITE8X8") ov:SetPoint("TOPLEFT", b, "TOPLEFT", inset, -inset) ov:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", -inset, inset) ov:SetVertexColor(1.0, 0.1, 0.1, 0.35) ov:Hide() b.sfRangeOverlay = ov return ov end function AB:SetupRangeCheck() local rangeFrame = CreateFrame("Frame", "SFramesActionBarRangeCheck", UIParent) rangeFrame.timer = 0 self.rangeFrame = rangeFrame rangeFrame:SetScript("OnUpdate", function() this.timer = this.timer + arg1 if this.timer < 0.2 then return end this.timer = 0 local db = AB:GetDB() local function CheckRange(buttons, idFunc) local getID = idFunc or ActionButton_GetPagedID if not getID then return end for _, b in ipairs(buttons) do local ok, action = pcall(getID, b) if ok and action and HasAction(action) then -- Range coloring if db.rangeColoring then local inRange = IsActionInRange(action) local ov = GetOrCreateRangeOverlay(b) if inRange == 0 then ov:Show() else ov:Hide() end end else if b.sfRangeOverlay then b.sfRangeOverlay:Hide() end end end end CheckRange(AB.mainButtons) if AB.bonusButtons and BonusActionBarFrame and BonusActionBarFrame:IsShown() then CheckRange(AB.bonusButtons, BonusActionButton_GetPagedID) end CheckRange(AB.bar2Buttons) CheckRange(AB.bar3Buttons) end) end -------------------------------------------------------------------------------- -- Initialize -------------------------------------------------------------------------------- function AB:Initialize() local db = self:GetDB() if not db.enable then return end self:HideBlizzardBars() self:CreateBars() self:ApplyConfig() self:SetupRangeCheck() -- Hook Blizzard ActionButton functions that reset NormalTexture. -- These use 'this' (no explicit args in vanilla), so parameterless hook is safe. local function SuppressNormalTexture() if this and styledButtons[this] then local nt = _G[this:GetName() .. "NormalTexture"] HideNormalTexture(nt) end end -- alwaysShowGrid 模式下,阻止空格子被隐藏 local function ForceShowIfGrid() if this and styledButtons[this] then local adb = AB:GetDB() if adb.alwaysShowGrid then this.showgrid = 1 this:Show() if this.sfBackdrop then this.sfBackdrop:Show() end end end end local hookTargets = { "ActionButton_Update", "ActionButton_UpdateUsable", "ActionButton_UpdateState", } for _, fn in ipairs(hookTargets) do local orig = _G[fn] if orig then _G[fn] = function() pcall(orig) SuppressNormalTexture() ForceShowIfGrid() end end end -- Hook ActionButton_ShowGrid / ActionButton_HideGrid local origShowGrid = _G["ActionButton_ShowGrid"] local origHideGrid = _G["ActionButton_HideGrid"] -- 判断某个按钮是否属于主动作条(ActionButton1-12) local function IsMainButton(b) if not b or not AB.mainButtons then return false end for _, mb in ipairs(AB.mainButtons) do if mb == b then return true end end return false end -- 当前是否处于 BonusBar 激活状态(潜行、变身等) local function IsBonusBarActive() local offset = GetBonusBarOffset and GetBonusBarOffset() or 0 local page = CURRENT_ACTIONBAR_PAGE or 1 return (page == 1 and offset > 0) end if origShowGrid then _G["ActionButton_ShowGrid"] = function() if this and this.showgrid == nil then this.showgrid = 0 end if this and styledButtons[this] and IsMainButton(this) and IsBonusBarActive() then SuppressNormalTexture() return end pcall(origShowGrid) SuppressNormalTexture() end end if origHideGrid then _G["ActionButton_HideGrid"] = function() if this and this.showgrid == nil then this.showgrid = 0 end if this and styledButtons[this] then local adb = AB:GetDB() if IsMainButton(this) and IsBonusBarActive() then this.showgrid = 0 this:Hide() SuppressNormalTexture() return end if adb.alwaysShowGrid then this.showgrid = 1 this:Show() SuppressNormalTexture() return end end pcall(origHideGrid) SuppressNormalTexture() end end -- BonusActionButton functions take an explicit button arg — must pass it through local function SuppressNT(b) if b and styledButtons[b] then local nt = _G[b:GetName() .. "NormalTexture"] HideNormalTexture(nt) end end local origBonusUpdate = _G["BonusActionButton_Update"] if origBonusUpdate then _G["BonusActionButton_Update"] = function(button) pcall(origBonusUpdate, button) local b = button or this SuppressNT(b) if b and styledButtons[b] and AB.bonusButtons then local db = AB:GetDB() local size = db.buttonSize local gap = db.buttonGap local bpr = db.bottomBar1PerRow or 12 for idx, btn in ipairs(AB.bonusButtons) do if btn == b then btn:SetWidth(size) btn:SetHeight(size) btn:ClearAllPoints() local col = math.fmod(idx - 1, bpr) local row = math.floor((idx - 1) / bpr) btn:SetPoint("TOPLEFT", BonusActionBarFrame, "TOPLEFT", col * (size + gap), -row * (size + gap)) break end end end end end local origBonusUsable = _G["BonusActionButton_UpdateUsable"] if origBonusUsable then _G["BonusActionButton_UpdateUsable"] = function(button) pcall(origBonusUsable, button) SuppressNT(button or this) end end -- Hook PetActionBar_Update: Blizzard resets NormalTexture2 vertex color/alpha local origPetBarUpdate = PetActionBar_Update if origPetBarUpdate then PetActionBar_Update = function() pcall(origPetBarUpdate) for _, b in ipairs(AB.petButtons or {}) do KillPetNormalTextures(b) end end end -- Shared helper: re-anchor row2/row3 inside their independent holders after -- Blizzard code resets their positions. local function FixRowParenting() if not AB.anchor then return end if AB.row2 and AB.row2Anchor then AB.row2:ClearAllPoints() AB.row2:SetPoint("BOTTOMLEFT", AB.row2Anchor, "BOTTOMLEFT", 0, 0) end if AB.row3 and AB.row3Anchor then AB.row3:ClearAllPoints() AB.row3:SetPoint("BOTTOMLEFT", AB.row3Anchor, "BOTTOMLEFT", 0, 0) end end -- Hook MultiActionBar_Update: Blizzard 在 PLAYER_ENTERING_WORLD 等事件中调用此函数, -- 它会根据经验条/声望条的高度重设 MultiBarBottomLeft/Right 的位置,覆盖我们的布局。 local origMultiActionBarUpdate = MultiActionBar_Update if origMultiActionBarUpdate then local inMultiBarHook = false MultiActionBar_Update = function() pcall(origMultiActionBarUpdate) if AB.anchor and not inMultiBarHook then inMultiBarHook = true FixRowParenting() inMultiBarHook = false end end end -- Hook UIParent_ManageFramePositions: Blizzard 在进出战斗、切换地图等场景调用此 -- 函数重排 MultiBar 位置(会考虑经验条/声望条高度),覆盖我们的布局。 local origManageFramePositions = UIParent_ManageFramePositions if origManageFramePositions then UIParent_ManageFramePositions = function(a1, a2, a3) origManageFramePositions(a1, a2, a3) if AB.anchor then FixRowParenting() end if SFrames and SFrames.Chat then SFrames.Chat:ReanchorChatFrames() end end end -- Direct event-driven bonus bar update + safety poller. -- Events trigger immediate update; poller catches any missed state every 0.2s. SFrames:RegisterEvent("PLAYER_ENTERING_WORLD", function() AB:UpdateBonusBar() AB:ApplyConfig() AB:RefreshAllHotkeys() end) SFrames:RegisterEvent("UPDATE_BINDINGS", function() AB:RefreshAllHotkeys() end) SFrames:RegisterEvent("UPDATE_BONUS_ACTIONBAR", function() AB:UpdateBonusBar() end) SFrames:RegisterEvent("ACTIONBAR_PAGE_CHANGED", function() AB:UpdateBonusBar() end) SFrames:RegisterEvent("UPDATE_SHAPESHIFT_FORMS", function() AB:UpdateBonusBar() AB:ApplyStanceBar() AB:ApplyPetBar() end) -- Safety poller: if Blizzard code or timing issues cause a mismatch, -- correct it within 0.2 seconds. local bonusPoller = CreateFrame("Frame") bonusPoller.timer = 0 bonusPoller:SetScript("OnUpdate", function() this.timer = this.timer + arg1 if this.timer < 0.2 then return end this.timer = 0 local offset = GetBonusBarOffset and GetBonusBarOffset() or 0 local page = CURRENT_ACTIONBAR_PAGE or 1 local want = (page == 1 and offset > 0) local have = BonusActionBarFrame and BonusActionBarFrame:IsShown() if (want and not have) or (not want and have) then AB:UpdateBonusBar() elseif have then local db = AB:GetDB() local size = db.buttonSize local gap = db.buttonGap local bpr = db.bottomBar1PerRow or 12 local numC = bpr local numR = math.ceil(BUTTONS_PER_ROW / bpr) local bW = numC * size + math.max(numC - 1, 0) * gap local bH = numR * size + math.max(numR - 1, 0) * gap BonusActionBarFrame:ClearAllPoints() BonusActionBarFrame:SetPoint("BOTTOMLEFT", AB.row1, "BOTTOMLEFT", 0, 0) BonusActionBarFrame:SetWidth(bW) BonusActionBarFrame:SetHeight(bH) BonusActionBarFrame:SetFrameLevel(AB.row1:GetFrameLevel() + 5) LayoutGrid(AB.bonusButtons, BonusActionBarFrame, size, gap, bpr) local btnLevel = BonusActionBarFrame:GetFrameLevel() + 1 for _, b in ipairs(AB.bonusButtons) do b:EnableMouse(true) b:SetFrameLevel(btnLevel) if b.sfBackdrop then b.sfBackdrop:SetFrameLevel(btnLevel - 1) end end for _, b in ipairs(AB.mainButtons) do if b:IsShown() then b:Hide() end end end AB:UpdatePageIndicator() end) SFrames:RegisterEvent("PET_BAR_UPDATE", function() AB:ApplyPetBar() end) SFrames:RegisterEvent("UNIT_PET", function() if arg1 == "player" then AB:ApplyPetBar() end end) -- 进出战斗和切换区域时立即修正行间距,防止 Blizzard 布局覆盖 local function FixRowAnchors() if not AB.anchor then return end FixRowParenting() end SFrames:RegisterEvent("PLAYER_REGEN_DISABLED", FixRowAnchors) SFrames:RegisterEvent("PLAYER_REGEN_ENABLED", FixRowAnchors) SFrames:RegisterEvent("ZONE_CHANGED", FixRowAnchors) SFrames:RegisterEvent("ZONE_CHANGED_NEW_AREA", FixRowAnchors) SFrames:RegisterEvent("ZONE_CHANGED_INDOORS", FixRowAnchors) -- Register movers if SFrames.Movers and SFrames.Movers.RegisterMover then if self.anchor then SFrames.Movers:RegisterMover("ActionBarBottom", self.anchor, "底部主动作条", "BOTTOM", "UIParent", "BOTTOM", db.bottomOffsetX, db.bottomOffsetY, function() AB:ApplyStanceBar(); AB:ApplyPetBar(); AB:ApplyGryphon() end) end if self.row2Anchor then SFrames.Movers:RegisterMover("ActionBarRow2", self.row2Anchor, "底部动作条2", "BOTTOMLEFT", "SFramesActionBarAnchor", "TOPLEFT", 0, db.buttonGap, function() AB:ApplyStanceBar(); AB:ApplyPetBar() end) end if self.row3Anchor then SFrames.Movers:RegisterMover("ActionBarRow3", self.row3Anchor, "底部动作条3", "BOTTOMLEFT", "SFramesRow2Anchor", "TOPLEFT", 0, db.buttonGap, function() AB:ApplyStanceBar(); AB:ApplyPetBar() end) end if self.rightBar1Holder then SFrames.Movers:RegisterMover("RightBar1", self.rightBar1Holder, "右侧动作条1", "RIGHT", "UIParent", "RIGHT", db.rightOffsetX, db.rightOffsetY) end if self.rightBar2Holder then SFrames.Movers:RegisterMover("RightBar2", self.rightBar2Holder, "右侧动作条2", "TOPRIGHT", "SFramesRightBar1Holder", "TOPLEFT", -db.buttonGap, 0) end local topAnchorName = "SFramesActionBarAnchor" if db.barCount >= 3 and self.row3Anchor then topAnchorName = "SFramesRow3Anchor" elseif db.barCount >= 2 and self.row2Anchor then topAnchorName = "SFramesRow2Anchor" end if self.stanceHolder then SFrames.Movers:RegisterMover("StanceBar", self.stanceHolder, "姿态条", "BOTTOMLEFT", topAnchorName, "TOPLEFT", 0, db.buttonGap) end if self.petHolder then SFrames.Movers:RegisterMover("PetBar", self.petHolder, "宠物条", "BOTTOMLEFT", topAnchorName, "TOPLEFT", 0, db.buttonGap) end if self.gryphonLeft then SFrames.Movers:RegisterMover("GryphonLeft", self.gryphonLeft, "狮鹫(左)", "BOTTOMRIGHT", "SFramesActionBarAnchor", "BOTTOMLEFT", db.gryphonOffsetX or 30, db.gryphonOffsetY or 0) end if self.gryphonRight then SFrames.Movers:RegisterMover("GryphonRight", self.gryphonRight, "狮鹫(右)", "BOTTOMLEFT", "SFramesActionBarAnchor", "BOTTOMRIGHT", -(db.gryphonOffsetX or 30), db.gryphonOffsetY or 0) end end end -------------------------------------------------------------------------------- -- Key Binding Mode -- Hover any action/stance/pet button and press a key to bind it. -- ESC exits. Right-click clears. Mouse wheel supported. -------------------------------------------------------------------------------- local BUTTON_BINDING_MAP = {} do for i = 1, 12 do BUTTON_BINDING_MAP["ActionButton" .. i] = "ACTIONBUTTON" .. i BUTTON_BINDING_MAP["MultiBarBottomLeftButton" .. i] = "MULTIACTIONBAR1BUTTON" .. i BUTTON_BINDING_MAP["MultiBarBottomRightButton" .. i] = "MULTIACTIONBAR2BUTTON" .. i BUTTON_BINDING_MAP["MultiBarRightButton" .. i] = "MULTIACTIONBAR3BUTTON" .. i BUTTON_BINDING_MAP["MultiBarLeftButton" .. i] = "MULTIACTIONBAR4BUTTON" .. i end for i = 1, 10 do BUTTON_BINDING_MAP["ShapeshiftButton" .. i] = "SHAPESHIFTBUTTON" .. i BUTTON_BINDING_MAP["PetActionButton" .. i] = "BONUSACTIONBUTTON" .. i end end function AB:RegisterBindButton(buttonName, bindCommand) BUTTON_BINDING_MAP[buttonName] = bindCommand end -------------------------------------------------------------------------------- -- Hotkey text refresh: update the HotKey FontString on buttons to reflect -- current keybindings (works for all bars including stance and pet). -------------------------------------------------------------------------------- local function RefreshButtonHotkey(button) if not button then return end local name = button:GetName() if not name then return end local cmd = BUTTON_BINDING_MAP[name] if not cmd then return end local hotkey = _G[name .. "HotKey"] if not hotkey then return end local key1 = GetBindingKey(cmd) if key1 then local text = key1 if GetBindingText then text = GetBindingText(key1, "KEY_", 1) or key1 end hotkey:SetText(text) else hotkey:SetText("") end end function AB:RefreshAllHotkeys() local function Refresh(list) if not list then return end for _, b in ipairs(list) do RefreshButtonHotkey(b) end end Refresh(self.mainButtons) Refresh(self.bonusButtons) Refresh(self.bar2Buttons) Refresh(self.bar3Buttons) Refresh(self.rightButtons) Refresh(self.leftButtons) Refresh(self.stanceButtons) Refresh(self.petButtons) if SFrames.ExtraBar and SFrames.ExtraBar.buttons then local ebDb = SFrames.ExtraBar:GetDB() if ebDb.enable then local ebCount = math.min(ebDb.buttonCount or 12, 48) for i = 1, ebCount do RefreshButtonHotkey(SFrames.ExtraBar.buttons[i]) end end end end local IGNORE_KEYS = { LSHIFT = true, RSHIFT = true, LCTRL = true, RCTRL = true, LALT = true, RALT = true, UNKNOWN = true, } local function BuildKeyString(key) if IGNORE_KEYS[key] then return nil end local mods = "" if IsAltKeyDown() then mods = mods .. "ALT-" end if IsControlKeyDown() then mods = mods .. "CTRL-" end if IsShiftKeyDown() then mods = mods .. "SHIFT-" end return mods .. key end local function GetButtonBindingCmd(button) if not button then return nil end local name = button:GetName() return name and BUTTON_BINDING_MAP[name] end local function ShortKeyText(key) if not key then return "" end key = string.gsub(key, "SHIFT%-", "S-") key = string.gsub(key, "CTRL%-", "C-") key = string.gsub(key, "ALT%-", "A-") key = string.gsub(key, "MOUSEWHEELUP", "WhlUp") key = string.gsub(key, "MOUSEWHEELDOWN", "WhlDn") key = string.gsub(key, "BUTTON3", "M3") key = string.gsub(key, "BUTTON4", "M4") key = string.gsub(key, "BUTTON5", "M5") return key end local keyBindActive = false local hoveredBindButton = nil local keyBindOverlays = {} local captureFrame = nil local statusFrame = nil local function UpdateOverlayText(button) local ov = keyBindOverlays[button] if not ov then return end local cmd = GetButtonBindingCmd(button) if not cmd then ov.label:SetText(""); return end local key1, key2 = GetBindingKey(cmd) local t = "" if key1 then t = ShortKeyText(key1) end if key2 then t = t .. "\n" .. ShortKeyText(key2) end ov.label:SetText(t) end local CLICK_TO_KEY = { MiddleButton = "BUTTON3", Button4 = "BUTTON4", Button5 = "BUTTON5", } local function CreateBindOverlay(button) if keyBindOverlays[button] then return keyBindOverlays[button] end local ov = CreateFrame("Button", nil, button) ov:SetAllPoints(button) ov:SetFrameLevel(button:GetFrameLevel() + 10) ov:RegisterForClicks("RightButtonUp", "MiddleButtonUp", "Button4Up", "Button5Up") local bg = ov:CreateTexture(nil, "BACKGROUND") bg:SetAllPoints() bg:SetTexture(0, 0, 0, 0.55) local label = ov:CreateFontString(nil, "OVERLAY") label:SetFont(SFrames:GetFont(), 7, "OUTLINE") label:SetPoint("CENTER", ov, "CENTER", 0, 0) label:SetWidth(button:GetWidth() - 2) label:SetJustifyH("CENTER") label:SetTextColor(1, 0.82, 0) ov.label = label local highlight = ov:CreateTexture(nil, "HIGHLIGHT") highlight:SetAllPoints() highlight:SetTexture(1, 1, 1, 0.18) ov:SetScript("OnEnter", function() hoveredBindButton = button GameTooltip:SetOwner(this, "ANCHOR_TOP") local cmd = GetButtonBindingCmd(button) if cmd then GameTooltip:AddLine(cmd, 1, 0.82, 0) local k1, k2 = GetBindingKey(cmd) if k1 then GameTooltip:AddLine("按键 1: " .. k1, 1, 1, 1) end if k2 then GameTooltip:AddLine("按键 2: " .. k2, 0.7, 0.7, 0.7) end end GameTooltip:AddLine("按下按键/鼠标键/滚轮绑定", 0.5, 0.8, 0.5) GameTooltip:AddLine("右键点击清除绑定", 0.8, 0.5, 0.5) GameTooltip:Show() end) ov:SetScript("OnLeave", function() hoveredBindButton = nil GameTooltip:Hide() end) ov:SetScript("OnClick", function() if arg1 == "RightButton" then local cmd = GetButtonBindingCmd(button) if cmd then local k1, k2 = GetBindingKey(cmd) if k2 then SetBinding(k2, nil) end if k1 then SetBinding(k1, nil) end SaveBindings(2) UpdateOverlayText(button) RefreshButtonHotkey(button) DEFAULT_CHAT_FRAME:AddMessage("|cff88ccff[Nanami-UI]|r 已清除 " .. cmd .. " 的绑定") end else local mouseKey = CLICK_TO_KEY[arg1] if mouseKey then local cmd = GetButtonBindingCmd(button) if not cmd then return end local keyStr = BuildKeyString(mouseKey) if not keyStr then return end local old = GetBindingAction(keyStr) if old and old ~= "" and old ~= cmd then SetBinding(keyStr, nil) end SetBinding(keyStr, cmd) SaveBindings(2) UpdateOverlayText(button) RefreshButtonHotkey(button) DEFAULT_CHAT_FRAME:AddMessage("|cff88ccff[Nanami-UI]|r " .. keyStr .. " -> " .. cmd) end end end) ov:EnableMouseWheel(true) ov:SetScript("OnMouseWheel", function() local cmd = GetButtonBindingCmd(button) if not cmd then return end local wheel = (arg1 > 0) and "MOUSEWHEELUP" or "MOUSEWHEELDOWN" local mods = "" if IsAltKeyDown() then mods = mods .. "ALT-" end if IsControlKeyDown() then mods = mods .. "CTRL-" end if IsShiftKeyDown() then mods = mods .. "SHIFT-" end local keyStr = mods .. wheel local old = GetBindingAction(keyStr) if old and old ~= "" and old ~= cmd then SetBinding(keyStr, nil) end SetBinding(keyStr, cmd) SaveBindings(2) UpdateOverlayText(button) RefreshButtonHotkey(button) DEFAULT_CHAT_FRAME:AddMessage("|cff88ccff[Nanami-UI]|r " .. keyStr .. " -> " .. cmd) end) ov:Hide() keyBindOverlays[button] = ov return ov end function AB:EnterKeyBindMode() if keyBindActive then return end keyBindActive = true local allButtons = {} local function Collect(list) if not list then return end for _, b in ipairs(list) do table.insert(allButtons, b) end end Collect(self.mainButtons) Collect(self.bar2Buttons) Collect(self.bar3Buttons) Collect(self.rightButtons) Collect(self.leftButtons) Collect(self.stanceButtons) Collect(self.petButtons) if SFrames.ExtraBar and SFrames.ExtraBar.buttons then local ebDb = SFrames.ExtraBar:GetDB() if ebDb.enable then local ebCount = math.min(ebDb.buttonCount or 12, 48) for i = 1, ebCount do local b = SFrames.ExtraBar.buttons[i] if b then table.insert(allButtons, b) end end end end for _, b in ipairs(allButtons) do local ov = CreateBindOverlay(b) ov:Show() UpdateOverlayText(b) end if not captureFrame then captureFrame = CreateFrame("Frame", "SFramesKeyBindCapture", UIParent) captureFrame:SetFrameStrata("TOOLTIP") captureFrame:SetWidth(1) captureFrame:SetHeight(1) captureFrame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 0, 0) captureFrame:EnableKeyboard(true) captureFrame:EnableMouse(false) captureFrame:SetScript("OnKeyDown", function() local key = arg1 if key == "ESCAPE" then AB:ExitKeyBindMode() return end if not hoveredBindButton then return end local keyStr = BuildKeyString(key) if not keyStr then return end local cmd = GetButtonBindingCmd(hoveredBindButton) if not cmd then return end local old = GetBindingAction(keyStr) if old and old ~= "" and old ~= cmd then SetBinding(keyStr, nil) end SetBinding(keyStr, cmd) SaveBindings(2) UpdateOverlayText(hoveredBindButton) RefreshButtonHotkey(hoveredBindButton) DEFAULT_CHAT_FRAME:AddMessage("|cff88ccff[Nanami-UI]|r " .. keyStr .. " -> " .. cmd) end) end captureFrame:Show() if not statusFrame then statusFrame = CreateFrame("Frame", "SFramesKeyBindStatus", UIParent) statusFrame:SetFrameStrata("TOOLTIP") statusFrame:SetWidth(340) statusFrame:SetHeight(100) statusFrame:SetPoint("TOP", UIParent, "TOP", 0, -40) statusFrame:SetMovable(true) statusFrame:EnableMouse(true) statusFrame:RegisterForDrag("LeftButton") statusFrame:SetScript("OnDragStart", function() this:StartMoving() end) statusFrame:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) SFrames:CreateBackdrop(statusFrame) local title = statusFrame:CreateFontString(nil, "OVERLAY") title:SetFont(SFrames:GetFont(), 13, "OUTLINE") title:SetPoint("TOP", statusFrame, "TOP", 0, -12) title:SetText("|cffffcc00按键绑定模式|r") local desc = statusFrame:CreateFontString(nil, "OVERLAY") desc:SetFont(SFrames:GetFont(), 10, "OUTLINE") desc:SetPoint("TOP", title, "BOTTOM", 0, -6) desc:SetWidth(300) desc:SetJustifyH("CENTER") desc:SetText("悬停按钮 + 按键/鼠标键/滚轮绑定 | 右键清除") desc:SetTextColor(0.82, 0.82, 0.82) local saveBtn = CreateFrame("Button", "SFramesKeyBindSave", statusFrame, "UIPanelButtonTemplate") saveBtn:SetWidth(120) saveBtn:SetHeight(26) saveBtn:SetPoint("BOTTOM", statusFrame, "BOTTOM", 0, 10) saveBtn:SetText("保存并退出") saveBtn:SetScript("OnClick", function() AB:ExitKeyBindMode() end) local _T = SFrames.ActiveTheme local function HideBindBtnTex(tex) if not tex then return end if tex.SetTexture then tex:SetTexture(nil) end if tex.SetAlpha then tex:SetAlpha(0) end if tex.Hide then tex:Hide() end end HideBindBtnTex(saveBtn:GetNormalTexture()) HideBindBtnTex(saveBtn:GetPushedTexture()) HideBindBtnTex(saveBtn:GetHighlightTexture()) HideBindBtnTex(saveBtn:GetDisabledTexture()) for _, sfx in ipairs({"Left","Right","Middle"}) do local t = _G["SFramesKeyBindSave"..sfx] if t then t:SetAlpha(0); t:Hide() end end saveBtn:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 14, insets = { left = 3, right = 3, top = 3, bottom = 3 }, }) saveBtn:SetBackdropColor(_T.btnBg[1], _T.btnBg[2], _T.btnBg[3], _T.btnBg[4]) saveBtn:SetBackdropBorderColor(_T.btnBorder[1], _T.btnBorder[2], _T.btnBorder[3], _T.btnBorder[4]) local fs = saveBtn:GetFontString() if fs then fs:SetFont(SFrames:GetFont(), 11, "OUTLINE") fs:SetTextColor(_T.btnText[1], _T.btnText[2], _T.btnText[3]) end saveBtn:SetScript("OnEnter", function() this:SetBackdropColor(_T.btnHoverBg[1], _T.btnHoverBg[2], _T.btnHoverBg[3], _T.btnHoverBg[4]) this:SetBackdropBorderColor(_T.btnHoverBorder[1], _T.btnHoverBorder[2], _T.btnHoverBorder[3], _T.btnHoverBorder[4]) local t = this:GetFontString() if t then t:SetTextColor(_T.btnActiveText[1], _T.btnActiveText[2], _T.btnActiveText[3]) end end) saveBtn:SetScript("OnLeave", function() this:SetBackdropColor(_T.btnBg[1], _T.btnBg[2], _T.btnBg[3], _T.btnBg[4]) this:SetBackdropBorderColor(_T.btnBorder[1], _T.btnBorder[2], _T.btnBorder[3], _T.btnBorder[4]) local t = this:GetFontString() if t then t:SetTextColor(_T.btnText[1], _T.btnText[2], _T.btnText[3]) end end) saveBtn:SetScript("OnMouseDown", function() this:SetBackdropColor(_T.btnDownBg[1], _T.btnDownBg[2], _T.btnDownBg[3], _T.btnDownBg[4]) end) saveBtn:SetScript("OnMouseUp", function() this:SetBackdropColor(_T.btnHoverBg[1], _T.btnHoverBg[2], _T.btnHoverBg[3], _T.btnHoverBg[4]) end) end statusFrame:Show() DEFAULT_CHAT_FRAME:AddMessage("|cff88ccff[Nanami-UI]|r 按键绑定模式已开启。悬停按钮后按键/鼠标键/滚轮绑定,右键清除,ESC或点击保存退出。") end function AB:ExitKeyBindMode() if not keyBindActive then return end keyBindActive = false hoveredBindButton = nil for _, ov in pairs(keyBindOverlays) do ov:Hide() end if captureFrame then captureFrame:Hide() end if statusFrame then statusFrame:Hide() end self:RefreshAllHotkeys() DEFAULT_CHAT_FRAME:AddMessage("|cff88ccff[Nanami-UI]|r 按键绑定已保存。") end function AB:ToggleKeyBindMode() if keyBindActive then self:ExitKeyBindMode() else self:EnterKeyBindMode() end end function AB:IsKeyBindMode() return keyBindActive end