7d3ea1ac89
Sans aucun groupe (guerrier, voleur, prêtre, chaman, chevalier de la mort, bas niveaux), le menu volant n'avait aucun fils protégé : sa poignée était refusée en combat (« Invalid frame handle »), et chaque onglet, page, réglage ou fermeture du grimoire levait une erreur. Il est désormais explicitement protégé (SecureFrameTemplate). En combat, l'ouverture du grimoire masquait depuis l'addon SpellButton1..12, que le client vient de réafficher et qui sont protégés : action bloquée à chaque ouverture. Ils ne sont plus que rendus transparents en combat. Le bouton de taille, sécurisé, était ancré au bouton de fermeture du client, qui devenait protégé alors que son niveau est reposé à chaque ouverture : il s'ancre au livre, au même endroit. L'environnement restreint garde chaque extrait exécuté, compilé, toute la session (clés chaînes jamais collectées) : les données publiées, plusieurs centaines de kilo-octets, ne partent plus que si elles changent, l'état dans un petit extrait à part. UNIT_PET des autres membres du groupe ne recalcule plus le grimoire. Métiers : un enchantement n'a pas de compteur, et le compteur caché gardait le nombre de la recette précédente (0 si elle n'était pas faisable) ; la création en lance un, comme le champ caché de 3.3.5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KxRZNHkq453dyUMcWsyUf1
1771 lines
68 KiB
Lua
1771 lines
68 KiB
Lua
-- ForeverUI: camelot's spellbook (blizzard_playerspells), drawn inside WotLK's SpellBookFrame.
|
|
-- SpellBookFrame keeps its open/close paths (ToggleSpellBook, P key, micro button) and its close
|
|
-- button, re-skinned, so closing never goes through our code. Its art and its twelve spell
|
|
-- buttons are hidden.
|
|
--
|
|
-- Casting is protected, and WotLK's buttons cast from their book's internal state, so each cell
|
|
-- is a secure button (SecureActionButtonTemplate, type "spell"). Out of combat, every category,
|
|
-- page and group is computed and published to a secure controller
|
|
-- (SecureHandlerAttributeTemplate). Tabs, arrows, wheel, groups, settings and resize only send
|
|
-- it requests; its "show" snippet places the cells, then CallMethod runs ordinary code for
|
|
-- textures and texts, which the client allows in combat. The same path runs out of combat.
|
|
-- Only the computation waits for the end of combat (a spell learned or a pet summoned in it).
|
|
--
|
|
-- Differences with camelot:
|
|
-- * 3.3.5 has no texture masks: square icons keep their corners; passive icons are rounded
|
|
-- by SetPortraitToTexture.
|
|
-- * 3.3.5 has no upcoming spells: nothing grayed or "available at level n".
|
|
-- * pet spells cast by name; right click toggles autocast (/petautocasttoggle).
|
|
-- * WotLK's panel keeps its "left" UI panel area; the book sits where camelot puts a
|
|
-- "center" panel, without pushing the others.
|
|
|
|
ForeverUI = ForeverUI or {}
|
|
|
|
local S = {}
|
|
ForeverUI.SpellBook = S
|
|
|
|
-- Layout values from blizzard_playerspells: PlayerSpellsFrame, SpellBookFrame,
|
|
-- CategoryTabSystem, PagedSpellsFrame (PagedCondensedVerticalGrid), PagingControls.
|
|
local G = {
|
|
width = 1618, compactWidth = 809, height = 720, top = -116,
|
|
-- margins of the fit to the screen (blizzard_playerspellsregistration: checkFitExtraWidth,
|
|
-- checkFitExtraHeight)
|
|
fitExtraW = 200, fitExtraH = 140,
|
|
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,
|
|
-- flyout (SpellBookItemButtonMixin, FlyoutButtonTemplate, SpellFlyout): opens to the RIGHT,
|
|
-- offset -4, 42 high; arrow 15 x 6, 4 from the edge closed, 2 open; small buttons of 30,
|
|
-- first at 9, gap 4, 9 after the last; border tinted 0.7
|
|
flyoutOffset = -4, flyoutH = 42, arrowW = 15, arrowH = 6,
|
|
arrowClosed = 4, arrowOpen = 2, smallSide = 30, flyoutStart = 9,
|
|
flyoutGap = 4, flyoutEnd = 9, flyoutTint = 0.7,
|
|
smallFrame = 35, smallStateW = 31.6, smallStateH = 30.9,
|
|
}
|
|
G.cellW = (G.viewW - G.gapX * (G.columns - 1)) / G.columns
|
|
|
|
-- metal corners: PortraitFrameTemplate as camelot adjusts it (top-right and bottom-right
|
|
-- x - 2, bottom y = -8)
|
|
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 FONT = "Fonts\\FRIZQT__.TTF"
|
|
local L = ForeverUI.L
|
|
local TEXT = {
|
|
title = SPELLBOOK,
|
|
page = L.SPELLBOOK_PAGE, -- PAGE_NUMBER_WITH_MAX
|
|
passive = SPELL_PASSIVE,
|
|
pet = PET,
|
|
-- settings: camelot's SPELLBOOK_FILTER_PASSIVES and SHOW_ALL_SPELL_RANKS
|
|
hidePassives = L.SPELLBOOK_HIDE_PASSIVES,
|
|
allRanks = SHOW_ALL_SPELL_RANKS,
|
|
flyouts = L.SPELLBOOK_USE_FLYOUTS,
|
|
passivesDisabled = L.SPELLBOOK_HIDE_PASSIVES_DISABLED, -- SPELLBOOK_SEARCH_HIDE_PASSIVES_DISABLED
|
|
}
|
|
|
|
-- Settings menu, made of secure buttons. It copies the client list as DropDown.lua skins it
|
|
-- (3.3.5 UIDropDownMenu.lua, UIDropDownMenuTemplates.xml): DIALOG strata, TOPLEFT on the
|
|
-- arrow's BOTTOMLEFT; rows of 20 (ForeverUI's UIDROPDOWNMENU_BUTTON_HEIGHT) at
|
|
-- x = 5 + 12 - 6 (checkbox, MENU mode) and y = -15 - (n - 1) x 20; list n x 20 + 2 x 15 high
|
|
-- and longest text + 40 + 25 wide, rows 25 narrower; text 20 from the edge; checkbox
|
|
-- common-dropdown-ticksquare 12, yellow check 15 x 14 at (2, 1); background
|
|
-- common-dropdown-bg-c60 nine-slice (18; 9, 6, 9, 12) at 0.925. It closes 2 s after the
|
|
-- mouse leaves (UIDROPDOWNMENU_SHOW_TIME), or by the arrow.
|
|
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,
|
|
}
|
|
-- settings and their bit in the settings combination (reg); flyouts is checked when its bit
|
|
-- is ABSENT (the bit means "no groups")
|
|
local SETTINGS = {
|
|
{ bit = 1, text = TEXT.hidePassives },
|
|
{ bit = 2, text = TEXT.flyouts, inverse = true },
|
|
{ bit = 4, text = TEXT.allRanks },
|
|
}
|
|
local SEP = string.char(92)
|
|
local ROCK = "interface" .. SEP .. "ForeverUI" .. SEP .. "framegeneral" .. SEP .. "ui-background-rock"
|
|
local ARROWS = "Interface" .. SEP .. "Buttons" .. SEP .. "UI-SpellbookIcon-"
|
|
local SQUARE_HOVER = "Interface" .. SEP .. "Buttons" .. SEP .. "UI-Common-MouseHilight"
|
|
local CLASS_ICON = "Interface" .. SEP .. "ForeverUI" .. SEP .. "icons" .. SEP .. "classicon_"
|
|
-- portrait: the icon of camelot's General line (inv_misc_book_09), with its round mask baked
|
|
-- in (tools/bake_masks.py)
|
|
local PORTRAIT = "Interface" .. SEP .. "ForeverUI" .. SEP .. "spellbook" .. SEP .. "portrait"
|
|
|
|
local function settings()
|
|
ForeverUIDB = ForeverUIDB or {}
|
|
ForeverUIDB.spellbook = ForeverUIDB.spellbook or {}
|
|
return ForeverUIDB.spellbook
|
|
end
|
|
|
|
local function font(fs, size, c)
|
|
fs:SetFont(FONT, size)
|
|
c = c or INK
|
|
fs:SetTextColor(c[1], c[2], c[3])
|
|
end
|
|
|
|
-- Hide a WotLK region or frame for good: texture, alpha, visibility and mouse. WotLK's code
|
|
-- shows again what is only hidden.
|
|
local function suppress(f)
|
|
if not f then return end
|
|
if f.SetTexture then f:SetTexture(nil) end
|
|
f:SetAlpha(0)
|
|
-- WotLK's SpellButton1..12 are protected (SpellButtonTemplate inherits SecureFrameTemplate)
|
|
-- and SpellBookFrame_Update shows them again: in combat they only turn transparent (their
|
|
-- mouse is already off since the calls made out of combat)
|
|
if InCombatLockdown() and f.IsProtected and f:IsProtected() then return end
|
|
if f.EnableMouse then f:EnableMouse(false) end
|
|
f:Hide()
|
|
end
|
|
|
|
-- ---------------------------------------------------------------- Data
|
|
-- camelot's categories: one per spell tab (General, then one per talent tree), plus the pet.
|
|
-- General shows the class icon.
|
|
local function categories()
|
|
local list = {}
|
|
for i = 1, GetNumSpellTabs() do
|
|
local name, icon, offset, count = GetSpellTabInfo(i)
|
|
if name then
|
|
if i == 1 then
|
|
local _, className = UnitClass("player")
|
|
icon = CLASS_ICON .. string.lower(className or "warrior")
|
|
end
|
|
table.insert(list, { name = name, icon = icon, offset = offset, count = count, book = BOOKTYPE_SPELL })
|
|
end
|
|
end
|
|
local numPetSpells = HasPetSpells()
|
|
if numPetSpells and numPetSpells > 0 then
|
|
table.insert(list, {
|
|
name = TEXT.pet, icon = GetPetIcon and GetPetIcon() or nil,
|
|
offset = 0, count = numPetSpells, book = BOOKTYPE_PET, pet = true,
|
|
})
|
|
end
|
|
return list
|
|
end
|
|
S.categories = categories
|
|
S.G = G
|
|
S.font = font
|
|
|
|
-- Spell options: passives = hide passives, flyouts = no flyout groups, ranks = all ranks.
|
|
-- Defaults: the saved settings and the ShowAllSpellRanks CVar.
|
|
local function currentOptions()
|
|
return { passives = settings().hidePassives and true or false,
|
|
flyouts = settings().noFlyouts and true or false,
|
|
ranks = GetCVar("ShowAllSpellRanks") == "1" }
|
|
end
|
|
|
|
-- Spells of a category. Without all ranks, a spell keeps only its highest rank (the last of
|
|
-- the line). Hide passives (camelot's spellBookHidePassives, missing in 3.3.5, is kept in
|
|
-- ForeverUIDB) removes passives. opts: optional, defaults to currentOptions()
|
|
local function spellsOf(cat, opts)
|
|
opts = opts or currentOptions()
|
|
local spells, last = {}, {}
|
|
local allRanks = opts.ranks
|
|
local noPassives = opts.passives
|
|
for slot = cat.offset + 1, cat.offset + cat.count do
|
|
local name, rank = GetSpellName(slot, cat.book)
|
|
local passive = name and IsPassiveSpell(slot, cat.book) and true or false
|
|
if name and not (noPassives 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 allRanks and last[name] then
|
|
spells[last[name]] = spell
|
|
else
|
|
table.insert(spells, spell)
|
|
last[name] = #spells
|
|
end
|
|
end
|
|
end
|
|
return spells
|
|
end
|
|
S.spellsOf = spellsOf
|
|
|
|
-- ------------------------------------------------------------ Flyouts
|
|
-- "Group Similar Spells on Flyouts" (SPELLBOOK_USE_FLYOUTS). camelot's server puts a flyout
|
|
-- entry in the book and removes its spells (IsSpellBookItemLooseFlyoutMember). 3.3.5 has
|
|
-- neither, so the groups are camelot's, from SpellFlyout.db2 and SpellFlyoutItem.db2 (name,
|
|
-- description, icon, spells in slot order), minus the two empty ones (263, 265) and the two
|
|
-- with only camelot spells (252 Spell Resistances, 257 Weapon Proficiencies). Each camelot
|
|
-- list is followed by the class spells TBC and WotLK add to that family (Spell.dbc,
|
|
-- SkillLineAbility.dbc).
|
|
-- A spell joins a group BY NAME (GetSpellInfo(id)), so the ranks 3.3.5 adds follow the
|
|
-- original one. camelot-only ids (above one million) do not exist here and are ignored.
|
|
local FLYOUTS = {
|
|
-- Portals and teleports: one group per faction (SpellFlyout race mask). After camelot's
|
|
-- list, the TBC and WotLK cities: Exodar / Silvermoon, Shattrath (one spell per faction),
|
|
-- Theramore / Stonard, Dalaran (the same spell for both).
|
|
{ id = 248, name = L.SPELLBOOK_FLYOUT_PORTAL, icon = "spell_arcane_portalstormwind", faction = "Alliance",
|
|
text = L.SPELLBOOK_FLYOUT_PORTAL_DESC,
|
|
spells = { 11419, 11416, 10059, 32266, 33691, 49360, 53142 } },
|
|
{ id = 249, name = L.SPELLBOOK_FLYOUT_PORTAL, icon = "spell_arcane_portalorgrimmar", faction = "Horde",
|
|
text = L.SPELLBOOK_FLYOUT_PORTAL_DESC,
|
|
spells = { 11417, 11420, 11418, 32267, 35717, 49361, 53142 } },
|
|
{ id = 250, name = L.SPELLBOOK_FLYOUT_TELEPORT, icon = "spell_arcane_teleportstormwind", faction = "Alliance",
|
|
text = L.SPELLBOOK_FLYOUT_TELEPORT_DESC,
|
|
spells = { 3565, 3562, 3561, 1297659, 32271, 33690, 49359, 53140 } },
|
|
{ id = 251, name = L.SPELLBOOK_FLYOUT_TELEPORT, icon = "spell_arcane_teleportorgrimmar", faction = "Horde",
|
|
text = L.SPELLBOOK_FLYOUT_TELEPORT_DESC,
|
|
spells = { 3567, 3566, 3563, 1297659, 32272, 35715, 49358, 53140 } },
|
|
{ id = 253, name = L.SPELLBOOK_FLYOUT_ASPECT, icon = "ability_hunter_aspectmastery",
|
|
text = L.SPELLBOOK_FLYOUT_ASPECT_DESC,
|
|
spells = { 13163, 13165, 14318, 14319, 14320, 14321, 14322, 25296, 5118, 13161,
|
|
1299445, 1299446, 1299447, 13159, 20043, 20190,
|
|
34074, 61846 } }, -- + Viper, Dragonhawk
|
|
{ id = 259, name = L.SPELLBOOK_FLYOUT_SUMMON_DEMON, icon = "spell_shadow_summonimp",
|
|
text = L.SPELLBOOK_FLYOUT_SUMMON_DEMON_DESC,
|
|
spells = { 688, 697, 712, 713, 691,
|
|
30146 } }, -- + Felguard
|
|
{ id = 261, name = L.SPELLBOOK_FLYOUT_IMP_SPELLS, icon = "spell_fire_firebolt",
|
|
text = L.SPELLBOOK_FLYOUT_IMP_SPELLS_DESC,
|
|
spells = { 20801, 6307, 2949, 4511 } },
|
|
{ id = 262, name = L.SPELLBOOK_FLYOUT_SHAPESHIFT, icon = "ability_racial_bearform",
|
|
text = L.SPELLBOOK_FLYOUT_SHAPESHIFT_DESC,
|
|
spells = { 5487, 9634, 1066, 768, 783, 24858,
|
|
33943, 40120, 33891 } }, -- + Flight, Swift Flight, Tree of Life
|
|
{ id = 264, name = L.SPELLBOOK_FLYOUT_BLESSINGS, icon = "spell_magic_magearmor",
|
|
text = L.SPELLBOOK_FLYOUT_BLESSINGS_DESC,
|
|
-- 1038 (camelot's Blessing of Salvation) is Hand of Salvation here: it goes with the other
|
|
-- Hands, in Utility Blessings
|
|
spells = { 20217, 25291, 19838, 19837, 19836, 19835, 19834, 19740, 25290,
|
|
19854, 19853, 19852, 19850, 19742, 19979, 19978, 19977,
|
|
20911 } }, -- + Sanctuary
|
|
{ id = 267, name = L.SPELLBOOK_FLYOUT_PET_UTILITIES, icon = "ability_hunter_beasttaming",
|
|
text = L.SPELLBOOK_FLYOUT_PET_UTILITIES_DESC,
|
|
spells = { 883, 982, 6991, 2641, 1515, 1462, 5149 } },
|
|
{ id = 268, name = L.SPELLBOOK_FLYOUT_TRACKING, icon = "ability_tracking",
|
|
text = L.SPELLBOOK_FLYOUT_TRACKING_DESC,
|
|
spells = { 1494, 19878, 19879, 19880, 19882, 19885, 19883, 19884 } },
|
|
{ id = 269, name = L.SPELLBOOK_FLYOUT_STANCES, icon = "ability_warrior_offensivestance",
|
|
text = L.SPELLBOOK_FLYOUT_STANCES_DESC,
|
|
spells = { 2457, 71, 2458 } },
|
|
{ id = 270, name = AURAS, icon = "spell_holy_devotionaura",
|
|
text = L.SPELLBOOK_FLYOUT_AURAS_DESC,
|
|
spells = { 10301, 10300, 10299, 10298, 7294, 10293, 10292, 1032, 10291, 643,
|
|
10290, 465, 19746, 20218, 19900, 19899, 19891, 19898, 19897, 19888,
|
|
19896, 19895, 19876,
|
|
32223 } }, -- + Crusader Aura
|
|
{ id = 272, name = L.SPELLBOOK_FLYOUT_GREATER_BLESSINGS, icon = "spell_holy_greaterblessingofkings",
|
|
text = L.SPELLBOOK_FLYOUT_GREATER_BLESSINGS_DESC,
|
|
spells = { 25895, 25898, 25916, 25782, 25918, 25894, 25890,
|
|
25899 } }, -- + Greater Sanctuary
|
|
{ id = 273, name = L.SPELLBOOK_FLYOUT_UTILITY_BLESSINGS, icon = "spell_holy_sealofvalor",
|
|
text = L.SPELLBOOK_FLYOUT_UTILITY_BLESSINGS_DESC,
|
|
spells = { 10278, 5599, 1022, 1044, 20729, 6940,
|
|
1038 } }, -- + Hand of Salvation
|
|
-- Two groups camelot does not have: paladin seals and judgements. No id, description or
|
|
-- icon: the group uses its first spell's icon. Spell ids from Spell.dbc and
|
|
-- SkillLineAbility.dbc.
|
|
{ name = L.SPELLBOOK_FLYOUT_SEALS,
|
|
spells = { 21084, 20164, 20165, 20166, 20375, 31801, 53736 } },
|
|
{ name = L.SPELLBOOK_FLYOUT_JUDGEMENTS,
|
|
spells = { 20271, 53408, 53407 } },
|
|
}
|
|
S.FLYOUTS = FLYOUTS
|
|
|
|
-- Group of a spell name: { flyout, order of the name in the group }. byName is built once;
|
|
-- a name can be in two groups (Dalaran, in both factions'), the player's faction wins.
|
|
local byName
|
|
local function flyoutOf(name)
|
|
if not byName then
|
|
byName = {}
|
|
for _, v in ipairs(FLYOUTS) do
|
|
local order, seen = 0, {}
|
|
for _, id in ipairs(v.spells) do
|
|
local n = GetSpellInfo(id)
|
|
if n and not seen[n] then
|
|
seen[n] = true
|
|
order = order + 1
|
|
byName[n] = byName[n] or {}
|
|
table.insert(byName[n], { flyout = v, order = order })
|
|
end
|
|
end
|
|
end
|
|
end
|
|
local faction = UnitFactionGroup("player")
|
|
for _, m in ipairs(byName[name] or {}) do
|
|
if not m.flyout.faction or m.flyout.faction == faction then
|
|
return m
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Groups of ONE category: a group only joins spells of the same tab (warrior stances, each
|
|
-- in its own tree, stay alone; mage portals and teleports, all in one tab, are grouped).
|
|
-- It needs at least two DIFFERENT spells: two ranks of one spell are not a group. Its spells
|
|
-- follow the group order.
|
|
local function groupsOf(spells)
|
|
local groups = {}
|
|
for _, spell in ipairs(spells) do
|
|
local m = flyoutOf(spell.name)
|
|
if m then
|
|
local g = groups[m.flyout]
|
|
if not g then
|
|
g = { members = {}, names = {}, distinctCount = 0 }
|
|
groups[m.flyout] = g
|
|
end
|
|
spell.order = m.order
|
|
table.insert(g.members, spell)
|
|
if not g.names[spell.name] then
|
|
g.names[spell.name] = true
|
|
g.distinctCount = g.distinctCount + 1
|
|
end
|
|
end
|
|
end
|
|
for v, g in pairs(groups) do
|
|
if g.distinctCount < 2 then
|
|
groups[v] = nil
|
|
else
|
|
table.sort(g.members, function(a, b)
|
|
if a.order ~= b.order then return a.order < b.order end
|
|
return a.slot < b.slot
|
|
end)
|
|
end
|
|
end
|
|
return groups
|
|
end
|
|
|
|
-- What a category shows: its spells and, when grouping, each group in place of its first
|
|
-- spell, its members removed. Pet spells are never grouped.
|
|
-- cats: categories(); index: category index; opts: see currentOptions
|
|
local function displayedList(cats, index, opts)
|
|
opts = opts or currentOptions()
|
|
local cat = cats[index]
|
|
if not cat then return {} end
|
|
local spells = spellsOf(cat, opts)
|
|
if cat.pet or opts.flyouts then
|
|
return spells
|
|
end
|
|
local groups = groupsOf(spells)
|
|
local list, placed = {}, {}
|
|
for _, spell in ipairs(spells) do
|
|
local m = flyoutOf(spell.name)
|
|
local g = m and groups[m.flyout]
|
|
if not g then
|
|
table.insert(list, spell)
|
|
elseif not placed[m.flyout] then
|
|
placed[m.flyout] = true
|
|
local v = m.flyout
|
|
local icon = v.icon and ("Interface" .. SEP .. "Icons" .. SEP .. v.icon) or g.members[1].icon
|
|
table.insert(list, { flyout = v, name = v.name, rank = "", passive = false,
|
|
icon = icon, members = g.members })
|
|
end
|
|
end
|
|
return list
|
|
end
|
|
S.displayedList = displayedList
|
|
S.currentOptions = currentOptions
|
|
|
|
-- ------------------------------------------------------------ Layout
|
|
-- PagedCondensedVerticalGrid: the header takes a row (again at the top of each following
|
|
-- view), then spells fill the columns one by one over ceil(remaining / 3) rows in the space
|
|
-- left; a full view opens another, and the row count restarts from the remaining spells.
|
|
-- Several sections (search results): each keeps its own rows; a section starting on a used
|
|
-- view gets camelot's spacer (spacerSize 20 + yPadding 10), and moves to the next view if its
|
|
-- header and one row do not fit.
|
|
-- groups: list of { title, spells }. Returns the views, each a list of
|
|
-- { header or sort, column, y }.
|
|
local function paginateGroups(groups)
|
|
local views = {}
|
|
local view, busy
|
|
local step = G.spellH + G.gapY
|
|
local function newView(title)
|
|
view = {}
|
|
table.insert(views, view)
|
|
busy = 0
|
|
if title then
|
|
table.insert(view, { header = title, column = 1, y = 0 })
|
|
busy = G.headerH + G.gapY
|
|
end
|
|
end
|
|
for _, g in ipairs(groups) do
|
|
local title, spells = g.title, g.spells
|
|
if not view then
|
|
newView(title)
|
|
elseif busy + G.space + G.gapY + G.headerH + G.gapY + step > G.viewH then
|
|
newView(title)
|
|
else
|
|
busy = busy + G.space + G.gapY
|
|
if title then
|
|
table.insert(view, { header = title, column = 1, y = busy })
|
|
busy = busy + G.headerH + G.gapY
|
|
end
|
|
end
|
|
local i = 1
|
|
while i <= #spells do
|
|
local available = math.floor((G.viewH - busy) / step)
|
|
if available < 1 then
|
|
newView(title)
|
|
available = math.floor((G.viewH - busy) / step)
|
|
end
|
|
local remaining = #spells - i + 1
|
|
local rowLines = math.min(math.ceil(remaining / G.columns), available)
|
|
for column = 1, G.columns do
|
|
for r = 1, rowLines do
|
|
if i <= #spells then
|
|
table.insert(view, { spell = spells[i], column = column, y = busy + (r - 1) * step })
|
|
i = i + 1
|
|
end
|
|
end
|
|
end
|
|
busy = busy + rowLines * step
|
|
if i <= #spells then
|
|
newView(title)
|
|
end
|
|
end
|
|
end
|
|
if not view then newView(nil) end
|
|
return views
|
|
end
|
|
S.paginateGroups = paginateGroups
|
|
|
|
local function paginate(title, spells)
|
|
return paginateGroups({ { title = title, spells = spells } })
|
|
end
|
|
S.paginate = paginate
|
|
|
|
-- --------------------------------------------------------------- Cells
|
|
local function atlas(t, name, keep)
|
|
return ForeverUI.SetAtlas(t, name, keep)
|
|
end
|
|
|
|
-- Create spell cell n: a holder frame (background, texts) with the secure icon button
|
|
-- inside.
|
|
local function createCheckbox(view, n)
|
|
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(0.25)
|
|
c.background = background
|
|
|
|
local b = CreateFrame("Button", "ForeverUISpellBookButton" .. n, c, "SecureActionButtonTemplate")
|
|
b:SetWidth(G.buttonSide)
|
|
b:SetHeight(G.buttonSide)
|
|
b:SetPoint("LEFT", c, "LEFT", 0, 0)
|
|
b:RegisterForClicks("AnyUp")
|
|
b:RegisterForDrag("LeftButton")
|
|
-- cell index: the group snippet passes it to ForeverUIFlyout
|
|
b:SetID(n)
|
|
-- a modified click links the spell in chat instead of casting it
|
|
b:SetAttribute("shift-type1", "link")
|
|
local icon = b:CreateTexture(nil, "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(0.35)
|
|
hover:Hide()
|
|
local auto = b:CreateTexture(nil, "OVERLAY")
|
|
atlas(auto, "spellbook-item-petautocast-corners", true)
|
|
auto:SetAllPoints(icon)
|
|
auto:Hide()
|
|
local cooldown = CreateFrame("Cooldown", "ForeverUISpellBookButton" .. n .. "Cooldown", b, "CooldownFrameTemplate")
|
|
cooldown:SetPoint("TOPLEFT", icon, "TOPLEFT", 2, -2)
|
|
cooldown:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", -2, 2)
|
|
-- flyout arrow (FlyoutButtonTemplate, Arrow)
|
|
local arrow = b:CreateTexture(nil, "OVERLAY")
|
|
arrow:Hide()
|
|
b.icon, b.shadow, b.frame, b.hover, b.auto, b.cooldown = icon, shadow, frame, hover, auto, cooldown
|
|
b.arrow = arrow
|
|
|
|
local name = c:CreateFontString(nil, "ARTWORK")
|
|
font(name, 16)
|
|
name:SetJustifyH("LEFT")
|
|
name: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 = name, sub
|
|
|
|
b:SetScript("OnEnter", function(self)
|
|
c.background:SetAlpha(1)
|
|
self.hover:Show()
|
|
self.isHovered = true
|
|
local spell = c.spell
|
|
if spell and spell.flyout then
|
|
-- group: its name and description (SpellFlyout.db2)
|
|
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
|
|
GameTooltip:SetText(spell.name, 1, 1, 1)
|
|
if spell.flyout.text then GameTooltip:AddLine(spell.flyout.text, nil, nil, nil, 1) end
|
|
GameTooltip:Show()
|
|
S.placeArrow(self)
|
|
elseif spell then
|
|
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
|
|
GameTooltip:SetSpell(spell.slot, spell.book)
|
|
GameTooltip:Show()
|
|
end
|
|
end)
|
|
b:SetScript("OnLeave", function(self)
|
|
self.isHovered, self.pressed = nil, nil
|
|
c.background:SetAlpha(0.25)
|
|
self.hover:Hide()
|
|
GameTooltip:Hide()
|
|
if c.spell and c.spell.flyout then S.placeArrow(self) end
|
|
end)
|
|
b:SetScript("OnMouseDown", function(self)
|
|
self.pressed = true
|
|
self.hover:SetAlpha(0.65)
|
|
if c.spell and c.spell.flyout then S.placeArrow(self) end
|
|
end)
|
|
b:SetScript("OnMouseUp", function(self)
|
|
self.pressed = nil
|
|
self.hover:SetAlpha(0.35)
|
|
if c.spell and c.spell.flyout then S.placeArrow(self) end
|
|
end)
|
|
b:SetScript("OnDragStart", function(self)
|
|
local spell = c.spell
|
|
-- a group cannot go on an action bar: 3.3.5 has no flyouts there
|
|
if spell and not spell.flyout then PickupSpell(spell.slot, spell.book) end
|
|
self.pressed = nil
|
|
end)
|
|
-- SpellButton_OnModifiedClick: the spell link. A group opens through the secure snippet
|
|
-- wrapped around OnClick (OPEN).
|
|
b:SetScript("PostClick", function(self, mouse)
|
|
local spell = c.spell
|
|
if spell and not spell.flyout and IsModifiedClick("CHATLINK") then
|
|
local link = GetSpellLink(spell.slot, spell.book)
|
|
if link then ChatEdit_InsertLink(link) end
|
|
end
|
|
end)
|
|
c.button = b
|
|
c:Hide()
|
|
return c
|
|
end
|
|
|
|
-- UpdateVisuals: square or round frame, texts, cooldown, autocast. Nothing protected:
|
|
-- called in combat by ForeverUIVisuals.
|
|
local function fillCell(c, spell)
|
|
c.spell = spell
|
|
local b = c.button
|
|
if spell.passive then
|
|
SetPortraitToTexture(b.icon, spell.icon)
|
|
b.icon:SetTexCoord(0, 1, 0, 1)
|
|
atlas(b.frame, "talents-node-circle-gray", true)
|
|
b.frame:ClearAllPoints()
|
|
b.frame:SetAllPoints(b)
|
|
atlas(b.hover, "spellbook-item-iconframe-passive-hover", true)
|
|
-- a passive has only its round icon, without an active spell's square shadow
|
|
b.shadow:Hide()
|
|
else
|
|
b.shadow:Show()
|
|
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)
|
|
end
|
|
c.name:SetText(spell.name)
|
|
local subtitle = spell.rank
|
|
if subtitle == "" and spell.passive then subtitle = TEXT.passive end
|
|
c.sub:SetText(subtitle)
|
|
-- 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 + (subH > 0 and (2 + subH) or 0)
|
|
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
|
|
|
|
if spell.flyout then
|
|
b.arrow:Show()
|
|
S.placeArrow(b)
|
|
else
|
|
b.arrow:Hide()
|
|
end
|
|
S.updateState(c)
|
|
end
|
|
|
|
-- "Name(Rank N)": what SecureActionButton passes to CastSpellByName
|
|
function S.spellText(spell)
|
|
local text = spell.name
|
|
if spell.rank ~= "" and not spell.pet then text = text .. "(" .. spell.rank .. ")" end
|
|
return text
|
|
end
|
|
|
|
-- cooldown and autocast, without touching the attributes
|
|
function S.updateState(c)
|
|
local spell = c.spell
|
|
if not spell then return end
|
|
local b = c.button
|
|
if spell.flyout then
|
|
if CooldownFrame_SetTimer then CooldownFrame_SetTimer(b.cooldown, 0, 0, 0) end
|
|
b.auto:Hide()
|
|
return
|
|
end
|
|
local start, duration, active = GetSpellCooldown(spell.slot, spell.book)
|
|
if CooldownFrame_SetTimer and start then
|
|
CooldownFrame_SetTimer(b.cooldown, start, duration, active)
|
|
end
|
|
if spell.pet then
|
|
local allowed, isLit = GetSpellAutocast(spell.slot, spell.book)
|
|
if allowed and isLit then b.auto:Show() else b.auto:Hide() end
|
|
else
|
|
b.auto:Hide()
|
|
end
|
|
end
|
|
|
|
-- ------------------------------------------------------------ Flyout menu
|
|
-- SetClampedTextureRotation at 90 or 270 on an atlas entry: the eight-argument SetTexCoord
|
|
-- (SetRotation would lose the atlas rectangle), width and height swapped.
|
|
local function rotateAtlas(t, name, degrees, width, height)
|
|
local e = ForeverUI.AtlasEntry(name)
|
|
if not e then return end
|
|
t:SetTexture(e[1])
|
|
local u1, u2, v1, v2 = e[2], e[3], e[4], e[5]
|
|
if degrees == 90 then
|
|
t:SetTexCoord(u1, v2, u2, v2, u1, v1, u2, v1)
|
|
else
|
|
t:SetTexCoord(u2, v1, u1, v1, u2, v2, u1, v2)
|
|
end
|
|
t:SetWidth(height)
|
|
t:SetHeight(width)
|
|
end
|
|
|
|
-- FlyoutButtonMixin: normal, hover or pressed arrow; pointing right (90) when closed,
|
|
-- flipped (270) when open, 4 or 2 from the right edge
|
|
function S.placeArrow(b)
|
|
local isOpen = S.openFlyout == b
|
|
local name = "ui-hud-actionbar-flyout"
|
|
if b.pressed then
|
|
name = name .. "-down"
|
|
elseif b.isHovered then
|
|
name = name .. "-mouseover"
|
|
end
|
|
rotateAtlas(b.arrow, name, isOpen and 270 or 90, G.arrowW, G.arrowH)
|
|
b.arrow:ClearAllPoints()
|
|
b.arrow:SetPoint("RIGHT", b, "RIGHT", isOpen and G.arrowOpen or G.arrowClosed, 0)
|
|
end
|
|
|
|
-- SpellFlyoutPopupButtonTemplate: the small action button (30; frame and pushed 35,
|
|
-- centered; hover 31.6 x 30.9), secure to cast. n: button index
|
|
local function createSmallButton(flyout, n)
|
|
local b = CreateFrame("Button", "ForeverUISpellFlyoutButton" .. n, flyout, "SecureActionButtonTemplate")
|
|
b:SetWidth(G.smallSide)
|
|
b:SetHeight(G.smallSide)
|
|
b:RegisterForClicks("AnyUp")
|
|
b:RegisterForDrag("LeftButton")
|
|
b:SetAttribute("type1", "spell")
|
|
b:SetAttribute("shift-type1", "link")
|
|
-- its position never changes: the secure snippet only shows it
|
|
b:SetPoint("LEFT", flyout, "LEFT", G.flyoutStart + (n - 1) * (G.smallSide + G.flyoutGap), 0)
|
|
b:Hide()
|
|
local icon = b:CreateTexture(nil, "BORDER")
|
|
icon:SetAllPoints(b)
|
|
b.icon = icon
|
|
local function state(t, name, l, h, add)
|
|
ForeverUI.SetAtlas(t, name, true)
|
|
t:SetWidth(l)
|
|
t:SetHeight(h)
|
|
t:ClearAllPoints()
|
|
t:SetPoint("CENTER", b, "CENTER", 0, 0)
|
|
if add then t:SetBlendMode("ADD") end
|
|
end
|
|
local e = ForeverUI.AtlasEntry("ui-hud-actionbar-iconframe")
|
|
b:SetNormalTexture(e and e[1] or "")
|
|
state(b:GetNormalTexture(), "ui-hud-actionbar-iconframe", G.smallFrame, G.smallFrame)
|
|
b:SetPushedTexture(e and e[1] or "")
|
|
state(b:GetPushedTexture(), "ui-hud-actionbar-iconframe-down", G.smallFrame, G.smallFrame)
|
|
b:SetHighlightTexture(e and e[1] or "")
|
|
state(b:GetHighlightTexture(), "ui-hud-actionbar-iconframe-mouseover", G.smallStateW, G.smallStateH)
|
|
-- CHECKED state: the hover art in ADD, shown while the spell is current
|
|
local checkMark = b:CreateTexture(nil, "OVERLAY")
|
|
state(checkMark, "ui-hud-actionbar-iconframe-mouseover", G.smallStateW, G.smallStateH, true)
|
|
checkMark:Hide()
|
|
b.checkMark = checkMark
|
|
b.cooldown = CreateFrame("Cooldown", "ForeverUISpellFlyoutButton" .. n .. "Cooldown", b, "CooldownFrameTemplate")
|
|
b.cooldown:SetAllPoints(icon)
|
|
|
|
b:SetScript("OnEnter", function(self)
|
|
if not self.spell then return end
|
|
GameTooltip:SetOwner(self, "ANCHOR_RIGHT", 4, 4)
|
|
GameTooltip:SetSpell(self.spell.slot, self.spell.book)
|
|
GameTooltip:Show()
|
|
end)
|
|
b:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
|
b:SetScript("OnDragStart", function(self)
|
|
if self.spell then PickupSpell(self.spell.slot, self.spell.book) end
|
|
end)
|
|
-- SpellFlyoutPopupButtonMixin:OnClick: the link here; the cast and closing the menu go
|
|
-- through the secure snippet (SMALL_PRE, SMALL_POST)
|
|
b:SetScript("PostClick", function(self)
|
|
if self.spell and IsModifiedClick("CHATLINK") then
|
|
local link = GetSpellLink(self.spell.slot, self.spell.book)
|
|
if link then ChatEdit_InsertLink(link) end
|
|
end
|
|
end)
|
|
return b
|
|
end
|
|
|
|
local function updateSmallButton(b)
|
|
local spell = b.spell
|
|
if not spell then return end
|
|
local start, duration, active = GetSpellCooldown(spell.slot, spell.book)
|
|
if CooldownFrame_SetTimer and start then
|
|
CooldownFrame_SetTimer(b.cooldown, start, duration, active)
|
|
end
|
|
if IsCurrentSpell and IsCurrentSpell(spell.slot, spell.book) then b.checkMark:Show() else b.checkMark:Hide() end
|
|
end
|
|
|
|
-- FlyoutPopupTemplate, opened to the right: start cap (FlyoutBottom) on the left, middle
|
|
-- (FlyoutMidLeft), end cap (FlyoutButton) on the right; both caps rotated by 90
|
|
local function buildFlyout(book)
|
|
-- Explicitly protected: DISPLAY and ON_HIDE hide it in combat, and the handle of a plain
|
|
-- frame only works there through protected children, which a book without groups never
|
|
-- creates ("Invalid frame handle", RestrictedFrames.lua GetHandleFrame)
|
|
local flyout = CreateFrame("Frame", "ForeverUISpellFlyout", book, "SecureFrameTemplate")
|
|
flyout:SetFrameStrata("DIALOG")
|
|
flyout:EnableMouse(true)
|
|
flyout:SetHeight(G.flyoutH)
|
|
flyout:Hide()
|
|
local start = flyout:CreateTexture(nil, "BACKGROUND")
|
|
rotateAtlas(start, "ui-hud-actionbar-iconframe-flyoutbottom", 90, 47, 5)
|
|
start:SetHeight(G.flyoutH)
|
|
start:SetPoint("LEFT", flyout, "LEFT", 0, 0)
|
|
local finish = flyout:CreateTexture(nil, "BACKGROUND")
|
|
rotateAtlas(finish, "ui-hud-actionbar-iconframe-flyoutbutton", 90, 47, 29)
|
|
finish:SetHeight(G.flyoutH)
|
|
finish:SetPoint("RIGHT", flyout, "RIGHT", 0, 0)
|
|
local middle = flyout:CreateTexture(nil, "BACKGROUND")
|
|
atlas(middle, "_ui-hud-actionbar-iconframe-flyoutmidleft", true)
|
|
middle:SetHeight(G.flyoutH)
|
|
middle:SetPoint("LEFT", start, "RIGHT", 0, 0)
|
|
middle:SetPoint("RIGHT", finish, "LEFT", 0, 0)
|
|
for _, t in ipairs({ start, middle, finish }) do
|
|
t:SetVertexColor(G.flyoutTint, G.flyoutTint, G.flyoutTint)
|
|
end
|
|
flyout.buttons = {}
|
|
-- hidden by anything (page, book, category): the arrow follows
|
|
flyout:SetScript("OnHide", function()
|
|
local b = S.openFlyout
|
|
S.openFlyout = nil
|
|
if b then S.placeArrow(b) end
|
|
end)
|
|
S.flyout = flyout
|
|
return flyout
|
|
end
|
|
|
|
-- ForeverUIFlyout, called by a group's secure snippet (CallMethod): textures of the small
|
|
-- buttons and the arrow. Nothing protected. k: cell index; fk: group index in S.flyoutData
|
|
function S.flyoutOpenedBy(k, fk)
|
|
local old = S.openFlyout
|
|
local c = S.cells and S.cells[k]
|
|
local members = S.flyoutData and S.flyoutData[fk]
|
|
if not c or not members then return end
|
|
S.openFlyout = c.button
|
|
if old and old ~= c.button then S.placeArrow(old) end
|
|
for i, p in ipairs(S.flyout.buttons) do
|
|
p.spell = members[i]
|
|
if p.spell then
|
|
p.icon:SetTexture(p.spell.icon)
|
|
updateSmallButton(p)
|
|
end
|
|
end
|
|
S.placeArrow(c.button)
|
|
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
|
|
-- The close button is not a child of the book, so its level is set again on each show,
|
|
-- above the metal (+20) and the title (+21).
|
|
function S.raiseCloseButton()
|
|
local close, book = SpellBookCloseButton, S.book
|
|
if not close or not book then return end
|
|
close:SetFrameStrata(book:GetFrameStrata())
|
|
close:SetFrameLevel(book:GetFrameLevel() + 22)
|
|
end
|
|
|
|
-- Red title bar button. normal, pressed: atlas names; template: frame template
|
|
local function redButton(parent, normal, pressed, template)
|
|
local b = CreateFrame("Button", nil, parent, template)
|
|
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
|
|
|
|
-- Window chrome of the book: rock, metal, portrait, title, close and resize buttons.
|
|
local function buildFrame(book)
|
|
-- rock background and metal (PortraitFrameTemplate)
|
|
local rock = book:CreateTexture(nil, "BACKGROUND")
|
|
rock:SetTexture(ROCK, true)
|
|
if rock.SetHorizTile then rock:SetHorizTile(true) rock:SetVertTile(true) end
|
|
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() + 20)
|
|
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)
|
|
t:SetPoint(a2, c2, r2)
|
|
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")
|
|
|
|
-- round portrait (PortraitContainer, below the metal)
|
|
local portraitFrame = CreateFrame("Frame", nil, book)
|
|
portraitFrame:SetAllPoints(book)
|
|
portraitFrame:SetFrameLevel(book:GetFrameLevel() + 19)
|
|
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(PORTRAIT)
|
|
book.portrait = portrait
|
|
|
|
-- title (TitleContainer, above the metal)
|
|
local banner = CreateFrame("Frame", nil, book)
|
|
banner:SetFrameLevel(book:GetFrameLevel() + 21)
|
|
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
|
|
book.banner = banner
|
|
|
|
-- close button: WotLK's, re-skinned. It stays a child of SpellBookFrame: its OnClick hides
|
|
-- ITS PARENT (HideUIPanel), so closing never goes through our code.
|
|
local close = SpellBookCloseButton
|
|
if close then
|
|
S.raiseCloseButton()
|
|
close:SetWidth(G.redSide)
|
|
close:SetHeight(G.redSide)
|
|
close:SetHitRectInsets(0, 0, 0, 0)
|
|
for _, state in ipairs({ { "Normal", "redbutton-exit" }, { "Pushed", "redbutton-exit-pressed" },
|
|
{ "Highlight", "redbutton-highlight" } }) do
|
|
local e = ForeverUI.AtlasEntry(state[2])
|
|
local t = close["Get" .. state[1] .. "Texture"](close)
|
|
if not t and e then
|
|
close["Set" .. state[1] .. "Texture"](close, e[1])
|
|
t = close["Get" .. state[1] .. "Texture"](close)
|
|
end
|
|
if t then
|
|
atlas(t, state[2], true)
|
|
t:ClearAllPoints()
|
|
t:SetAllPoints(close)
|
|
end
|
|
end
|
|
close:ClearAllPoints()
|
|
close:SetPoint("TOPRIGHT", book, "TOPRIGHT", G.closeX, G.closeY)
|
|
end
|
|
|
|
-- maximize / minimize (MaximizeMinimizeButtonFrameTemplate), on the left. It tells the
|
|
-- controller to switch mode (SecureHandlerClickTemplate), in combat too.
|
|
local size = redButton(book, "redbutton-condense", "redbutton-condense-pressed", "SecureHandlerClickTemplate")
|
|
size:SetFrameLevel(book:GetFrameLevel() + 22)
|
|
-- Left of the close button, anchored to the book: a secure button anchored to the client's
|
|
-- close button would make it protected, and S.raiseCloseButton sets its level in combat too
|
|
size:SetPoint("TOPRIGHT", book, "TOPRIGHT", G.closeX - G.redSide, G.closeY)
|
|
size:SetAttribute("_onclick", [==[ self:GetFrameRef("ctrl"):SetAttribute("size", 1) ]==])
|
|
size:HookScript("OnClick", function() PlaySound("igMainMenuOptionCheckBoxOn") end)
|
|
book.size = size
|
|
end
|
|
|
|
local function build()
|
|
if S.book or not SpellBookFrame then
|
|
return S.book
|
|
end
|
|
-- WotLK's panel no longer catches the mouse: it is empty
|
|
SpellBookFrame:EnableMouse(false)
|
|
|
|
local book = CreateFrame("Frame", "ForeverUISpellBookFrame", SpellBookFrame)
|
|
book:SetPoint("TOP", UIParent, "TOP", 0, G.top)
|
|
book:SetHeight(G.height)
|
|
book:SetWidth(G.width)
|
|
book:EnableMouse(true)
|
|
-- NO SetToplevel here: a book raising itself would cover the close button, its sibling.
|
|
-- SpellBookFrame is toplevel and raises both.
|
|
S.book = book
|
|
buildFrame(book)
|
|
|
|
-- open book: two pages, or one when reduced
|
|
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 singlePage = pages:CreateTexture(nil, "BACKGROUND")
|
|
atlas(singlePage, "spellbook-page-right-c60-2x", true)
|
|
singlePage:SetAllPoints(pages)
|
|
singlePage:Hide()
|
|
pages.left, pages.right, pages.singlePage = left, right, singlePage
|
|
S.pages = pages
|
|
|
|
-- category tabs
|
|
local tabs = CreateFrame("Frame", nil, pages)
|
|
tabs:SetPoint("TOPLEFT", pages, "TOPLEFT", G.tabsX, G.tabsY)
|
|
tabs:SetWidth(1)
|
|
tabs:SetHeight(G.tabH)
|
|
tabs.buttons = {}
|
|
S.tabs = tabs
|
|
|
|
-- the two views; the content turns pages with the mouse wheel through a secure snippet
|
|
-- (SecureHandlerMouseWheelTemplate)
|
|
local content = CreateFrame("Frame", "ForeverUISpellBookContent", pages, "SecureHandlerMouseWheelTemplate")
|
|
content:SetPoint("TOPLEFT", pages, "TOPLEFT", 0, G.pagesTop)
|
|
content:SetPoint("BOTTOMRIGHT", pages, "BOTTOMRIGHT", 0, 0)
|
|
S.content = content
|
|
S.views = {}
|
|
S.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] = createCheckbox(view, k)
|
|
S.cells[k] = view.cells[n]
|
|
end
|
|
view.headers = { createHeader(view) }
|
|
view.header = view.headers[1]
|
|
S.views[v] = view
|
|
end
|
|
|
|
-- paging: "Page n/m", previous, next (right to left); a click tells the controller to
|
|
-- turn (SecureHandlerClickTemplate)
|
|
local nextPage = CreateFrame("Button", "ForeverUISpellBookNextPage", content, "SecureHandlerClickTemplate")
|
|
local prev = CreateFrame("Button", "ForeverUISpellBookPrevPage", content, "SecureHandlerClickTemplate")
|
|
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(ARROWS .. f[2] .. "-Up")
|
|
b:SetPushedTexture(ARROWS .. f[2] .. "-Down")
|
|
b:SetDisabledTexture(ARROWS .. f[2] .. "-Disabled")
|
|
b:SetHighlightTexture(SQUARE_HOVER)
|
|
b:GetHighlightTexture():SetBlendMode("ADD")
|
|
b:SetAttribute("_onclick", ([[ self:GetFrameRef("ctrl"):SetAttribute("rotate", %d) ]]):format(f[3]))
|
|
b:HookScript("OnClick", function() PlaySound("igAbiliityPageTurn") 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)
|
|
S.pager = { nextPage = nextPage, prev = prev, text = text }
|
|
content:EnableMouseWheel(true)
|
|
content:SetAttribute("_onmousewheel", [[ self:GetFrameRef("ctrl"):SetAttribute("rotate", -delta) ]])
|
|
|
|
-- settings (SpellBookSettingsDropdown): TOPRIGHT (-30, -27) of the book
|
|
S.createSettings(pages)
|
|
buildFlyout(book)
|
|
S.createController(book)
|
|
|
|
S.suppressWotLK()
|
|
S.applySize()
|
|
return book
|
|
end
|
|
S.build = build
|
|
|
|
-- ------------------------------------------------------------ Settings
|
|
-- UIPanelArrowDropdownButtonTemplate: 15 x 16, arrow common-dropdown-a-button at atlas
|
|
-- size, centered; on hover the same in ADD at 0.4; pressed, the icon moves by (1, -1). The
|
|
-- menu opens TOPLEFT on BOTTOMLEFT (DropdownButton). Entries (camelot's
|
|
-- SetupSettingsDropdown): hide passives; group on flyouts (kept in ForeverUIDB, 3.3.5 has no
|
|
-- spellBookHideFlyouts; on by default); all ranks, for every class (camelot hides it for
|
|
-- rogues and warriors). A click toggles and keeps the menu open.
|
|
function S.createSettings(pages)
|
|
local b = CreateFrame("Button", "ForeverUISpellBookSettingsButton", pages, "SecureHandlerClickTemplate")
|
|
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.icon = icon
|
|
b:SetScript("OnMouseDown", function() icon:SetPoint("CENTER", b, "CENTER", 1, -1) end)
|
|
b:SetScript("OnMouseUp", function() icon:SetPoint("CENTER", b, "CENTER", 0, 0) end)
|
|
|
|
-- dropdown list
|
|
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, "SecureHandlerClickTemplate")
|
|
l:SetID(r.bit)
|
|
l:SetHeight(MENU.rowH)
|
|
l:SetPoint("TOPLEFT", list, "TOPLEFT", MENU.rowX, -MENU.edge - (i - 1) * MENU.rowH)
|
|
l:SetHighlightTexture("Interface" .. SEP .. "QuestFrame" .. SEP .. "UI-QuestTitleHighlight")
|
|
l:GetHighlightTexture():SetBlendMode("ADD")
|
|
local text = l:CreateFontString(nil, "ARTWORK", "GameFontHighlightLeft")
|
|
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")
|
|
ForeverUI.SetAtlas(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")
|
|
ForeverUI.SetAtlas(checkMark, "common-dropdown-icon-checkmark-yellow", true)
|
|
checkMark:SetWidth(MENU.checkMarkW)
|
|
checkMark:SetHeight(MENU.checkMarkH)
|
|
checkMark:SetPoint("CENTER", checkbox, "CENTER", MENU.checkMarkX, MENU.checkMarkY)
|
|
checkMark:Hide()
|
|
l.text, l.checkMark, l.setting = text, checkMark, r
|
|
-- UIDropDownMenuButton_OnClick, keepShownOnClick: the list stays open; the setting goes
|
|
-- through the controller, in combat too
|
|
l:SetAttribute("_onclick", [==[ self:GetFrameRef("ctrl"):SetAttribute("setting", self:GetID()) ]==])
|
|
l:HookScript("OnClick", function() PlaySound("UChatScrollButton") end)
|
|
-- SetupHidePassivesCheckbox: disabled during a search, with a tooltip (the other two have
|
|
-- none)
|
|
if r.bit == 1 then
|
|
l:SetScript("OnEnter", function(self)
|
|
if not S.settingsGrayed then return end
|
|
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
|
|
GameTooltip:SetText(TEXT.passivesDisabled, 1, 1, 1, 1, 1)
|
|
GameTooltip:Show()
|
|
end)
|
|
l:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
|
end
|
|
list.rows[i] = l
|
|
end
|
|
local width = longest + MENU.extraWidth + MENU.margin
|
|
list:SetWidth(width)
|
|
list:SetHeight(#SETTINGS * MENU.rowH + 2 * MENU.edge)
|
|
for _, l in ipairs(list.rows) do l:SetWidth(width - MENU.margin) end
|
|
|
|
-- the arrow opens or closes the list, in combat too; the list hides itself 2 s after the
|
|
-- mouse leaves it
|
|
b:SetFrameRef("list", list)
|
|
b:SetAttribute("_onclick", ([==[
|
|
local l = self:GetFrameRef("list")
|
|
if l:IsShown() then
|
|
l:Hide()
|
|
else
|
|
l:Show()
|
|
l:RegisterAutoHide(%d)
|
|
l:AddToAutoHide(self)
|
|
end
|
|
]==]):format(MENU.pending))
|
|
S.settings, S.settingsList = b, list
|
|
return b
|
|
end
|
|
|
|
-- In combat too: grayed during a search (the secure snippet disables the rows; this sets
|
|
-- the text color). grayed: true during a search
|
|
function S.graySettings(grayed)
|
|
if not S.settingsList then return end
|
|
S.settingsGrayed = grayed
|
|
for _, l in ipairs(S.settingsList.rows) do
|
|
-- "Show all spell ranks" stays enabled during a search (camelot disables it)
|
|
if grayed and l.setting.bit ~= 4 then
|
|
l.text:SetTextColor(0.5, 0.5, 0.5)
|
|
else
|
|
l.text:SetTextColor(1, 1, 1)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- In combat too: check marks follow the settings combination (textures only). reg: settings
|
|
-- bits
|
|
function S.checkSettings(reg)
|
|
if not S.settingsList then return end
|
|
for _, l in ipairs(S.settingsList.rows) do
|
|
local active = math.floor(reg / l.setting.bit) % 2 == 1
|
|
if l.setting.inverse then active = not active end
|
|
if active then l.checkMark:Show() else l.checkMark:Hide() end
|
|
end
|
|
end
|
|
|
|
-- Hide WotLK's regions and frames in SpellBookFrame, except the book and the close button.
|
|
function S.suppressWotLK()
|
|
for _, region in ipairs({ SpellBookFrame:GetRegions() }) do
|
|
suppress(region)
|
|
end
|
|
for _, child in ipairs({ SpellBookFrame:GetChildren() }) do
|
|
if child ~= S.book and child ~= SpellBookCloseButton then
|
|
suppress(child)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Two pages or one: page and button textures. Widths and the right view are set by the
|
|
-- secure "place" snippet; this follows, in combat too. reduced: one page; nil reads the
|
|
-- saved setting
|
|
function S.applySize(reduced)
|
|
local book, pages = S.book, S.pages
|
|
if not book then return end
|
|
if reduced == nil then reduced = settings().reduced end
|
|
if S.onResize then S.onResize(reduced) end
|
|
if reduced then
|
|
pages.left:Hide() pages.right:Hide() pages.singlePage:Show()
|
|
atlas(book.size:GetNormalTexture(), "redbutton-expand", true)
|
|
atlas(book.size:GetPushedTexture(), "redbutton-expand-pressed", true)
|
|
else
|
|
pages.left:Show() pages.right:Show() pages.singlePage:Hide()
|
|
atlas(book.size:GetNormalTexture(), "redbutton-condense", true)
|
|
atlas(book.size:GetPushedTexture(), "redbutton-condense-pressed", true)
|
|
end
|
|
end
|
|
|
|
-- ------------------------------------------------------------ Tabs
|
|
-- A tab tells the controller "category n" (SecureHandlerClickTemplate); tabs are created
|
|
-- out of combat only.
|
|
local function createTab(parent, n)
|
|
local b = CreateFrame("Button", "ForeverUISpellBookTab" .. n, parent, "SecureHandlerClickTemplate")
|
|
b:SetID(n)
|
|
b:SetFrameRef("ctrl", S.ctrl)
|
|
b:SetAttribute("_onclick", [[ self:GetFrameRef("ctrl"):SetAttribute("category", self:GetID()) ]])
|
|
S.ctrl:SetFrameRef("o" .. n, b)
|
|
S.ctrl:Execute(("TABS[%d] = self:GetFrameRef(\"o%d\")"):format(n, 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)
|
|
-- frame ABOVE the icon (camelot: ARTWORK sublevel 1): a child frame
|
|
local hovered = CreateFrame("Frame", nil, b)
|
|
hovered:SetAllPoints(b)
|
|
hovered:SetFrameLevel(b:GetFrameLevel() + 1)
|
|
local frame = hovered:CreateTexture(nil, "ARTWORK")
|
|
atlas(frame, "spellbook-tab-frame-c60")
|
|
frame:SetPoint("BOTTOM", b, "BOTTOM", 0, 1)
|
|
local active = hovered:CreateTexture(nil, "ARTWORK")
|
|
atlas(active, "spellbook-tab-frame-glow-c60")
|
|
active:SetPoint("BOTTOM", b, "BOTTOM", 0, 1)
|
|
local glow = hovered: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:HookScript("OnClick", function() PlaySound("igSpellBookOpen") end)
|
|
b:SetScript("OnEnter", function(self)
|
|
GameTooltip:SetOwner(self, "ANCHOR_RIGHT", -12, -6)
|
|
GameTooltip:SetText(self.name)
|
|
GameTooltip:Show()
|
|
end)
|
|
b:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
|
return b
|
|
end
|
|
|
|
-- out of combat: one tab per category, in place
|
|
local function layoutTabs(cats)
|
|
local tabs = S.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.index, b.name = i, cat.name
|
|
b.icon:SetTexture(cat.icon)
|
|
b:ClearAllPoints()
|
|
b:SetPoint("TOPLEFT", tabs, "TOPLEFT", (i - 1) * (G.tabW + G.tabGap), 0)
|
|
b:Show()
|
|
end
|
|
for i = #cats + 1, #tabs.buttons do
|
|
tabs.buttons[i]:Hide()
|
|
end
|
|
end
|
|
|
|
-- in combat too: the selected tab lights up (textures only; enabling is done by the secure
|
|
-- snippet)
|
|
local function highlightTabs(selectedItem)
|
|
for i, b in ipairs(S.tabs.buttons) do
|
|
-- 3.3.5 has no SetShown
|
|
if i == selectedItem then
|
|
b.frame:Hide() b.active:Show() b.glow:Show()
|
|
else
|
|
b.frame:Show() b.active:Hide() b.glow:Hide()
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ------------------------------------------------------------ Filling
|
|
S.state = { category = 1, page = 1 }
|
|
|
|
-- Requests to the controller, out of combat only (tabs, arrows and wheel are secure and work
|
|
-- in combat).
|
|
function S.choose(index)
|
|
if InCombatLockdown() or not S.ctrl then return end
|
|
S.ctrl:SetAttribute("category", index)
|
|
end
|
|
|
|
function S.rotate(direction)
|
|
if InCombatLockdown() or not S.ctrl then return end
|
|
S.ctrl:SetAttribute("rotate", direction)
|
|
end
|
|
|
|
-- The controller. Its restricted environment holds the published data (D: cells of each
|
|
-- page, NUM_PAGES: pages of each category, V: spells of each group), the state (SETTINGS,
|
|
-- MODE, CAT, PAGE, OPEN_OWNER) and frame handles. Restricted code (RestrictedExecution.lua,
|
|
-- RestrictedFrames.lua) allows no braces and no "function" keyword, hence newtable;
|
|
-- SetPoint only targets an explicitly protected frame or "$parent".
|
|
local DISPLAY = [==[
|
|
local list = D[SETTINGS .. ":" .. MODE .. ":" .. CAT .. ":" .. PAGE]
|
|
FLYOUT:Hide()
|
|
OPEN_OWNER = nil
|
|
for k = 1, NUM_CELLS do
|
|
CELLS[k]:Hide()
|
|
BUTTONS[k]:SetAttribute("flyout", nil)
|
|
end
|
|
if list then
|
|
for i = 1, #list do
|
|
local e = list[i]
|
|
local c = CELLS[e[1]]
|
|
local b = BUTTONS[e[1]]
|
|
c:ClearAllPoints()
|
|
c:SetPoint("TOPLEFT", "$parent", "TOPLEFT", e[2], e[3])
|
|
if e[4] == "" then b:SetAttribute("type1", nil) else b:SetAttribute("type1", e[4]) end
|
|
if e[5] == "" then b:SetAttribute("spell", nil) else b:SetAttribute("spell", e[5]) end
|
|
if e[6] == "" then
|
|
b:SetAttribute("type2", nil)
|
|
b:SetAttribute("macrotext2", nil)
|
|
else
|
|
b:SetAttribute("type2", e[6])
|
|
b:SetAttribute("macrotext2", e[7])
|
|
end
|
|
if e[8] > 0 then b:SetAttribute("flyout", e[8]) end
|
|
c:Show()
|
|
end
|
|
end
|
|
for i = 1, NUM_TABS do
|
|
if i == CAT then TABS[i]:Disable() else TABS[i]:Enable() end
|
|
end
|
|
for i = 1, NUM_ENTRIES do
|
|
if CAT == 0 and ENTRIES[i]:GetID() ~= 4 then ENTRIES[i]:Disable() else ENTRIES[i]:Enable() end
|
|
end
|
|
if PAGE > 1 then PREV:Enable() else PREV:Disable() end
|
|
if PAGE < NUM_PAGES[SETTINGS .. ":" .. MODE .. ":" .. CAT] then NEXT:Enable() else NEXT:Disable() end
|
|
control:CallMethod("ForeverUIVisuals", CAT, PAGE, MODE, SETTINGS)
|
|
]==]
|
|
|
|
local ON_ATTRIBUTE_CHANGED = [==[
|
|
if name == "category" and value then
|
|
self:SetAttribute("category", nil)
|
|
if (value >= 1 and value <= NUM_TABS) or (value == 0 and NUM_PAGES[SETTINGS .. ":" .. MODE .. ":0"]) then
|
|
CAT = value
|
|
PAGE = 1
|
|
control:RunAttribute("show")
|
|
end
|
|
elseif name == "rotate" and value then
|
|
self:SetAttribute("rotate", nil)
|
|
local p = PAGE + value
|
|
if p >= 1 and p <= NUM_PAGES[SETTINGS .. ":" .. MODE .. ":" .. CAT] then
|
|
PAGE = p
|
|
control:RunAttribute("show")
|
|
end
|
|
elseif name == "size" and value then
|
|
self:SetAttribute("size", nil)
|
|
if MODE == 2 then
|
|
MODE = 1
|
|
PAGE = PAGE * 2 - 1
|
|
else
|
|
MODE = 2
|
|
PAGE = ceil(PAGE / 2)
|
|
end
|
|
if PAGE > NUM_PAGES[SETTINGS .. ":" .. MODE .. ":" .. CAT] then PAGE = NUM_PAGES[SETTINGS .. ":" .. MODE .. ":" .. CAT] end
|
|
control:RunAttribute("place")
|
|
control:RunAttribute("show")
|
|
elseif name == "clear" and value then
|
|
self:SetAttribute("clear", nil)
|
|
if CAT == 0 then
|
|
CAT = 1
|
|
PAGE = 1
|
|
control:RunAttribute("show")
|
|
end
|
|
elseif name == "setting" and value then
|
|
self:SetAttribute("setting", nil)
|
|
if floor(SETTINGS / value) % 2 == 1 then SETTINGS = SETTINGS - value else SETTINGS = SETTINGS + value end
|
|
if PAGE > NUM_PAGES[SETTINGS .. ":" .. MODE .. ":" .. CAT] then PAGE = NUM_PAGES[SETTINGS .. ":" .. MODE .. ":" .. CAT] end
|
|
control:RunAttribute("show")
|
|
end
|
|
]==]
|
|
|
|
-- MaximizeMinimizeButtonFrameTemplate: 1618 / 809 wide, two pages or one (right view
|
|
-- hidden). %d: widths from G.
|
|
local LAYOUT = [==[
|
|
if MODE == 1 then
|
|
SPELL_BOOK:SetWidth(%d)
|
|
PAGES:SetWidth(%d)
|
|
VIEW2:Hide()
|
|
else
|
|
SPELL_BOOK:SetWidth(%d)
|
|
PAGES:SetWidth(%d)
|
|
VIEW2:Show()
|
|
end
|
|
]==]
|
|
|
|
-- SpellFlyoutMixin:Toggle, wrapped around each cell's OnClick: the same group closes the
|
|
-- menu, another moves it. A modifier does not open it (link). %d: base width, width per
|
|
-- button and x offset, from G.
|
|
local OPEN = [==[
|
|
local v = self:GetAttribute("flyout")
|
|
if v and not IsModifierKeyDown() then
|
|
if OPEN_OWNER == self and FLYOUT:IsShown() then
|
|
FLYOUT:Hide()
|
|
OPEN_OWNER = nil
|
|
else
|
|
local list = V[v]
|
|
local n = #list
|
|
for i = 1, NUM_SMALL do
|
|
if i <= n then
|
|
SMALL[i]:SetAttribute("spell", list[i])
|
|
SMALL[i]:Show()
|
|
else
|
|
SMALL[i]:Hide()
|
|
end
|
|
end
|
|
FLYOUT:SetWidth(%d + n * %d)
|
|
FLYOUT:ClearAllPoints()
|
|
FLYOUT:SetPoint("LEFT", self, "RIGHT", %d, 0)
|
|
FLYOUT:Show()
|
|
OPEN_OWNER = self
|
|
control:CallMethod("ForeverUIFlyout", self:GetID(), v)
|
|
end
|
|
end
|
|
]==]
|
|
|
|
-- a small button: the spell is cast, then the menu closes (except for a link)
|
|
local SMALL_PRE = [==[ if not IsModifiedClick("CHATLINK") then return nil, "close" end ]==]
|
|
local SMALL_POST = [==[ FLYOUT:Hide() OPEN_OWNER = nil ]==]
|
|
-- the book hides: the menu too (FlyoutButtonMixin:OnHide)
|
|
local ON_HIDE = [==[ FLYOUT:Hide() OPEN_OWNER = nil ]==]
|
|
|
|
-- Create the secure controller and give it the handles of the cells, buttons, flyout, pages,
|
|
-- page arrows and settings rows.
|
|
function S.createController(book)
|
|
local ctrl = CreateFrame("Frame", "ForeverUISpellBookControl", book, "SecureHandlerAttributeTemplate")
|
|
S.ctrl = ctrl
|
|
for k, c in ipairs(S.cells) do
|
|
ctrl:SetFrameRef("c" .. k, c)
|
|
ctrl:SetFrameRef("b" .. k, c.button)
|
|
ctrl:WrapScript(c.button, "OnClick", OPEN:format(G.flyoutStart + G.flyoutEnd - G.flyoutGap,
|
|
G.smallSide + G.flyoutGap, G.flyoutOffset))
|
|
end
|
|
ctrl:SetFrameRef("flyout", S.flyout)
|
|
ctrl:SetFrameRef("book", book)
|
|
ctrl:SetFrameRef("pages", S.pages)
|
|
ctrl:SetFrameRef("view2", S.views[2])
|
|
book.size:SetFrameRef("ctrl", ctrl)
|
|
for i, l in ipairs(S.settingsList.rows) do
|
|
l:SetFrameRef("ctrl", ctrl)
|
|
ctrl:SetFrameRef("e" .. i, l)
|
|
end
|
|
ctrl:SetFrameRef("prevPage", S.pager.prev)
|
|
ctrl:SetFrameRef("nextPage", S.pager.nextPage)
|
|
for _, b in ipairs({ S.pager.prev, S.pager.nextPage, S.content }) do
|
|
b:SetFrameRef("ctrl", ctrl)
|
|
end
|
|
ctrl:Execute(([==[
|
|
NUM_CELLS = %d
|
|
CELLS = newtable()
|
|
BUTTONS = newtable()
|
|
for k = 1, NUM_CELLS do
|
|
CELLS[k] = self:GetFrameRef("c" .. k)
|
|
BUTTONS[k] = self:GetFrameRef("b" .. k)
|
|
end
|
|
FLYOUT = self:GetFrameRef("flyout")
|
|
SPELL_BOOK = self:GetFrameRef("book")
|
|
PAGES = self:GetFrameRef("pages")
|
|
VIEW2 = self:GetFrameRef("view2")
|
|
MODE = 2
|
|
SETTINGS = 0
|
|
PREV = self:GetFrameRef("prevPage")
|
|
NEXT = self:GetFrameRef("nextPage")
|
|
TABS = newtable()
|
|
SMALL = newtable()
|
|
ENTRIES = newtable()
|
|
NUM_ENTRIES = %d
|
|
for i = 1, NUM_ENTRIES do ENTRIES[i] = self:GetFrameRef("e" .. i) end
|
|
NUM_SMALL = 0
|
|
D = newtable()
|
|
NUM_PAGES = newtable()
|
|
V = newtable()
|
|
NUM_TABS = 0
|
|
CAT = 1
|
|
PAGE = 1
|
|
]==]):format(#S.cells, #S.settingsList.rows))
|
|
ctrl:SetAttribute("show", DISPLAY)
|
|
ctrl:SetAttribute("place", LAYOUT:format(G.compactWidth, G.reducedBookW, G.width, G.bookW))
|
|
ctrl:SetAttribute("_onattributechanged", ON_ATTRIBUTE_CHANGED)
|
|
ctrl:WrapScript(S.content, "OnHide", ON_HIDE)
|
|
-- called by CallMethod: ordinary code, allowed in combat
|
|
ctrl.ForeverUIVisuals = function(_, cat, page, mode, reg) S.updateVisuals(cat, page, mode, reg) end
|
|
ctrl.ForeverUIFlyout = function(_, k, fk) S.flyoutOpenedBy(k, fk) end
|
|
return ctrl
|
|
end
|
|
|
|
-- Small flyout buttons: as many as the largest group, created out of combat, each known to
|
|
-- the controller. n: number needed
|
|
local function ensureSmallButtons(n)
|
|
local flyout, ctrl = S.flyout, S.ctrl
|
|
for i = #flyout.buttons + 1, n do
|
|
local p = createSmallButton(flyout, i)
|
|
flyout.buttons[i] = p
|
|
ctrl:SetFrameRef("p" .. i, p)
|
|
ctrl:WrapScript(p, "OnClick", SMALL_PRE, SMALL_POST)
|
|
ctrl:Execute(("SMALL[%d] = self:GetFrameRef(\"p%d\") NUM_SMALL = %d"):format(i, i, i))
|
|
end
|
|
end
|
|
|
|
-- quoted string for a secure snippet
|
|
local function q(text)
|
|
return string.format("%q", text or "")
|
|
end
|
|
|
|
-- Out of combat: lay out each category, publish it to the controller, then show the current
|
|
-- state.
|
|
function S.update()
|
|
local book = S.book
|
|
if not book then return end
|
|
if InCombatLockdown() then
|
|
S.queued = true
|
|
return
|
|
end
|
|
S.queued, S.dirty = nil, nil
|
|
local cats = categories()
|
|
local e = S.state
|
|
if e.category > #cats then e.category = 1 end
|
|
-- Search (SpellBookSearch.lua): its results form one more category, 0, without a tab. With
|
|
-- no result the search ends (DisplayFullSearchResults -> ClearActiveSearchState).
|
|
local search = ForeverUI.SpellBookSearch
|
|
if search then search.invalidate() end
|
|
local searchGroups = {}
|
|
if search and search.active() then
|
|
-- one variant per settings set: search follows Hide Passives and Show all spell ranks, and
|
|
-- never groups (camelot shows passives and all ranks there, and groups)
|
|
local empty = true
|
|
for r = 0, 7 do
|
|
local g = search.groups(cats, { passives = r % 2 == 1, flyouts = true,
|
|
ranks = math.floor(r / 4) % 2 == 1 })
|
|
searchGroups[r] = g
|
|
if #g > 0 then empty = false end
|
|
end
|
|
if empty then
|
|
search.quit()
|
|
searchGroups = {}
|
|
end
|
|
end
|
|
if e.category == 0 and not searchGroups[0] then e.category = 1 end
|
|
layoutTabs(cats)
|
|
local mode = settings().reduced and 1 or 2
|
|
local current = currentOptions()
|
|
local reg = (current.passives and 1 or 0) + (current.flyouts and 2 or 0) + (current.ranks and 4 or 0)
|
|
|
|
-- Everything is published: the eight settings sets (reg, three bits), each in one page (1)
|
|
-- or two (2), so settings and resize work in combat. In a set, the views are the same for
|
|
-- both modes; only their grouping into pages changes.
|
|
S.pageData, S.np, S.flyoutData = {}, {}, {}
|
|
local code = { "wipe(D) wipe(NUM_PAGES) wipe(V)" }
|
|
local largest = 0
|
|
for r = 0, 7 do
|
|
local opts = { passives = r % 2 == 1, flyouts = math.floor(r / 2) % 2 == 1, ranks = math.floor(r / 4) % 2 == 1 }
|
|
S.pageData[r], S.np[r] = { {}, {} }, { {}, {} }
|
|
-- category 0: search results for this settings set
|
|
local g = searchGroups[r]
|
|
for ci = (g and 0 or 1), #cats do
|
|
local cat = cats[ci]
|
|
local views
|
|
if ci == 0 then
|
|
views = paginateGroups(g)
|
|
else
|
|
views = paginate(cat.name, displayedList(cats, ci, opts))
|
|
end
|
|
for perPage = 1, 2 do
|
|
local nPages = math.max(1, math.ceil(#views / perPage))
|
|
S.np[r][perPage][ci] = nPages
|
|
S.pageData[r][perPage][ci] = {}
|
|
table.insert(code, ('NUM_PAGES["%d:%d:%d"] = %d'):format(r, perPage, ci, nPages))
|
|
for page = 1, nPages do
|
|
local data = { headers = {}, cells = {} }
|
|
S.pageData[r][perPage][ci][page] = data
|
|
local entries = {}
|
|
for slot = 1, perPage do
|
|
local n = 0
|
|
for _, el in ipairs(views[(page - 1) * perPage + slot] or {}) do
|
|
if el.header then
|
|
data.headers[slot] = data.headers[slot] or {}
|
|
table.insert(data.headers[slot], el)
|
|
else
|
|
n = n + 1
|
|
local k = (slot - 1) * G.cellsPerView + n
|
|
local spell = el.spell
|
|
data.cells[k] = spell
|
|
-- a group is published once, for both modes
|
|
if spell.flyout and not spell.fk then
|
|
table.insert(S.flyoutData, spell.members)
|
|
spell.fk = #S.flyoutData
|
|
local texts = {}
|
|
for _, m in ipairs(spell.members) do table.insert(texts, q(S.spellText(m))) end
|
|
table.insert(code, ("V[%d] = newtable(%s)"):format(spell.fk, table.concat(texts, ", ")))
|
|
largest = math.max(largest, #spell.members)
|
|
end
|
|
local castable = not (spell.passive or spell.flyout)
|
|
table.insert(entries, ("newtable(%d, %.14g, %d, %s, %s, %s, %s, %d)"):format(k,
|
|
(el.column - 1) * (G.cellW + G.gapX), -el.y,
|
|
q(castable and "spell" or ""), q(castable and S.spellText(spell) or ""),
|
|
q(spell.pet and "macro" or ""), q(spell.pet and ("/petautocasttoggle " .. spell.name) or ""),
|
|
spell.fk or 0))
|
|
end
|
|
end
|
|
end
|
|
table.insert(code, ('D["%d:%d:%d:%d"] = newtable(%s)'):format(r, perPage, ci, page, table.concat(entries, ", ")))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
ensureSmallButtons(largest)
|
|
if e.page > S.np[reg][mode][e.category] then e.page = S.np[reg][mode][e.category] end
|
|
-- RestrictedExecution keeps every snippet body it ran, compiled, for the whole session (its
|
|
-- factory is weak-keyed, but Lua 5.1 never collects string keys): the published data, a few
|
|
-- hundred kilobytes, is sent only when it changes, and the state in a small snippet of its own.
|
|
local body = table.concat(code, "\n")
|
|
if body ~= S.publishedBody then
|
|
S.ctrl:Execute(body)
|
|
S.publishedBody = body
|
|
end
|
|
-- Run "place" on EACH computation: the layout follows the setting even when it arrives
|
|
-- later (saved variables load after the file, so after the book is built).
|
|
S.ctrl:Execute(("NUM_TABS = %d CAT = %d PAGE = %d MODE = %d SETTINGS = %d control:RunAttribute(\"place\") control:RunAttribute(\"show\")")
|
|
:format(#cats, e.category, e.page, mode, reg))
|
|
end
|
|
|
|
-- ForeverUIVisuals: textures and texts for what the secure snippet just placed. Nothing
|
|
-- protected: this keeps the book readable in combat.
|
|
-- cat: category (0 = search); mode: 1 or 2 pages; reg: settings bits
|
|
function S.updateVisuals(cat, page, mode, reg)
|
|
local e = S.state
|
|
e.category, e.page = cat, page
|
|
mode, reg = mode or 2, reg or 0
|
|
-- the chosen mode and settings are saved, even when changed in combat (SetCVar is not
|
|
-- protected)
|
|
settings().reduced = (mode == 1) or nil
|
|
settings().hidePassives = (reg % 2 == 1) or nil
|
|
settings().noFlyouts = (math.floor(reg / 2) % 2 == 1) or nil
|
|
local ranks = math.floor(reg / 4) % 2 == 1
|
|
if ranks ~= (GetCVar("ShowAllSpellRanks") == "1") then
|
|
SetCVar("ShowAllSpellRanks", ranks and "1" or "0")
|
|
end
|
|
S.reg = reg
|
|
S.checkSettings(reg)
|
|
S.applySize(mode == 1)
|
|
e.pages = S.np and S.np[reg][mode][cat] or 1
|
|
highlightTabs(cat)
|
|
local data = S.pageData and S.pageData[reg][mode][cat] and S.pageData[reg][mode][cat][page]
|
|
-- headers: one per section of the view (plain frames, created as needed, allowed in
|
|
-- combat)
|
|
for slot, view in ipairs(S.views) do
|
|
local list = data and data.headers[slot] or {}
|
|
for i, el in ipairs(list) do
|
|
local h = view.headers[i]
|
|
if not h then
|
|
h = createHeader(view)
|
|
view.headers[i] = h
|
|
end
|
|
h.text:SetText(el.header)
|
|
h:ClearAllPoints()
|
|
h:SetPoint("TOPLEFT", view, "TOPLEFT", 0, -el.y)
|
|
h:Show()
|
|
end
|
|
for i = #list + 1, #view.headers do view.headers[i]:Hide() end
|
|
end
|
|
-- settings are grayed during a search (camelot disables them)
|
|
S.graySettings(cat == 0)
|
|
if S.onVisuals then S.onVisuals(cat) end
|
|
for k, c in ipairs(S.cells) do
|
|
local spell = data and data.cells[k]
|
|
c.spell = spell
|
|
if spell then fillCell(c, spell) end
|
|
end
|
|
S.pager.text:SetText(string.format(TEXT.page, e.page, e.pages))
|
|
end
|
|
|
|
-- state of the shown cells: cooldowns, autocast
|
|
function S.updateStates()
|
|
if not S.views then return end
|
|
if S.flyout and S.flyout:IsShown() then
|
|
for _, p in ipairs(S.flyout.buttons) do
|
|
if p:IsShown() then updateSmallButton(p) end
|
|
end
|
|
end
|
|
for _, view in ipairs(S.views) do
|
|
for _, c in ipairs(view.cells) do
|
|
if c:IsShown() and c.spell then S.updateState(c) end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ------------------------------------------------------------ Assembly
|
|
build()
|
|
|
|
if S.book then
|
|
-- movable by its title; in front when shown or clicked
|
|
ForeverUI.WindowStack.makeMovable(S.book, S.book.banner, "spellbook")
|
|
ForeverUI.WindowStack.fitToScreen("spellbook", SpellBookFrame, S.book, 0, G.top, G.fitExtraW, G.fitExtraH)
|
|
ForeverUI.WindowStack.register("spellbook", SpellBookFrame, function()
|
|
return { S.book, _G.ForeverUISpellFlyout, _G.ForeverUISpellBookSettingsList,
|
|
_G.ForeverUISpellBookSearchPreview }
|
|
end)
|
|
-- on each show: the requested book (spells or pet) and its content
|
|
SpellBookFrame:HookScript("OnShow", function()
|
|
S.suppressWotLK()
|
|
S.raiseCloseButton()
|
|
-- in combat the book opens as last published
|
|
if InCombatLockdown() then return end
|
|
if SpellBookFrame.bookType == BOOKTYPE_PET then
|
|
local cats = categories()
|
|
for i, cat in ipairs(cats) do
|
|
if cat.pet then S.state.category = i S.state.page = 1 end
|
|
end
|
|
elseif S.state.category and categories()[S.state.category] and categories()[S.state.category].pet then
|
|
S.state.category = 1
|
|
S.state.page = 1
|
|
end
|
|
S.update()
|
|
end)
|
|
if hooksecurefunc and SpellBookFrame_Update then
|
|
hooksecurefunc("SpellBookFrame_Update", S.suppressWotLK)
|
|
end
|
|
-- The book must be published BEFORE combat, shown or not, since it can open in combat. A
|
|
-- change marks it and the next frame recomputes it (SPELLS_CHANGED comes in bursts);
|
|
-- PLAYER_REGEN_DISABLED, the last moment before lockdown, catches what missed its frame.
|
|
local watcher = CreateFrame("Frame")
|
|
for _, ev in ipairs({ "PLAYER_ENTERING_WORLD", "SPELLS_CHANGED", "LEARNED_SPELL_IN_TAB", "PET_BAR_UPDATE",
|
|
"UNIT_PET", "SPELL_UPDATE_COOLDOWN", "CURRENT_SPELL_CAST_CHANGED", "PLAYER_REGEN_DISABLED",
|
|
"PLAYER_REGEN_ENABLED", "CVAR_UPDATE" }) do
|
|
watcher:RegisterEvent(ev)
|
|
end
|
|
local function onNextFrame()
|
|
watcher:SetScript("OnUpdate", nil)
|
|
if S.dirty and not InCombatLockdown() then S.update() end
|
|
end
|
|
function S.mark()
|
|
S.dirty = true
|
|
watcher:SetScript("OnUpdate", onNextFrame)
|
|
end
|
|
watcher:SetScript("OnEvent", function(_, ev, unit)
|
|
-- UNIT_PET fires for every party and raid member; only the player's pet changes the book
|
|
if ev == "UNIT_PET" and unit ~= "player" then return end
|
|
if ev == "SPELL_UPDATE_COOLDOWN" or ev == "CURRENT_SPELL_CAST_CHANGED" then
|
|
S.updateStates()
|
|
return
|
|
end
|
|
if ev == "PET_BAR_UPDATE" then S.updateStates() end
|
|
if ev == "PLAYER_REGEN_DISABLED" then
|
|
if S.dirty then S.update() end
|
|
return
|
|
end
|
|
if ev == "PLAYER_REGEN_ENABLED" then
|
|
if S.dirty or S.queued then S.update() end
|
|
return
|
|
end
|
|
S.mark()
|
|
end)
|
|
end
|
|
|
|
-- Print the book's size, mode and state to the chat.
|
|
ForeverUI.SpellBookDebug = function()
|
|
local prefix = "|cff66ccffForeverUI|r "
|
|
if not S.book then
|
|
DEFAULT_CHAT_FRAME:AddMessage(prefix .. L.SPELLBOOK_DEBUG_NOT_BUILT)
|
|
return
|
|
end
|
|
local cats = categories()
|
|
local e = S.state
|
|
local names = {}
|
|
for _, c in ipairs(cats) do table.insert(names, c.name .. " (" .. c.count .. ")") end
|
|
DEFAULT_CHAT_FRAME:AddMessage(prefix .. string.format(
|
|
L.SPELLBOOK_DEBUG_STATE,
|
|
S.book:GetWidth(), S.book:GetHeight(), settings().reduced and L.SPELLBOOK_DEBUG_ONE_PAGE or L.SPELLBOOK_DEBUG_TWO_PAGES,
|
|
e.category, #cats, e.page, e.pages or 1, table.concat(names, ", ")))
|
|
end
|