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