ca2b78e217
/fui keeps edit mode, reset and list. The thirty diagnostic and tuning functions behind the other subcommands (bar, bags, character, tabs, close, model, minimap, mouse, map, questlog, chat, chatlines, tabard...) go with them, with Panes.Report, the bags read-back and the 205 texts they alone used. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2182 lines
77 KiB
Lua
2182 lines
77 KiB
Lua
-- 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.
|
|
|
|
-- 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 |c<class color>Class|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; 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()
|