Files
Adrestie 5842eb7a2b ForeverUI : code en anglais, commentaires resserres, code mort retire
Traduction
- Identifiants en anglais dans tout le module : addon, ecrans d'accueil,
  outils Python, banc d'essai et patcheur C#.
- Fichiers au nom francais renommes (Gabarits.lua -> Templates.lua,
  Textes_*.lua -> Texts_*.lua, deployer.py -> deploy.py,
  tools/patcheur -> tools/patcher, etc.).
- Commandes /fui et options de deploy.py en anglais (--check, --server,
  --remove-dbc).
- Cles des variables sauvegardees et de data/dbc/statistics.json
  traduites, sans reprise des anciennes cles.
- API de la micro-barre : ForeverUI.AddMicroButton(def) avec les champs
  name, atlasSet, after, tooltip, onClick, ready, et
  ForeverUI.UpdateMicro(name, isOpen).
- Commentaires reecrits en anglais, courts, sans dates ni historique ;
  les fonctions au nom peu parlant et leurs parametres sont expliques.

Code mort
- Exports, fonctions et variables jamais lus retires.
- Retires a la demande des modules : ForeverUI.PvPJauge, les exports
  FondEntete / TuilerEntete / Polices de TradeSkill, l'art legacy-up-c60,
  honorfill(half)blue et inv_misc_book_09, leurs entrees d'atlas et les
  parties d'outils correspondantes.

Verifications : liaisons des noms comparees avant et apres, banc vert,
chemins de textures inchanges, generateurs reproduisant les fichiers,
patcheur identique octet pour octet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 09:15:52 +02:00

751 lines
26 KiB
Lua

