eb681aa9cd
- socle : Textes.lua (ForeverUI.L) et ForeverUIGlueTextes.lua (G.L) ; les textes dans Textes_enUS / Textes_frFR (325 cles en jeu) et ForeverUIGlueTextes_enUS / _frFR (41 cles aux ecrans d'accueil) ; une cle absente prend l'anglais, une langue sans fichier lit l'anglais ; ajouter une langue = un fichier de plus - les textes que le client 3.3.5 connait se lisent dans ses globales (GlobalStrings, GlueStrings en glue) ; les textes de secours sont retires - tous les fichiers repris : libelles, infobulles, mode edition, messages et aide de /fui, erreurs internes ; .toc : Notes + Notes-frFR - selection des personnages : race et classe traduites ramenees a leur jeton par les tables de langue (GetAvailableRaces / GetAvailableClasses font planter le client hors de la creation) - tools/cartes_instances.py : noms des instances dans toutes les langues de la LibBabble-Zone de WDM (portails de la carte sur tout client) - banc : toute cle employee existe en anglais et en francais, memes cles dans les deux, bascule frFR / deDE essayee ; chaines 3.3.5 ajoutees au faux client - README : etat des sections mis a jour, langues, glue, deploiement --glue Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
810 lines
30 KiB
Lua
810 lines
30 KiB
Lua
-- ForeverUI : la recherche du grimoire de camelot (docs/GRIMOIRE.md,
|
|
-- etape 3).
|
|
--
|
|
-- RELEVE -- blizzard_spellsearch (util, filtres, controleur, gabarits) et
|
|
-- blizzard_playerspells/spellbook/blizzard_spellbooksearch.lua :
|
|
-- champ SpellSearchBoxTemplate < SearchBoxTemplate, 300 x 30, 40
|
|
-- lettres ; RIGHT sur le LEFT des reglages (-5, 4) ; largeur
|
|
-- min(droite du champ - droite des onglets - 10, 300) ; bord
|
|
-- common-search-border-left / -middle / -right (8 x 20, a -5) ;
|
|
-- loupe common-search-magnifyingglass 10 a LEFT (1, -1), grise
|
|
-- (0,6) sans focus ni texte ; bouton d'effacement 17 a RIGHT
|
|
-- (-3), icone common-search-clearbutton 10 a (3, -3), alpha
|
|
-- 0,5 (1 au survol, (4, -4) enfonce) ; consigne
|
|
-- SPELLBOOK_SEARCH_INSTRUCTIONS en GameFontDisableSmall
|
|
-- (0,35) de 16 a -20 ; texte GameFontHighlightSmall, marges
|
|
-- 16 / 20
|
|
-- apercu SpellSearchPreviewContainerTemplate : sous le champ, de
|
|
-- (20, 4) a (-3, 4) ; 3 lignes au plus de 27 (marges 1 / 3,
|
|
-- ICI 5, a la demande -- 2026-09-25 --
|
|
-- ecart 1), fond _search-rowbg, surbrillance search-highlight
|
|
-- en ADD, cadre d'icone talents-search-suggestion-itemborder
|
|
-- 18 a (5, 1), nom GameFontHighlightSmall ; "And %s more"
|
|
-- (TALENT_FRAME_SEARCH_PREVIEW_OVERFLOW_FORMAT) en
|
|
-- GameFontDisableSmall, 16 de haut ; bordure UI-Frame-BotCorner*
|
|
-- / _UI-Frame-Bot / !UI-Frame-*Tile ; sous 3 lettres, la
|
|
-- suggestion "Missing from action bar" (loupe 14 a (10, 1))
|
|
-- clavier haut / bas parcourent l'apercu, Entree choisit (ou lance la
|
|
-- recherche entiere), Echap rend le focus
|
|
-- correspond. sans casse, sous-chaine litterale ; 3 lettres au moins
|
|
-- (MIN_CHARACTER_SEARCH). Apercu : exact ou nom. Recherche
|
|
-- entiere : exact, nom (ou sous-titre : rang, "Passive"),
|
|
-- description, puis "apparente" (le nom apparait dans la
|
|
-- description du sort qui porte exactement le texte). Un
|
|
-- groupe prend le meilleur de ses sorts.
|
|
-- resultats une categorie sans onglet, en sections (Exact / Related /
|
|
-- Name / Description Matches ; "Matches" pour les barres) ;
|
|
-- tri : type, actifs avant passifs, livre, emplacement ;
|
|
-- aucun resultat : on
|
|
-- sort de la recherche. On en sort aussi par l'effacement,
|
|
-- Entree sous 3 lettres, ou un onglet. Les reglages y sont
|
|
-- desactives, sauf "Show all spell ranks" (voir plus bas).
|
|
-- barres "Missing from action bar" : ni passifs ni attaques
|
|
-- automatiques ; absent de toute barre, sur une barre
|
|
-- desactivee, sur une barre de posture inactive.
|
|
--
|
|
-- CE QUI DIFFERE, ET POURQUOI.
|
|
-- * l'apercu montre 5 resultats (camelot : 3), a la demande.
|
|
-- * une ligne de resultat : 3.3.5 n'a pas de sous-niveau de calque, et la
|
|
-- texture normale d'un bouton (le fond _search-rowbg) se dessine en
|
|
-- ARTWORK. L'icone y passait tantot dessous, tantot dessus : elle va en
|
|
-- OVERLAY, et son cadre et la surbrillance -- qui la recouvrent chez
|
|
-- camelot (OVERLAY 2 et 3) -- dans un cadre fils, au-dessus.
|
|
-- * 3.3.5 ne donne pas la description d'un sort : elle se lit dans une
|
|
-- infobulle cachee (sa derniere ligne).
|
|
-- * les resultats suivent "Hide Passives" et "Show all spell ranks", et
|
|
-- ce dernier reste actif pendant une recherche ; la recherche ne groupe
|
|
-- jamais les sorts en menus volants (demandes du 2026-09-25) : camelot y
|
|
-- montre passifs et tous les rangs, groupe selon son reglage, et
|
|
-- desactive ses reglages.
|
|
-- * pas de combat assiste (SPELLBOOK_SEARCH_ASSISTED_COMBAT) : 3.3.5 ne l'a
|
|
-- pas, sa suggestion n'est pas montree.
|
|
-- * EN COMBAT, le champ ne prend pas le focus : une recherche publie des
|
|
-- pages, ce qui ne se fait qu'hors combat. Des resultats deja affiches
|
|
-- se feuillettent en combat, et l'effacement ou un onglet en sortent
|
|
-- (par le controleur securise).
|
|
-- * le bouton d'effacement est securise (il sort de la recherche en combat)
|
|
-- : en combat, il ne peut plus se cacher, il devient transparent.
|
|
|
|
ForeverUI = ForeverUI or {}
|
|
|
|
local S = ForeverUI.SpellBook
|
|
if not (S and S.livre and S.pages) then return end
|
|
|
|
local R = {}
|
|
ForeverUI.SpellBookSearch = R
|
|
|
|
local SEP = string.char(92)
|
|
local L = ForeverUI.L
|
|
local TEXTE = {
|
|
consigne = L.SPELLBOOKSEARCH_INSTRUCTIONS, -- SPELLBOOK_SEARCH_INSTRUCTIONS
|
|
pasSurBarre = L.SPELLBOOKSEARCH_NOT_ON_ACTIONBAR, -- SPELLBOOK_SEARCH_NOT_ON_ACTIONBAR
|
|
exact = L.SPELLBOOKSEARCH_HEADER_EXACT, -- SPELLBOOK_SEARCH_HEADER_EXACT
|
|
apparente = L.SPELLBOOKSEARCH_HEADER_RELATED, -- SPELLBOOK_SEARCH_HEADER_RELATED
|
|
nom = L.SPELLBOOKSEARCH_HEADER_NAME, -- SPELLBOOK_SEARCH_HEADER_NAME
|
|
description = L.SPELLBOOKSEARCH_HEADER_DESCRIPTION, -- SPELLBOOK_SEARCH_HEADER_DESCRIPTION
|
|
generique = L.SPELLBOOKSEARCH_HEADER_GENERIC, -- SPELLBOOK_SEARCH_HEADER_GENERIC
|
|
depassement = L.SPELLBOOKSEARCH_PREVIEW_OVERFLOW, -- TALENT_FRAME_SEARCH_PREVIEW_OVERFLOW_FORMAT
|
|
passif = SPELL_PASSIVE,
|
|
}
|
|
local MIN_LETTRES = 3 -- MIN_CHARACTER_SEARCH
|
|
|
|
local M = {
|
|
boiteL = 300, boiteH = 30, boiteX = -5, boiteY = 4, ecartOnglets = 10, lettres = 40,
|
|
bordL = 8, bordH = 20, bordX = -5, loupe = 10, loupeX = 1, loupeY = -1, gris = 0.6,
|
|
effacer = 17, effacerX = -3, effacerIcone = 10, effacerIconeX = 3, effacerIconeY = -3,
|
|
margeG = 16, margeD = 20, consigneGris = 0.35,
|
|
apercuX1 = 20, apercuX2 = -3, apercuY = 4, ligneH = 27, lignes = 5, hautMarge = 1, basMarge = 3,
|
|
ecart = 1, depassementH = 16, depassementY = 5, depassementX = 9,
|
|
cadreIcone = 18, cadreIconeX = 5, cadreIconeY = 1, nomX = 5, nomY = 1, nomD = -5,
|
|
loupeSugg = 14, loupeSuggX = 10, loupeSuggY = 1, texteSuggX = 10,
|
|
}
|
|
|
|
-- SpellSearchUtil.MatchType : le plus grand est le meilleur
|
|
local T = { description = 1, nom = 2, apparente = 3, exact = 4, absent = 5, postureInactive = 6, barreDesactivee = 7 }
|
|
local SECTIONS = {
|
|
{ titre = TEXTE.exact, types = { [T.exact] = true } },
|
|
{ titre = TEXTE.apparente, types = { [T.apparente] = true } },
|
|
{ titre = TEXTE.nom, types = { [T.nom] = true } },
|
|
{ titre = TEXTE.description, types = { [T.description] = true } },
|
|
{ titre = TEXTE.generique, types = { [T.absent] = true, [T.postureInactive] = true, [T.barreDesactivee] = true } },
|
|
}
|
|
|
|
-- ------------------------------------------------------------ les chaines
|
|
-- DoStringsMatch / DoesStringContain : sans casse, litteral
|
|
local function egal(a, b)
|
|
return a and b and string.lower(a) == string.lower(b)
|
|
end
|
|
local function contient(parent, sous)
|
|
return parent and sous and string.find(string.lower(parent), string.lower(sous), 1, true) ~= nil
|
|
end
|
|
|
|
-- ------------------------------------------------------------ la description
|
|
-- 3.3.5 n'a pas GetSpellBookItemDescription : une infobulle cachee, dont la
|
|
-- derniere ligne est la description.
|
|
local lecteur = CreateFrame("GameTooltip", "ForeverUISpellBookScanTooltip", nil, "GameTooltipTemplate")
|
|
local descriptions = {}
|
|
local function description(sort)
|
|
local cle = sort.livre .. ":" .. sort.nom .. ":" .. (sort.rang or "")
|
|
local d = descriptions[cle]
|
|
if d == nil then
|
|
d = ""
|
|
lecteur:SetOwner(WorldFrame, "ANCHOR_NONE")
|
|
lecteur:ClearLines()
|
|
lecteur:SetSpell(sort.slot, sort.livre)
|
|
for i = lecteur:NumLines(), 2, -1 do
|
|
local ligne = _G["ForeverUISpellBookScanTooltipTextLeft" .. i]
|
|
local t = ligne and ligne:GetText()
|
|
if t and t ~= "" then
|
|
d = t
|
|
break
|
|
end
|
|
end
|
|
lecteur:Hide()
|
|
descriptions[cle] = d
|
|
end
|
|
return d
|
|
end
|
|
R.description = description
|
|
|
|
-- ------------------------------------------------------------ l'etat
|
|
R.etat = nil -- { filtre = "texte" | "barres", texte = ... }
|
|
local cache = {}
|
|
|
|
function R.active() return R.etat ~= nil end
|
|
function R.nouveauCalcul() cache = {} end
|
|
|
|
-- les elements cherchables : ceux des categories, dans leur ordre
|
|
local function elements(cats, opts)
|
|
local liste = {}
|
|
for ci = 1, #cats do
|
|
for _, el in ipairs(S.listeAffichee(cats, ci, opts)) do
|
|
table.insert(liste, el)
|
|
end
|
|
end
|
|
return liste
|
|
end
|
|
|
|
-- le livre (sorts avant familier) et l'emplacement d'un element
|
|
local function rangLivre(el)
|
|
local sort = el.volant and el.membres[1] or el
|
|
return (sort.livre == BOOKTYPE_PET) and 1 or 0, sort.slot or 0
|
|
end
|
|
|
|
-- le sous-titre d'un sort, qui compte comme son nom (extraSpellName)
|
|
local function sousTitre(sort)
|
|
if sort.rang and sort.rang ~= "" then return sort.rang end
|
|
if sort.passif then return TEXTE.passif end
|
|
end
|
|
|
|
-- GetMatchTypeForText, pour un sort
|
|
local function typeTexte(sort, texte, descExacte)
|
|
if egal(sort.nom, texte) then return T.exact end
|
|
if contient(sort.nom, texte) or contient(sousTitre(sort), texte) then return T.nom end
|
|
if contient(description(sort), texte) then return T.description end
|
|
if descExacte and contient(descExacte, sort.nom) then return T.apparente end
|
|
end
|
|
|
|
-- un groupe : le meilleur de ses sorts, et ce sort-la
|
|
local function meilleur(el, fn)
|
|
if not el.volant then return fn(el), el end
|
|
local best, lequel
|
|
for _, m in ipairs(el.membres) do
|
|
local t = fn(m)
|
|
if t and (not best or t > best) then best, lequel = t, m end
|
|
end
|
|
return best, lequel
|
|
end
|
|
|
|
-- ------------------------------------------------------------ les barres
|
|
-- GetActionBarStatusForSpell, en 3.3.5 : 1-24 la barre principale (pages 1
|
|
-- et 2) ; 25-36 droite, 37-48 gauche, 49-60 bas droite, 61-72 bas gauche
|
|
-- (GetActionBarToggles : bas gauche, bas droite, droite, gauche) ; 73-120
|
|
-- les barres de posture, une active (GetBonusBarOffset).
|
|
local function nomsDeLAction(slot, noms)
|
|
local t, id, sous, global = GetActionInfo(slot)
|
|
if t ~= "spell" then return end
|
|
if global then noms[#noms + 1] = GetSpellInfo(global) end
|
|
if id then
|
|
if sous == BOOKTYPE_SPELL or sous == BOOKTYPE_PET then
|
|
noms[#noms + 1] = GetSpellName(id, sous)
|
|
else
|
|
noms[#noms + 1] = GetSpellInfo(id)
|
|
end
|
|
end
|
|
end
|
|
|
|
local function etatDesBarres()
|
|
if cache.barres then return cache.barres end
|
|
local actives, desactivees, inactives = {}, {}, {}
|
|
local basG, basD, droite, gauche = GetActionBarToggles()
|
|
local multi = { [3] = droite, [4] = gauche, [5] = basD, [6] = basG }
|
|
local posture = GetBonusBarOffset and GetBonusBarOffset() or 0
|
|
for slot = 1, 120 do
|
|
local noms = {}
|
|
nomsDeLAction(slot, noms)
|
|
for _, n in ipairs(noms) do
|
|
if slot <= 24 then
|
|
actives[n] = true
|
|
elseif slot <= 72 then
|
|
local barre = math.floor((slot - 1) / 12) + 1
|
|
if multi[barre] then actives[n] = true else desactivees[n] = true end
|
|
else
|
|
local barre = math.floor((slot - 73) / 12) + 1
|
|
if barre == posture then actives[n] = true else inactives[n] = true end
|
|
end
|
|
end
|
|
end
|
|
local familier = {}
|
|
for i = 1, (NUM_PET_ACTION_SLOTS or 10) do
|
|
local n = GetPetActionInfo(i)
|
|
if n then familier[n] = true end
|
|
end
|
|
cache.barres = { actives = actives, desactivees = desactivees, inactives = inactives, familier = familier }
|
|
return cache.barres
|
|
end
|
|
|
|
-- pour la recherche des talents (TalentsSearch.lua) : les barres, relues
|
|
function R.barres()
|
|
cache.barres = nil
|
|
return etatDesBarres()
|
|
end
|
|
|
|
-- l'etat d'un sort : nil s'il est sur une barre active (ou exclu)
|
|
local function typeBarre(sort)
|
|
if sort.passif then return end
|
|
if IsAttackSpell and IsAttackSpell(sort.nom) then return end
|
|
if IsAutoRepeatSpell and IsAutoRepeatSpell(sort.nom) then return end
|
|
local b = etatDesBarres()
|
|
if sort.livre == BOOKTYPE_PET then
|
|
if b.familier[sort.nom] then return end
|
|
return T.absent
|
|
end
|
|
if b.actives[sort.nom] then return end
|
|
if b.desactivees[sort.nom] then return T.barreDesactivee end
|
|
if b.inactives[sort.nom] then return T.postureInactive end
|
|
return T.absent
|
|
end
|
|
|
|
-- un groupe n'est absent que si aucun de ses sorts n'est sur une barre active
|
|
local function typeBarreElement(el)
|
|
if not el.volant then return typeBarre(el) end
|
|
local best
|
|
for _, m in ipairs(el.membres) do
|
|
local t = typeBarre(m)
|
|
if not t then return end
|
|
if not best or t < best then best = t end
|
|
end
|
|
return best
|
|
end
|
|
|
|
-- ------------------------------------------------------------ les resultats
|
|
-- FullSearchResultSort : type (a rebours pour les barres), actifs avant
|
|
-- passifs, livre, emplacement
|
|
local function trier(liste, aRebours)
|
|
table.sort(liste, function(a, b)
|
|
if a.type ~= b.type then
|
|
if aRebours then return a.type < b.type end
|
|
return a.type > b.type
|
|
end
|
|
local pa, pb = a.el.passif and 1 or 0, b.el.passif and 1 or 0
|
|
if pa ~= pb then return pa < pb end
|
|
local la, sa = rangLivre(a.el)
|
|
local lb, sb = rangLivre(b.el)
|
|
if la ~= lb then return la < lb end
|
|
return sa < sb
|
|
end)
|
|
end
|
|
|
|
-- les sections de la recherche en cours : { { titre, sorts }, ... }
|
|
function R.groupes(cats, opts)
|
|
local cle = (opts.passifs and "p" or "a") .. (opts.volants and "v" or "s") .. (opts.rangs and "r" or "h")
|
|
if cache[cle] then return cache[cle] end
|
|
local etat = R.etat
|
|
local trouves = {}
|
|
if etat then
|
|
local tous = elements(cats, opts)
|
|
if etat.filtre == "barres" then
|
|
for _, el in ipairs(tous) do
|
|
local t = typeBarreElement(el)
|
|
if t then table.insert(trouves, { el = el, type = t }) end
|
|
end
|
|
else
|
|
-- la description du sort qui porte exactement le texte
|
|
local descExacte
|
|
for _, el in ipairs(tous) do
|
|
local t, m = meilleur(el, function(s) return egal(s.nom, etat.texte) and T.exact or nil end)
|
|
if t then
|
|
descExacte = description(m)
|
|
break
|
|
end
|
|
end
|
|
for _, el in ipairs(tous) do
|
|
local t = meilleur(el, function(s) return typeTexte(s, etat.texte, descExacte) end)
|
|
if t then table.insert(trouves, { el = el, type = t }) end
|
|
end
|
|
end
|
|
trier(trouves, etat.filtre == "barres")
|
|
end
|
|
local groupes = {}
|
|
for _, section in ipairs(SECTIONS) do
|
|
local sorts = {}
|
|
for _, r in ipairs(trouves) do
|
|
if section.types[r.type] then table.insert(sorts, r.el) end
|
|
end
|
|
if #sorts > 0 then table.insert(groupes, { titre = section.titre, sorts = sorts }) end
|
|
end
|
|
cache[cle] = groupes
|
|
return groupes
|
|
end
|
|
|
|
-- l'apercu : le filtre de nom, exact ou contenu ; un groupe montre son
|
|
-- meilleur sort (nom et icone) ; PreviewSearchResultSort
|
|
function R.apercu(texte)
|
|
local cats = S.categories()
|
|
local opts = S.optionsCourantes()
|
|
opts.volants = true -- jamais de groupes dans la recherche
|
|
local trouves = {}
|
|
for _, el in ipairs(elements(cats, opts)) do
|
|
local t, m = meilleur(el, function(s)
|
|
if egal(s.nom, texte) then return T.exact end
|
|
if contient(s.nom, texte) then return T.nom end
|
|
end)
|
|
if t then
|
|
local montre = el
|
|
if el.volant and m then montre = m end
|
|
table.insert(trouves, { el = el, type = t, nom = montre.nom, icone = montre.icone })
|
|
end
|
|
end
|
|
table.sort(trouves, function(a, b)
|
|
if a.type ~= b.type then return a.type > b.type end
|
|
local la, sa = rangLivre(a.el)
|
|
local lb, sb = rangLivre(b.el)
|
|
if la ~= lb then return la < lb end
|
|
return sa < sb
|
|
end)
|
|
return trouves
|
|
end
|
|
|
|
-- ------------------------------------------------------------ le champ
|
|
local boite, effacer, apercu
|
|
|
|
-- EvaluateSearchText : le texte, s'il a 3 lettres au moins
|
|
local function evaluer()
|
|
local t = boite:GetText() or ""
|
|
if string.len(t) >= MIN_LETTRES then return t end
|
|
end
|
|
|
|
local function cacherApercu()
|
|
apercu:Hide()
|
|
apercu.surligne = 0
|
|
end
|
|
|
|
-- le bouton d'effacement : cache hors combat, transparent en combat (il est
|
|
-- securise)
|
|
local function majEffacer()
|
|
local visible = boite:HasFocus() or (boite:GetText() or "") ~= ""
|
|
if InCombatLockdown() then
|
|
effacer:SetAlpha(visible and 1 or 0)
|
|
return
|
|
end
|
|
effacer:SetAlpha(1)
|
|
if visible then effacer:Show() else effacer:Hide() end
|
|
end
|
|
|
|
-- SearchBoxTemplate : loupe et consigne
|
|
local function majLoupe()
|
|
local actif = boite:HasFocus() or (boite:GetText() or "") ~= ""
|
|
local g = actif and 1 or M.gris
|
|
boite.loupe:SetVertexColor(g, g, g)
|
|
if (boite:GetText() or "") == "" then boite.consigne:Show() else boite.consigne:Hide() end
|
|
majEffacer()
|
|
end
|
|
|
|
-- SetFullResultSearch
|
|
function R.chercher(texte)
|
|
if InCombatLockdown() then return end
|
|
if not texte then
|
|
R.quitter(true)
|
|
return
|
|
end
|
|
if egal(texte, TEXTE.pasSurBarre) then
|
|
R.etat = { filtre = "barres" }
|
|
else
|
|
R.etat = { filtre = "texte", texte = texte }
|
|
end
|
|
-- la categorie 0 AVANT le calcul : il affiche aussitot l'etat voulu (un
|
|
-- affichage sur un onglet passerait pour un clic d'onglet, qui sort de
|
|
-- la recherche)
|
|
S.etat.categorie, S.etat.page = 0, 1
|
|
S.maj()
|
|
end
|
|
|
|
-- ClearActiveSearchState : le texte, le focus, l'apercu ; revenir au premier
|
|
-- onglet si l'on etait en recherche (sauf quand c'est un onglet qui en sort)
|
|
function R.quitter(versPremierOnglet)
|
|
local etait = R.etat ~= nil
|
|
R.etat = nil
|
|
if boite then
|
|
boite:ClearFocus()
|
|
boite:SetText("")
|
|
cacherApercu()
|
|
majLoupe()
|
|
end
|
|
if etait and versPremierOnglet and not InCombatLockdown() then
|
|
S.choisir(1)
|
|
end
|
|
end
|
|
|
|
-- ------------------------------------------------------------ l'apercu
|
|
local function ligneResultat(parent, i)
|
|
local l = CreateFrame("Button", "ForeverUISpellBookSearchResult" .. i, parent)
|
|
l:SetHeight(M.ligneH)
|
|
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 dessus = CreateFrame("Frame", nil, l)
|
|
dessus:SetAllPoints(l)
|
|
dessus:SetFrameLevel(l:GetFrameLevel() + 1)
|
|
l.dessus = dessus
|
|
local surligne = dessus:CreateTexture(nil, "OVERLAY")
|
|
ForeverUI.SetAtlas(surligne, "search-highlight", true)
|
|
surligne:SetAllPoints(l)
|
|
surligne:SetBlendMode("ADD")
|
|
surligne:Hide()
|
|
l.surligne = surligne
|
|
return l
|
|
end
|
|
|
|
local function construireApercu()
|
|
local a = CreateFrame("Frame", "ForeverUISpellBookSearchPreview", S.pages)
|
|
a:SetFrameStrata("HIGH")
|
|
a:SetPoint("TOPLEFT", boite, "BOTTOMLEFT", M.apercuX1, M.apercuY)
|
|
a:SetPoint("TOPRIGHT", boite, "BOTTOMRIGHT", M.apercuX2, M.apercuY)
|
|
a:SetHeight(M.ligneH)
|
|
a:EnableMouse(true)
|
|
a:Hide()
|
|
local fond = a:CreateTexture(nil, "BACKGROUND")
|
|
ForeverUI.SetAtlas(fond, "_search-rowbg", true)
|
|
fond:SetAllPoints(a)
|
|
-- la bordure (UI-Frame-BotCorner*, _UI-Frame-Bot, !UI-Frame-*Tile)
|
|
local coinG = a:CreateTexture(nil, "OVERLAY")
|
|
ForeverUI.SetAtlas(coinG, "ui-frame-botcornerleft")
|
|
coinG:SetPoint("LEFT", a, "LEFT", -7, 0)
|
|
coinG:SetPoint("BOTTOM", a, "BOTTOM", 0, -7)
|
|
local coinD = a:CreateTexture(nil, "OVERLAY")
|
|
ForeverUI.SetAtlas(coinD, "ui-frame-botcornerright")
|
|
coinD:SetPoint("BOTTOM", coinG, "BOTTOM", 0, 0)
|
|
coinD:SetPoint("RIGHT", a, "RIGHT", 4, 0)
|
|
local bas = a:CreateTexture(nil, "OVERLAY")
|
|
ForeverUI.SetAtlas(bas, "_ui-frame-bot", true)
|
|
bas:SetHeight(9)
|
|
bas:SetPoint("BOTTOMLEFT", coinG, "BOTTOMRIGHT", 0, 0)
|
|
bas:SetPoint("BOTTOMRIGHT", coinD, "BOTTOMLEFT", 0, 0)
|
|
local gauche = a:CreateTexture(nil, "OVERLAY")
|
|
ForeverUI.SetAtlas(gauche, "!ui-frame-lefttile", true)
|
|
gauche:SetWidth(16)
|
|
gauche:SetPoint("BOTTOMLEFT", coinG, "TOPLEFT", 0, 0)
|
|
gauche:SetPoint("TOPLEFT", a, "TOPLEFT", -7, 2)
|
|
local droite = a:CreateTexture(nil, "OVERLAY")
|
|
ForeverUI.SetAtlas(droite, "!ui-frame-righttile", true)
|
|
droite:SetWidth(10)
|
|
droite:SetPoint("BOTTOMRIGHT", coinD, "TOPRIGHT", 1, 0)
|
|
droite:SetPoint("TOPRIGHT", a, "TOPRIGHT", 5, 2)
|
|
|
|
-- les lignes de resultat (SpellSearchPreviewResultTemplate)
|
|
a.lignes = {}
|
|
for i = 1, M.lignes do
|
|
local l = ligneResultat(a, i)
|
|
l:SetPoint("TOPLEFT", a, "TOPLEFT", 0, -M.hautMarge - (i - 1) * (M.ligneH + M.ecart))
|
|
l:SetPoint("TOPRIGHT", a, "TOPRIGHT", 0, -M.hautMarge - (i - 1) * (M.ligneH + M.ecart))
|
|
local cadre = l.dessus:CreateTexture(nil, "ARTWORK")
|
|
ForeverUI.SetAtlas(cadre, "talents-search-suggestion-itemborder")
|
|
cadre:SetWidth(M.cadreIcone)
|
|
cadre:SetHeight(M.cadreIcone)
|
|
cadre:SetPoint("LEFT", l, "LEFT", M.cadreIconeX, M.cadreIconeY)
|
|
local icone = l:CreateTexture(nil, "OVERLAY")
|
|
icone:SetPoint("TOPLEFT", cadre, "TOPLEFT", 1, -1)
|
|
icone:SetPoint("BOTTOMRIGHT", cadre, "BOTTOMRIGHT", -1, 1)
|
|
local nom = l:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
|
|
nom:SetJustifyH("LEFT")
|
|
nom:SetPoint("LEFT", icone, "RIGHT", M.nomX, M.nomY)
|
|
nom:SetPoint("RIGHT", l, "RIGHT", M.nomD, 0)
|
|
l.icone, l.nom = icone, nom
|
|
l:SetScript("OnEnter", function() R.surligner(i) end)
|
|
l:SetScript("OnClick", function() R.choisir(i) end)
|
|
l:Hide()
|
|
a.lignes[i] = l
|
|
end
|
|
-- la suggestion (SpellSearchSuggestedResultButtonTemplate)
|
|
local sugg = ligneResultat(a, "Suggestion")
|
|
sugg:SetPoint("TOPLEFT", a, "TOPLEFT", 0, 0)
|
|
sugg:SetPoint("TOPRIGHT", a, "TOPRIGHT", 0, 0)
|
|
local loupe = sugg:CreateTexture(nil, "OVERLAY")
|
|
ForeverUI.SetAtlas(loupe, "talents-search-suggestion-magnifyingglass", true)
|
|
loupe:SetWidth(M.loupeSugg)
|
|
loupe:SetHeight(M.loupeSugg)
|
|
loupe:SetPoint("LEFT", sugg, "LEFT", M.loupeSuggX, M.loupeSuggY)
|
|
local texte = sugg:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
|
|
texte:SetJustifyH("LEFT")
|
|
texte:SetPoint("LEFT", loupe, "RIGHT", M.texteSuggX, 0)
|
|
texte:SetPoint("RIGHT", sugg, "RIGHT", -5, 0)
|
|
texte:SetText(TEXTE.pasSurBarre)
|
|
sugg.texte = texte
|
|
sugg:SetScript("OnEnter", function() R.surligner(1) end)
|
|
sugg:SetScript("OnClick", function() R.choisir(1) end)
|
|
sugg:Hide()
|
|
a.suggestion = sugg
|
|
-- le depassement
|
|
local plus = CreateFrame("Frame", nil, a)
|
|
plus:SetHeight(M.depassementH)
|
|
plus:SetPoint("BOTTOMLEFT", a, "BOTTOMLEFT", 0, M.depassementY)
|
|
plus:SetPoint("BOTTOMRIGHT", a, "BOTTOMRIGHT", 0, M.depassementY)
|
|
local plusTexte = plus:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
|
|
plusTexte:SetJustifyH("LEFT")
|
|
plusTexte:SetPoint("TOPLEFT", plus, "TOPLEFT", M.depassementX, 0)
|
|
plusTexte:SetPoint("BOTTOMRIGHT", plus, "BOTTOMRIGHT", 0, 0)
|
|
plus.texte = plusTexte
|
|
plus:Hide()
|
|
a.depassement = plus
|
|
a.surligne = 0
|
|
-- le champ a perdu le focus pendant qu'on cliquait ici : l'apercu reste
|
|
-- le temps du clic, puis part des que la souris le quitte
|
|
a:SetScript("OnUpdate", function(self)
|
|
if not boite:HasFocus() and not MouseIsOver(self) then cacherApercu() end
|
|
end)
|
|
return a
|
|
end
|
|
|
|
-- SetPreviewResults / UpdateResultsDisplay
|
|
function R.majApercu(texte)
|
|
if InCombatLockdown() then return end
|
|
local a = apercu
|
|
a.surligne = 0
|
|
a.resultats = nil
|
|
for _, l in ipairs(a.lignes) do l:Hide() l.surligne:Hide() end
|
|
a.suggestion:Hide()
|
|
a.suggestion.surligne:Hide()
|
|
a.depassement:Hide()
|
|
if not texte then
|
|
-- sous 3 lettres : la suggestion (le combat assiste n'existe pas ici)
|
|
a.suggestion:Show()
|
|
a.nombre = 1
|
|
a:SetHeight(M.ligneH * 1 + M.basMarge)
|
|
a:Show()
|
|
return
|
|
end
|
|
local trouves = R.apercu(texte)
|
|
if #trouves == 0 then
|
|
cacherApercu()
|
|
return
|
|
end
|
|
a.resultats = trouves
|
|
local n = math.min(#trouves, M.lignes)
|
|
for i = 1, n do
|
|
local l = a.lignes[i]
|
|
l.nom:SetText(trouves[i].nom)
|
|
l.icone:SetTexture(trouves[i].icone)
|
|
l:Show()
|
|
end
|
|
a.nombre = n
|
|
local h = M.hautMarge + n * M.ligneH + (n - 1) * M.ecart + M.basMarge
|
|
if #trouves > M.lignes then
|
|
a.depassement.texte:SetText(string.format(TEXTE.depassement, #trouves - M.lignes))
|
|
a.depassement:Show()
|
|
h = h + M.depassementH
|
|
end
|
|
a:SetHeight(h)
|
|
a:Show()
|
|
end
|
|
|
|
-- HighlightPreviewResult
|
|
function R.surligner(i)
|
|
local a = apercu
|
|
a.surligne = i
|
|
if a.resultats then
|
|
for j, l in ipairs(a.lignes) do
|
|
if j == i then l.surligne:Show() else l.surligne:Hide() end
|
|
end
|
|
else
|
|
if i == 1 then a.suggestion.surligne:Show() else a.suggestion.surligne:Hide() end
|
|
end
|
|
end
|
|
|
|
-- CycleHighlightedResultUp / Down (la formule de camelot, telle quelle)
|
|
local function parcourir(sens)
|
|
local a = apercu
|
|
if not a:IsShown() or not a.nombre or a.nombre == 0 then return end
|
|
local n, i = a.nombre, a.surligne or 0
|
|
if sens < 0 then
|
|
i = (i - 2) % n + 1
|
|
else
|
|
i = i % n + 1
|
|
end
|
|
R.surligner(i)
|
|
end
|
|
|
|
-- SelectPreviewResult / le clic d'une suggestion
|
|
function R.choisir(i)
|
|
local a = apercu
|
|
PlaySound("igMainMenuOptionCheckBoxOn")
|
|
if a.resultats then
|
|
local r = a.resultats[i]
|
|
if not r then return end
|
|
boite:ClearFocus()
|
|
boite:SetText(r.nom)
|
|
cacherApercu()
|
|
R.chercher(r.nom)
|
|
else
|
|
boite:ClearFocus()
|
|
boite:SetText(TEXTE.pasSurBarre)
|
|
cacherApercu()
|
|
R.chercher(TEXTE.pasSurBarre)
|
|
end
|
|
end
|
|
|
|
-- ------------------------------------------------------------ construction
|
|
local function construire()
|
|
local b = CreateFrame("EditBox", "ForeverUISpellBookSearchBox", S.pages)
|
|
boite = b
|
|
b:SetAutoFocus(false)
|
|
b:SetMaxLetters(M.lettres)
|
|
b:SetHeight(M.boiteH)
|
|
b:SetWidth(M.boiteL)
|
|
b:SetPoint("RIGHT", S.reglages, "LEFT", M.boiteX, M.boiteY)
|
|
b:SetFontObject(GameFontHighlightSmall)
|
|
b:SetTextInsets(M.margeG, M.margeD, 0, 0)
|
|
-- le bord (InputBoxVisualTemplate)
|
|
local g = b:CreateTexture(nil, "BACKGROUND")
|
|
ForeverUI.SetAtlas(g, "common-search-border-left", true)
|
|
g:SetWidth(M.bordL) g:SetHeight(M.bordH)
|
|
g:SetPoint("LEFT", b, "LEFT", M.bordX, 0)
|
|
local d = b:CreateTexture(nil, "BACKGROUND")
|
|
ForeverUI.SetAtlas(d, "common-search-border-right", true)
|
|
d:SetWidth(M.bordL) d:SetHeight(M.bordH)
|
|
d:SetPoint("RIGHT", b, "RIGHT", 0, 0)
|
|
local m = b:CreateTexture(nil, "BACKGROUND")
|
|
ForeverUI.SetAtlas(m, "common-search-border-middle", true)
|
|
m:SetHeight(M.bordH)
|
|
m:SetPoint("LEFT", g, "RIGHT", 0, 0)
|
|
m:SetPoint("RIGHT", d, "LEFT", 0, 0)
|
|
-- la loupe
|
|
local loupe = b:CreateTexture(nil, "OVERLAY")
|
|
ForeverUI.SetAtlas(loupe, "common-search-magnifyingglass", true)
|
|
loupe:SetWidth(M.loupe) loupe:SetHeight(M.loupe)
|
|
loupe:SetPoint("LEFT", b, "LEFT", M.loupeX, M.loupeY)
|
|
b.loupe = loupe
|
|
-- la consigne
|
|
local consigne = b:CreateFontString(nil, "ARTWORK", "GameFontDisableSmall")
|
|
consigne:SetJustifyH("LEFT")
|
|
consigne:SetJustifyV("MIDDLE")
|
|
consigne:SetPoint("TOPLEFT", b, "TOPLEFT", M.margeG, 0)
|
|
consigne:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", -M.margeD, 0)
|
|
consigne:SetTextColor(M.consigneGris, M.consigneGris, M.consigneGris)
|
|
consigne:SetText(TEXTE.consigne)
|
|
b.consigne = consigne
|
|
|
|
-- l'effacement : securise, il sort de la recherche en combat aussi
|
|
local e = CreateFrame("Button", "ForeverUISpellBookSearchClear", S.pages, "SecureHandlerClickTemplate")
|
|
effacer = e
|
|
e:SetWidth(M.effacer) e:SetHeight(M.effacer)
|
|
e:SetPoint("RIGHT", b, "RIGHT", M.effacerX, 0)
|
|
e:SetFrameLevel(b:GetFrameLevel() + 2)
|
|
local icone = e:CreateTexture(nil, "ARTWORK")
|
|
ForeverUI.SetAtlas(icone, "common-search-clearbutton", true)
|
|
icone:SetWidth(M.effacerIcone) icone:SetHeight(M.effacerIcone)
|
|
icone:SetPoint("TOPLEFT", e, "TOPLEFT", M.effacerIconeX, M.effacerIconeY)
|
|
icone:SetAlpha(0.5)
|
|
e.icone = icone
|
|
e:SetScript("OnEnter", function() icone:SetAlpha(1) end)
|
|
e:SetScript("OnLeave", function() icone:SetAlpha(0.5) end)
|
|
e:SetScript("OnMouseDown", function() icone:SetPoint("TOPLEFT", e, "TOPLEFT", M.effacerIconeX + 1, M.effacerIconeY - 1) end)
|
|
e:SetScript("OnMouseUp", function() icone:SetPoint("TOPLEFT", e, "TOPLEFT", M.effacerIconeX, M.effacerIconeY) end)
|
|
e:SetFrameRef("ctrl", S.ctrl)
|
|
e:SetAttribute("_onclick", [==[ self:GetFrameRef("ctrl"):SetAttribute("effacer", 1) ]==])
|
|
-- SearchBoxTemplateClearButton_OnClick : le son, le texte, le focus
|
|
e:HookScript("OnClick", function()
|
|
PlaySound("igMainMenuOptionCheckBoxOn")
|
|
R.quitter(false)
|
|
end)
|
|
e:Hide()
|
|
|
|
apercu = construireApercu()
|
|
|
|
-- SpellSearchBoxMixin
|
|
b:SetScript("OnEditFocusGained", function(self)
|
|
-- en combat, rien a publier : le champ rend le focus
|
|
if InCombatLockdown() then
|
|
self:ClearFocus()
|
|
return
|
|
end
|
|
majLoupe()
|
|
R.majApercu(evaluer())
|
|
end)
|
|
b:SetScript("OnEditFocusLost", function(self)
|
|
majLoupe()
|
|
if not MouseIsOver(apercu) then cacherApercu() end
|
|
end)
|
|
b:SetScript("OnTextChanged", function(self)
|
|
majLoupe()
|
|
if self:HasFocus() then R.majApercu(evaluer()) end
|
|
end)
|
|
b:SetScript("OnEnterPressed", function(self)
|
|
local a = apercu
|
|
if a:IsShown() and (a.surligne or 0) > 0 then
|
|
R.choisir(a.surligne)
|
|
return
|
|
end
|
|
cacherApercu()
|
|
R.chercher(evaluer())
|
|
self:ClearFocus()
|
|
end)
|
|
b:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
|
|
b:SetScript("OnKeyDown", function(self, touche)
|
|
if touche == "UP" then parcourir(-1) elseif touche == "DOWN" then parcourir(1) end
|
|
end)
|
|
majLoupe()
|
|
end
|
|
|
|
construire()
|
|
R.boite, R.effacer, R.apercuCadre = boite, effacer, apercu
|
|
|
|
-- un onglet sort de la recherche (ClearActiveSearchState, sans revenir au
|
|
-- premier onglet) : en combat aussi, rien de protege ici
|
|
S.surVisuels = function(cat)
|
|
if cat ~= 0 and R.etat then
|
|
R.quitter(false)
|
|
end
|
|
end
|
|
|
|
-- ResizeSearchBox : entre la droite des onglets (+ 10) et les reglages, 300
|
|
-- au plus. Hors combat seulement : le champ porte un bouton securise ancre
|
|
-- a lui.
|
|
S.surTaille = function(reduit)
|
|
if InCombatLockdown() or not boite then return end
|
|
local G = S.G
|
|
local pagesL = reduit and G.livreLReduit or G.livreL
|
|
local nOnglets = 0
|
|
for _, o in ipairs(S.onglets.boutons) do
|
|
if o:IsShown() then nOnglets = nOnglets + 1 end
|
|
end
|
|
local droiteOnglets = G.ongletsX + nOnglets * (G.ongletL + G.ongletEcart) - G.ongletEcart
|
|
local droiteBoite = pagesL + G.reglagesX - G.reglagesL + M.boiteX
|
|
boite:SetWidth(math.min(droiteBoite - droiteOnglets - M.ecartOnglets, M.boiteL))
|
|
end
|
|
|
|
-- le combat commence : le champ rend le focus, l'apercu part ; il finit :
|
|
-- l'effacement reprend sa visibilite
|
|
local veille = CreateFrame("Frame")
|
|
veille:RegisterEvent("PLAYER_REGEN_DISABLED")
|
|
veille:RegisterEvent("PLAYER_REGEN_ENABLED")
|
|
veille:SetScript("OnEvent", function(_, ev)
|
|
if ev == "PLAYER_REGEN_DISABLED" then
|
|
boite:ClearFocus()
|
|
cacherApercu()
|
|
else
|
|
majEffacer()
|
|
end
|
|
end)
|
|
|
|
-- LES BARRES CHANGENT (un sort pose, retire, une page ou une posture) : une
|
|
-- recherche "Missing from action bar" se refait (demande du 2026-09-25) --
|
|
-- une fois par image, un glisser en declenchant plusieurs ; en combat, S.maj
|
|
-- attend la fin du combat, comme pour tout le reste
|
|
local barres = CreateFrame("Frame")
|
|
barres:Hide()
|
|
for _, ev in ipairs({ "ACTIONBAR_SLOT_CHANGED", "ACTIONBAR_PAGE_CHANGED", "UPDATE_BONUS_ACTIONBAR",
|
|
"PET_BAR_UPDATE", "UPDATE_MULTI_ACTIONBAR" }) do
|
|
barres:RegisterEvent(ev)
|
|
end
|
|
barres:SetScript("OnEvent", function(self)
|
|
if R.etat and R.etat.filtre == "barres" then self:Show() end
|
|
end)
|
|
barres:SetScript("OnUpdate", function(self)
|
|
self:Hide()
|
|
if R.etat and R.etat.filtre == "barres" and S.livre:IsVisible() then S.maj() end
|
|
end)
|
|
R.veilleBarres = barres
|