Files
Nanami-UI/ActionBars.lua
rucky ec9e3c29d6 完成多出修改
修复拾取界面点击无效问题
修复宠物训练界面不显示训练点问题
新增天赋分享到聊天界面
修复飞行界面无法关闭问题
修复术士宠物的显示问题
为天赋界面添加默认数据库支持
框架现在也可自主选择是否启用
玩家框架和目标框架可取消显示3D头像以及透明度修改
背包和银行也添加透明度自定义支持
优化tooltip性能和背包物品显示方式
修复布局模式在ui缩放后不能正常定位的问题
添加硬核模式危险和死亡的工会通报
添加拾取和已拾取的框体
等等
2026-03-23 10:25:25 +08:00

1894 lines
68 KiB
Lua
Raw 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,
}
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
--------------------------------------------------------------------------------
-- 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 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 rowWidth = (size + gap) * BUTTONS_PER_ROW - gap
-- === BOTTOM BARS ===
local anchor = CreateFrame("Frame", "SFramesActionBarAnchor", UIParent)
anchor:SetWidth(rowWidth)
anchor:SetHeight(size * 3 + gap * 2)
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(rowWidth)
row1:SetHeight(size)
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(rowWidth)
BonusActionBarFrame:SetHeight(size)
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
LayoutRow(AB.bonusButtons, BonusActionBarFrame, s, g)
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: reposition the original MultiBarBottomLeft frame
if MultiBarBottomLeft then
MultiBarBottomLeft:SetParent(anchor)
MultiBarBottomLeft:ClearAllPoints()
MultiBarBottomLeft:SetPoint("BOTTOMLEFT", row1, "TOPLEFT", 0, gap)
MultiBarBottomLeft:SetWidth(rowWidth)
MultiBarBottomLeft:SetHeight(size)
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: reposition the original MultiBarBottomRight frame
if MultiBarBottomRight then
MultiBarBottomRight:SetParent(anchor)
MultiBarBottomRight:ClearAllPoints()
MultiBarBottomRight:SetPoint("BOTTOMLEFT", MultiBarBottomLeft or row1, "TOPLEFT", 0, gap)
MultiBarBottomRight:SetWidth(rowWidth)
MultiBarBottomRight:SetHeight(size)
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 ===
local rightHolder = CreateFrame("Frame", "SFramesRightBarHolder", UIParent)
rightHolder:SetWidth(size * 2 + gap)
rightHolder:SetHeight((size + gap) * BUTTONS_PER_ROW - gap)
local rbPos = SFramesDB and SFramesDB.Positions and SFramesDB.Positions["ActionBarRight"]
if rbPos and rbPos.point and rbPos.relativePoint then
rightHolder:SetPoint(rbPos.point, UIParent, rbPos.relativePoint, rbPos.xOfs or 0, rbPos.yOfs or 0)
else
rightHolder:SetPoint("RIGHT", UIParent, "RIGHT", db.rightOffsetX, db.rightOffsetY)
end
rightHolder:SetScale(db.scale)
self.rightHolder = rightHolder
if MultiBarRight then
MultiBarRight:SetParent(rightHolder)
MultiBarRight:ClearAllPoints()
MultiBarRight:SetPoint("TOPRIGHT", rightHolder, "TOPRIGHT", 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
if MultiBarLeft then
MultiBarLeft:SetParent(rightHolder)
MultiBarLeft:ClearAllPoints()
MultiBarLeft:SetPoint("TOPRIGHT", MultiBarRight or rightHolder, "TOPLEFT", -gap, 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
-- === STANCE BAR ===
local stanceHolder = CreateFrame("Frame", "SFramesStanceHolder", UIParent)
stanceHolder:SetWidth(rowWidth)
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(rowWidth)
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
local rowWidth = (size + gap) * BUTTONS_PER_ROW - gap
local colHeight = (size + gap) * BUTTONS_PER_ROW - gap
local totalHeight = db.barCount * size + (db.barCount - 1) * gap
-- Bottom bars anchor
self.anchor:SetScale(db.scale)
self.anchor:SetWidth(rowWidth)
self.anchor:SetHeight(totalHeight)
-- Row 1
self.row1:SetWidth(rowWidth)
self.row1:SetHeight(size)
LayoutRow(self.mainButtons, self.row1, size, gap)
-- Bonus bar (druid forms) — same layout as row 1, overlays when active
if self.bonusButtons and BonusActionBarFrame then
BonusActionBarFrame:SetWidth(rowWidth)
BonusActionBarFrame:SetHeight(size)
LayoutRow(self.bonusButtons, BonusActionBarFrame, size, gap)
end
-- Row 2
if self.row2 then
self.row2:SetWidth(rowWidth)
self.row2:SetHeight(size)
self.row2:ClearAllPoints()
self.row2:SetPoint("BOTTOMLEFT", self.row1, "TOPLEFT", 0, gap)
LayoutRow(self.bar2Buttons, self.row2, size, gap)
if db.barCount >= 2 then self.row2:Show() else self.row2:Hide() end
end
-- Row 3
if self.row3 then
self.row3:SetWidth(rowWidth)
self.row3:SetHeight(size)
self.row3:ClearAllPoints()
self.row3:SetPoint("BOTTOMLEFT", self.row2 or self.row1, "TOPLEFT", 0, gap)
LayoutRow(self.bar3Buttons, self.row3, size, gap)
if db.barCount >= 3 then self.row3:Show() else self.row3:Hide() end
end
-- Right-side bars
if self.rightHolder then
self.rightHolder:SetScale(db.scale)
self.rightHolder:SetWidth(size * 2 + gap)
self.rightHolder:SetHeight(colHeight)
if MultiBarRight then
MultiBarRight:SetWidth(size)
MultiBarRight:SetHeight(colHeight)
MultiBarRight:ClearAllPoints()
MultiBarRight:SetPoint("TOPRIGHT", self.rightHolder, "TOPRIGHT", 0, 0)
LayoutColumn(self.rightButtons, MultiBarRight, size, gap)
end
if MultiBarLeft then
MultiBarLeft:SetWidth(size)
MultiBarLeft:SetHeight(colHeight)
MultiBarLeft:ClearAllPoints()
MultiBarLeft:SetPoint("TOPRIGHT", MultiBarRight or self.rightHolder, "TOPLEFT", -gap, 0)
LayoutColumn(self.leftButtons, MultiBarLeft, size, gap)
end
if db.showRightBars then
self.rightHolder:Show()
else
self.rightHolder: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.rightHolder then self.rightHolder: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: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 topRow = self.row1
if db.barCount >= 3 and self.row3 then topRow = self.row3
elseif db.barCount >= 2 and self.row2 then topRow = self.row2 end
self.stanceHolder:ClearAllPoints()
self.stanceHolder:SetPoint("BOTTOMLEFT", topRow, "TOPLEFT", 0, gap)
local totalW = numForms * size + (numForms - 1) * gap
self.stanceHolder:SetWidth(totalW)
self.stanceHolder:SetHeight(size)
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)
self.petHolder:ClearAllPoints()
local numForms = GetNumShapeshiftForms and GetNumShapeshiftForms() or 0
if db.showStanceBar and numForms > 0 and self.stanceHolder:IsShown() then
self.petHolder:SetPoint("BOTTOMLEFT", self.stanceHolder, "TOPLEFT", 0, gap)
else
local topRow = self.row1
if db.barCount >= 3 and self.row3 then topRow = self.row3
elseif db.barCount >= 2 and self.row2 then topRow = self.row2 end
self.petHolder:SetPoint("BOTTOMLEFT", topRow, "TOPLEFT", 0, gap)
end
local numPet = 10
local totalW = numPet * size + (numPet - 1) * gap
self.petHolder:SetWidth(totalW)
self.petHolder:SetHeight(size)
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
--------------------------------------------------------------------------------
-- Slider-based position update
--------------------------------------------------------------------------------
function AB:ApplyPosition()
local db = self:GetDB()
local positions = SFramesDB and SFramesDB.Positions
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.rightHolder then
self.rightHolder:ClearAllPoints()
local pos = positions and positions["ActionBarRight"]
if pos and pos.point and pos.relativePoint then
self.rightHolder:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)
else
self.rightHolder:SetPoint("RIGHT", UIParent, "RIGHT", db.rightOffsetX, db.rightOffsetY)
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 rowWidth = (size + gap) * BUTTONS_PER_ROW - gap
BonusActionBarFrame:ClearAllPoints()
BonusActionBarFrame:SetPoint("BOTTOMLEFT", self.row1, "BOTTOMLEFT", 0, 0)
BonusActionBarFrame:SetWidth(rowWidth)
BonusActionBarFrame:SetHeight(size)
BonusActionBarFrame:SetFrameLevel(self.row1:GetFrameLevel() + 5)
LayoutRow(self.bonusButtons, BonusActionBarFrame, size, gap)
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()
if not db.rangeColoring then return end
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
local inRange = IsActionInRange(action)
local ov = GetOrCreateRangeOverlay(b)
if inRange == 0 then
ov:Show()
else
ov:Hide()
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
for idx, btn in ipairs(AB.bonusButtons) do
if btn == b then
btn:SetWidth(size)
btn:SetHeight(size)
btn:ClearAllPoints()
if idx == 1 then
btn:SetPoint("BOTTOMLEFT", BonusActionBarFrame, "BOTTOMLEFT", 0, 0)
else
btn:SetPoint("LEFT", AB.bonusButtons[idx - 1], "RIGHT", gap, 0)
end
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
-- Hook MultiActionBar_Update: Blizzard 在 PLAYER_ENTERING_WORLD 等事件中调用此函数,
-- 它会根据经验条/声望条的高度重设 MultiBarBottomLeft/Right 的位置,覆盖我们的布局。
-- 仅修正 row2/row3 的锚点,不调用 ApplyConfig 避免触发 MultiBarBottomLeft:Show 再次递归。
local origMultiActionBarUpdate = MultiActionBar_Update
if origMultiActionBarUpdate then
local inMultiBarHook = false
MultiActionBar_Update = function()
pcall(origMultiActionBarUpdate)
if AB.anchor and not inMultiBarHook then
inMultiBarHook = true
local g = AB:GetDB().buttonGap
if AB.row2 then
AB.row2:ClearAllPoints()
AB.row2:SetPoint("BOTTOMLEFT", AB.row1, "TOPLEFT", 0, g)
end
if AB.row3 then
AB.row3:ClearAllPoints()
AB.row3:SetPoint("BOTTOMLEFT", AB.row2 or AB.row1, "TOPLEFT", 0, g)
end
inMultiBarHook = false
end
end
end
-- Hook UIParent_ManageFramePositions: Blizzard 在进出战斗、切换地图等场景调用此
-- 函数重排 MultiBar 位置(会考虑经验条/声望条高度),覆盖我们的布局。
-- 在原函数执行完毕后立即修正 row2/row3 锚点。
local origManageFramePositions = UIParent_ManageFramePositions
if origManageFramePositions then
UIParent_ManageFramePositions = function(a1, a2, a3)
origManageFramePositions(a1, a2, a3)
if AB.anchor and AB.row1 then
local g = AB:GetDB().buttonGap
if AB.row2 then
AB.row2:ClearAllPoints()
AB.row2:SetPoint("BOTTOMLEFT", AB.row1, "TOPLEFT", 0, g)
end
if AB.row3 then
AB.row3:ClearAllPoints()
AB.row3:SetPoint("BOTTOMLEFT", AB.row2 or AB.row1, "TOPLEFT", 0, g)
end
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 rowWidth = (size + gap) * BUTTONS_PER_ROW - gap
BonusActionBarFrame:ClearAllPoints()
BonusActionBarFrame:SetPoint("BOTTOMLEFT", AB.row1, "BOTTOMLEFT", 0, 0)
BonusActionBarFrame:SetWidth(rowWidth)
BonusActionBarFrame:SetHeight(size)
BonusActionBarFrame:SetFrameLevel(AB.row1:GetFrameLevel() + 5)
LayoutRow(AB.bonusButtons, BonusActionBarFrame, size, gap)
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 or not AB.row1 then return end
local g = AB:GetDB().buttonGap
if AB.row2 then
AB.row2:ClearAllPoints()
AB.row2:SetPoint("BOTTOMLEFT", AB.row1, "TOPLEFT", 0, g)
end
if AB.row3 then
AB.row3:ClearAllPoints()
AB.row3:SetPoint("BOTTOMLEFT", AB.row2 or AB.row1, "TOPLEFT", 0, g)
end
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.rightHolder then
SFrames.Movers:RegisterMover("ActionBarRight", self.rightHolder, "右侧动作条",
"RIGHT", "UIParent", "RIGHT", db.rightOffsetX, db.rightOffsetY)
end
if self.stanceHolder then
SFrames.Movers:RegisterMover("StanceBar", self.stanceHolder, "姿态条",
"BOTTOMLEFT", "SFramesActionBarAnchor", "TOPLEFT", 0, db.buttonGap)
end
if self.petHolder then
SFrames.Movers:RegisterMover("PetBar", self.petHolder, "宠物条",
"BOTTOMLEFT", "SFramesActionBarAnchor", "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
--------------------------------------------------------------------------------
-- 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)
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)
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