1f1cd55486
The character sheet's tab macros move to TabMacros.lua, which any addon or AIO window can call (Register, Pickup, EnableDrag); ActionBar's ActionButton_Update hook shows each registered tab's icon. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2506 lines
88 KiB
Lua
2506 lines
88 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. 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 |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 (/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
|