984 lines
34 KiB
Lua
984 lines
34 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
|
|
)
|
|
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)
|