Files
moon-well-ui/vanilla/Interface/FrameXML/MoonWellUI/Forever/SpellBook.lua
T
sindoring c7ff319ccc Fix the pet stat tooltip and the spellbook tab flash
PaperDollStatTooltip upper-cases UnitClass(unit)'s token, and a unit whose
class byte has no ChrClasses row answers two nils (UnitClass, 0x518350):
the server's demons do, so hovering the pet page's stats raised "bad
argument #1 to strupper". Such a unit now gets the stock default line, in
both styles.

LEARNED_SPELL_IN_TAB's glow flashed where 1.12's hidden skill line tabs
stood: it now sits on the book's own tabs, and choosing a tab stops it, as
SpellBookSkillLineTab_OnClick does.

Verified: harness probe (the pet tooltip shows the default strength line,
no error; the tab 2 glow's center matches our tab 2's; choosing tab 2
hides it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:36:06 +04:00

1007 lines
33 KiB
Lua

-- After data/addon/ForeverUI/SpellBook.lua: camelot's spellbook (blizzard_playerspells) inside
-- 1.12's SpellBookFrame. SpellBookFrame keeps its open and close paths (ToggleSpellBook, the P
-- and Shift-P bindings, the micro button) and its close button, re-skinned; its art, tabs and
-- twelve spell buttons are hidden.
-- 1.12 casts from any button (CastSpell is not protected), so the cells are plain buttons and
-- the 3.3.5 port's secure controller goes. Clicks follow 1.12's SpellButton_OnClick: Shift
-- adds a /cast line to an open macro window or picks the spell up, right click toggles a pet
-- spell's autocast.
-- Differences with camelot, beyond the 3.3.5 port's (square icons, no upcoming spells):
-- * no flyout groups: 1.12 has no GetSpellInfo to name a group's spells by id;
-- * no search;
-- * "Show all spell ranks" is ours (1.12 has no ShowAllSpellRanks CVar) and on by default,
-- as 1.12's own book lists every rank.
if not MoonWellUI_IsForever() then
return
end
local L = ForeverUI.L
local B = {}
ForeverUI.SpellBook = B
-- Layout values from blizzard_playerspells: PlayerSpellsFrame, SpellBookFrame,
-- CategoryTabSystem, PagedSpellsFrame (PagedCondensedVerticalGrid), PagingControls.
local G = {
width = 1618, compactWidth = 809, height = 720, top = -116,
fitExtraW = 200, fitExtraH = 140, clampBottom = 140, clampTop = -10,
bookW = 1612, reducedBookW = 806, bookH = 702, bookBottom = 4,
tabsX = 70, tabsY = -26, tabW = 44, tabH = 32, tabGap = 1,
tabIconW = 34, tabIconH = 33,
pagesTop = -50, viewW = 680, viewH = 590, view1X = 85, viewY = -45, view2X = -50,
columns = 3, gapX = 15, gapY = 10, headerH = 51, spellH = 60, space = 20,
pagerX = -75, pagerY = 40, pagerGap = 8, arrowSide = 32,
buttonSide = 40, iconSide = 36, textX = 50,
cellsPerView = 24,
closeX = -2, closeY = 1, redSide = 24,
portraitX = -5, portraitY = 7, portraitSide = 62,
titleX1 = 58, titleX2 = -24, titleY = -1, titleH = 20,
settingsX = -30, settingsY = -27, settingsW = 15, settingsH = 16,
metalLevel = 20, portraitLevel = 19, titleLevel = 21, buttonLevel = 22,
backplateAlpha = 0.25, hoverAlpha = 0.35, pressedAlpha = 0.65,
}
G.cellW = (G.viewW - G.gapX * (G.columns - 1)) / G.columns
B.G = G
-- metal corners: PortraitFrameTemplate as camelot adjusts it
local METAL = {
{ key = "topLeft", name = "ui-frame-portraitmetal-cornertopleft", point = "TOPLEFT", x = -13, y = 16 },
{ key = "topRight", name = "ui-frame-metal-cornertopright", point = "TOPRIGHT", x = 2, y = 16 },
{ key = "bottomLeft", name = "ui-frame-metal-cornerbottomleft", point = "BOTTOMLEFT", x = -13, y = -8 },
{ key = "bottomRight", name = "ui-frame-metal-cornerbottomright", point = "BOTTOMRIGHT", x = 2, y = -8 },
}
local INK = { 0.18, 0.106, 0.059 } -- SPELLBOOK_FONT_COLOR
local TEXT = {
title = SPELLBOOK,
page = L.SPELLBOOK_PAGE,
passive = SPELL_PASSIVE,
hidePassives = L.SPELLBOOK_HIDE_PASSIVES,
allRanks = MOONWELLUI_TEXT.ALL_RANKS,
}
-- Settings menu (camelot's SetupSettingsDropdown, as DropDown.lua skins the client list):
-- rows of 20 at x 11 from 15 down, text 20 in, a ticked square of 12, the yellow check 15 x 14
-- at (2, 1); common-dropdown-bg-c60 nine-slice at 0.925; closes 2 s after the mouse leaves.
local MENU = {
rowH = 20, edge = 15, rowX = 11, textX = 20, extraWidth = 40, margin = 25,
checkbox = 12, checkMarkW = 15, checkMarkH = 14, checkMarkX = 2, checkMarkY = 1,
backgroundCorner = 18, backgroundMargins = { 9, 6, 9, 12 }, backgroundAlpha = 0.925, pending = 2,
}
local SETTINGS = {
{ key = "hidePassives", text = TEXT.hidePassives },
{ key = "allRanks", text = TEXT.allRanks },
}
local FILES = {
rock = "Interface\\ForeverUI\\framegeneral\\ui-background-rock",
arrows = "Interface\\Buttons\\UI-SpellbookIcon-",
squareHover = "Interface\\Buttons\\UI-Common-MouseHilight",
classIcon = "Interface\\ForeverUI\\icons\\classicon_",
-- the icon of camelot's General line (inv_misc_book_09), its round mask baked in
portrait = "Interface\\ForeverUI\\spellbook\\portrait",
rowHighlight = "Interface\\QuestFrame\\UI-QuestTitleHighlight",
petIcons = {
HUNTER = "Interface\\Icons\\Ability_Hunter_BeastTaming",
WARLOCK = "Interface\\Icons\\Spell_Shadow_SummonImp",
},
}
local function settings()
ForeverUIDB = ForeverUIDB or {}
ForeverUIDB.spellbook = ForeverUIDB.spellbook or {}
local s = ForeverUIDB.spellbook
if s.allRanks == nil then
s.allRanks = true
end
return s
end
local function font(fs, size, c)
fs:SetFont((GameFontNormal:GetFont()), size)
c = c or INK
fs:SetTextColor(c[1], c[2], c[3])
end
local function atlas(t, name, keep)
return ForeverUI.SetAtlas(t, name, keep)
end
-- Hides a client region or frame for good: SpellBookFrame_Update shows its tabs again.
local function suppress(f)
f:SetAlpha(0)
if f.EnableMouse then
f:EnableMouse(false)
end
f:Hide()
end
-- ---------------------------------------------------------------- Data
-- camelot's categories: one per spell tab (General with the class icon, then one per class
-- tree), plus the pet's (HasPetSpells: count and PET_TYPE_ token).
function B.categories()
local list = {}
for i = 1, GetNumSpellTabs() do
local name, icon, offset, count = GetSpellTabInfo(i)
if name then
if i == 1 then
local _, token = UnitClass("player")
icon = FILES.classIcon .. string.lower(token or "warrior")
end
table.insert(list, { name = name, icon = icon, offset = offset, count = count, book = BOOKTYPE_SPELL })
end
end
local count, token = HasPetSpells()
if count and count > 0 then
local _, class = UnitClass("player")
table.insert(list, { name = getglobal("PET_TYPE_" .. (token or "PET")) or PET_TYPE_PET,
icon = FILES.petIcons[class or ""] or FILES.petIcons.HUNTER,
offset = 0, count = count, book = BOOKTYPE_PET, pet = true })
end
return list
end
-- A category's spells. Without all ranks a spell keeps its highest rank, the last of its line;
-- hide passives drops IsSpellPassive's.
function B.spellsOf(cat)
local s = settings()
local spells, last = {}, {}
for slot = cat.offset + 1, cat.offset + cat.count do
local name, rank = GetSpellName(slot, cat.book)
local passive = name and IsSpellPassive(slot, cat.book) and true or false
if name and not (s.hidePassives and passive) then
local spell = { slot = slot, book = cat.book, name = name, rank = rank or "", passive = passive,
icon = GetSpellTexture(slot, cat.book), pet = cat.pet }
if not s.allRanks and last[name] then
spells[last[name]] = spell
else
table.insert(spells, spell)
last[name] = table.getn(spells)
end
end
end
return spells
end
-- PagedCondensedVerticalGrid: the header takes a row (again on each following view), then the
-- spells fill the columns one by one over ceil(remaining / 3) rows in the space left; a full
-- view opens another. Returns the views, each a list of { header or spell, column, y }.
function B.paginate(title, spells)
local views = {}
local view, busy
local step = G.spellH + G.gapY
local function newView()
view = {}
table.insert(views, view)
table.insert(view, { header = title, column = 1, y = 0 })
busy = G.headerH + G.gapY
end
newView()
local total = table.getn(spells)
local i = 1
while i <= total do
local available = math.floor((G.viewH - busy) / step)
if available < 1 then
newView()
available = math.floor((G.viewH - busy) / step)
end
local rows = math.min(math.ceil((total - i + 1) / G.columns), available)
for column = 1, G.columns do
for r = 1, rows do
if i <= total then
table.insert(view, { spell = spells[i], column = column, y = busy + (r - 1) * step })
i = i + 1
end
end
end
busy = busy + rows * step
if i <= total then
newView()
end
end
return views
end
-- --------------------------------------------------------------- Cells
-- 1.12's SpellButton_OnClick on a cell's spell.
local function clickSpell(spell)
if IsShiftKeyDown() then
if MacroFrame and MacroFrame:IsVisible() then
if not spell.passive then
local line = SLASH_CAST1 .. " " .. spell.name
if spell.rank ~= "" then
line = line .. "(" .. spell.rank .. ")"
end
MacroFrame_AddMacroLine(line)
end
else
PickupSpell(spell.slot, spell.book)
end
elseif arg1 ~= "LeftButton" and spell.pet then
ToggleSpellAutocast(spell.slot, spell.book)
else
CastSpell(spell.slot, spell.book)
end
B.updateStates()
end
local function showTooltip(button)
local spell = button.cell.spell
if not spell then
return
end
GameTooltip:SetOwner(button, "ANCHOR_RIGHT")
if GameTooltip:SetSpell(spell.slot, spell.book) then
button.updateTooltip = TOOLTIP_UPDATE_TIME
else
button.updateTooltip = nil
end
end
-- Spell cell k: a holder (backplate, texts) with the icon button inside.
local function createCell(view, k)
local c = CreateFrame("Frame", nil, view)
c:SetWidth(G.cellW)
c:SetHeight(G.spellH)
local background = c:CreateTexture(nil, "BACKGROUND")
atlas(background, "spellbook-item-backplate")
background:SetPoint("CENTER", c, "CENTER", 5, -5)
background:SetAlpha(G.backplateAlpha)
c.background = background
local name = "ForeverUISpellBookButton" .. k
local b = CreateFrame("Button", name, c)
b:SetWidth(G.buttonSide)
b:SetHeight(G.buttonSide)
b:SetPoint("LEFT", c, "LEFT", 0, 0)
b:RegisterForClicks("LeftButtonUp", "RightButtonUp")
b:RegisterForDrag("LeftButton")
b.cell = c
-- named: SetPortraitToTexture takes a texture's name
local icon = b:CreateTexture(name .. "Icon", "ARTWORK")
icon:SetWidth(G.iconSide)
icon:SetHeight(G.iconSide)
icon:SetPoint("CENTER", b, "CENTER", 0, 0)
local shadow = b:CreateTexture(nil, "ARTWORK")
atlas(shadow, "spellbook-item-iconframe-shadow", true)
shadow:SetPoint("TOPLEFT", b, "TOPLEFT", -12, 3)
shadow:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", 2, -8)
local frame = b:CreateTexture(nil, "OVERLAY")
local hover = b:CreateTexture(nil, "OVERLAY")
hover:SetAllPoints(b)
hover:SetBlendMode("ADD")
hover:SetAlpha(G.hoverAlpha)
hover:Hide()
local auto = b:CreateTexture(nil, "OVERLAY")
atlas(auto, "spellbook-item-petautocast-corners", true)
auto:SetAllPoints(icon)
auto:Hide()
local cooldown = CreateFrame("Model", name .. "Cooldown", b, "CooldownFrameTemplate")
cooldown:SetPoint("TOPLEFT", icon, "TOPLEFT", 2, -2)
cooldown:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", -2, 2)
b.icon, b.iconName, b.shadow, b.frame, b.hover, b.auto, b.cooldown = icon, name .. "Icon", shadow, frame, hover, auto, cooldown
local title = c:CreateFontString(nil, "ARTWORK")
font(title, 16)
title:SetJustifyH("LEFT")
title:SetWidth(G.cellW - G.textX)
local sub = c:CreateFontString(nil, "ARTWORK")
font(sub, 12)
sub:SetJustifyH("LEFT")
sub:SetWidth(G.cellW - G.textX)
c.name, c.sub = title, sub
b:SetScript("OnEnter", function()
this.cell.background:SetAlpha(1)
this.hover:Show()
showTooltip(this)
end)
b:SetScript("OnLeave", function()
this.cell.background:SetAlpha(G.backplateAlpha)
this.hover:SetAlpha(G.hoverAlpha)
this.hover:Hide()
this.updateTooltip = nil
GameTooltip:Hide()
end)
b:SetScript("OnUpdate", function()
if not this.updateTooltip then
return
end
this.updateTooltip = this.updateTooltip - arg1
if this.updateTooltip > 0 then
return
end
if GameTooltip:IsOwned(this) then
showTooltip(this)
else
this.updateTooltip = nil
end
end)
b:SetScript("OnMouseDown", function() this.hover:SetAlpha(G.pressedAlpha) end)
b:SetScript("OnMouseUp", function() this.hover:SetAlpha(G.hoverAlpha) end)
b:SetScript("OnDragStart", function()
local spell = this.cell.spell
if spell then
PickupSpell(spell.slot, spell.book)
end
end)
b:SetScript("OnClick", function()
local spell = this.cell.spell
if spell then
clickSpell(spell)
end
end)
c.button = b
c:Hide()
return c
end
-- Square frame or round passive, texts, then cooldown and autocast.
local function fillCell(c, spell)
c.spell = spell
local b = c.button
if spell.passive then
SetPortraitToTexture(b.iconName, spell.icon or "")
atlas(b.frame, "talents-node-circle-gray", true)
b.frame:ClearAllPoints()
b.frame:SetAllPoints(b)
atlas(b.hover, "spellbook-item-iconframe-passive-hover", true)
b.shadow:Hide()
else
b.icon:SetTexture(spell.icon)
b.icon:SetTexCoord(0, 1, 0, 1)
atlas(b.frame, "spellbook-item-iconframe-c60", true)
b.frame:ClearAllPoints()
b.frame:SetPoint("TOPLEFT", b, "TOPLEFT", -11, 1)
b.frame:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", 1, -7)
atlas(b.hover, "spellbook-item-iconframe-hover", true)
b.shadow:Show()
end
b.hover:SetAllPoints(b)
c.name:SetText(spell.name)
local subtitle = spell.rank
if subtitle == "" and spell.passive then
subtitle = TEXT.passive
end
c.sub:SetText(subtitle)
-- the text block centered on the cell at (50, -1)
local nameH = c.name:GetHeight() or 0
local subH = (subtitle ~= "" and (c.sub:GetHeight() or 0)) or 0
local total = nameH
if subH > 0 then
total = total + 2 + subH
end
c.name:ClearAllPoints()
c.name:SetPoint("TOPLEFT", c, "LEFT", G.textX, total / 2 - 1)
c.sub:ClearAllPoints()
c.sub:SetPoint("TOPLEFT", c.name, "BOTTOMLEFT", 0, -2)
if subtitle == "" then
c.sub:Hide()
else
c.sub:Show()
end
B.updateState(c)
end
-- Cooldown, autocast and the current cast (IsCurrentCast, 1.12's checked spell button).
function B.updateState(c)
local spell = c.spell
if not spell then
return
end
local b = c.button
local start, duration, enable = GetSpellCooldown(spell.slot, spell.book)
if start then
CooldownFrame_SetTimer(b.cooldown, start, duration, enable)
end
local _, lit = nil, nil
if spell.pet then
_, lit = GetSpellAutocast(spell.slot, spell.book)
end
if lit then
b.auto:Show()
else
b.auto:Hide()
end
if IsCurrentCast(spell.slot, spell.book) then
b.hover:Show()
elseif not MouseIsOver(b) then
b.hover:Hide()
end
end
-- category header (SpellBookHeaderTemplate)
local function createHeader(view)
local h = CreateFrame("Frame", nil, view)
h:SetWidth(G.viewW)
h:SetHeight(G.headerH)
local background = h:CreateTexture(nil, "BACKGROUND")
atlas(background, "spellbook-list-backplate", true)
background:SetWidth(416)
background:SetHeight(106)
background:SetPoint("LEFT", h, "LEFT", -85, 10)
background:SetAlpha(0.65)
local text = h:CreateFontString(nil, "ARTWORK")
font(text, 24)
text:SetJustifyH("LEFT")
text:SetPoint("TOPLEFT", h, "TOPLEFT", -8, 0)
text:SetPoint("BOTTOMRIGHT", h, "BOTTOMRIGHT", -60, 0)
local line = h:CreateTexture(nil, "ARTWORK")
atlas(line, "spellbook-divider", true)
line:SetHeight(11)
line:SetPoint("BOTTOMLEFT", h, "BOTTOMLEFT", -32, 0)
line:SetPoint("BOTTOMRIGHT", h, "BOTTOMRIGHT", -60, 0)
h.text = text
h:Hide()
return h
end
-- ------------------------------------------------------------ Frame
-- Red title bar button. normal, pressed: atlas names
local function redButton(parent, normal, pressed)
local b = CreateFrame("Button", nil, parent)
b:SetWidth(G.redSide)
b:SetHeight(G.redSide)
local e = ForeverUI.AtlasEntry(normal)
b:SetNormalTexture(e and e[1] or "")
atlas(b:GetNormalTexture(), normal, true)
b:SetPushedTexture(e and e[1] or "")
atlas(b:GetPushedTexture(), pressed, true)
b:SetHighlightTexture(e and e[1] or "")
atlas(b:GetHighlightTexture(), "redbutton-highlight", true)
b:GetHighlightTexture():SetBlendMode("ADD")
return b
end
-- The close button is the client's: its OnClick hides its parent, SpellBookFrame.
function B.placeCloseButton()
local close, book = SpellBookCloseButton, B.book
close:SetFrameLevel(book:GetFrameLevel() + G.buttonLevel)
close:SetWidth(G.redSide)
close:SetHeight(G.redSide)
close:ClearAllPoints()
close:SetPoint("TOPRIGHT", book, "TOPRIGHT", G.closeX, G.closeY)
if close.foreverSkinned then
return
end
close:SetHitRectInsets(0, 0, 0, 0)
for _, state in ipairs({ { "Normal", "redbutton-exit" }, { "Pushed", "redbutton-exit-pressed" },
{ "Highlight", "redbutton-highlight" } }) do
local t = close["Get" .. state[1] .. "Texture"](close)
if t then
atlas(t, state[2], true)
t:ClearAllPoints()
t:SetAllPoints(close)
end
end
close.foreverSkinned = true
end
-- Window chrome: rock, metal, portrait, title, the one/two page button.
local function buildFrame(book)
local rock = book:CreateTexture(nil, "BACKGROUND")
rock:SetTexture(FILES.rock)
rock:SetPoint("TOPLEFT", book, "TOPLEFT", 2, -21)
rock:SetPoint("BOTTOMRIGHT", book, "BOTTOMRIGHT", -2, 2)
local metal = CreateFrame("Frame", nil, book)
metal:SetAllPoints(book)
metal:SetFrameLevel(book:GetFrameLevel() + G.metalLevel)
local p = {}
for _, corner in ipairs(METAL) do
local t = metal:CreateTexture(nil, "OVERLAY")
atlas(t, corner.name)
t:SetPoint(corner.point, metal, corner.point, corner.x, corner.y)
p[corner.key] = t
end
local function edge(name, a1, c1, r1, a2, c2, r2)
local t = metal:CreateTexture(nil, "OVERLAY")
atlas(t, name)
t:SetPoint(a1, c1, r1, 0, 0)
t:SetPoint(a2, c2, r2, 0, 0)
end
edge("_ui-frame-metal-edgetop", "TOPLEFT", p.topLeft, "TOPRIGHT", "TOPRIGHT", p.topRight, "TOPLEFT")
edge("_ui-frame-metal-edgebottom", "BOTTOMLEFT", p.bottomLeft, "BOTTOMRIGHT", "BOTTOMRIGHT", p.bottomRight, "BOTTOMLEFT")
edge("!ui-frame-metal-edgeleft", "TOPLEFT", p.topLeft, "BOTTOMLEFT", "BOTTOMLEFT", p.bottomLeft, "TOPLEFT")
edge("!ui-frame-metal-edgeright", "TOPRIGHT", p.topRight, "BOTTOMRIGHT", "BOTTOMRIGHT", p.bottomRight, "TOPRIGHT")
local portraitFrame = CreateFrame("Frame", nil, book)
portraitFrame:SetAllPoints(book)
portraitFrame:SetFrameLevel(book:GetFrameLevel() + G.portraitLevel)
local portrait = portraitFrame:CreateTexture(nil, "OVERLAY")
portrait:SetWidth(G.portraitSide)
portrait:SetHeight(G.portraitSide)
portrait:SetPoint("TOPLEFT", book, "TOPLEFT", G.portraitX, G.portraitY)
portrait:SetTexture(FILES.portrait)
local banner = CreateFrame("Frame", nil, book)
banner:SetFrameLevel(book:GetFrameLevel() + G.titleLevel)
banner:SetHeight(G.titleH)
banner:SetPoint("TOPLEFT", book, "TOPLEFT", G.titleX1, G.titleY)
banner:SetPoint("TOPRIGHT", book, "TOPRIGHT", G.titleX2, G.titleY)
local title = banner:CreateFontString(nil, "OVERLAY", "GameFontNormal")
title:SetPoint("TOP", banner, "TOP", 0, -5)
title:SetText(TEXT.title)
book.title = title
-- maximize / minimize (MaximizeMinimizeButtonFrameTemplate), left of the close button
local size = redButton(book, "redbutton-condense", "redbutton-condense-pressed")
size:SetFrameLevel(book:GetFrameLevel() + G.buttonLevel)
size:SetPoint("TOPRIGHT", book, "TOPRIGHT", G.closeX - G.redSide, G.closeY)
size:SetScript("OnClick", function()
PlaySound("igMainMenuOptionCheckBoxOn")
B.toggleSize()
end)
book.size = size
end
-- ------------------------------------------------------------ Settings
local function createSettings(pages)
local b = CreateFrame("Button", "ForeverUISpellBookSettingsButton", pages)
b:SetWidth(G.settingsW)
b:SetHeight(G.settingsH)
b:SetPoint("TOPRIGHT", pages, "TOPRIGHT", G.settingsX, G.settingsY)
local icon = b:CreateTexture(nil, "ARTWORK")
atlas(icon, "common-dropdown-a-button")
icon:SetPoint("CENTER", b, "CENTER", 0, 0)
local hover = b:CreateTexture(nil, "HIGHLIGHT")
atlas(hover, "common-dropdown-a-button")
hover:SetPoint("CENTER", icon, "CENTER", 0, 0)
hover:SetBlendMode("ADD")
hover:SetAlpha(0.4)
b:SetScript("OnMouseDown", function() icon:SetPoint("CENTER", b, "CENTER", 1, -1) end)
b:SetScript("OnMouseUp", function() icon:SetPoint("CENTER", b, "CENTER", 0, 0) end)
local list = CreateFrame("Frame", "ForeverUISpellBookSettingsList", pages)
list:SetFrameStrata("DIALOG")
list:EnableMouse(true)
list:SetPoint("TOPLEFT", b, "BOTTOMLEFT", 0, 0)
list:Hide()
local slices = ForeverUI.CreateNineSlice(list, "common-dropdown-bg-c60", MENU.backgroundCorner,
MENU.backgroundMargins, "BACKGROUND")
for _, t in ipairs(slices or {}) do
t:SetAlpha(MENU.backgroundAlpha)
end
list.rows = {}
local longest = 0
for i, r in ipairs(SETTINGS) do
local l = CreateFrame("Button", "ForeverUISpellBookSettingsEntry" .. i, list)
l:SetHeight(MENU.rowH)
l:SetPoint("TOPLEFT", list, "TOPLEFT", MENU.rowX, -MENU.edge - (i - 1) * MENU.rowH)
l:SetHighlightTexture(FILES.rowHighlight)
l:GetHighlightTexture():SetBlendMode("ADD")
local text = l:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
text:SetJustifyH("LEFT")
text:SetPoint("LEFT", l, "LEFT", MENU.textX, 0)
text:SetText(r.text)
longest = math.max(longest, text:GetStringWidth() or 0)
local checkbox = l:CreateTexture(nil, "BORDER")
atlas(checkbox, "common-dropdown-ticksquare", true)
checkbox:SetWidth(MENU.checkbox)
checkbox:SetHeight(MENU.checkbox)
checkbox:SetPoint("LEFT", l, "LEFT", 0, 0)
local checkMark = l:CreateTexture(nil, "ARTWORK")
atlas(checkMark, "common-dropdown-icon-checkmark-yellow", true)
checkMark:SetWidth(MENU.checkMarkW)
checkMark:SetHeight(MENU.checkMarkH)
checkMark:SetPoint("CENTER", checkbox, "CENTER", MENU.checkMarkX, MENU.checkMarkY)
l.checkMark, l.setting = checkMark, r
-- keepShownOnClick: the list stays open
l:SetScript("OnClick", function()
PlaySound("UChatScrollButton")
local s = settings()
s[this.setting.key] = not s[this.setting.key]
B.update()
end)
list.rows[i] = l
end
local width = longest + MENU.extraWidth + MENU.margin
list:SetWidth(width)
list:SetHeight(table.getn(SETTINGS) * MENU.rowH + 2 * MENU.edge)
for _, l in ipairs(list.rows) do
l:SetWidth(width - MENU.margin)
end
-- UIDROPDOWNMENU_SHOW_TIME: hidden 2 s after the mouse leaves the list and its arrow
list:SetScript("OnShow", function() this.away = 0 end)
list:SetScript("OnUpdate", function()
if MouseIsOver(this) or MouseIsOver(b) then
this.away = 0
return
end
this.away = (this.away or 0) + arg1
if this.away > MENU.pending then
this:Hide()
end
end)
b:SetScript("OnClick", function()
if list:IsShown() then
list:Hide()
else
list:Show()
end
end)
B.settingsList = list
end
local function checkSettings()
local s = settings()
for _, l in ipairs(B.settingsList.rows) do
if s[l.setting.key] then
l.checkMark:Show()
else
l.checkMark:Hide()
end
end
end
-- ------------------------------------------------------------ Tabs
local function createTab(parent, n)
local b = CreateFrame("Button", "ForeverUISpellBookTab" .. n, parent)
b:SetID(n)
b:SetWidth(G.tabW)
b:SetHeight(G.tabH)
local icon = b:CreateTexture(nil, "ARTWORK")
icon:SetWidth(G.tabIconW)
icon:SetHeight(G.tabIconH)
icon:SetPoint("CENTER", b, "CENTER", -1, -1)
-- the frame above the icon (camelot: ARTWORK sublevel 1): a child frame
local over = CreateFrame("Frame", nil, b)
over:SetAllPoints(b)
over:SetFrameLevel(b:GetFrameLevel() + 1)
local frame = over:CreateTexture(nil, "ARTWORK")
atlas(frame, "spellbook-tab-frame-c60")
frame:SetPoint("BOTTOM", b, "BOTTOM", 0, 1)
local active = over:CreateTexture(nil, "ARTWORK")
atlas(active, "spellbook-tab-frame-glow-c60")
active:SetPoint("BOTTOM", b, "BOTTOM", 0, 1)
local glow = over:CreateTexture(nil, "OVERLAY")
atlas(glow, "spellbook-tab-frame-glow-gradient-c60")
glow:SetPoint("BOTTOM", b, "BOTTOM", 0, 0)
b.icon, b.frame, b.active, b.glow = icon, frame, active, glow
b:SetScript("OnClick", function()
PlaySound("igSpellBookOpen")
B.choose(this:GetID())
end)
b:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_RIGHT", -12, -6)
GameTooltip:SetText(this.name)
GameTooltip:Show()
end)
b:SetScript("OnLeave", function() GameTooltip:Hide() end)
return b
end
local function layoutTabs(cats, selected)
local tabs = B.tabs
for i, cat in ipairs(cats) do
local b = tabs.buttons[i]
if not b then
b = createTab(tabs, i)
tabs.buttons[i] = b
end
b.name = cat.name
b.icon:SetTexture(cat.icon)
b:ClearAllPoints()
b:SetPoint("TOPLEFT", tabs, "TOPLEFT", (i - 1) * (G.tabW + G.tabGap), 0)
b:Show()
-- LEARNED_SPELL_IN_TAB's glow (SpellBookSkillLineTab<i>Flash, 64 on a 32 tab) on our tab
local flash = getglobal("SpellBookSkillLineTab" .. i .. "Flash")
if flash and not cat.pet then
flash:ClearAllPoints()
flash:SetPoint("CENTER", b, "CENTER", 0, 0)
flash:SetWidth(G.tabW * 2)
flash:SetHeight(G.tabH * 2)
end
if i == selected then
b.frame:Hide()
b.active:Show()
b.glow:Show()
else
b.frame:Show()
b.active:Hide()
b.glow:Hide()
end
end
for i = table.getn(cats) + 1, table.getn(tabs.buttons) do
tabs.buttons[i]:Hide()
end
SpellBookTabFlashFrame:SetFrameLevel(tabs:GetFrameLevel() + 3)
end
-- ------------------------------------------------------------ Build
local function build()
SpellBookFrame:EnableMouse(false)
local book = CreateFrame("Frame", "ForeverUISpellBookFrame", SpellBookFrame)
book:SetHeight(G.height)
book:SetWidth(G.width)
book:EnableMouse(true)
B.book = book
buildFrame(book)
local pages = CreateFrame("Frame", "ForeverUISpellBookPages", book)
pages:SetPoint("BOTTOMLEFT", book, "BOTTOMLEFT", 0, G.bookBottom)
pages:SetHeight(G.bookH)
pages:SetWidth(G.bookW)
local left = pages:CreateTexture(nil, "BACKGROUND")
atlas(left, "spellbook-page-left-c60-2x", true)
left:SetPoint("TOPLEFT", pages, "TOPLEFT", 0, 0)
left:SetPoint("BOTTOMRIGHT", pages, "BOTTOM", 0, 0)
local right = pages:CreateTexture(nil, "BACKGROUND")
atlas(right, "spellbook-page-right-c60-2x", true)
right:SetPoint("TOPLEFT", pages, "TOP", 0, 0)
right:SetPoint("BOTTOMRIGHT", pages, "BOTTOMRIGHT", 0, 0)
local single = pages:CreateTexture(nil, "BACKGROUND")
atlas(single, "spellbook-page-right-c60-2x", true)
single:SetAllPoints(pages)
single:Hide()
pages.left, pages.right, pages.single = left, right, single
B.pages = pages
local tabs = CreateFrame("Frame", nil, pages)
tabs:SetPoint("TOPLEFT", pages, "TOPLEFT", G.tabsX, G.tabsY)
tabs:SetWidth(1)
tabs:SetHeight(G.tabH)
tabs.buttons = {}
B.tabs = tabs
local content = CreateFrame("Frame", "ForeverUISpellBookContent", pages)
content:SetPoint("TOPLEFT", pages, "TOPLEFT", 0, G.pagesTop)
content:SetPoint("BOTTOMRIGHT", pages, "BOTTOMRIGHT", 0, 0)
content:EnableMouseWheel(true)
content:SetScript("OnMouseWheel", function() B.rotate(-arg1) end)
B.views, B.cells = {}, {}
for v = 1, 2 do
local view = CreateFrame("Frame", "ForeverUISpellBookView" .. v, content)
view:SetWidth(G.viewW)
view:SetHeight(G.viewH)
if v == 1 then
view:SetPoint("TOPLEFT", content, "TOPLEFT", G.view1X, G.viewY)
else
view:SetPoint("TOPRIGHT", content, "TOPRIGHT", G.view2X, G.viewY)
end
view.cells = {}
for n = 1, G.cellsPerView do
local k = (v - 1) * G.cellsPerView + n
view.cells[n] = createCell(view, k)
B.cells[k] = view.cells[n]
end
view.header = createHeader(view)
B.views[v] = view
end
-- paging: "Page n/m", previous, next, right to left
local nextPage = CreateFrame("Button", "ForeverUISpellBookNextPage", content)
local prev = CreateFrame("Button", "ForeverUISpellBookPrevPage", content)
for _, f in ipairs({ { nextPage, "NextPage", 1 }, { prev, "PrevPage", -1 } }) do
local b = f[1]
b:SetWidth(G.arrowSide)
b:SetHeight(G.arrowSide)
b:SetNormalTexture(FILES.arrows .. f[2] .. "-Up")
b:SetPushedTexture(FILES.arrows .. f[2] .. "-Down")
b:SetDisabledTexture(FILES.arrows .. f[2] .. "-Disabled")
b:SetHighlightTexture(FILES.squareHover)
b:GetHighlightTexture():SetBlendMode("ADD")
b.direction = f[3]
b:SetScript("OnClick", function()
PlaySound("igAbiliityPageTurn")
B.rotate(this.direction)
end)
end
nextPage:SetPoint("BOTTOMRIGHT", content, "BOTTOMRIGHT", G.pagerX, G.pagerY)
prev:SetPoint("RIGHT", nextPage, "LEFT", -G.pagerGap, 0)
local text = content:CreateFontString(nil, "ARTWORK")
font(text, 14)
text:SetPoint("RIGHT", prev, "LEFT", -G.pagerGap, 0)
B.pager = { nextPage = nextPage, prev = prev, text = text }
createSettings(pages)
end
-- Hides 1.12's regions and frames in SpellBookFrame, except the book, the close button and
-- the tabs' flash, which UIFrameFlash shows on our tabs.
function B.suppressClient()
for _, region in ipairs({ SpellBookFrame:GetRegions() }) do
suppress(region)
end
for _, child in ipairs({ SpellBookFrame:GetChildren() }) do
if child ~= B.book and child ~= SpellBookCloseButton and child ~= SpellBookTabFlashFrame then
suppress(child)
end
end
end
-- camelot's fit (blizzard_playerspellsregistration): SpellBookFrame scales down until the
-- book and its margins fit the screen; the book's top at -116, raised while its bottom would
-- come within 140 of the screen's, up to 10 below its top.
function B.fit()
local book = B.book
local k = math.min(1, UIParent:GetWidth() / (G.width + G.fitExtraW),
UIParent:GetHeight() / (G.height + G.fitExtraH))
if math.abs(SpellBookFrame:GetScale() - k) > 0.001 then
SpellBookFrame:SetScale(k)
end
local y = G.top
local bottom = UIParent:GetHeight() + y - G.height * k
if bottom < G.clampBottom then
y = y + G.clampBottom - bottom
end
y = math.min(y, G.clampTop)
book:ClearAllPoints()
book:SetPoint("TOP", UIParent, "TOP", 0, y / k)
end
-- ------------------------------------------------------------ Filling
B.state = { category = 1, page = 1 }
function B.choose(index)
local cats = B.categories()
local cat = cats[index]
if not cat then
return
end
B.state.category, B.state.page = index, 1
-- so P and Shift-P toggle as 1.12's book does
SpellBookFrame.bookType = cat.book
-- SpellBookSkillLineTab_OnClick: a chosen tab stops flashing
local flash = not cat.pet and getglobal("SpellBookSkillLineTab" .. index .. "Flash")
if flash then
flash:Hide()
end
B.update()
end
function B.rotate(direction)
local p = B.state.page + direction
if p >= 1 and p <= (B.state.pages or 1) then
B.state.page = p
B.update()
end
end
function B.toggleSize()
local s = settings()
if s.reduced then
s.reduced = nil
B.state.page = math.ceil(B.state.page / 2)
else
s.reduced = true
B.state.page = B.state.page * 2 - 1
end
B.update()
end
-- Two pages or one: widths, page art, the right view and the size button.
local function applySize(reduced)
local book, pages = B.book, B.pages
if reduced then
book:SetWidth(G.compactWidth)
pages:SetWidth(G.reducedBookW)
pages.left:Hide()
pages.right:Hide()
pages.single:Show()
B.views[2]:Hide()
atlas(book.size:GetNormalTexture(), "redbutton-expand", true)
atlas(book.size:GetPushedTexture(), "redbutton-expand-pressed", true)
else
book:SetWidth(G.width)
pages:SetWidth(G.bookW)
pages.left:Show()
pages.right:Show()
pages.single:Hide()
B.views[2]:Show()
atlas(book.size:GetNormalTexture(), "redbutton-condense", true)
atlas(book.size:GetPushedTexture(), "redbutton-condense-pressed", true)
end
end
function B.update()
if not SpellBookFrame:IsVisible() then
return
end
local e = B.state
local cats = B.categories()
if e.category > table.getn(cats) then
e.category, e.page = 1, 1
end
local reduced = settings().reduced and true or false
local perPage = reduced and 1 or 2
applySize(reduced)
checkSettings()
layoutTabs(cats, e.category)
local cat = cats[e.category]
local views = cat and B.paginate(cat.name, B.spellsOf(cat)) or {}
e.pages = math.max(1, math.ceil(table.getn(views) / perPage))
if e.page > e.pages then
e.page = e.pages
end
for slot, view in ipairs(B.views) do
local list = views[(e.page - 1) * perPage + slot] or {}
local n = 0
view.header:Hide()
for _, el in ipairs(list) do
if el.header then
view.header.text:SetText(el.header)
view.header:ClearAllPoints()
view.header:SetPoint("TOPLEFT", view, "TOPLEFT", 0, -el.y)
view.header:Show()
else
n = n + 1
local c = view.cells[n]
if c then
c:ClearAllPoints()
c:SetPoint("TOPLEFT", view, "TOPLEFT", (el.column - 1) * (G.cellW + G.gapX), -el.y)
fillCell(c, el.spell)
c:Show()
end
end
end
for i = n + 1, G.cellsPerView do
view.cells[i].spell = nil
view.cells[i]:Hide()
end
end
if e.page > 1 then B.pager.prev:Enable() else B.pager.prev:Disable() end
if e.page < e.pages then B.pager.nextPage:Enable() else B.pager.nextPage:Disable() end
B.pager.text:SetText(string.format(TEXT.page, e.page, e.pages))
end
-- Cooldowns, autocast and the current cast of the shown cells.
function B.updateStates()
for _, c in ipairs(B.cells) do
if c:IsShown() and c.spell then
B.updateState(c)
end
end
end
-- ------------------------------------------------------------ Assembly
build()
-- On each show: the requested book (ToggleSpellBook sets bookType: the pet's or a spell tab).
ForeverUI.HookScript(SpellBookFrame, "OnShow", function()
B.suppressClient()
B.fit()
B.placeCloseButton()
local cats = B.categories()
local current = cats[B.state.category]
if SpellBookFrame.bookType == BOOKTYPE_PET then
for i, cat in ipairs(cats) do
if cat.pet then
B.state.category, B.state.page = i, 1
end
end
elseif current and current.pet then
B.state.category, B.state.page = 1, 1
end
B.update()
end)
ForeverUI.HookScript(SpellBookFrame, "OnHide", function()
B.settingsList:Hide()
end)
ForeverUI.HookGlobal("SpellBookFrame_Update", B.suppressClient)
local watcher = CreateFrame("Frame")
for _, name in ipairs({ "SPELLS_CHANGED", "LEARNED_SPELL_IN_TAB", "PET_BAR_UPDATE", "SPELL_UPDATE_COOLDOWN",
"CURRENT_SPELL_CAST_CHANGED", "TRADE_SKILL_SHOW", "TRADE_SKILL_CLOSE", "CRAFT_SHOW", "CRAFT_CLOSE" }) do
watcher:RegisterEvent(name)
end
watcher:SetScript("OnEvent", function()
if not SpellBookFrame:IsVisible() then
return
end
if event == "SPELLS_CHANGED" or event == "LEARNED_SPELL_IN_TAB" then
B.update()
else
B.updateStates()
end
end)