Files
Nanami-UI/SpellBookUI.lua
2026-03-31 18:03:23 +08:00

997 lines
35 KiB
Lua

--------------------------------------------------------------------------------
-- Nanami-UI: SpellBook UI (SpellBookUI.lua)
-- Replaces the default SpellBookFrame with a modern rounded UI
--------------------------------------------------------------------------------
SFrames = SFrames or {}
SFrames.SpellBookUI = {}
local SB = SFrames.SpellBookUI
SFramesDB = SFramesDB or {}
--------------------------------------------------------------------------------
-- Theme (aligned with CharacterPanel / SocialUI standard palette)
--------------------------------------------------------------------------------
local T = SFrames.ActiveTheme
--------------------------------------------------------------------------------
-- Layout (single table to stay under upvalue limit)
--------------------------------------------------------------------------------
local L = {
RIGHT_TAB_W = 56,
SIDE_PAD = 10,
CONTENT_W = 316,
HEADER_H = 30,
SPELL_COLS = 2,
SPELL_ROWS = 8,
SPELL_H = 38,
ICON_SIZE = 30,
PAGE_H = 26,
OPTIONS_H = 26,
TAB_H = 40,
TAB_GAP = 2,
TAB_ICON = 26,
BOOK_TAB_W = 52,
BOOK_TAB_H = 22,
}
L.SPELLS_PER_PAGE = L.SPELL_COLS * L.SPELL_ROWS
L.SPELL_W = (L.CONTENT_W - 4) / L.SPELL_COLS
L.MAIN_W = L.CONTENT_W + L.SIDE_PAD * 2
L.FRAME_W = L.MAIN_W + L.RIGHT_TAB_W + 4
L.FRAME_H = L.HEADER_H + 6 + L.SPELL_ROWS * L.SPELL_H + 6 + L.PAGE_H + 4 + L.OPTIONS_H + 10
--------------------------------------------------------------------------------
-- State (single table to stay under upvalue limit)
--------------------------------------------------------------------------------
local S = {
frame = nil,
spellButtons = {},
tabButtons = {},
bookTabs = {},
currentTab = 1,
currentPage = 1,
currentBook = "spell",
initialized = false,
filteredCache = nil,
scanTip = nil,
}
local widgetId = 0
local function NextName(p)
widgetId = widgetId + 1
return "SFramesSB" .. (p or "") .. tostring(widgetId)
end
--------------------------------------------------------------------------------
-- Helpers
--------------------------------------------------------------------------------
local function GetFont()
if SFrames and SFrames.GetFont then return SFrames:GetFont() end
return "Fonts\\ARIALN.TTF"
end
local function SetRoundBackdrop(frame, bgColor, borderColor)
frame: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 },
})
local bg = bgColor or T.panelBg
local bd = borderColor or T.panelBorder
frame:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 1)
frame:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4] or 1)
end
local function SetPixelBackdrop(frame, bgColor, borderColor)
frame:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
edgeFile = "Interface\\Buttons\\WHITE8X8",
tile = false, tileSize = 0, edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 },
})
if bgColor then
frame:SetBackdropColor(bgColor[1], bgColor[2], bgColor[3], bgColor[4] or 1)
end
if borderColor then
frame:SetBackdropBorderColor(borderColor[1], borderColor[2], borderColor[3], borderColor[4] or 1)
end
end
local function CreateShadow(parent)
local s = CreateFrame("Frame", nil, parent)
s:SetPoint("TOPLEFT", parent, "TOPLEFT", -4, 4)
s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 4, -4)
s:SetFrameLevel(math.max(parent:GetFrameLevel() - 1, 0))
s:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 16,
insets = { left = 4, right = 4, top = 4, bottom = 4 },
})
s:SetBackdropColor(0, 0, 0, 0.6)
s:SetBackdropBorderColor(0, 0, 0, 0.45)
return s
end
local function MakeFS(parent, size, justifyH, color)
local fs = parent:CreateFontString(nil, "OVERLAY")
fs:SetFont(GetFont(), size or 11, "OUTLINE")
fs:SetJustifyH(justifyH or "LEFT")
local c = color or T.nameText
fs:SetTextColor(c[1], c[2], c[3])
return fs
end
local function MakeButton(parent, text, w, h)
local btn = CreateFrame("Button", NextName("Btn"), parent)
btn:SetWidth(w or 80)
btn:SetHeight(h or 22)
SetRoundBackdrop(btn, T.btnBg, T.btnBorder)
local fs = MakeFS(btn, 10, "CENTER", T.btnText)
fs:SetPoint("CENTER", 0, 0)
fs:SetText(text or "")
btn.text = fs
btn:SetScript("OnEnter", function()
SetRoundBackdrop(this, T.btnHoverBg, T.tabActiveBorder)
this.text:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3])
end)
btn:SetScript("OnLeave", function()
SetRoundBackdrop(this, T.btnBg, T.btnBorder)
this.text:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3])
end)
return btn
end
local function MakeSep(parent, y)
local sep = parent:CreateTexture(nil, "ARTWORK")
sep:SetTexture("Interface\\Buttons\\WHITE8X8")
sep:SetHeight(1)
sep:SetPoint("TOPLEFT", parent, "TOPLEFT", 4, y)
sep:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -4, y)
sep:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4])
return sep
end
--------------------------------------------------------------------------------
-- Hide Blizzard SpellBook
--------------------------------------------------------------------------------
local function HideBlizzardSpellBook()
if not SpellBookFrame then return end
SpellBookFrame:SetAlpha(0)
SpellBookFrame:EnableMouse(false)
SpellBookFrame:ClearAllPoints()
SpellBookFrame:SetPoint("TOPLEFT", UIParent, "BOTTOMRIGHT", 2000, 2000)
if SpellBookFrame.SetScript then
SpellBookFrame:SetScript("OnShow", function() this:Hide() end)
end
end
--------------------------------------------------------------------------------
-- Spell Data Helpers
--------------------------------------------------------------------------------
local function GetBookType()
if S.currentBook == "pet" then return BOOKTYPE_PET or "pet" end
return BOOKTYPE_SPELL or "spell"
end
local function GetTabInfo()
local numTabs = GetNumSpellTabs()
local tabs = {}
for i = 1, numTabs do
local name, texture, offset, numSpells = GetSpellTabInfo(i)
if name then
table.insert(tabs, {
name = name, texture = texture,
offset = offset, numSpells = numSpells, index = i,
})
end
end
return tabs
end
local function GetCurrentTabSpells()
local tabs = GetTabInfo()
local tab = tabs[S.currentTab]
if not tab then return 0, 0 end
return tab.offset, tab.numSpells
end
local function GetFilteredSpellList()
local offset, numSpells = GetCurrentTabSpells()
local bookType = GetBookType()
if not SFramesDB.spellBookHighestOnly then
local list = {}
for i = 1, numSpells do
table.insert(list, offset + i)
end
S.filteredCache = list
return list
end
local seen = {}
local order = {}
for i = 1, numSpells do
local idx = offset + i
local name, rank = GetSpellName(idx, bookType)
if name then
if seen[name] then
for k = 1, table.getn(order) do
if order[k].name == name then
order[k].idx = idx
break
end
end
else
seen[name] = true
table.insert(order, { name = name, idx = idx })
end
end
end
local list = {}
for _, v in ipairs(order) do
table.insert(list, v.idx)
end
S.filteredCache = list
return list
end
local function GetMaxPages()
local list = S.filteredCache or GetFilteredSpellList()
return math.max(1, math.ceil(table.getn(list) / L.SPELLS_PER_PAGE))
end
--------------------------------------------------------------------------------
-- Auto-Replace Action Bar (lower rank -> highest rank)
--------------------------------------------------------------------------------
local function EnsureScanTooltip()
if S.scanTip then return end
S.scanTip = CreateFrame("GameTooltip", "SFramesSBScanTip", UIParent, "GameTooltipTemplate")
S.scanTip:SetOwner(UIParent, "ANCHOR_NONE")
S.scanTip:SetPoint("TOPLEFT", UIParent, "BOTTOMRIGHT", 1000, 1000)
end
local function AutoReplaceActionBarSpells()
if not SFramesDB.spellBookAutoReplace then return end
if UnitAffectingCombat and UnitAffectingCombat("player") then return end
EnsureScanTooltip()
local highestByName = {}
local highestRankText = {}
local numTabs = GetNumSpellTabs()
for tab = 1, numTabs do
local _, _, offset, numSpells = GetSpellTabInfo(tab)
for i = 1, numSpells do
local idx = offset + i
local name, rank = GetSpellName(idx, "spell")
if name and not IsSpellPassive(idx, "spell") then
highestByName[name] = idx
highestRankText[name] = rank or ""
end
end
end
local tipLeft = getglobal("SFramesSBScanTipTextLeft1")
local tipRight = getglobal("SFramesSBScanTipTextRight1")
for slot = 1, 120 do
if HasAction(slot) then
S.scanTip:ClearLines()
S.scanTip:SetAction(slot)
local actionName = tipLeft and tipLeft:GetText()
local actionRank = tipRight and tipRight:GetText()
if actionName and highestByName[actionName] then
local bestRank = highestRankText[actionName]
if bestRank and bestRank ~= "" and actionRank and actionRank ~= bestRank then
PickupSpell(highestByName[actionName], "spell")
PlaceAction(slot)
ClearCursor()
end
end
end
end
end
--------------------------------------------------------------------------------
-- Slot backdrop helper: backdrop on a SEPARATE child at lower frameLevel
-- so icon / text render cleanly above it (same fix as ActionBars)
--------------------------------------------------------------------------------
local function SetSlotBg(btn, bgColor, borderColor)
if not btn.sfBg then return end
SetPixelBackdrop(btn.sfBg, bgColor or T.slotBg, borderColor or T.slotBorder)
end
local function CreateSlotBackdrop(btn)
if btn:GetBackdrop() then btn:SetBackdrop(nil) end
local level = btn:GetFrameLevel()
local bd = CreateFrame("Frame", nil, btn)
bd:SetFrameLevel(level > 0 and (level - 1) or 0)
bd:SetAllPoints(btn)
SetPixelBackdrop(bd, T.slotBg, T.slotBorder)
btn.sfBg = bd
return bd
end
--------------------------------------------------------------------------------
-- Update Spell Buttons
--------------------------------------------------------------------------------
local function UpdateSpellButtons()
if not S.frame or not S.frame:IsShown() then return end
local list = GetFilteredSpellList()
local bookType = GetBookType()
local startIdx = (S.currentPage - 1) * L.SPELLS_PER_PAGE
local totalSpells = table.getn(list)
for i = 1, L.SPELLS_PER_PAGE do
local btn = S.spellButtons[i]
if not btn then break end
local listIdx = startIdx + i
local spellIdx = list[listIdx]
if spellIdx and listIdx <= totalSpells then
local spellName, spellRank = GetSpellName(spellIdx, bookType)
local texture = GetSpellTexture(spellIdx, bookType)
btn.icon:SetTexture(texture)
btn.icon:SetAlpha(1)
btn.nameFS:SetText(spellName or "")
btn.subFS:SetText(spellRank or "")
btn.spellId = spellIdx
btn.bookType = bookType
local isPassive = IsSpellPassive(spellIdx, bookType)
if isPassive then
btn.nameFS:SetTextColor(T.passive[1], T.passive[2], T.passive[3])
btn.subFS:SetTextColor(T.passive[1], T.passive[2], T.passive[3])
btn.icon:SetVertexColor(T.passive[1], T.passive[2], T.passive[3])
btn.passiveBadge:Show()
else
btn.nameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3])
btn.subFS:SetTextColor(T.subText[1], T.subText[2], T.subText[3])
btn.icon:SetVertexColor(1, 1, 1)
btn.passiveBadge:Hide()
end
SetSlotBg(btn, T.slotBg, T.slotBorder)
btn:Show()
btn:Enable()
else
btn.icon:SetTexture(nil)
btn.icon:SetAlpha(0)
btn.nameFS:SetText("")
btn.subFS:SetText("")
btn.spellId = nil
btn.bookType = nil
btn.passiveBadge:Hide()
SetSlotBg(btn, T.emptySlotBg, T.emptySlotBd)
btn:Show()
btn:Disable()
end
end
local maxPages = GetMaxPages()
local f = S.frame
if f.pageText then
if maxPages <= 1 then
f.pageText:SetText("")
else
f.pageText:SetText(S.currentPage .. " / " .. maxPages)
end
end
if f.prevBtn then
if S.currentPage > 1 then
f.prevBtn:Enable(); f.prevBtn:SetAlpha(1)
else
f.prevBtn:Disable(); f.prevBtn:SetAlpha(0.4)
end
end
if f.nextBtn then
if S.currentPage < maxPages then
f.nextBtn:Enable(); f.nextBtn:SetAlpha(1)
else
f.nextBtn:Disable(); f.nextBtn:SetAlpha(0.4)
end
end
end
--------------------------------------------------------------------------------
-- Update Skill Line Tabs (right side)
--------------------------------------------------------------------------------
local function UpdateSkillLineTabs()
local tabs = GetTabInfo()
for i = 1, 8 do
local btn = S.tabButtons[i]
if not btn then break end
local tab = tabs[i]
if tab then
if tab.texture then
btn.tabIcon:SetTexture(tab.texture)
btn.tabIcon:Show()
else
btn.tabIcon:Hide()
end
btn.tabLabel:SetText(tab.name or "")
btn:Show()
if i == S.currentTab then
SetRoundBackdrop(btn, T.tabActiveBg, T.tabActiveBorder)
btn.tabIcon:SetVertexColor(1, 1, 1)
btn.tabLabel:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3])
btn.indicator:Show()
else
SetRoundBackdrop(btn, T.tabBg, T.tabBorder)
btn.tabIcon:SetVertexColor(T.tabText[1], T.tabText[2], T.tabText[3])
btn.tabLabel:SetTextColor(T.tabText[1], T.tabText[2], T.tabText[3])
btn.indicator:Hide()
end
else
btn:Hide()
end
end
end
--------------------------------------------------------------------------------
-- Update Book Tabs (Spell / Pet)
--------------------------------------------------------------------------------
local function UpdateBookTabs()
for i = 1, 2 do
local bt = S.bookTabs[i]
if not bt then break end
local isActive = (i == 1 and S.currentBook == "spell") or (i == 2 and S.currentBook == "pet")
if isActive then
SetRoundBackdrop(bt, T.tabActiveBg, T.tabActiveBorder)
bt.text:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3])
else
SetRoundBackdrop(bt, T.tabBg, T.tabBorder)
bt.text:SetTextColor(T.tabText[1], T.tabText[2], T.tabText[3])
end
end
local petTab = S.bookTabs[2]
if petTab then
local hasPet = HasPetSpells and HasPetSpells()
if hasPet then petTab:Show() else petTab:Hide() end
end
end
local function FullRefresh()
S.filteredCache = nil
UpdateSkillLineTabs()
UpdateBookTabs()
UpdateSpellButtons()
if S.frame and S.frame.optHighest then
S.frame.optHighest:UpdateVisual()
end
if S.frame and S.frame.optReplace then
S.frame.optReplace:UpdateVisual()
end
end
--------------------------------------------------------------------------------
-- Build: Header
--------------------------------------------------------------------------------
local function BuildHeader(f)
local header = CreateFrame("Frame", nil, f)
header:SetHeight(L.HEADER_H)
header:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
header:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0)
local function MakeBookTab(parent, label, idx, xOff)
local bt = CreateFrame("Button", NextName("BookTab"), parent)
bt:SetWidth(L.BOOK_TAB_W)
bt:SetHeight(L.BOOK_TAB_H)
bt:SetPoint("LEFT", parent, "LEFT", xOff, 0)
SetRoundBackdrop(bt, T.tabBg, T.tabBorder)
local txt = MakeFS(bt, 10, "CENTER", T.tabText)
txt:SetPoint("CENTER", 0, 0)
txt:SetText(label)
bt.text = txt
bt.bookIdx = idx
bt:SetScript("OnClick", function()
if this.bookIdx == 1 then S.currentBook = "spell" else S.currentBook = "pet" end
S.currentTab = 1
S.currentPage = 1
FullRefresh()
end)
bt:SetScript("OnEnter", function()
this.text:SetTextColor(T.gold[1], T.gold[2], T.gold[3])
end)
bt:SetScript("OnLeave", function()
local active = (this.bookIdx == 1 and S.currentBook == "spell") or (this.bookIdx == 2 and S.currentBook == "pet")
if active then
this.text:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3])
else
this.text:SetTextColor(T.tabText[1], T.tabText[2], T.tabText[3])
end
end)
return bt
end
S.bookTabs[1] = MakeBookTab(header, "法术", 1, 8)
S.bookTabs[2] = MakeBookTab(header, "宠物", 2, 8 + L.BOOK_TAB_W + 4)
local titleIco = SFrames:CreateIcon(header, "spellbook", 14)
titleIco:SetDrawLayer("OVERLAY")
titleIco:SetPoint("CENTER", header, "CENTER", -28, 0)
titleIco:SetVertexColor(T.gold[1], T.gold[2], T.gold[3])
local title = MakeFS(header, 13, "CENTER", T.gold)
title:SetPoint("LEFT", titleIco, "RIGHT", 4, 0)
title:SetText("法术书")
f.titleFS = title
local closeBtn = CreateFrame("Button", NextName("Close"), header)
closeBtn:SetWidth(20)
closeBtn:SetHeight(20)
closeBtn:SetPoint("RIGHT", header, "RIGHT", -8, 0)
SetRoundBackdrop(closeBtn, T.buttonDownBg, T.btnBorder)
closeBtn:SetScript("OnClick", function() this:GetParent():GetParent():Hide() end)
local closeIco = SFrames:CreateIcon(closeBtn, "close", 12)
closeIco:SetDrawLayer("OVERLAY")
closeIco:SetPoint("CENTER", closeBtn, "CENTER", 0, 0)
closeIco:SetVertexColor(1, 0.7, 0.7)
closeBtn.nanamiIcon = closeIco
closeBtn:SetScript("OnEnter", function()
SetRoundBackdrop(this, T.btnHoverBg, T.btnHoverBd)
if this.nanamiIcon then this.nanamiIcon:SetVertexColor(1, 1, 1) end
end)
closeBtn:SetScript("OnLeave", function()
SetRoundBackdrop(this, T.buttonDownBg, T.btnBorder)
if this.nanamiIcon then this.nanamiIcon:SetVertexColor(1, 0.7, 0.7) end
end)
MakeSep(f, -L.HEADER_H)
end
--------------------------------------------------------------------------------
-- Build: Right Skill Line Tabs
--------------------------------------------------------------------------------
local function BuildSkillTabs(f)
for idx = 1, 8 do
local btn = CreateFrame("Button", NextName("Tab"), f)
btn:SetWidth(L.RIGHT_TAB_W)
btn:SetHeight(L.TAB_H)
btn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -3,
-(L.HEADER_H + 4 + (idx - 1) * (L.TAB_H + L.TAB_GAP)))
SetRoundBackdrop(btn, T.tabBg, T.tabBorder)
btn.tabIndex = idx
local indicator = btn:CreateTexture(nil, "OVERLAY")
indicator:SetTexture("Interface\\Buttons\\WHITE8X8")
indicator:SetWidth(3)
indicator:SetPoint("TOPLEFT", btn, "TOPLEFT", 1, -4)
indicator:SetPoint("BOTTOMLEFT", btn, "BOTTOMLEFT", 1, 4)
indicator:SetVertexColor(T.accent[1], T.accent[2], T.accent[3], T.accent[4])
indicator:Hide()
btn.indicator = indicator
local icon = btn:CreateTexture(nil, "ARTWORK")
icon:SetWidth(L.TAB_ICON)
icon:SetHeight(L.TAB_ICON)
icon:SetPoint("TOP", btn, "TOP", 0, -4)
icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
btn.tabIcon = icon
local label = MakeFS(btn, 7, "CENTER", T.tabText)
label:SetPoint("BOTTOM", btn, "BOTTOM", 0, 3)
label:SetWidth(L.RIGHT_TAB_W - 6)
btn.tabLabel = label
btn:SetScript("OnClick", function()
S.currentTab = this.tabIndex
S.currentPage = 1
FullRefresh()
end)
btn:SetScript("OnEnter", function()
if this.tabIndex ~= S.currentTab then
SetRoundBackdrop(this, T.slotHover, T.tabActiveBorder)
this.tabIcon:SetVertexColor(1, 1, 1)
this.tabLabel:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3])
end
local tabs = GetTabInfo()
local tab = tabs[this.tabIndex]
if tab then
GameTooltip:SetOwner(this, "ANCHOR_LEFT")
GameTooltip:AddLine(tab.name, T.gold[1], T.gold[2], T.gold[3])
GameTooltip:AddLine(tab.numSpells .. " 个法术", T.subText[1], T.subText[2], T.subText[3])
GameTooltip:Show()
end
end)
btn:SetScript("OnLeave", function()
GameTooltip:Hide()
if this.tabIndex ~= S.currentTab then
SetRoundBackdrop(this, T.tabBg, T.tabBorder)
this.tabIcon:SetVertexColor(T.tabText[1], T.tabText[2], T.tabText[3])
this.tabLabel:SetTextColor(T.tabText[1], T.tabText[2], T.tabText[3])
end
end)
btn:Hide()
S.tabButtons[idx] = btn
end
local tabSep = f:CreateTexture(nil, "ARTWORK")
tabSep:SetTexture("Interface\\Buttons\\WHITE8X8")
tabSep:SetWidth(1)
tabSep:SetPoint("TOPLEFT", f, "TOPRIGHT", -(L.RIGHT_TAB_W + 5), -(L.HEADER_H + 2))
tabSep:SetPoint("BOTTOMLEFT", f, "BOTTOMRIGHT", -(L.RIGHT_TAB_W + 5), 4)
tabSep:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4])
end
--------------------------------------------------------------------------------
-- Build: Spell Buttons Grid
-- Backdrop on a SEPARATE child frame at lower frameLevel (ActionBars fix)
--------------------------------------------------------------------------------
local function BuildSpellGrid(f)
local contentTop = -(L.HEADER_H + 6)
local contentFrame = CreateFrame("Frame", nil, f)
contentFrame:SetPoint("TOPLEFT", f, "TOPLEFT", L.SIDE_PAD, contentTop)
contentFrame:SetWidth(L.CONTENT_W)
contentFrame:SetHeight(L.SPELL_ROWS * L.SPELL_H + 4)
for row = 1, L.SPELL_ROWS do
for col = 1, L.SPELL_COLS do
local idx = (row - 1) * L.SPELL_COLS + col
local btn = CreateFrame("Button", NextName("Spell"), contentFrame)
btn:SetWidth(L.SPELL_W - 2)
btn:SetHeight(L.SPELL_H - 2)
local x = (col - 1) * L.SPELL_W + 1
local y = -((row - 1) * L.SPELL_H)
btn:SetPoint("TOPLEFT", contentFrame, "TOPLEFT", x, y)
CreateSlotBackdrop(btn)
local icon = btn:CreateTexture(nil, "ARTWORK")
icon:SetWidth(L.ICON_SIZE)
icon:SetHeight(L.ICON_SIZE)
icon:SetPoint("LEFT", btn, "LEFT", 5, 0)
icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
btn.icon = icon
local nameFS = MakeFS(btn, 11, "LEFT", T.nameText)
nameFS:SetPoint("TOPLEFT", icon, "TOPRIGHT", 6, -2)
nameFS:SetPoint("RIGHT", btn, "RIGHT", -4, 0)
btn.nameFS = nameFS
local subFS = MakeFS(btn, 9, "LEFT", T.subText)
subFS:SetPoint("BOTTOMLEFT", icon, "BOTTOMRIGHT", 6, 2)
subFS:SetPoint("RIGHT", btn, "RIGHT", -4, 0)
btn.subFS = subFS
local passiveBadge = MakeFS(btn, 7, "RIGHT", T.passive)
passiveBadge:SetPoint("TOPRIGHT", btn, "TOPRIGHT", -4, -3)
passiveBadge:SetText("被动")
passiveBadge:Hide()
btn.passiveBadge = passiveBadge
btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
btn:RegisterForDrag("LeftButton")
btn:SetScript("OnClick", function()
if not this.spellId then return end
if arg1 == "LeftButton" then
CastSpell(this.spellId, this.bookType)
elseif arg1 == "RightButton" then
PickupSpell(this.spellId, this.bookType)
end
end)
btn:SetScript("OnDragStart", function()
if this.spellId then
PickupSpell(this.spellId, this.bookType)
end
end)
btn:SetScript("OnEnter", function()
if this.spellId then
SetSlotBg(this, T.slotHover, T.slotSelected)
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:SetSpell(this.spellId, this.bookType)
GameTooltip:Show()
end
end)
btn:SetScript("OnLeave", function()
if this.spellId then
SetSlotBg(this, T.slotBg, T.slotBorder)
else
SetSlotBg(this, T.emptySlotBg, T.emptySlotBd)
end
GameTooltip:Hide()
end)
S.spellButtons[idx] = btn
end
end
contentFrame:EnableMouseWheel(true)
contentFrame:SetScript("OnMouseWheel", function()
if arg1 > 0 then
if S.currentPage > 1 then
S.currentPage = S.currentPage - 1
UpdateSpellButtons()
end
else
if S.currentPage < GetMaxPages() then
S.currentPage = S.currentPage + 1
UpdateSpellButtons()
end
end
end)
return contentTop
end
--------------------------------------------------------------------------------
-- Build: Pagination
--------------------------------------------------------------------------------
local function BuildPagination(f, contentTop)
local pageY = contentTop - L.SPELL_ROWS * L.SPELL_H - 6
local prevBtn = MakeButton(f, "< 上一页", 66, L.PAGE_H)
prevBtn:SetPoint("TOPLEFT", f, "TOPLEFT", L.SIDE_PAD, pageY)
prevBtn:SetScript("OnClick", function()
if S.currentPage > 1 then
S.currentPage = S.currentPage - 1
UpdateSpellButtons()
end
end)
f.prevBtn = prevBtn
local nextBtn = MakeButton(f, "下一页 >", 66, L.PAGE_H)
nextBtn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -(L.RIGHT_TAB_W + L.SIDE_PAD + 4), pageY)
nextBtn:SetScript("OnClick", function()
if S.currentPage < GetMaxPages() then
S.currentPage = S.currentPage + 1
UpdateSpellButtons()
end
end)
f.nextBtn = nextBtn
local pageText = MakeFS(f, 11, "CENTER", T.pageText)
pageText:SetPoint("LEFT", prevBtn, "RIGHT", 4, 0)
pageText:SetPoint("RIGHT", nextBtn, "LEFT", -4, 0)
f.pageText = pageText
return pageY
end
--------------------------------------------------------------------------------
-- Build: Options bar
--------------------------------------------------------------------------------
local function BuildOptions(f, pageY)
local optY = pageY - L.PAGE_H - 4
MakeSep(f, optY + 2)
local function MakeCheckOption(parent, label, xOff, yOff, getFunc, setFunc)
local btn = CreateFrame("Button", NextName("Opt"), parent)
btn:SetHeight(L.OPTIONS_H - 4)
btn:SetPoint("TOPLEFT", parent, "TOPLEFT", xOff, yOff)
local box = CreateFrame("Frame", nil, btn)
box:SetWidth(12)
box:SetHeight(12)
box:SetPoint("LEFT", btn, "LEFT", 0, 0)
SetPixelBackdrop(box, T.checkOff, T.tabBorder)
btn.box = box
local checkMark = MakeFS(box, 10, "CENTER", T.checkOn)
checkMark:SetPoint("CENTER", 0, 1)
checkMark:SetText("")
btn.checkMark = checkMark
local txt = MakeFS(btn, 9, "LEFT", T.optionText)
txt:SetPoint("LEFT", box, "RIGHT", 4, 0)
txt:SetText(label)
btn.label = txt
btn:SetWidth(txt:GetStringWidth() + 20)
btn.getFunc = getFunc
btn.setFunc = setFunc
function btn:UpdateVisual()
if self.getFunc() then
self.checkMark:SetText("")
SetPixelBackdrop(self.box, { T.checkOn[1]*0.3, T.checkOn[2]*0.3, T.checkOn[3]*0.3, 0.8 }, T.checkOn)
self.label:SetTextColor(T.tabActiveText[1], T.tabActiveText[2], T.tabActiveText[3])
else
self.checkMark:SetText("")
SetPixelBackdrop(self.box, T.checkOff, T.tabBorder)
self.label:SetTextColor(T.optionText[1], T.optionText[2], T.optionText[3])
end
end
btn:SetScript("OnClick", function()
this.setFunc(not this.getFunc())
this:UpdateVisual()
FullRefresh()
end)
btn:SetScript("OnEnter", function()
this.label:SetTextColor(T.gold[1], T.gold[2], T.gold[3])
end)
btn:SetScript("OnLeave", function()
this:UpdateVisual()
end)
btn:UpdateVisual()
return btn
end
f.optHighest = MakeCheckOption(f, "只显示最高等级", L.SIDE_PAD, optY,
function() return SFramesDB.spellBookHighestOnly == true end,
function(v) SFramesDB.spellBookHighestOnly = v; S.currentPage = 1 end
)
f.optReplace = MakeCheckOption(f, "学习新等级自动替换动作条", L.SIDE_PAD + 120, optY,
function() return SFramesDB.spellBookAutoReplace == true end,
function(v) SFramesDB.spellBookAutoReplace = v end
)
f.optMouseover = MakeCheckOption(f, "鼠标指向施法", L.SIDE_PAD + 280, optY,
function()
return SFramesDB.Tweaks and SFramesDB.Tweaks.mouseoverCast == true
end,
function(v)
if not SFramesDB.Tweaks then SFramesDB.Tweaks = {} end
SFramesDB.Tweaks.mouseoverCast = v
if SFrames.Tweaks and SFrames.Tweaks.SetMouseoverCast then
SFrames.Tweaks:SetMouseoverCast(v)
end
end
)
end
--------------------------------------------------------------------------------
-- Build Main Frame
--------------------------------------------------------------------------------
local function BuildMainFrame()
if S.frame then return end
local f = CreateFrame("Frame", "SFramesSpellBookUI", UIParent)
f:SetWidth(L.FRAME_W)
f:SetHeight(L.FRAME_H)
f:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
f:SetFrameStrata("HIGH")
f:SetFrameLevel(10)
SetRoundBackdrop(f)
CreateShadow(f)
f:EnableMouse(true)
f:SetMovable(true)
f:RegisterForDrag("LeftButton")
f:SetScript("OnDragStart", function() this:StartMoving() end)
f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
f:SetScript("OnShow", function()
if SpellBookFrame and SpellBookFrame:IsShown() then
SpellBookFrame:Hide()
end
FullRefresh()
end)
BuildHeader(f)
BuildSkillTabs(f)
local contentTop = BuildSpellGrid(f)
local pageY = BuildPagination(f, contentTop)
BuildOptions(f, pageY)
table.insert(UISpecialFrames, "SFramesSpellBookUI")
local scale = SFramesDB.spellBookScale or 1
if scale ~= 1 then f:SetScale(scale) end
S.frame = f
f:Hide()
end
--------------------------------------------------------------------------------
-- Public API
--------------------------------------------------------------------------------
function SB:Toggle(bookType)
if not S.initialized then return end
if not S.frame then BuildMainFrame() end
if bookType then
if bookType == (BOOKTYPE_PET or "pet") then
S.currentBook = "pet"
else
S.currentBook = "spell"
end
end
if S.frame:IsShown() then
S.frame:Hide()
else
S.currentTab = 1
S.currentPage = 1
S.frame:Show()
end
end
function SB:Show(bookType)
if not S.initialized then return end
if not S.frame then BuildMainFrame() end
if bookType then
if bookType == (BOOKTYPE_PET or "pet") then
S.currentBook = "pet"
else
S.currentBook = "spell"
end
end
S.currentTab = 1
S.currentPage = 1
S.frame:Show()
end
function SB:Hide()
if S.frame and S.frame:IsShown() then
S.frame:Hide()
end
end
function SB:IsShown()
return S.frame and S.frame:IsShown()
end
--------------------------------------------------------------------------------
-- Initialize
--------------------------------------------------------------------------------
function SB:Initialize()
if S.initialized then return end
S.initialized = true
if SFramesDB.spellBookHighestOnly == nil then SFramesDB.spellBookHighestOnly = false end
if SFramesDB.spellBookAutoReplace == nil then SFramesDB.spellBookAutoReplace = false end
HideBlizzardSpellBook()
BuildMainFrame()
local ef = CreateFrame("Frame", nil, UIParent)
ef:RegisterEvent("SPELLS_CHANGED")
ef:RegisterEvent("LEARNED_SPELL_IN_TAB")
ef:RegisterEvent("SPELL_UPDATE_COOLDOWN")
ef:SetScript("OnEvent", function()
if event == "LEARNED_SPELL_IN_TAB" then
AutoReplaceActionBarSpells()
end
if S.frame and S.frame:IsShown() then
FullRefresh()
end
end)
end
--------------------------------------------------------------------------------
-- Hook ToggleSpellBook
--------------------------------------------------------------------------------
local origToggleSpellBook = ToggleSpellBook
ToggleSpellBook = function(bookType)
if SFramesDB and SFramesDB.enableSpellBook == false then
if origToggleSpellBook then
origToggleSpellBook(bookType)
end
return
end
SB:Toggle(bookType)
end
--------------------------------------------------------------------------------
-- Bootstrap
--------------------------------------------------------------------------------
local bootstrap = CreateFrame("Frame", nil, UIParent)
bootstrap:RegisterEvent("PLAYER_LOGIN")
bootstrap:SetScript("OnEvent", function()
if event == "PLAYER_LOGIN" then
if SFramesDB.enableSpellBook == nil then
SFramesDB.enableSpellBook = true
end
if SFramesDB.enableSpellBook ~= false then
SB:Initialize()
end
end
end)