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