10ce15481b
Reprise du checkFit de Camelot (marges 200 x 140 de PlayerSpellsFrame) : la fenêtre est réduite jusqu'à tenir dans l'écran, position par défaut bornée comme ClampUIPanelY. Positions enregistrées en unités d'écran. Hors combat seulement. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1757 lines
67 KiB
Lua
1757 lines
67 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)
|
|
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)
|
|
local flyout = CreateFrame("Frame", "ForeverUISpellFlyout", book)
|
|
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)
|
|
if close then
|
|
size:SetPoint("RIGHT", close, "LEFT", 0, 0)
|
|
else
|
|
size:SetPoint("TOPRIGHT", book, "TOPRIGHT", G.closeX - G.redSide, G.closeY)
|
|
end
|
|
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
|
|
-- 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).
|
|
table.insert(code, ("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))
|
|
S.ctrl:Execute(table.concat(code, "\n"))
|
|
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)
|
|
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
|