-- ForeverUI: WotLK's Dungeon Finder in a movable window styled like camelot's LFGParentFrame -- (Blizzard_GroupFinder_VanillaStyle; camelot has no dungeon finder), with the raid browser -- as a second side tab (GroupFinderRaid.lua). -- -- All data and actions come from WotLK (LFDFrame.lua, LFGFrame.lua). LFDParentFrame stays the -- client's: the micro button, /lfd, the key binding, the minimap eye, Escape and NPC gossip -- open and close it. Its content (LFDQueueFrame) stays shown but transparent under our window, -- so its logic keeps running; we read its state after each update and act through its -- functions (LFDQueueFrame_SetType, list checkboxes and +/-, role checkboxes, search button). -- -- Layout from blizzard_lfgvanilla_parentframe.xml, blizzard_lfgvanilla_listing.xml/.lua, -- lfgvanilla_constants.lua, sharedutils.lua and blizzard_sharedxml (values in G). -- camelot -> WotLK: -- categories the "Type" menu: one button per displayable random dungeon, then -- "Specific Dungeons"; the chosen type's button shows the selection -- list the specific dungeon list, drawn as the category list (F.createCatList) -- details text and rewards of the chosen random dungeon, under the buttons -- (camelot has none) -- Back, Post Back returns to the categories (list only); Post clicks the client's -- search button (Find Group / Join as Party / Leave Queue) -- veils WotLK's cooldown, backfill and "no LFD while in LFR" veils, over the -- details or the whole list -- Differences: no options button, no comment under the list, WotLK's lock replaces a locked -- dungeon's checkbox, no heroic icon on headers (their name says it). local ForeverUI = ForeverUI or {} _G.ForeverUI = ForeverUI local F = {} ForeverUI.GroupFinder = F local SEP = string.char(92) local L = ForeverUI.L local LFG = "Interface" .. SEP .. "LFGFrame" .. SEP local BUTTONS = "Interface" .. SEP .. "Buttons" .. SEP -- Layout values in one table: Lua 5.1 allows a function at most 60 upvalues. local G = { width = 458, height = 535, rock = "interface" .. SEP .. "ForeverUI" .. SEP .. "framegeneral" .. SEP .. "ui-background-rock", portraitSide = 62, portraitX = -5, portraitY = 7, titleX1 = 58, titleX2 = -24, titleY = -1, titleH = 20, titleTextY = -5, closeButton = 24, closeButtonX = 0, closeButtonY = 0, blue = "interface" .. SEP .. "ForeverUI" .. SEP .. "lfgframe" .. SEP .. "ui-lfg-bluebg", blueX1 = 2, blueY1 = -25, blueX2 = 248, blueY2 = -169, role = 64, roleY = -41, roleBackground = 80, roleCheckbox = 24, roleCheckboxX = -5, roleCheckboxY = -5, roleIcons = LFG .. "UI-LFG-ICON-ROLES", roleBackgrounds = LFG .. "UI-LFG-ICONS-ROLEBACKGROUNDS", frameBoxX1 = 4, frameBoxY1 = -118, frameBoxX2 = -4, frameBoxY2 = 37, backgroundTop = 0.093, catX1 = 8, catY1 = -124, catX2 = -8, catY2 = 40, catFirst = -4, catStep = 50, -- rule under the buttons, then the details; the details edges follow the 380-wide buttons -- centered in the 442-wide area (8 + (442 - 380) / 2 = 39), left edge 6 further in ruleGap = 6, ruleH = 16, detailsGap = 6, detailsPadding = 6, tolerance = 2, detailsX1 = 45, detailsX2 = -39, detailsBottom = 50, scrollStep = 20, catW = 380, catH = 50, catSelW = 366, catSelH = 40, -- image in the button: camelot's (5,-8 / -5,5), scaled to a 50-high button catImageTop = -6, catImageBottom = 4, mega = "interface" .. SEP .. "ForeverUI" .. SEP .. "pvpframe" .. SEP .. "pvpmegaqueue", pieces = "Interface" .. SEP .. "Icons" .. SEP .. "inv_misc_coin_02", listX1 = 7, listY1 = -125, listX2 = -7, listY2 = 40, margin = 4, barRight = -28, row = 22, plus = 16, checkbox = 30, levelL = 60, backL = 111, postW = 109, buttonH = 28, buttonBottom = 6, buttonSide = 4, tab = 55, tabY = -60, tabGap = -2, tabIcon = 50, tabIconX = -3, crop = 0.03125, veilW = 330, veilH = 257, } -- texcoords from sharedutils.lua (GetTexCoordsForRole: 256 x 256, 67 px cells; -- GetBackgroundTexCoordsForRole: 256 x 128, 75 px cells) local ROLES = { { key = "tank", client = "LFDQueueFrameRoleButtonTank", id = 2, x = 70, alpha = 0.6, icon = { 0, 0.26171875, 0.26171875, 0.5234375 }, background = { 0.29296875, 0.5859375, 0, 0.5859375 } }, { key = "healer", client = "LFDQueueFrameRoleButtonHealer", id = 3, x = 174, alpha = 0.4, icon = { 0.26171875, 0.5234375, 0, 0.26171875 }, background = { 0, 0.29296875, 0, 0.5859375 } }, { key = "damage", client = "LFDQueueFrameRoleButtonDPS", id = 1, x = 278, alpha = 0.6, icon = { 0.26171875, 0.5234375, 0.26171875, 0.5234375 }, background = { 0.5859375, 0.87890625, 0, 0.5859375 } }, -- camelot's fourth slot (TOPRIGHT -20,-41) holds WotLK's leader role, without background { key = "leader", client = "LFDQueueFrameRoleButtonLeader", id = 4, right = -20, icon = { 0, 0.26171875, 0, 0.26171875 } }, } local ROLE_VEIL = { 0, 0.2617, 0.5234, 0.7851 } -- colors of the fontstyles.xml fonts (LFGActivityHeader, LFGActivityEntry...) local COLORS = { header = { 0.7, 0.7, 0.7 }, entry = { 1.0, 0.82, 0 }, easy = { 0.5, 0.5, 0.5 }, hard = { 1.0, 0.5, 0.25 }, } -- category button image local ART = { random = "groupfinder-button-dungeons", holiday = "groupfinder-button-questing", specific = "groupfinder-button-custom-pve", } local function txt(key) return _G[key] or key end -- 3.3.5 returns 1 or nil (the test bench 0 or 1), so compare to 1 local function active(b) return b ~= nil and b.IsEnabled ~= nil and b:IsEnabled() == 1 end -- truly visible: the frame and its parents up to LFDParentFrame are shown local function display(c) while c do if not c:IsShown() then return false end if c == LFDParentFrame then return true end c = c:GetParent() end return false end -- ------------------------------------------------------------------ Frame 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 }, } local function redCloseButton(b) for _, state in ipairs({ { "SetNormalTexture", "GetNormalTexture", "redbutton-exit" }, { "SetPushedTexture", "GetPushedTexture", "redbutton-exit-pressed" }, { "SetHighlightTexture", "GetHighlightTexture", "redbutton-highlight" }, }) 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) if state[3] == "redbutton-highlight" then t:SetBlendMode("ADD") end end end end 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") -- portrait: camelot's eye (SetPortraitAtlasRaw, 62 x 62) local portraitFrame = CreateFrame("Frame", nil, f) portraitFrame:SetAllPoints(f) portraitFrame:SetFrameLevel(f:GetFrameLevel() + 19) local portrait = portraitFrame:CreateTexture(nil, "OVERLAY") ForeverUI.SetAtlas(portrait, "groupfinder-eye-frame", true) portrait:SetWidth(G.portraitSide) portrait:SetHeight(G.portraitSide) portrait:SetPoint("TOPLEFT", f, "TOPLEFT", G.portraitX, G.portraitY) 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.title:SetText(txt("LFG_TITLE")) f.banner = banner -- The close button hides the client panel that holds the window, like the client's own. local closeButton = CreateFrame("Button", "ForeverUIGroupFinderCloseButton", f) closeButton:SetWidth(G.closeButton) closeButton:SetHeight(G.closeButton) closeButton:SetFrameLevel(f:GetFrameLevel() + 22) closeButton:SetPoint("TOPRIGHT", f, "TOPRIGHT", G.closeButtonX, G.closeButtonY) redCloseButton(closeButton) closeButton:SetScript("OnClick", function() HideUIPanel(F.frame:GetParent()) end) f.closeButton = closeButton end -- ------------------------------------------------------------ Tabs -- LargeSideTabButtonTemplate, as on the character sheet (CharacterFrame.lua): common-sidetab -- background, icon baked by tools/bake_masks.py (50 x 50 at (-3, 0), cropped by 0.03125), -- common-sidetab-selected marker, common-sidetab-hover highlight. local ICONS = "Interface" .. SEP .. "ForeverUI" .. SEP .. "tabicons" .. SEP local TABS = { { key = "dungeons", text = "LOOKING_FOR_DUNGEON", icon = ICONS .. "inv_helmet_08" }, -- a single tab for the raid browser { key = "raid", text = "LOOKING_FOR_RAID", raid = true, icon = ICONS .. "achievement_general_stayclassy" }, } local function createTab(f, def, n) local o = CreateFrame("Button", "ForeverUIGroupFinderTab" .. n, f) o:SetWidth(G.tab) o:SetHeight(G.tab) local background = o:CreateTexture(nil, "BACKGROUND") ForeverUI.SetAtlas(background, "common-sidetab", true) background:SetAllPoints(o) local icon = o:CreateTexture(nil, "ARTWORK") icon:SetWidth(G.tabIcon) icon:SetHeight(G.tabIcon) icon:SetPoint("CENTER", o, "CENTER", G.tabIconX, 0) icon:SetTexCoord(G.crop, 1 - G.crop, G.crop, 1 - G.crop) icon:SetTexture(def.icon) o.icon = icon local selected = o:CreateTexture(nil, "OVERLAY") ForeverUI.SetAtlas(selected, "common-sidetab-selected", true) selected:SetAllPoints(o) selected:Hide() o.selected = selected local hover = o:CreateTexture(nil, "HIGHLIGHT") ForeverUI.SetAtlas(hover, "common-sidetab-hover", true) hover:SetAllPoints(o) o:SetScript("OnEnter", function(self) GameTooltip:SetOwner(self, "ANCHOR_RIGHT", -4, -4) GameTooltip:SetText(txt(def.text)) if def.detail then GameTooltip:AddLine(txt(def.detail), 1, 1, 1) end GameTooltip:Show() end) o:SetScript("OnLeave", function() GameTooltip:Hide() end) o:SetScript("OnClick", function() PlaySound("igCharacterInfoTab") F.goTo(def.key) end) o.key = def.key return o end function F.selectTab(key) F.tab = key for _, o in ipairs(F.tabs or {}) do if o.key == key then o.selected:Show() else o.selected:Hide() end end end -- ------------------------------------------------------------ Roles -- Role button with its checkbox; r: a ROLES entry (r.page: optional name suffix). local function createRole(parent, r) local b = CreateFrame("Button", "ForeverUIGroupFinderRole" .. (r.page or "") .. r.key, parent) b:SetWidth(G.role) b:SetHeight(G.role) if r.right then b:SetPoint("TOPRIGHT", F.frame, "TOPRIGHT", r.right, G.roleY) else b:SetPoint("TOPLEFT", F.frame, "TOPLEFT", r.x, G.roleY) end if r.background then local background = b:CreateTexture(nil, "BACKGROUND") background:SetTexture(G.roleBackgrounds) background:SetTexCoord(r.background[1], r.background[2], r.background[3], r.background[4]) background:SetWidth(G.roleBackground) background:SetHeight(G.roleBackground) background:SetPoint("CENTER", b, "CENTER", 0, 0) background:SetAlpha(r.alpha) b.background = background end local icon = b:CreateTexture(nil, "ARTWORK") icon:SetTexture(G.roleIcons) icon:SetTexCoord(r.icon[1], r.icon[2], r.icon[3], r.icon[4]) icon:SetAllPoints(b) b.icon = icon local veil = b:CreateTexture(nil, "OVERLAY") veil:SetTexture(G.roleIcons) veil:SetTexCoord(ROLE_VEIL[1], ROLE_VEIL[2], ROLE_VEIL[3], ROLE_VEIL[4]) veil:SetAllPoints(b) veil:SetAlpha(0.5) veil:Hide() b.veil = veil local c = CreateFrame("CheckButton", "ForeverUIGroupFinderRole" .. (r.page or "") .. r.key .. "Check", b) c:SetWidth(G.roleCheckbox) c:SetHeight(G.roleCheckbox) c:SetPoint("BOTTOMLEFT", b, "BOTTOMLEFT", G.roleCheckboxX, G.roleCheckboxY) c:SetNormalTexture(BUTTONS .. "UI-CheckBox-Up") c:SetPushedTexture(BUTTONS .. "UI-CheckBox-Down") c:SetHighlightTexture(BUTTONS .. "UI-CheckBox-Highlight") local s = c:GetHighlightTexture() if s then s:SetBlendMode("ADD") end c:SetCheckedTexture(BUTTONS .. "UI-CheckBox-Check") c:SetDisabledCheckedTexture(BUTTONS .. "UI-CheckBox-Check-Disabled") b.checkbox = c -- The client checkbox does the work: its Click toggles it and runs its OnClick -- (sound, LFDQueueFrame_SetRoles). Ours then follows. c:SetScript("OnClick", function(self) local client = _G[r.client] self:SetChecked(not self:GetChecked()) if client and client.checkButton and active(client.checkButton) then client.checkButton:Click() end F.updateRoles() end) b:SetScript("OnClick", function() if active(c) then c:Click() end end) b:SetScript("OnEnter", function(self) local client = _G[r.client] GameTooltip:SetOwner(self, "ANCHOR_RIGHT") if r.key == "leader" then GameTooltip:SetText(txt("GUIDE_TOOLTIP"), nil, nil, nil, nil, 1) else GameTooltip:SetText(txt("ROLE_DESCRIPTION" .. r.id), nil, nil, nil, nil, 1) if client and client.permDisabled then local red = RED_FONT_COLOR or { r = 1, g = 0.1, b = 0.1 } GameTooltip:AddLine(txt("YOUR_CLASS_MAY_NOT_PERFORM_ROLE"), red.r, red.g, red.b, 1) end end GameTooltip:Show() if active(c) then c:LockHighlight() end end) b:SetScript("OnLeave", function() GameTooltip:Hide() c:UnlockHighlight() end) b.def = r return b end F.createRole = createRole -- Copies the state of each client role button to ours (on every page). function F.updateRoles() for _, b in ipairs(F.allRoles or {}) do local client = _G[b.def.client] if client and client.checkButton then b.checkbox:SetChecked(client.checkButton:GetChecked()) if client.checkButton:IsShown() then b.checkbox:Show() else b.checkbox:Hide() end if active(client.checkButton) then b.checkbox:Enable() else b.checkbox:Disable() end if client.cover and client.cover:IsShown() then b.veil:SetAlpha(client.cover:GetAlpha()) b.veil:Show() else b.veil:Hide() end if b.background then if client.background and client.background:IsShown() then b.background:Show() else b.background:Hide() end end b.icon:SetDesaturated(client.permDisabled and true or false) end end end -- ------------------------------------------------------------ Categories -- isRandomDungeonDisplayable from LFDFrame.lua (local there) local function isRandomEligible(id) local _, _, minimum, maximum, _, _, _, extension = GetLFGDungeonInfo(id) local level = UnitLevel("player") local ext = (GetExpansionLevel and GetExpansionLevel()) or 2 return minimum and level >= minimum and level <= maximum and ext >= (extension or 0) end -- LFDQueueFrameTypeDropDown_Initialize: "specific" first, then each displayable random -- dungeon, joinable or not function F.categories() local l = { { value = "specific", text = txt("SPECIFIC_DUNGEONS"), art = ART.specific, available = true } } for i = 1, (GetNumRandomDungeons and GetNumRandomDungeons() or 0) do local id, name = GetLFGRandomDungeonInfo(i) if id and isRandomEligible(id) then local holiday = select(14, GetLFGDungeonInfo(id)) table.insert(l, { value = id, text = name, art = holiday and ART.holiday or ART.random, available = IsLFGDungeonJoinable(id) and true or false }) end end return l end local function createCategory(row) local b = CreateFrame("Button", nil, row) b:SetWidth(G.catW) b:SetHeight(G.catH) b:SetPoint("TOP", row, "TOP", 0, 0) local image = b:CreateTexture(nil, "BACKGROUND") image:SetPoint("TOPLEFT", b, "TOPLEFT", 5, G.catImageTop) image:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", -5, G.catImageBottom) b.image = image local coverTexture = b:CreateTexture(nil, "ARTWORK") ForeverUI.SetAtlas(coverTexture, "groupfinder-button-cover", true) coverTexture:SetAllPoints(b) local choice = b:CreateTexture(nil, "OVERLAY") choice:SetTexture(G.mega) choice:SetTexCoord(0.00195313, 0.63867188, 0.76953125, 0.83007813) choice:SetBlendMode("ADD") choice:SetWidth(G.catSelW) choice:SetHeight(G.catSelH) choice:SetPoint("CENTER", b, "CENTER", 0, 0) choice:Hide() b.choice = choice b:SetHighlightTexture(G.mega) local s = b:GetHighlightTexture() if s then s:SetTexCoord(0.00195313, 0.63867188, 0.70703125, 0.76757813) s:SetBlendMode("ADD") s:ClearAllPoints() s:SetWidth(G.catSelW) s:SetHeight(G.catSelH) s:SetPoint("CENTER", b, "CENTER", 0, 0) end local text = b:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") text:SetPoint("LEFT", b, "LEFT", 20, 0) text:SetJustifyH("LEFT") b.text = text b:SetScript("OnClick", function(self) local c = self.category if not c or not c.available then return end PlaySound("igMainMenuOptionCheckBoxOn") LFDQueueFrame_SetType(c.value) F.view = (c.value == "specific") and "list" or "categories" F.update() end) b:SetScript("OnEnter", function(self) local c = self.category if c and not c.available then GameTooltip:SetOwner(self, "ANCHOR_RIGHT") GameTooltip:SetText(txt("YOU_MAY_NOT_QUEUE_FOR_THIS"), 1, 1, 1) local reason = LFDConstructDeclinedMessage and LFDConstructDeclinedMessage(c.value) if reason then GameTooltip:AddLine(reason, nil, nil, nil, 1) end GameTooltip:Show() end end) b:SetScript("OnLeave", function() GameTooltip:Hide() end) row.button = b end local function fillCategory(row, index) local c = F.category_list[index] local b = row.button b.category = c ForeverUI.SetAtlas(b.image, c.art, true) b.image:SetDesaturated(not c.available) b.text:SetText(c.text) if c.available then b.text:SetTextColor(1, 0.82, 0) else b.text:SetTextColor(0.5, 0.5, 0.5) end if LFDQueueFrame.type == c.value then b.choice:Show() else b.choice:Hide() end end -- ------------------------------------------------------------ List -- Dungeon and raid rows are the same; what differs (the list, the client functions, the -- radio buttons of a raid in a group) comes from the list source (zone.src). -- LFGSpecificChoiceEnableButton_SetIsRadio (LFGFrame.lua): in a group a raid is chosen with a -- radio button (UI-RadioButton in four quarters, 17 x 17); solo, with a checkbox. -- Sources with style "camelot" use checkbox-minimal / checkmark-minimal, as on the character -- sheet and Social (Social.skinCell), and common-radiobutton-circle / -dot in a group. -- 20 x 20 in a 22-high row (camelot scales its 30 x 29 box by 0.7); the radio is 16. local CAMELOT_CHECKBOX, CAMELOT_RADIO = 20, 16 local function skinCamelotCheckbox(c, radio) if not radio then ForeverUI.Social.skinCell(c) return end local function place(set, get, atlas, grayed) local e = ForeverUI.AtlasEntry(atlas) c[set](c, e and e[1] or "") local t = c[get](c) if t then ForeverUI.SetAtlas(t, atlas, true) t:ClearAllPoints() t:SetWidth(CAMELOT_RADIO) t:SetHeight(CAMELOT_RADIO) t:SetPoint("CENTER", c, "CENTER", 0, 0) if grayed then t:SetDesaturated(true) end end end place("SetNormalTexture", "GetNormalTexture", "common-radiobutton-circle") place("SetPushedTexture", "GetPushedTexture", "common-radiobutton-circle") place("SetCheckedTexture", "GetCheckedTexture", "common-radiobutton-dot") place("SetDisabledCheckedTexture", "GetDisabledCheckedTexture", "common-radiobutton-dot", true) c:SetHighlightTexture("") end -- Switches checkbox c between checkbox and radio art; radio: true for a raid in a group. local function setRadioStyle(c, radio) if c.radio == radio then return end c.radio = radio if c.style == "camelot" then skinCamelotCheckbox(c, radio) return end local states = { { "SetNormalTexture", "GetNormalTexture", "UI-CheckBox-Up", 0, 0.25 }, { "SetPushedTexture", "GetPushedTexture", "UI-CheckBox-Down", 0, 0.25 }, { "SetHighlightTexture", "GetHighlightTexture", "UI-CheckBox-Highlight", 0.5, 0.75 }, { "SetCheckedTexture", "GetCheckedTexture", "UI-CheckBox-Check", 0.25, 0.5 }, } for _, e in ipairs(states) do c[e[1]](c, BUTTONS .. (radio and "UI-RadioButton" or e[3])) local t = c[e[2]](c) if t then t:ClearAllPoints() if radio then t:SetTexCoord(e[4], e[5], 0, 1) t:SetWidth(17) t:SetHeight(17) t:SetPoint("CENTER", c, "CENTER", 0, 0) else t:SetTexCoord(0, 1, 0, 1) t:SetAllPoints(c) end if e[1] == "SetHighlightTexture" then t:SetBlendMode("ADD") end end end end -- dungeon source (LFDFrame.lua): camelot checkboxes, and headers keep WotLK's -- "check all" box local SOURCE_LFD = { style = "camelot", headerCheckbox = true, collapse = function(id, collapsed) LFDList_SetHeaderCollapsed(id, collapsed) end, list = function() return LFDDungeonList end, empowered = function() return LFD_IsEmpowered() end, plus = function(b) LFDQueueFrameExpandOrCollapseButton_OnClick(b) end, checkbox = function(c) LFDQueueFrameDungeonChoiceEnableButton_OnClick(c) end, multiple = function() return true end, lock = function(id) return LFGLockList[id] end, state = function(id, mode) if mode == "queued" or mode == "listed" then return LFGQueuedForList[id] end return LFGEnabledList[id] end, } F.SOURCE_LFD = SOURCE_LFD local function createRow(row) row.src = row:GetParent().src local plus = CreateFrame("Button", nil, row) plus:SetWidth(G.plus) plus:SetHeight(G.plus) plus:SetPoint("LEFT", row, "LEFT", 0, 0) plus:SetHitRectInsets(1, -4, -2, -2) plus:SetNormalTexture(BUTTONS .. "UI-MinusButton-UP") local n = plus:GetNormalTexture() if n then n:ClearAllPoints() n:SetWidth(20) n:SetHeight(20) n:SetPoint("LEFT", plus, "LEFT", 3, 0) end plus:SetHighlightTexture(BUTTONS .. "UI-PlusButton-Hilight") local s = plus:GetHighlightTexture() if s then s:SetBlendMode("ADD") s:ClearAllPoints() s:SetWidth(20) s:SetHeight(20) s:SetPoint("LEFT", plus, "LEFT", 3, 0) end -- client +/-: LFDQueueFrameExpandOrCollapseButton_OnClick reads -- self:GetParent().id and .isCollapsed plus:SetScript("OnClick", function(self) PlaySound("igMainMenuOptionCheckBoxOn") row.src.plus(self) end) row.plus = plus local c = CreateFrame("CheckButton", nil, row) c.style = row.src.style local side = (c.style == "camelot") and CAMELOT_CHECKBOX or G.checkbox c:SetWidth(side) c:SetHeight(side) c:SetPoint("LEFT", plus, "RIGHT", 4, 0) c:SetNormalTexture(BUTTONS .. "UI-CheckBox-Up") c:SetPushedTexture(BUTTONS .. "UI-CheckBox-Down") c:SetHighlightTexture(BUTTONS .. "UI-CheckBox-Highlight") local h = c:GetHighlightTexture() if h then h:SetBlendMode("ADD") end c:SetCheckedTexture(BUTTONS .. "UI-CheckBox-Check") c:SetDisabledCheckedTexture(BUTTONS .. "UI-CheckBox-Check-Disabled") -- client handler: LFDQueueFrameDungeonChoiceEnableButton_OnClick reads -- self:GetParent().id and self:GetChecked() c:SetScript("OnClick", function(self) row.src.checkbox(self) end) row.checkbox = c -- WotLK lock, in place of the checkbox local lock = row:CreateTexture(nil, "ARTWORK") lock:SetTexture(LFG .. "UI-LFG-ICON-LOCK") lock:SetTexCoord(0, 0.71875, 0, 0.875) lock:SetWidth(12) lock:SetHeight(14) lock:SetPoint("CENTER", c, "CENTER", 0, 0) lock:Hide() row.lockedIndicator = lock local level = row:CreateFontString(nil, "ARTWORK", "GameFontNormal") level:SetWidth(G.levelL) level:SetHeight(G.row) level:SetPoint("RIGHT", row, "RIGHT", 0, 0) level:SetJustifyH("LEFT") row.level = level local name = row:CreateFontString(nil, "ARTWORK", "GameFontNormal") name:SetHeight(G.row) name:SetPoint("LEFT", c, "RIGHT", 0, 0) name:SetPoint("RIGHT", level, "LEFT", -4, 0) name:SetJustifyH("LEFT") row.name = name -- the name clicks and highlights the checkbox (camelot's NameButton); -- the lock tooltip is the client's row:EnableMouse(true) row:SetScript("OnEnter", function(self) if active(self.checkbox) and self.checkbox:IsShown() then self.checkbox:LockHighlight() end LFDQueueFrameDungeonListButton_OnEnter(self) end) row:SetScript("OnLeave", function(self) self.checkbox:UnlockHighlight() GameTooltip:Hide() end) row:SetScript("OnClick", function(self) if self.checkbox:IsShown() and active(self.checkbox) then self.checkbox:Click() end end) end local function color(fs, c) fs:SetTextColor(c[1], c[2], c[3]) end -- LFDQueueFrameSpecificListButton_SetDungeon (and the raid one), camelot style local function populateRow(row, index) local src = row.src local id = src.list()[index] local info = LFGDungeonInfo and LFGDungeonInfo[id] or {} local mode = GetLFGMode() local locked = mode == "rolecheck" or mode == "queued" or mode == "listed" or not src.empowered() row.id = id row.name:SetText(info[1] or "") if id < 0 then row.plus:Show() row.isCollapsed = LFGCollapseList[id] row.plus:SetNormalTexture(BUTTONS .. (row.isCollapsed and "UI-PlusButton-UP" or "UI-MinusButton-UP")) local n = row.plus:GetNormalTexture() if n then n:ClearAllPoints() n:SetWidth(20) n:SetHeight(20) n:SetPoint("LEFT", row.plus, "LEFT", 3, 0) end row.level:Hide() color(row.name, COLORS.header) else row.plus:Hide() row.isCollapsed = false local minimum, maximum = info[3] or 0, info[4] or 0 if minimum == maximum then row.level:SetText(format(txt("LFD_LEVEL_FORMAT_SINGLE"), minimum)) else row.level:SetText(format(txt("LFD_LEVEL_FORMAT_RANGE"), minimum, maximum)) end row.level:Show() local player = UnitLevel("player") local c = COLORS.entry if maximum ~= 0 and maximum < player then c = COLORS.easy elseif minimum ~= 0 and minimum > player then c = COLORS.hard end color(row.level, c) color(row.name, locked and COLORS.header or c) end local multiple = src.multiple() if src.lock(id) then row.checkbox:Hide() row.lockedIndicator:Show() else -- in a group, a raid header has no button if multiple or id >= 0 then row.checkbox:Show() else row.checkbox:Hide() end row.lockedIndicator:Hide() end setRadioStyle(row.checkbox, not multiple) local state = src.state(id, mode) if multiple and row.checkbox.style == "camelot" then row.checkbox:SetChecked(state and state ~= 0) elseif multiple then if state == 1 then row.checkbox:SetCheckedTexture(BUTTONS .. "UI-MultiCheck-Up") row.checkbox:SetDisabledCheckedTexture(BUTTONS .. "UI-MultiCheck-Disabled") else row.checkbox:SetCheckedTexture(BUTTONS .. "UI-CheckBox-Check") row.checkbox:SetDisabledCheckedTexture(BUTTONS .. "UI-CheckBox-Check-Disabled") end row.checkbox:SetChecked(state and state ~= 0) else row.checkbox:SetChecked(state) end if locked then row.checkbox:Disable() else row.checkbox:Enable() end end F.createRow = createRow F.populateRow = populateRow -- ------------------------------------------------------------ Category list -- Category list, shared by the raid browser (GroupFinderRaid.lua) and the specific dungeons. -- Headers follow the character sheet (TokensTab.lua, TokenHeaderTemplate): nine-sliced -- common-button-list-collapseExpand plate, minus / plus arrow on the right, plate in ADD at -- 0.3 on hover. Entries use camelot checkboxes and the sheet's hover rectangle -- (charactercreate-customize-dropdown-linemouseover, right side mirrored). -- Clicking a header collapses or expands it (src.collapse). Rows have two heights, so they -- are stacked by hand and the scroll offset counts rows. -- With src.headerCheckbox (dungeons), the header keeps WotLK's "check all" box before the -- name; partly checked (LFGEnabledList at 1) shows a gray check mark, like the AddOns list. -- A locked header shows WotLK's lock and its tooltip instead. local LC = { header = 26, entry = 22, gap = 3, indent = 2, margin = 4, corner = 12, nameX = 10, nameH = 15, arrowX = -8, arrowY = -1, arrowSpace = 16, checkboxGap = 2, side = 6, selectedItem = 0.20, hover = 0.10, plate = "common-button-list-collapseexpand", hoverSide = "charactercreate-customize-dropdown-linemouseover-side", hoverMiddle = "charactercreate-customize-dropdown-linemouseover-middle", } F.LC = LC local function createCatHeader(zone, n) local b = CreateFrame("Button", zone.prefix .. "Header" .. n, zone) b:SetHeight(LC.header) ForeverUI.CreateNineSlice(b, LC.plate, LC.corner, { 0, 0, 0, 0 }, "BACKGROUND") for _, t in ipairs(ForeverUI.CreateNineSlice(b, LC.plate, LC.corner, { 0, 0, 0, 0 }, "HIGHLIGHT") or {}) do t:SetBlendMode("ADD") t:SetAlpha(0.3) end local name = b:CreateFontString(nil, "OVERLAY", "GameFontNormalLeft") name:SetHeight(LC.nameH) name:SetJustifyH("LEFT") name:SetPoint("RIGHT", b, "RIGHT", -LC.arrowSpace, 0) b.name = name local arrow = b:CreateTexture(nil, "OVERLAY") arrow:SetPoint("RIGHT", b, "RIGHT", LC.arrowX, LC.arrowY) b.arrow = arrow if zone.src.headerCheckbox then -- client handler: LFDQueueFrameDungeonChoiceEnableButton_OnClick reads -- self:GetParent().id and self:GetChecked() local c = CreateFrame("CheckButton", nil, b) c.style = "camelot" c:SetWidth(CAMELOT_CHECKBOX) c:SetHeight(CAMELOT_CHECKBOX) c:SetPoint("LEFT", b, "LEFT", LC.nameX, 0) skinCamelotCheckbox(c, false) c:SetScript("OnClick", function(self) zone.src.checkbox(self) end) b.checkbox = c local lock = b:CreateTexture(nil, "OVERLAY") lock:SetTexture(LFG .. "UI-LFG-ICON-LOCK") lock:SetTexCoord(0, 0.71875, 0, 0.875) lock:SetWidth(12) lock:SetHeight(14) lock:SetPoint("CENTER", c, "CENTER", 0, 0) lock:Hide() b.lockedIndicator = lock name:SetPoint("LEFT", c, "RIGHT", LC.checkboxGap, 0) b:SetScript("OnEnter", function(self) LFDQueueFrameDungeonListButton_OnEnter(self) end) b:SetScript("OnLeave", function() GameTooltip:Hide() end) else name:SetPoint("LEFT", b, "LEFT", LC.nameX, 0) end b:SetScript("OnClick", function(self) PlaySound("igMainMenuOptionCheckBoxOn") zone.src.collapse(self.id, not LFGCollapseList[self.id]) F.request() end) return b end local function fillCatHeader(b, id, src) local info = LFGDungeonInfo and LFGDungeonInfo[id] or {} b.id = id b.name:SetText(info[1] or "") ForeverUI.SetAtlas(b.arrow, LFGCollapseList[id] and "common-button-list-plus" or "common-button-list-minus", false) local c = b.checkbox if not c then return end local mode = GetLFGMode() local locked = mode == "rolecheck" or mode == "queued" or mode == "listed" or not src.empowered() if src.lock(id) then c:Hide() b.lockedIndicator:Show() else c:Show() b.lockedIndicator:Hide() end local state = src.state(id, mode) c:SetChecked(state and state ~= 0) local checkMark = c:GetCheckedTexture() if checkMark then checkMark:SetDesaturated(state == 1) end if locked then c:Disable() else c:Enable() end end local function createCatEntry(zone, n) local l = CreateFrame("Button", zone.prefix .. "Row" .. n, zone) l:SetHeight(LC.entry) -- the sheet's hover rectangle, under the row local hover = CreateFrame("Frame", nil, l) hover:SetAllPoints(l) hover:SetAlpha(0) local g = hover:CreateTexture(nil, "BACKGROUND") ForeverUI.SetAtlas(g, LC.hoverSide, true) g:SetWidth(LC.side) g:SetPoint("TOPLEFT", hover, "TOPLEFT", 0, 0) g:SetPoint("BOTTOMLEFT", hover, "BOTTOMLEFT", 0, 0) local d = hover:CreateTexture(nil, "BACKGROUND") if ForeverUI.SetAtlas(d, LC.hoverSide, true) then local e = ForeverUI.AtlasEntry(LC.hoverSide) d:SetTexCoord(e[3], e[2], e[4], e[5]) end d:SetWidth(LC.side) d:SetPoint("TOPRIGHT", hover, "TOPRIGHT", 0, 0) d:SetPoint("BOTTOMRIGHT", hover, "BOTTOMRIGHT", 0, 0) local m = hover:CreateTexture(nil, "BACKGROUND") ForeverUI.SetAtlas(m, LC.hoverMiddle, true) m:SetPoint("TOPLEFT", g, "TOPRIGHT", 0, 0) m:SetPoint("BOTTOMRIGHT", d, "BOTTOMLEFT", 0, 0) l.hover = hover createRow(l) return l end -- rectangle opacity: 0.20 checked, 0.10 hovered, 0 otherwise local function placeCatHover(l) local checkMark = l.checkbox:IsShown() and l.checkbox:GetChecked() l.hover:SetAlpha((checkMark and LC.selectedItem) or (l:IsMouseOver() and LC.hover) or 0) end -- number of rows that fit starting at fromIndex; top: available height local function countFittingCats(l, fromIndex, top) local y, n = LC.margin, 0 for i = fromIndex, #l do local h = (l[i] < 0) and LC.header or LC.entry if y + h > top then break end y = y + h + LC.gap n = n + 1 end return n end -- zone: from the top of the list (G.listY1, margin included) to zone.down above the window -- bottom; the scroll bar takes its room on the right (G.barRight), else it goes to the margin local function placeCatZone(zone) local f = F.frame zone:ClearAllPoints() zone:SetPoint("TOPLEFT", f, "TOPLEFT", G.listX1 + G.margin, G.listY1 - G.margin) zone:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", zone.hasBar and (G.listX2 + G.barRight - G.margin) or (G.listX2 - G.margin), zone.down) end function F.updateCategoryList(zone) local l = zone.src.list() or {} local total = #l local top = G.height + (G.listY1 - G.margin) - zone.down -- the maximum offset: the smallest one at which the end of the list fits local maxValue = 0 for d = 0, total do if countFittingCats(l, d + 1, top) >= total - d then maxValue = d break end end zone.maxValue = maxValue zone.offset = math.max(0, math.min(zone.offset or 0, maxValue)) local hasBar = maxValue > 0 if hasBar ~= zone.hasBar then zone.hasBar = hasBar placeCatZone(zone) end local y = LC.margin local ne, nn = 0, 0 for i = zone.offset + 1, total do local id = l[i] local h = (id < 0) and LC.header or LC.entry if y + h > top then break end local row if id < 0 then ne = ne + 1 row = zone.headers[ne] or createCatHeader(zone, ne) zone.headers[ne] = row fillCatHeader(row, id, zone.src) row:ClearAllPoints() row:SetPoint("TOPLEFT", zone, "TOPLEFT", 0, -y) row:SetPoint("TOPRIGHT", zone, "TOPRIGHT", 0, -y) else nn = nn + 1 row = zone.entries[nn] or createCatEntry(zone, nn) zone.entries[nn] = row populateRow(row, i) placeCatHover(row) row:ClearAllPoints() row:SetPoint("TOPLEFT", zone, "TOPLEFT", LC.indent, -y) row:SetPoint("TOPRIGHT", zone, "TOPRIGHT", 0, -y) end row:Show() y = y + h + LC.gap end for i = ne + 1, #zone.headers do zone.headers[i]:Hide() end for i = nn + 1, #zone.entries do zone.entries[i]:Hide() end zone.bar:Configure(maxValue + 1, 1, zone.offset) end -- Category list in page p. name: zone frame name; prefix: row name prefix; src: list source; -- down: zone bottom above the window bottom; frameBox: the inset it sits on function F.createCatList(p, name, prefix, src, down, frameBox) local zone = CreateFrame("Frame", name, p) zone.src = src zone.prefix = prefix zone.headers, zone.entries = {}, {} zone.offset = 0 zone.down = down zone:SetFrameLevel(frameBox:GetFrameLevel() + 1) zone.hasBar = false placeCatZone(zone) local bar = ForeverUI.CreateScrollBar(name .. "ScrollBar", p, zone) bar:SetFrameLevel(frameBox:GetFrameLevel() + 2) bar:ClearAllPoints() bar:SetPoint("TOPLEFT", zone, "TOPRIGHT", 13 + G.margin, 0) bar:SetPoint("BOTTOMLEFT", zone, "BOTTOMRIGHT", 13 + G.margin, -2) bar.onScroll = function(new) zone.offset = new F.updateCategoryList(zone) end zone.bar = bar zone:EnableMouseWheel(true) zone:SetScript("OnMouseWheel", function(self, direction) self.offset = math.max(0, math.min((self.offset or 0) - direction, self.maxValue or 0)) F.updateCategoryList(self) end) -- hover tracked every frame, as on the character sheet (trackHover) zone:SetScript("OnUpdate", function(self) for _, l in ipairs(self.entries) do if l:IsShown() then placeCatHover(l) end end end) return zone end -- ------------------------------------------------------------ Rewards -- Reward item button; i: reward index for the tooltip and link (0 for money). local function createItem(parent, i) local b = CreateFrame("Button", "ForeverUIGroupFinderReward" .. i, parent) b:SetWidth(147) b:SetHeight(41) local icon = b:CreateTexture(nil, "BORDER") icon:SetWidth(39) icon:SetHeight(39) icon:SetPoint("TOPLEFT", b, "TOPLEFT", 0, 0) b.icon = icon local nameFrame = b:CreateTexture(nil, "BACKGROUND") nameFrame:SetTexture("Interface" .. SEP .. "QuestFrame" .. SEP .. "UI-QuestItemNameFrame") nameFrame:SetWidth(128) nameFrame:SetHeight(64) nameFrame:SetPoint("LEFT", icon, "RIGHT", -10, 0) local name = b:CreateFontString(nil, "ARTWORK", "GameFontHighlight") name:SetWidth(90) name:SetHeight(36) name:SetPoint("LEFT", icon, "RIGHT", 8, 0) name:SetJustifyH("LEFT") b.name = name local count = b:CreateFontString(nil, "OVERLAY", "NumberFontNormal") count:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", -5, 2) b.count = count b:SetID(i) -- LFDRandomDungeonLootTemplate b:SetScript("OnEnter", function(self) GameTooltip:SetOwner(self, "ANCHOR_RIGHT") GameTooltip:SetLFGDungeonReward(LFDQueueFrame.type, self:GetID()) GameTooltip:Show() end) b:SetScript("OnLeave", function() GameTooltip:Hide() end) b:SetScript("OnClick", function(self) HandleModifiedItemClick(GetLFGDungeonRewardLink(LFDQueueFrame.type, self:GetID())) end) return b end -- Details: a scroll area under the rule. Each piece is placed at its height from the top of -- the content (not from the previous piece), which gives the total height for the bar. local function buildRewards(p) local view = CreateFrame("ScrollFrame", "ForeverUIGroupFinderDetails", p) view.offset = 0 local r = CreateFrame("Frame", "ForeverUIGroupFinderRewards", view) r:SetWidth(G.width + G.detailsX2 - G.detailsX1) r:SetHeight(1) view:SetScrollChild(r) local width = G.width + G.detailsX2 - G.detailsX1 local function text(font) local fs = r:CreateFontString(nil, "ARTWORK", font) fs:SetWidth(width) fs:SetJustifyH("LEFT") return fs end r.description = text("GameFontHighlight") r.tag = text("GameFontNormalLarge") r.tag:SetText(txt("LFD_REWARDS")) r.explanation = text("GameFontHighlight") r.pug = text("GameFontHighlight") -- money: an item cell, without tooltip or link local money = createItem(r, 0) money:SetScript("OnEnter", nil) money:SetScript("OnLeave", nil) money:SetScript("OnClick", nil) money.icon:SetTexture(G.pieces) money.count:Hide() r.money = money r.xp = text("GameFontNormal") r.objects = {} F.rewards = r local bar = ForeverUI.CreateScrollBar("ForeverUIGroupFinderDetailsScrollBar", p, view) bar.onScroll = function(new) view.offset = new view:SetVerticalScroll(math.min(new * G.scrollStep, math.max(0, (view.content or 0) - (view.visible or 0)))) end view:EnableMouseWheel(true) view:SetScript("OnMouseWheel", function(self, direction) bar:MoveTo(self.offset - direction) end) view.bar = bar F.details = view -- Second measure, on the next frame: from the content top to the bottom of its last piece. -- Wrapped text can measure short at first; if taller, everything is laid out again. local remeasure = CreateFrame("Frame") remeasure:Hide() remeasure:SetScript("OnUpdate", function(self) self:Hide() local piece = F.lastPiece if not (view:IsShown() and piece and piece:IsShown()) then return end local top, down = r:GetTop(), piece:GetBottom() if not (top and down) then return end local actual = top - down if actual > (view.content or 0) + 0.5 then F.layoutContent(actual) end end) F.remeasure = remeasure end -- Rule and details. The details end at the bottom of the frame box; the rule sits just -- above their content, never higher than F.ruleMax (6 below the last button). Without -- content it stays there. local function placeRule(content) local y = F.ruleMax or 0 if content and content > 0 then local wanted = -(G.height - G.detailsBottom) + content + G.detailsPadding + G.detailsGap + G.ruleH / 2 if wanted < y then y = wanted end end -- visible height of the details, from our own gaps: GetHeight right after re-anchoring -- can still return the old height F.details.visible = (y - G.ruleH / 2 - G.detailsGap) + (G.height - G.detailsBottom) F.rule:ClearAllPoints() F.rule:SetPoint("LEFT", F.frame, "TOPLEFT", G.listX1, y) F.rule:SetPoint("RIGHT", F.frame, "TOPRIGHT", G.listX2, y) F.details:ClearAllPoints() F.details:SetPoint("TOPLEFT", F.frame, "TOPLEFT", G.detailsX1, y - G.ruleH / 2 - G.detailsGap) F.details:SetPoint("BOTTOMRIGHT", F.frame, "BOTTOMRIGHT", G.detailsX2, G.detailsBottom) -- the detail veils start at the rule F.detailsTop = y - G.ruleH / 2 - G.frameBoxY1 end -- height of a wrapped text: GetHeight, as in the client's QuestInfo.lua local function height(fs) local h = fs:GetHeight() or 0 if h <= 0 and fs.GetStringHeight then h = fs:GetStringHeight() or 0 end return h end -- LFDQueueFrameRandom_UpdateFrame has set the texts on the client regions; we copy them. -- The title is skipped: the chosen button already shows it. local function updateRewards() local r = F.rewards local view = F.details local client = LFDQueueFrameRandomScrollFrameChildFrame local id = LFDQueueFrame.type if not client or type(id) ~= "number" then placeRule(nil) view:Hide() view.bar:Hide() return end view:Show() local y = 0 local last local function place(piece, x, gap) piece:ClearAllPoints() piece:SetPoint("TOPLEFT", r, "TOPLEFT", x or 0, -(y + (gap or 0))) piece:Show() y = y + (gap or 0) + height(piece) last = piece end r.description:SetText(client.description:GetText() or "") place(r.description) if client.rewardsLabel:IsShown() then r.explanation:SetText(client.rewardsDescription:GetText() or "") place(r.tag, 0, 10) place(r.explanation, 0, 6) else r.tag:Hide() r.explanation:Hide() end local _, baseMoney, moneyVar, baseXp, xpVar, count = GetLFGDungeonRewards(id) count = count or 0 -- money and experience: the LFDQueueFrameRandom_UpdateFrame formula local missingMembers = 4 - GetNumPartyMembers() local money = (baseMoney or 0) + (moneyVar or 0) * missingMembers local xp = (baseXp or 0) + (xpVar or 0) * missingMembers local top = y + 8 local function checkbox(b, i) local rank, column = math.floor((i - 1) / 2), (i - 1) % 2 b:ClearAllPoints() b:SetPoint("TOPLEFT", r, "TOPLEFT", column * 157, -(top + rank * 44)) b:Show() y = top + rank * 44 + 41 last = b end for i = 1, count do local b = r.objects[i] or createItem(r, i) r.objects[i] = b local name, image, quantity = GetLFGDungeonRewardInfo(id, i) b.name:SetText(name or "") b.icon:SetTexture(image) if quantity and quantity > 1 then b.count:SetText(quantity) b.count:Show() else b.count:Hide() end checkbox(b, i) end for i = count + 1, #r.objects do r.objects[i]:Hide() end if money > 0 then r.money.name:SetText(GetCoinTextureString(money)) checkbox(r.money, count + 1) else r.money:Hide() end if client.pugDescription:IsShown() then r.pug:SetText(client.pugDescription:GetText() or "") place(r.pug, 0, 5) else r.pug:Hide() end if xp > 0 then r.xp:SetText(txt("EXPERIENCE_COLON") .. " |cffffffff" .. xp .. "|r") place(r.xp, 0, 8) else r.xp:Hide() end -- the rule moves down onto the content, then the scroll bar F.lastPiece = last F.layoutContent(y) -- second measure, once the client has set the texts F.remeasure:Show() end -- Sizes the details content to height y, places the rule and sets up the scroll bar. function F.layoutContent(y) local view = F.details F.rewards:SetHeight(math.max(1, y)) view.content = y placeRule(y) local visible = view.visible or 0 if visible <= 0 or y <= visible + G.tolerance then view.offset = 0 view:SetVerticalScroll(0) view.bar:Configure(0, 0, 0) else -- steps of 20: the last step shows the end of the content local total, visibleCount = math.ceil((y - visible) / G.scrollStep) + 1, 1 view.offset = math.min(view.offset, total - visibleCount) view.bar:Configure(total, visibleCount, view.offset) view.bar.onScroll(view.offset) end end -- ------------------------------------------------------------ Veils -- WotLK's veils (330 x 257, black at 0.93), over the frame box in the same order: cooldown -- (11), party backfill (14), "no LFD while in LFR" (16). -- WotLK-style veil. level: frame level above the frame box; page, frameBox: default to -- the dungeon page and its frame box local function createVeil(name, level, page, frameBox) local v = CreateFrame("Frame", name, page or F.dungeonsPage) v:SetFrameLevel((frameBox or F.frameBox):GetFrameLevel() + level) v:EnableMouse(true) local black = v:CreateTexture(nil, "BACKGROUND") black:SetTexture(0, 0, 0, 0.93) black:SetAllPoints(v) v.description = v:CreateFontString(nil, "ARTWORK", "GameFontNormal") v.description:SetWidth(300) v.description:SetPoint("TOP", v, "TOP", 0, -70) v:Hide() return v end F.createVeil = createVeil local function buildVeils() local pending = createVeil("ForeverUIGroupFinderCooldown", 11) pending.clock = pending:CreateFontString(nil, "ARTWORK", "GameFontHighlightLarge") pending.clock:SetPoint("TOP", pending.description, "BOTTOM", 0, -10) pending.names, pending.states = {}, {} for i = 1, 4 do local n = pending:CreateFontString(nil, "ARTWORK", "GameFontNormal") n:SetWidth(120) n:SetJustifyH("LEFT") if i == 1 then n:SetPoint("TOPLEFT", pending.description, "BOTTOMLEFT", 25, -60) else n:SetPoint("TOP", pending.names[i - 1], "BOTTOM", 0, -5) end local e = pending:CreateFontString(nil, "ARTWORK", "GameFontNormal") e:SetWidth(110) e:SetJustifyH("RIGHT") e:SetPoint("TOPLEFT", n, "TOPRIGHT", 0, 0) pending.names[i], pending.states[i] = n, e end -- the client updates the remaining time every frame pending:SetScript("OnUpdate", function(self) local c = LFDQueueFrameCooldownFrame if c and c.time then self.clock:SetText(c.time:GetText() or "") end end) F.pending = pending local backfill = createVeil("ForeverUIGroupFinderBackfill", 14) backfill.yes = ForeverUI.CreatePanelButton(backfill, txt("YES"), 153, 22, nil, "GameFontNormal") backfill.yes:SetPoint("TOPRIGHT", backfill.description, "BOTTOM", -5, -10) backfill.yes:SetScript("OnClick", function() if LFDQueueFramePartyBackfillBackfillButton then LFDQueueFramePartyBackfillBackfillButton:Click() end end) backfill.no = ForeverUI.CreatePanelButton(backfill, txt("HIDE"), 153, 22, nil, "GameFontNormal") backfill.no:SetPoint("TOPLEFT", backfill.description, "BOTTOM", 5, -10) backfill.no:SetScript("OnClick", function() if LFDQueueFramePartyBackfillNoBackfillButton then LFDQueueFramePartyBackfillNoBackfillButton:Click() end end) F.backfill = backfill local raid = createVeil("ForeverUIGroupFinderNoLFDWhileLFR", 16) raid.quit = ForeverUI.CreatePanelButton(raid, "", 153, 22, nil, "GameFontNormal") raid.quit:SetPoint("TOP", raid.description, "BOTTOM", 0, -10) raid.quit:SetScript("OnClick", function() if LFDQueueFrameNoLFDWhileLFRLeaveQueueButton then LFDQueueFrameNoLFDWhileLFRLeaveQueueButton:Click() end end) F.raid = raid end local function textOf(name) local r = _G[name] return r and r.GetText and r:GetText() or "" end -- area they cover: the details under the rule, or the whole list local function placeVeils() for _, v in ipairs({ F.pending, F.backfill, F.raid }) do v:ClearAllPoints() if F.view == "list" then v:SetAllPoints(F.frameBox) else v:SetPoint("TOPLEFT", F.frameBox, "TOPLEFT", 0, F.detailsTop) v:SetPoint("BOTTOMRIGHT", F.frameBox, "BOTTOMRIGHT", 0, 0) end end end local function updateVeils() placeVeils() -- cooldown: shown when visible in the client (its parent depends on deserter state); -- texts copied local c = LFDQueueFrameCooldownFrame if c and display(c) then F.pending.description:SetText(c.description and c.description:GetText() or "") F.pending.description:ClearAllPoints() F.pending.description:SetPoint("TOP", F.pending, "TOP", 0, GetNumPartyMembers() == 0 and -85 or -30) F.pending.clock:SetText(c.time and c.time:GetText() or "") if c.time and c.time:IsShown() then F.pending.clock:Show() else F.pending.clock:Hide() end for i = 1, 4 do local n, e = _G["LFDQueueFrameCooldownFrameName" .. i], _G["LFDQueueFrameCooldownFrameStatus" .. i] if n and n:IsShown() then F.pending.names[i]:SetText(n:GetText() or "") F.pending.states[i]:SetText(e and e:GetText() or "") F.pending.names[i]:Show() F.pending.states[i]:Show() else F.pending.names[i]:Hide() F.pending.states[i]:Hide() end end F.pending:Show() else F.pending:Hide() end local b = LFDQueueFramePartyBackfill if b and display(b) then F.backfill.description:SetText(textOf("LFDQueueFramePartyBackfillDescription")) F.backfill.yes:Activate(active(LFDQueueFramePartyBackfillBackfillButton)) F.backfill:Show() else F.backfill:Hide() end local l = LFDQueueFrameNoLFDWhileLFR if l and display(l) then F.raid.description:SetText(textOf("LFDQueueFrameNoLFDWhileLFRDescription")) F.raid.quit:SetText(textOf("LFDQueueFrameNoLFDWhileLFRLeaveQueueButton")) F.raid.quit:Activate(active(LFDQueueFrameNoLFDWhileLFRLeaveQueueButton)) F.raid:Show() else F.raid:Hide() end end -- ------------------------------------------------------------ Page -- Role banner of a page: a child frame above the stripes (camelot's RolesSection), -- with the blue background and the role buttons function F.buildBanner(p, roles) local f = F.frame local banner = CreateFrame("Frame", nil, p) banner:SetAllPoints(f) banner:SetFrameLevel(f:GetFrameLevel() + 1) local blue = banner:CreateTexture(nil, "BACKGROUND") blue:SetTexture(G.blue) blue:SetPoint("TOPLEFT", f, "TOPLEFT", G.blueX1, G.blueY1) blue:SetPoint("BOTTOMRIGHT", f, "TOPRIGHT", G.blueX2, G.blueY2) local l = {} F.allRoles = F.allRoles or {} for _, r in ipairs(roles) do local b = createRole(banner, r) table.insert(l, b) table.insert(F.allRoles, b) end return blue, l end -- Frame box of a page, above the banner: no marble, camelot's background 3 px from the -- edge. top, down: y offsets of its edges; trim: crop the background top (0.093) under a -- role banner function F.buildFrameBox(p, name, top, trim, down) local f = F.frame local frameBox = ForeverUI.CreateInset(p, name) frameBox:SetFrameLevel(f:GetFrameLevel() + 2) frameBox:SetPoint("TOPLEFT", f, "TOPLEFT", G.frameBoxX1, top) frameBox:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", G.frameBoxX2, down or G.frameBoxY2) frameBox.background:Hide() local background = frameBox:CreateTexture(nil, "BACKGROUND") ForeverUI.SetAtlas(background, "groupfinder-background", true) local e = ForeverUI.AtlasEntry("groupfinder-background") if e and trim then background:SetTexCoord(e[2], e[3], e[4] + (e[5] - e[4]) * G.backgroundTop, e[5]) end background:SetPoint("TOPLEFT", frameBox, "TOPLEFT", 3, -3) background:SetPoint("BOTTOMRIGHT", frameBox, "BOTTOMRIGHT", -3, 3) return frameBox end local function buildPage(f) local p = CreateFrame("Frame", "ForeverUIGroupFinderDungeons", f) p:SetAllPoints(f) F.dungeonsPage = p F.blue, F.roles = F.buildBanner(p, ROLES) -- its camelot background covers the bottom of the blue, as in the source local frameBox = F.buildFrameBox(p, "ForeverUIGroupFinderInset", G.frameBoxY1, true) F.frameBox = frameBox -- categories: stacked from the top, no scrolling; the rule and details follow the -- last button local cats = CreateFrame("Frame", "ForeverUIGroupFinderCategories", p) cats:SetFrameLevel(frameBox:GetFrameLevel() + 1) cats:SetPoint("TOPLEFT", f, "TOPLEFT", G.catX1, G.catY1) cats:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", G.catX2, G.catY2) cats.rows = {} F.viewCategories = cats local rule = cats:CreateTexture(nil, "OVERLAY") ForeverUI.SetAtlas(rule, "shop-list-rule", true) rule:SetHeight(G.ruleH) F.rule = rule -- specific dungeon list: the category list, down to the frame box bottom margin F.listView = F.createCatList(p, "ForeverUIGroupFinderList", "ForeverUIGroupFinder", SOURCE_LFD, G.listY2 + G.margin, frameBox) buildRewards(p) F.details:SetFrameLevel(frameBox:GetFrameLevel() + 1) F.details.bar:SetFrameLevel(frameBox:GetFrameLevel() + 2) buildVeils() -- bottom buttons local backButton = ForeverUI.CreatePanelButton(p, txt("BACK"), G.backL, G.buttonH, "ForeverUIGroupFinderBackButton", "GameFontNormal") backButton:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", G.buttonSide, G.buttonBottom) backButton:SetScript("OnClick", function() PlaySound("igMainMenuOptionCheckBoxOn") F.view = "categories" F.update() end) F.backButton = backButton local find = ForeverUI.CreatePanelButton(p, txt("FIND_A_GROUP"), G.postW, G.buttonH, "ForeverUIGroupFinderFindButton", "GameFontNormal") find:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -G.buttonSide, G.buttonBottom) -- the client button: its OnClick picks LeaveLFG or LFDQueueFrame_Join find:SetScript("OnClick", function() if LFDQueueFrameFindGroupButton and active(LFDQueueFrameFindGroupButton) then LFDQueueFrameFindGroupButton:Click() end end) F.find = find end -- ------------------------------------------------------------ Update local function isQueued() local mode = GetLFGMode() return mode == "queued" or mode == "rolecheck" or mode == "proposal" end -- category buttons, the rule under the last one, the details below local function placeCategories() local cats = F.viewCategories local n = #F.category_list for i = 1, n do local l = cats.rows[i] if not l then l = CreateFrame("Frame", nil, cats) l:SetHeight(G.catH) createCategory(l) cats.rows[i] = l end l:ClearAllPoints() l:SetPoint("TOPLEFT", cats, "TOPLEFT", 0, G.catFirst - (i - 1) * G.catStep) l:SetPoint("TOPRIGHT", cats, "TOPRIGHT", 0, G.catFirst - (i - 1) * G.catStep) fillCategory(l, i) l:Show() end for i = n + 1, #cats.rows do cats.rows[i]:Hide() end -- bottom of the last button, from the window top local down = G.catY1 + G.catFirst - (n - 1) * G.catStep - G.catH F.ruleMax = down - G.ruleGap placeRule(nil) end local function updateDungeons() local view = F.view or "categories" if view == "list" and LFDQueueFrame.type ~= "specific" then view = "categories" end F.view = view F.category_list = F.categories() placeCategories() if view == "categories" then F.viewCategories:Show() updateRewards() else F.viewCategories:Hide() F.details:Hide() F.details.bar:Hide() end if view == "list" then F.listView:Show() F.updateCategoryList(F.listView) else F.listView:Hide() F.listView.bar:Hide() end updateVeils() -- Back exists only in the "Specific Dungeons" list if view == "list" then F.backButton:Show() else F.backButton:Hide() end local client = LFDQueueFrameFindGroupButton if client then F.find:SetText(client:GetText() or txt("FIND_A_GROUP")) F.find:Activate(active(client)) end end -- Pages: one per tab; the dungeon page here, the raid page in GroupFinderRaid.lua -- (F.registerPage) F.pages = {} function F.registerPage(key, frame, update) F.pages[key] = { frame = frame, update = update } end function F.update() if not F.frame then return end F.updateRoles() local pg = F.pages[F.tab or "dungeons"] if pg then pg.update() end end -- show a page: select its tab, hide the other pages function F.show(key) F.selectTab(key) for k, pg in pairs(F.pages) do if k == key then pg.frame:Show() else pg.frame:Hide() end end F.update() end -- several client updates in one frame give a single update here local waiter = CreateFrame("Frame") waiter:Hide() waiter:SetScript("OnUpdate", function(self) self:Hide() F.update() end) F.waiter = waiter F.G, F.txt, F.active, F.color, F.COLORS, F.ROLES = G, txt, active, color, COLORS, ROLES -- Schedules one update on the next frame while the window is shown. function F.request() if F.frame and F.frame:IsShown() then waiter:Show() end end -- ------------------------------------------------------------ Client panel -- Silence the client panel: hide its regions and children except its content and our -- window, and stop it catching the mouse. Its content stays shown (its logic depends on -- it) but transparent and under our window, so its invisible buttons catch nothing. local function suppressClient() local p = LFDParentFrame for _, r in ipairs({ p:GetRegions() }) do r:Hide() end for _, c in ipairs({ p:GetChildren() }) do if c ~= F.frame and c ~= LFDQueueFrame then c:Hide() end end p:EnableMouse(false) LFDQueueFrame:SetAlpha(0) LFDQueueFrame:ClearAllPoints() LFDQueueFrame:SetPoint("TOPLEFT", F.frame, "TOPLEFT", 0, 0) LFDQueueFrame:SetWidth(p:GetWidth()) LFDQueueFrame:SetHeight(p:GetHeight()) end -- Our window moves between client panels: child of the open one, above its content; -- at its saved position if moved, else at the panel's. function F.attach(panel) local f = F.frame if f:GetParent() ~= panel then f:SetParent(panel) end f:SetFrameLevel(panel:GetFrameLevel() + 30) local pos = ForeverUIDB and ForeverUIDB.positions and ForeverUIDB.positions.finder if not pos then f:ClearAllPoints() f:SetPoint("TOPLEFT", panel, "TOPLEFT", 0, 0) end end -- A tab opens its client panel and closes the other. Dungeons stay closed below -- SHOW_LFD_LEVEL, like the micro button. function F.goTo(key) if key == "dungeons" then if UnitLevel("player") < (SHOW_LFD_LEVEL or 15) then return end if LFRParentFrame and LFRParentFrame:IsShown() then HideUIPanel(LFRParentFrame) end if not LFDParentFrame:IsShown() then ShowUIPanel(LFDParentFrame) end elseif LFRParentFrame then if LFDParentFrame:IsShown() then HideUIPanel(LFDParentFrame) end if LFRParentFrame:IsShown() then F.show(key) else ShowUIPanel(LFRParentFrame) end end end local function open() if LFRParentFrame and LFRParentFrame:IsShown() then HideUIPanel(LFRParentFrame) end F.attach(LFDParentFrame) suppressClient() -- on open, the categories; the list when queued for specific dungeons if isQueued() and LFDQueueFrame.type == "specific" then F.view = "list" else F.view = "categories" end F.frame:Show() F.show("dungeons") end local function build() if F.frame or not LFDParentFrame or not LFDQueueFrame then return end local f = CreateFrame("Frame", "ForeverUIGroupFinderFrame", LFDParentFrame) f:SetWidth(G.width) f:SetHeight(G.height) f:SetPoint("TOPLEFT", LFDParentFrame, "TOPLEFT", 0, 0) -- above all client content (its veils go up to +16) f:SetFrameLevel(LFDParentFrame:GetFrameLevel() + 30) f:EnableMouse(true) F.frame = f buildFrame(f) buildPage(f) F.registerPage("dungeons", F.dungeonsPage, updateDungeons) F.tabs = {} local previous for n, def in ipairs(TABS) do if def.raid and not LFRParentFrame then break end local o = createTab(f, def, n) if previous then o:SetPoint("TOPLEFT", previous, "BOTTOMLEFT", 0, G.tabGap) else o:SetPoint("TOPLEFT", f, "TOPRIGHT", 0, G.tabY) end F.tabs[n] = o previous = o end f:Hide() end build() if F.frame then LFDParentFrame:HookScript("OnShow", open) LFDParentFrame:HookScript("OnHide", function() if F.frame:GetParent() == LFDParentFrame then F.frame:Hide() end end) -- each client update and type change (NPC gossip also calls it): the view follows -- the chosen type for _, name in ipairs({ "LFDQueueFrameSpecificList_Update", "LFDQueueFrameRandom_UpdateFrame", "LFDQueueFrameFindGroupButton_Update", "LFG_UpdateRolesChangeable", "LFG_UpdateRoleCheckboxes", "LFG_UpdateLockedOutPanels", "LFDFrame_UpdateBackfill", "LFDQueueFrameRandomCooldownFrame_Update" }) do if _G[name] then hooksecurefunc(name, F.request) end end if LFDQueueFrame_SetType then hooksecurefunc("LFDQueueFrame_SetType", function(value) F.view = (value == "specific") and "list" or "categories" F.request() end) end local listener = CreateFrame("Frame") for _, ev in ipairs({ "LFG_UPDATE", "LFG_ROLE_UPDATE", "LFG_LOCK_INFO_RECEIVED", "LFG_UPDATE_RANDOM_INFO", "PARTY_MEMBERS_CHANGED" }) do listener:RegisterEvent(ev) end listener:SetScript("OnEvent", F.request) -- movable by its title; raised when opened or clicked ForeverUI.WindowStack.makeMovable(F.frame, F.frame.banner, "finder") ForeverUI.WindowStack.register("finder", LFDParentFrame, function() local z = { F.frame } for _, o in ipairs(F.tabs) do z[#z + 1] = o end return z end) end