c7c4d52197
- 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>
1225 lines
47 KiB
Lua
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
|