-- ForeverUI: the character sheet in Camelot's layout: metal frame, left pane (model, gear -- slots), right pane (stats, gear manager, titles) and a column of side tabs on the right. -- Client frames (slots, tabs, model, stat rows) are resized and moved, never recreated, and -- never in combat. Debug commands: /fui character, /fui tabs, /fui close, /fui model. -- camelot: CHARACTER_FRAME_WIDTH/HEIGHT; LeftPaneHost 398 (the collapsed width), RightPaneHost 233 local WIDTH, HEIGHT = 631, 484 local LEFT_PANE, RIGHT_PANE = 398, 233 local L = ForeverUI.L -- Right pane collapse button (camelot $parentRightPaneToggleButton: 28 x 28 at TOPRIGHT -- (-6, -6) of the left pane). Collapsed, the window is the left pane's width; the side tabs -- are anchored to the window edge and stay visible. 3.3.5 has no CVar for the state (kept in -- ForeverUIDB) and no SOUNDKIT (sounds are named by string). local COLLAPSE_SIZE = 28 local COLLAPSE_X, COLLAPSE_Y = -6, -6 local COLLAPSE_ART = { expanded = { "Interface\\Buttons\\UI-SpellbookIcon-PrevPage-Up", "Interface\\Buttons\\UI-SpellbookIcon-PrevPage-Down" }, collapsed = { "Interface\\Buttons\\UI-SpellbookIcon-NextPage-Up", "Interface\\Buttons\\UI-SpellbookIcon-NextPage-Down" }, } local COLLAPSE_HOVER = "Interface\\Buttons\\UI-Common-MouseHilight" local OVERHEAD = 20 -- panes start below the title bar local ART_LEVEL = 5 -- frame level offset of the metal art, above the panes -- Metal frame corner offsets. The camelot layout (PortraitFrameTemplate plus -- NineSliceLayoutOverrides.lua) places the image's outer edge, which leaves the inner rim -- 6 px inside the window. Each offset here is the rim-to-edge distance measured on the art -- (uiframemetal2xc60, double density): left 18.5, right 8.5, bottom 14. The top keeps 16: -- the title bar is inside by design. PANEL_RISE lifts the whole frame by 1 px. local PANEL_RISE = 1 local PANEL_CORNERS = { topLeftCorner = { x = -18.5, y = 16 + PANEL_RISE }, topRightCorner = { x = 8.5, y = 16 + PANEL_RISE }, bottomLeftCorner = { x = -18.5, y = -14 + PANEL_RISE }, bottomRightCorner = { x = 8.5, y = -14 + PANEL_RISE }, } local SLOT_SIZE = 40 -- Slot layout (camelot/PaperDollFrame.xml): columns at (24, -60) and (-20, -60), main hand at -- BOTTOM (-60, 30), the variant used when a ranged slot is shown (always, in 3.3.5). -- The source gap is 6 both ways; the columns use 2 px less, the weapon row keeps 6. local GAP = 6 local VERTICAL_GAP = GAP - 2 local LEFT_X, LEFT_Y = 24, -60 local RIGHT_X, RIGHT_Y = -20, -60 local WEAPON_X, WEAPON_Y = -60, 30 local SMALL_SLOT_SIZE = 27 -- ammo slot only; camelot: 27, AMMO_GAP 19 after the ranged slot local AMMO_GAP = 19 -- The ranged slot is full size, unlike camelot's 27 px GearSlotSmall: it holds a ranged weapon -- or a relic, items of the same rank as the melee weapons. -- Right pane tabs (camelot PaperDollSidebarTabs: 42 x 42 buttons, bar at TOP (0, -4) of the -- pane, tabs at (0, -5) of the bar): stats, gear manager and titles (from mainline; camelot -- has no titles tab). 3.3.5 lacks PaperDollSidebarTabs.blp and the PAPERDOLL_SIDEBAR_* strings: -- the gear tab uses UI-GearManager-Button, the tooltips CHARACTER_INFO and EQUIPMENT_MANAGER. local PANE_TAB = 42 local PANE_TAB_Y = -9 -- bar at -4, first tab at -5 inside it -- Each visible side tab sits at BOTTOMLEFT (0, -2) of the previous visible one -- (camelot CharacterFrame.lua, UpdateTabLayout). local TAB_GAP = -2 -- Side tab icons (camelot CHARACTER_MODE_TAB_ICONS and SetupModeTabs), baked with the tab mask. -- Indexes are the 3.3.5 ones: 1 character, 2 pet, 3 reputation, 4 skills, 5 currency; pvp and -- stats are tabs we create (CHARACTERFRAME_SUBFRAMES has only five). The PvP icon follows the -- faction. Some files exist only by FileDataID: tools/simple_files.txt maps them to names. local TAB_ICONS = { [3] = "Interface\\ForeverUI\\TabIcons\\Inv_SideTab_Reputation2_c60", [4] = "Interface\\ForeverUI\\TabIcons\\Ability_Racial_JackofAllTrades", [5] = "Interface\\ForeverUI\\TabIcons\\Inv_SideTab_Currency_c60", pvp = { Alliance = "Interface\\ForeverUI\\TabIcons\\Inv_SideTab_Honor_Alliance_c60", Horde = "Interface\\ForeverUI\\TabIcons\\Inv_SideTab_Honor_Horde_c60", }, stats = "Interface\\ForeverUI\\TabIcons\\Inv_SideTab_Stats_c60", mounts = "Interface\\ForeverUI\\TabIcons\\MountJournalPortrait", } -- Pet tab icon by class (camelot has no pet tab); the hunter one otherwise, e.g. for a mage -- with the water elemental glyph local PET_TAB = { HUNTER = "Ability_Hunter_BeastTaming", WARLOCK = "Spell_Shadow_SummonImp", DEATHKNIGHT = "Spell_Shadow_AnimateDead", } TAB_ICONS[2] = "Interface\\ForeverUI\\TabIcons\\" .. (PET_TAB[select(2, UnitClass("player")) or ""] or PET_TAB.HUNTER) -- Column order: camelot's (character, reputation, skills, PvP, currency, stats) with the 3.3.5 -- pet tab second and the mounts under it. A number is a client tab, a string one of ours. local TAB_ORDER = { 1, 2, "mounts", 3, 4, "pvp", 5, "stats" } -- Character tab icon: the class icon (camelot uses the player portrait), one baked file per -- class, named after UnitClass's token (WARRIOR, DEATHKNIGHT). MPQ lookups ignore case. local CLASS_TAB = "Interface\\ForeverUI\\TabIcons\\ClassIcon_" -- Screen opened by each client tab (CharacterFrameTab_OnClick); tells which tab is active local TAB_SCREEN = { [1] = "PaperDollFrame", [2] = "PetPaperDollFrame", [3] = "ReputationFrame", [4] = "SkillFrame", [5] = "TokenFrame", } -- Our screens, used as ForeverUI.Panes group names; they are not client subframes local PVP_SCREEN = "ForeverUIPvPPane" local STATS_SCREEN = "ForeverUIStatsPane" local MOUNTS_SCREEN = "ForeverUIMountsPane" local TAB_CREATED_SCREENS = { pvp = PVP_SCREEN, stats = STATS_SCREEN, mounts = MOUNTS_SCREEN } -- Right pane tab icon: 28 px, the opening of UI-Character-Info-StatTab (metal at 3..6 and -- 35..38 of 42). A portrait fills its texture and would overflow the rounded frame. local PANE_TAB_ICON = 28 local ATLAS_PANE_TAB = "ui-character-info-stattab" local ATLAS_PANE_TAB_SELECTED = "ui-character-info-stattab-selected" local TAB_PORTRAIT_COORD = { 0.109375, 0.890625, 0.09375, 0.90625 } local GEAR_MANAGER_ICON = "Interface\\PaperDollInfoFrame\\UI-GearManager-Button" -- UI-GearManager-Button is opaque only in x 6..58 of 64: crop the central 52 x 52 square local GEAR_MANAGER_ICON_COORD = { 6 / 64, 58 / 64, 6 / 64, 58 / 64 } -- Titles tab icon: mainline PAPERDOLL_SIDEBARTAB_TITLES (camelot has no titles tab). The sheet -- is not in 3.3.5 and ships with ForeverUI; the crop is the sealed scroll. local TITLES_ICON = "Interface\\ForeverUI\\PaperDollInfoFrame\\paperdollsidebartabs" local TITLES_ICON_COORD = { 0.015625, 0.53125, 0.32421875, 0.46093750 } -- camelot 70450: UI-Character-Info-Stat-StoneBG3, 233 x 60 (the level line left the band for the -- left pane). StoneBG3 is StoneBG (233 x 85) with 25 px less in the middle: drawn from StoneBG's -- top and bottom 30 px, so no new art is needed. local STONE_HEIGHT = 60 local STONE_SOURCE_HEIGHT = 85 local STONE_PART = 30 local SEPARATOR_WIDTH = 11 local SEPARATOR_CAP = 4 -- cap height; the art is 11 x 50 with a cap at each end local TABS_W, TABS_H = 64, 384 -- CharacterFrameModeTabs local TABS_X = 1 -- 1 px right of the source local TABS_Y = -30 local TAB_W, TAB_H = 55, 55 -- 55 x 60 minus 5 transparent px -- Side tab icon (camelot SidePanelTabButtonMixin:UpdateIconInterior, fillToInterior): 50 px, -- cropped by 0.03125, offset (-3, 0) since the tab art is transparent on the right. -- 3.3.5 has no MaskTexture: tools/bake_masks.py bakes common-sidetab-mask into the icons -- (TabIcons/). The bake assumes this size and crop. local SIDE_TAB_ICON = 50 -- interiorExtent local TAB_ICON_X = -3 -- GetIconAnchorOffsetsForTabArt local TAB_PORTRAIT_INSET = 0.03125 -- UpdateIconInterior crop -- Side tab layers (LargeSideTabButtonTemplate): BACKGROUND common-sidetab, ARTWORK icon, -- OVERLAY common-sidetab-selected for the active tab, HIGHLIGHT common-sidetab-hover. local PORTRAIT = 48 -- see placePortrait -- The portrait follows the top-left corner image, which carries the ring: the hole's center is -- at (38, -38.5) from the image's top-left (measured on the double-density art). local PORTRAIT_HOLE_X, PORTRAIT_HOLE_Y = 38, -38.5 local PORTRAIT_X = PANEL_CORNERS.topLeftCorner.x + PORTRAIT_HOLE_X local PORTRAIT_Y = PANEL_CORNERS.topLeftCorner.y + PORTRAIT_HOLE_Y -- Title (TitledPanelMixin:SetTitleOffsets defaults, which CharacterFrame keeps): container at -- TOPLEFT (58, -1) and TOPRIGHT (-24, -1), text at TOP (0, -5). It is centered between the -- portrait and the close button, not on the window; one line. local TITLE_LEFT, TITLE_RIGHT = 58, -24 local TITLE_CONTAINER, TITLE_TEXT = -1, -5 local TITLE_STRIP_HEIGHT = 20 -- The title always shows the character's name and title (camelot shows the open panel's name). -- Level and class line (camelot 70450 paperdollframe.xml): TopBackgroundStripHost, 36 high across -- the top of the left pane, UI-Background-Rock tiled with _UI-Frame-InnerTopTile along its bottom; -- PaperDollLevelInfo 220 x 20 at CENTER (0, -2) of the strip, GameFontNormalMed2Outline (14, -- outlined; 3.3.5 has no such font object), "Level %d |cClass|r" -- (PLAYER_LEVEL_NO_SPEC). local STRIP_HEIGHT = 36 local STRIP_EDGE = 3 local LEVEL_Y = -2 local LEVEL_WIDTH, LEVEL_HEIGHT = 220, 20 local LEVEL_FONT_SIZE = 14 -- UI-Background-Rock is 1024 x 1024: the strip shows a crop at its own size (3.3.5 textures -- cannot tile) local ROCK_FILE = "Interface" .. string.char(92) .. "ForeverUI" .. string.char(92) .. "framegeneral" .. string.char(92) .. "ui-background-rock" local ROCK_SIZE = 1024 -- Model rotation arrows, side by side, centered at the top of the left pane, under the strip -- (camelot 70450: the model scene's ControlFrame at TOP (0, -40) of the left pane). local ROTATION_Y = -40 local ROTATION_GAP = 4 -- The model is raised in its pane: camelot's scene fills the pane, but the 3.3.5 camera frames -- the character lower. Tuned by eye. local MODEL_Y = 24 -- Camelot frames the character with a model scene; 3.3.5 has none. This client's Model has -- SetModelScale, SetPosition, SetCamera and SetFacing, but no SetCameraDistance, -- SetCamDistanceScale or SetPortraitZoom. SetModelScale shrinks the model around its origin -- (it slides down); SetPosition(depth, side, height) moves it away without reframing. -- Values tuned by eye (/fui model); a nil scale leaves the client's scale untouched. local MODEL_SCALE = nil local MODEL_POSITION = { -6.5, 0, 0 } -- depth, side, height -- Resistances panel offset to the right, added to its original anchor local RESISTANCES_X = 30 -- Statistics: CharacterAttributesFrame holds two groups of six StatFrameTemplate rows (104 x 13), -- PlayerStatFrameLeft1..6 and Right1..6, each under a category dropdown. They stay the client's -- (values, tooltips, menus) and are stacked in one column like camelot, widened to the pane. -- The client's FrameXML is in patch-enUS-2/3 (patch-enUS-9 is disabled). local STAT_STEP = 13 -- StatFrameTemplate height local STAT_MARGIN = 20 -- on each side of the pane local STAT_TOP = 2 -- below the stone band (tightened by the user, 2026-10-17) -- 12 (was 16): three groups under the 60 px stone take 2 + 3 x (35 + 78) + 2 x 12 = 365 of 404 px local STAT_GROUP_GAP = 12 local STAT_PER_GROUP = 6 -- Category selectors (UIDropDownMenuTemplate): camelot's dropdown control, -- WowStyle1DropdownTemplate (blizzard_menu/mainline/MenuTemplates.xml, 25 high), laid over the -- client's menu by Tpl.MenuStyle1: common-dropdown-textholder background, common-dropdown-a-button -- arrow and its states, GameFontHighlight text on the left. The menu and its categories stay the -- client's. The control is as wide as the rows; its background reaches 8 px past them on each -- side (TOPLEFT (-8, 7) / BOTTOMRIGHT (8, -9)): STAT_HEADER leaves room for it. local STAT_SELECTOR_HEIGHT = 25 local STAT_SELECTOR_TOP = 7 -- the background starts 7 px above the control -- rows start 3 px under the control, inside the shadow its background casts (tightened by the -- user, 2026-10-17: was the whole 9 px shadow + 2) local STAT_HEADER = STAT_SELECTOR_TOP + STAT_SELECTOR_HEIGHT + 3 -- Alternating stat row bands (neither camelot nor 3.3.5 has them), from paperdollinfopart1c60: -- dark UI-Character-Info-ItemLevel-Bounce first, then light UI-Character-Info-Line-Bounce; -- the count restarts at each category. StatFrameTemplate's label is on BACKGROUND and 3.3.5 -- has no sublevels, so the band would draw over it: the label moves up to ARTWORK. local ATLAS_STAT_DARK = "ui-character-info-itemlevel-bounce" local ATLAS_STAT_LIGHT = "ui-character-info-line-bounce" local STAT_GROUPS = { { selector = "PlayerStatFrameLeftDropDown", prefix = "PlayerStatFrameLeft", cvar = "playerStatLeftDropdown", }, { selector = "PlayerStatFrameRightDropDown", prefix = "PlayerStatFrameRight", cvar = "playerStatRightDropdown", }, -- A third group, ForeverUI's own (not in camelot nor in 3.3.5), in the 25 px the stone gave -- back: same dropdown and rows, its choice kept in ForeverUIDB.statThirdCategory (3.3.5 cannot -- create a CVar); by default a category the two others do not show. { selector = "ForeverUIStatFrameThirdDropDown", prefix = "ForeverUIStatFrameThird", third = true, }, } -- The client's category list (PaperDollFrame.lua PLAYERSTAT_DROPDOWN_OPTIONS), in its order local STAT_CATEGORIES = PLAYERSTAT_DROPDOWN_OPTIONS or { "PLAYERSTAT_BASE_STATS", "PLAYERSTAT_MELEE_COMBAT", "PLAYERSTAT_RANGED_COMBAT", "PLAYERSTAT_SPELL_COMBAT", "PLAYERSTAT_DEFENSES" } local CLOSE_SIZE = 24 local CLOSE_X, CLOSE_Y = 1, 0 local CLOSE_ATLAS = { { atlas = "redbutton-exit", method = "GetNormalTexture" }, { atlas = "redbutton-exit-pressed", method = "GetPushedTexture" }, { atlas = "redbutton-exit-disabled", method = "GetDisabledTexture" }, { atlas = "redbutton-highlight", method = "GetHighlightTexture" }, } local PORTRAIT_CORNER = "ui-frame-portraitmetal-cornertopleft" local ATLAS = { backgroundLeft = "ui-character-info-general-bg", backgroundRight = "ui-character-info-stat-bg", stone = "ui-character-info-stat-stonebg", separator = "common-framedivider", slot = "ui-character-info-gearslot", smallSlot = "ui-character-info-gearslotsmall", tab = "common-sidetab", tabHover = "common-sidetab-hover", tabActive = "common-sidetab-selected", } local LEFT_COLUMN = { "Head", "Neck", "Shoulder", "Back", "Chest", "Shirt", "Tabard", "Wrist", } local RIGHT_COLUMN = { "Hands", "Waist", "Legs", "Feet", "Finger0", "Finger1", "Trinket0", "Trinket1", } local WEAPON_ROW = { "MainHand", "SecondaryHand", "Ranged" } -- Weapon row: extra gap per slot, relative to the left neighbor and cumulative along the chain -- (main hand 0, off hand -2, ranged -4; the ammo slot follows the ranged one). The ammo arrow -- is a region of CharacterAmmoSlot and moves with it. local ROW_OFFSET = { 0, -2, -2 } -- Frames whose old art is hidden: 3.3.5 spreads it over child frames, which a sweep of -- CharacterFrame alone misses. local OLD_FRAMES = { "CharacterFrame", "PaperDollFrame", "PetPaperDollFrame", "SkillFrame", "ReputationFrame", "HonorFrame", "TokenFrame", "PaperDollItemsFrame", "CharacterAttributesFrame", "CharacterAttributesFrameer", "CharacterResistanceFrame", } -- Saved window positions (ForeverUIDB). The client's panel manager re-places the sheet on each -- show, so the position saved by dragging is re-applied after it. local function savedPositions() ForeverUIDB = ForeverUIDB or {} ForeverUIDB.positions = ForeverUIDB.positions or {} return ForeverUIDB.positions end local leftPane, rightPane, tabBar local tabs = {} -- Hides the texture regions of a frame (not those of its children) local function sweepTextures(frame) if not frame or not frame.GetRegions then return end local regions = { frame:GetRegions() } for _, region in ipairs(regions) do if region and region.GetObjectType and region:GetObjectType() == "Texture" then region:SetAlpha(0) end end end local function clearLegacyArt() for _, name in ipairs(OLD_FRAMES) do sweepTextures(_G[name]) end end -- A file added to the .toc loads only after a client restart (/reload does not find new -- files). If Panes.lua is missing, say so in the chat instead of failing. local missingReported = false local function libraryPresent() if ForeverUI.Panes then return true end if not missingReported then missingReported = true DEFAULT_CHAT_FRAME:AddMessage("|cff66ccffForeverUI|r : " .. L.CHARACTERFRAME_PANES_MISSING) end return false end -- Creates the left and right panes (backgrounds, stone band, divider) as ForeverUI.Panes hosts local function setupPanes(frame) if leftPane then return end leftPane = CreateFrame("Frame", "ForeverUICharacterLeftPane", frame) leftPane:SetWidth(LEFT_PANE) leftPane:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, -OVERHEAD) leftPane:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 0, 0) local backgroundLeft = leftPane:CreateTexture(nil, "BACKGROUND") ForeverUI.SetAtlas(backgroundLeft, ATLAS.backgroundLeft) backgroundLeft:SetPoint("TOPLEFT", leftPane, "TOPLEFT", 0, 0) rightPane = CreateFrame("Frame", "ForeverUICharacterRightPane", frame) rightPane:SetWidth(RIGHT_PANE) rightPane:SetPoint("TOPLEFT", leftPane, "TOPRIGHT", 0, 0) rightPane:SetPoint("BOTTOMLEFT", leftPane, "BOTTOMRIGHT", 0, 0) -- The source sets no anchor: the background fills the pane (its atlas is 233 x 383). local backgroundRight = rightPane:CreateTexture(nil, "BACKGROUND") ForeverUI.SetAtlas(backgroundRight, ATLAS.backgroundRight, true) backgroundRight:SetAllPoints(rightPane) -- StoneBG3: StoneBG's top and bottom STONE_PART px, one under the other, in a 233 x 60 frame -- that is the stone band for whatever anchors under it (equipment and titles pages) local stone = CreateFrame("Frame", "ForeverUICharacterStone", rightPane) stone:SetWidth(RIGHT_PANE) stone:SetHeight(STONE_HEIGHT) stone:SetPoint("TOPLEFT", rightPane, "TOPLEFT", 0, 0) stone:SetFrameLevel(rightPane:GetFrameLevel()) local upper = stone:CreateTexture(nil, "ARTWORK") local lower = stone:CreateTexture(nil, "ARTWORK") local e = ForeverUI.AtlasEntry(ATLAS.stone) if e then local cut = (e[5] - e[4]) * STONE_PART / STONE_SOURCE_HEIGHT upper:SetTexture(e[1]) upper:SetTexCoord(e[2], e[3], e[4], e[4] + cut) lower:SetTexture(e[1]) lower:SetTexCoord(e[2], e[3], e[5] - cut, e[5]) end upper:SetPoint("TOPLEFT", stone, "TOPLEFT", 0, 0) upper:SetPoint("TOPRIGHT", stone, "TOPRIGHT", 0, 0) upper:SetHeight(STONE_PART) lower:SetPoint("TOPLEFT", upper, "BOTTOMLEFT", 0, 0) lower:SetPoint("BOTTOMRIGHT", stone, "BOTTOMRIGHT", 0, 0) stone.upper, stone.lower = upper, lower rightPane.stone = stone -- The divider has a cap at each end, so it is drawn in three slices. It sits one level above -- the skin layer (ART_LEVEL), else the metal covers it; that layer does not exist yet here, -- hence the constant. local separator = ForeverUI.CreateVerticalDivider(rightPane, ATLAS.separator, SEPARATOR_CAP, frame:GetFrameLevel() + ART_LEVEL + 1) separator:SetWidth(SEPARATOR_WIDTH) separator:SetPoint("TOPLEFT", rightPane, "TOPLEFT", -6, -1) separator:SetPoint("BOTTOMLEFT", rightPane, "BOTTOMLEFT", -6, 0) rightPane.separator = separator ForeverUI.CharacterPanes = { left = leftPane, right = rightPane } -- Both panes are ForeverUI.Panes hosts: what they show is decided in Panes.lua. ForeverUI.Panes.NewHost("left", leftPane) ForeverUI.Panes.NewHost("right", rightPane) end -- Portrait: the ring's hole crops it, as on the bags (camelot masks a 62 px portrait). -- Ring profile by radius: 0-12 hole, 13-25 gradient, 26-29 opaque, 30-32 fade. A square of -- side C reaches C/2 on its axes and C/2 x 1.41 in its corners: 48 reaches opaque metal on the -- axes (24), with corners at 33.9, just past the ring; 46 would keep the corners inside. local function placePortrait(frame) local host = frame.foreverSkinLayer or frame if not frame.foreverPortrait then local portrait = host:CreateTexture(nil, "BACKGROUND") portrait:SetWidth(PORTRAIT) portrait:SetHeight(PORTRAIT) portrait:SetPoint("CENTER", host, "TOPLEFT", PORTRAIT_X, PORTRAIT_Y) frame.foreverPortrait = portrait end if SetPortraitTexture then SetPortraitTexture(frame.foreverPortrait, "player") end end -- Title in a child frame above the metal (camelot: TitleContainer at level 510, NineSlice at -- 400): the metal is OVERLAY on its own frame. local function placeTitle(frame) if not frame.foreverTitleStrip then local old = _G["CharacterNameText"] if old then old:Hide() end local strip = CreateFrame("Frame", "ForeverUICharacterTitle", frame) strip:SetFrameLevel(frame:GetFrameLevel() + ART_LEVEL + 2) strip:SetHeight(TITLE_STRIP_HEIGHT) strip:SetPoint("TOPLEFT", frame, "TOPLEFT", TITLE_LEFT, TITLE_CONTAINER) strip:SetPoint("TOPRIGHT", frame, "TOPRIGHT", TITLE_RIGHT, TITLE_CONTAINER) local text = strip:CreateFontString(nil, "OVERLAY", "GameFontNormal") text:SetPoint("TOP", strip, "TOP", 0, TITLE_TEXT) text:SetPoint("LEFT", strip, "LEFT") text:SetPoint("RIGHT", strip, "RIGHT") text:SetJustifyH("CENTER") frame.foreverTitleStrip = strip frame.foreverTitle = text -- The title bar is the drag handle frame:SetMovable(true) frame:SetClampedToScreen(true) strip:EnableMouse(true) strip:RegisterForDrag("LeftButton") strip:SetScript("OnDragStart", function() if not InCombatLockdown() then frame:StartMoving() end end) strip:SetScript("OnDragStop", function() frame:StopMovingOrSizing() local point, _, relativePoint, x, y = frame:GetPoint(1) if point then savedPositions()["sheet"] = { point = point, relativePoint = relativePoint, x = x, y = y, } end end) end -- UnitPVPName gives the name with the current title, if any local text = frame.foreverTitle local name = (UnitPVPName and UnitPVPName("player")) or UnitName("player") text:SetText(name or "") if HIGHLIGHT_FONT_COLOR then text:SetTextColor(HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b) end end -- Close button: the client's, with the modern red X, at the top right corner local function placeCloseButton(frame) local close = _G["CharacterFrameCloseButton"] if not close then return end close:SetWidth(CLOSE_SIZE) close:SetHeight(CLOSE_SIZE) close:ClearAllPoints() close:SetPoint("TOPRIGHT", frame, "TOPRIGHT", CLOSE_X, CLOSE_Y) close:SetFrameLevel(frame:GetFrameLevel() + ART_LEVEL + 2) if frame.foreverCloseSkinned then return end for _, entry in ipairs(CLOSE_ATLAS) do local texture = close[entry.method] and close[entry.method](close) if texture then ForeverUI.SetAtlas(texture, entry.atlas, true) texture:ClearAllPoints() texture:SetAllPoints(close) if entry.atlas == "redbutton-highlight" then texture:SetBlendMode("ADD") end end end frame.foreverCloseSkinned = true end -- Resizes a client gear slot and gives it camelot's slot frame. -- name: slot name part (Head, Ammo); side: size; frameAtlas: frame art local function skinSlot(name, side, frameAtlas) local button = _G["Character" .. name .. "Slot"] if not button then return nil end button:SetWidth(side) button:SetHeight(side) -- Camelot lays the slots over a model that fills the left pane. The model takes the mouse and -- shares the slots' frame level (all PaperDollFrame children); at equal levels the model gets -- the click, so the slot goes one level above. Raised only when needed: this runs on each pass. local model = _G["CharacterModelFrame"] if model and model.GetFrameLevel and button:GetFrameLevel() <= model:GetFrameLevel() then button:SetFrameLevel(model:GetFrameLevel() + 1) end if not button.foreverFrame then local normalFont = button:GetNormalTexture() if normalFont then normalFont:SetAlpha(0) end local frame = button:CreateTexture(nil, "BACKGROUND") ForeverUI.SetAtlas(frame, frameAtlas) frame:SetPoint("CENTER", button, "CENTER", 0, 0) button.foreverFrame = frame end local icon = _G["Character" .. name .. "SlotIconTexture"] if icon then icon:ClearAllPoints() icon:SetAllPoints(button) icon:SetTexCoord(0, 1, 0, 1) end return button end -- Equipment flyout arrows (camelot EquipmentFlyoutPopoutButtonTemplate, 1.60.1.70450): the 3.3.5 -- popout button of each slot (PaperDollFrameItemPopoutButton: same flyout, same "ignore this -- slot" entries; shown while GearManagerDialog is, so on the equipment page only) gets camelot's -- art and place. Left column: points right, 20 x 43 at LEFT on the slot's RIGHT (5, 0); right -- column: points left, at RIGHT on its LEFT (-5, 0); weapons: points up, 43 x 20 at BOTTOM on -- its TOP (0, 5). UI-Character-Info-Button-PullSide / PullUp, -Open while the flyout is locked -- open, -Pressed, highlight = the image in ADD. camelot rotates the side image for the right -- direction; 3.3.5 has no SetRotation: the half turn is done by swapping the tex coords. local POPOUT = { side = { w = 20, h = 43, atlas = "ui-character-info-button-pullside" }, up = { w = 43, h = 20, atlas = "ui-character-info-button-pullup" }, gap = 5, } -- Sets atlas name on texture, turned half a turn when flip is set local function popoutTexture(texture, name, flip) local e = texture and ForeverUI.AtlasEntry(name) if not e then return end texture:SetTexture(e[1]) if flip then texture:SetTexCoord(e[3], e[2], e[5], e[4]) else texture:SetTexCoord(e[2], e[3], e[4], e[5]) end texture:ClearAllPoints() texture:SetAllPoints(texture:GetParent()) end -- direction: "LEFT", "RIGHT" or "UP"; open: the flyout is locked open local function paintPopout(popout, direction, open) local set = (direction == "UP") and POPOUT.up or POPOUT.side -- the side image points left (camelot's rotation for LEFT is 0) local flip = direction == "RIGHT" local normal = set.atlas .. (open and "-open" or "") popout:SetWidth(set.w) popout:SetHeight(set.h) popoutTexture(popout:GetNormalTexture(), normal, flip) popoutTexture(popout:GetPushedTexture(), set.atlas .. "-pressed", flip) local highlight = popout:GetHighlightTexture() popoutTexture(highlight, normal, flip) if highlight then highlight:SetBlendMode("ADD") end end local function skinPopout(name, direction) local slot = _G["Character" .. name .. "Slot"] local popout = slot and (slot.popoutButton or _G["Character" .. name .. "SlotPopoutButton"]) if not popout then return end -- the Pushed state does not exist in the client's template if not popout:GetPushedTexture() and popout.SetPushedTexture then local e = ForeverUI.AtlasEntry(POPOUT.side.atlas) if e then popout:SetPushedTexture(e[1]) end end popout.foreverDirection = direction paintPopout(popout, direction, popout.flyoutLocked) popout:ClearAllPoints() if direction == "RIGHT" then popout:SetPoint("LEFT", slot, "RIGHT", POPOUT.gap, 0) elseif direction == "LEFT" then popout:SetPoint("RIGHT", slot, "LEFT", -POPOUT.gap, 0) else popout:SetPoint("BOTTOM", slot, "TOP", 0, POPOUT.gap) end end local function skinPopouts() for _, name in ipairs(LEFT_COLUMN) do skinPopout(name, "RIGHT") end for _, name in ipairs(RIGHT_COLUMN) do skinPopout(name, "LEFT") end for _, name in ipairs(WEAPON_ROW) do skinPopout(name, "UP") end end -- The flyout opens the way its arrow points (camelot EquipmentFlyout: flyoutDirection); 3.3.5 -- always opens it right of the arrow (below it for the weapons). Re-anchored after the client's -- PaperDollFrameItemFlyout_Show, which runs again on each update of the open list. if hooksecurefunc and type(PaperDollFrameItemFlyout_Show) == "function" then hooksecurefunc("PaperDollFrameItemFlyout_Show", function(slot) local popout = slot and slot.popoutButton local flyout = _G["PaperDollFrameItemFlyout"] local list = flyout and flyout.buttonFrame local direction = popout and popout.foreverDirection if not list or not direction then return end list:ClearAllPoints() if direction == "LEFT" then list:SetPoint("TOPRIGHT", popout, "TOPLEFT", 0, 0) elseif direction == "UP" then list:SetPoint("BOTTOMLEFT", popout, "TOPLEFT", 0, PDFITEMFLYOUT_BORDERWIDTH or 0) else list:SetPoint("TOPLEFT", popout, "TOPRIGHT", 0, 0) end end) end -- The client turns its arrow when the flyout opens or closes: our image follows, open or not if hooksecurefunc and type(PaperDollFrameItemPopoutButton_SetReversed) == "function" then hooksecurefunc("PaperDollFrameItemPopoutButton_SetReversed", function(popout, reversed) if popout and popout.foreverDirection then paintPopout(popout, popout.foreverDirection, reversed) end end) end local function placeSlots() local previous for index, name in ipairs(LEFT_COLUMN) do local button = skinSlot(name, SLOT_SIZE, ATLAS.slot) if button then button:ClearAllPoints() if index == 1 then button:SetPoint("TOPLEFT", leftPane, "TOPLEFT", LEFT_X, LEFT_Y) else button:SetPoint("TOPLEFT", previous, "BOTTOMLEFT", 0, -VERTICAL_GAP) end previous = button end end previous = nil for index, name in ipairs(RIGHT_COLUMN) do local button = skinSlot(name, SLOT_SIZE, ATLAS.slot) if button then button:ClearAllPoints() if index == 1 then button:SetPoint("TOPRIGHT", leftPane, "TOPRIGHT", RIGHT_X, RIGHT_Y) else button:SetPoint("TOPLEFT", previous, "BOTTOMLEFT", 0, -VERTICAL_GAP) end previous = button end end previous = nil for index, name in ipairs(WEAPON_ROW) do local button = skinSlot(name, SLOT_SIZE, ATLAS.slot) if button then button:ClearAllPoints() if index == 1 then button:SetPoint("BOTTOM", leftPane, "BOTTOM", WEAPON_X, WEAPON_Y) else button:SetPoint("TOPLEFT", previous, "TOPRIGHT", GAP + (ROW_OFFSET[index] or 0), 0) end previous = button end end local ammo = skinSlot("Ammo", SMALL_SLOT_SIZE, ATLAS.smallSlot) if ammo and previous then ammo:ClearAllPoints() ammo:SetPoint("LEFT", previous, "RIGHT", AMMO_GAP, 0) end skinPopouts() end ForeverUI.ModelSetting = { scale = MODEL_SCALE, position = MODEL_POSITION } -- Mouse drag rotation for the character and pet models (3.3.5 only rotates them with the arrow -- buttons). Camelot's orbit camera: 0.008 rad per UI unit of horizontal drag, eased at 0.15 -- per frame (OrbitCameraMixin). Here the model turns through model.rotation and SetRotation. local MOUSE_ROTATION, ROTATION_SMOOTHING = 0.008, 0.15 local function rotateWithMouse(model) if not model or model.foreverMouseHooked then return end model.foreverMouseHooked = true model:EnableMouse(true) model:HookScript("OnMouseDown", function(self, button) if button == "LeftButton" then self.foreverCursor = GetCursorPosition() self.foreverTarget = self.foreverTarget or self.rotation or 0 end end) model:HookScript("OnMouseUp", function(self, button) if button == "LeftButton" then self.foreverCursor = nil end end) model:HookScript("OnHide", function(self) self.foreverCursor, self.foreverTarget = nil, nil end) model:HookScript("OnUpdate", function(self, elapsed) if self.foreverCursor then local x = GetCursorPosition() local d = (x - self.foreverCursor) / UIParent:GetEffectiveScale() self.foreverCursor = x self.foreverTarget = self.foreverTarget + d * MOUSE_ROTATION end if not self.foreverTarget then return end local r = self.rotation or 0 local k = 1 - (1 - ROTATION_SMOOTHING) ^ ((elapsed or 0) * 60) r = r + (self.foreverTarget - r) * k if math.abs(self.foreverTarget - r) < 0.0005 then r = self.foreverTarget if not self.foreverCursor then self.foreverTarget = nil end end self.rotation = r self:SetRotation(r) end) end ForeverUI.RotateWithMouse = rotateWithMouse rotateWithMouse(_G["CharacterModelFrame"]) rotateWithMouse(_G["PetModelFrame"]) -- The model loads asynchronously after PaperDollFrame_OnShow's SetUnit, and the load resets its -- transform. The setting is re-applied every frame for 1.5 s after each pass, without -- comparing: GetPosition may not match what is drawn. local MODEL_RECHECK_TIME = 1.5 local modelRecheck = CreateFrame("Frame", "ForeverUICharacterModelRecheck") modelRecheck:Hide() modelRecheck.rest = 0 local function applyModelSetting() local model = _G["CharacterModelFrame"] if not model then return end local setting = ForeverUI.ModelSetting if model.SetModelScale and setting.scale then model:SetModelScale(setting.scale) end if model.SetPosition and setting.position then model:SetPosition(setting.position[1], setting.position[2], setting.position[3]) end end modelRecheck:SetScript("OnUpdate", function(self, elapsed) applyModelSetting() self.rest = self.rest - (elapsed or 0) if self.rest <= 0 then self:Hide() end end) local function placeModel() local model = CharacterModelFrame if not model then return end -- camelot 70450: the model scene starts under the level strip model:ClearAllPoints() model:SetPoint("TOPLEFT", leftPane, "TOPLEFT", 0, MODEL_Y - STRIP_HEIGHT) model:SetPoint("BOTTOMRIGHT", leftPane, "BOTTOMRIGHT", 0, MODEL_Y) -- Now, then every frame until the model has loaded applyModelSetting() modelRecheck.rest = MODEL_RECHECK_TIME modelRecheck:Show() -- Rotation arrows, side by side, centered on the pane local left = _G["CharacterModelFrameRotateLeftButton"] local right = _G["CharacterModelFrameRotateRightButton"] if left and right then local half = left:GetWidth() / 2 + ROTATION_GAP / 2 left:ClearAllPoints() left:SetPoint("TOP", leftPane, "TOP", -half, ROTATION_Y) left:SetFrameLevel(model:GetFrameLevel() + 2) right:ClearAllPoints() right:SetPoint("TOP", leftPane, "TOP", half, ROTATION_Y) right:SetFrameLevel(model:GetFrameLevel() + 2) end end -- Resistances panel: always offset from its original anchor, kept aside, so it does not drift local function placeResistances() local frame = _G["CharacterResistanceFrame"] if not frame or not frame.GetPoint or frame:GetNumPoints() == 0 then return end if not frame.foreverAnchor then local point, target, targetPoint, x, y = frame:GetPoint(1) frame.foreverAnchor = { point, target, targetPoint, x or 0, y or 0 } end local a = frame.foreverAnchor frame:ClearAllPoints() frame:SetPoint(a[1], a[2], a[3], a[4] + RESISTANCES_X, a[5]) end -- Forward declaration: skinSelector hooks it before its definition below; without it the name -- would resolve to a nil global. local updateSelectorStates -- Third group: its category, from ForeverUIDB, else the first category the two client groups do -- not show, casters' spell and hunters' ranged categories first local THIRD_PREFERRED = { HUNTER = "PLAYERSTAT_RANGED_COMBAT", MAGE = "PLAYERSTAT_SPELL_COMBAT", PRIEST = "PLAYERSTAT_SPELL_COMBAT", WARLOCK = "PLAYERSTAT_SPELL_COMBAT", DRUID = "PLAYERSTAT_SPELL_COMBAT", SHAMAN = "PLAYERSTAT_SPELL_COMBAT", PALADIN = "PLAYERSTAT_SPELL_COMBAT", } local function thirdCategory() ForeverUIDB = ForeverUIDB or {} local saved = ForeverUIDB.statThirdCategory if saved and _G[saved] then return saved end local shown = {} shown[GetCVar and GetCVar("playerStatLeftDropdown") or ""] = true shown[GetCVar and GetCVar("playerStatRightDropdown") or ""] = true local _, classToken = UnitClass("player") local preferred = classToken and THIRD_PREFERRED[classToken] if preferred and _G[preferred] and not shown[preferred] then return preferred end for _, key in ipairs(STAT_CATEGORIES) do if _G[key] and not shown[key] then return key end end return STAT_CATEGORIES[1] end -- PlayerStatFrameLeftDropDown_Initialize / _OnClick for the third group local function initializeThird(self) local current = thirdCategory() for _, key in ipairs(STAT_CATEGORIES) do if _G[key] then local info = UIDropDownMenu_CreateInfo() info.text = _G[key] info.value = key info.checked = (key == current) and 1 or nil info.func = function(button) ForeverUIDB = ForeverUIDB or {} ForeverUIDB.statThirdCategory = button.value UIDropDownMenu_SetSelectedValue(_G["ForeverUIStatFrameThirdDropDown"], button.value) UpdatePaperdollStats("ForeverUIStatFrameThird", button.value) end UIDropDownMenu_AddButton(info) end end end -- Creates the third group once, in CharacterAttributesFrame like the client's two (the stats page -- shows and hides that frame) local function createThirdGroup() local parent = _G["CharacterAttributesFrame"] if not parent or _G["ForeverUIStatFrameThirdDropDown"] then return end for i = 1, STAT_PER_GROUP do CreateFrame("Frame", "ForeverUIStatFrameThird" .. i, parent, "StatFrameTemplate") end local dropdown = CreateFrame("Frame", "ForeverUIStatFrameThirdDropDown", parent, "UIDropDownMenuTemplate") UIDropDownMenu_Initialize(dropdown, initializeThird) UIDropDownMenu_SetSelectedValue(dropdown, thirdCategory()) if type(UpdatePaperdollStats) == "function" then UpdatePaperdollStats("ForeverUIStatFrameThird", thirdCategory()) end end -- Gives a category selector camelot's dropdown look (Tpl.MenuStyle1); the menu stays the client's -- Templates.lua loads after this file: the first pass, at load, comes too early; the skin is -- applied again each time the sheet opens. local function skinSelector(selector, width) if selector.foreverButton or not (ForeverUI.Templates and ForeverUI.Templates.MenuStyle1) then return end ForeverUI.Templates.MenuStyle1(selector, width, STAT_SELECTOR_HEIGHT) end -- Pressed state of the selectors while their list is open. The list closes through -- ToggleDropDownMenu or CloseDropDownMenus, so the list's own OnShow and OnHide are hooked. -- Only selectors with a foreverPress function react. function updateSelectorStates() local list = _G["DropDownList1"] for _, group in ipairs(STAT_GROUPS) do local selector = _G[group.selector] if selector and selector.foreverPress then selector.foreverPress(list and list:IsShown() and UIDROPDOWNMENU_OPEN_MENU == selector) end end end ForeverUI.CharacterStatTabsState = updateSelectorStates -- UIDropDownMenu_InitializeHelper sets the selector height to UIDROPDOWNMENU_BUTTON_HEIGHT * 2 -- on each open (40, as ForeverUI sets the constant to 20). The saved size is re-applied after -- UIDropDownMenu_Initialize, which calls it through securecall. local function reapplySelectorSize(frame) for _, group in ipairs(STAT_GROUPS) do local selector = _G[group.selector] if selector == frame and selector.foreverSize then selector:SetWidth(selector.foreverSize[1]) selector:SetHeight(selector.foreverSize[2]) return end end end ForeverUI.CharacterStatTabsSize = reapplySelectorSize if hooksecurefunc and type(UIDropDownMenu_Initialize) == "function" then hooksecurefunc("UIDropDownMenu_Initialize", reapplySelectorSize) end -- Category label: the CVar holds a key (PLAYERSTAT_BASE_STATS); the global of that name is the -- text. local function writeCategory(selector, key) if selector and key and _G[key] and UIDropDownMenu_SetText then UIDropDownMenu_SetText(selector, _G[key]) end end -- Background band of a stat row, created once. A child of the row, so it follows its -- visibility. local function applyRowBackground(row) if row.foreverBackground then return row.foreverBackground end local background = row:CreateTexture(nil, "BACKGROUND") background:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0) background:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0) row.foreverBackground = background -- The label is on the same layer and created first, and 3.3.5 has no sublevels: move it up so -- the band stays behind. local name = row:GetName() local label = name and _G[name .. "Label"] if label and label.SetDrawLayer then label:SetDrawLayer("ARTWORK") end return background end -- Stripes the rows of each category, counting only the rows the client shows (it hides some -- depending on the category). local function stripeStatistics() for _, group in ipairs(STAT_GROUPS) do local rank = 0 for index = 1, STAT_PER_GROUP do local row = _G[group.prefix .. index] if row and row.foreverBackground and row:IsShown() then rank = rank + 1 ForeverUI.SetAtlas(row.foreverBackground, (rank % 2 == 1) and ATLAS_STAT_DARK or ATLAS_STAT_LIGHT, true) end end end end ForeverUI.CharacterStatStripes = stripeStatistics -- Moves and widens the client's stat rows and selectors into the right pane. They are not -- recreated: they carry the tooltips and category menus. Geometry only: ForeverUI.Panes shows -- and hides them. local function placeStatistics() if not rightPane then return end local width = RIGHT_PANE - 2 * STAT_MARGIN local level = rightPane:GetFrameLevel() + 3 local y = -(STONE_HEIGHT + STAT_TOP) createThirdGroup() for _, group in ipairs(STAT_GROUPS) do local selector = _G[group.selector] if selector then skinSelector(selector, width) writeCategory(selector, group.third and thirdCategory() or (GetCVar and GetCVar(group.cvar))) -- Size kept: the client resets it each time the menu opens selector.foreverSize = { width, STAT_SELECTOR_HEIGHT } selector:SetFrameLevel(level) selector:SetWidth(selector.foreverSize[1]) selector:SetHeight(selector.foreverSize[2]) selector:ClearAllPoints() selector:SetPoint("TOPLEFT", rightPane, "TOPLEFT", STAT_MARGIN, y - STAT_SELECTOR_TOP) y = y - STAT_HEADER end for index = 1, STAT_PER_GROUP do local row = _G[group.prefix .. index] if row then row:SetFrameLevel(level) row:SetWidth(width) row:ClearAllPoints() row:SetPoint("TOPLEFT", rightPane, "TOPLEFT", STAT_MARGIN, y) applyRowBackground(row) y = y - STAT_STEP end end y = y - STAT_GROUP_GAP end stripeStatistics() end -- Level and class line. 3.3.5 FontStrings have no SetParent, and the client's line belongs to -- CharacterFrame, so it would draw under the panes (child frames). It is hidden and our own -- line, in the rock strip at the top of the left pane, shows the text. local function buildLevelStrip() if leftPane.levelStrip then return leftPane.levelStrip end local strip = CreateFrame("Frame", "ForeverUICharacterLevelStrip", leftPane) strip:SetPoint("TOPLEFT", leftPane, "TOPLEFT", 0, 0) strip:SetPoint("TOPRIGHT", leftPane, "TOPRIGHT", 0, 0) strip:SetHeight(STRIP_HEIGHT) -- Above the model (camelot: frameLevel 50 against the scene's 50, declared later), below the -- collapse button and the rotation arrows (model + 2) local model = _G["CharacterModelFrame"] strip:SetFrameLevel(((model and model:GetFrameLevel()) or leftPane:GetFrameLevel()) + 1) local rock = strip:CreateTexture(nil, "ARTWORK") rock:SetTexture(ROCK_FILE) rock:SetTexCoord(0, LEFT_PANE / ROCK_SIZE, 0, STRIP_HEIGHT / ROCK_SIZE) rock:SetAllPoints(strip) local edge = strip:CreateTexture(nil, "OVERLAY") ForeverUI.SetAtlas(edge, "_ui-frame-innertoptile", true) edge:SetHeight(STRIP_EDGE) edge:SetPoint("BOTTOMLEFT", strip, "BOTTOMLEFT", 0, 0) edge:SetPoint("BOTTOMRIGHT", strip, "BOTTOMRIGHT", 0, 0) local line = strip:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") local fontFile = line:GetFont() line:SetFont(fontFile, LEVEL_FONT_SIZE, "OUTLINE") line:SetPoint("CENTER", strip, "CENTER", 0, LEVEL_Y) line:SetWidth(LEVEL_WIDTH) line:SetHeight(LEVEL_HEIGHT) line:SetJustifyH("CENTER") line:SetJustifyV("MIDDLE") strip.rock, strip.edge, strip.text = rock, edge, line leftPane.levelStrip = strip return strip end local function placeLevel() local source = _G["CharacterLevelText"] local strip = buildLevelStrip() if source then source:Hide() end -- camelot PLAYER_LEVEL_NO_SPEC: "Level %d", then the class in its color. 3.3.5 has neither that -- string nor colorStr: built from UNIT_LEVEL_TEMPLATE and RAID_CLASS_COLORS, so it stays -- localized. local level = UnitLevel and UnitLevel("player") -- not "UnitClass and UnitClass(...)": "and" keeps only the first value, the token is lost local className, classToken if UnitClass then className, classToken = UnitClass("player") end local text = "" if level and className then local color = RAID_CLASS_COLORS and classToken and RAID_CLASS_COLORS[classToken] local name = className if color then local function byte(v) return math.floor(v * 255 + 0.5) end name = string.format("|cff%02x%02x%02x%s|r", byte(color.r), byte(color.g), byte(color.b), className) end text = string.format(UNIT_LEVEL_TEMPLATE, level) .. " " .. name elseif source then text = source:GetText() or "" end strip.text:SetText(text) end -- Creates a right pane tab: camelot's frame art, an icon under it and a selection mark. -- SetCheckedTexture only takes a file path in 3.3.5, not an atlas, so the mark is our own -- texture. local function createPaneTab(name, tooltip, onClick) local tab = CreateFrame("Button", name, rightPane) tab:SetWidth(PANE_TAB) tab:SetHeight(PANE_TAB) tab:SetFrameLevel(rightPane:GetFrameLevel() + 3) local icon = tab:CreateTexture(nil, "BACKGROUND") icon:SetWidth(PANE_TAB_ICON) icon:SetHeight(PANE_TAB_ICON) icon:SetPoint("CENTER", tab, "CENTER", 0, 0) tab.icon = icon local selected = tab:CreateTexture(nil, "BORDER") ForeverUI.SetAtlas(selected, ATLAS_PANE_TAB_SELECTED) selected:SetPoint("CENTER", tab, "CENTER", 0, 0) selected:Hide() tab.selected = selected local frame = tab:CreateTexture(nil, "ARTWORK") ForeverUI.SetAtlas(frame, ATLAS_PANE_TAB) frame:SetPoint("CENTER", tab, "CENTER", 0, 0) tab:SetScript("OnEnter", function(self) GameTooltip:SetOwner(self, "ANCHOR_RIGHT") GameTooltip:SetText(tooltip) GameTooltip:Show() end) tab:SetScript("OnLeave", function() GameTooltip:Hide() end) if onClick then tab:SetScript("OnClick", onClick) end return tab end -- Creates the three right pane tabs once and refreshes the stats tab portrait. ForeverUI.Panes -- remembers each host's open page, so collapsing and expanding keeps it. local function placePaneTabs() if not rightPane then return end if not rightPane.statsTab then -- The tabs only select the right host's page and move the mark; ForeverUI.Panes shows and -- hides. The gear manager stays the client's. local function choose(page) -- Legacy callers pass a boolean: true is stats, false is equipment if page == true then page = "stats" elseif page == false then page = "equipment" end local markers = { stats = rightPane.statsTab, equipment = rightPane.equipmentTab, titles = rightPane.titlesTab, } for name, tab in pairs(markers) do if tab then if name == page then tab.selected:Show() else tab.selected:Hide() end end end ForeverUI.Panes.ShowPage("right", page) end rightPane.selectPage = choose rightPane.statsTab = createPaneTab("ForeverUICharacterStatsTab", CHARACTER_INFO, function() choose("stats") end) rightPane.statsTab.selected:Show() rightPane.equipmentTab = createPaneTab( "ForeverUICharacterGearTab", EQUIPMENT_MANAGER, function() choose("equipment") end) rightPane.equipmentTab.icon:SetTexture(GEAR_MANAGER_ICON) rightPane.equipmentTab.icon:SetTexCoord( GEAR_MANAGER_ICON_COORD[1], GEAR_MANAGER_ICON_COORD[2], GEAR_MANAGER_ICON_COORD[3], GEAR_MANAGER_ICON_COORD[4]) rightPane.titlesTab = createPaneTab( "ForeverUICharacterTitlesTab", L.CHARACTERFRAME_TITLES, function() choose("titles") end) rightPane.titlesTab.icon:SetTexture(TITLES_ICON) rightPane.titlesTab.icon:SetTexCoord( TITLES_ICON_COORD[1], TITLES_ICON_COORD[2], TITLES_ICON_COORD[3], TITLES_ICON_COORD[4]) -- camelot PaperDollFrame_UpdateSidebarTabLayout: the middle tab holds the anchor -- at TOP (0, -5) of the bar, the others touch its sides. Stats left, gear manager -- middle, titles right. rightPane.equipmentTab:SetPoint("TOP", rightPane, "TOP", 0, PANE_TAB_Y) rightPane.statsTab:SetPoint("RIGHT", rightPane.equipmentTab, "LEFT", 0, 0) rightPane.titlesTab:SetPoint("LEFT", rightPane.equipmentTab, "RIGHT", 0, 0) end -- Portrait cropped as in camelot; re-applied since the client redraws it on appearance changes if SetPortraitTexture then SetPortraitTexture(rightPane.statsTab.icon, "player") rightPane.statsTab.icon:SetTexCoord(TAB_PORTRAIT_COORD[1], TAB_PORTRAIT_COORD[2], TAB_PORTRAIT_COORD[3], TAB_PORTRAIT_COORD[4]) end -- The client's gear manager button is hidden: the tab opens the panel local old = _G["GearManagerToggleButton"] if old then old:Hide() end end -- Side tab screens. 3.3.5 lists its five screens in CHARACTERFRAME_SUBFRAMES and -- CharacterFrame_ShowSubFrame shows one of them; the screen name is the ForeverUI.Panes group. -- The right pane stays open on every tab, like camelot (UpdateRightPaneHeader hides only the -- stone band); the character's stone band, page tabs and level line go with its group. local CHARACTER_SCREEN = "PaperDollFrame" local SIMPLE_SCREENS = { { group = "PetPaperDollFrame", id = "pet", module = "PetTab" }, { group = "ReputationFrame", id = "reputation", module = "ReputationTab" }, { group = "SkillFrame", id = "skills", module = "SkillsTab" }, { group = "TokenFrame", id = "currency", module = "TokensTab" }, } local contentsDeclared = false -- Registers every screen's left and right content with ForeverUI.Panes, once local function declareContents() if contentsDeclared or not rightPane then return end contentsDeclared = true local Panes = ForeverUI.Panes -- Left pane, character group: the model, slots and resistances are PaperDollFrame children the -- client shows and hides; this entry only marks the left host as used. Panes.Register({ host = "left", group = CHARACTER_SCREEN, id = "character", build = function() return nil, {} end, }) -- The four other client screens are sized for the narrower original window: they are anchored -- to the left host when built, unless their module lays them out. for _, screen in ipairs(SIMPLE_SCREENS) do Panes.Register({ host = "left", group = screen.group, id = screen.id, build = function(host) -- A screen with its own module (ForeverUI[screen.module]) builds its own content local module = ForeverUI[screen.module or ""] if module and module.Build then return module.Build(host) end local frame = _G[screen.group] if not frame then return nil, {} end frame:ClearAllPoints() frame:SetPoint("TOPLEFT", host, "TOPLEFT", 0, 0) return nil, { frame } end, }) end -- PvP: 3.3.5 has a separate toplevel window (PVPParentFrame, 384 x 512) where camelot has a -- sheet tab; ForeverUI.PvPTab moves it into the left pane. Reparenting is safe since it carries -- none of our level settings, and needed since a toplevel frame stays above everything. Panes.Register({ host = "left", group = PVP_SCREEN, id = "pvp", build = function(host) return ForeverUI.PvPTab.Build(host) end, }) -- Statistics: camelot's StatisticsFrame (StatisticsTab.lua) on the client's achievement -- statistics Panes.Register({ host = "left", group = STATS_SCREEN, id = "stats", build = function(host) return ForeverUI.StatisticsTab.Build(host) end, }) -- Mounts: the chosen one in 3D on the left, the list on the right (MountsTab.lua) Panes.Register({ host = "left", group = MOUNTS_SCREEN, id = "mounts", build = function(host) return ForeverUI.MountsTab.Build(host) end, }) Panes.Register({ host = "right", group = MOUNTS_SCREEN, id = MOUNTS_SCREEN .. ".droit", build = function(host) return ForeverUI.MountsTab.BuildRight(host) end, }) -- The PvP tab shows its rank details on the right; statistics leave the right pane empty Panes.Register({ host = "right", group = PVP_SCREEN, id = PVP_SCREEN .. ".droit", build = function(host) return ForeverUI.PvPTab.BuildRight(host) end, }) Panes.Register({ host = "right", group = STATS_SCREEN, id = STATS_SCREEN .. ".droit", build = function() return nil, {} end, }) -- Right pane furniture of the character group: shown whatever the page, hidden on other tabs -- (camelot: UpdateRightPaneHeader, HidePaperDollRightPane). Panes.Furniture("right", CHARACTER_SCREEN, { rightPane.stone, rightPane.statsTab, rightPane.equipmentTab, rightPane.titlesTab, }) -- The level strip belongs to the character group too (camelot: PaperDollFrame's own child) Panes.Furniture("left", CHARACTER_SCREEN, { buildLevelStrip() }) -- The four other client screens keep the right pane: a module may fill it (reputation shows the -- selected faction); an empty page still keeps the host open and the window wide. for _, screen in ipairs(SIMPLE_SCREENS) do Panes.Register({ host = "right", group = screen.group, id = screen.id .. ".droit", build = function(host) local module = ForeverUI[screen.module or ""] if module and module.BuildRight then return module.BuildRight(host) end return nil, {} end, }) end -- Right pane pages of the character group; the first registered opens by default Panes.Register({ host = "right", group = CHARACTER_SCREEN, id = "stats", build = function() -- The page owns CharacterAttributesFrame, parent of the two selectors and the twelve -- rows, not the rows: showing each row brought back those the client hides -- (UpdatePaperdollStats hides the sixth row of the ranged category), with the content -- of the category shown before. Hidden, it also hides the rows the client shows over -- the gear or titles page. return nil, { _G["CharacterAttributesFrame"] } end, }) Panes.Register({ host = "right", group = CHARACTER_SCREEN, id = "equipment", build = function() if ForeverUI.EquipmentPane then ForeverUI.EquipmentPane.Apply() end local panel = ForeverUI.EquipmentPane and ForeverUI.EquipmentPane.Frame() -- GearManagerDialog is a child of the panel but hidden by default: the page shows it return panel, { _G["GearManagerDialog"] } end, }) -- Titles page (camelot has no titles tab, see Titles.lua) Panes.Register({ host = "right", group = CHARACTER_SCREEN, id = "titles", build = function(host) if ForeverUI.TitlesPane and ForeverUI.TitlesPane.Build then return ForeverUI.TitlesPane.Build(host) end return nil, {} end, }) -- Open the group of the screen the client currently shows local isOpen = CHARACTER_SCREEN for _, name in ipairs(CHARACTERFRAME_SUBFRAMES or { CHARACTER_SCREEN }) do local frame = _G[name] if frame and frame:IsShown() then isOpen = name break end end Panes.ShowGroup(isOpen) end -- Follows the client's tab switch (CharacterFrame_ShowSubFrame): ForeverUI.Panes hides what -- belongs to the previous screen, including our right pane, which is not a PaperDollFrame child. local function trackScreen(name) if not contentsDeclared then return end ForeverUI.Panes.ShowGroup(name) if ForeverUI.CharacterUpdateActiveTab then ForeverUI.CharacterUpdateActiveTab() end if ForeverUI.CharacterApplyPanes then ForeverUI.CharacterApplyPanes(CharacterFrame) end end if hooksecurefunc and type(_G["CharacterFrame_ShowSubFrame"]) == "function" then hooksecurefunc("CharacterFrame_ShowSubFrame", trackScreen) end -- Shows the active marker on the tab of the open ForeverUI.Panes group; call it whenever the -- group changes. local function updateActiveTab() local isOpen = ForeverUI.Panes.CurrentGroup() for key, tab in pairs(tabs) do if tab.foreverActive then local ownScreen = TAB_SCREEN[key] or TAB_CREATED_SCREENS[key] if ownScreen and ownScreen == isOpen then tab.foreverActive:Show() else tab.foreverActive:Hide() end end end end ForeverUI.CharacterUpdateActiveTab = updateActiveTab -- Opens one of our screens. CharacterFrame_ShowSubFrame only knows its five frames (an unknown -- name hides them all), so the five are hidden here and our group is shown. local function openOwnScreen(group) -- PetPaperDollFrame is protected (secure companion buttons): the addon cannot hide it in -- combat, so while it is the screen shown our screens open only out of combat for _, name in ipairs(CHARACTERFRAME_SUBFRAMES or {}) do local frame = _G[name] if frame and frame:IsShown() and InCombatLockdown() and frame:IsProtected() then return end end for _, name in ipairs(CHARACTERFRAME_SUBFRAMES or {}) do local frame = _G[name] if frame and frame:IsShown() then frame:Hide() end end -- PanelTemplates_SelectTab disables the selected client tab and only PanelTemplates_DeselectTab -- (from ToggleCharacter) enables it again. Our screens use neither, so the client tabs are -- enabled here; otherwise the previous one stays unclickable. for index = 1, (CharacterFrame and CharacterFrame.numTabs) or 5 do local tab = _G["CharacterFrameTab" .. index] if tab and tab.Enable and not tab.isDisabled then tab:Enable() end end ForeverUI.Panes.ShowGroup(group) updateActiveTab() if ForeverUI.CharacterApplyPanes then ForeverUI.CharacterApplyPanes(CharacterFrame) end end -- The PvP window is a tab now: its key binding and micro button call TogglePVPFrame, whose -- ToggleFrame(PVPParentFrame) no longer shows anything. Open the sheet on the PvP tab instead. if hooksecurefunc and type(_G["TogglePVPFrame"]) == "function" then hooksecurefunc("TogglePVPFrame", function() if not CharacterFrame then return end if not CharacterFrame:IsShown() and ShowUIPanel then ShowUIPanel(CharacterFrame) end openOwnScreen(PVP_SCREEN) end) end -- Right pane collapse, kept across sessions in ForeverUIDB (camelot uses a CVar). -- Forward declaration: reapplyPosition is defined below; without it the name would resolve to -- a nil global when the button is clicked. local reapplyPosition local function isPaneCollapsed() ForeverUIDB = ForeverUIDB or {} return ForeverUIDB.rightPaneCollapsed == true end local function updateCollapseButton(frame) local button = frame and frame.foreverCollapse if not button then return end local collapsed = isPaneCollapsed() local art = collapsed and COLLAPSE_ART.collapsed or COLLAPSE_ART.expanded button:SetNormalTexture(art[1]) button:SetPushedTexture(art[2]) if collapsed then button.tooltip = L.CHARACTERFRAME_SHOW_DETAILS else button.tooltip = L.CHARACTERFRAME_HIDE_DETAILS end end -- Applies the right pane state and the window width. The right host is shown only when it has -- a page for the open group and is not collapsed; the window is then WIDTH wide, else -- LEFT_PANE. The side tabs are anchored to the window's right edge and follow it. local function applyCollapse(frame) if not frame then return end local Panes = ForeverUI.Panes local hasContentToShow = Panes.HasContent("right", Panes.CurrentGroup()) local isOpen = hasContentToShow and not isPaneCollapsed() Panes.SetHostShown("right", isOpen) -- CharacterFrame is protected (secure companion buttons in its pet screen): resized out of -- combat only, and only when the width changes. PLAYER_REGEN_ENABLED runs applySkin again. local width = isOpen and WIDTH or LEFT_PANE if not (InCombatLockdown() and frame:IsProtected()) and math.abs((frame:GetWidth() or 0) - width) > 0.5 then frame:SetWidth(width) end -- No collapse button on a tab without a right pane local button = frame.foreverCollapse if button then if hasContentToShow then button:Show() else button:Hide() end end updateCollapseButton(frame) if ForeverUI.UpdatePanelCorners then ForeverUI.UpdatePanelCorners(frame) end end ForeverUI.CharacterApplyPanes = applyCollapse local function togglePane() local frame = CharacterFrame if not frame or InCombatLockdown() then return end ForeverUIDB = ForeverUIDB or {} ForeverUIDB.rightPaneCollapsed = not isPaneCollapsed() if PlaySound then PlaySound(isPaneCollapsed() and "igCharacterInfoClose" or "igCharacterInfoOpen") end applyCollapse(frame) reapplyPosition(frame) -- The tooltip text changed under a still cursor: show it again, like camelot local button = frame.foreverCollapse if button and GameTooltip:GetOwner() == button then GameTooltip:SetOwner(button, "ANCHOR_RIGHT") GameTooltip:SetText(button.tooltip) GameTooltip:Show() end end local function placeCollapseButton(frame) if not leftPane then return end local button = frame.foreverCollapse if not button then button = CreateFrame("Button", "ForeverUICharacterRightPaneToggle", frame) button:SetWidth(COLLAPSE_SIZE) button:SetHeight(COLLAPSE_SIZE) button:SetPoint("TOPRIGHT", leftPane, "TOPRIGHT", COLLAPSE_X, COLLAPSE_Y) button:SetHighlightTexture(COLLAPSE_HOVER, "ADD") button:SetScript("OnClick", togglePane) button:SetScript("OnEnter", function(self) GameTooltip:SetOwner(self, "ANCHOR_RIGHT") GameTooltip:SetText(self.tooltip) GameTooltip:Show() end) button:SetScript("OnLeave", function() GameTooltip:Hide() end) frame.foreverCollapse = button end -- Above the metal and the model: the button sits in the corner the model covers, and at an -- equal level the model takes the mouse. Two levels above the model, like the rotation -- arrows, and never below the skin layer. local level = frame:GetFrameLevel() + ART_LEVEL + 2 local model = _G["CharacterModelFrame"] if model and model.GetFrameLevel then local wanted = model:GetFrameLevel() + 2 if wanted > level then level = wanted end end button:SetFrameLevel(level) updateCollapseButton(frame) end -- Side tabs: a 64 x 384 column right of the frame, 30 px down (camelot CharacterFrameModeTabs). -- Only visible tabs are stacked, each on the previous visible one: 3.3.5 hides the pet tab -- without a pet (PetPaperDollFrame_UpdateIsAvailable), which would leave a gap in a column. -- Restacked each time the client changes that state. local function stackTabs() if not tabBar then return end local previous for _, key in ipairs(TAB_ORDER) do local tab = tabs[key] if tab and tab:IsShown() then tab:ClearAllPoints() if previous then tab:SetPoint("TOPLEFT", previous, "BOTTOMLEFT", 0, TAB_GAP) else tab:SetPoint("TOPLEFT", tabBar, "TOPLEFT", 0, 0) end previous = tab end end end -- The pet tab is for the pet alone: 3.3.5 also offers it for companions and mounts. Without a -- pet it is hidden as the client hides it when there is nothing to show, then the column closes up. local function updatePetTab() local tab, screen = _G["CharacterFrameTab2"], _G["PetPaperDollFrame"] if tab and screen and not HasPetUI() then screen.hidden = true tab:Hide() -- ToggleCharacter hides the protected pet screen: not from here in combat if screen:IsVisible() and not InCombatLockdown() then ToggleCharacter("PaperDollFrame") end end stackTabs() end if hooksecurefunc and type(_G["PetPaperDollFrame_UpdateIsAvailable"]) == "function" then hooksecurefunc("PetPaperDollFrame_UpdateIsAvailable", updatePetTab) end -- A client tab shown again runs CharacterFrame_TabBoundsCheck, which resizes the five client -- tabs to their text, empty here: they get the side tab width back. local function keepTabWidth() for key, tab in pairs(tabs) do if type(key) == "number" and tab.foreverSkinned then tab:SetWidth(TAB_W) end end end if hooksecurefunc and type(_G["CharacterFrame_TabBoundsCheck"]) == "function" then hooksecurefunc("CharacterFrame_TabBoundsCheck", keepTabWidth) end -- Side tabs on the action bars (TabMacros.lua): a dragged tab becomes a macro whose body opens -- its screen; on a bar it shows the tab's icon. local TAB_MACRO_BODY = "/run ForeverUI.ToggleCharacterTab(%s)" local TAB_MACRO_NAMES = { [1] = "CHARACTER", [2] = "PET", [3] = "REPUTATION", [4] = "SKILLS", [5] = "CURRENCY", pvp = "PVP", stats = "STATISTICS", mounts = "MOUNTS", } -- Macro icon, seen on the cursor and in the macro window: the nearest game icon local TAB_MACRO_ICONS = { [1] = "INV_Chest_Cloth_17", [3] = "Achievement_Reputation_01", [4] = "Trade_Engineering", [5] = "INV_Misc_Coin_01", stats = "INV_Misc_Note_01", mounts = "Ability_Mount_RidingHorse", pvp = { Alliance = "INV_BannerPVP_02", Horde = "INV_BannerPVP_01" }, } local function tabMacroBody(key) return string.format(TAB_MACRO_BODY, type(key) == "number" and tostring(key) or ('"' .. key .. '"')) end -- The tab's icon, also before the sheet was ever opened (its tabs do not exist yet) local function tabIcon(key) local tab = tabs[key] local texture = tab and tab.foreverIcon and tab.foreverIcon:IsShown() and tab.foreverIcon:GetTexture() if texture then return texture end if key == 1 then local _, token = UnitClass("player") return token and (CLASS_TAB .. token) elseif key == "pvp" then local faction = (UnitFactionGroup and UnitFactionGroup("player")) or "Alliance" return TAB_ICONS.pvp[faction] or TAB_ICONS.pvp.Alliance end return TAB_ICONS[key] end local function tabMacroIcon(key) local wanted = TAB_MACRO_ICONS[key] if key == 2 then return PET_TAB[select(2, UnitClass("player")) or ""] or PET_TAB.HUNTER elseif type(wanted) == "table" then return wanted[(UnitFactionGroup and UnitFactionGroup("player")) or "Alliance"] or wanted.Alliance end return wanted end local function tabMacroInfo(key) return { name = function() return _G[TAB_MACRO_NAMES[key]] or TAB_MACRO_NAMES[key] end, icon = function() return tabIcon(key) end, macroIcon = function() return tabMacroIcon(key) end, } end -- The bars show the tabs' icons from login, before the sheet is opened for key in pairs(TAB_MACRO_NAMES) do ForeverUI.TabMacros.Register(tabMacroBody(key), tabMacroInfo(key)) end local function enableTabDrag(tab, key) if tab.foreverDrag then return end ForeverUI.TabMacros.EnableDrag(tab, tabMacroBody(key), tabMacroInfo(key)) tab.foreverDrag = true end -- Opens the tab's screen, or closes the sheet when it already shows it (ToggleCharacter's rule) function ForeverUI.ToggleCharacterTab(key) local frame = CharacterFrame if not frame then return end if type(key) == "number" then local screen = TAB_SCREEN[key] if not screen or (key == 2 and not HasPetUI()) then return end -- The pet screen is protected: the client cannot show or hide it from a macro in combat local pet = _G["PetPaperDollFrame"] if InCombatLockdown() and (key == 2 or (pet and pet:IsVisible())) then UIErrorsFrame:AddMessage(ERR_NOT_IN_COMBAT, 1, 0.1, 0.1, 1) return end ToggleCharacter(screen) return end local group = TAB_CREATED_SCREENS[key] if not group or (key == "mounts" and GetNumCompanions("MOUNT") == 0) then return end if frame:IsShown() and ForeverUI.Panes.CurrentGroup() == group then HideUIPanel(frame) return end if not frame:IsShown() then ShowUIPanel(frame) end openOwnScreen(group) end -- Creates one of our side tabs, skinned like the client's. -- key: tabs table key; name: frame name; group: ForeverUI.Panes group it opens local function createSideTab(key, name, icon, tooltip, group) if tabs[key] then return tabs[key] end local tab = CreateFrame("Button", name, tabBar) tab:SetWidth(TAB_W) tab:SetHeight(TAB_H) local background = tab:CreateTexture(nil, "BACKGROUND") ForeverUI.SetAtlas(background, ATLAS.tab, true) background:SetAllPoints(tab) tab.foreverBackground = background local image = tab:CreateTexture(nil, "ARTWORK") image:SetWidth(SIDE_TAB_ICON) image:SetHeight(SIDE_TAB_ICON) image:SetPoint("CENTER", tab, "CENTER", TAB_ICON_X, 0) image:SetTexCoord(TAB_PORTRAIT_INSET, 1 - TAB_PORTRAIT_INSET, TAB_PORTRAIT_INSET, 1 - TAB_PORTRAIT_INSET) image:SetTexture(icon) tab.foreverIcon = image -- Active marker over the icon (camelot OVERLAY); the hover stays on its own HIGHLIGHT layer local active = tab:CreateTexture(nil, "OVERLAY") ForeverUI.SetAtlas(active, ATLAS.tabActive, true) active:SetAllPoints(tab) active:Hide() tab.foreverActive = active local hover = tab:CreateTexture(nil, "HIGHLIGHT") ForeverUI.SetAtlas(hover, ATLAS.tabHover, true) hover:SetAllPoints(tab) tab:SetScript("OnEnter", function(self) GameTooltip:SetOwner(self, "ANCHOR_RIGHT") GameTooltip:SetText(tooltip) GameTooltip:Show() end) tab:SetScript("OnLeave", function() GameTooltip:Hide() end) tab:SetScript("OnClick", function() if PlaySound then PlaySound("igCharacterInfoTab") end openOwnScreen(group) end) tab.foreverSkinned = true tabs[key] = tab enableTabDrag(tab, key) return tab end -- The mounts tab shows only with mounts, as the pet tab only with a pet. Losing the last one -- while its screen is open brings back the character page. local function updateMountsTab() local tab = tabs.mounts if not tab then return end if GetNumCompanions("MOUNT") > 0 then tab:Show() else tab:Hide() if ForeverUI.Panes.CurrentGroup() == MOUNTS_SCREEN and CharacterFrame:IsShown() then ToggleCharacter("PaperDollFrame") end end end -- Creates our PvP, statistics and mounts side tabs, sets their level and the faction icon local function placeCreatedTabs() if not tabBar then return end local faction = (UnitFactionGroup and UnitFactionGroup("player")) or "Alliance" createSideTab("pvp", "ForeverUICharacterTabPvP", TAB_ICONS.pvp[faction] or TAB_ICONS.pvp.Alliance, PVP, PVP_SCREEN) createSideTab("stats", "ForeverUICharacterTabStats", TAB_ICONS.stats, STATISTICS, STATS_SCREEN) createSideTab("mounts", "ForeverUICharacterTabMounts", TAB_ICONS.mounts, MOUNTS, MOUNTS_SCREEN) updateMountsTab() -- Same level rule as the client tabs: below the metal local tabsLevel = tabBar:GetFrameLevel() for _, key in ipairs({ "pvp", "stats", "mounts" }) do if tabs[key] then tabs[key]:SetFrameLevel(tabsLevel + 1) end end -- The tab may be created before the client knows the faction: set the icon on each pass if tabs.pvp and faction then tabs.pvp.foreverIcon:SetTexture( TAB_ICONS.pvp[faction] or TAB_ICONS.pvp.Alliance) end end -- A mount learned or forgotten, or the list arriving after login local mountsWatcher = CreateFrame("Frame") for _, event in ipairs({ "PLAYER_ENTERING_WORLD", "COMPANION_LEARNED", "COMPANION_UNLEARNED", "COMPANION_UPDATE" }) do mountsWatcher:RegisterEvent(event) end mountsWatcher:SetScript("OnEvent", function() updateMountsTab() stackTabs() end) -- Skins the client side tabs and stacks all side tabs in the column right of the frame local function layoutTabs(frame) if not tabBar then tabBar = CreateFrame("Frame", "ForeverUICharacterModeTabs", frame) tabBar:SetWidth(TABS_W) tabBar:SetHeight(TABS_H) tabBar:SetPoint("TOPLEFT", frame, "TOPRIGHT", TABS_X, TABS_Y) end -- The tab art has a tongue on its left that slides under the window; its hover and active -- marker would overlap the right pane's metal. The bar sits at the frame level and the tabs -- one level above: over the panel background, below the skin layer (ART_LEVEL). local tabsLevel = frame:GetFrameLevel() tabBar:SetFrameLevel(tabsLevel) local previous local index = 1 while true do local tab = _G["CharacterFrameTab" .. index] if not tab then break end if not tab.foreverSkinned then sweepTextures(tab) for _, method in ipairs({ "GetNormalTexture", "GetPushedTexture", "GetDisabledTexture", "GetHighlightTexture" }) do local texture = tab[method] and tab[method](tab) if texture then texture:SetAlpha(0) end end local background = tab:CreateTexture(nil, "BACKGROUND") ForeverUI.SetAtlas(background, ATLAS.tab, true) background:SetAllPoints(tab) tab.foreverBackground = background local icon = tab:CreateTexture(nil, "ARTWORK") icon:SetWidth(SIDE_TAB_ICON) icon:SetHeight(SIDE_TAB_ICON) icon:SetPoint("CENTER", tab, "CENTER", TAB_ICON_X, 0) icon:SetTexCoord(TAB_PORTRAIT_INSET, 1 - TAB_PORTRAIT_INSET, TAB_PORTRAIT_INSET, 1 - TAB_PORTRAIT_INSET) icon:Hide() tab.foreverIcon = icon local active = tab:CreateTexture(nil, "OVERLAY") ForeverUI.SetAtlas(active, ATLAS.tabActive, true) active:SetAllPoints(tab) active:Hide() tab.foreverActive = active local hover = tab:CreateTexture(nil, "HIGHLIGHT") ForeverUI.SetAtlas(hover, ATLAS.tabHover, true) hover:SetAllPoints(tab) -- The icon replaces the text when there is one; otherwise the client text -- stays, centered like the icon local text = _G["CharacterFrameTab" .. index .. "Text"] if text then text:ClearAllPoints() text:SetPoint("CENTER", tab, "CENTER", TAB_ICON_X, 0) text:SetWidth(TAB_W - 10) end if TAB_ICONS[index] then icon:SetTexture(TAB_ICONS[index]) icon:Show() if text then text:Hide() text:SetText("") end end tab.foreverSkinned = true end tab:SetWidth(TAB_W) tab:SetHeight(TAB_H) tab:SetFrameLevel(tabsLevel + 1) tabs[index] = tab enableTabDrag(tab, index) index = index + 1 end placeCreatedTabs() stackTabs() updateActiveTab() -- The character tab shows the class icon local first = tabs[1] local _, token = UnitClass("player") if first and first.foreverIcon and token then first.foreverIcon:SetTexture(CLASS_TAB .. token) -- Keep the crop: the mask was baked for it first.foreverIcon:SetTexCoord(TAB_PORTRAIT_INSET, 1 - TAB_PORTRAIT_INSET, TAB_PORTRAIT_INSET, 1 - TAB_PORTRAIT_INSET) first.foreverIcon:Show() local text = _G["CharacterFrameTab1Text"] if text then text:Hide() text:SetText("") end end end -- The client's panel manager re-places the window on each show: re-apply the saved position -- after it function reapplyPosition(frame) local position = savedPositions()["sheet"] if not position or InCombatLockdown() then return end frame:ClearAllPoints() frame:SetPoint(position.point, UIParent, position.relativePoint or position.point, position.x or 0, position.y or 0) end -- GearManagerDialog's OnShow and OnHide call UpdateUIPanelPositions, which puts the sheet back -- at its default place. Hook UpdateUIPanelPositions itself, which covers every caller. if hooksecurefunc and type(UpdateUIPanelPositions) == "function" then hooksecurefunc("UpdateUIPanelPositions", function() if CharacterFrame and CharacterFrame:IsShown() then reapplyPosition(CharacterFrame) end end) end -- Skins and lays out the whole sheet; runs on each show and client update, out of combat local function applySkin() local frame = CharacterFrame if not frame or InCombatLockdown() or not libraryPresent() then return end -- The width is set by applyCollapse at the end, after the stats and gear panel are placed; -- otherwise they would show again. frame:SetHeight(HEIGHT) if not frame.foreverSkinned then -- Order matters: the panes are child frames and cover their parent's regions, so the panel -- art lives in a higher child frame, like camelot's NineSlice. clearLegacyArt() setupPanes(frame) ForeverUI.SetPanelArt(frame, { topLeftCorner = PORTRAIT_CORNER, corners = PANEL_CORNERS, level = ART_LEVEL, }) frame.foreverSkinned = true -- Brought to the front when shown or clicked (WindowStack.lua) ForeverUI.WindowStack.register("sheet", frame, function() -- The arena team details open next to the sheet (PvPArena.lua): a click on -- them counts as a click on the sheet return { frame, tabBar, _G["ForeverUIArenaTeamDetails"] } end) end clearLegacyArt() if ForeverUI.UpdatePanelCorners then ForeverUI.UpdatePanelCorners(frame) end placePortrait(frame) placeTitle(frame) reapplyPosition(frame) placeCloseButton(frame) placeModel() placeResistances() placeStatistics() placeLevel() placePaneTabs() -- The gear manager panel is built with its page by ForeverUI.Panes, not on each pass; its -- list updates itself. placeSlots() layoutTabs(frame) placeCollapseButton(frame) declareContents() applyCollapse(frame) end -- The client calls UpdatePaperdollStats(prefix, key) when a category changes: update the -- matching header label local refreshingThird = false local function trackCategory(prefix, key) -- The client refreshes its two groups on every stat change (PaperDollFrame_UpdateStats): the -- third follows, from the same call if not refreshingThird and (prefix == "PlayerStatFrameLeft" or prefix == "PlayerStatFrameRight") and _G["ForeverUIStatFrameThirdDropDown"] then refreshingThird = true UpdatePaperdollStats("ForeverUIStatFrameThird", thirdCategory()) refreshingThird = false end for _, group in ipairs(STAT_GROUPS) do if group.prefix == prefix then writeCategory(_G[group.selector], key) -- The client has just shown and hidden rows: restripe the visible ones. The rows it -- shows while another page is open stay invisible: the stats page hides their parent. stripeStatistics() return end end end ForeverUI.CharacterSheet = { Apply = applySkin, Tabs = tabs } if hooksecurefunc and type(_G["UpdatePaperdollStats"]) == "function" then hooksecurefunc("UpdatePaperdollStats", trackCategory) end if hooksecurefunc then for _, funcName in ipairs({ "CharacterFrame_ShowSubFrame", "PaperDollFrame_OnShow", "PaperDollFrame_SetLevel", "CharacterFrame_Collapse", "CharacterFrame_Expand" }) do if type(_G[funcName]) == "function" then hooksecurefunc(funcName, applySkin) end end end local listener = CreateFrame("Frame", "ForeverUICharacterWatcher") listener:RegisterEvent("PLAYER_ENTERING_WORLD") listener:RegisterEvent("PLAYER_REGEN_ENABLED") listener:RegisterEvent("UNIT_INVENTORY_CHANGED") listener:RegisterEvent("UNIT_PORTRAIT_UPDATE") listener:RegisterEvent("UNIT_MODEL_CHANGED") -- A title change reaches UnitPVPName only after the server answers (UNIT_NAME_UPDATE). The -- event fires for every unit: only the title strip is redrawn, and only for the player. listener:RegisterEvent("UNIT_NAME_UPDATE") listener:SetScript("OnEvent", function(_self, event, unit) if event == "UNIT_NAME_UPDATE" then if unit == "player" and CharacterFrame then placeTitle(CharacterFrame) end return end -- The UNIT_ events fire for every unit (party, raid, target...): only the player's concern -- the sheet, and only while it is shown (its OnShow hook lays it out again anyway) if unit and (unit ~= "player" or not (CharacterFrame and CharacterFrame:IsShown())) then return end applySkin() end) if CharacterFrame then CharacterFrame:HookScript("OnShow", applySkin) end applySkin() -- Debug helpers: find the frames that take the mouse over a point local function nameOf(frame) if not frame then return "?" end return (frame.GetName and frame:GetName()) or L.CHARACTERFRAME_DEBUG_UNNAMED end local function containsPoint(frame, x, y) if not frame.GetLeft then return false end local g, d, h, b = frame:GetLeft(), frame:GetRight(), frame:GetTop(), frame:GetBottom() return g and d and h and b and x >= g and x <= d and y >= b and y <= h end -- Collects the visible, mouse-enabled frames containing (x, y) in the subtree of frame local function browse(frame, x, y, matches, depth) if not frame or depth > 6 then return end if frame.IsVisible and frame:IsVisible() and frame.IsMouseEnabled and frame:IsMouseEnabled() and containsPoint(frame, x, y) then matches[#matches + 1] = frame end if frame.GetChildren then local children = { frame:GetChildren() } for _, child in ipairs(children) do browse(child, x, y, matches, depth + 1) end end end -- Whether a tab is enabled: IsEnabled returns a number in 3.3.5, and 0 is true in Lua. local function isActive(tab) if not tab or not tab.IsEnabled then return true end local state = tab:IsEnabled() return state ~= nil and state ~= false and state ~= 0 end -- /fui tabs: why a side tab ignores clicks. Prints the client screens shown, then each tab's -- state and the mouse-enabled frames covering it, then the frame under the cursor for 5 s. function ForeverUI.CharacterTabsDebug() local say = function(text) DEFAULT_CHAT_FRAME:AddMessage("|cff66ccffForeverUI|r " .. text) end if not CharacterFrame or not CharacterFrame:IsShown() then say(L.CHARACTERFRAME_DEBUG_TABS_OPEN_FIRST) return end -- Client screens still shown: if one is while our screen is open, ToggleCharacter closes the -- window instead of switching tabs. local shownItems = {} for _, name in ipairs(CHARACTERFRAME_SUBFRAMES or {}) do local frame = _G[name] if frame and frame:IsShown() then shownItems[#shownItems + 1] = name end end say(string.format(L.CHARACTERFRAME_DEBUG_SHOWN_SCREENS, (#shownItems > 0) and table.concat(shownItems, ", ") or L.CHARACTERFRAME_DEBUG_NONE, tostring(ForeverUI.Panes and ForeverUI.Panes.CurrentGroup and ForeverUI.Panes.CurrentGroup()))) local column = {} for _, key in ipairs(TAB_ORDER) do local tab if type(key) == "number" then tab = _G["CharacterFrameTab" .. key] else tab = tabs[key] end if tab then column[#column + 1] = tab end end for _, tab in ipairs(column) do local left = tab.GetLeft and tab:GetLeft() say(string.format(L.CHARACTERFRAME_DEBUG_TAB_STATE, nameOf(tab), tostring(tab:IsVisible()), tostring(tab:IsMouseEnabled()), tostring(isActive(tab)), tostring(tab:GetFrameStrata()), tab:GetFrameLevel(), tostring(tab:GetScript("OnClick") ~= nil))) if left then local x = (tab:GetLeft() + tab:GetRight()) / 2 local y = (tab:GetTop() + tab:GetBottom()) / 2 local matches = {} -- From UIParent: a covering frame need not be related to the tab browse(UIParent, x, y, matches, 0) local names = {} for _, frame in ipairs(matches) do if frame ~= tab then names[#names + 1] = string.format("%s(%d)", nameOf(frame), frame:GetFrameLevel()) end end if #names > 0 then DEFAULT_CHAT_FRAME:AddMessage(L.CHARACTERFRAME_DEBUG_COVERED_BY .. table.concat(names, ", ")) end end end local lookout = ForeverUI.mouseLookout if lookout and lookout.SetScript then lookout.rest = 5 lookout.last = nil lookout:Show() end say(L.CHARACTERFRAME_DEBUG_HOVER_TABS) end -- Close button probe helpers. Edges are compared in screen pixels (edge x effective scale) -- since frames have different scales. Visibility counts, not the mouse: the metal draws over -- the button without taking the mouse. local function containsScreenPoint(frame, x, y) if not frame.GetLeft or not frame.GetEffectiveScale then return false end local g, d, h, b = frame:GetLeft(), frame:GetRight(), frame:GetTop(), frame:GetBottom() if not (g and d and h and b) then return false end local s = frame:GetEffectiveScale() return x >= g * s and x <= d * s and y >= b * s and y <= h * s end local function visibleUnder(frame, x, y, matches, depth) if not frame or depth > 10 then return end if frame.IsVisible and frame:IsVisible() and containsScreenPoint(frame, x, y) then matches[#matches + 1] = frame end if frame.GetChildren then for _, child in ipairs({ frame:GetChildren() }) do visibleUnder(child, x, y, matches, depth + 1) end end end -- /fui close: records the close button, its textures, the skin and title levels, the highest -- accepted frame level and every visible frame over its center into -- ForeverUIDB.closeButtonProbe (written to disk on the next /reload). function ForeverUI.CharacterCloseDebug() local r = {} local function note(...) local t = {} for i = 1, select("#", ...) do t[i] = tostring((select(i, ...))) end r[#r + 1] = table.concat(t, " ") end local c, b = CharacterFrame, _G["CharacterFrameCloseButton"] note("feuille shown", c:IsShown(), "level", c:GetFrameLevel(), "strata", c:GetFrameStrata(), "alpha", c:GetAlpha()) if b then note("croix shown", b:IsShown(), "visible", b:IsVisible(), "alpha", b:GetAlpha(), "level", b:GetFrameLevel(), "strata", b:GetFrameStrata(), "parent", nameOf(b:GetParent()), "w", b:GetWidth(), "h", b:GetHeight(), "left", b:GetLeft(), "top", b:GetTop(), "scale", b:GetEffectiveScale(), "mouse", b:IsMouseEnabled()) for _, state in ipairs({ "Normal", "Pushed", "Disabled", "Highlight" }) do local read = b["Get" .. state .. "Texture"] local t = read and read(b) if t then note(state, "tex", t:GetTexture(), "coords", table.concat({ t:GetTexCoord() }, ","), "shown", t:IsShown(), "alpha", t:GetAlpha(), "layer", t.GetDrawLayer and t:GetDrawLayer(), "w", t:GetWidth(), "h", t:GetHeight(), "left", t:GetLeft(), "top", t:GetTop()) else note(state, "nil") end end end local h, title = c.foreverSkinLayer, c.foreverTitleStrip note("habillage level", h and h:GetFrameLevel(), "visible", h and h:IsVisible()) note("titre level", title and title:GetFrameLevel()) -- highest accepted frame level: probe with a very high value local probe = ForeverUI.levelProbe or CreateFrame("Frame") ForeverUI.levelProbe = probe local ok = pcall(probe.SetFrameLevel, probe, 100000) note("niveau-max", ok, probe:GetFrameLevel()) if b and b:GetLeft() then local s = b:GetEffectiveScale() local x = (b:GetLeft() + b:GetRight()) / 2 * s local y = (b:GetTop() + b:GetBottom()) / 2 * s local matches = {} visibleUnder(UIParent, x, y, matches, 0) for _, f in ipairs(matches) do note("containsPoint", nameOf(f), "parent", nameOf(f:GetParent()), "strata", f:GetFrameStrata(), "level", f:GetFrameLevel()) end end ForeverUIDB = ForeverUIDB or {} ForeverUIDB.closeButtonProbe = r DEFAULT_CHAT_FRAME:AddMessage("|cff66ccffForeverUI|r " .. L.CHARACTERFRAME_DEBUG_CLOSE_SAVED) end -- /fui character: prints the head slot state and every mouse-enabled frame over its center -- (strata and level decide the click), then the frame under the cursor for 5 s. function ForeverUI.CharacterSheetDebug() -- Each host's content first: most display faults of this window show there if ForeverUI.Panes and ForeverUI.Panes.Report then for _, row in ipairs(ForeverUI.Panes.Report()) do DEFAULT_CHAT_FRAME:AddMessage("|cff66ccffForeverUI|r " .. L.CHARACTERFRAME_DEBUG_PANES .. row) end end local say = function(text) DEFAULT_CHAT_FRAME:AddMessage("|cff66ccffForeverUI|r " .. text) end local button = _G["CharacterHeadSlot"] if not button or not button:GetLeft() then say(L.CHARACTERFRAME_DEBUG_SHEET_OPEN_FIRST) return end say(string.format(L.CHARACTERFRAME_DEBUG_HEAD_STATE, button:GetWidth(), button:GetHeight(), tostring(button:GetFrameStrata()), button:GetFrameLevel(), tostring(button:IsMouseEnabled()), tostring(button:IsVisible()))) -- A button may also have lost its scripts or been disabled DEFAULT_CHAT_FRAME:AddMessage(string.format( L.CHARACTERFRAME_DEBUG_HEAD_SCRIPTS, tostring(button:GetScript("OnClick") ~= nil), tostring(button:GetScript("OnDragStart") ~= nil), tostring(button:GetScript("OnReceiveDrag") ~= nil), tostring(not button.IsEnabled or button:IsEnabled() and true or false))) local x = (button:GetLeft() + button:GetRight()) / 2 local y = (button:GetTop() + button:GetBottom()) / 2 local matches = {} browse(CharacterFrame, x, y, matches, 0) DEFAULT_CHAT_FRAME:AddMessage(L.CHARACTERFRAME_DEBUG_COVERING) for _, frame in ipairs(matches) do DEFAULT_CHAT_FRAME:AddMessage(string.format(L.CHARACTERFRAME_DEBUG_COVERING_ROW, nameOf(frame), tostring(frame:GetFrameStrata()), frame:GetFrameLevel())) end local lookout = ForeverUI.mouseLookout if not lookout then lookout = CreateFrame("Frame", "ForeverUICharacterMouseWatch") lookout:Hide() ForeverUI.mouseLookout = lookout end lookout.rest = 5 lookout.last = nil lookout:SetScript("OnUpdate", function(self, elapsed) self.rest = self.rest - elapsed if self.rest <= 0 then self:Hide() self:SetScript("OnUpdate", nil) DEFAULT_CHAT_FRAME:AddMessage(L.CHARACTERFRAME_DEBUG_WATCH_END) return end local sub = GetMouseFocus and GetMouseFocus() local name = nameOf(sub) if name ~= self.last then self.last = name DEFAULT_CHAT_FRAME:AddMessage(L.CHARACTERFRAME_DEBUG_UNDER_CURSOR .. name) end end) lookout:Show() say(L.CHARACTERFRAME_DEBUG_HOVER_SLOT) end -- /fui model: in-game tuning of the model scale and position (camelot uses a scene, so there -- is no source value). Sets the values and prints the result, without a reload. -- /fui model current values -- /fui model scale 0.8 SetModelScale -- /fui model position -5 0 0 SetPosition(depth, side, height) -- /fui model position default back to the client's position function ForeverUI.CharacterModelTune(argument) local say = function(text) DEFAULT_CHAT_FRAME:AddMessage("|cff66ccffForeverUI|r " .. text) end local model = _G["CharacterModelFrame"] if not model then return end local key, rest = string.match(argument or "", "^(%S*)%s*(.*)$") key = string.lower(key or "") local setting = ForeverUI.ModelSetting if key == "scale" then setting.scale = tonumber(rest) or setting.scale elseif key == "position" then if string.lower(rest) == "default" then setting.position = nil model:RefreshUnit() else local x, y, z = string.match(rest, "^(%-?[%d%.]+)%s+(%-?[%d%.]+)%s+(%-?[%d%.]+)$") if x then setting.position = { tonumber(x), tonumber(y), tonumber(z) } else say(L.CHARACTERFRAME_MODEL_POSITION_USAGE) return end end elseif key ~= "" then say(L.CHARACTERFRAME_MODEL_USAGE) return end if ForeverUI.CharacterSheet then ForeverUI.CharacterSheet.Apply() end local scale = model.GetModelScale and model:GetModelScale() local x, y, z = nil, nil, nil if model.GetPosition then x, y, z = model:GetPosition() end say(string.format(L.CHARACTERFRAME_MODEL_STATE, tostring(scale), tostring(x), tostring(y), tostring(z))) end