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