2359 lines
86 KiB
Lua
2359 lines
86 KiB
Lua
--------------------------------------------------------------------------------
|
||
-- 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
|