5842eb7a2b
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>
751 lines
26 KiB
Lua
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
|