Files
Nanami-UI/ActionBars.lua
2026-04-09 09:46:47 +08:00

2359 lines
86 KiB
Lua
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

--------------------------------------------------------------------------------
-- 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