ca2b78e217
/fui keeps edit mode, reset and list. The thirty diagnostic and tuning functions behind the other subcommands (bar, bags, character, tabs, close, model, minimap, mouse, map, questlog, chat, chatlines, tabard...) go with them, with Panes.Report, the bags read-back and the 205 texts they alone used. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1039 lines
36 KiB
Lua
1039 lines
36 KiB
Lua
-- ForeverUI: the chat window with Camelot's layout. The art files are the same as in 3.3.5;
|
|
-- only the positions change, plus a scroll bar, a scroll-to-bottom button and Camelot's friends
|
|
-- button. The WotLK chat code is kept: no function is replaced, the ones that reset what we
|
|
-- change are hooked (hooksecurefunc). At rest only the messages stay visible.
|
|
|
|
local ForeverUI = ForeverUI or {}
|
|
_G.ForeverUI = ForeverUI
|
|
|
|
local C = {}
|
|
ForeverUI.Chat = C
|
|
|
|
local SEP = string.char(92)
|
|
local L = ForeverUI.L
|
|
|
|
-- Camelot layout values (FloatingChatFrame, FCF_SetButtonSide, MinimalScrollBar,
|
|
-- ScrollToBottomButton, ChatTabArtTemplate, FCFDock_*)
|
|
local G = {
|
|
backgroundLeft = -2, backgroundTop = 3, backgroundRight = 7 + 8, backgroundBottom = -6,
|
|
columnX = 3, columnTop = -3, columnBottom = 6,
|
|
minimizeY = 4,
|
|
friends = 32, friendsY = 27,
|
|
backW = 17, backH = 15, returnX = -2, returnY = -2, barBottom = 2,
|
|
barAlpha = 0.6, returnAlpha = 0.65,
|
|
fadeIn = 0.15, returnFadeIn = 0.1, origin = 2.0,
|
|
idleBackground = 0,
|
|
flashFadeIn = 0.1, flashHold = 0.5,
|
|
lineSpacing = 2,
|
|
tabSide = 2, tabMargins = 20, tabTextY = -5,
|
|
tabGap = 1, mobileTabY = -1, overflowGap = 5, overflowY = -1,
|
|
inputX = -5, inputY = -2, inputRight = 8,
|
|
}
|
|
|
|
local ATLAS = {
|
|
backButton = "minimal-scrollbar-arrow-returntobottom-c60",
|
|
returnDown = "minimal-scrollbar-arrow-returntobottom-down-c60",
|
|
returnHover = "minimal-scrollbar-arrow-returntobottom-over-c60",
|
|
friends = "quickjoin-button-friendslist-up",
|
|
friendsDown = "quickjoin-button-friendslist-down",
|
|
}
|
|
|
|
C.windows = {}
|
|
|
|
-- ------------------------------------------------------------ button column
|
|
|
|
-- Camelot's FCF_SetButtonSide: the button column is anchored to the background
|
|
local function placeColumn(window)
|
|
local bf = window.buttonFrame
|
|
local background = _G[window:GetName() .. "Background"]
|
|
if not (bf and background) then return end
|
|
bf:ClearAllPoints()
|
|
if window.buttonSide == "right" then
|
|
bf:SetPoint("TOPLEFT", background, "TOPRIGHT", G.columnX, G.columnTop)
|
|
bf:SetPoint("BOTTOMLEFT", background, "BOTTOMRIGHT", G.columnX, G.columnBottom)
|
|
else
|
|
bf:SetPoint("TOPRIGHT", background, "TOPLEFT", -G.columnX, G.columnTop)
|
|
bf:SetPoint("BOTTOMRIGHT", background, "BOTTOMLEFT", -G.columnX, G.columnBottom)
|
|
end
|
|
end
|
|
|
|
-- Hides the up / down / bottom buttons; the window's OnShow shows them again
|
|
local function hideArrows(window)
|
|
local name = window:GetName()
|
|
for _, suffix in ipairs({ "ButtonFrameUpButton", "ButtonFrameDownButton", "ButtonFrameBottomButton" }) do
|
|
local b = _G[name .. suffix]
|
|
if b then
|
|
b:SetAlpha(0)
|
|
b:EnableMouse(false)
|
|
b:Hide()
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Friends button: left or right of the default chat's button column
|
|
local function placeFriends()
|
|
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.friendsY)
|
|
else
|
|
b:SetPoint("BOTTOMLEFT", bf, "TOPLEFT", 0, G.friendsY)
|
|
end
|
|
end
|
|
|
|
local function skinFriends()
|
|
local b = FriendsMicroButton
|
|
if not b or b.foreverChat then return end
|
|
b.foreverChat = true
|
|
b:SetWidth(G.friends)
|
|
b:SetHeight(G.friends)
|
|
for _, state in ipairs({
|
|
{ "SetNormalTexture", "GetNormalTexture", ATLAS.friends },
|
|
{ "SetPushedTexture", "GetPushedTexture", ATLAS.friendsDown },
|
|
}) do
|
|
local e = ForeverUI.AtlasEntry(state[3])
|
|
b[state[1]](b, e and e[1] or "")
|
|
local t = b[state[2]](b)
|
|
if t then
|
|
ForeverUI.SetAtlas(t, state[3], true)
|
|
t:ClearAllPoints()
|
|
t:SetAllPoints(b)
|
|
end
|
|
end
|
|
placeFriends()
|
|
end
|
|
|
|
-- ------------------------------------------------------------ scroll bar
|
|
|
|
local function fontSize(window)
|
|
local _, size = window:GetFont()
|
|
return size or 14
|
|
end
|
|
|
|
-- What the bar shows: message count, how many fit, and the position in messages from the top.
|
|
-- 3.3.5 counts the scroll in messages from the bottom (GetCurrentScroll).
|
|
local function scrollState(window)
|
|
local total = window:GetNumMessages() or 0
|
|
local visibleCount = math.max(1, math.floor((window:GetHeight() or 0) / (fontSize(window) + G.lineSpacing)))
|
|
local maxValue = math.max(0, total - visibleCount)
|
|
local fromBottom = window.GetCurrentScroll and window:GetCurrentScroll() or 0
|
|
return total, visibleCount, math.max(0, math.min(maxValue, maxValue - fromBottom)), maxValue
|
|
end
|
|
|
|
-- Moving the bar scrolls the chat by the difference (SetScrollOffset when the client has it)
|
|
function C.scrollTo(window, rank)
|
|
local _, _, current, maxValue = scrollState(window)
|
|
local wanted = math.max(0, math.min(maxValue, rank))
|
|
if window.SetScrollOffset then
|
|
window:SetScrollOffset(maxValue - wanted)
|
|
else
|
|
for _ = 1, current - wanted do window:ScrollUp() end
|
|
for _ = 1, wanted - current do window:ScrollDown() end
|
|
end
|
|
C.updateBar(window)
|
|
end
|
|
|
|
function C.updateBar(window)
|
|
local d = C.windows[window]
|
|
if not d then return end
|
|
local total, visibleCount, rank = scrollState(window)
|
|
d.bar:Configure(total, visibleCount, rank)
|
|
end
|
|
|
|
-- ------------------------------------------------------------ scroll-to-bottom button
|
|
|
|
local function createReturnButton(window)
|
|
local name = window:GetName()
|
|
local b = CreateFrame("Button", name .. "ForeverScrollToBottom", window)
|
|
b:SetWidth(G.backW)
|
|
b:SetHeight(G.backH)
|
|
b:SetPoint("BOTTOMRIGHT", window.resizeButton, "TOPRIGHT", G.returnX, G.returnY)
|
|
for _, state in ipairs({
|
|
{ "SetNormalTexture", "GetNormalTexture", ATLAS.backButton },
|
|
{ "SetPushedTexture", "GetPushedTexture", ATLAS.returnDown },
|
|
{ "SetHighlightTexture", "GetHighlightTexture", ATLAS.returnHover },
|
|
}) do
|
|
local e = ForeverUI.AtlasEntry(state[3])
|
|
b[state[1]](b, e and e[1] or "")
|
|
local t = b[state[2]](b)
|
|
if t then
|
|
ForeverUI.SetAtlas(t, state[3], true)
|
|
t:ClearAllPoints()
|
|
t:SetAllPoints(b)
|
|
end
|
|
end
|
|
local flash = b:CreateTexture(nil, "OVERLAY")
|
|
ForeverUI.SetAtlas(flash, ATLAS.backButton, true)
|
|
flash:SetBlendMode("ADD")
|
|
flash:SetAllPoints(b)
|
|
flash:SetAlpha(0)
|
|
b.flash = flash
|
|
b:SetAlpha(0)
|
|
b:SetScript("OnClick", function()
|
|
PlaySound("igChatBottom")
|
|
window:ScrollToBottom()
|
|
C.updateBar(window)
|
|
end)
|
|
return b
|
|
end
|
|
|
|
-- ------------------------------------------------------------ idle state
|
|
|
|
-- Appends frame's regions to inside, skipping those in exclude (animated by the client)
|
|
local function regions(frame, exclude, inside)
|
|
for _, r in ipairs({ frame:GetRegions() }) do
|
|
if not exclude[r] then table.insert(inside, r) end
|
|
end
|
|
end
|
|
|
|
-- What fades out at rest: the tab's regions (not its alert glow; TabFlash is a child frame),
|
|
-- the column buttons (not the hidden arrows), and for the default chat the menu button, the
|
|
-- friends button and the dock overflow button (not its highlight, which flashes on alerts).
|
|
-- The input box regions are kept apart.
|
|
function C.recordIdle(window)
|
|
local d = C.windows[window]
|
|
local name = window:GetName()
|
|
d.faded, d.input = {}, {}
|
|
local tab = _G[name .. "Tab"]
|
|
if tab then
|
|
regions(tab, { [tab.glow or false] = true }, d.faded)
|
|
end
|
|
local arrows = {}
|
|
for _, suffix in ipairs({ "ButtonFrameUpButton", "ButtonFrameDownButton", "ButtonFrameBottomButton" }) do
|
|
if _G[name .. suffix] then arrows[_G[name .. suffix]] = true end
|
|
end
|
|
for _, b in ipairs({ window.buttonFrame:GetChildren() }) do
|
|
if not arrows[b] then table.insert(d.faded, b) end
|
|
end
|
|
if window == DEFAULT_CHAT_FRAME then
|
|
if ChatFrameMenuButton then table.insert(d.faded, ChatFrameMenuButton) end
|
|
if FriendsMicroButton then table.insert(d.faded, FriendsMicroButton) end
|
|
local overhang = GENERAL_CHAT_DOCK and GENERAL_CHAT_DOCK.overflowButton
|
|
if overhang then regions(overhang, { [overhang:GetHighlightTexture() or false] = true }, d.faded) end
|
|
end
|
|
if window.editBox then regions(window.editBox, {}, d.input) end
|
|
end
|
|
|
|
-- Applies the idle factor every frame (the client resets the tab text alpha);
|
|
-- the active input box stays fully visible
|
|
function C.applyIdle(window)
|
|
local d = C.windows[window]
|
|
local input = window.editBox
|
|
local active = input and (ACTIVE_CHAT_EDIT_BOX == input or input:HasFocus()) and true or false
|
|
for _, object in ipairs(d.faded) do object:SetAlpha(d.idle) end
|
|
for _, r in ipairs(d.input) do r:SetAlpha(active and 1 or d.idle) end
|
|
end
|
|
|
|
-- Lining: a copy of each background and border texture, with the same parent, layer and anchors.
|
|
-- The client paints the background; the lining tops it up.
|
|
function C.createLining(window)
|
|
local d = C.windows[window]
|
|
local name = window:GetName()
|
|
d.doubles = {}
|
|
for _, suffix in ipairs(CHAT_FRAME_TEXTURES or {}) do
|
|
local o = _G[name .. suffix]
|
|
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.doubles, { o, t })
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Adds what the client's background lacks to reach the lit opacity, max(opacity, 0.25):
|
|
-- alpha = 1 - (1 - wanted) / (1 - client); nothing when the client already reaches it
|
|
function C.applyLining(window)
|
|
local d = C.windows[window]
|
|
local wanted = math.max(window.oldAlpha or 0, DEFAULT_CHATFRAME_ALPHA or 0.25) * d.idle
|
|
for _, pair in ipairs(d.doubles) do
|
|
local o, t = pair[1], pair[2]
|
|
local client = o:GetAlpha()
|
|
if o:IsShown() and wanted > client + 0.001 then
|
|
t:SetVertexColor(o:GetVertexColor())
|
|
t:SetAlpha(1 - (1 - wanted) / (1 - client))
|
|
t:Show()
|
|
elseif t:IsShown() then
|
|
t:SetAlpha(0)
|
|
t:Hide()
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ------------------------------------------------------------ tabs
|
|
|
|
C.tabs = {}
|
|
|
|
-- Camelot's ChatTabArtTemplate: the sides overhang the tab by 2, the middle stretches between
|
|
-- them (selected and highlight textures follow). The text is centered, except on whisper tabs,
|
|
-- where it stays left after the icon (FCF_OpenTemporaryWindow).
|
|
function C.skinTab(tab)
|
|
if not tab or C.tabs[tab] then return end
|
|
local name = tab:GetName()
|
|
local left, middle, right = _G[name .. "Left"], _G[name .. "Middle"], _G[name .. "Right"]
|
|
if not (left and middle and right) then return end
|
|
C.tabs[tab] = true
|
|
left:ClearAllPoints()
|
|
left:SetPoint("BOTTOMLEFT", tab, "BOTTOMLEFT", -G.tabSide, 0)
|
|
right:ClearAllPoints()
|
|
right:SetPoint("BOTTOMRIGHT", tab, "BOTTOMRIGHT", G.tabSide, 0)
|
|
middle:ClearAllPoints()
|
|
middle:SetPoint("LEFT", left, "RIGHT", 0, 0)
|
|
middle:SetPoint("RIGHT", right, "LEFT", 0, 0)
|
|
local text = _G[name .. "Text"]
|
|
if text and not tab.conversationIcon then
|
|
text:ClearAllPoints()
|
|
text:SetPoint("CENTER", tab, "CENTER", 0, G.tabTextY)
|
|
end
|
|
end
|
|
|
|
-- Camelot's PanelTemplates_TabResize: text + 20 + margin, at least both sides;
|
|
-- with a forced size, the text takes the rest
|
|
function C.resize(tab, margin, size, fixedTextWidth)
|
|
local name = tab:GetName()
|
|
local text = _G[name .. "Text"]
|
|
if not text then return end
|
|
margin = margin or 0
|
|
text:SetWidth(fixedTextWidth or 0)
|
|
local textWidth = text:GetStringWidth()
|
|
local width = textWidth + G.tabMargins + margin
|
|
local sides = _G[name .. "Left"]:GetWidth() + _G[name .. "Right"]:GetWidth()
|
|
if size then
|
|
width = math.max(size, sides)
|
|
textWidth = width - G.tabMargins - margin
|
|
elseif width < sides then
|
|
width = sides
|
|
textWidth = width - G.tabMargins - margin
|
|
end
|
|
text:SetWidth(textWidth)
|
|
tab:SetWidth(width)
|
|
end
|
|
|
|
-- Camelot's FCFDock_SetPrimary: the dock sits on top of the primary chat's background
|
|
function C.placeDockAnchor(dock)
|
|
local primary = dock and dock.primary
|
|
local background = primary and _G[primary:GetName() .. "Background"]
|
|
if not background then return end
|
|
dock:ClearAllPoints()
|
|
dock:SetPoint("BOTTOMLEFT", background, "TOPLEFT", 0, 0)
|
|
dock:SetPoint("BOTTOMRIGHT", background, "TOPRIGHT", 0, 0)
|
|
end
|
|
|
|
-- Camelot's FCFDock_UpdateTabs: fixed tabs from the dock's bottom left, whisper tabs from the
|
|
-- scroll list's left, 1 apart; the overflow button at the dock's bottom right, the list 5
|
|
-- before it
|
|
function C.placeDock(dock)
|
|
if not (dock and dock.DOCKED_CHAT_FRAMES and dock.scrollFrame) then return end
|
|
local child = dock.scrollFrame:GetScrollChild()
|
|
local lastFixed, lastMobile
|
|
for _, window in ipairs(dock.DOCKED_CHAT_FRAMES) do
|
|
local tab = _G[window:GetName() .. "Tab"]
|
|
if tab then
|
|
tab:ClearAllPoints()
|
|
if window.isStaticDocked then
|
|
if lastFixed then
|
|
tab:SetPoint("LEFT", lastFixed, "RIGHT", G.tabGap, 0)
|
|
else
|
|
tab:SetPoint("BOTTOMLEFT", dock, "BOTTOMLEFT", 0, 0)
|
|
end
|
|
lastFixed = tab
|
|
else
|
|
if lastMobile then
|
|
tab:SetPoint("LEFT", lastMobile, "RIGHT", G.tabGap, 0)
|
|
elseif child then
|
|
tab:SetPoint("LEFT", child, "LEFT", 0, G.mobileTabY)
|
|
end
|
|
lastMobile = tab
|
|
end
|
|
end
|
|
end
|
|
local overhang = dock.overflowButton
|
|
if overhang then
|
|
overhang:ClearAllPoints()
|
|
overhang:SetPoint("BOTTOMRIGHT", dock, "BOTTOMRIGHT", 0, 0)
|
|
end
|
|
if overhang and overhang:IsShown() then
|
|
dock.scrollFrame:SetPoint("BOTTOMRIGHT", overhang, "BOTTOMLEFT", -G.overflowGap, G.overflowY)
|
|
else
|
|
dock.scrollFrame:SetPoint("BOTTOMRIGHT", dock, "BOTTOMRIGHT", 0, 0)
|
|
end
|
|
end
|
|
|
|
-- Skins a window's tab, then sizes it (the dock's size if it is in the scroll list)
|
|
local function skinTabOf(window)
|
|
local tab = _G[window:GetName() .. "Tab"]
|
|
C.skinTab(tab)
|
|
if not C.tabs[tab] then return end
|
|
local dock = GENERAL_CHAT_DOCK
|
|
local size
|
|
if window.isDocked and not window.isStaticDocked and dock and dock.scrollFrame then
|
|
size = dock.scrollFrame.dynTabSize
|
|
end
|
|
C.resize(tab, tab.sizePadding or 0, size)
|
|
end
|
|
|
|
-- ------------------------------------------------------------ selection
|
|
|
|
-- Alt + drag on the chat highlights text. 3.3.5 reaches the clipboard only by Ctrl+C in an
|
|
-- edit box, so a hidden read-only box holds the text and takes the keyboard only while Ctrl is
|
|
-- held (3.3.5 cannot see a key without taking the keyboard). Any other action clears the
|
|
-- selection: Ctrl released, a click, moving, casting, another edit box, a panel, Escape, scrolling.
|
|
-- The engine draws one font string per message, wrapped at the chat width without indent; a
|
|
-- measuring string with the same font and width wraps the same way, so a word's line comes from
|
|
-- the text height up to that word. A word longer than a line is measured letter by letter.
|
|
|
|
local S = { hidden = {} }
|
|
C.selection = S
|
|
|
|
-- Calls a method the client may lack: nil instead of an error
|
|
local function call(object, method, ...)
|
|
if not object[method] then return nil end
|
|
local r = { pcall(object[method], object, ...) }
|
|
if not r[1] then return nil end
|
|
return r[2], r[3], r[4], r[5]
|
|
end
|
|
|
|
-- Highlight color (not a Camelot element)
|
|
S.color = { 0.3, 0.5, 1, 0.4 }
|
|
|
|
-- Raid markers as typed in chat: the client's ICON_TAG_RAID_TARGET_*1 tag
|
|
-- ({rt1} to {rt8} in English)
|
|
local MARKERS = { "STAR", "CIRCLE", "DIAMOND", "TRIANGLE", "MOON", "SQUARE", "CROSS", "SKULL" }
|
|
local function imageTag(raw)
|
|
local n = tonumber(string.match(raw, "RaidTargetingIcon_(%d)"))
|
|
local name = n and MARKERS[n] and _G["ICON_TAG_RAID_TARGET_" .. MARKERS[n] .. "1"]
|
|
return name and ("{" .. name .. "}") or ""
|
|
end
|
|
|
|
-- Visible units of a message: a letter (UTF-8), a bar (||), an image (|T...|t);
|
|
-- color codes (|c, |r) and link codes (|H...|h, |h) are invisible, the link text is kept;
|
|
-- an image copies as its raid marker tag
|
|
function C.units(text)
|
|
local u, i, n = {}, 1, string.len(text or "")
|
|
while i <= n do
|
|
local c = string.sub(text, i, i)
|
|
if c == "|" then
|
|
local s = string.sub(text, 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 finish = string.find(text, "|h", i + 2, true)
|
|
i = finish and finish + 2 or n + 1
|
|
elseif s == "T" then
|
|
local finish = string.find(text, "|t", i + 2, true) or n
|
|
local raw = string.sub(text, i, finish + 1)
|
|
table.insert(u, { raw = raw, copy = imageTag(raw) })
|
|
i = finish + 2
|
|
elseif s == "n" then
|
|
table.insert(u, { raw = "", copy = "\n" })
|
|
i = i + 2
|
|
else
|
|
table.insert(u, { raw = "||", copy = "|" })
|
|
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 letter = string.sub(text, i, i + l - 1)
|
|
table.insert(u, { raw = letter, copy = letter, space = (letter == " ") })
|
|
i = i + l
|
|
end
|
|
end
|
|
return u
|
|
end
|
|
|
|
-- Raw text of units a to b
|
|
local function raw(units, a, b)
|
|
local t = {}
|
|
for k = a, b do t[#t + 1] = units[k].raw end
|
|
return table.concat(t)
|
|
end
|
|
|
|
-- Two hidden measuring strings, one on a single line, one at the message width;
|
|
-- they take the font and settings of the measured font string
|
|
local measureStrings
|
|
local function measureMessage(r, c)
|
|
if not measureStrings then
|
|
local frame = CreateFrame("Frame", nil, UIParent)
|
|
frame:SetAlpha(0)
|
|
frame:SetWidth(1)
|
|
frame:SetHeight(1)
|
|
frame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 0, 0)
|
|
measureStrings = { row = frame:CreateFontString(nil, "ARTWORK"), block = frame:CreateFontString(nil, "ARTWORK") }
|
|
measureStrings.row:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0)
|
|
measureStrings.block:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0)
|
|
end
|
|
local font, size, outline = r:GetFont()
|
|
for _, m in pairs(measureStrings) do
|
|
m:SetFont(font, size, outline)
|
|
if m.SetSpacing then m:SetSpacing(call(r, "GetSpacing") or 0) end
|
|
end
|
|
measureStrings.row:SetWidth(0)
|
|
measureStrings.block:SetWidth(c and c.width or r:GetWidth())
|
|
if measureStrings.block.SetIndentedWordWrap then measureStrings.block:SetIndentedWordWrap(call(r, "GetIndentedWordWrap") and true or false) end
|
|
if measureStrings.block.SetNonSpaceWrap then measureStrings.block:SetNonSpaceWrap(call(r, "CanNonSpaceWrap") and true or false) end
|
|
return measureStrings
|
|
end
|
|
|
|
-- Line layout of a chat font string: its units, the first unit of each line, the line height;
|
|
-- cached while its text and width do not change
|
|
function C.layout(r)
|
|
local text, width = r:GetText() or "", r:GetWidth()
|
|
local c = S.hidden[r]
|
|
if c and c.text == text and c.width == width then return c end
|
|
c = { text = text, width = width, widths = {} }
|
|
local m = measureMessage(r, c)
|
|
m.row:SetText("A")
|
|
c.lineHeight = math.max(1, m.row:GetStringHeight())
|
|
local u = C.units(text)
|
|
c.units = u
|
|
local function rows(b)
|
|
m.block:SetText(raw(u, 1, b))
|
|
return math.floor(m.block:GetStringHeight() / c.lineHeight + 0.5)
|
|
end
|
|
local starts, done = { 1 }, 1
|
|
local a, n = 1, #u
|
|
while a <= n do
|
|
while a <= n and u[a].space do a = a + 1 end
|
|
if a > n then break end
|
|
local b = a
|
|
while b < n and not u[b + 1].space do b = b + 1 end
|
|
local L = rows(b)
|
|
if L > done then
|
|
m.row:SetText(raw(u, a, b))
|
|
if m.row:GetStringWidth() <= width then
|
|
-- the word moves to the next line
|
|
for k = done + 1, L do starts[k] = a end
|
|
else
|
|
-- a word longer than a line: break by letter
|
|
for v = a, b do
|
|
local Lv = rows(v)
|
|
for k = done + 1, Lv do starts[k] = v end
|
|
if Lv > done then done = Lv end
|
|
end
|
|
end
|
|
done = math.max(done, L)
|
|
end
|
|
a = b + 1
|
|
end
|
|
c.starts, c.lineCount = starts, #starts
|
|
S.hidden[r] = c
|
|
return c
|
|
end
|
|
|
|
-- Width from the start of line n to boundary b (before unit b)
|
|
local function widthUpTo(r, c, n, b)
|
|
local start = c.starts[n]
|
|
if b <= start then return 0 end
|
|
local key = n * 100000 + b
|
|
if not c.widths[key] then
|
|
local m = measureMessage(r, c)
|
|
m.row:SetText(raw(c.units, start, b - 1))
|
|
c.widths[key] = m.row:GetStringWidth()
|
|
end
|
|
return c.widths[key]
|
|
end
|
|
|
|
-- Displayed messages, top to bottom
|
|
function C.visibleMessages(window)
|
|
local list = {}
|
|
for _, r in ipairs({ window: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(list, r)
|
|
end
|
|
end
|
|
table.sort(list, function(p, q) return p:GetTop() > q:GetTop() end)
|
|
return list
|
|
end
|
|
|
|
-- Cursor position in the chat's scale
|
|
local function cursor(window)
|
|
local x, y = GetCursorPosition()
|
|
local e = window:GetEffectiveScale()
|
|
return x / e, y / e
|
|
end
|
|
|
|
-- Under the cursor: the message rank and the nearest boundary;
|
|
-- above the first message, its start; below the last, its end
|
|
function C.hitTest(list, x, y)
|
|
if #list == 0 then return nil end
|
|
if y > list[1]:GetTop() then return 1, 1 end
|
|
for rank, r in ipairs(list) do
|
|
if y >= r:GetBottom() or rank == #list then
|
|
local c = C.layout(r)
|
|
if y < r:GetBottom() then return rank, #c.units + 1 end
|
|
local n = math.min(c.lineCount, math.max(1, math.floor((r:GetTop() - y) / c.lineHeight) + 1))
|
|
local start = c.starts[n]
|
|
local finish = c.starts[n + 1] or (#c.units + 1)
|
|
local xr = x - r:GetLeft()
|
|
local down, top = start, finish
|
|
while down < top do
|
|
local middle = math.floor((down + top) / 2)
|
|
if widthUpTo(r, c, n, middle) < xr then down = middle + 1 else top = middle end
|
|
end
|
|
local b = down
|
|
if b > start and (xr - widthUpTo(r, c, n, b - 1)) < (widthUpTo(r, c, n, b) - xr) then
|
|
b = b - 1
|
|
end
|
|
return rank, b
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Anchor and tip, in reading order
|
|
local function bounds()
|
|
local a, b = S.anchor, S.tip
|
|
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
|
|
|
|
-- Highlight: one rectangle per line piece, on the chat itself
|
|
-- (BORDER layer: below the text)
|
|
function C.paint()
|
|
local d = S.window and C.windows[S.window]
|
|
if not d then return end
|
|
local k = 0
|
|
if S.anchor and S.tip then
|
|
local a, b = bounds()
|
|
for rank = a[1], b[1] do
|
|
local r = S.messages[rank]
|
|
local c = C.layout(r)
|
|
local first = (rank == a[1]) and a[2] or 1
|
|
local last = (rank == b[1]) and b[2] or (#c.units + 1)
|
|
for n = 1, c.lineCount do
|
|
local p = math.max(c.starts[n], first)
|
|
local q = math.min(c.starts[n + 1] or (#c.units + 1), last)
|
|
if p < q then
|
|
local x1, x2 = widthUpTo(r, c, n, p), widthUpTo(r, c, n, q)
|
|
if x2 > x1 then
|
|
k = k + 1
|
|
local t = d.highlights[k]
|
|
if not t then
|
|
t = S.window:CreateTexture(nil, "BORDER")
|
|
t:SetTexture(S.color[1], S.color[2], S.color[3], S.color[4])
|
|
d.highlights[k] = t
|
|
end
|
|
t:ClearAllPoints()
|
|
t:SetPoint("TOPLEFT", r, "TOPLEFT", x1, -(n - 1) * c.lineHeight)
|
|
t:SetWidth(x2 - x1)
|
|
t:SetHeight(c.lineHeight)
|
|
t:Show()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
for i = k + 1, #d.highlights do d.highlights[i]:Hide() end
|
|
end
|
|
|
|
-- Text to copy: what is visible, links by their text, markers by name; one message per line
|
|
function C.selectionText()
|
|
if not (S.anchor and S.tip) then return "" end
|
|
local a, b = bounds()
|
|
local parts = {}
|
|
for rank = a[1], b[1] do
|
|
local c = C.layout(S.messages[rank])
|
|
local first = (rank == a[1]) and a[2] or 1
|
|
local last = (rank == b[1]) and b[2] or (#c.units + 1)
|
|
local t = {}
|
|
for k = first, last - 1 do t[#t + 1] = c.units[k].copy end
|
|
parts[#parts + 1] = table.concat(t)
|
|
end
|
|
return table.concat(parts, "\n")
|
|
end
|
|
|
|
-- Hidden edit box holding the copy; read-only: typed text is undone
|
|
local box = CreateFrame("EditBox", nil, UIParent)
|
|
box:SetMultiLine(true)
|
|
box:SetAutoFocus(false)
|
|
box:SetFontObject(ChatFontNormal)
|
|
box:SetWidth(200)
|
|
box:SetHeight(20)
|
|
box:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", 0, 0)
|
|
box:SetAlpha(0)
|
|
box:EnableMouse(false)
|
|
S.box = box
|
|
|
|
function C.clear()
|
|
local d = S.window and C.windows[S.window]
|
|
S.window, S.messages, S.anchor, S.tip, S.dragging, S.text = nil, nil, nil, nil, false, nil
|
|
S.ctrl, S.base = false, nil
|
|
if d then
|
|
for _, t in ipairs(d.highlights) do t:Hide() end
|
|
end
|
|
if box:HasFocus() then box:ClearFocus() end
|
|
end
|
|
|
|
box:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
|
|
box:SetScript("OnEnterPressed", function(self) self:ClearFocus() end)
|
|
box:SetScript("OnEditFocusLost", function()
|
|
if S.text then C.clear() end
|
|
end)
|
|
box:SetScript("OnTextChanged", function(self, userInput)
|
|
if userInput and S.text then
|
|
self:SetText(S.text)
|
|
self:HighlightText()
|
|
end
|
|
end)
|
|
|
|
function C.beginSelection(window)
|
|
C.clear()
|
|
local list = C.visibleMessages(window)
|
|
local rank, b = C.hitTest(list, cursor(window))
|
|
if not rank then return end
|
|
S.window, S.messages, S.dragging = window, list, true
|
|
S.anchor, S.tip = { rank, b }, { rank, b }
|
|
C.paint()
|
|
end
|
|
|
|
-- Moves the selection tip to the cursor
|
|
function C.follow()
|
|
local rank, b = C.hitTest(S.messages, cursor(S.window))
|
|
if rank and (rank ~= S.tip[1] or b ~= S.tip[2]) then
|
|
S.tip = { rank, b }
|
|
C.paint()
|
|
end
|
|
end
|
|
|
|
-- Ends the drag and puts the selected text in the hidden box
|
|
function C.finalize()
|
|
if not S.dragging then return end
|
|
C.follow()
|
|
S.dragging = false
|
|
local text = C.selectionText()
|
|
if text == "" then
|
|
C.clear()
|
|
return
|
|
end
|
|
S.text = text
|
|
box:SetText(text)
|
|
-- State watched to clear the selection: the character, the chat scroll, the selected texts
|
|
local speed = GetUnitSpeed and GetUnitSpeed("player") or 0
|
|
S.base = {
|
|
taxi = UnitOnTaxi and UnitOnTaxi("player") and true or false,
|
|
speed = speed,
|
|
facing = GetPlayerFacing and GetPlayerFacing() or 0,
|
|
falling = IsFalling and IsFalling() and true or false,
|
|
scrolling = S.window.GetCurrentScroll and S.window:GetCurrentScroll() or 0,
|
|
texts = {},
|
|
}
|
|
for rank, r in ipairs(S.messages) do S.base.texts[rank] = r:GetText() end
|
|
end
|
|
|
|
-- Any action other than Ctrl+C since the selection was made?
|
|
function C.otherInput()
|
|
local b = S.base
|
|
if IsMouseButtonDown("LeftButton") or IsMouseButtonDown("RightButton") or IsMouseButtonDown("MiddleButton") then
|
|
return true
|
|
end
|
|
if not S.window:IsVisible() then return true end
|
|
local focus = GetCurrentKeyBoardFocus and GetCurrentKeyBoardFocus()
|
|
if focus and focus ~= box then return true end
|
|
if not b.taxi then
|
|
if (GetUnitSpeed and GetUnitSpeed("player") or 0) ~= b.speed then return true end
|
|
if math.abs((GetPlayerFacing and GetPlayerFacing() or 0) - b.facing) > 0.001 then return true end
|
|
if (IsFalling and IsFalling() and true or false) ~= b.falling then return true end
|
|
end
|
|
if (S.window.GetCurrentScroll and S.window:GetCurrentScroll() or 0) ~= b.scrolling then return true end
|
|
for rank, r in ipairs(S.messages) do
|
|
if r:GetText() ~= b.texts[rank] then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Once selected: Ctrl down gives the hidden box the keyboard;
|
|
-- Ctrl up, or any other action, clears the selection
|
|
function C.watchCopy()
|
|
if IsControlKeyDown() then
|
|
if not S.ctrl then
|
|
S.ctrl = true
|
|
box:SetText(S.text)
|
|
box:SetFocus()
|
|
box:HighlightText()
|
|
end
|
|
elseif S.ctrl then
|
|
C.clear()
|
|
return
|
|
end
|
|
if C.otherInput() then C.clear() end
|
|
end
|
|
|
|
-- Mouse catcher over the chat, active only while Alt is held;
|
|
-- without Alt, clicks pass through to the world and links
|
|
function C.createCatcher(window, d)
|
|
d.highlights = {}
|
|
local catcher = CreateFrame("Frame", nil, window)
|
|
catcher:SetAllPoints(window)
|
|
catcher:EnableMouse(false)
|
|
catcher:SetScript("OnMouseDown", function(_, button)
|
|
if button == "LeftButton" then C.beginSelection(window) end
|
|
end)
|
|
catcher:SetScript("OnMouseUp", function(_, button)
|
|
if button == "LeftButton" then C.finalize() end
|
|
end)
|
|
d.catcher, d.grab = catcher, false
|
|
end
|
|
|
|
local selectionWatcher = CreateFrame("Frame")
|
|
selectionWatcher:SetScript("OnUpdate", function()
|
|
local alt = IsAltKeyDown() and true or false
|
|
for window, d in pairs(C.windows) do
|
|
local grab = ((alt and window:IsVisible()) or (S.dragging and S.window == window)) and true or false
|
|
if grab ~= d.grab then
|
|
d.grab = grab
|
|
d.catcher:EnableMouse(grab)
|
|
end
|
|
end
|
|
if S.dragging then
|
|
C.follow()
|
|
elseif S.text then
|
|
C.watchCopy()
|
|
end
|
|
end)
|
|
C.selectionWatcher = selectionWatcher
|
|
|
|
-- A spell cast, a panel opened or Escape clears the selection
|
|
local function cancelCopy()
|
|
if S.text and not S.dragging then C.clear() end
|
|
end
|
|
selectionWatcher:RegisterEvent("UNIT_SPELLCAST_SENT")
|
|
selectionWatcher:SetScript("OnEvent", function(_, _, unit)
|
|
if unit == "player" then cancelCopy() end
|
|
end)
|
|
if ShowUIPanel then hooksecurefunc("ShowUIPanel", cancelCopy) end
|
|
if ToggleGameMenu then hooksecurefunc("ToggleGameMenu", cancelCopy) end
|
|
|
|
-- ------------------------------------------------------------ window skin
|
|
|
|
function C.applySkin(window)
|
|
if not window or C.windows[window] or not window.buttonFrame then return end
|
|
local name = window:GetName()
|
|
-- The background overhangs on the right to hold the scroll bar
|
|
local background = _G[name .. "Background"]
|
|
if background then
|
|
background:ClearAllPoints()
|
|
background:SetPoint("TOPLEFT", window, "TOPLEFT", G.backgroundLeft, G.backgroundTop)
|
|
background:SetPoint("TOPRIGHT", window, "TOPRIGHT", G.backgroundRight, G.backgroundTop)
|
|
background:SetPoint("BOTTOMLEFT", window, "BOTTOMLEFT", G.backgroundLeft, G.backgroundBottom)
|
|
background:SetPoint("BOTTOMRIGHT", window, "BOTTOMRIGHT", G.backgroundRight, G.backgroundBottom)
|
|
end
|
|
hideArrows(window)
|
|
window:HookScript("OnShow", hideArrows)
|
|
local bf = window.buttonFrame
|
|
if bf.minimizeButton then
|
|
bf.minimizeButton:ClearAllPoints()
|
|
bf.minimizeButton:SetPoint("TOP", bf, "TOP", 0, G.minimizeY)
|
|
end
|
|
placeColumn(window)
|
|
|
|
local d = { window = window }
|
|
d.tab = _G[name .. "Tab"]
|
|
d.backButton = createReturnButton(window)
|
|
local bar = ForeverUI.CreateScrollBar(name .. "ForeverScrollBar", window, window)
|
|
bar:ClearAllPoints()
|
|
bar:SetPoint("TOPLEFT", window, "TOPRIGHT", 0, 0)
|
|
bar:SetPoint("BOTTOMLEFT", d.backButton, "TOPLEFT", 0, G.barBottom)
|
|
bar:SetAlpha(0)
|
|
bar.onScroll = function(rank) C.scrollTo(window, rank) end
|
|
d.bar = bar
|
|
-- Input box: TOPLEFT as in 3.3.5, right edge on the bar's right edge + 8 (camelot: RIGHT on the
|
|
-- bar; the same point is taken on the chat, where the bar starts, so only TOPLEFT sets the height)
|
|
local input = window.editBox
|
|
if input then
|
|
input:ClearAllPoints()
|
|
input:SetPoint("TOPLEFT", window, "BOTTOMLEFT", G.inputX, G.inputY)
|
|
input:SetPoint("TOPRIGHT", window, "BOTTOMRIGHT", bar:GetWidth() + G.inputRight, G.inputY)
|
|
end
|
|
skinTabOf(window)
|
|
-- Target alphas: the bar and the return button light up with the chat
|
|
d.barTarget, d.returnTarget = 0, 0
|
|
d.barSpeed, d.returnSpeed = 1, 1
|
|
d.flashTime = 0
|
|
C.windows[window] = d
|
|
C.createCatcher(window, d)
|
|
C.recordIdle(window)
|
|
C.createLining(window)
|
|
d.isLit = window.hasBeenFaded and true or false
|
|
d.hover, d.inside, d.outside = false, 0, 0
|
|
d.idle = d.isLit and 1 or 0
|
|
d.idleTarget = d.idle
|
|
d.idleSpeed = 1
|
|
C.applyIdle(window)
|
|
C.updateBar(window)
|
|
end
|
|
|
|
-- ------------------------------------------------------------ fade
|
|
|
|
-- Target alphas of the bar and return button, with durations in seconds
|
|
local function setTargets(d, bar, backButton, barDuration, returnDuration)
|
|
d.barTarget = bar
|
|
d.returnTarget = backButton
|
|
d.barSpeed = G.barAlpha / math.max(0.01, barDuration)
|
|
d.returnSpeed = G.returnAlpha / math.max(0.01, returnDuration)
|
|
end
|
|
|
|
local function lightUp(d)
|
|
setTargets(d, G.barAlpha, G.returnAlpha, G.fadeIn, G.returnFadeIn)
|
|
d.idleTarget, d.idleSpeed = 1, 1 / G.fadeIn
|
|
end
|
|
|
|
local function turnOff(d)
|
|
setTargets(d, 0, 0, G.origin, G.origin)
|
|
d.idleTarget, d.idleSpeed = 0, 1 / G.origin
|
|
end
|
|
|
|
-- FCF_OnUpdate's hover zones, plus the bar and the return button (outside the chat)
|
|
local function isHovered(window, d)
|
|
if not window:IsShown() then return false end
|
|
local top = 28
|
|
if IsCombatLog and CombatLogQuickButtonFrame_Custom and IsCombatLog(window) then
|
|
top = top + CombatLogQuickButtonFrame_Custom:GetHeight()
|
|
end
|
|
return (window:IsMouseOver(top, -2, -2, 2)
|
|
or (window.isDocked and FriendsMicroButton and FriendsMicroButton:IsMouseOver())
|
|
or (window.buttonFrame and window.buttonFrame:IsMouseOver())
|
|
or (d.bar:IsShown() and MouseIsOver(d.bar)) or MouseIsOver(d.backButton)) and true or false
|
|
end
|
|
C.isHovered = isHovered
|
|
|
|
-- Moves object's alpha toward target at speed per second
|
|
local function approach(object, target, speed, elapsed)
|
|
local a = object:GetAlpha()
|
|
if a < target then
|
|
a = math.min(target, a + speed * elapsed)
|
|
elseif a > target then
|
|
a = math.max(target, a - speed * elapsed)
|
|
end
|
|
object:SetAlpha(a)
|
|
end
|
|
|
|
-- Every frame: the bar follows the chat, the fade advances, the return button flashes while
|
|
-- not at the bottom. FCF_OnUpdate lights the chat only if the cursor stays still, and calling
|
|
-- FCF_FadeInChatFrame would set hasBeenFaded from the addon: FCF_OnUpdate runs in UIParent's
|
|
-- OnUpdate before UnitPopup_OnUpdate, so the taint would reach the unit menus.
|
|
local engine = CreateFrame("Frame")
|
|
engine:SetScript("OnUpdate", function(self, elapsed)
|
|
elapsed = elapsed or 0
|
|
-- Hover: 0.2 s inside to light up, 1 s outside to turn off, cursor still or not;
|
|
-- a lit docked window lights the whole dock
|
|
local showRegion, hide = CHAT_TAB_SHOW_DELAY or 0.2, CHAT_TAB_HIDE_DELAY or 1
|
|
local dock = false
|
|
for window, d in pairs(C.windows) do
|
|
if isHovered(window, d) then
|
|
d.inside, d.outside = d.inside + elapsed, 0
|
|
if d.inside >= showRegion then d.hover = true end
|
|
else
|
|
d.inside, d.outside = 0, d.outside + elapsed
|
|
if d.outside >= hide then d.hover = false end
|
|
end
|
|
if d.hover and window.isDocked then dock = true end
|
|
end
|
|
for window, d in pairs(C.windows) do
|
|
local isLit = (d.hover or window.hasBeenFaded or (dock and window.isDocked)) and true or false
|
|
if isLit ~= d.isLit then
|
|
d.isLit = isLit
|
|
if isLit then lightUp(d) else turnOff(d) end
|
|
end
|
|
if d.idle < d.idleTarget then
|
|
d.idle = math.min(d.idleTarget, d.idle + d.idleSpeed * elapsed)
|
|
elseif d.idle > d.idleTarget then
|
|
d.idle = math.max(d.idleTarget, d.idle - d.idleSpeed * elapsed)
|
|
end
|
|
-- A closed window (chat and tab hidden) shows none of this: its seventy-odd calls are
|
|
-- skipped every frame (FCF_OnUpdate skips hidden frames too)
|
|
if window:IsShown() or (d.tab and d.tab:IsShown()) then
|
|
C.applyIdle(window)
|
|
C.applyLining(window)
|
|
end
|
|
if window:IsShown() then
|
|
C.updateBar(window)
|
|
local atBottom = window:AtBottom()
|
|
if atBottom then
|
|
d.flashTime = 0
|
|
d.backButton.flash:SetAlpha(0)
|
|
else
|
|
-- 0.1 s in, 0.5 s held, 0.1 s out, 0.5 s off (ScrollToBottomFlashInterval)
|
|
local cycle = 2 * (G.flashFadeIn + G.flashHold)
|
|
d.flashTime = (d.flashTime + elapsed) % cycle
|
|
local t = d.flashTime
|
|
local a
|
|
if t < G.flashFadeIn then a = t / G.flashFadeIn
|
|
elseif t < G.flashFadeIn + G.flashHold then a = 1
|
|
elseif t < 2 * G.flashFadeIn + G.flashHold then a = 1 - (t - G.flashFadeIn - G.flashHold) / G.flashFadeIn
|
|
else a = 0 end
|
|
d.backButton.flash:SetAlpha(a)
|
|
end
|
|
-- Not at the bottom: the bar lights up and the return button stays visible
|
|
local barTarget = atBottom and d.barTarget or G.barAlpha
|
|
local returnTarget = atBottom and d.returnTarget or 1
|
|
-- Not at the bottom, the bar fades in as in FCF_FadeInScrollbar
|
|
approach(d.bar, barTarget, atBottom and d.barSpeed or G.barAlpha / G.fadeIn, elapsed)
|
|
approach(d.backButton, returnTarget, math.max(d.returnSpeed, G.returnAlpha / G.returnFadeIn), elapsed)
|
|
end
|
|
end
|
|
end)
|
|
C.engine = engine
|
|
|
|
-- ------------------------------------------------------------ hooks
|
|
|
|
function C.skinAll()
|
|
for _, name in ipairs(CHAT_FRAMES or {}) do
|
|
C.applySkin(_G[name])
|
|
end
|
|
for i = 1, (NUM_CHAT_WINDOWS or 10) do
|
|
C.applySkin(_G["ChatFrame" .. i])
|
|
end
|
|
skinFriends()
|
|
-- Menu button at the bottom of the default chat's column
|
|
if ChatFrameMenuButton and ChatFrame1ButtonFrame then
|
|
ChatFrameMenuButton:ClearAllPoints()
|
|
ChatFrameMenuButton:SetPoint("BOTTOM", ChatFrame1ButtonFrame, "BOTTOM", 0, 0)
|
|
end
|
|
-- The dock and its tabs (the client placed a newly skinned window's tab before the skin)
|
|
C.placeDockAnchor(GENERAL_CHAT_DOCK)
|
|
C.placeDock(GENERAL_CHAT_DOCK)
|
|
end
|
|
|
|
C.skinAll()
|
|
|
|
-- On first load, each window's saved background opacity is set to 0 once, so only messages
|
|
-- show at rest. The client keeps it; later changes from the tab menu are respected.
|
|
local watcher = CreateFrame("Frame")
|
|
watcher:RegisterEvent("ADDON_LOADED")
|
|
watcher:SetScript("OnEvent", function(self, _, addon)
|
|
if addon ~= "ForeverUI" then return end
|
|
self:UnregisterEvent("ADDON_LOADED")
|
|
ForeverUIDB = ForeverUIDB or {}
|
|
if ForeverUIDB.chatIdleBackground then return end
|
|
for i = 1, (NUM_CHAT_WINDOWS or 10) do
|
|
SetChatWindowAlpha(i, G.idleBackground)
|
|
end
|
|
ForeverUIDB.chatIdleBackground = true
|
|
end)
|
|
C.watcher = watcher
|
|
|
|
if FCF_SetButtonSide then
|
|
hooksecurefunc("FCF_SetButtonSide", function(window)
|
|
if C.windows[window] then placeColumn(window) end
|
|
if window == DEFAULT_CHAT_FRAME then placeFriends() end
|
|
end)
|
|
end
|
|
if FCF_OpenTemporaryWindow then hooksecurefunc("FCF_OpenTemporaryWindow", C.skinAll) end
|
|
-- Chat tab sizes only (the function serves every tab in the game);
|
|
-- the dock, after each client reorder
|
|
if PanelTemplates_TabResize then
|
|
hooksecurefunc("PanelTemplates_TabResize", function(tab, margin, size, _, fixedTextWidth)
|
|
if C.tabs[tab] then C.resize(tab, margin, size, fixedTextWidth) end
|
|
end)
|
|
end
|
|
if FCFDock_UpdateTabs then hooksecurefunc("FCFDock_UpdateTabs", C.placeDock) end
|
|
if FCFDock_SetPrimary then hooksecurefunc("FCFDock_SetPrimary", C.placeDockAnchor) end
|