Files
moon-well-ui/data/addon/ForeverUI/Chat.lua
T
Adrestie 2be83180ad Selection du chat : le jeu garde le clavier, tout autre geste efface
- la saisie de copie ne prend le clavier que pendant que Ctrl est enfonce
  (Ctrl+C copie) ; au relachement de Ctrl, la selection s'efface
- effacent aussi : un clic, se deplacer, tourner, sauter (hors taxi), lancer
  un sort, une autre saisie qui prend le clavier, une fenetre qui s'ouvre
  (ShowUIPanel), Echap (ToggleGameMenu), le chat qui defile, un message
  selectionne qui change, le chat cache
- banc : personnage (vitesse, orientation, chute, taxi), focus du clavier,
  menu du jeu ; chaque geste essaye

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 02:54:40 +02:00

1292 lines
50 KiB
Lua

-- ForeverUI : la fenetre de chat, a la DA de camelot -- etape 1, le cadre
-- (demande de l'utilisateur, 2026-09-26) ; etape 2, les onglets et la saisie
-- (2026-09-28) ; memoire foreverui-chat.
--
-- CE QUI NE CHANGE PAS : L'ART. Le fond, les bords, les onglets, la saisie,
-- le bouton de menu, la poignee et le dock de camelot sont les MEMES fichiers
-- que ceux de 3.3.5, pixel pour pixel (compare le 2026-09-26 :
-- ChatFrameBackground, UI-ChatFrame-Border*, ChatFrameTab-*,
-- UI-ChatInputBorder*, UI-ChatIcon-Chat-*, UI-ChatIM-SizeGrabber-*,
-- chat-tab-arrow...). Ce qui fait camelot, ce sont les places, et quelques
-- pieces nouvelles.
--
-- RELEVE -- blizzard_chatframebase/mainline (floatingchatframe.xml et .lua,
-- chatframe.xml) et shared (floatingchatframe.lua), charges par [Family] :
-- fond FloatingChatFrame_UpdateBackgroundAnchors : le fond deborde
-- a droite de 7 + la largeur de la barre (8), soit 15, pour
-- loger la barre ; a gauche -2, en haut 3, en bas -6
-- colonne FCF_SetButtonSide : TOPRIGHT sur le TOPLEFT du fond (-3,-3)
-- et BOTTOMRIGHT sur son BOTTOMLEFT (-3,6) a gauche ; a droite
-- TOPLEFT sur TOPRIGHT (3,-3) et BOTTOMLEFT sur BOTTOMRIGHT
-- (3,6) ; plus de boutons haut / bas / fin : la barre les
-- remplace ; le bouton de menu a BOTTOM (0,0) de la colonne de
-- ChatFrame1, le bouton de reduction a TOP (0,4)
-- barre MinimalScrollBar, 8 de large, TOPLEFT sur le TOPRIGHT du chat
-- (0,0), BOTTOMLEFT sur le TOPLEFT du bouton de retour (0,2) ;
-- cachee au repos, 0,6 quand le chat s'allume (0,15 s), 0
-- quand il s'eteint (2 s)
-- retour ScrollToBottomButton, 17 x 15, BOTTOMRIGHT sur le TOPRIGHT de
-- la poignee (-2,-2) : minimal-scrollbar-arrow-returntobottom
-- (-down enfonce, -over au survol) ; 0,65 quand le chat
-- s'allume (0,1 s), 0 quand il s'eteint ; tant que le chat
-- n'est pas en bas il clignote -- le meme art en ADD, 0,1 s
-- pour venir, 0,5 s tenu, 0,1 s pour partir, 0,5 s eteint
-- (ChatFrameConstants.ScrollToBottomFlashInterval) -- et la
-- barre s'allume
-- amis QuickJoinToastButton, 32 x 32, dans ChatAlertFrame :
-- BOTTOMLEFT sur le TOPLEFT de la colonne du chat par defaut
-- (0,27), a droite quand la colonne est a droite ;
-- quickjoin-button-friendslist-up / -down, surbrillance
-- UI-Common-MouseHilight, nombre d'amis GameFontHighlightSmall
-- a BOTTOM (0,4)
-- onglets ChatTabArtTemplate : le cote gauche a BOTTOMLEFT (-2,0) de
-- l'onglet, le droit a BOTTOMRIGHT (2,0), le milieu tendu
-- entre eux (3.3.5 : le gauche a TOPLEFT (0,0), le milieu de
-- la largeur du texte, le droit a sa suite) ; ChatTabTemplate :
-- le texte au CENTRE (0,-5) (3.3.5 : a gauche, contre le cote
-- gauche, (0,-5)) ; PanelTemplates_TabResize : largeur = texte
-- + 20 + marge, au moins les deux cotes (32) ; a taille
-- imposee, le texte prend taille - 20 - marge (3.3.5 : texte +
-- marge + les deux cotes)
-- dock FCFDock_SetPrimary : le dock sur le HAUT DU FOND du chat
-- principal (3.3.5 : sur le haut du chat, a 6) ;
-- FCFDock_UpdateTabs : le premier onglet a BOTTOMLEFT (0,0) du
-- dock, les suivants a 1 du precedent (3.3.5 : a 0) ; ceux des
-- chuchotements depuis LEFT (0,-1) de la liste defilante, a 1
-- l'un de l'autre ; la liste a BOTTOMRIGHT (0,0) du dock
-- (3.3.5 : (0,-5)) ; le bouton de debordement a BOTTOMRIGHT
-- (0,0) du dock (3.3.5 : (0,-5)), la liste s'arretant alors 5
-- avant lui, 1 plus bas
-- saisie TOPLEFT sur le BOTTOMLEFT du chat (-5,-2), comme 3.3.5 ; le
-- bord droit sur celui de la barre, +8 -- 16 au-dela du chat
-- (3.3.5 : 5)
--
-- DECISIONS DE L'UTILISATEUR (2026-09-26) : pas de bouton des canaux (son
-- icone est un haut-parleur, chatframe-button-icon-voicechat, et les canaux
-- sont dans Social) ; le bouton d'amis de camelot a la place du bouton
-- Battle.net de WotLK (FriendsMicroButton : il garde son role, son nombre
-- d'amis et son ouverture de Social).
--
-- CE QUI VIENT DE WotLK : tout le chat -- ses fenetres, le dock, le fondu
-- (FCF_OnUpdate), les onglets, la saisie. On ne remplace aucune fonction ;
-- on accroche (hooksecurefunc) celles qui reposent ce qu'on change :
-- FCF_SetButtonSide (la colonne), FCF_OpenTemporaryWindow (les fenetres de chuchotement),
-- PanelTemplates_TabResize (pour les seuls onglets du chat), FCFDock_UpdateTabs
-- et FCFDock_SetPrimary (le dock). Les
-- boutons haut / bas / fin de la colonne sont remontres par l'OnShow de
-- chaque fenetre : on les recache apres lui.
--
-- AU REPOS (demande de l'utilisateur, 2026-09-26) : souris hors du chat, il
-- ne reste que les messages. WotLK et camelot laissent au repos le fond a
-- l'opacite retenue, les onglets a 0,4 / 0,2, la colonne du chat par defaut
-- a 1 et la saisie a 0,35 ; ici :
-- le fond l'opacite retenue de chaque fenetre passe a 0, UNE fois
-- (SetChatWindowAlpha a ADDON_LOADED, ForeverUIDB.chatFondRepos)
-- : le client la relit a UPDATE_CHAT_WINDOWS et la garde ;
-- au survol il monte a max(opacite, 0,25) comme toujours ; ce
-- que l'utilisateur regle ensuite dans le menu de l'onglet
-- est respecte
-- le reste onglets, boutons de la colonne (menu, amis, reduction), la
-- saisie : un coefficient par fenetre, 1 allumee, 0 eteinte
-- (0,15 s pour venir, 2 s pour partir, comme le client),
-- pose A CHAQUE IMAGE sur les REGIONS des onglets et de la
-- saisie -- leur alpha a eux appartient au client, qui remet
-- aussi celui du texte des onglets -- et sur les boutons ; la
-- saisie active reste a 1. Les lueurs d'alerte des onglets
-- (glow, TabFlash) sont animees par UIFrameFlash : on n'y
-- touche pas
--
-- LE SURVOL (demande de l'utilisateur, 2026-09-26) : le chat s'allume au
-- survol. FCF_OnUpdate ne l'allume que si le curseur reste IMMOBILE 0,2 s
-- sur lui ; on ne remplace pas FCF_OnUpdate et on n'appelle pas
-- FCF_FadeInChatFrame (il ecrirait hasBeenFaded depuis l'addon, et
-- FCF_OnUpdate tourne dans l'OnUpdate de UIParent, avant UnitPopup_OnUpdate :
-- la souillure gagnerait les menus d'unite). Notre moteur regarde les memes
-- zones que FCF_OnUpdate (plus la barre et le retour), sans exiger
-- l'immobilite : survolee 0,2 s, la fenetre s'allume ; dehors 1 s, elle
-- s'eteint (CHAT_TAB_SHOW_DELAY, CHAT_TAB_HIDE_DELAY). Allumee par nous ou
-- par le client (hasBeenFaded), c'est pareil ; une fenetre du dock allume
-- tout le dock. Le fond, lui, est peint par le client : une DOUBLURE de
-- chacune de ses textures (CHAT_FRAME_TEXTURES), posee dessus, complete ce
-- qui manque pour atteindre max(opacite, 0,25) -- 1 - (1 - voulu) / (1 -
-- client) -- et ne fait rien quand le client l'y a deja mis
--
-- LE DEFILEMENT. 3.3.5 compte le defilement en messages depuis le bas
-- (GetCurrentScroll) ; la barre en deduit sa place, et la deplacer fait
-- defiler de la difference (ScrollUp / ScrollDown), SetScrollOffset quand le
-- client l'a.
local ForeverUI = ForeverUI or {}
_G.ForeverUI = ForeverUI
local C = {}
ForeverUI.Chat = C
local SEP = string.char(92)
local G = {
fondGauche = -2, fondHaut = 3, fondDroite = 7 + 8, fondBas = -6,
colonneX = 3, colonneHaut = -3, colonneBas = 6,
minimiserY = 4,
amis = 32, amisY = 27,
retourL = 17, retourH = 15, retourX = -2, retourY = -2, barreBas = 2,
barreAlpha = 0.6, retourAlpha = 0.65,
venue = 0.15, venueRetour = 0.1, depart = 2.0,
fondRepos = 0,
flashVenue = 0.1, flashTenu = 0.5,
hautParLigne = 2,
ongletCote = 2, ongletMarges = 20, ongletTexteY = -5,
ongletEcart = 1, ongletMobileY = -1, debordEcart = 5, debordY = -1,
saisieX = -5, saisieY = -2, saisieDroite = 8,
}
local ATLAS = {
retour = "minimal-scrollbar-arrow-returntobottom-c60",
retourBas = "minimal-scrollbar-arrow-returntobottom-down-c60",
retourSurvol = "minimal-scrollbar-arrow-returntobottom-over-c60",
amis = "quickjoin-button-friendslist-up",
amisBas = "quickjoin-button-friendslist-down",
}
C.fenetres = {}
-- ------------------------------------------------------------ la colonne
-- FCF_SetButtonSide de camelot : la colonne sur le fond
local function poserColonne(fenetre)
local bf = fenetre.buttonFrame
local fond = _G[fenetre:GetName() .. "Background"]
if not (bf and fond) then return end
bf:ClearAllPoints()
if fenetre.buttonSide == "right" then
bf:SetPoint("TOPLEFT", fond, "TOPRIGHT", G.colonneX, G.colonneHaut)
bf:SetPoint("BOTTOMLEFT", fond, "BOTTOMRIGHT", G.colonneX, G.colonneBas)
else
bf:SetPoint("TOPRIGHT", fond, "TOPLEFT", -G.colonneX, G.colonneHaut)
bf:SetPoint("BOTTOMRIGHT", fond, "BOTTOMLEFT", -G.colonneX, G.colonneBas)
end
end
-- les boutons haut / bas / fin : l'OnShow de la fenetre les remontre
local function cacherFleches(fenetre)
local nom = fenetre:GetName()
for _, suffixe in ipairs({ "ButtonFrameUpButton", "ButtonFrameDownButton", "ButtonFrameBottomButton" }) do
local b = _G[nom .. suffixe]
if b then
b:SetAlpha(0)
b:EnableMouse(false)
b:Hide()
end
end
end
-- le bouton d'amis : a gauche ou a droite de la colonne du chat par defaut
local function poserAmis()
local b = FriendsMicroButton
local bf = DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.buttonFrame
if not (b and bf) then return end
b:ClearAllPoints()
if DEFAULT_CHAT_FRAME.buttonSide == "right" then
b:SetPoint("BOTTOMRIGHT", bf, "TOPRIGHT", 0, G.amisY)
else
b:SetPoint("BOTTOMLEFT", bf, "TOPLEFT", 0, G.amisY)
end
end
local function habillerAmis()
local b = FriendsMicroButton
if not b or b.foreverChat then return end
b.foreverChat = true
b:SetWidth(G.amis)
b:SetHeight(G.amis)
for _, etat in ipairs({
{ "SetNormalTexture", "GetNormalTexture", ATLAS.amis },
{ "SetPushedTexture", "GetPushedTexture", ATLAS.amisBas },
}) do
local e = ForeverUI.AtlasEntry(etat[3])
b[etat[1]](b, e and e[1] or "")
local t = b[etat[2]](b)
if t then
ForeverUI.SetAtlas(t, etat[3], true)
t:ClearAllPoints()
t:SetAllPoints(b)
end
end
poserAmis()
end
-- ------------------------------------------------------------ la barre
local function taillePolice(fenetre)
local _, taille = fenetre:GetFont()
return taille or 14
end
-- ce que la barre montre : le nombre de messages, ceux qui tiennent, et
-- ou l'on est (en messages depuis le haut)
local function etatDefilement(fenetre)
local total = fenetre:GetNumMessages() or 0
local visibles = math.max(1, math.floor((fenetre:GetHeight() or 0) / (taillePolice(fenetre) + G.hautParLigne)))
local maxi = math.max(0, total - visibles)
local depuisBas = fenetre.GetCurrentScroll and fenetre:GetCurrentScroll() or 0
return total, visibles, math.max(0, math.min(maxi, maxi - depuisBas)), maxi
end
-- deplacer la barre : le chat defile de la difference
function C.defiler(fenetre, rang)
local _, _, courant, maxi = etatDefilement(fenetre)
local voulu = math.max(0, math.min(maxi, rang))
if fenetre.SetScrollOffset then
fenetre:SetScrollOffset(maxi - voulu)
else
for _ = 1, courant - voulu do fenetre:ScrollUp() end
for _ = 1, voulu - courant do fenetre:ScrollDown() end
end
C.majBarre(fenetre)
end
function C.majBarre(fenetre)
local d = C.fenetres[fenetre]
if not d then return end
local total, visibles, rang = etatDefilement(fenetre)
d.barre:Regler(total, visibles, rang)
end
-- ------------------------------------------------------------ le retour
local function creerRetour(fenetre)
local nom = fenetre:GetName()
local b = CreateFrame("Button", nom .. "ForeverScrollToBottom", fenetre)
b:SetWidth(G.retourL)
b:SetHeight(G.retourH)
b:SetPoint("BOTTOMRIGHT", fenetre.resizeButton, "TOPRIGHT", G.retourX, G.retourY)
for _, etat in ipairs({
{ "SetNormalTexture", "GetNormalTexture", ATLAS.retour },
{ "SetPushedTexture", "GetPushedTexture", ATLAS.retourBas },
{ "SetHighlightTexture", "GetHighlightTexture", ATLAS.retourSurvol },
}) do
local e = ForeverUI.AtlasEntry(etat[3])
b[etat[1]](b, e and e[1] or "")
local t = b[etat[2]](b)
if t then
ForeverUI.SetAtlas(t, etat[3], true)
t:ClearAllPoints()
t:SetAllPoints(b)
end
end
local flash = b:CreateTexture(nil, "OVERLAY")
ForeverUI.SetAtlas(flash, ATLAS.retour, true)
flash:SetBlendMode("ADD")
flash:SetAllPoints(b)
flash:SetAlpha(0)
b.flash = flash
b:SetAlpha(0)
b:SetScript("OnClick", function()
PlaySound("igChatBottom")
fenetre:ScrollToBottom()
C.majBarre(fenetre)
end)
return b
end
-- ------------------------------------------------------------ le repos
-- les regions d'un cadre, sauf celles que le client anime lui-meme
local function regions(cadre, sauf, dans)
for _, r in ipairs({ cadre:GetRegions() }) do
if not sauf[r] then table.insert(dans, r) end
end
end
-- ce qui s'efface au repos : les regions de l'onglet (sans sa lueur
-- d'alerte ; l'eclat TabFlash est un cadre fils, pas une region), les boutons de la colonne (sans les fleches, cachees), et pour
-- le chat par defaut le bouton de menu, le bouton d'amis et le bouton de
-- debordement du dock (sans sa surbrillance, qui clignote aux alertes) ;
-- a part, les regions de la saisie
function C.releverRepos(fenetre)
local d = C.fenetres[fenetre]
local nom = fenetre:GetName()
d.effaces, d.saisie = {}, {}
local onglet = _G[nom .. "Tab"]
if onglet then
regions(onglet, { [onglet.glow or false] = true }, d.effaces)
end
local fleches = {}
for _, suffixe in ipairs({ "ButtonFrameUpButton", "ButtonFrameDownButton", "ButtonFrameBottomButton" }) do
if _G[nom .. suffixe] then fleches[_G[nom .. suffixe]] = true end
end
for _, b in ipairs({ fenetre.buttonFrame:GetChildren() }) do
if not fleches[b] then table.insert(d.effaces, b) end
end
if fenetre == DEFAULT_CHAT_FRAME then
if ChatFrameMenuButton then table.insert(d.effaces, ChatFrameMenuButton) end
if FriendsMicroButton then table.insert(d.effaces, FriendsMicroButton) end
local debord = GENERAL_CHAT_DOCK and GENERAL_CHAT_DOCK.overflowButton
if debord then regions(debord, { [debord:GetHighlightTexture() or false] = true }, d.effaces) end
end
if fenetre.editBox then regions(fenetre.editBox, {}, d.saisie) end
end
-- le coefficient sur les pieces, a chaque image (le client remet l'alpha
-- du texte des onglets) ; la saisie active reste entiere
function C.poserRepos(fenetre)
local d = C.fenetres[fenetre]
local saisie = fenetre.editBox
local active = saisie and (ACTIVE_CHAT_EDIT_BOX == saisie or saisie:HasFocus()) and true or false
for _, objet in ipairs(d.effaces) do objet:SetAlpha(d.repos) end
for _, r in ipairs(d.saisie) do r:SetAlpha(active and 1 or d.repos) end
end
-- la doublure : une copie de chaque texture du fond et de ses bords, sur
-- le meme cadre, au meme calque, aux memes coins
function C.doubler(fenetre)
local d = C.fenetres[fenetre]
local nom = fenetre:GetName()
d.doublures = {}
for _, suffixe in ipairs(CHAT_FRAME_TEXTURES or {}) do
local o = _G[nom .. suffixe]
if o then
local t = o:GetParent():CreateTexture(nil, (o:GetDrawLayer()))
t:SetTexture(o:GetTexture())
t:SetTexCoord(o:GetTexCoord())
t:SetAllPoints(o)
t:SetAlpha(0)
t:Hide()
table.insert(d.doublures, { o, t })
end
end
end
-- ce qui manque au fond du client pour atteindre l'opacite allumee
function C.poserDoublure(fenetre)
local d = C.fenetres[fenetre]
local voulu = math.max(fenetre.oldAlpha or 0, DEFAULT_CHATFRAME_ALPHA or 0.25) * d.repos
for _, paire in ipairs(d.doublures) do
local o, t = paire[1], paire[2]
local client = o:GetAlpha()
if o:IsShown() and voulu > client + 0.001 then
t:SetVertexColor(o:GetVertexColor())
t:SetAlpha(1 - (1 - voulu) / (1 - client))
t:Show()
elseif t:IsShown() then
t:SetAlpha(0)
t:Hide()
end
end
end
-- ------------------------------------------------------------ les onglets
C.onglets = {}
-- ChatTabArtTemplate de camelot : les cotes debordent de 2 de chaque cote de
-- l'onglet, le milieu s'etire entre eux (le choisi et la surbrillance
-- suivent : ils sont poses sur eux) ; le texte au centre, sauf celui d'un
-- onglet de chuchotement, a gauche derriere son icone (FCF_OpenTemporaryWindow,
-- le meme dans les deux)
function C.habillerOnglet(onglet)
if not onglet or C.onglets[onglet] then return end
local nom = onglet:GetName()
local gauche, milieu, droite = _G[nom .. "Left"], _G[nom .. "Middle"], _G[nom .. "Right"]
if not (gauche and milieu and droite) then return end
C.onglets[onglet] = true
gauche:ClearAllPoints()
gauche:SetPoint("BOTTOMLEFT", onglet, "BOTTOMLEFT", -G.ongletCote, 0)
droite:ClearAllPoints()
droite:SetPoint("BOTTOMRIGHT", onglet, "BOTTOMRIGHT", G.ongletCote, 0)
milieu:ClearAllPoints()
milieu:SetPoint("LEFT", gauche, "RIGHT", 0, 0)
milieu:SetPoint("RIGHT", droite, "LEFT", 0, 0)
local texte = _G[nom .. "Text"]
if texte and not onglet.conversationIcon then
texte:ClearAllPoints()
texte:SetPoint("CENTER", onglet, "CENTER", 0, G.ongletTexteY)
end
end
-- PanelTemplates_TabResize de camelot : le texte + 20 + la marge, au moins
-- les deux cotes ; a taille imposee, le texte prend le reste
function C.redimensionner(onglet, marge, taille, texteAbsolu)
local nom = onglet:GetName()
local texte = _G[nom .. "Text"]
if not texte then return end
marge = marge or 0
texte:SetWidth(texteAbsolu or 0)
local largeurTexte = texte:GetStringWidth()
local largeur = largeurTexte + G.ongletMarges + marge
local cotes = _G[nom .. "Left"]:GetWidth() + _G[nom .. "Right"]:GetWidth()
if taille then
largeur = math.max(taille, cotes)
largeurTexte = largeur - G.ongletMarges - marge
elseif largeur < cotes then
largeur = cotes
largeurTexte = largeur - G.ongletMarges - marge
end
texte:SetWidth(largeurTexte)
onglet:SetWidth(largeur)
end
-- FCFDock_SetPrimary de camelot : le dock sur le haut du fond du chat
-- principal
function C.poserAncrageDock(dock)
local primaire = dock and dock.primary
local fond = primaire and _G[primaire:GetName() .. "Background"]
if not fond then return end
dock:ClearAllPoints()
dock:SetPoint("BOTTOMLEFT", fond, "TOPLEFT", 0, 0)
dock:SetPoint("BOTTOMRIGHT", fond, "TOPRIGHT", 0, 0)
end
-- FCFDock_UpdateTabs de camelot : les onglets fixes depuis le bas a gauche
-- du dock, ceux des chuchotements depuis la gauche de la liste defilante,
-- chacun a 1 du precedent ; le bouton de debordement au bas a droite du
-- dock, la liste 5 avant lui
function C.poserDock(dock)
if not (dock and dock.DOCKED_CHAT_FRAMES and dock.scrollFrame) then return end
local enfant = dock.scrollFrame:GetScrollChild()
local dernierFixe, dernierMobile
for _, fenetre in ipairs(dock.DOCKED_CHAT_FRAMES) do
local onglet = _G[fenetre:GetName() .. "Tab"]
if onglet then
onglet:ClearAllPoints()
if fenetre.isStaticDocked then
if dernierFixe then
onglet:SetPoint("LEFT", dernierFixe, "RIGHT", G.ongletEcart, 0)
else
onglet:SetPoint("BOTTOMLEFT", dock, "BOTTOMLEFT", 0, 0)
end
dernierFixe = onglet
else
if dernierMobile then
onglet:SetPoint("LEFT", dernierMobile, "RIGHT", G.ongletEcart, 0)
elseif enfant then
onglet:SetPoint("LEFT", enfant, "LEFT", 0, G.ongletMobileY)
end
dernierMobile = onglet
end
end
end
local debord = dock.overflowButton
if debord then
debord:ClearAllPoints()
debord:SetPoint("BOTTOMRIGHT", dock, "BOTTOMRIGHT", 0, 0)
end
if debord and debord:IsShown() then
dock.scrollFrame:SetPoint("BOTTOMRIGHT", debord, "BOTTOMLEFT", -G.debordEcart, G.debordY)
else
dock.scrollFrame:SetPoint("BOTTOMRIGHT", dock, "BOTTOMRIGHT", 0, 0)
end
end
-- l'onglet d'une fenetre qu'on habille : l'art, puis la taille (celle que
-- le dock lui impose s'il est dans sa liste defilante)
local function habillerOngletDe(fenetre)
local onglet = _G[fenetre:GetName() .. "Tab"]
C.habillerOnglet(onglet)
if not C.onglets[onglet] then return end
local dock = GENERAL_CHAT_DOCK
local taille
if fenetre.isDocked and not fenetre.isStaticDocked and dock and dock.scrollFrame then
taille = dock.scrollFrame.dynTabSize
end
C.redimensionner(onglet, onglet.sizePadding or 0, taille)
end
-- ------------------------------------------------------------ la selection
-- Alt maintenu, on glisse sur le chat : le texte se surligne comme dans un
-- editeur. Le seul chemin vers le presse-papiers en 3.3.5 est Ctrl+C dans
-- une saisie : une saisie invisible, en lecture seule, porte le texte ; elle
-- ne prend le clavier que PENDANT QUE Ctrl EST ENFONCE (le C qui suit copie),
-- et le rend au relachement de Ctrl. Le reste du temps, le clavier est au
-- jeu : 3.3.5 ne sait pas voir une touche sans la lui prendre (ni IsKeyDown
-- ni transmission des touches).
-- TOUT AUTRE GESTE EFFACE LA SELECTION (demande du 2026-09-28, « le jeu garde
-- le clavier ») : Ctrl relache (Ctrl+C fait ou non), un clic, se deplacer,
-- tourner, sauter (hors taxi), lancer un sort, une autre saisie qui prend le
-- clavier (celle du chat), une fenetre qui s'ouvre (ShowUIPanel), Echap
-- (ToggleGameMenu), le chat qui defile ou dont un message selectionne
-- change, le chat cache. Une touche sans effet visible de l'addon laisse la
-- selection.
-- (demande de l'utilisateur, 2026-09-28 ; ni 3.3.5 ni camelot ne le font)
--
-- RELEVE EN JEU (/fui chatlignes, 2026-09-28) : le moteur fait UN objet
-- texte par message, coupe sur plusieurs lignes a la largeur du chat (5
-- lignes de 14,33 pour le message d'essai), sans retrait (l'objet texte ne
-- l'a pas, meme si le chat l'a) ; la meme police, a la meme largeur, rend
-- la meme hauteur, et la meme largeur a 0,7 pres. On retrouve donc les
-- coupures du moteur en mesurant : la hauteur du texte jusqu'a la fin de
-- chaque mot donne la ligne ou il tombe (le moteur coupe mot a mot, et un
-- debut de texte se coupe comme le texte entier) ; un mot plus long qu'une
-- ligne se mesure lettre a lettre.
local S = { cache = {} }
C.selection = S
-- une methode que le client n'a peut-etre pas : rien plutot qu'une erreur
local function appel(objet, methode, ...)
if not objet[methode] then return nil end
local r = { pcall(objet[methode], objet, ...) }
if not r[1] then return nil end
return r[2], r[3], r[4], r[5]
end
-- la couleur du surlignage (ce n'est pas un element de camelot)
S.couleur = { 0.3, 0.5, 1, 0.4 }
-- les marques de raid, telles qu'on les tape dans le chat : l'etiquette du
-- client (ICON_TAG_RAID_TARGET_*1 ; en anglais {rt1} a {rt8})
local MARQUES = { "STAR", "CIRCLE", "DIAMOND", "TRIANGLE", "MOON", "SQUARE", "CROSS", "SKULL" }
local function etiquetteImage(brut)
local n = tonumber(string.match(brut, "RaidTargetingIcon_(%d)"))
local nom = n and MARQUES[n] and _G["ICON_TAG_RAID_TARGET_" .. MARQUES[n] .. "1"]
return nom and ("{" .. nom .. "}") or ""
end
-- ce qui se voit d'un message, unite par unite : une lettre (UTF-8), une
-- barre (||), une image (|T...|t) ; les codes de couleur (|c, |r) et de
-- lien (|H...|h, |h) ne se voient pas -- le texte d'un lien, si
function C.unites(texte)
local u, i, n = {}, 1, string.len(texte or "")
while i <= n do
local c = string.sub(texte, i, i)
if c == "|" then
local s = string.sub(texte, i + 1, i + 1)
if s == "c" then
i = i + 10
elseif s == "r" or s == "h" then
i = i + 2
elseif s == "H" then
local fin = string.find(texte, "|h", i + 2, true)
i = fin and fin + 2 or n + 1
elseif s == "T" then
local fin = string.find(texte, "|t", i + 2, true) or n
local brut = string.sub(texte, i, fin + 1)
table.insert(u, { brut = brut, copie = etiquetteImage(brut) })
i = fin + 2
elseif s == "n" then
table.insert(u, { brut = "", copie = "\n" })
i = i + 2
else
table.insert(u, { brut = "||", copie = "|" })
i = i + (s == "|" and 2 or 1)
end
else
local b = string.byte(c)
local l = (b >= 240 and 4) or (b >= 224 and 3) or (b >= 192 and 2) or 1
local lettre = string.sub(texte, i, i + l - 1)
table.insert(u, { brut = lettre, copie = lettre, espace = (lettre == " ") })
i = i + l
end
end
return u
end
local function brut(unites, a, b)
local t = {}
for k = a, b do t[#t + 1] = unites[k].brut end
return table.concat(t)
end
-- deux objets texte de mesure, invisibles : l'un sur une ligne, l'autre a
-- la largeur du message ; ils prennent la police et les reglages de
-- l'objet texte mesure
local mesures
local function mesurer(r, c)
if not mesures then
local cadre = CreateFrame("Frame", nil, UIParent)
cadre:SetAlpha(0)
cadre:SetWidth(1)
cadre:SetHeight(1)
cadre:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 0, 0)
mesures = { ligne = cadre:CreateFontString(nil, "ARTWORK"), bloc = cadre:CreateFontString(nil, "ARTWORK") }
mesures.ligne:SetPoint("TOPLEFT", cadre, "TOPLEFT", 0, 0)
mesures.bloc:SetPoint("TOPLEFT", cadre, "TOPLEFT", 0, 0)
end
local police, taille, contour = r:GetFont()
for _, m in pairs(mesures) do
m:SetFont(police, taille, contour)
if m.SetSpacing then m:SetSpacing(appel(r, "GetSpacing") or 0) end
end
mesures.ligne:SetWidth(0)
mesures.bloc:SetWidth(c and c.largeur or r:GetWidth())
if mesures.bloc.SetIndentedWordWrap then mesures.bloc:SetIndentedWordWrap(appel(r, "GetIndentedWordWrap") and true or false) end
if mesures.bloc.SetNonSpaceWrap then mesures.bloc:SetNonSpaceWrap(appel(r, "CanNonSpaceWrap") and true or false) end
return mesures
end
-- la mise en lignes d'un objet texte du chat : ses unites, le debut de
-- chaque ligne, la hauteur d'une ligne ; gardee tant que son texte et sa
-- largeur ne changent pas
function C.disposer(r)
local texte, largeur = r:GetText() or "", r:GetWidth()
local c = S.cache[r]
if c and c.texte == texte and c.largeur == largeur then return c end
c = { texte = texte, largeur = largeur, largeurs = {} }
local m = mesurer(r, c)
m.ligne:SetText("A")
c.hauteurLigne = math.max(1, m.ligne:GetStringHeight())
local u = C.unites(texte)
c.unites = u
local function lignes(b)
m.bloc:SetText(brut(u, 1, b))
return math.floor(m.bloc:GetStringHeight() / c.hauteurLigne + 0.5)
end
local debuts, faites = { 1 }, 1
local a, n = 1, #u
while a <= n do
while a <= n and u[a].espace do a = a + 1 end
if a > n then break end
local b = a
while b < n and not u[b + 1].espace do b = b + 1 end
local L = lignes(b)
if L > faites then
m.ligne:SetText(brut(u, a, b))
if m.ligne:GetStringWidth() <= largeur then
-- le mot passe a la ligne suivante
for k = faites + 1, L do debuts[k] = a end
else
-- un mot plus long qu'une ligne : coupe a la lettre
for v = a, b do
local Lv = lignes(v)
for k = faites + 1, Lv do debuts[k] = v end
if Lv > faites then faites = Lv end
end
end
faites = math.max(faites, L)
end
a = b + 1
end
c.debuts, c.nLignes = debuts, #debuts
S.cache[r] = c
return c
end
-- la largeur, depuis le debut de la ligne n, jusqu'a la frontiere b (avant
-- l'unite b)
local function largeurJusqua(r, c, n, b)
local debut = c.debuts[n]
if b <= debut then return 0 end
local cle = n * 100000 + b
if not c.largeurs[cle] then
local m = mesurer(r, c)
m.ligne:SetText(brut(c.unites, debut, b - 1))
c.largeurs[cle] = m.ligne:GetStringWidth()
end
return c.largeurs[cle]
end
-- les messages affiches, de haut en bas
function C.messagesVisibles(fenetre)
local liste = {}
for _, r in ipairs({ fenetre:GetRegions() }) do
if r:GetObjectType() == "FontString" and r:IsVisible() and (r:GetAlpha() or 0) > 0.01
and (r:GetText() or "") ~= "" and r:GetTop() then
table.insert(liste, r)
end
end
table.sort(liste, function(p, q) return p:GetTop() > q:GetTop() end)
return liste
end
-- le curseur, dans l'unite du chat
local function curseur(fenetre)
local x, y = GetCursorPosition()
local e = fenetre:GetEffectiveScale()
return x / e, y / e
end
-- sous le curseur : le rang du message et la frontiere la plus proche ;
-- au-dessus du premier, son debut ; sous le dernier, sa fin
function C.pointer(liste, x, y)
if #liste == 0 then return nil end
if y > liste[1]:GetTop() then return 1, 1 end
for rang, r in ipairs(liste) do
if y >= r:GetBottom() or rang == #liste then
local c = C.disposer(r)
if y < r:GetBottom() then return rang, #c.unites + 1 end
local n = math.min(c.nLignes, math.max(1, math.floor((r:GetTop() - y) / c.hauteurLigne) + 1))
local debut = c.debuts[n]
local fin = c.debuts[n + 1] or (#c.unites + 1)
local xr = x - r:GetLeft()
local bas, haut = debut, fin
while bas < haut do
local milieu = math.floor((bas + haut) / 2)
if largeurJusqua(r, c, n, milieu) < xr then bas = milieu + 1 else haut = milieu end
end
local b = bas
if b > debut and (xr - largeurJusqua(r, c, n, b - 1)) < (largeurJusqua(r, c, n, b) - xr) then
b = b - 1
end
return rang, b
end
end
end
-- l'ancre et le bout, dans l'ordre de lecture
local function bornes()
local a, b = S.ancre, S.bout
if a[1] > b[1] or (a[1] == b[1] and a[2] > b[2]) then a, b = b, a end
return a, b
end
-- le surlignage : un rectangle par morceau de ligne, sur le chat lui-meme
-- (calque BORDER : sous le texte)
function C.peindre()
local d = S.fenetre and C.fenetres[S.fenetre]
if not d then return end
local k = 0
if S.ancre and S.bout then
local a, b = bornes()
for rang = a[1], b[1] do
local r = S.messages[rang]
local c = C.disposer(r)
local du = (rang == a[1]) and a[2] or 1
local au = (rang == b[1]) and b[2] or (#c.unites + 1)
for n = 1, c.nLignes do
local p = math.max(c.debuts[n], du)
local q = math.min(c.debuts[n + 1] or (#c.unites + 1), au)
if p < q then
local x1, x2 = largeurJusqua(r, c, n, p), largeurJusqua(r, c, n, q)
if x2 > x1 then
k = k + 1
local t = d.surlignes[k]
if not t then
t = S.fenetre:CreateTexture(nil, "BORDER")
t:SetTexture(S.couleur[1], S.couleur[2], S.couleur[3], S.couleur[4])
d.surlignes[k] = t
end
t:ClearAllPoints()
t:SetPoint("TOPLEFT", r, "TOPLEFT", x1, -(n - 1) * c.hauteurLigne)
t:SetWidth(x2 - x1)
t:SetHeight(c.hauteurLigne)
t:Show()
end
end
end
end
end
for i = k + 1, #d.surlignes do d.surlignes[i]:Hide() end
end
-- le texte a copier : ce qui se voit, les liens par leur texte, les
-- marques par leur nom ; un message par ligne
function C.texteSelection()
if not (S.ancre and S.bout) then return "" end
local a, b = bornes()
local parties = {}
for rang = a[1], b[1] do
local c = C.disposer(S.messages[rang])
local du = (rang == a[1]) and a[2] or 1
local au = (rang == b[1]) and b[2] or (#c.unites + 1)
local t = {}
for k = du, au - 1 do t[#t + 1] = c.unites[k].copie end
parties[#parties + 1] = table.concat(t)
end
return table.concat(parties, "\n")
end
-- la saisie invisible qui porte la copie ; en lecture seule : ce qu'on y
-- tape est defait
local boite = CreateFrame("EditBox", nil, UIParent)
boite:SetMultiLine(true)
boite:SetAutoFocus(false)
boite:SetFontObject(ChatFontNormal)
boite:SetWidth(200)
boite:SetHeight(20)
boite:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", 0, 0)
boite:SetAlpha(0)
boite:EnableMouse(false)
S.boite = boite
function C.effacer()
local d = S.fenetre and C.fenetres[S.fenetre]
S.fenetre, S.messages, S.ancre, S.bout, S.glisse, S.texte = nil, nil, nil, nil, false, nil
S.ctrl, S.base = false, nil
if d then
for _, t in ipairs(d.surlignes) do t:Hide() end
end
if boite:HasFocus() then boite:ClearFocus() end
end
boite:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
boite:SetScript("OnEnterPressed", function(self) self:ClearFocus() end)
boite:SetScript("OnEditFocusLost", function()
if S.texte then C.effacer() end
end)
boite:SetScript("OnTextChanged", function(self, saisi)
if saisi and S.texte then
self:SetText(S.texte)
self:HighlightText()
end
end)
function C.commencer(fenetre)
C.effacer()
local liste = C.messagesVisibles(fenetre)
local rang, b = C.pointer(liste, curseur(fenetre))
if not rang then return end
S.fenetre, S.messages, S.glisse = fenetre, liste, true
S.ancre, S.bout = { rang, b }, { rang, b }
C.peindre()
end
function C.suivre()
local rang, b = C.pointer(S.messages, curseur(S.fenetre))
if rang and (rang ~= S.bout[1] or b ~= S.bout[2]) then
S.bout = { rang, b }
C.peindre()
end
end
function C.terminer()
if not S.glisse then return end
C.suivre()
S.glisse = false
local texte = C.texteSelection()
if texte == "" then
C.effacer()
return
end
S.texte = texte
boite:SetText(texte)
-- ce qu'on surveille pour effacer : le personnage, le defilement du
-- chat, le texte des messages selectionnes
local vitesse = GetUnitSpeed and GetUnitSpeed("player") or 0
S.base = {
taxi = UnitOnTaxi and UnitOnTaxi("player") and true or false,
vitesse = vitesse,
face = GetPlayerFacing and GetPlayerFacing() or 0,
chute = IsFalling and IsFalling() and true or false,
defilement = S.fenetre.GetCurrentScroll and S.fenetre:GetCurrentScroll() or 0,
textes = {},
}
for rang, r in ipairs(S.messages) do S.base.textes[rang] = r:GetText() end
end
-- un geste autre que Ctrl+C, une fois la selection faite ?
function C.autreGeste()
local b = S.base
if IsMouseButtonDown("LeftButton") or IsMouseButtonDown("RightButton") or IsMouseButtonDown("MiddleButton") then
return true
end
if not S.fenetre:IsVisible() then return true end
local focus = GetCurrentKeyBoardFocus and GetCurrentKeyBoardFocus()
if focus and focus ~= boite then return true end
if not b.taxi then
if (GetUnitSpeed and GetUnitSpeed("player") or 0) ~= b.vitesse then return true end
if math.abs((GetPlayerFacing and GetPlayerFacing() or 0) - b.face) > 0.001 then return true end
if (IsFalling and IsFalling() and true or false) ~= b.chute then return true end
end
if (S.fenetre.GetCurrentScroll and S.fenetre:GetCurrentScroll() or 0) ~= b.defilement then return true end
for rang, r in ipairs(S.messages) do
if r:GetText() ~= b.textes[rang] then return true end
end
return false
end
-- une fois la selection faite : Ctrl enfonce, la saisie prend le clavier ;
-- Ctrl relache, ou tout autre geste, et la selection s'efface
function C.veillerCopie()
if IsControlKeyDown() then
if not S.ctrl then
S.ctrl = true
boite:SetText(S.texte)
boite:SetFocus()
boite:HighlightText()
end
elseif S.ctrl then
C.effacer()
return
end
if C.autreGeste() then C.effacer() end
end
-- la surface qui prend la souris sur le chat, Alt maintenu seulement : sans
-- Alt, le chat laisse passer les clics (monde, liens) comme avant
function C.creerCapteur(fenetre, d)
d.surlignes = {}
local capteur = CreateFrame("Frame", nil, fenetre)
capteur:SetAllPoints(fenetre)
capteur:EnableMouse(false)
capteur:SetScript("OnMouseDown", function(_, bouton)
if bouton == "LeftButton" then C.commencer(fenetre) end
end)
capteur:SetScript("OnMouseUp", function(_, bouton)
if bouton == "LeftButton" then C.terminer() end
end)
d.capteur, d.prise = capteur, false
end
local veilleSelection = CreateFrame("Frame")
veilleSelection:SetScript("OnUpdate", function()
local alt = IsAltKeyDown() and true or false
for fenetre, d in pairs(C.fenetres) do
local prise = ((alt and fenetre:IsVisible()) or (S.glisse and S.fenetre == fenetre)) and true or false
if prise ~= d.prise then
d.prise = prise
d.capteur:EnableMouse(prise)
end
end
if S.glisse then
C.suivre()
elseif S.texte then
C.veillerCopie()
end
end)
C.veilleSelection = veilleSelection
-- un sort lance, une fenetre ouverte, Echap : la selection s'efface
local function horsCopie()
if S.texte and not S.glisse then C.effacer() end
end
veilleSelection:RegisterEvent("UNIT_SPELLCAST_SENT")
veilleSelection:SetScript("OnEvent", function(_, _, unite)
if unite == "player" then horsCopie() end
end)
if ShowUIPanel then hooksecurefunc("ShowUIPanel", horsCopie) end
if ToggleGameMenu then hooksecurefunc("ToggleGameMenu", horsCopie) end
-- ------------------------------------------------------------ une fenetre
function C.habiller(fenetre)
if not fenetre or C.fenetres[fenetre] or not fenetre.buttonFrame then return end
local nom = fenetre:GetName()
-- le fond deborde a droite pour loger la barre
local fond = _G[nom .. "Background"]
if fond then
fond:ClearAllPoints()
fond:SetPoint("TOPLEFT", fenetre, "TOPLEFT", G.fondGauche, G.fondHaut)
fond:SetPoint("TOPRIGHT", fenetre, "TOPRIGHT", G.fondDroite, G.fondHaut)
fond:SetPoint("BOTTOMLEFT", fenetre, "BOTTOMLEFT", G.fondGauche, G.fondBas)
fond:SetPoint("BOTTOMRIGHT", fenetre, "BOTTOMRIGHT", G.fondDroite, G.fondBas)
end
cacherFleches(fenetre)
fenetre:HookScript("OnShow", cacherFleches)
local bf = fenetre.buttonFrame
if bf.minimizeButton then
bf.minimizeButton:ClearAllPoints()
bf.minimizeButton:SetPoint("TOP", bf, "TOP", 0, G.minimiserY)
end
poserColonne(fenetre)
local d = { fenetre = fenetre }
d.retour = creerRetour(fenetre)
local barre = ForeverUI.CreateScrollBar(nom .. "ForeverScrollBar", fenetre, fenetre)
barre:ClearAllPoints()
barre:SetPoint("TOPLEFT", fenetre, "TOPRIGHT", 0, 0)
barre:SetPoint("BOTTOMLEFT", d.retour, "TOPLEFT", 0, G.barreBas)
barre:SetAlpha(0)
barre.surDefilement = function(rang) C.defiler(fenetre, rang) end
d.barre = barre
-- la saisie : TOPLEFT comme 3.3.5, le bord droit sur celui de la barre,
-- + 8 (camelot : RIGHT sur la barre ; le meme point, pris ici sur le
-- chat -- la barre part de son TOPRIGHT -- pour que la hauteur reste
-- donnee par le seul TOPLEFT)
local saisie = fenetre.editBox
if saisie then
saisie:ClearAllPoints()
saisie:SetPoint("TOPLEFT", fenetre, "BOTTOMLEFT", G.saisieX, G.saisieY)
saisie:SetPoint("TOPRIGHT", fenetre, "BOTTOMRIGHT", barre:GetWidth() + G.saisieDroite, G.saisieY)
end
habillerOngletDe(fenetre)
-- les alphas vises : la barre et le retour s'allument avec le chat
d.cibleBarre, d.cibleRetour = 0, 0
d.vitesseBarre, d.vitesseRetour = 1, 1
d.flashTemps = 0
C.fenetres[fenetre] = d
C.creerCapteur(fenetre, d)
C.releverRepos(fenetre)
C.doubler(fenetre)
d.allume = fenetre.hasBeenFaded and true or false
d.survol, d.dedans, d.dehors = false, 0, 0
d.repos = d.allume and 1 or 0
d.cibleRepos = d.repos
d.vitesseRepos = 1
C.poserRepos(fenetre)
C.majBarre(fenetre)
end
-- ------------------------------------------------------------ le fondu
local function viser(d, barre, retour, dureeBarre, dureeRetour)
d.cibleBarre = barre
d.cibleRetour = retour
d.vitesseBarre = G.barreAlpha / math.max(0.01, dureeBarre)
d.vitesseRetour = G.retourAlpha / math.max(0.01, dureeRetour)
end
local function allumer(d)
viser(d, G.barreAlpha, G.retourAlpha, G.venue, G.venueRetour)
d.cibleRepos, d.vitesseRepos = 1, 1 / G.venue
end
local function eteindre(d)
viser(d, 0, 0, G.depart, G.depart)
d.cibleRepos, d.vitesseRepos = 0, 1 / G.depart
end
-- les zones de FCF_OnUpdate, plus la barre et le retour (hors du chat)
local function survolee(fenetre, d)
if not fenetre:IsShown() then return false end
local haut = 28
if IsCombatLog and CombatLogQuickButtonFrame_Custom and IsCombatLog(fenetre) then
haut = haut + CombatLogQuickButtonFrame_Custom:GetHeight()
end
return (fenetre:IsMouseOver(haut, -2, -2, 2)
or (fenetre.isDocked and FriendsMicroButton and FriendsMicroButton:IsMouseOver())
or (fenetre.buttonFrame and fenetre.buttonFrame:IsMouseOver())
or (d.barre:IsShown() and MouseIsOver(d.barre)) or MouseIsOver(d.retour)) and true or false
end
C.survolee = survolee
local function approcher(objet, cible, vitesse, ecoule)
local a = objet:GetAlpha()
if a < cible then
a = math.min(cible, a + vitesse * ecoule)
elseif a > cible then
a = math.max(cible, a - vitesse * ecoule)
end
objet:SetAlpha(a)
end
-- chaque image : la barre suit le chat, le fondu avance, le retour clignote
-- tant qu'on n'est pas en bas
local moteur = CreateFrame("Frame")
moteur:SetScript("OnUpdate", function(self, ecoule)
ecoule = ecoule or 0
-- le survol : 0,2 s dedans pour s'allumer, 1 s dehors pour s'eteindre
local montrer, cacher = CHAT_TAB_SHOW_DELAY or 0.2, CHAT_TAB_HIDE_DELAY or 1
local dock = false
for fenetre, d in pairs(C.fenetres) do
if survolee(fenetre, d) then
d.dedans, d.dehors = d.dedans + ecoule, 0
if d.dedans >= montrer then d.survol = true end
else
d.dedans, d.dehors = 0, d.dehors + ecoule
if d.dehors >= cacher then d.survol = false end
end
if d.survol and fenetre.isDocked then dock = true end
end
for fenetre, d in pairs(C.fenetres) do
local allume = (d.survol or fenetre.hasBeenFaded or (dock and fenetre.isDocked)) and true or false
if allume ~= d.allume then
d.allume = allume
if allume then allumer(d) else eteindre(d) end
end
if d.repos < d.cibleRepos then
d.repos = math.min(d.cibleRepos, d.repos + d.vitesseRepos * ecoule)
elseif d.repos > d.cibleRepos then
d.repos = math.max(d.cibleRepos, d.repos - d.vitesseRepos * ecoule)
end
C.poserRepos(fenetre)
C.poserDoublure(fenetre)
if fenetre:IsShown() then
C.majBarre(fenetre)
local enBas = fenetre:AtBottom()
if enBas then
d.flashTemps = 0
d.retour.flash:SetAlpha(0)
else
-- 0,1 s pour venir, 0,5 s tenu, 0,1 s pour partir, 0,5 s eteint
local cycle = 2 * (G.flashVenue + G.flashTenu)
d.flashTemps = (d.flashTemps + ecoule) % cycle
local t = d.flashTemps
local a
if t < G.flashVenue then a = t / G.flashVenue
elseif t < G.flashVenue + G.flashTenu then a = 1
elseif t < 2 * G.flashVenue + G.flashTenu then a = 1 - (t - G.flashVenue - G.flashTenu) / G.flashVenue
else a = 0 end
d.retour.flash:SetAlpha(a)
end
-- pas en bas : la barre s'allume, et le retour reste visible
local cibleBarre = enBas and d.cibleBarre or G.barreAlpha
local cibleRetour = enBas and d.cibleRetour or 1
-- pas en bas, la barre vient comme au fondu (FCF_FadeInScrollbar)
approcher(d.barre, cibleBarre, enBas and d.vitesseBarre or G.barreAlpha / G.venue, ecoule)
approcher(d.retour, cibleRetour, math.max(d.vitesseRetour, G.retourAlpha / G.venueRetour), ecoule)
end
end
end)
C.moteur = moteur
-- ------------------------------------------------------------ le branchement
function C.habillerTout()
for _, nom in ipairs(CHAT_FRAMES or {}) do
C.habiller(_G[nom])
end
for i = 1, (NUM_CHAT_WINDOWS or 10) do
C.habiller(_G["ChatFrame" .. i])
end
habillerAmis()
-- le bouton de menu, en bas de la colonne du chat par defaut
if ChatFrameMenuButton and ChatFrame1ButtonFrame then
ChatFrameMenuButton:ClearAllPoints()
ChatFrameMenuButton:SetPoint("BOTTOM", ChatFrame1ButtonFrame, "BOTTOM", 0, 0)
end
-- le dock, et ses onglets (ceux d'une fenetre qu'on vient d'habiller
-- ont ete poses par le client avant elle)
C.poserAncrageDock(GENERAL_CHAT_DOCK)
C.poserDock(GENERAL_CHAT_DOCK)
end
C.habillerTout()
local veille = CreateFrame("Frame")
veille:RegisterEvent("ADDON_LOADED")
veille:SetScript("OnEvent", function(self, _, addon)
if addon ~= "ForeverUI" then return end
self:UnregisterEvent("ADDON_LOADED")
ForeverUIDB = ForeverUIDB or {}
if ForeverUIDB.chatFondRepos then return end
for i = 1, (NUM_CHAT_WINDOWS or 10) do
SetChatWindowAlpha(i, G.fondRepos)
end
ForeverUIDB.chatFondRepos = true
end)
C.veille = veille
if FCF_SetButtonSide then
hooksecurefunc("FCF_SetButtonSide", function(fenetre)
if C.fenetres[fenetre] then poserColonne(fenetre) end
if fenetre == DEFAULT_CHAT_FRAME then poserAmis() end
end)
end
if FCF_OpenTemporaryWindow then hooksecurefunc("FCF_OpenTemporaryWindow", C.habillerTout) end
-- la taille des onglets du chat (et d'eux seuls : la fonction sert a tous
-- les onglets du jeu) ; le dock, apres chaque remise en ordre du client
if PanelTemplates_TabResize then
hooksecurefunc("PanelTemplates_TabResize", function(onglet, marge, taille, _, texteAbsolu)
if C.onglets[onglet] then C.redimensionner(onglet, marge, taille, texteAbsolu) end
end)
end
if FCFDock_UpdateTabs then hooksecurefunc("FCFDock_UpdateTabs", C.poserDock) end
if FCFDock_SetPrimary then hooksecurefunc("FCFDock_SetPrimary", C.poserAncrageDock) end
-- TEMOIN -- /fui chat
function ForeverUI.ChatDebug()
local dire = function(t) DEFAULT_CHAT_FRAME:AddMessage("|cff66ccffForeverUI|r " .. t) end
for fenetre, d in pairs(C.fenetres) do
if fenetre:IsShown() then
local total, visibles, rang, maxi = etatDefilement(fenetre)
dire(string.format("chat %s : %d messages, %d visibles, rang %d/%d, en bas %s | barre %s alpha %.2f | retour alpha %.2f | SetScrollOffset %s",
fenetre:GetName(), total, visibles, rang, maxi, tostring(fenetre:AtBottom()),
d.barre:IsShown() and "affichee" or "cachee", d.barre:GetAlpha(), d.retour:GetAlpha(),
fenetre.SetScrollOffset and "oui" or "non"))
-- ce que FCF_OnUpdate regarde avant d'eteindre le chat, et nos pieces
local function oui(v) return v and "OUI" or "non" end
dire(string.format(" allume %s (client %s, survol %s), dehors depuis %.1f s | souris sur : chat %s, amis %s, colonne %s, barre %s, retour %s",
oui(d.allume), oui(fenetre.hasBeenFaded), oui(d.survol), d.dehors,
oui(fenetre:IsMouseOver(28, -2, -2, 2)), oui(fenetre.isDocked and FriendsMicroButton and FriendsMicroButton:IsMouseOver()),
oui(fenetre.buttonFrame and fenetre.buttonFrame:IsMouseOver()),
oui(d.barre:IsShown() and MouseIsOver(d.barre)), oui(MouseIsOver(d.retour))))
local _, _, _, _, _, opacite = GetChatWindowInfo(fenetre:GetID())
dire(string.format(" repos : coefficient %.2f (vise %d) sur %d pieces + %d de saisie | fond %.2f (retenue %.2f, faite %s)",
d.repos, d.cibleRepos, #d.effaces, #d.saisie, _G[fenetre:GetName() .. "Background"]:GetAlpha(),
opacite or -1, oui(ForeverUIDB and ForeverUIDB.chatFondRepos)))
end
end
end
-- RELEVE -- /fui chatlignes (2026-09-28, avant la selection au glisser) :
-- comment le moteur pose les lignes du chat -- un objet texte par message,
-- ou un par ligne a l'ecran -- et si une mesure avec la meme police rend la
-- meme largeur. Un long message d'essai est ajoute a la fenetre affichee ;
-- deux images plus tard, ses objets texte sont releves dans
-- ForeverUIDB.releveChat (ecrit sur le disque au /reload suivant).
local ESSAI = "ForeverUI, essai de selection : une ligne assez longue pour etre coupee par le chat,"
.. " avec des accents (\195\169 \195\168 \195\160 \195\167), un lien |cffffffff|Hitem:6948:0:0:0:0:0:0:0:80|h[Hearthstone]|h|r,"
.. " une icone |TInterface" .. SEP .. "TargetingFrame" .. SEP .. "UI-RaidTargetingIcon_1:0|t"
.. " et un motsansespacetreslongpourvoircommentlemoteurlecoupequandiln'yapasdespacedutout fin."
local support, mesure
function C.releverLignes(fenetre)
if not support then
support = CreateFrame("Frame", nil, UIParent)
support:SetAlpha(0)
support:SetWidth(1)
support:SetHeight(1)
support:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 0, 0)
mesure = support:CreateFontString(nil, "ARTWORK")
mesure:SetPoint("TOPLEFT", support, "TOPLEFT", 0, 0)
end
local rel = { fenetre = fenetre:GetName() }
rel.gauche, rel.haut, rel.droite, rel.bas = fenetre:GetLeft(), fenetre:GetTop(), fenetre:GetRight(), fenetre:GetBottom()
rel.largeur, rel.hauteur, rel.echelle = fenetre:GetWidth(), fenetre:GetHeight(), fenetre:GetEffectiveScale()
rel.police, rel.taille, rel.contour = fenetre:GetFont()
rel.espacement = appel(fenetre, "GetSpacing")
rel.justification = appel(fenetre, "GetJustifyH")
rel.retrait = appel(fenetre, "GetIndentedWordWrap")
rel.insertion = appel(fenetre, "GetInsertMode")
rel.messages = appel(fenetre, "GetNumMessages")
rel.lignesAffichees = appel(fenetre, "GetNumLinesDisplayed")
rel.ligneCourante = appel(fenetre, "GetCurrentLine")
rel.defilement = appel(fenetre, "GetCurrentScroll")
-- les derniers messages, tels que le client les garde
rel.derniers = {}
local n = rel.messages or 0
for i = math.max(1, n - 5), n do
table.insert(rel.derniers, { rang = i, texte = (appel(fenetre, "GetMessageInfo", i)) })
end
-- les objets texte de la fenetre
rel.textes = {}
local visibles = 0
for rang, r in ipairs({ fenetre:GetRegions() }) do
if r:GetObjectType() == "FontString" then
local t = { rang = rang }
t.montre, t.visible, t.alpha = r:IsShown() and true or false, r:IsVisible() and true or false, r:GetAlpha()
t.gauche, t.haut, t.droite, t.bas = r:GetLeft(), r:GetTop(), r:GetRight(), r:GetBottom()
t.largeur, t.hauteur = r:GetWidth(), r:GetHeight()
t.largeurTexte, t.hauteurTexte = r:GetStringWidth(), appel(r, "GetStringHeight")
t.texte = r:GetText()
t.rouge, t.vert, t.bleu, t.opacite = r:GetTextColor()
t.police, t.taille, t.contour = r:GetFont()
t.justification = appel(r, "GetJustifyH")
t.coupure = appel(r, "CanWordWrap")
t.retrait = appel(r, "GetIndentedWordWrap")
t.points = r:GetNumPoints()
if t.points > 0 then
local p, cible, pc, x, y = r:GetPoint(1)
t.point = string.format("%s %s %s %.2f %.2f", tostring(p),
cible and cible.GetName and (cible:GetName() or "?") or "nil", tostring(pc), x or 0, y or 0)
end
-- la mesure qu'on ferait : meme police, sur une ligne, puis a la
-- largeur de la fenetre
if t.texte and t.police then
mesure:SetFont(t.police, t.taille, t.contour)
mesure:SetWidth(0)
mesure:SetText(t.texte)
t.mesure = mesure:GetStringWidth()
t.mesureHauteur = mesure:GetStringHeight()
mesure:SetWidth(rel.largeur)
t.mesureCoupee = mesure:GetStringHeight()
end
if t.visible then visibles = visibles + 1 end
table.insert(rel.textes, t)
end
end
ForeverUIDB = ForeverUIDB or {}
ForeverUIDB.releveChat = rel
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff66ccffForeverUI|r releve du chat %s : %d objets texte (%d visibles), %d messages ; faites /reload pour l'enregistrer.",
rel.fenetre, #rel.textes, visibles, n))
end
local attente = CreateFrame("Frame")
attente:Hide()
attente:SetScript("OnUpdate", function(self)
self.images = self.images + 1
if self.images < 3 then return end
self:Hide()
C.releverLignes(self.fenetre)
end)
C.attenteReleve = attente
function ForeverUI.ChatReleveLignes()
local fenetre = (GENERAL_CHAT_DOCK and GENERAL_CHAT_DOCK.selected) or DEFAULT_CHAT_FRAME
if not (fenetre and fenetre:IsShown()) then fenetre = DEFAULT_CHAT_FRAME end
fenetre:AddMessage(ESSAI, 1, 1, 0)
attente.fenetre, attente.images = fenetre, 0
attente:Show()
end