Files
moon-well-ui/data/addon/ForeverUI/Chat.lua
T
Adrestie c7c4d52197 Chat a la DA de camelot : onglets, dock, saisie et selection du texte
- onglets (Chat.lua) : l'art de ChatTabArtTemplate (cotes a -2 / +2, milieu
  tendu), texte au centre (0, -5), largeur texte + 20 + marge (au moins 32)
  par accroche de PanelTemplates_TabResize, pour les seuls onglets du chat
- dock : pose sur le haut du fond du chat principal (FCFDock_SetPrimary),
  onglets a 1 l'un de l'autre, bouton de debordement au bas a droite
  (FCFDock_UpdateTabs)
- saisie : jusqu'au bout de la barre de defilement + 8
- selection : Alt maintenu, glisser sur le chat surligne le texte comme dans
  un editeur ; au relacher, il passe dans une saisie invisible en lecture
  seule (Ctrl+C) ; coupures du moteur retrouvees par mesure ; liens copies
  par leur texte, marques par leur etiquette
- /fui chatlignes : releve de la mise en lignes du chat (ForeverUIDB)
- banc : onglets, dock, PanelTemplates_TabResize et FCFDock de 3.3.5,
  fenetres de chuchotement, touches Alt / Ctrl, coupure mot a mot

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

1225 lines
47 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 ; au relacher, il part, surligne, dans une saisie invisible qui
-- prend le clavier -- Ctrl+C le copie (le seul chemin vers le presse-papiers
-- en 3.3.5), Echap, Entree ou un clic ailleurs rendent la main.
-- (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
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)
-- Ctrl+C fait : la main revient au jeu (si la saisie connait OnKeyUp ;
-- sinon Echap, Entree ou un clic ailleurs)
if boite.HasScript and boite:HasScript("OnKeyUp") then
boite:SetScript("OnKeyUp", function(self, touche)
if touche == "C" and IsControlKeyDown() then self:ClearFocus() end
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)
boite:SetFocus()
boite:HighlightText()
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() end
end)
C.veilleSelection = veilleSelection
-- ------------------------------------------------------------ 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