-- ForeverUI: spell book search, after camelot's blizzard_spellsearch and
-- blizzard_spellbooksearch.lua. Search box, preview (5 rows; camelot: 3), full results as a
-- tab-less category (0) in sections, and the "Missing from action bar" filter.
-- Unlike camelot, results follow "Hide Passives" and "Show all spell ranks" (which stays
-- enabled while searching) and never group spells into flyouts.
-- No assisted combat: 3.3.5 lacks it.
ForeverUI = ForeverUI or {}
local S = ForeverUI.SpellBook
if not (S and S.book and S.pages) then return end
local R = {}
ForeverUI.SpellBookSearch = R
local L = ForeverUI.L
local TEXT = {
instruction = L.SPELLBOOKSEARCH_INSTRUCTIONS, -- SPELLBOOK_SEARCH_INSTRUCTIONS
notOnBar = L.SPELLBOOKSEARCH_NOT_ON_ACTIONBAR, -- SPELLBOOK_SEARCH_NOT_ON_ACTIONBAR
exact = L.SPELLBOOKSEARCH_HEADER_EXACT, -- SPELLBOOK_SEARCH_HEADER_EXACT
related = L.SPELLBOOKSEARCH_HEADER_RELATED, -- SPELLBOOK_SEARCH_HEADER_RELATED
name = L.SPELLBOOKSEARCH_HEADER_NAME, -- SPELLBOOK_SEARCH_HEADER_NAME
description = L.SPELLBOOKSEARCH_HEADER_DESCRIPTION, -- SPELLBOOK_SEARCH_HEADER_DESCRIPTION
generic = L.SPELLBOOKSEARCH_HEADER_GENERIC, -- SPELLBOOK_SEARCH_HEADER_GENERIC
overflow = L.SPELLBOOKSEARCH_PREVIEW_OVERFLOW, -- TALENT_FRAME_SEARCH_PREVIEW_OVERFLOW_FORMAT
passive = SPELL_PASSIVE,
}
local MIN_LETTERS = 3 -- MIN_CHARACTER_SEARCH
local M = {
boxW = 300, boxH = 30, boxX = -5, boxY = 4, tabsGap = 10, maxLetters = 40,
edgeW = 8, edgeH = 20, edgeX = -5, magnifier = 10, magnifierX = 1, magnifierY = -1, gray = 0.6,
clear = 17, clearX = -3, clearIcon = 10, clearIconX = 3, clearIconY = -3,
insetLeft = 16, insetRight = 20, instructionGray = 0.35,
previewX1 = 20, previewX2 = -3, previewY = 4, rowH = 27, rows = 5, topMargin = 1, bottomMargin = 3,
gap = 1, overflowH = 16, overflowY = 5, overflowX = 9,
iconFrame = 18, iconFrameX = 5, iconFrameY = 1, nameX = 5, nameY = 1, nameRight = -5,
suggMagnifier = 14, suggMagnifierX = 10, suggMagnifierY = 1, suggTextX = 10,
}
-- SpellSearchUtil.MatchType: higher is better
local T = { description = 1, name = 2, related = 3, exact = 4, absent = 5, inactiveStance = 6, disabledBar = 7 }
local SECTIONS = {
{ title = TEXT.exact, types = { [T.exact] = true } },
{ title = TEXT.related, types = { [T.related] = true } },
{ title = TEXT.name, types = { [T.name] = true } },
{ title = TEXT.description, types = { [T.description] = true } },
{ title = TEXT.generic, types = { [T.absent] = true, [T.inactiveStance] = true, [T.disabledBar] = true } },
}
-- ------------------------------------------------------------ Strings
-- DoStringsMatch / DoesStringContain: case-insensitive, literal
local function equals(a, b)
return a and b and string.lower(a) == string.lower(b)
end
local function contains(parent, sub)
return parent and sub and string.find(string.lower(parent), string.lower(sub), 1, true) ~= nil
end
-- ------------------------------------------------------------ Description
-- 3.3.5 has no GetSpellBookItemDescription: a hidden tooltip is read instead, and its last
-- non-empty line is the description.
local scanner = CreateFrame("GameTooltip", "ForeverUISpellBookScanTooltip", nil, "GameTooltipTemplate")
local descriptions = {}
local function description(spell)
local key = spell.book .. ":" .. spell.name .. ":" .. (spell.rank or "")
local d = descriptions[key]
if d == nil then
d = ""
scanner:SetOwner(WorldFrame, "ANCHOR_NONE")
scanner:ClearLines()
scanner:SetSpell(spell.slot, spell.book)
for i = scanner:NumLines(), 2, -1 do
local row = _G["ForeverUISpellBookScanTooltipTextLeft" .. i]
local t = row and row:GetText()
if t and t ~= "" then
d = t
break
end
end
scanner:Hide()
descriptions[key] = d
end
return d
end
R.description = description
-- ------------------------------------------------------------ State
R.state = nil -- { filter = "text" | "bars", text = ... }
-- Cache of the bar state and of the result sections per option set; R.invalidate clears it
local hidden = {}
function R.active() return R.state ~= nil end
function R.invalidate() hidden = {} end
-- Searchable elements: those of every category, in order
local function elements(cats, opts)
local list = {}
for ci = 1, #cats do
for _, el in ipairs(S.displayedList(cats, ci, opts)) do
table.insert(list, el)
end
end
return list
end
-- Sort keys of an element: book (spells before pet) and slot
local function bookRank(el)
local spell = el.flyout and el.members[1] or el
return (spell.book == BOOKTYPE_PET) and 1 or 0, spell.slot or 0
end
-- Spell subtitle (rank or "Passive"), matched like its name (extraSpellName)
local function subtitle(spell)
if spell.rank and spell.rank ~= "" then return spell.rank end
if spell.passive then return TEXT.passive end
end
-- GetMatchTypeForText for one spell. exactDesc: description of the spell named exactly
-- as the text (for related matches)
local function textMatchType(spell, text, exactDesc)
if equals(spell.name, text) then return T.exact end
if contains(spell.name, text) or contains(subtitle(spell), text) then return T.name end
if contains(description(spell), text) then return T.description end
if exactDesc and contains(exactDesc, spell.name) then return T.related end
end
-- A flyout group takes its best member: returns that type and member; fn: spell -> type
local function bestOf(el, fn)
if not el.flyout then return fn(el), el end
local best, bestMember
for _, m in ipairs(el.members) do
local t = fn(m)
if t and (not best or t > best) then best, bestMember = t, m end
end
return best, bestMember
end
-- ------------------------------------------------------------ Action bars
-- Appends the spell names of action slot `slot` to `names`
local function actionNames(slot, names)
local t, id, sub, global = GetActionInfo(slot)
if t ~= "spell" then return end
if global then names[#names + 1] = GetSpellInfo(global) end
if id then
if sub == BOOKTYPE_SPELL or sub == BOOKTYPE_PET then
names[#names + 1] = GetSpellName(id, sub)
else
names[#names + 1] = GetSpellInfo(id)
end
end
end
-- Spell names on active bars, disabled bars, inactive stance bars and the pet bar (cached).
-- 3.3.5 slots: 1-24 main bar (pages 1-2); 25-36 right, 37-48 left, 49-60 bottom right,
-- 61-72 bottom left (GetActionBarToggles order: bottom left, bottom right, right, left);
-- 73-120 stance bars, one active (GetBonusBarOffset).
local function barsState()
if hidden.bars then return hidden.bars end
local activeSet, disabled, inactive = {}, {}, {}
local bottomLeft, bottomRight, right, left = GetActionBarToggles()
local multi = { [3] = right, [4] = left, [5] = bottomRight, [6] = bottomLeft }
local stance = GetBonusBarOffset and GetBonusBarOffset() or 0
for slot = 1, 120 do
local names = {}
actionNames(slot, names)
for _, n in ipairs(names) do
if slot <= 24 then
activeSet[n] = true
elseif slot <= 72 then
local bar = math.floor((slot - 1) / 12) + 1
if multi[bar] then activeSet[n] = true else disabled[n] = true end
else
local bar = math.floor((slot - 73) / 12) + 1
if bar == stance then activeSet[n] = true else inactive[n] = true end
end
end
end
local pet = {}
for i = 1, (NUM_PET_ACTION_SLOTS or 10) do
local n = GetPetActionInfo(i)
if n then pet[n] = true end
end
hidden.bars = { activeSet = activeSet, disabled = disabled, inactive = inactive, pet = pet }
return hidden.bars
end
-- For the talent search (TalentsSearch.lua): bar state, read afresh
function R.bars()
hidden.bars = nil
return barsState()
end
-- Bar match type of a spell: nil if it is on an active bar, passive or an auto attack
local function barType(spell)
if spell.passive then return end
if IsAttackSpell and IsAttackSpell(spell.name) then return end
if IsAutoRepeatSpell and IsAutoRepeatSpell(spell.name) then return end
local b = barsState()
if spell.book == BOOKTYPE_PET then
if b.pet[spell.name] then return end
return T.absent
end
if b.activeSet[spell.name] then return end
if b.disabled[spell.name] then return T.disabledBar end
if b.inactive[spell.name] then return T.inactiveStance end
return T.absent
end
-- A group is missing only if none of its spells is on an active bar
local function elementBarType(el)
if not el.flyout then return barType(el) end
local best
for _, m in ipairs(el.members) do
local t = barType(m)
if not t then return end
if not best or t < best then best = t end
end
return best
end
-- ------------------------------------------------------------ Results
-- FullSearchResultSort: type (reversed for bars), active before passive, book, slot
local function sortBy(list, reversed)
table.sort(list, function(a, b)
if a.type ~= b.type then
if reversed then return a.type < b.type end
return a.type > b.type
end
local pa, pb = a.el.passive and 1 or 0, b.el.passive and 1 or 0
if pa ~= pb then return pa < pb end
local la, sa = bookRank(a.el)
local lb, sb = bookRank(b.el)
if la ~= lb then return la < lb end
return sa < sb
end)
end
-- Sections of the current search: { { title, spells }, ... }, cached per option set
function R.groups(cats, opts)
local key = (opts.passives and "p" or "a") .. (opts.flyouts and "v" or "s") .. (opts.ranks and "r" or "h")
if hidden[key] then return hidden[key] end
local state = R.state
local matches = {}
if state then
local all = elements(cats, opts)
if state.filter == "bars" then
for _, el in ipairs(all) do
local t = elementBarType(el)
if t then table.insert(matches, { el = el, type = t }) end
end
else
-- Description of the spell named exactly as the text (for related matches)
local exactDesc
for _, el in ipairs(all) do
local t, m = bestOf(el, function(s) return equals(s.name, state.text) and T.exact or nil end)
if t then
exactDesc = description(m)
break
end
end
for _, el in ipairs(all) do
local t = bestOf(el, function(s) return textMatchType(s, state.text, exactDesc) end)
if t then table.insert(matches, { el = el, type = t }) end
end
end
sortBy(matches, state.filter == "bars")
end
local groups = {}
for _, section in ipairs(SECTIONS) do
local spells = {}
for _, r in ipairs(matches) do
if section.types[r.type] then table.insert(spells, r.el) end
end
if #spells > 0 then table.insert(groups, { title = section.title, spells = spells }) end
end
hidden[key] = groups
return groups
end
-- Preview: name filter, exact or contained; a group shows its best spell (name and icon);
-- PreviewSearchResultSort
function R.preview(text)
local cats = S.categories()
local opts = S.currentOptions()
opts.flyouts = true -- the search never groups spells
local matches = {}
for _, el in ipairs(elements(cats, opts)) do
local t, m = bestOf(el, function(s)
if equals(s.name, text) then return T.exact end
if contains(s.name, text) then return T.name end
end)
if t then
local displayed = el
if el.flyout and m then displayed = m end
table.insert(matches, { el = el, type = t, name = displayed.name, icon = displayed.icon })
end
end
table.sort(matches, function(a, b)
if a.type ~= b.type then return a.type > b.type end
local la, sa = bookRank(a.el)
local lb, sb = bookRank(b.el)
if la ~= lb then return la < lb end
return sa < sb
end)
return matches
end
-- ------------------------------------------------------------ Search box
local box, clear, preview
-- EvaluateSearchText: the text, if it has at least 3 letters
local function evaluate()
local t = box:GetText() or ""
if string.len(t) >= MIN_LETTERS then return t end
end
local function hidePreview()
preview:Hide()
preview.highlighted = 0
end
-- Clear button: hidden out of combat. It is secure, so in combat it cannot hide and turns
-- transparent instead.
local function updateClearButton()
local visible = box:HasFocus() or (box:GetText() or "") ~= ""
if InCombatLockdown() then
clear:SetAlpha(visible and 1 or 0)
return
end
clear:SetAlpha(1)
if visible then clear:Show() else clear:Hide() end
end
-- SearchBoxTemplate: magnifier and instruction
local function updateMagnifier()
local active = box:HasFocus() or (box:GetText() or "") ~= ""
local g = active and 1 or M.gray
box.magnifier:SetVertexColor(g, g, g)
if (box:GetText() or "") == "" then box.instruction:Show() else box.instruction:Hide() end
updateClearButton()
end
-- SetFullResultSearch; a nil text leaves the search and goes back to the first tab
function R.find(text)
if InCombatLockdown() then return end
if not text then
R.quit(true)
return
end
if equals(text, TEXT.notOnBar) then
R.state = { filter = "bars" }
else
R.state = { filter = "text", text = text }
end
-- Set category 0 BEFORE updating, so the search shows at once (an update on a tab would
-- count as a tab click, which leaves the search)
S.state.category, S.state.page = 0, 1
S.update()
end
-- ClearActiveSearchState: text, focus, preview. toFirstTab: go back to the first tab if a
-- search was shown (false when a tab click leaves the search)
function R.quit(toFirstTab)
local wasShown = R.state ~= nil
R.state = nil
if box then
box:ClearFocus()
box:SetText("")
hidePreview()
updateMagnifier()
end
if wasShown and toFirstTab and not InCombatLockdown() then
S.choose(1)
end
end
-- ------------------------------------------------------------ Preview
-- Preview row button. 3.3.5 has no draw sublevels and draws a button's normal texture
-- (_search-rowbg) in ARTWORK, so the icon goes in OVERLAY and its border and highlight
-- (camelot: OVERLAY 2 and 3) go in a child frame above it.
local function resultRow(parent, i)
local l = CreateFrame("Button", "ForeverUISpellBookSearchResult" .. i, parent)
l:SetHeight(M.rowH)
local e = ForeverUI.AtlasEntry("_search-rowbg")
l:SetNormalTexture(e and e[1] or "")
ForeverUI.SetAtlas(l:GetNormalTexture(), "_search-rowbg", true)
l:SetPushedTexture(e and e[1] or "")
ForeverUI.SetAtlas(l:GetPushedTexture(), "_search-rowbg", true)
local hovered = CreateFrame("Frame", nil, l)
hovered:SetAllPoints(l)
hovered:SetFrameLevel(l:GetFrameLevel() + 1)
l.hovered = hovered
local highlighted = hovered:CreateTexture(nil, "OVERLAY")
ForeverUI.SetAtlas(highlighted, "search-highlight", true)
highlighted:SetAllPoints(l)
highlighted:SetBlendMode("ADD")
highlighted:Hide()
l.highlighted = highlighted
return l
end
-- Preview list under the box (SpellSearchPreviewContainerTemplate)
local function buildPreview()
local a = CreateFrame("Frame", "ForeverUISpellBookSearchPreview", S.pages)
a:SetFrameStrata("HIGH")
a:SetPoint("TOPLEFT", box, "BOTTOMLEFT", M.previewX1, M.previewY)
a:SetPoint("TOPRIGHT", box, "BOTTOMRIGHT", M.previewX2, M.previewY)
a:SetHeight(M.rowH)
a:EnableMouse(true)
a:Hide()
local background = a:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(background, "_search-rowbg", true)
background:SetAllPoints(a)
-- Border (UI-Frame-BotCorner*, _UI-Frame-Bot, !UI-Frame-*Tile)
local leftCorner = a:CreateTexture(nil, "OVERLAY")
ForeverUI.SetAtlas(leftCorner, "ui-frame-botcornerleft")
leftCorner:SetPoint("LEFT", a, "LEFT", -7, 0)
leftCorner:SetPoint("BOTTOM", a, "BOTTOM", 0, -7)
local rightCorner = a:CreateTexture(nil, "OVERLAY")
ForeverUI.SetAtlas(rightCorner, "ui-frame-botcornerright")
rightCorner:SetPoint("BOTTOM", leftCorner, "BOTTOM", 0, 0)
rightCorner:SetPoint("RIGHT", a, "RIGHT", 4, 0)
local down = a:CreateTexture(nil, "OVERLAY")
ForeverUI.SetAtlas(down, "_ui-frame-bot", true)
down:SetHeight(9)
down:SetPoint("BOTTOMLEFT", leftCorner, "BOTTOMRIGHT", 0, 0)
down:SetPoint("BOTTOMRIGHT", rightCorner, "BOTTOMLEFT", 0, 0)
local left = a:CreateTexture(nil, "OVERLAY")
ForeverUI.SetAtlas(left, "!ui-frame-lefttile", true)
left:SetWidth(16)
left:SetPoint("BOTTOMLEFT", leftCorner, "TOPLEFT", 0, 0)
left:SetPoint("TOPLEFT", a, "TOPLEFT", -7, 2)
local right = a:CreateTexture(nil, "OVERLAY")
ForeverUI.SetAtlas(right, "!ui-frame-righttile", true)
right:SetWidth(10)
right:SetPoint("BOTTOMRIGHT", rightCorner, "TOPRIGHT", 1, 0)
right:SetPoint("TOPRIGHT", a, "TOPRIGHT", 5, 2)
-- Result rows (SpellSearchPreviewResultTemplate)
a.rows = {}
for i = 1, M.rows do
local l = resultRow(a, i)
l:SetPoint("TOPLEFT", a, "TOPLEFT", 0, -M.topMargin - (i - 1) * (M.rowH + M.gap))
l:SetPoint("TOPRIGHT", a, "TOPRIGHT", 0, -M.topMargin - (i - 1) * (M.rowH + M.gap))
local frame = l.hovered:CreateTexture(nil, "ARTWORK")
ForeverUI.SetAtlas(frame, "talents-search-suggestion-itemborder")
frame:SetWidth(M.iconFrame)
frame:SetHeight(M.iconFrame)
frame:SetPoint("LEFT", l, "LEFT", M.iconFrameX, M.iconFrameY)
local icon = l:CreateTexture(nil, "OVERLAY")
icon:SetPoint("TOPLEFT", frame, "TOPLEFT", 1, -1)
icon:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -1, 1)
local name = l:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
name:SetJustifyH("LEFT")
name:SetPoint("LEFT", icon, "RIGHT", M.nameX, M.nameY)
name:SetPoint("RIGHT", l, "RIGHT", M.nameRight, 0)
l.icon, l.name = icon, name
l:SetScript("OnEnter", function() R.toggleHighlight(i) end)
l:SetScript("OnClick", function() R.choose(i) end)
l:Hide()
a.rows[i] = l
end
-- Suggestion (SpellSearchSuggestedResultButtonTemplate)
local sugg = resultRow(a, "Suggestion")
sugg:SetPoint("TOPLEFT", a, "TOPLEFT", 0, 0)
sugg:SetPoint("TOPRIGHT", a, "TOPRIGHT", 0, 0)
local magnifier = sugg:CreateTexture(nil, "OVERLAY")
ForeverUI.SetAtlas(magnifier, "talents-search-suggestion-magnifyingglass", true)
magnifier:SetWidth(M.suggMagnifier)
magnifier:SetHeight(M.suggMagnifier)
magnifier:SetPoint("LEFT", sugg, "LEFT", M.suggMagnifierX, M.suggMagnifierY)
local text = sugg:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
text:SetJustifyH("LEFT")
text:SetPoint("LEFT", magnifier, "RIGHT", M.suggTextX, 0)
text:SetPoint("RIGHT", sugg, "RIGHT", -5, 0)
text:SetText(TEXT.notOnBar)
sugg.text = text
sugg:SetScript("OnEnter", function() R.toggleHighlight(1) end)
sugg:SetScript("OnClick", function() R.choose(1) end)
sugg:Hide()
a.suggestion = sugg
-- Overflow line ("And %s more")
local plus = CreateFrame("Frame", nil, a)
plus:SetHeight(M.overflowH)
plus:SetPoint("BOTTOMLEFT", a, "BOTTOMLEFT", 0, M.overflowY)
plus:SetPoint("BOTTOMRIGHT", a, "BOTTOMRIGHT", 0, M.overflowY)
local moreText = plus:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
moreText:SetJustifyH("LEFT")
moreText:SetPoint("TOPLEFT", plus, "TOPLEFT", M.overflowX, 0)
moreText:SetPoint("BOTTOMRIGHT", plus, "BOTTOMRIGHT", 0, 0)
plus.text = moreText
plus:Hide()
a.overflow = plus
a.highlighted = 0
-- If the box loses focus during a click here, the preview stays for the click and hides
-- once the mouse leaves it
a:SetScript("OnUpdate", function(self)
if not box:HasFocus() and not MouseIsOver(self) then hidePreview() end
end)
return a
end
-- SetPreviewResults / UpdateResultsDisplay; text: nil below 3 letters
function R.updatePreview(text)
if InCombatLockdown() then return end
local a = preview
a.highlighted = 0
a.results = nil
for _, l in ipairs(a.rows) do l:Hide() l.highlighted:Hide() end
a.suggestion:Hide()
a.suggestion.highlighted:Hide()
a.overflow:Hide()
if not text then
-- Below 3 letters: the suggestion only (3.3.5 has no assisted combat)
a.suggestion:Show()
a.count = 1
a:SetHeight(M.rowH * 1 + M.bottomMargin)
a:Show()
return
end
local matches = R.preview(text)
if #matches == 0 then
hidePreview()
return
end
a.results = matches
local n = math.min(#matches, M.rows)
for i = 1, n do
local l = a.rows[i]
l.name:SetText(matches[i].name)
l.icon:SetTexture(matches[i].icon)
l:Show()
end
a.count = n
local h = M.topMargin + n * M.rowH + (n - 1) * M.gap + M.bottomMargin
if #matches > M.rows then
a.overflow.text:SetText(string.format(TEXT.overflow, #matches - M.rows))
a.overflow:Show()
h = h + M.overflowH
end
a:SetHeight(h)
a:Show()
end
-- HighlightPreviewResult
function R.toggleHighlight(i)
local a = preview
a.highlighted = i
if a.results then
for j, l in ipairs(a.rows) do
if j == i then l.highlighted:Show() else l.highlighted:Hide() end
end
else
if i == 1 then a.suggestion.highlighted:Show() else a.suggestion.highlighted:Hide() end
end
end
-- CycleHighlightedResultUp / Down (camelot's formula as is)
local function browse(direction)
local a = preview
if not a:IsShown() or not a.count or a.count == 0 then return end
local n, i = a.count, a.highlighted or 0
if direction < 0 then
i = (i - 2) % n + 1
else
i = i % n + 1
end
R.toggleHighlight(i)
end
-- SelectPreviewResult / suggestion click
function R.choose(i)
local a = preview
PlaySound("igMainMenuOptionCheckBoxOn")
if a.results then
local r = a.results[i]
if not r then return end
box:ClearFocus()
box:SetText(r.name)
hidePreview()
R.find(r.name)
else
box:ClearFocus()
box:SetText(TEXT.notOnBar)
hidePreview()
R.find(TEXT.notOnBar)
end
end
-- ------------------------------------------------------------ Build
local function build()
local b = CreateFrame("EditBox", "ForeverUISpellBookSearchBox", S.pages)
box = b
b:SetAutoFocus(false)
b:SetMaxLetters(M.maxLetters)
b:SetHeight(M.boxH)
b:SetWidth(M.boxW)
b:SetPoint("RIGHT", S.settings, "LEFT", M.boxX, M.boxY)
b:SetFontObject(GameFontHighlightSmall)
b:SetTextInsets(M.insetLeft, M.insetRight, 0, 0)
-- Border (InputBoxVisualTemplate)
local g = b:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(g, "common-search-border-left", true)
g:SetWidth(M.edgeW) g:SetHeight(M.edgeH)
g:SetPoint("LEFT", b, "LEFT", M.edgeX, 0)
local d = b:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(d, "common-search-border-right", true)
d:SetWidth(M.edgeW) d:SetHeight(M.edgeH)
d:SetPoint("RIGHT", b, "RIGHT", 0, 0)
local m = b:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(m, "common-search-border-middle", true)
m:SetHeight(M.edgeH)
m:SetPoint("LEFT", g, "RIGHT", 0, 0)
m:SetPoint("RIGHT", d, "LEFT", 0, 0)
-- Magnifier
local magnifier = b:CreateTexture(nil, "OVERLAY")
ForeverUI.SetAtlas(magnifier, "common-search-magnifyingglass", true)
magnifier:SetWidth(M.magnifier) magnifier:SetHeight(M.magnifier)
magnifier:SetPoint("LEFT", b, "LEFT", M.magnifierX, M.magnifierY)
b.magnifier = magnifier
-- Instruction text
local instruction = b:CreateFontString(nil, "ARTWORK", "GameFontDisableSmall")
instruction:SetJustifyH("LEFT")
instruction:SetJustifyV("MIDDLE")
instruction:SetPoint("TOPLEFT", b, "TOPLEFT", M.insetLeft, 0)
instruction:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", -M.insetRight, 0)
instruction:SetTextColor(M.instructionGray, M.instructionGray, M.instructionGray)
instruction:SetText(TEXT.instruction)
b.instruction = instruction
-- Clear button: secure, so it leaves the search in combat too
local e = CreateFrame("Button", "ForeverUISpellBookSearchClear", S.pages, "SecureHandlerClickTemplate")
clear = e
e:SetWidth(M.clear) e:SetHeight(M.clear)
e:SetPoint("RIGHT", b, "RIGHT", M.clearX, 0)
e:SetFrameLevel(b:GetFrameLevel() + 2)
local icon = e:CreateTexture(nil, "ARTWORK")
ForeverUI.SetAtlas(icon, "common-search-clearbutton", true)
icon:SetWidth(M.clearIcon) icon:SetHeight(M.clearIcon)
icon:SetPoint("TOPLEFT", e, "TOPLEFT", M.clearIconX, M.clearIconY)
icon:SetAlpha(0.5)
e.icon = icon
e:SetScript("OnEnter", function() icon:SetAlpha(1) end)
e:SetScript("OnLeave", function() icon:SetAlpha(0.5) end)
e:SetScript("OnMouseDown", function() icon:SetPoint("TOPLEFT", e, "TOPLEFT", M.clearIconX + 1, M.clearIconY - 1) end)
e:SetScript("OnMouseUp", function() icon:SetPoint("TOPLEFT", e, "TOPLEFT", M.clearIconX, M.clearIconY) end)
e:SetFrameRef("ctrl", S.ctrl)
e:SetAttribute("_onclick", [==[ self:GetFrameRef("ctrl"):SetAttribute("clear", 1) ]==])
-- SearchBoxTemplateClearButton_OnClick: sound, text, focus
e:HookScript("OnClick", function()
PlaySound("igMainMenuOptionCheckBoxOn")
R.quit(false)
end)
e:Hide()
preview = buildPreview()
-- SpellSearchBoxMixin
b:SetScript("OnEditFocusGained", function(self)
-- A search publishes pages, which only works out of combat: in combat the box drops focus
if InCombatLockdown() then
self:ClearFocus()
return
end
updateMagnifier()
R.updatePreview(evaluate())
end)
b:SetScript("OnEditFocusLost", function(self)
updateMagnifier()
if not MouseIsOver(preview) then hidePreview() end
end)
b:SetScript("OnTextChanged", function(self)
updateMagnifier()
if self:HasFocus() then R.updatePreview(evaluate()) end
end)
b:SetScript("OnEnterPressed", function(self)
local a = preview
if a:IsShown() and (a.highlighted or 0) > 0 then
R.choose(a.highlighted)
return
end
hidePreview()
R.find(evaluate())
self:ClearFocus()
end)
b:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
b:SetScript("OnKeyDown", function(self, pressedKey)
if pressedKey == "UP" then browse(-1) elseif pressedKey == "DOWN" then browse(1) end
end)
updateMagnifier()
end
build()
R.box, R.clear, R.previewFrame = box, clear, preview
-- A tab click leaves the search (ClearActiveSearchState, without going to the first tab);
-- nothing protected here, so it works in combat too
S.onVisuals = function(cat)
if cat ~= 0 and R.state then
R.quit(false)
end
end
-- ResizeSearchBox: between the tabs' right edge (+ 10) and the settings, 300 at most.
-- Out of combat only: a secure button is anchored to the box. reduced: book at reduced width
S.onResize = function(reduced)
if InCombatLockdown() or not box then return end
local G = S.G
local pagesW = reduced and G.reducedBookW or G.bookW
local nTabs = 0
for _, o in ipairs(S.tabs.buttons) do
if o:IsShown() then nTabs = nTabs + 1 end
end
local tabsRight = G.tabsX + nTabs * (G.tabW + G.tabGap) - G.tabGap
local boxRight = pagesW + G.settingsX - G.settingsW + M.boxX
box:SetWidth(math.min(boxRight - tabsRight - M.tabsGap, M.boxW))
end
-- Combat start: the box drops focus and the preview hides. Combat end: the clear button
-- gets its visibility back
local watcher = CreateFrame("Frame")
watcher:RegisterEvent("PLAYER_REGEN_DISABLED")
watcher:RegisterEvent("PLAYER_REGEN_ENABLED")
watcher:SetScript("OnEvent", function(_, ev)
if ev == "PLAYER_REGEN_DISABLED" then
box:ClearFocus()
hidePreview()
else
updateClearButton()
end
end)
-- When the bars change (spell placed or removed, page, stance), a "Missing from action bar"
-- search is redone once per frame (a drag fires several events). In combat, S.update waits
-- for combat to end.
local bars = CreateFrame("Frame")
bars:Hide()
for _, ev in ipairs({ "ACTIONBAR_SLOT_CHANGED", "ACTIONBAR_PAGE_CHANGED", "UPDATE_BONUS_ACTIONBAR",
"PET_BAR_UPDATE", "UPDATE_MULTI_ACTIONBAR" }) do
bars:RegisterEvent(ev)
end
bars:SetScript("OnEvent", function(self)
if R.state and R.state.filter == "bars" then self:Show() end
end)
bars:SetScript("OnUpdate", function(self)
self:Hide()
if R.state and R.state.filter == "bars" and S.book:IsVisible() then S.update() end
end)
R.barWatcher = bars