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>
1150 lines
42 KiB
Lua
1150 lines
42 KiB
Lua
-- Social window skinned like camelot: the frame, its tabs and the Contacts page (friends
|
|
-- and ignores). The Who, Guild, Chat and Raid pages are in SocialWho.lua, SocialGuild.lua,
|
|
-- SocialChat.lua and SocialRaid.lua and register with S.registerPage. Data, row order,
|
|
-- colors, menu, tooltip and button actions follow the client's FriendsFrame.lua.
|
|
|
|
local ForeverUI = ForeverUI or {}
|
|
_G.ForeverUI = ForeverUI
|
|
|
|
local S = {}
|
|
ForeverUI.Social = S
|
|
|
|
local SEP = string.char(92)
|
|
local L = ForeverUI.L
|
|
|
|
-- Layout values from camelot friendsframe.xml, shareduipaneltemplates, nineslicelayouts
|
|
-- and tabsystemtemplates. A table, because Lua 5.1 limits a function to 60 upvalues.
|
|
local G = {
|
|
width = 385, height = 424,
|
|
rock = "interface" .. SEP .. "ForeverUI" .. SEP .. "framegeneral" .. SEP .. "ui-background-rock",
|
|
-- battlenet-portrait refined by tools/refine_portrait.py: 128 px, mipmaps, uncompressed;
|
|
-- the original 64 px DXT5 looks pixelated
|
|
portrait = "Interface" .. SEP .. "ForeverUI" .. SEP .. "friendsframe" .. SEP .. "battlenet-portrait-hd",
|
|
portraitSide = 60, portraitX = -5, portraitY = 7,
|
|
titleX1 = 58, titleX2 = -24, titleY = -1, titleH = 20, titleTextY = -5,
|
|
closeButton = 24, closeButtonX = -2, closeButtonY = 1,
|
|
frameBoxX1 = 4, frameBoxY1 = -83, frameBoxX2 = -6, frameBoxY2 = 26,
|
|
listX = 8, listY = -87, listX2 = -22, listY2 = 2, listX2NoBar = -4,
|
|
buttonW = 134, buttonH = 21, buttonBottom = 4, buttonLeft = 4, buttonRight = -6,
|
|
tabH = 32, firstTabX = 5, firstTabY = 2, tabGap = 3, tabMargin = 20,
|
|
subTabX = 18, subTabY = -60, subTabH = 24, subTabMin = 100, subTabMax = 150, subTabGap = 1,
|
|
friendRowH = 34, shortRowH = 16,
|
|
stateSide = 16, stateX = 4, stateY = -3, nameX = 20, nameY = -4, infoY = -3,
|
|
highlight = "Interface" .. SEP .. "QuestFrame" .. SEP .. "UI-QuestLogTitleHighlight",
|
|
ignoreHighlight = "Interface" .. SEP .. "QuestFrame" .. SEP .. "UI-QuestTitleHighlight",
|
|
highlightTint = { 0.243, 0.570, 1 },
|
|
separator = "Interface" .. SEP .. "FriendsFrame" .. SEP .. "UI-FriendsFrame-OnlineDivider",
|
|
state = "Interface" .. SEP .. "FriendsFrame" .. SEP .. "StatusIcon-",
|
|
}
|
|
|
|
-- PortraitFrameTemplate metal with camelot's fixes (nineslicelayoutoverrides.lua): right
|
|
-- corners at x = 2, bottom corners at y = -8.
|
|
local METAL = {
|
|
{ key = "topLeft", name = "ui-frame-portraitmetal-cornertopleft", point = "TOPLEFT", x = -13, y = 16 },
|
|
{ key = "topRight", name = "ui-frame-metal-cornertopright", point = "TOPRIGHT", x = 2, y = 16 },
|
|
{ key = "bottomLeft", name = "ui-frame-metal-cornerbottomleft", point = "BOTTOMLEFT", x = -13, y = -8 },
|
|
{ key = "bottomRight", name = "ui-frame-metal-cornerbottomright", point = "BOTTOMRIGHT", x = 2, y = -8 },
|
|
}
|
|
|
|
-- Tab art: active pieces from the base sheet (camelot has no c60 ones), inactive from c60.
|
|
local TAB_ART = {
|
|
activeLeft = "uiframe-activetab-left", activeMiddle = "_uiframe-activetab-center", activeRight = "uiframe-activetab-right",
|
|
inactiveLeft = "uiframe-tab-left-c60", inactiveMiddle = "_uiframe-tab-center-c60", inactiveRight = "uiframe-tab-right-c60",
|
|
}
|
|
|
|
-- Bottom tabs: Contacts, then the WotLK ones. id is the client tab's, FriendsFrameTab<id>.
|
|
-- 3.3.5 lacks camelot's CONTACTS_* strings, so tabs and titles use FRIENDS, FRIENDS_LIST
|
|
-- and IGNORE_LIST.
|
|
local TABS = {
|
|
{ id = 1, text = "FRIENDS" },
|
|
{ id = 2, text = "WHO" },
|
|
{ id = 3, text = "GUILD" },
|
|
{ id = 4, text = "CHAT" },
|
|
{ id = 5, text = "RAID" },
|
|
}
|
|
local SUB_TABS = {
|
|
{ id = 1, text = "FRIENDS", title = "FRIENDS_LIST" },
|
|
{ id = 2, text = "IGNORE", title = "IGNORE_LIST" },
|
|
}
|
|
|
|
-- Client global string, or the key itself when missing.
|
|
local function txt(key)
|
|
return _G[key] or key
|
|
end
|
|
|
|
-- r, g, b, a of the client color global name, or of default when missing
|
|
local function color(name, default)
|
|
local c = _G[name]
|
|
if type(c) == "table" and c.r then
|
|
return c.r, c.g, c.b, c.a
|
|
end
|
|
return default[1], default[2], default[3], default[4]
|
|
end
|
|
|
|
-- ------------------------------------------------------------------ Frame
|
|
|
|
-- Builds the window art: rock background, top streaks, metal border, portrait, title
|
|
-- and close button.
|
|
local function buildFrame(f)
|
|
local rock = f:CreateTexture(nil, "BACKGROUND")
|
|
rock:SetTexture(G.rock, true)
|
|
if rock.SetHorizTile then rock:SetHorizTile(true) rock:SetVertTile(true) end
|
|
rock:SetPoint("TOPLEFT", f, "TOPLEFT", 2, -21)
|
|
rock:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -2, 2)
|
|
|
|
local stripes = f:CreateTexture(nil, "BORDER")
|
|
ForeverUI.SetAtlas(stripes, "_ui-frame-toptilestreaks", true)
|
|
stripes:SetHeight(43)
|
|
stripes:SetPoint("TOPLEFT", f, "TOPLEFT", 6, -21)
|
|
stripes:SetPoint("TOPRIGHT", f, "TOPRIGHT", -2, -21)
|
|
|
|
local metal = CreateFrame("Frame", nil, f)
|
|
metal:SetAllPoints(f)
|
|
metal:SetFrameLevel(f:GetFrameLevel() + 20)
|
|
local p = {}
|
|
for _, corner in ipairs(METAL) do
|
|
local t = metal:CreateTexture(nil, "OVERLAY")
|
|
ForeverUI.SetAtlas(t, corner.name)
|
|
t:SetPoint(corner.point, metal, corner.point, corner.x, corner.y)
|
|
p[corner.key] = t
|
|
end
|
|
local function edge(name, a1, c1, r1, a2, c2, r2)
|
|
local t = metal:CreateTexture(nil, "OVERLAY")
|
|
ForeverUI.SetAtlas(t, name)
|
|
t:SetPoint(a1, c1, r1)
|
|
t:SetPoint(a2, c2, r2)
|
|
end
|
|
edge("_ui-frame-metal-edgetop", "TOPLEFT", p.topLeft, "TOPRIGHT", "TOPRIGHT", p.topRight, "TOPLEFT")
|
|
edge("_ui-frame-metal-edgebottom", "BOTTOMLEFT", p.bottomLeft, "BOTTOMRIGHT", "BOTTOMRIGHT", p.bottomRight, "BOTTOMLEFT")
|
|
edge("!ui-frame-metal-edgeleft", "TOPLEFT", p.topLeft, "BOTTOMLEFT", "BOTTOMLEFT", p.bottomLeft, "TOPLEFT")
|
|
edge("!ui-frame-metal-edgeright", "TOPRIGHT", p.topRight, "BOTTOMRIGHT", "BOTTOMRIGHT", p.bottomRight, "TOPRIGHT")
|
|
|
|
local portraitFrame = CreateFrame("Frame", nil, f)
|
|
portraitFrame:SetAllPoints(f)
|
|
portraitFrame:SetFrameLevel(f:GetFrameLevel() + 19)
|
|
local portrait = portraitFrame:CreateTexture(nil, "OVERLAY")
|
|
portrait:SetWidth(G.portraitSide)
|
|
portrait:SetHeight(G.portraitSide)
|
|
portrait:SetPoint("TOPLEFT", f, "TOPLEFT", G.portraitX, G.portraitY)
|
|
portrait:SetTexture(G.portrait)
|
|
f.portrait = portrait
|
|
|
|
local banner = CreateFrame("Frame", nil, f)
|
|
banner:SetFrameLevel(f:GetFrameLevel() + 21)
|
|
banner:SetHeight(G.titleH)
|
|
banner:SetPoint("TOPLEFT", f, "TOPLEFT", G.titleX1, G.titleY)
|
|
banner:SetPoint("TOPRIGHT", f, "TOPRIGHT", G.titleX2, G.titleY)
|
|
f.title = banner:CreateFontString(nil, "OVERLAY", "GameFontNormal")
|
|
f.title:SetPoint("TOP", banner, "TOP", 0, G.titleTextY)
|
|
f.banner = banner
|
|
|
|
-- Close button: clicks the client's (SecureTemplates "click" action), whose HideParentPanel
|
|
-- then hides FriendsFrame as client code. Protected by the secure tabs, FriendsFrame could
|
|
-- not be hidden by ours in combat.
|
|
local closeButton = CreateFrame("Button", "ForeverUISocialCloseButton", f, "SecureActionButtonTemplate")
|
|
closeButton:SetWidth(G.closeButton)
|
|
closeButton:SetHeight(G.closeButton)
|
|
closeButton:SetFrameLevel(f:GetFrameLevel() + 22)
|
|
closeButton:SetPoint("TOPRIGHT", f, "TOPRIGHT", G.closeButtonX, G.closeButtonY)
|
|
for _, state in ipairs({
|
|
{ "SetNormalTexture", "GetNormalTexture", "redbutton-exit" },
|
|
{ "SetPushedTexture", "GetPushedTexture", "redbutton-exit-pressed" },
|
|
{ "SetHighlightTexture", "GetHighlightTexture", "redbutton-highlight" },
|
|
}) do
|
|
local e = ForeverUI.AtlasEntry(state[3])
|
|
closeButton[state[1]](closeButton, e and e[1] or "")
|
|
local t = closeButton[state[2]](closeButton)
|
|
if t then
|
|
ForeverUI.SetAtlas(t, state[3], true)
|
|
t:ClearAllPoints()
|
|
t:SetAllPoints(closeButton)
|
|
if state[3] == "redbutton-highlight" then t:SetBlendMode("ADD") end
|
|
end
|
|
end
|
|
closeButton:SetAttribute("type", "click")
|
|
closeButton:SetAttribute("clickbutton", FriendsFrameCloseButton)
|
|
f.closeButton = closeButton
|
|
end
|
|
|
|
-- --------------------------------------------------------------- Tabs
|
|
|
|
-- One tab texture. Top tabs use it flipped: a half turn swaps both edges in width and
|
|
-- height (SetRotation would drop the atlas rectangle).
|
|
local function tabPiece(b, layer, atlas, flipped)
|
|
local t = b:CreateTexture(nil, layer)
|
|
ForeverUI.SetAtlas(t, atlas)
|
|
if flipped then
|
|
local e = ForeverUI.AtlasEntry(atlas)
|
|
if e then t:SetTexCoord(e[3], e[2], e[5], e[4]) end
|
|
end
|
|
return t
|
|
end
|
|
|
|
-- PanelTabButtonTemplate (bottom) or TabSystemButtonArtTemplate + isTabOnTop (top).
|
|
-- Two sets of three pieces, active and inactive, plus the highlight: inactive art in ADD
|
|
-- at 0.4. atTop: sub-tab above the list; template: optional frame template
|
|
local function createTab(parent, name, atTop, template)
|
|
local b = CreateFrame("Button", name, parent, template)
|
|
b:SetHeight(atTop and G.subTabH or G.tabH)
|
|
local a = {}
|
|
a.activeLeft = tabPiece(b, "BACKGROUND", TAB_ART.activeLeft, atTop)
|
|
a.activeRight = tabPiece(b, "BACKGROUND", TAB_ART.activeRight, atTop)
|
|
a.activeMiddle = tabPiece(b, "BACKGROUND", TAB_ART.activeMiddle, atTop)
|
|
a.g = tabPiece(b, "BACKGROUND", TAB_ART.inactiveLeft, atTop)
|
|
a.d = tabPiece(b, "BACKGROUND", TAB_ART.inactiveRight, atTop)
|
|
a.m = tabPiece(b, "BACKGROUND", TAB_ART.inactiveMiddle, atTop)
|
|
a.sg = tabPiece(b, "HIGHLIGHT", TAB_ART.inactiveLeft, atTop)
|
|
a.sd = tabPiece(b, "HIGHLIGHT", TAB_ART.inactiveRight, atTop)
|
|
a.sm = tabPiece(b, "HIGHLIGHT", TAB_ART.inactiveMiddle, atTop)
|
|
for _, t in ipairs({ a.sg, a.sd, a.sm }) do
|
|
t:SetBlendMode("ADD")
|
|
t:SetAlpha(0.4)
|
|
end
|
|
if atTop then
|
|
-- HandleRotation: the flipped right piece moves to the left; SetTabHeight(24) sets all
|
|
-- heights
|
|
for _, t in pairs(a) do t:SetHeight(G.subTabH) end
|
|
a.activeRight:SetPoint("BOTTOMLEFT", b, "BOTTOMLEFT", -7, 0)
|
|
a.activeLeft:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", 0, 0)
|
|
a.d:SetPoint("BOTTOMLEFT", b, "BOTTOMLEFT", -6, 0)
|
|
a.g:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", 0, 0)
|
|
a.activeMiddle:SetPoint("TOPLEFT", a.activeRight, "TOPRIGHT", 0, 0)
|
|
a.activeMiddle:SetPoint("BOTTOMRIGHT", a.activeLeft, "BOTTOMLEFT", 0, 0)
|
|
a.m:SetPoint("TOPLEFT", a.d, "TOPRIGHT", 0, 0)
|
|
a.m:SetPoint("BOTTOMRIGHT", a.g, "BOTTOMLEFT", 0, 0)
|
|
a.sg:SetPoint("TOPRIGHT", a.g, "TOPRIGHT", 0, 0)
|
|
a.sd:SetPoint("TOPLEFT", a.d, "TOPLEFT", 0, 0)
|
|
a.sm:SetPoint("TOPLEFT", a.m, "TOPLEFT", 0, 0)
|
|
a.sm:SetPoint("BOTTOMRIGHT", a.m, "BOTTOMRIGHT", 0, 0)
|
|
else
|
|
a.activeLeft:SetPoint("TOPLEFT", b, "TOPLEFT", -1, 0)
|
|
a.activeRight:SetPoint("TOPRIGHT", b, "TOPRIGHT", 8, 0)
|
|
a.g:SetPoint("TOPLEFT", b, "TOPLEFT", -3, 0)
|
|
a.d:SetPoint("TOPRIGHT", b, "TOPRIGHT", 7, 0)
|
|
a.activeMiddle:SetPoint("TOPLEFT", a.activeLeft, "TOPRIGHT", 0, 0)
|
|
a.activeMiddle:SetPoint("BOTTOMRIGHT", a.activeRight, "BOTTOMLEFT", 0, 0)
|
|
a.m:SetPoint("TOPLEFT", a.g, "TOPRIGHT", 0, 0)
|
|
a.m:SetPoint("BOTTOMRIGHT", a.d, "BOTTOMLEFT", 0, 0)
|
|
a.sg:SetPoint("TOPLEFT", a.g, "TOPLEFT", 0, 0)
|
|
a.sd:SetPoint("TOPRIGHT", a.d, "TOPRIGHT", 0, 0)
|
|
a.sm:SetPoint("TOPLEFT", a.m, "TOPLEFT", 0, 0)
|
|
a.sm:SetPoint("BOTTOMRIGHT", a.m, "BOTTOMRIGHT", 0, 0)
|
|
end
|
|
b.art = a
|
|
b.atTop = atTop
|
|
local fs = b:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
|
|
fs:SetHeight(10)
|
|
b:SetFontString(fs)
|
|
b.text = fs
|
|
return b
|
|
end
|
|
|
|
-- SetTabSelected / PanelTemplates_SelectTab: active art, highlight font, text lowered
|
|
-- (bottom) or raised (top), selected tab disabled.
|
|
-- selected: this tab is chosen; active: the tab can be clicked
|
|
local function selectTab(b, selected, active)
|
|
local a = b.art
|
|
for _, t in ipairs({ a.activeLeft, a.activeMiddle, a.activeRight }) do
|
|
if selected then t:Show() else t:Hide() end
|
|
end
|
|
for _, t in ipairs({ a.g, a.m, a.d }) do
|
|
if selected then t:Hide() else t:Show() end
|
|
end
|
|
local font = selected and GameFontHighlightSmall or (active and GameFontNormalSmall or GameFontDisableSmall)
|
|
b.text:SetFontObject(font)
|
|
b:SetNormalFontObject(font)
|
|
local y
|
|
if b.atTop then
|
|
y = selected and 0 or -3
|
|
else
|
|
y = selected and -3 or 2
|
|
end
|
|
b.text:ClearAllPoints()
|
|
b.text:SetPoint("CENTER", b, "CENTER", 0, y)
|
|
if b.secureTab then
|
|
-- A secure tab cannot be disabled in combat, and need not be: it clicks the client's
|
|
-- tab, which the client disables when it is chosen or unavailable. Only the highlight
|
|
-- goes, as on a disabled tab.
|
|
for _, t in ipairs({ a.sg, a.sd, a.sm }) do
|
|
if selected or not active then t:Hide() else t:Show() end
|
|
end
|
|
elseif selected or not active then
|
|
b:Disable()
|
|
else
|
|
b:Enable()
|
|
end
|
|
end
|
|
|
|
-- PanelTemplates_TabResize / TabSystemButtonMixin:UpdateTabWidth
|
|
local function tabWidth(b)
|
|
local text = b.text:GetStringWidth() or 0
|
|
local g = ForeverUI.AtlasEntry(TAB_ART.inactiveLeft)
|
|
local d = ForeverUI.AtlasEntry(TAB_ART.inactiveRight)
|
|
local sides = (g and g[6] or 35) + (d and d[6] or 37)
|
|
if b.atTop then
|
|
local l = sides + 20
|
|
if l < text then l = text + 10 end
|
|
return math.max(G.subTabMin, math.min(G.subTabMax, l))
|
|
end
|
|
return math.max(sides, text + G.tabMargin)
|
|
end
|
|
|
|
-- bottom tabs, shared with other windows (raid browser)
|
|
S.createTab, S.selectTab, S.tabWidth = createTab, selectTab, tabWidth
|
|
|
|
-- ------------------------------------------------------------------ List
|
|
|
|
local ROWS = {} -- rows created once, then reused
|
|
|
|
-- Creates list row n, holding the parts of every row kind.
|
|
local function createRow(n)
|
|
local l = CreateFrame("Button", "ForeverUISocialRow" .. n, S.list)
|
|
l:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
|
l.background = l:CreateTexture(nil, "BACKGROUND")
|
|
l.background:SetPoint("TOPLEFT", l, "TOPLEFT", 0, -1)
|
|
l.background:SetPoint("BOTTOMRIGHT", l, "BOTTOMRIGHT", 0, 1)
|
|
l.state = l:CreateTexture(nil, "ARTWORK")
|
|
l.state:SetWidth(G.stateSide)
|
|
l.state:SetHeight(G.stateSide)
|
|
l.state:SetPoint("TOPLEFT", l, "TOPLEFT", G.stateX, G.stateY)
|
|
l.name = l:CreateFontString(nil, "ARTWORK", "FriendsFont_Normal")
|
|
l.name:SetJustifyH("LEFT")
|
|
l.name:SetHeight(12)
|
|
l.info = l:CreateFontString(nil, "ARTWORK", "FriendsFont_Small")
|
|
l.info:SetJustifyH("LEFT")
|
|
l.info:SetHeight(10)
|
|
l.info:SetPoint("TOPLEFT", l.name, "BOTTOMLEFT", 0, G.infoY)
|
|
l.info:SetPoint("TOPRIGHT", l.name, "BOTTOMRIGHT", 0, G.infoY)
|
|
l.line = l:CreateTexture(nil, "ARTWORK")
|
|
l.line:SetTexture(G.separator)
|
|
l.line:SetAllPoints(l)
|
|
l.title = l:CreateFontString(nil, "ARTWORK", "GameFontHighlightLeft")
|
|
l.title:SetPoint("LEFT", l, "LEFT", 5, 1)
|
|
l:SetHighlightTexture(G.highlight)
|
|
local s = l:GetHighlightTexture()
|
|
if s then
|
|
s:SetBlendMode("ADD")
|
|
s:ClearAllPoints()
|
|
s:SetPoint("TOPLEFT", l, "TOPLEFT", 0, -1)
|
|
s:SetPoint("BOTTOMRIGHT", l, "BOTTOMRIGHT", 0, 1)
|
|
end
|
|
l:SetScript("OnClick", function(self, button) S.click(self, button) end)
|
|
l:SetScript("OnEnter", function(self)
|
|
if self.kind == "friend" and FriendsFrameTooltip_Show then
|
|
FriendsFrameTooltip_Show(self)
|
|
end
|
|
end)
|
|
l:SetScript("OnLeave", function()
|
|
if FriendsTooltip then
|
|
FriendsTooltip.button = nil
|
|
FriendsTooltip:Hide()
|
|
end
|
|
end)
|
|
return l
|
|
end
|
|
|
|
-- Lays out a row for its entry kind: friend, line, header or ignore. Parts of the other
|
|
-- kinds are hidden. l: row; e: entry from entries()
|
|
local function layoutRow(l, e)
|
|
l.kind = e.kind
|
|
local isFriend, line, header = e.kind == "friend", e.kind == "line", e.kind == "header"
|
|
local ignore = e.kind == "ignore"
|
|
l:SetHeight(isFriend and G.friendRowH or G.shortRowH)
|
|
if isFriend then l.background:Show() else l.background:Hide() end
|
|
if isFriend then l.state:Show() else l.state:Hide() end
|
|
if isFriend or ignore then l.name:Show() else l.name:Hide() end
|
|
if isFriend then l.info:Show() else l.info:Hide() end
|
|
if line then l.line:Show() else l.line:Hide() end
|
|
if header then l.title:Show() else l.title:Hide() end
|
|
l:EnableMouse(isFriend or ignore)
|
|
l:UnlockHighlight()
|
|
|
|
local s = l:GetHighlightTexture()
|
|
l.name:ClearAllPoints()
|
|
if isFriend then
|
|
l.name:SetFontObject(FriendsFont_Normal)
|
|
l.name:SetPoint("TOPLEFT", l, "TOPLEFT", G.nameX, G.nameY)
|
|
l.name:SetPoint("TOPRIGHT", l, "TOPRIGHT", -4, G.nameY)
|
|
if s then
|
|
s:SetTexture(G.highlight)
|
|
s:SetVertexColor(G.highlightTint[1], G.highlightTint[2], G.highlightTint[3])
|
|
end
|
|
elseif ignore then
|
|
-- FriendsFrameIgnoreButtonTemplate: GameFontNormal at (10, 1)
|
|
l.name:SetFontObject(GameFontNormal)
|
|
l.name:SetPoint("LEFT", l, "LEFT", 10, 1)
|
|
l.name:SetPoint("RIGHT", l, "RIGHT", -4, 1)
|
|
if s then
|
|
s:SetTexture(G.ignoreHighlight)
|
|
s:SetVertexColor(1, 1, 1)
|
|
end
|
|
end
|
|
|
|
l.id = e.index
|
|
l.squelch = e.squelch
|
|
l.buttonType = isFriend and FRIENDS_BUTTON_TYPE_WOW or nil
|
|
if isFriend then
|
|
-- FriendsFrame_SetButton, for a game friend
|
|
local name, level, className, zone, connected, status = GetFriendInfo(e.index)
|
|
l.friendName, l.connected = name, connected
|
|
if connected then
|
|
l.background:SetTexture(color("FRIENDS_WOW_BACKGROUND_COLOR", { 1.0, 0.824, 0.0, 0.05 }))
|
|
if status == CHAT_FLAG_AFK then
|
|
l.state:SetTexture(G.state .. "Away")
|
|
elseif status == CHAT_FLAG_DND then
|
|
l.state:SetTexture(G.state .. "DnD")
|
|
else
|
|
l.state:SetTexture(G.state .. "Online")
|
|
end
|
|
l.name:SetText((name or "") .. ", " .. string.format(txt("FRIENDS_LEVEL_TEMPLATE"), level or 0, className or ""))
|
|
l.name:SetTextColor(color("FRIENDS_WOW_NAME_COLOR", { 0.996, 0.882, 0.361 }))
|
|
else
|
|
l.background:SetTexture(color("FRIENDS_OFFLINE_BACKGROUND_COLOR", { 0.588, 0.588, 0.588, 0.05 }))
|
|
l.state:SetTexture(G.state .. "Offline")
|
|
l.name:SetText(name or "")
|
|
l.name:SetTextColor(color("FRIENDS_GRAY_COLOR", { 0.486, 0.518, 0.541 }))
|
|
end
|
|
l.info:SetText(zone or "")
|
|
l.info:SetTextColor(color("FRIENDS_GRAY_COLOR", { 0.486, 0.518, 0.541 }))
|
|
if GetSelectedFriend() == e.index then l:LockHighlight() end
|
|
elseif ignore then
|
|
local name = GetIgnoreName(e.index)
|
|
if FriendsFrame.selectedSquelchType == SQUELCH_TYPE_IGNORE and GetSelectedIgnore() == e.index then
|
|
l:LockHighlight()
|
|
end
|
|
l.name:SetText(name or txt("UNKNOWN"))
|
|
l.name:SetTextColor(color("NORMAL_FONT_COLOR", { 1, 0.82, 0 }))
|
|
elseif header then
|
|
l.title:SetText(e.text)
|
|
end
|
|
end
|
|
|
|
-- What the list shows, in WotLK order.
|
|
local function entries()
|
|
local list = {}
|
|
if S.subTab() == 2 then
|
|
-- IgnoreList_Update: ignored players
|
|
local ignores = GetNumIgnores() or 0
|
|
if ignores > 0 then
|
|
list[#list + 1] = { kind = "header", text = txt("IGNORED") }
|
|
for i = 1, ignores do
|
|
list[#list + 1] = { kind = "ignore", index = i, squelch = SQUELCH_TYPE_IGNORE }
|
|
end
|
|
end
|
|
else
|
|
-- FriendsList_Update: online, a divider, offline
|
|
local total, online = GetNumFriends()
|
|
total, online = total or 0, online or 0
|
|
for i = 1, online do
|
|
list[#list + 1] = { kind = "friend", index = i }
|
|
end
|
|
if online > 0 and total > online then
|
|
list[#list + 1] = { kind = "line" }
|
|
end
|
|
for i = online + 1, total do
|
|
list[#list + 1] = { kind = "friend", index = i }
|
|
end
|
|
end
|
|
return list
|
|
end
|
|
|
|
local function heightOf(e)
|
|
return e.kind == "friend" and G.friendRowH or G.shortRowH
|
|
end
|
|
|
|
-- Fills the rows that fit, from S.offset (friend rows are taller).
|
|
function S.layoutList()
|
|
local list = S.content or {}
|
|
local position = S.list:GetHeight() or 0
|
|
if position <= 0 then
|
|
position = G.height + G.listY - (G.frameBoxY2 + G.listY2)
|
|
end
|
|
local total = #list
|
|
S.offset = math.max(0, math.min(S.offset or 0, total - 1))
|
|
local y, rank, n = 0, 0, S.offset + 1
|
|
while n <= total do
|
|
local h = heightOf(list[n])
|
|
if y + h > position then break end
|
|
rank = rank + 1
|
|
local l = ROWS[rank]
|
|
if not l then
|
|
l = createRow(rank)
|
|
ROWS[rank] = l
|
|
end
|
|
l:ClearAllPoints()
|
|
l:SetPoint("TOPLEFT", S.list, "TOPLEFT", 0, -y)
|
|
l:SetPoint("TOPRIGHT", S.list, "TOPRIGHT", 0, -y)
|
|
layoutRow(l, list[n])
|
|
l:Show()
|
|
y = y + h
|
|
n = n + 1
|
|
end
|
|
for i = rank + 1, #ROWS do
|
|
ROWS[i]:Hide()
|
|
end
|
|
S.visibleCount = rank
|
|
S.bar:Configure(total, rank, S.offset)
|
|
end
|
|
|
|
-- ---------------------------------------------------------------- Buttons
|
|
|
|
-- 3.3.5 has no SetShown
|
|
local function showRegion(x, yes)
|
|
if yes then x:Show() else x:Hide() end
|
|
end
|
|
|
|
-- Enables or disables a button (Activate on ForeverUI panel buttons).
|
|
local function active(b, yes)
|
|
if b.Activate then b:Activate(yes) elseif yes then b:Enable() else b:Disable() end
|
|
end
|
|
|
|
local function updateButtons()
|
|
local b = S.buttons
|
|
local ignore = S.subTab() == 2
|
|
showRegion(b.add, not ignore)
|
|
showRegion(b.message, not ignore)
|
|
showRegion(b.ignoreButton, ignore)
|
|
showRegion(b.remove, ignore)
|
|
if not ignore then
|
|
-- FriendsList_Update: Send Message only to an online friend
|
|
local selected = GetSelectedFriend() or 0
|
|
local connected = false
|
|
if selected > 0 then
|
|
connected = select(5, GetFriendInfo(selected)) and true or false
|
|
end
|
|
active(b.message, connected)
|
|
else
|
|
local index = 0
|
|
if FriendsFrame.selectedSquelchType == SQUELCH_TYPE_IGNORE then
|
|
index = GetSelectedIgnore() or 0
|
|
end
|
|
active(b.remove, index > 0)
|
|
end
|
|
end
|
|
|
|
-- ------------------------------------------------------------ Update
|
|
|
|
-- Selected Contacts sub-tab: 1 friends, 2 ignore. Kept here: written from addon code,
|
|
-- FriendsTabHeader.selectedTab would taint the client's FriendsFrame_Update, which reads it
|
|
-- and shows or hides RaidFrame (protected once Blizzard_RaidUI loads), blocked in combat.
|
|
S.selectedSubTab = 1
|
|
function S.subTab()
|
|
return S.selectedSubTab == 2 and 2 or 1
|
|
end
|
|
|
|
-- /friends and /ignore (ToggleFriendsPanel, ToggleIgnorePanel) select the client's sub-tab:
|
|
-- ours follows.
|
|
for name, id in pairs({ ToggleFriendsPanel = 1, ToggleIgnorePanel = 2 }) do
|
|
if type(_G[name]) == "function" then
|
|
hooksecurefunc(name, function()
|
|
if S.selectedSubTab ~= id then
|
|
S.selectedSubTab, S.offset = id, 0
|
|
end
|
|
if S.frame and S.frame:IsVisible() then S.update() end
|
|
end)
|
|
end
|
|
end
|
|
|
|
-- Default selection, like the client: first friend, first ignored player.
|
|
local function selectDefault()
|
|
if S.subTab() == 2 then
|
|
local ok = FriendsFrame.selectedSquelchType == SQUELCH_TYPE_IGNORE and (GetSelectedIgnore() or 0) > 0
|
|
if not ok and (GetNumIgnores() or 0) > 0 then
|
|
FriendsFrame_SelectSquelched(SQUELCH_TYPE_IGNORE, 1)
|
|
end
|
|
else
|
|
local total = GetNumFriends() or 0
|
|
if total > 0 and (GetSelectedFriend() or 0) == 0 then
|
|
FriendsFrame_SelectFriend(FRIENDS_BUTTON_TYPE_WOW, 1)
|
|
end
|
|
FriendsFrame.selectedFriend = GetSelectedFriend()
|
|
FriendsFrame.selectedFriendType = FRIENDS_BUTTON_TYPE_WOW
|
|
end
|
|
end
|
|
|
|
-- Bottom tabs follow the client's selected tab; Guild is disabled outside a guild, like
|
|
-- the client's (InGuildCheck).
|
|
function S.updateTabs()
|
|
local selected = FriendsFrame.selectedTab or 1
|
|
for _, o in ipairs(S.tabs) do
|
|
local client = _G["FriendsFrameTab" .. o.id]
|
|
local isOpen = true
|
|
if client and client.IsEnabled and o.id ~= selected then
|
|
local e = client:IsEnabled()
|
|
isOpen = (e ~= nil and e ~= false and e ~= 0)
|
|
end
|
|
o.button:SetText(txt(o.text))
|
|
selectTab(o.button, o.id == selected, isOpen)
|
|
-- secure, so sized out of combat only (their texts do not change)
|
|
if not InCombatLockdown() then
|
|
o.button:SetWidth(tabWidth(o.button))
|
|
end
|
|
end
|
|
end
|
|
|
|
function S.updateContacts()
|
|
local f = S.frame
|
|
if not f then return end
|
|
local sub = S.subTab()
|
|
f.title:SetText(txt(SUB_TABS[sub].title))
|
|
|
|
-- sub-tabs
|
|
for _, o in ipairs(S.subTabs) do
|
|
o.button:SetText(txt(o.text))
|
|
selectTab(o.button, o.id == sub, true)
|
|
o.button:SetWidth(tabWidth(o.button))
|
|
end
|
|
|
|
selectDefault()
|
|
S.content = entries()
|
|
S.layoutList()
|
|
updateButtons()
|
|
end
|
|
|
|
-- ------------------------------------------------------------ Clicks
|
|
|
|
function S.click(l, button)
|
|
if l.kind == "friend" then
|
|
if button == "RightButton" then
|
|
local name, _, _, _, connected = GetFriendInfo(l.id)
|
|
ForeverUI.UnitMenu.open(FriendsFrame_ShowDropdown, name, connected, nil, nil, nil, 1)
|
|
else
|
|
PlaySound("igMainMenuOptionCheckBoxOn")
|
|
FriendsFrame_SelectFriend(FRIENDS_BUTTON_TYPE_WOW, l.id)
|
|
FriendsFrame.selectedFriend = l.id
|
|
end
|
|
elseif l.kind == "ignore" then
|
|
PlaySound("igMainMenuOptionCheckBoxOn")
|
|
FriendsFrame_SelectSquelched(l.squelch, l.id)
|
|
end
|
|
S.update()
|
|
end
|
|
|
|
-- ------------------------------------------------------ WotLK or camelot
|
|
|
|
-- Client frames our window keeps and borrows: the right-click menu and the tooltip.
|
|
local KEPT_FRAMES = { "FriendsDropDown", "FriendsTooltip" }
|
|
|
|
-- A page can keep a client frame (the raid frame of Blizzard_RaidUI...).
|
|
function S.keep(name)
|
|
KEPT_FRAMES[#KEPT_FRAMES + 1] = name
|
|
end
|
|
|
|
-- Hides the client panel's own content, except kept frames, and stops it taking the
|
|
-- mouse.
|
|
local function suppressWotLK()
|
|
local ff = FriendsFrame
|
|
S.killedRegions = S.killedRegions or {}
|
|
for _, r in ipairs({ ff:GetRegions() }) do
|
|
if r:IsShown() then
|
|
S.killedRegions[r] = true
|
|
r:Hide()
|
|
end
|
|
end
|
|
local keep = { [S.frame] = true }
|
|
for _, n in ipairs(KEPT_FRAMES) do
|
|
local c = type(n) == "string" and _G[n] or n
|
|
if c then keep[c] = true end
|
|
end
|
|
-- A protected frame cannot be hidden in combat: in a raid, RaidFrame holds the secure
|
|
-- buttons of Blizzard_RaidUI. It is faded with alpha, which combat allows, and hidden on
|
|
-- the next pass out of combat.
|
|
local combat = InCombatLockdown and InCombatLockdown()
|
|
for _, c in ipairs({ ff:GetChildren() }) do
|
|
if not keep[c] and c:IsShown() then
|
|
if combat and c.IsProtected and c:IsProtected() then
|
|
c:SetAlpha(0)
|
|
else
|
|
c:Hide()
|
|
end
|
|
end
|
|
end
|
|
if not (combat and ff.IsProtected and ff:IsProtected()) then
|
|
ff:EnableMouse(false)
|
|
end
|
|
end
|
|
|
|
-- Pages: one per client tab. Each registers build(frame), update(), showRegion(firstShow),
|
|
-- hide() and title().
|
|
S.pages = {}
|
|
local registeredPages = {}
|
|
|
|
function S.registerPage(id, def)
|
|
registeredPages[id] = def
|
|
end
|
|
|
|
-- Returns the page of tab id, building it on first use.
|
|
local function page(id)
|
|
local pg = S.pages[id]
|
|
if pg then return pg end
|
|
local def = registeredPages[id]
|
|
if not def then return nil end
|
|
local frame = CreateFrame("Frame", "ForeverUISocialPage" .. id, S.frame)
|
|
frame:SetAllPoints(S.frame)
|
|
frame:Hide()
|
|
pg = { id = id, frame = frame, def = def }
|
|
S.pages[id] = pg
|
|
def.build(frame)
|
|
return pg
|
|
end
|
|
|
|
-- Builds a page ahead of time (the raid page, out of combat).
|
|
function S.prepare(id)
|
|
if S.frame then page(id) end
|
|
end
|
|
|
|
-- Refreshes the selected page and the tabs.
|
|
function S.update()
|
|
S.updateTabs()
|
|
local pg = S.pages[FriendsFrame.selectedTab or 1]
|
|
if pg and pg.frame:IsShown() then
|
|
if pg.def.title then S.frame.title:SetText(pg.def.title() or "") end
|
|
if pg.def.update then pg.def.update() end
|
|
end
|
|
end
|
|
|
|
-- Runs after FriendsFrame_Update, which has already set up the client screen.
|
|
-- FriendsFrame stays the client panel (ToggleFriendsFrame, micro button, Esc, panel slot,
|
|
-- selectedTab); our window, its child, hides the WotLK content and shows the page of the
|
|
-- selected tab. The page redoes what the client subframe does on show (SetWhoToUI,
|
|
-- GuildRoster...).
|
|
function S.apply()
|
|
if not S.frame or not FriendsFrame:IsShown() then return end
|
|
local selected = FriendsFrame.selectedTab or 1
|
|
-- page first: while building, it can keep a client frame
|
|
page(selected)
|
|
suppressWotLK()
|
|
-- Protected through its secure tabs: kept shown, it follows FriendsFrame
|
|
if not S.frame:IsShown() then S.frame:Show() end
|
|
for id, pg in pairs(S.pages) do
|
|
if id ~= selected and pg.frame:IsShown() then
|
|
pg.frame:Hide()
|
|
if pg.def.hide then pg.def.hide() end
|
|
end
|
|
end
|
|
local pg = page(selected)
|
|
if pg then
|
|
local wasShown = pg.frame:IsShown()
|
|
pg.frame:Show()
|
|
if pg.def.showRegion then pg.def.showRegion(not wasShown) end
|
|
end
|
|
S.update()
|
|
end
|
|
|
|
-- On close, the open page does what the client subframe does on hide (SetWhoToUI(0)...).
|
|
local function close()
|
|
for _, pg in pairs(S.pages) do
|
|
if pg.frame:IsShown() then
|
|
pg.frame:Hide()
|
|
if pg.def.hide then pg.def.hide() end
|
|
end
|
|
end
|
|
if S.popup then S.popup:Hide() end
|
|
end
|
|
|
|
-- ------------------------------------------------------ Shared parts
|
|
|
|
S.txt = txt
|
|
S.color = color
|
|
S.G = G
|
|
|
|
-- A panel button with the window's button font (GameFontNormal).
|
|
function S.button(parent, text, width, name)
|
|
return ForeverUI.CreatePanelButton(parent, text, width, G.buttonH, name, "GameFontNormal")
|
|
end
|
|
|
|
-- Column header: WhoFrameColumnHeaderTemplate, whose WotLK art camelot keeps (mainline):
|
|
-- WhoFrame-ColumnTabs in three pieces, UI-Character-Tab-Highlight in ADD.
|
|
local COLUMN_TABS = "Interface" .. SEP .. "FriendsFrame" .. SEP .. "WhoFrame-ColumnTabs"
|
|
local COLUMN_HIGHLIGHT = "Interface" .. SEP .. "PaperDollInfoFrame" .. SEP .. "UI-Character-Tab-Highlight"
|
|
S.COLUMN_H = 24
|
|
|
|
function S.createHeader(parent, name, width, text, onClick)
|
|
local b = CreateFrame("Button", name, parent)
|
|
b:SetHeight(S.COLUMN_H)
|
|
local g = b:CreateTexture(nil, "BACKGROUND")
|
|
g:SetTexture(COLUMN_TABS)
|
|
g:SetTexCoord(0, 0.078125, 0, 0.75)
|
|
g:SetWidth(5)
|
|
g:SetHeight(S.COLUMN_H)
|
|
g:SetPoint("TOPLEFT", b, "TOPLEFT", 0, 0)
|
|
local d = b:CreateTexture(nil, "BACKGROUND")
|
|
d:SetTexture(COLUMN_TABS)
|
|
d:SetTexCoord(0.90625, 0.96875, 0, 0.75)
|
|
d:SetWidth(4)
|
|
d:SetHeight(S.COLUMN_H)
|
|
d:SetPoint("TOPRIGHT", b, "TOPRIGHT", 0, 0)
|
|
local m = b:CreateTexture(nil, "BACKGROUND")
|
|
m:SetTexture(COLUMN_TABS)
|
|
m:SetTexCoord(0.078125, 0.90625, 0, 0.75)
|
|
m:SetHeight(S.COLUMN_H)
|
|
m:SetPoint("LEFT", g, "RIGHT", 0, 0)
|
|
m:SetPoint("RIGHT", d, "LEFT", 0, 0)
|
|
local fs = b:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
|
|
fs:SetPoint("LEFT", b, "LEFT", 8, 0)
|
|
fs:SetPoint("RIGHT", b, "RIGHT", -8, 0)
|
|
fs:SetJustifyH("LEFT")
|
|
b:SetFontString(fs)
|
|
b.text = fs
|
|
b:SetText(text or "")
|
|
b:SetHighlightTexture(COLUMN_HIGHLIGHT)
|
|
local s = b:GetHighlightTexture()
|
|
if s then
|
|
s:SetBlendMode("ADD")
|
|
s:ClearAllPoints()
|
|
s:SetPoint("TOPLEFT", g, "TOPLEFT", -2, 5)
|
|
s:SetPoint("BOTTOMRIGHT", d, "BOTTOMRIGHT", 2, -7)
|
|
end
|
|
b:SetWidth(width)
|
|
if onClick then
|
|
b:SetScript("OnClick", function(self)
|
|
onClick(self)
|
|
PlaySound("igMainMenuOptionCheckBoxOn")
|
|
end)
|
|
end
|
|
return b
|
|
end
|
|
|
|
-- Scrolling list with fixed-height rows: area, rows, camelot scroll bar and mouse wheel.
|
|
-- create(row, n) builds a row; populate(row, index) fills it; list:Update(total) redraws.
|
|
function S.createList(parent, name, lineHeight, create, populate)
|
|
local zone = CreateFrame("Frame", name, parent)
|
|
zone.rows = {}
|
|
zone.offset = 0
|
|
zone.total = 0
|
|
zone.lineHeight = lineHeight
|
|
zone.bar = ForeverUI.CreateScrollBar(name .. "ScrollBar", parent, zone)
|
|
function zone:VisibleCount()
|
|
local h = self:GetHeight() or 0
|
|
if h <= 0 then h = self.defaultHeight or (lineHeight * 10) end
|
|
return math.max(1, math.floor(h / lineHeight))
|
|
end
|
|
function zone:Update(total)
|
|
self.total = total or self.total
|
|
local visibleCount = self:VisibleCount()
|
|
self.offset = math.max(0, math.min(self.offset, self.total - visibleCount))
|
|
for n = 1, visibleCount do
|
|
local l = self.rows[n]
|
|
if not l then
|
|
l = CreateFrame("Button", name .. "Row" .. n, self)
|
|
l:SetHeight(lineHeight)
|
|
l:SetPoint("TOPLEFT", self, "TOPLEFT", 0, -(n - 1) * lineHeight)
|
|
l:SetPoint("TOPRIGHT", self, "TOPRIGHT", 0, -(n - 1) * lineHeight)
|
|
create(l, n)
|
|
self.rows[n] = l
|
|
end
|
|
local index = self.offset + n
|
|
if index <= self.total then
|
|
l.index = index
|
|
populate(l, index)
|
|
l:Show()
|
|
else
|
|
l.index = nil
|
|
l:Hide()
|
|
end
|
|
end
|
|
for n = visibleCount + 1, #self.rows do
|
|
self.rows[n]:Hide()
|
|
end
|
|
self.bar:Configure(self.total, visibleCount, self.offset)
|
|
-- the bar appears or goes: the right edge follows
|
|
local hasBar = self.total > visibleCount
|
|
if hasBar ~= self.hasBar then
|
|
self.hasBar = hasBar
|
|
self:PlaceAnchors()
|
|
end
|
|
end
|
|
-- Right edge follows the bar: with it, the list leaves room for it; without it, the list
|
|
-- (and the rows and headers anchored to it) reaches the edge. topLeft and
|
|
-- bottomRightPoint are { point, relativeTo, relativePoint, x, y }; noBar replaces the
|
|
-- bottom-right x when the bar is hidden.
|
|
function zone:FollowBar(topLeft, bottomRightPoint, noBar)
|
|
self.anchors = { topLeft = topLeft, bottomRightPoint = bottomRightPoint, noBar = noBar }
|
|
self:PlaceAnchors()
|
|
end
|
|
function zone:PlaceAnchors()
|
|
local a = self.anchors
|
|
if not a then return end
|
|
local h, b = a.topLeft, a.bottomRightPoint
|
|
self:ClearAllPoints()
|
|
self:SetPoint(h[1], h[2], h[3], h[4], h[5])
|
|
self:SetPoint(b[1], b[2], b[3], self.hasBar and b[4] or a.noBar, b[5])
|
|
end
|
|
zone.bar.onScroll = function(new)
|
|
zone.offset = new
|
|
zone:Update()
|
|
end
|
|
zone:EnableMouseWheel(true)
|
|
zone:SetScript("OnMouseWheel", function(self, direction)
|
|
self.offset = math.max(0, math.min(self.offset - direction, self.total - self:VisibleCount()))
|
|
self:Update()
|
|
end)
|
|
return zone
|
|
end
|
|
|
|
-- Camelot border on an edit box, ours or the client's (InputBoxTemplate: its <name>Left,
|
|
-- Middle and Right pieces are hidden).
|
|
function S.skinInput(b)
|
|
local name = b.GetName and b:GetName()
|
|
if name then
|
|
for _, side in ipairs({ "Left", "Middle", "Right" }) do
|
|
local t = _G[name .. side]
|
|
if t then t:SetAlpha(0) t:Hide() end
|
|
end
|
|
end
|
|
local g = b:CreateTexture(nil, "BACKGROUND")
|
|
ForeverUI.SetAtlas(g, "common-search-border-left", true)
|
|
g:SetWidth(8) g:SetHeight(20)
|
|
g:SetPoint("LEFT", b, "LEFT", -5, 0)
|
|
local d = b:CreateTexture(nil, "BACKGROUND")
|
|
ForeverUI.SetAtlas(d, "common-search-border-right", true)
|
|
d:SetWidth(8) d:SetHeight(20)
|
|
d:SetPoint("RIGHT", b, "RIGHT", 0, 0)
|
|
local m = b:CreateTexture(nil, "BACKGROUND")
|
|
ForeverUI.SetAtlas(m, "common-search-border-middle", true)
|
|
m:SetHeight(20)
|
|
m:SetPoint("LEFT", g, "RIGHT", 0, 0)
|
|
m:SetPoint("RIGHT", d, "LEFT", 0, 0)
|
|
b.camelotBorder = { g, m, d }
|
|
end
|
|
|
|
-- Checkbox: checkbox-minimal and checkmark-minimal, 26 x 26, like the Currency tab boxes.
|
|
function S.createCheckbox(parent, name, text)
|
|
local c = CreateFrame("CheckButton", name, parent)
|
|
c:SetWidth(26)
|
|
c:SetHeight(26)
|
|
S.skinCell(c)
|
|
c.text = c:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
|
|
c.text:SetPoint("LEFT", c, "RIGHT", 2, 1)
|
|
c.text:SetText(text or "")
|
|
return c
|
|
end
|
|
|
|
-- Camelot art on a checkbox, ours or the client's: empty box and check, without the WotLK
|
|
-- pushed and highlight states.
|
|
function S.skinCell(c)
|
|
local function place(setMethod, getMethod, atlas)
|
|
local e = ForeverUI.AtlasEntry(atlas)
|
|
if c[setMethod] then c[setMethod](c, e and e[1] or "") end
|
|
local t = c[getMethod] and c[getMethod](c)
|
|
if t then
|
|
ForeverUI.SetAtlas(t, atlas, true)
|
|
t:ClearAllPoints()
|
|
t:SetAllPoints(c)
|
|
end
|
|
return t
|
|
end
|
|
place("SetNormalTexture", "GetNormalTexture", "checkbox-minimal")
|
|
place("SetCheckedTexture", "GetCheckedTexture", "checkmark-minimal")
|
|
local d = place("SetDisabledCheckedTexture", "GetDisabledCheckedTexture", "checkmark-minimal")
|
|
if d then d:SetDesaturated(true) end
|
|
-- The pushed texture replaces the normal one, so it carries the same box, or the outline
|
|
-- vanishes during the click. Only the check comes and goes.
|
|
place("SetPushedTexture", "GetPushedTexture", "checkbox-minimal")
|
|
if c.SetHighlightTexture then c:SetHighlightTexture("") end
|
|
end
|
|
|
|
-- Side popup: member details, guild info, log, saved instances. One at a time, right of
|
|
-- the window, like GuildFramePopup_Show; the same portrait-less frame as arena team
|
|
-- details.
|
|
function S.createPopup(name, width, height)
|
|
local a = CreateFrame("Frame", name, S.frame)
|
|
a:SetWidth(width)
|
|
a:SetHeight(height)
|
|
a:SetPoint("TOPLEFT", S.frame, "TOPRIGHT", 12, 0)
|
|
a:SetFrameLevel(S.frame:GetFrameLevel() + 30)
|
|
a:EnableMouse(true)
|
|
a:Hide()
|
|
ForeverUI.SetPanelArt(a, { topLeftCorner = "ui-frame-metal-cornertopleft", level = 5 })
|
|
local metal = a.foreverSkinLayer or a
|
|
local banner = CreateFrame("Frame", nil, a)
|
|
banner:SetAllPoints(a)
|
|
banner:SetFrameLevel(metal:GetFrameLevel() + 1)
|
|
a.title = banner:CreateFontString(nil, "OVERLAY", "GameFontNormal")
|
|
a.title:SetPoint("TOP", a, "TOP", 0, -6)
|
|
a.title:SetWidth(width - 60)
|
|
local closeButton = CreateFrame("Button", nil, a)
|
|
closeButton:SetWidth(G.closeButton)
|
|
closeButton:SetHeight(G.closeButton)
|
|
closeButton:SetFrameLevel(metal:GetFrameLevel() + 2)
|
|
closeButton:SetPoint("TOPRIGHT", a, "TOPRIGHT", 1, 0)
|
|
for _, state in ipairs({
|
|
{ "SetNormalTexture", "GetNormalTexture", "redbutton-exit" },
|
|
{ "SetPushedTexture", "GetPushedTexture", "redbutton-exit-pressed" },
|
|
{ "SetHighlightTexture", "GetHighlightTexture", "redbutton-highlight" },
|
|
}) do
|
|
local e = ForeverUI.AtlasEntry(state[3])
|
|
closeButton[state[1]](closeButton, e and e[1] or "")
|
|
local t = closeButton[state[2]](closeButton)
|
|
if t then
|
|
ForeverUI.SetAtlas(t, state[3], true)
|
|
t:ClearAllPoints()
|
|
t:SetAllPoints(closeButton)
|
|
if state[3] == "redbutton-highlight" then t:SetBlendMode("ADD") end
|
|
end
|
|
end
|
|
closeButton:SetScript("OnClick", function() a:Hide() end)
|
|
a.closeButton = closeButton
|
|
a:HookScript("OnShow", function(self)
|
|
if S.popup and S.popup ~= self then S.popup:Hide() end
|
|
S.popup = self
|
|
PlaySound("igSpellBookOpen")
|
|
end)
|
|
a:HookScript("OnHide", function(self)
|
|
if S.popup == self then S.popup = nil end
|
|
PlaySound("igSpellBookClose")
|
|
end)
|
|
S.annexes = S.annexes or {}
|
|
table.insert(S.annexes, a)
|
|
return a
|
|
end
|
|
|
|
-- ------------------------------------------------------------ Assembly
|
|
|
|
local function build()
|
|
if S.frame or not FriendsFrame then return end
|
|
local f = CreateFrame("Frame", "ForeverUISocialFrame", FriendsFrame)
|
|
f:SetWidth(G.width)
|
|
f:SetHeight(G.height)
|
|
f:SetPoint("TOPLEFT", FriendsFrame, "TOPLEFT", 0, 0)
|
|
f:SetFrameLevel(FriendsFrame:GetFrameLevel() + 1)
|
|
f:EnableMouse(true)
|
|
-- FriendsFrame (384 x 512) catches no click outside our window. Set now, before the secure
|
|
-- tabs protect it: on a protected frame in combat, suppressWotLK cannot.
|
|
FriendsFrame:EnableMouse(false)
|
|
S.frame = f
|
|
buildFrame(f)
|
|
|
|
-- Contacts page: sub-tabs, inset, list and buttons.
|
|
S.registerPage(1, {
|
|
build = function() end,
|
|
title = function() return txt(SUB_TABS[S.subTab()].title) end,
|
|
update = function() S.updateContacts() end,
|
|
})
|
|
local p1 = page(1)
|
|
local contacts = p1.frame
|
|
|
|
local frameBox = ForeverUI.CreateInset(contacts, "ForeverUISocialInset")
|
|
frameBox:SetPoint("TOPLEFT", f, "TOPLEFT", G.frameBoxX1, G.frameBoxY1)
|
|
frameBox:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", G.frameBoxX2, G.frameBoxY2)
|
|
S.frameBox = frameBox
|
|
local list = CreateFrame("Frame", "ForeverUISocialList", contacts)
|
|
list:SetPoint("TOPLEFT", f, "TOPLEFT", G.listX, G.listY)
|
|
list:SetPoint("BOTTOMRIGHT", frameBox, "BOTTOMRIGHT", G.listX2, G.listY2)
|
|
list:EnableMouseWheel(true)
|
|
list:SetScript("OnMouseWheel", function(_, direction)
|
|
S.offset = math.max(0, math.min((S.offset or 0) - direction, #(S.content or {}) - (S.visibleCount or 0)))
|
|
S.layoutList()
|
|
end)
|
|
S.list = list
|
|
S.bar = ForeverUI.CreateScrollBar("ForeverUISocialScrollBar", contacts, list)
|
|
S.bar.onScroll = function(new)
|
|
S.offset = new
|
|
S.layoutList()
|
|
end
|
|
-- without the bar the list reaches the inset edge, like the Who and Guild lists; rows,
|
|
-- anchored on both sides, follow
|
|
S.bar.onVisibility = function(hasBar)
|
|
list:SetPoint("BOTTOMRIGHT", frameBox, "BOTTOMRIGHT", hasBar and G.listX2 or G.listX2NoBar, G.listY2)
|
|
end
|
|
|
|
-- bottom tabs, on the window
|
|
S.tabs = {}
|
|
local previous
|
|
for i, def in ipairs(TABS) do
|
|
-- Secure: a click on our tab clicks the client's (SecureTemplates "click" action), whose
|
|
-- PanelTemplates_Tab_OnClick and FriendsFrame_OnShow (sound included) then run as the
|
|
-- client's own code. Called from ours, they wrote FriendsFrame.selectedTab tainted, and
|
|
-- FriendsFrame_ShowSubFrame shows or hides RaidFrame, protected once Blizzard_RaidUI
|
|
-- loads: blocked in combat, and so was the next opening of the window in combat.
|
|
local b = createTab(f, "ForeverUISocialTab" .. i, false, "SecureActionButtonTemplate")
|
|
b.secureTab = true
|
|
b:SetAttribute("type", "click")
|
|
b:SetAttribute("clickbutton", _G["FriendsFrameTab" .. def.id])
|
|
if previous then
|
|
b:SetPoint("TOPLEFT", previous, "TOPRIGHT", G.tabGap, 0)
|
|
else
|
|
b:SetPoint("TOPLEFT", f, "BOTTOMLEFT", G.firstTabX, G.firstTabY)
|
|
end
|
|
-- sized now, out of combat: in combat a secure tab keeps its size
|
|
b:SetText(txt(def.text))
|
|
b:SetWidth(tabWidth(b))
|
|
S.tabs[i] = { id = def.id, text = def.text, button = b }
|
|
previous = b
|
|
end
|
|
|
|
-- sub-tabs: Friends and Ignore, state kept here (S.selectedSubTab)
|
|
S.subTabs = {}
|
|
previous = nil
|
|
for i, def in ipairs(SUB_TABS) do
|
|
local b = createTab(contacts, "ForeverUISocialSubTab" .. i, true)
|
|
b:SetFrameLevel(f:GetFrameLevel() + 2)
|
|
if previous then
|
|
b:SetPoint("TOPLEFT", previous, "TOPRIGHT", G.subTabGap, 0)
|
|
else
|
|
b:SetPoint("TOPLEFT", f, "TOPLEFT", G.subTabX, G.subTabY)
|
|
end
|
|
b:SetScript("OnClick", function()
|
|
PlaySound("igMainMenuOptionCheckBoxOn")
|
|
S.selectedSubTab = def.id
|
|
S.offset = 0
|
|
-- the client's Friends sub-tab asks the server for the list (FriendsFrame_Update)
|
|
if def.id == 1 then ShowFriends() end
|
|
S.update()
|
|
end)
|
|
S.subTabs[i] = { id = def.id, text = def.text, button = b }
|
|
previous = b
|
|
end
|
|
|
|
-- buttons, using the client's handlers
|
|
local b = {}
|
|
local function button(text, width)
|
|
return ForeverUI.CreatePanelButton(contacts, text, width, G.buttonH, nil, "GameFontNormal")
|
|
end
|
|
b.add = button(txt("ADD_FRIEND"), G.buttonW)
|
|
b.add:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", G.buttonLeft, G.buttonBottom)
|
|
b.add:SetScript("OnClick", function(self) FriendsFrameAddFriendButton_OnClick(self) end)
|
|
b.message = button(txt("SEND_MESSAGE"), G.buttonW)
|
|
b.message:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", G.buttonRight, G.buttonBottom)
|
|
b.message:SetScript("OnClick", function(self) FriendsFrameSendMessageButton_OnClick(self) end)
|
|
b.ignoreButton = button(txt("IGNORE_PLAYER"), G.buttonW)
|
|
b.ignoreButton:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", G.buttonLeft, G.buttonBottom)
|
|
b.ignoreButton:SetScript("OnClick", function()
|
|
-- OnClick of FriendsFrameIgnorePlayerButton, from the client XML
|
|
if UnitCanCooperate("player", "target") then
|
|
AddIgnore(UnitName("target"))
|
|
else
|
|
StaticPopup_Show("ADD_IGNORE")
|
|
end
|
|
end)
|
|
b.remove = button(txt("REMOVE_PLAYER"), G.buttonW)
|
|
b.remove:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", G.buttonRight, G.buttonBottom)
|
|
b.remove:SetScript("OnClick", function(self) FriendsFrameUnsquelchButton_OnClick(self) end)
|
|
S.buttons = b
|
|
-- left shown: the secure tabs protect the window, which could not be shown in combat; as a
|
|
-- child of FriendsFrame it appears and goes with it
|
|
end
|
|
|
|
build()
|
|
|
|
if S.frame then
|
|
hooksecurefunc("FriendsFrame_Update", S.apply)
|
|
FriendsFrame:HookScript("OnShow", S.apply)
|
|
FriendsFrame:HookScript("OnHide", close)
|
|
-- movable by its title; raised when opened or clicked
|
|
ForeverUI.WindowStack.makeMovable(S.frame, S.frame.banner, "social")
|
|
ForeverUI.WindowStack.register("social", FriendsFrame, function()
|
|
local z = { S.frame }
|
|
for _, a in ipairs(S.annexes or {}) do z[#z + 1] = a end
|
|
return z
|
|
end)
|
|
local listener = CreateFrame("Frame")
|
|
for _, ev in ipairs({ "FRIENDLIST_UPDATE", "IGNORELIST_UPDATE",
|
|
"PARTY_MEMBERS_CHANGED", "PLAYER_GUILD_UPDATE" }) do
|
|
listener:RegisterEvent(ev)
|
|
end
|
|
listener:SetScript("OnEvent", function()
|
|
if S.frame:IsVisible() and (FriendsFrame.selectedTab or 1) == 1 then S.update() end
|
|
end)
|
|
end
|