Files
moon-well-ui/data/addon/ForeverUI/TalentsSearch.lua
T
Adrestie cc7d2d45fa Talents camelot, glyphes refaits, cadre du familier, superposition des fenetres
- Talents : trois arbres cote a cote facon camelot sur PlayerTalentFrame,
  noeuds carres/ronds (TalentsData.lua genere des DBC), fleches, portes,
  changements en attente (Apply / Undo), onglets lateraux des specialisations,
  Activate, talents du familier en fenetre reduite, recherche (marques sur les
  noeuds, apercu, options, absents des barres), confirmation a la fermeture,
  glisser un sort de talent appris vers une barre
- Glyphes : page refaite sur l'art fourni (tools/cuire_glyphes.py), lueurs,
  etincelles, cercles concentriques tournants ; le GlyphFrame de WotLK reste
  sous PlayerTalentFrame (le reparenter plantait le client a la deconnexion)
- Cadre du familier facon camelot (PetFrame.lua)
- Superposition.lua : feuille, grimoire et talents passent entierement l'un
  devant l'autre, grimoire et talents deplacables
- Grimoire : la recherche "Missing from action bar" suit les barres

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 18:12:34 +02:00

802 lines
30 KiB
Lua

-- ForeverUI : la recherche des talents de camelot (docs/TALENTS.md, etape 4).
--
-- RELEVE -- blizzard_playerspells (camelot/classtalents), blizzard_sharedtalentui
-- (classtalentsearch, talentbuttonart, sharedtalentbuttontemplates,
-- sharedtalentutil) et blizzard_spellsearch (filtres, gabarits) :
-- champ SpellSearchBoxTemplate 184 x 30, 40 lettres ; consigne SEARCH
-- (SearchBoxTemplate) ; loupe, bord et effacement comme le
-- grimoire (ForeverUI SpellBookSearch.lua)
-- options SearchOptionsDropdown (WowStyle1ArrowDropdownTemplate 25 x 25,
-- fleche common-dropdown-a-button et ses etats) a RIGHT du champ
-- (3, -2) ; deux cases : CLASS_TALENT_SEARCH_OPTION_HIDE_PASSIVES
-- "Hide Passives" et _SHOW_RANKS "Show Ranks", non retenues ;
-- elles ne touchent QUE l'apercu (TransformPreviewResults) :
-- passifs retires, "Nom (rang/max)"
-- apercu SpellSearchPreviewContainerTemplate 176 de large, TOPRIGHT
-- sur le BOTTOMRIGHT du champ (-4, 2) ; filtre de NOM (exact,
-- contenu) ; la suggestion TALENT_FRAME_SEARCH_NOT_ON_ACTIONBAR
-- "Missing from action bar" sous 3 lettres
-- recherche filtre de TEXTE (GetMatchTypeForText) : exact, nom,
-- description, puis "apparente" (le nom du talent figure dans
-- la description du talent qui porte exactement le texte) ;
-- ou le filtre des BARRES : un talent appris, sort actif,
-- absent d'une barre active
-- marques pas de liste : chaque noeud trouve porte SearchIcon, 63 x 63
-- (talents-search-*, taille d'atlas), CENTER sur le TOPRIGHT de
-- son icone, au-dessus du noeud ; OverlayIcon, la meme en ADD,
-- bat de 0 a 0,5 en 1 s puis retombe en 1 s ; au survol de son
-- centre (18 x 18), TALENT_FRAME_SEARCH_TOOLTIP_* :
-- exact talents-search-exactmatch "Exact search match"
-- nom, description talents-search-match "Search match"
-- apparente talents-search-relatedmatch
-- "Related to the talent you searched for"
-- absent talents-search-notonactionbar "Not on action bar"
-- posture inactive talents-search-notonactionbarhidden
-- "On an action bar belonging to a different stance"
-- barre desactivee talents-search-notonactionbarhidden
-- "On a disabled action bar"
-- mise a jour la recherche se recalcule a chaque changement de l'ecran
-- (UpdateFullSearchResults)
--
-- CE QUI DIFFERE, ET POURQUOI.
-- * le champ est a GAUCHE du compteur "Unspent Talents" (camelot : a droite,
-- la ou le compteur a ete remonte a la demande) ; en fenetre reduite il se
-- retrecit pour tenir (decision du 2026-09-25).
-- * l'apercu montre 5 resultats (camelot : 3), comme le grimoire
-- (decision du 2026-09-25).
-- * 3.3.5 ne donne pas la description d'un talent : elle se lit dans une
-- infobulle cachee (SetTalent), sans ses lignes de rang, de prerequis ni
-- "Click to learn".
-- * pas de champ sur la page des glyphes.
ForeverUI = ForeverUI or {}
local K = {}
ForeverUI.TalentsSearch = K
local SEP = string.char(92)
local TEXTE = {
consigne = SEARCH or "Search",
pasSurBarre = "Missing from action bar", -- TALENT_FRAME_SEARCH_NOT_ON_ACTIONBAR
masquerPassifs = "Hide Passives", -- CLASS_TALENT_SEARCH_OPTION_HIDE_PASSIVES
rangs = "Show Ranks", -- CLASS_TALENT_SEARCH_OPTION_SHOW_RANKS
depassement = "And %s more", -- TALENT_FRAME_SEARCH_PREVIEW_OVERFLOW_FORMAT
}
local MIN_LETTRES = 3 -- MIN_CHARACTER_SEARCH
-- SpellSearchUtil.MatchType : le plus grand est le meilleur
local TYPE = { description = 1, nom = 2, apparente = 3, exact = 4, absent = 5, postureInactive = 6, barreDesactivee = 7 }
-- SearchMatchStyles (blizzard_sharedtalentutil.lua)
local STYLES = {
[TYPE.apparente] = { icone = "talents-search-relatedmatch", info = "Related to the talent you searched for" },
[TYPE.nom] = { icone = "talents-search-match", info = "Search match" },
[TYPE.description] = { icone = "talents-search-match", info = "Search match" },
[TYPE.exact] = { icone = "talents-search-exactmatch", info = "Exact search match" },
[TYPE.absent] = { icone = "talents-search-notonactionbar", info = "Not on action bar" },
[TYPE.postureInactive] = { icone = "talents-search-notonactionbarhidden",
info = "On an action bar belonging to a different stance" },
[TYPE.barreDesactivee] = { icone = "talents-search-notonactionbarhidden", info = "On a disabled action bar" },
}
local M = {
boiteL = 184, boiteH = 30, lettres = 40, ecartCompteur = 10, margeGauche = 10,
fleche = 25, flecheX = 3, flecheY = -2,
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,
apercuL = 176, apercuX = -4, apercuY = 2, 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,
-- la marque d'un noeud et son battement
marque = 63, marqueSurvol = 18, battement = 1, battementAlpha = 0.5,
-- la liste des options (celle des reglages du grimoire)
ligneMenuH = 20, bordMenu = 15, ligneMenuX = 11, texteMenuX = 20, largeurPlus = 40, margeMenu = 25,
case = 12, cocheL = 15, cocheH = 14, cocheX = 2, cocheY = 1,
fondCoin = 18, fondMarges = { 9, 6, 9, 12 }, fondAlpha = 0.925, attente = 2,
}
-- ------------------------------------------------------------ les chaines
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
-- l'infobulle d'un talent, sans son nom, ses lignes de rang, de prerequis ni
-- l'invite d'apprentissage : ce qui reste est la description (et celle du
-- rang suivant)
local lecteur = CreateFrame("GameTooltip", "ForeverUITalentsScanTooltip", nil, "GameTooltipTemplate")
-- un format du client en motif Lua ; ses arguments peuvent etre numerotes
-- ("Requires %1$d points in %2$s Talents" : TOOLTIP_TALENT_TIER_POINTS)
local function motif(format)
if not format then return nil end
local m = string.gsub(format, "%%%d%$", "%%")
m = string.gsub(m, "([%(%)%.%+%-%*%?%[%]%^%$])", "%%%1")
m = string.gsub(m, "%%d", "%%d+")
m = string.gsub(m, "%%s", ".+")
return "^" .. m .. "$"
end
local LIGNES_ECARTEES = {}
for _, f in ipairs({ TOOLTIP_TALENT_RANK, TOOLTIP_TALENT_NEXT_RANK, TOOLTIP_TALENT_LEARN,
TOOLTIP_TALENT_TIER_POINTS, TOOLTIP_TALENT_PREREQ, TOOLTIP_TALENT_UNLEARN }) do
local m = motif(f)
if m then table.insert(LIGNES_ECARTEES, m) end
end
local descriptions = {}
local function description(T, t)
local cle = (T.pet and "p" or "j") .. T.groupe .. ":" .. t.onglet .. ":" .. t.index .. ":" .. t.rang
local d = descriptions[cle]
if d == nil then
local parties = {}
lecteur:SetOwner(WorldFrame, "ANCHOR_NONE")
lecteur:ClearLines()
lecteur:SetTalent(t.onglet, t.index, false, T.pet, T.groupe, true)
for i = 2, lecteur:NumLines() do
local ligne = _G["ForeverUITalentsScanTooltipTextLeft" .. i]
local texte = ligne and ligne:GetText()
if texte and texte ~= "" then
local ecartee = false
for _, m in ipairs(LIGNES_ECARTEES) do
local ok, trouve = pcall(string.find, texte, m)
if ok and trouve then ecartee = true break end
end
if not ecartee then table.insert(parties, texte) end
end
end
lecteur:Hide()
d = table.concat(parties, "\n")
descriptions[cle] = d
end
return d
end
K.description = description
-- ------------------------------------------------------------ l'etat
K.etat = nil -- { filtre = "texte", texte } | { filtre = "barres" }
K.options = { passifs = false, rangs = false }
local T -- ForeverUI.Talents, a la construction
-- les talents de l'ecran, dans l'ordre (arbre, palier, colonne)
local function talents()
local liste = {}
for _, o in ipairs(T.onglets or {}) do
for _, t in ipairs(o.talents) do table.insert(liste, t) end
end
table.sort(liste, function(a, b)
if a.onglet ~= b.onglet then return a.onglet < b.onglet end
if a.palier ~= b.palier then return a.palier < b.palier end
return a.colonne < b.colonne
end)
return liste
end
-- GetActionbarStatusForSpell : un talent appris (valide), sort actif, qui
-- n'est sur aucune barre active
local function typeBarre(t, barres)
if not t.carre or (t.appris or 0) == 0 then return end
if T.pet then
if barres.familier[t.nom] then return end
return TYPE.absent
end
if barres.actives[t.nom] then return end
if barres.desactivees[t.nom] then return TYPE.barreDesactivee end
if barres.inactives[t.nom] then return TYPE.postureInactive end
return TYPE.absent
end
-- les types de la recherche en cours, par talent (onglet:index)
function K.calculer()
local etat = K.etat
local types = {}
if not etat then return types end
local tous = talents()
if etat.filtre == "barres" then
local R = ForeverUI.SpellBookSearch
local barres = R and R.barres and R.barres()
if barres then
for _, t in ipairs(tous) do
types[t.onglet .. ":" .. t.index] = typeBarre(t, barres)
end
end
return types
end
local texte = etat.texte
local descExacte
for _, t in ipairs(tous) do
if egal(t.nom, texte) then
descExacte = description(T, t)
break
end
end
for _, t in ipairs(tous) do
local ty
if egal(t.nom, texte) then
ty = TYPE.exact
elseif contient(t.nom, texte) then
ty = TYPE.nom
elseif contient(description(T, t), texte) then
ty = TYPE.description
elseif descExacte and contient(descExacte, t.nom) then
ty = TYPE.apparente
end
types[t.onglet .. ":" .. t.index] = ty
end
return types
end
-- l'apercu : le filtre de nom ; puis les options (TransformPreviewResults)
function K.apercu(texte)
local trouves = {}
for _, t in ipairs(talents()) do
local ty
if egal(t.nom, texte) then ty = TYPE.exact elseif contient(t.nom, texte) then ty = TYPE.nom end
if ty and not (K.options.passifs and not t.carre) then
local nom = t.nom
if K.options.rangs then nom = nom .. " (" .. t.rang .. "/" .. t.max .. ")" end
table.insert(trouves, { talent = t, type = ty, nom = nom, cherche = t.nom, icone = t.icone })
end
end
-- PreviewSearchResultSort : le type, puis l'ordre de l'ecran
local ordre = {}
for i, r in ipairs(trouves) do ordre[r] = i end
table.sort(trouves, function(a, b)
if a.type ~= b.type then return a.type > b.type end
return ordre[a] < ordre[b]
end)
return trouves
end
-- ------------------------------------------------------------ les marques
local function creerMarque(b)
local m = CreateFrame("Frame", nil, b)
m:SetWidth(M.marque)
m:SetHeight(M.marque)
m:SetPoint("CENTER", b.icone, "TOPRIGHT", 0, 0)
m:SetFrameLevel(b:GetFrameLevel() + 50)
local icone = m:CreateTexture(nil, "OVERLAY")
icone:SetAllPoints(m)
local battant = m:CreateTexture(nil, "OVERLAY")
battant:SetAllPoints(m)
battant:SetBlendMode("ADD")
battant:SetAlpha(0)
m.icone, m.battant = icone, battant
-- le survol : son centre seulement
local survol = CreateFrame("Frame", nil, m)
survol:SetWidth(M.marqueSurvol)
survol:SetHeight(M.marqueSurvol)
survol:SetPoint("CENTER", m, "CENTER", 0, 0)
survol:EnableMouse(true)
survol:SetScript("OnEnter", function(self)
if not m.info then return end
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:SetText(m.info, NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b)
GameTooltip:Show()
end)
survol:SetScript("OnLeave", function() GameTooltip:Hide() end)
m.survol = survol
-- GlowAnim : 0 -> 0,5 en 1 s, puis 0,5 -> 0 en 1 s, en boucle
m:SetScript("OnUpdate", function(self, ecoule)
self.temps = ((self.temps or 0) + ecoule) % (2 * M.battement)
local t = self.temps
local a = t < M.battement and t / M.battement or 2 - t / M.battement
self.battant:SetAlpha(a * M.battementAlpha)
end)
m:Hide()
b.marque = m
-- une marque monte tres haut (noeud + 50) : la croix repasse au-dessus
if T.leverCroix then T.leverCroix() end
return m
end
-- SetSearchMatchType sur chaque noeud montre
local function marquer(types)
for _, b in pairs(T.arbre and T.arbre.noeuds or {}) do
local t = b:IsShown() and b.talent
local ty = t and types[t.onglet .. ":" .. t.index]
if ty then
local m = b.marque or creerMarque(b)
local style = STYLES[ty]
ForeverUI.SetAtlas(m.icone, style.icone, true)
ForeverUI.SetAtlas(m.battant, style.icone, true)
m.info = style.info
m.type = ty
m:Show()
elseif b.marque then
b.marque.type = nil
b.marque:Hide()
end
end
end
-- ------------------------------------------------------------ le champ
local boite, effacer, apercu, fleche, liste
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
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
if actif then effacer:Show() else effacer:Hide() end
end
-- SetFullResultSearch
function K.chercher(texte)
if not texte then
K.quitter()
return
end
if egal(texte, TEXTE.pasSurBarre) then
K.etat = { filtre = "barres" }
else
K.etat = { filtre = "texte", texte = texte }
end
K.maj()
end
-- ClearActiveSearchState
function K.quitter()
K.etat = nil
if boite then
boite:ClearFocus()
boite:SetText("")
cacherApercu()
majLoupe()
end
K.maj()
end
-- ------------------------------------------------------------ l'apercu
local function ligneResultat(parent, i)
local l = CreateFrame("Button", "ForeverUITalentsSearchResult" .. 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(parent)
local a = CreateFrame("Frame", "ForeverUITalentsSearchPreview", parent)
a:SetFrameStrata("HIGH")
a:SetWidth(M.apercuL)
a:SetPoint("TOPRIGHT", boite, "BOTTOMRIGHT", M.apercuX, 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)
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)
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() K.surligner(i) end)
l:SetScript("OnClick", function() K.choisir(i) end)
l:Hide()
a.lignes[i] = l
end
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() K.surligner(1) end)
sugg:SetScript("OnClick", function() K.choisir(1) end)
sugg:Hide()
a.suggestion = sugg
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
a:SetScript("OnUpdate", function(self)
if not boite:HasFocus() and not MouseIsOver(self) then cacherApercu() end
end)
return a
end
function K.majApercu(texte)
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
a.suggestion:Show()
a.nombre = 1
a:SetHeight(M.ligneH + M.basMarge)
a:Show()
return
end
local trouves = K.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
function K.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
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
K.surligner(i)
end
-- OnPreviewSearchResultClicked : le nom d'origine (sans "(rang/max)")
function K.choisir(i)
local a = apercu
PlaySound("igMainMenuOptionCheckBoxOn")
local texte
if a.resultats then
local r = a.resultats[i]
if not r then return end
texte = r.cherche
else
texte = TEXTE.pasSurBarre
end
boite:ClearFocus()
boite:SetText(texte)
cacherApercu()
K.chercher(texte)
end
-- ------------------------------------------------------------ les options
local function majFleche()
local etat = "common-dropdown-a-button"
if fleche.enfonce and fleche.survol then
etat = "common-dropdown-a-button-pressedhover"
elseif fleche.survol then
etat = "common-dropdown-a-button-hover"
elseif fleche.enfonce then
etat = "common-dropdown-a-button-pressed"
elseif liste and liste:IsShown() then
etat = "common-dropdown-a-button-open"
end
ForeverUI.SetAtlas(fleche.icone, etat)
end
local function majCoches()
for _, l in ipairs(liste.lignes) do
if K.options[l.cle] then l.coche:Show() else l.coche:Hide() end
end
end
local function construireOptions(parent)
local b = CreateFrame("Button", "ForeverUITalentsSearchOptions", parent)
fleche = b
b:SetWidth(M.fleche)
b:SetHeight(M.fleche)
b:SetPoint("LEFT", boite, "RIGHT", M.flecheX, M.flecheY)
b.icone = b:CreateTexture(nil, "OVERLAY")
b.icone:SetPoint("CENTER", b, "CENTER", 0, -2)
b:SetScript("OnEnter", function(self) self.survol = true majFleche() end)
b:SetScript("OnLeave", function(self) self.survol = false majFleche() end)
b:SetScript("OnMouseDown", function(self) self.enfonce = true majFleche() end)
b:SetScript("OnMouseUp", function(self) self.enfonce = false majFleche() end)
b:SetScript("OnClick", function()
if liste:IsShown() then liste:Hide() else liste.attente = 0 liste:Show() end
majFleche()
end)
local l0 = CreateFrame("Frame", "ForeverUITalentsSearchOptionsList", parent)
liste = l0
l0:SetFrameStrata("DIALOG")
l0:EnableMouse(true)
l0:SetPoint("TOPLEFT", b, "BOTTOMLEFT", 0, 0)
l0:Hide()
local tranches = ForeverUI.CreateNineSlice(l0, "common-dropdown-bg-c60", M.fondCoin, M.fondMarges, "BACKGROUND")
for _, t in ipairs(tranches or {}) do t:SetAlpha(M.fondAlpha) end
l0.lignes = {}
local plusLong = 0
for i, def in ipairs({ { cle = "passifs", texte = TEXTE.masquerPassifs }, { cle = "rangs", texte = TEXTE.rangs } }) do
local l = CreateFrame("Button", "ForeverUITalentsSearchOption" .. i, l0)
l:SetHeight(M.ligneMenuH)
l:SetPoint("TOPLEFT", l0, "TOPLEFT", M.ligneMenuX, -M.bordMenu - (i - 1) * M.ligneMenuH)
l:SetHighlightTexture("Interface" .. SEP .. "QuestFrame" .. SEP .. "UI-QuestTitleHighlight")
l:GetHighlightTexture():SetBlendMode("ADD")
local texte = l:CreateFontString(nil, "ARTWORK", "GameFontHighlightLeft")
texte:SetPoint("LEFT", l, "LEFT", M.texteMenuX, 0)
texte:SetText(def.texte)
plusLong = math.max(plusLong, texte:GetStringWidth() or 0)
local case = l:CreateTexture(nil, "BORDER")
ForeverUI.SetAtlas(case, "common-dropdown-ticksquare", true)
case:SetWidth(M.case)
case:SetHeight(M.case)
case:SetPoint("LEFT", l, "LEFT", 0, 0)
local coche = l:CreateTexture(nil, "ARTWORK")
ForeverUI.SetAtlas(coche, "common-dropdown-icon-checkmark-yellow", true)
coche:SetWidth(M.cocheL)
coche:SetHeight(M.cocheH)
coche:SetPoint("CENTER", case, "CENTER", M.cocheX, M.cocheY)
coche:Hide()
l.cle, l.texte, l.coche = def.cle, texte, coche
-- CreateCheckbox : la case bascule, la liste reste ouverte ; l'apercu
-- ouvert se refait
l:SetScript("OnClick", function(self)
PlaySound("UChatScrollButton")
K.options[self.cle] = not K.options[self.cle]
majCoches()
if boite:HasFocus() then K.majApercu(evaluer()) end
end)
l0.lignes[i] = l
end
local largeur = plusLong + M.largeurPlus + M.margeMenu
l0:SetWidth(largeur)
l0:SetHeight(#l0.lignes * M.ligneMenuH + 2 * M.bordMenu)
for _, l in ipairs(l0.lignes) do l:SetWidth(largeur - M.margeMenu) end
-- elle se ferme 2 s apres que la souris l'a quittee (et la fleche)
l0:SetScript("OnUpdate", function(self, ecoule)
if MouseIsOver(self) or MouseIsOver(fleche) then
self.attente = 0
else
self.attente = (self.attente or 0) + ecoule
if self.attente >= M.attente then self:Hide() end
end
end)
l0:SetScript("OnShow", majCoches)
l0:SetScript("OnHide", majFleche)
majFleche()
end
-- ------------------------------------------------------------ construction
function K.construire(talentsModule)
if boite then return end
T = talentsModule
local parent = T.cadre
local b = CreateFrame("EditBox", "ForeverUITalentsSearchBox", parent)
boite = b
b:SetAutoFocus(false)
b:SetMaxLetters(M.lettres)
b:SetHeight(M.boiteH)
b:SetWidth(M.boiteL)
b:SetFrameLevel(parent:GetFrameLevel() + 6)
b:SetFontObject(GameFontHighlightSmall)
b:SetTextInsets(M.margeG, M.margeD, 0, 0)
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)
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
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
local e = CreateFrame("Button", "ForeverUITalentsSearchClear", b)
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:SetScript("OnClick", function()
PlaySound("igMainMenuOptionCheckBoxOn")
K.quitter()
end)
e:Hide()
construireOptions(parent)
fleche:SetFrameLevel(b:GetFrameLevel())
-- la place : la fleche contre le libelle du compteur, le champ contre la
-- fleche (camelot : la fleche a RIGHT du champ en (3, -2))
fleche:ClearAllPoints()
fleche:SetPoint("RIGHT", T.points.libelle, "LEFT", -M.ecartCompteur, M.flecheY - 1)
b:SetPoint("RIGHT", fleche, "LEFT", -M.flecheX, -M.flecheY)
apercu = construireApercu(parent)
b:SetScript("OnEditFocusGained", function()
majLoupe()
K.majApercu(evaluer())
end)
b:SetScript("OnEditFocusLost", function()
majLoupe()
if not MouseIsOver(apercu) then cacherApercu() end
end)
b:SetScript("OnTextChanged", function(self)
majLoupe()
if self:HasFocus() then K.majApercu(evaluer()) end
end)
b:SetScript("OnEnterPressed", function(self)
local a = apercu
if a:IsShown() and (a.surligne or 0) > 0 then
K.choisir(a.surligne)
return
end
cacherApercu()
K.chercher(evaluer())
self:ClearFocus()
end)
b:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
b:SetScript("OnKeyDown", function(_, touche)
if touche == "UP" then parcourir(-1) elseif touche == "DOWN" then parcourir(1) end
end)
majLoupe()
K.boite, K.effacer, K.apercuCadre, K.fleche, K.liste = boite, effacer, apercu, fleche, liste
end
-- la largeur : 184, ou ce qui reste a gauche du compteur (fenetre reduite)
function K.poser(largeurPage)
if not boite then return end
local libelle = T.points.libelle
local droiteLibelle = T.points.droiteLibelle or 0
local w = libelle:GetStringWidth() or 0
-- du bord gauche du cadre (page + 2 de chaque cote) a la gauche du champ
local reste = largeurPage + 4 + droiteLibelle - w - M.ecartCompteur - M.fleche - M.flecheX - M.margeGauche
boite:SetWidth(math.max(60, math.min(M.boiteL, reste)))
end
-- apres chaque mise a jour de l'ecran : les marques (UpdateFullSearchResults) ;
-- rien sur la page des glyphes
function K.maj()
if not boite or not T then return end
if T.glyphes then
boite:Hide()
fleche:Hide()
liste:Hide()
cacherApercu()
marquer({})
return
end
boite:Show()
fleche:Show()
marquer(K.calculer())
end
-- LES BARRES CHANGENT (un sort pose, retire, une page ou une posture) : la
-- recherche "Missing from action bar" se refait, et le talent qu'on vient de
-- poser perd sa marque (demande du 2026-09-25)
local veille = CreateFrame("Frame")
for _, ev in ipairs({ "ACTIONBAR_SLOT_CHANGED", "ACTIONBAR_PAGE_CHANGED", "UPDATE_BONUS_ACTIONBAR",
"ACTIONBAR_SHOWGRID", "ACTIONBAR_HIDEGRID", "PET_BAR_UPDATE", "UPDATE_MULTI_ACTIONBAR" }) do
veille:RegisterEvent(ev)
end
veille:SetScript("OnEvent", function()
if K.etat and K.etat.filtre == "barres" and T and T.livre and T.livre:IsVisible() then
K.maj()
end
end)
K.veilleBarres = veille
-- les talents deja construits (Blizzard_TalentUI charge avant ce fichier)
if ForeverUI.Talents and ForeverUI.Talents.livre then
K.construire(ForeverUI.Talents)
ForeverUI.Talents.maj()
end