Files
moon-well-ui/data/addon/ForeverUI/BottomBar.lua
T
Adrestie 7eff45cdf7 ForeverUI : barre de véhicule
Dans un véhicule, la barre d'action, les griffons, le bandeau des
micro-boutons, le cadre des sacs et les barres d'expérience et de
réputation disparaissent (pilotes sécurisés [vehicleui], valables en
combat). Les micro-boutons passent dans la place que la barre du
véhicule leur garde, sur deux rangées, à l'échelle 0,85 de Camelot ou
moins pour tenir, quel que soit leur nombre ; ils reprennent leur place
et leur taille à la sortie. Les barres de droite gardent leur ajustement
tant que le véhicule cache le griffon.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 11:21:52 +02:00

1029 lines
38 KiB
Lua

-- Bottom of the screen: the micro menu and the bags bar, rebuilt from camelot.
-- Sources: camelot/MainMenuBarMicroMenu.xml, MicroMenuContainerOverrides.lua (order),
-- MainMenuBarBagButtons.xml, shared/BagsBar.lua, EditModePresetLayoutConstants.lua (positions).
-- The client's PvP and Help micro buttons are removed; the strip is as wide as its buttons.
-- camelot/MainMenuBarMicroMenu.xml: 32 x 40 buttons, childXPadding = -5 (5 px overlap)
local MICRO_W, MICRO_H = 32, 40
local MICRO_PADDING = -5
local MICRO_PITCH = MICRO_W + MICRO_PADDING
-- Displayed button height: the 40 px strip plus its 8 px frame on each side (56),
-- minus the 5 px bronze edge on each side, leaves a 46 px opening.
-- Camelot's 41 px art is stretched to fill it.
local MICRO_ART_H = 46
-- camelot/MainMenuBarBagButtons.xml: bags 45 x 45, bagPadding = 2, bag frame 46 x 46,
-- keyring 33 wide
local BAG_SIZE = 45
local BAG_PADDING = 2
local BAG_FRAME_W, BAG_FRAME_H = 46, 46
local KEYRING_W = 33
-- camelot/EditModePresetLayoutConstants.lua: micro menu at BOTTOM (116.5, 6); action bar
-- BOTTOMRIGHT on its BOTTOMLEFT (-4.5, -4); bags BOTTOMLEFT on its BOTTOMRIGHT (7, -4)
local MICRO_X, MICRO_Y = 116.5, 6
local BAR_OFFSET_X, BAR_OFFSET_Y = -4.5, -4
local BAGS_OFFSET_X, BAGS_OFFSET_Y = 7, -4
-- In a vehicle the buttons go in two rows (camelot OverrideMicroMenuPosition: stacked, the
-- strip's padding between rows too) at camelot's 0.85 scale, smaller if needed to fit the room
-- the vehicle bar keeps for them. That room, per skin, from VehicleMenuBar's BOTTOMRIGHT: between
-- the borders around it (VehicleMenuBar.lua SkinsData), above the bar's bottom, under its top border.
local VEHICLE_MICRO_SCALE = 0.85
local VEHICLE_MICRO_AREA = {
Mechanical = { left = -335, right = -219, bottom = 3, top = 77 },
Natural = { left = -363, right = -237, bottom = 3, top = 77 },
}
-- Hidden while the vehicle bar replaces the player's bar (secure driver: works in combat)
local VEHICLE_HIDDEN = "[vehicleui] hide; show"
-- Backslash built with string.char, so the path needs no escaped separators.
local SEP = string.char(92)
local BAG_ICON = "Interface" .. SEP .. "ForeverUI" .. SEP .. "icons" .. SEP .. "ui-hud-actionbar-bag"
local PERFORMANCE_IMAGE = "Interface" .. SEP .. "ForeverUI" .. SEP .. "mainmenubar" .. SEP .. "ui-mainmenubar-performancebar"
local L = ForeverUI.L
-- Camelot's order, limited to the buttons this client has.
-- The professions button (camelot ProfessionMicroButton) does not exist in 3.3.5: it is
-- created here and opens ProfessionsBook.lua.
-- create: role of a created button, which picks its tooltip and click (MICRO_ROLES).
-- Other addons add their own buttons with ForeverUI.AddMicroButton (below).
local MICRO = {
{ name = "CharacterMicroButton", portrait = true },
{ name = "ForeverUIProfessionMicroButton", atlasSet = "professions", create = "professions" },
{ name = "SpellbookMicroButton", atlasSet = "spellbookabilities" },
{ name = "TalentMicroButton", atlasSet = "spectalents" },
{ name = "AchievementMicroButton", atlasSet = "achievements" },
{ name = "QuestLogMicroButton", atlasSet = "questlog" },
{ name = "SocialsMicroButton", atlasSet = "guildcommunities" },
{ name = "LFDMicroButton", atlasSet = "groupfinder" },
{ name = "MainMenuMicroButton", atlasSet = "gamemenu" },
}
local MICRO_STATES = {
up = "GetNormalTexture",
down = "GetPushedTexture",
disabled = "GetDisabledTexture",
mouseover = "GetHighlightTexture",
}
-- Camelot shows the c60 set. The logical atlas name resolves to the base variant (the c60
-- sheet sorts after it), so the c60 name is tried first, with the base one as fallback.
-- The disabled state is -disable on the c60 sheet and -disabled on the base sheet.
local function microAtlas(atlasSet, state)
local candidates = {
"ui-hud-micromenu-" .. atlasSet .. "-" .. state .. "-c60-2x",
"ui-hud-micromenu-" .. atlasSet .. "-" .. state .. "-2x",
}
if state == "disabled" then
table.insert(candidates, 1, "ui-hud-micromenu-" .. atlasSet .. "-disable-c60-2x")
end
for _, name in ipairs(candidates) do
if ForeverUI.AtlasEntry(name) then
return name
end
end
return nil
end
-- --------------------------------------------------------- Micro menu
local micro = CreateFrame("Frame", "ForeverUIMicroMenu", UIParent)
micro:SetHeight(MICRO_H)
local blockBackground = micro:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(blockBackground, "ui-hud-actionbar-iconframe-background", true)
local blockFrame = CreateFrame("Frame", nil, micro)
blockFrame:SetPoint("TOPLEFT", -8, 8)
blockFrame:SetPoint("BOTTOMRIGHT", 8, -8)
blockFrame:SetFrameLevel(micro:GetFrameLevel())
ForeverUI.SetBarFrameArt(blockFrame, "BORDER")
-- The background overflows the frame. The source declares it after the frame but in a
-- lower sublevel, so it draws below; 3.3.5 has no sublevels, so it uses a lower layer.
blockBackground:SetPoint("TOPLEFT", blockFrame, "TOPLEFT", -13, 0)
blockBackground:SetPoint("BOTTOMRIGHT", blockFrame, "BOTTOMRIGHT", 14, 4)
local microButtons = {}
-- Shows the normal or pushed art of a micro button entry, from its button state.
local function applyMicroState(entry)
local button = entry.button
local pressed = button:GetButtonState() == "PUSHED"
if pressed then
entry.background:Hide()
entry.pressedBackground:Show()
else
entry.background:Show()
entry.pressedBackground:Hide()
end
if entry.pressedShadow then
if pressed then
entry.pressedShadow:Show()
else
entry.pressedShadow:Hide()
end
end
-- tabard emblem: CENTER (0, 2), pushed (1, 1)
if entry.emblem then
for _, t in ipairs({ entry.emblem, entry.emblemHover }) do
t:ClearAllPoints()
t:SetPoint("CENTER", button, "CENTER", pressed and 1 or 0, pressed and 1 or 2)
end
end
-- CharacterMicroButton_SetPushed changes the portrait crop; camelot keeps the same crop
-- in both states.
if entry.portrait and MicroButtonPortrait then
MicroButtonPortrait:SetTexCoord(0.2, 0.8, 0.0666, 0.9)
end
local highlight = button:GetHighlightTexture()
if highlight and entry.atlasSet then
if pressed then
if ForeverUI.SetAtlas(highlight, microAtlas(entry.atlasSet, "down"), true) then
highlight:SetBlendMode("ADD")
highlight:SetAlpha(0.5)
end
else
if ForeverUI.SetAtlas(highlight, microAtlas(entry.atlasSet, "mouseover"), true) then
highlight:SetBlendMode("BLEND")
highlight:SetAlpha(1)
end
end
end
end
-- Latency bar of the game menu button. Camelot anchors it at (0, -2); here the strip
-- frame's inner edge covers the button bottom, so it is anchored at (0, 0).
-- The bar's line is rows 58-59 of the 64-row image. The height gives each row 1.5 screen
-- pixels, so the line is 3 pixels thick; the width stays camelot's 19.
-- Pixels come from gxResolution and the button's effective scale.
local LATENCY = { rows = 64, pixelsPerLine = 1.5, width = 19 }
local function pixelsPerUnit(frame)
local h = tonumber(string.match(GetCVar("gxResolution") or "", "%d+x(%d+)")) or 768
return h / 768 * frame:GetEffectiveScale()
end
local function placeLatency(button)
MainMenuBarPerformanceBar:SetWidth(LATENCY.width)
MainMenuBarPerformanceBar:SetHeight(LATENCY.rows * LATENCY.pixelsPerLine / pixelsPerUnit(button))
MainMenuBarPerformanceBar:ClearAllPoints()
MainMenuBarPerformanceBar:SetPoint("BOTTOM", button, "BOTTOM", 0, 0)
end
-- Roles of created micro buttons: tooltip and click.
-- ProfessionMicroButtonMixin (mainline/mainmenubarmicrobuttons.lua): tooltip
-- MicroButtonTooltipText(PROFESSIONS_BUTTON, TOGGLEPROFESSIONBOOK); PROFESSIONS_BUTTON is
-- TRADE_SKILLS in 3.3.5, which has no such key binding.
-- Tooltip and textures are set like the client's micro buttons (MainMenuBarMicroButton
-- OnEnter, GameTooltip_AddNewbieTip), then skinMicro re-skins them.
-- A button added by another addon carries its own tooltip (text or function) and click.
local MICRO_ROLES = {
professions = {
tooltip = function() return MicroButtonTooltipText(TRADE_SKILLS, "TOGGLEPROFESSIONBOOK") end,
onClick = function()
if ForeverUI.ProfessionsBook then ForeverUI.ProfessionsBook.Toggle() end
end,
},
}
local function createMicro(definition)
local role = MICRO_ROLES[definition.create] or definition
local tooltip = role.tooltip
if type(tooltip) ~= "function" then
local text = tooltip
tooltip = function() return text end
end
local parent = (CharacterMicroButton and CharacterMicroButton:GetParent()) or micro
local button = CreateFrame("Button", definition.name, parent)
for state, method in pairs(MICRO_STATES) do
local e = ForeverUI.AtlasEntry(microAtlas(definition.atlasSet, state))
if e then
button[string.gsub(method, "^Get", "Set")](button, e[1])
end
end
button:RegisterForClicks("AnyUp")
button.tooltipText = tooltip()
button:SetScript("OnEnter", function(self)
self.tooltipText = tooltip()
GameTooltip_AddNewbieTip(self, self.tooltipText, 1.0, 1.0, 1.0, self.newbieText)
end)
button:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
button:SetScript("OnClick", role.onClick)
return button
end
-- Skins a micro button with camelot art and places it in the strip; creates it if needed.
-- index: 1-based slot in the strip
local function skinMicro(definition, index)
if definition.create and not _G[definition.name] then
createMicro(definition)
end
local button = _G[definition.name]
if not button then
return nil
end
button:SetWidth(MICRO_W)
button:SetHeight(MICRO_ART_H)
-- 3.3.5 makes the top 18 px ignore the mouse (decor of the old button art); undo that.
button:SetHitRectInsets(0, 0, 0, 0)
local entry = { button = button, atlasSet = definition.atlasSet, portrait = definition.portrait, created = definition.create }
entry.background = button:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(entry.background, "ui-hud-micromenu-buttonbg-up-c60-2x", true)
entry.background:SetAllPoints(button)
entry.pressedBackground = button:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(entry.pressedBackground, "ui-hud-micromenu-buttonbg-down-c60-2x", true)
entry.pressedBackground:SetAllPoints(button)
entry.pressedBackground:Hide()
if definition.atlasSet then
for state, method in pairs(MICRO_STATES) do
local texture = button[method] and button[method](button)
if texture then
ForeverUI.SetAtlas(texture, microAtlas(definition.atlasSet, state), true)
texture:ClearAllPoints()
texture:SetAllPoints(button)
end
end
else
-- The portrait button has no icon set: hide the client's textures and let camelot's
-- background do all the drawing.
for _, method in ipairs({ "GetNormalTexture", "GetPushedTexture", "GetDisabledTexture" }) do
local texture = button[method] and button[method](button)
if texture then
texture:SetAlpha(0)
end
end
local highlight = button:GetHighlightTexture()
if highlight then
ForeverUI.SetAtlas(highlight, "ui-hud-micromenu-buttonbg-down-c60-2x", true)
highlight:ClearAllPoints()
highlight:SetAllPoints(button)
highlight:SetBlendMode("ADD")
highlight:SetAlpha(0.4)
end
end
if definition.portrait then
local shadow = button:CreateTexture(nil, "BORDER")
ForeverUI.SetAtlas(shadow, "ui-hud-micromenu-portrait-shadow-2x", true)
shadow:SetAllPoints(button)
entry.shadow = shadow
if MicroButtonPortrait then
-- The source insets the portrait 7 px on a 32 x 40 button, i.e. 18 x 26. Keep that
-- size: following the button height would stretch the face.
MicroButtonPortrait:SetDrawLayer("ARTWORK")
MicroButtonPortrait:ClearAllPoints()
MicroButtonPortrait:SetWidth(MICRO_W - 14)
MicroButtonPortrait:SetHeight(MICRO_H - 14)
MicroButtonPortrait:SetPoint("CENTER", button, "CENTER", 0, 0)
MicroButtonPortrait:SetTexCoord(0.2, 0.8, 0.0666, 0.9)
end
local pressedShadow = button:CreateTexture(nil, "OVERLAY")
ForeverUI.SetAtlas(pressedShadow, "ui-hud-micromenu-portrait-down-2x", true)
pressedShadow:SetWidth(MICRO_W)
pressedShadow:SetHeight(MICRO_ART_H)
pressedShadow:SetPoint("CENTER", button, "CENTER", 1, -4)
pressedShadow:Hide()
entry.pressedShadow = pressedShadow
end
if definition.name == "MainMenuMicroButton" and MainMenuBarPerformanceBar then
-- camelot image (32 x 64, a line at the bottom); the 3.3.5 one is a 16 x 8 block
-- that stretches into a big green square
MainMenuBarPerformanceBar:SetTexture(PERFORMANCE_IMAGE)
placeLatency(button)
-- the button's scale changes in a vehicle
entry.placeLatency = function() placeLatency(button) end
-- the pixel size changes with the screen or the scale
local latencyWatcher = CreateFrame("Frame")
latencyWatcher:RegisterEvent("DISPLAY_SIZE_CHANGED")
latencyWatcher:RegisterEvent("UI_SCALE_CHANGED")
latencyWatcher:RegisterEvent("PLAYER_ENTERING_WORLD")
latencyWatcher:SetScript("OnEvent", function() placeLatency(button) end)
-- 3.3.5 re-anchors the bar on every push and release (MainMenuMicroButton_SetPushed /
-- _SetNormal: SetPoint TOPLEFT (9, -36) / (10, -34), without ClearAllPoints). That
-- anchor adds to ours and squeezes the image to 12 x 12, so ours is re-applied after
-- them; camelot does not move the bar when the button is pushed.
for _, name in ipairs({ "MainMenuMicroButton_SetPushed", "MainMenuMicroButton_SetNormal" }) do
if _G[name] then
hooksecurefunc(name, function() placeLatency(button) end)
end
end
end
button:ClearAllPoints()
button:SetPoint("LEFT", micro, "LEFT", (index - 1) * MICRO_PITCH, 0)
return entry
end
local microCount = 0
for index, definition in ipairs(MICRO) do
local entry = skinMicro(definition, index)
if entry then
table.insert(microButtons, entry)
microCount = index
end
end
local BUTTONS_WIDTH = microCount * MICRO_W + (microCount - 1) * MICRO_PADDING
-- Guild tabard on the Social button (camelot: GuildMicroButtonMixin:UpdateTabard).
-- With a tabard: the GuildCommunities-GuildColor set tinted with the tabard background,
-- and the 12 x 14 emblem at CENTER (0, 2), (1, 1) pushed, in OVERLAY and HIGHLIGHT, cut
-- from GuildEmblems_01 (SetSmallGuildTabardTextures: 18/256 cells, 14 per row, inset
-- 1/256) and tinted with the emblem color. Without a tabard: the plain GuildCommunities set.
-- 3.3.5 lacks C_GuildInfo.GetGuildTabardInfo; GetGuildTabardFileNames only returns texture
-- names (Background_<bg>_TU_U, Emblem_<motif>_<color>_TU_U). The motif is the cell index on
-- the sheet; colors come from TabardColors.lua (tools/tabard_colors.py).
local EMBLEM_SHEET = "Interface" .. SEP .. "ForeverUI" .. SEP .. "guildframe" .. SEP .. "guildemblems_01"
local EMBLEM_CELL, EMBLEM_COLUMNS, EMBLEM_EDGE = 18 / 256, 14, 1 / 256
-- Returns background color, emblem cell index and emblem color; nil without a readable
-- tabard.
local function guildTabard()
if not (GetGuildTabardFileNames and IsInGuild and IsInGuild()) then return nil end
local background, _, emblem = GetGuildTabardFileNames()
if not background or not emblem then return nil end
local f = tonumber(string.match(string.lower(background), "background_(%d+)"))
local motif, color = string.match(string.lower(emblem), "emblem_(%d+)_(%d+)")
local colors = ForeverUI.TabardColors
if not (f and motif and colors) then return nil end
local cf, ce = colors.background[f], colors.emblem[tonumber(color)]
if not (cf and ce) then return nil end
return cf, tonumber(motif), ce
end
local social
for _, entry in ipairs(microButtons) do
if entry.button:GetName() == "SocialsMicroButton" then social = entry end
end
if social then
social.baseSet = social.atlasSet
social.emblem = social.button:CreateTexture(nil, "OVERLAY")
social.emblemHover = social.button:CreateTexture(nil, "HIGHLIGHT")
for _, t in ipairs({ social.emblem, social.emblemHover }) do
t:SetTexture(EMBLEM_SHEET)
t:SetWidth(12)
t:SetHeight(14)
t:SetPoint("CENTER", social.button, "CENTER", 0, 2)
t:Hide()
end
end
-- After an emblem change, GetGuildTabardFileNames returns nothing at GUILDTABARD_UPDATE
-- until the new guild data arrives, and no event follows that arrival. So in a guild an
-- unreadable tabard keeps the current look, and the tabard is re-read every
-- RECHECK_INTERVAL s, RECHECK_COUNT times after each event; the last read decides
-- (no tabard: base set).
local RECHECK_INTERVAL, RECHECK_COUNT = 0.5, 20
local rechecker = CreateFrame("Frame")
rechecker:Hide()
ForeverUI.TabardReread = rechecker
-- Applies the tabard look (values from guildTabard); nil background restores the base set.
local function applyTabard(background, motif, color)
social.atlasSet = background and (social.baseSet .. "-guildcolor") or social.baseSet
for state, method in pairs(MICRO_STATES) do
local texture = social.button[method] and social.button[method](social.button)
if texture and ForeverUI.SetAtlas(texture, microAtlas(social.atlasSet, state), true) then
if background then
texture:SetVertexColor(background[1], background[2], background[3])
else
texture:SetVertexColor(1, 1, 1)
end
end
end
if background then
local x = (motif % EMBLEM_COLUMNS) * EMBLEM_CELL
local y = math.floor(motif / EMBLEM_COLUMNS) * EMBLEM_CELL
for _, t in ipairs({ social.emblem, social.emblemHover }) do
t:SetTexCoord(x + EMBLEM_EDGE, x + EMBLEM_CELL - EMBLEM_EDGE, y + EMBLEM_EDGE, y + EMBLEM_CELL - EMBLEM_EDGE)
t:SetVertexColor(color[1], color[2], color[3])
t:Show()
end
else
social.emblem:Hide()
social.emblemHover:Hide()
end
applyMicroState(social)
end
-- GuildMicroButtonMixin:UpdateTabard; isFinal: last re-read, applies even without a tabard
function ForeverUI.UpdateSocialTabard(isFinal)
if not social then return end
local background, motif, color = guildTabard()
local inGuild = IsInGuild and IsInGuild()
if background or isFinal or not inGuild then
applyTabard(background, motif, color)
end
end
rechecker:SetScript("OnUpdate", function(self, elapsed)
self.pending = (self.pending or 0) + elapsed
if self.pending < RECHECK_INTERVAL then return end
self.pending = 0
self.remaining = (self.remaining or 0) - 1
local finish = self.remaining <= 0
ForeverUI.UpdateSocialTabard(finish)
if finish then self:Hide() end
end)
local tabardWatcher = CreateFrame("Frame")
for _, ev in ipairs({ "PLAYER_ENTERING_WORLD", "PLAYER_GUILD_UPDATE", "GUILDTABARD_UPDATE" }) do
tabardWatcher:RegisterEvent(ev)
end
tabardWatcher:SetScript("OnEvent", function()
ForeverUI.UpdateSocialTabard()
rechecker.remaining, rechecker.pending = RECHECK_COUNT, 0
rechecker:Show()
end)
micro:SetWidth(BUTTONS_WIDTH)
-- Remove the client's PvP button (PvP opens from the character sheet). Hiding is not
-- enough: UpdateMicroButtons and VehicleMenuBar_MoveMicroButtons show it again.
ForeverUI.Suppress(_G["PVPMicroButton"])
-- Remove the client's Help button; Help moves to the Esc menu (camelot: GAMEMENU_SUPPORT),
-- between AddOns and Log Out with a gap on each side. AddOns is ACP (patch-5.mpq), under
-- Macros; it re-anchors Log Out under itself on every show, so we re-place after its
-- OnShow. Without ACP, Help goes under Macros. The gap is the one the client menu leaves
-- before Return to Game (16, GameMenuFrame.xml). A plain button: a secure one would make
-- the whole Esc menu protected.
local MENU_GAP = 16
ForeverUI.Suppress(_G["HelpMicroButton"])
if GameMenuFrame and GameMenuButtonMacros and GameMenuButtonLogout then
local help = CreateFrame("Button", "ForeverUIGameMenuButtonHelp", GameMenuFrame, "GameMenuButtonTemplate")
help:SetText(HELP_BUTTON)
help:SetScript("OnClick", function()
PlaySound("igMainMenuOption")
HideUIPanel(GameMenuFrame)
ToggleHelpFrame()
end)
local function place()
help:ClearAllPoints()
help:SetPoint("TOP", _G["GameMenuButtonAddOns"] or GameMenuButtonMacros, "BOTTOM", 0, -MENU_GAP)
GameMenuButtonLogout:SetPoint("TOP", help, "BOTTOM", 0, -MENU_GAP)
end
-- ACP loads before us (alphabetical order); if it loads later, hook it as soon as it
-- arrives, before the menu is first opened
local hooked = false
local function attachACP()
local acp = _G["GameMenuButtonAddOns"]
if acp and not hooked and acp.HookScript then
hooked = true
acp:HookScript("OnShow", place)
end
place()
end
attachACP()
local acpWatcher = CreateFrame("Frame")
acpWatcher:RegisterEvent("ADDON_LOADED")
acpWatcher:SetScript("OnEvent", function(self)
attachACP()
if hooked then self:UnregisterEvent("ADDON_LOADED") end
end)
-- The client places Log Out 1 below its neighbour; add Help and two gaps
GameMenuFrame:SetHeight(GameMenuFrame:GetHeight() + help:GetHeight() + 2 * MENU_GAP - 1)
end
-- Skin of the vehicle bar holding the buttons, nil outside a vehicle
-- (VehicleMenuBar_MoveMicroButtons).
local vehicleSkin
-- Buttons run from the strip's LEFT edge (camelot: layoutFramesGoingRight).
-- Re-applied after VehicleMenuBar_MoveMicroButtons, which re-anchors CharacterMicroButton
-- and SocialsMicroButton on every vehicle enter or exit (MainMenuBar_ToPlayerArt /
-- _ToVehicleArt). In a vehicle the client moves its buttons to VehicleMenuBarArtFrame and
-- created buttons follow; there they fill the vehicle bar's room in two rows, the first
-- holding the extra button of an odd count, at the scale that fits however many there are.
-- Each button is scaled on its own, its anchor offsets divided by its scale. The right
-- button (in a vehicle, the lower row too) draws on top.
local function layoutMicro()
local area = vehicleSkin and VEHICLE_MICRO_AREA[vehicleSkin]
local scale, height, columns, left, top = 1, MICRO_ART_H
if area then
columns = math.ceil(#microButtons / 2)
local rows = #microButtons > 1 and 2 or 1
local blockWidth = columns * MICRO_PITCH - MICRO_PADDING
local blockHeight = rows * (MICRO_H + MICRO_PADDING) - MICRO_PADDING
local width, roomHeight = area.right - area.left, area.top - area.bottom
scale = math.min(VEHICLE_MICRO_SCALE, width / blockWidth, roomHeight / blockHeight)
height = MICRO_H
left = area.left + (width - blockWidth * scale) / 2
top = area.top - (roomHeight - blockHeight * scale) / 2
end
for index, entry in ipairs(microButtons) do
local button = entry.button
if entry.created and CharacterMicroButton then
local parent = CharacterMicroButton:GetParent()
if button:GetParent() ~= parent then
button:SetParent(parent)
end
end
button:SetFrameLevel(button:GetParent():GetFrameLevel() + index)
button:SetScale(scale)
button:SetHeight(height)
if entry.pressedShadow then
entry.pressedShadow:SetHeight(height)
end
button:ClearAllPoints()
if area then
local row = index > columns and 1 or 0
local x = left + (index - 1 - row * columns) * MICRO_PITCH * scale
local y = top - row * (MICRO_H + MICRO_PADDING) * scale
button:SetPoint("TOPLEFT", VehicleMenuBar, "BOTTOMRIGHT", x / scale, y / scale)
else
button:SetPoint("LEFT", micro, "LEFT", (index - 1) * MICRO_PITCH, 0)
end
if entry.placeLatency then
entry.placeLatency()
end
end
end
-- Pushes a created button while its window is open.
-- role: create role, or the button name for one added by another addon
function ForeverUI.UpdateMicro(role, isOpen)
for _, entry in ipairs(microButtons) do
if entry.created == role then
if isOpen then
entry.button:SetButtonState("PUSHED", 1)
else
entry.button:SetButtonState("NORMAL")
end
applyMicroState(entry)
end
end
end
-- professions button: pushed while the book or the crafting page is open
-- (ProfessionsBook.lua)
function ForeverUI.UpdateProfessionsMicro(isOpen)
ForeverUI.UpdateMicro("professions", isOpen)
end
layoutMicro()
if hooksecurefunc and type(_G["VehicleMenuBar_MoveMicroButtons"]) == "function" then
-- skinName: the vehicle bar's skin, nil when the player's bar comes back
hooksecurefunc("VehicleMenuBar_MoveMicroButtons", function(skinName)
vehicleSkin = skinName
layoutMicro()
end)
end
-- ------------------------------------------------------ Bags bar
local bags = CreateFrame("Frame", "ForeverUIBagsBar", UIParent)
bags:SetHeight(BAG_SIZE)
bags:SetWidth(5 * BAG_SIZE + KEYRING_W + 5 * BAG_PADDING)
local bagsFrame = CreateFrame("Frame", nil, bags)
bagsFrame:SetPoint("TOPLEFT", -6, 6)
bagsFrame:SetPoint("BOTTOMRIGHT", 5, -5)
bagsFrame:SetFrameLevel(bags:GetFrameLevel())
ForeverUI.SetBarFrameArt(bagsFrame)
-- Skins a bag slot with a camelot frame.
-- frameAtlas: frame art; frameWidth: art width (KEYRING_W for the keyring)
local function skinBag(button, frameAtlas, frameWidth)
if not button then
return false
end
button:SetWidth(frameWidth == KEYRING_W and KEYRING_W or BAG_SIZE)
button:SetHeight(BAG_SIZE)
local normalFont = button:GetNormalTexture()
if normalFont then
ForeverUI.SetAtlas(normalFont, frameAtlas, true)
normalFont:SetWidth(frameWidth)
normalFont:SetHeight(BAG_FRAME_H)
normalFont:ClearAllPoints()
normalFont:SetPoint("TOPLEFT", button, "TOPLEFT")
end
local pushed = button:GetPushedTexture()
if pushed then
ForeverUI.SetAtlas(pushed, frameAtlas, true)
pushed:SetWidth(frameWidth)
pushed:SetHeight(BAG_FRAME_H)
pushed:ClearAllPoints()
pushed:SetPoint("TOPLEFT", button, "TOPLEFT")
end
local highlight = button:GetHighlightTexture()
if highlight then
ForeverUI.SetAtlas(highlight, frameAtlas, true)
highlight:ClearAllPoints()
highlight:SetAllPoints(button)
highlight:SetBlendMode("ADD")
highlight:SetAlpha(0.4)
end
-- The 3.3.5 green check does not fit this frame: use the same art in ADD, as on the
-- action buttons.
local checked = button.GetCheckedTexture and button:GetCheckedTexture()
if checked then
ForeverUI.SetAtlas(checked, frameAtlas, true)
checked:SetWidth(frameWidth)
checked:SetHeight(BAG_FRAME_H)
checked:ClearAllPoints()
checked:SetPoint("TOPLEFT", button, "TOPLEFT")
checked:SetBlendMode("ADD")
end
local icon = _G[button:GetName() .. "IconTexture"]
if icon then
icon:ClearAllPoints()
icon:SetAllPoints(button)
icon:SetTexCoord(0, 1, 0, 1)
end
return true
end
-- Keyring: a narrower button with its own frame and image.
local keyringIcon
local function skinKeyring()
local button = KeyRingButton
if not button then
return
end
skinBag(button, "ui-hud-actionbar-iconframe-small", KEYRING_W)
if not keyringIcon then
keyringIcon = button:CreateTexture(nil, "BORDER")
keyringIcon:SetPoint("CENTER")
end
-- Deliberate difference: KeyRingMixin:OnBagUpdate shows the keyring image only when the
-- showKeyring CVar is on (set by a tutorial on the first key looted). In 3.3.5 the
-- keyring is a permanent part of the bar, so the image is always set.
-- The -2x variant (54 x 80 texels for 27 x 40 units) stays sharp; the 1x one looks
-- pixelated because a unit is more than one screen pixel.
ForeverUI.SetAtlas(keyringIcon, "ui-hud-actionbar-keyring-small-c60-2x")
-- camelot always keeps this button in the bar; 3.3.5 hides it until the player loots
-- a key.
button:Show()
end
-- Keyring fly-in (BaseBagSlotButtonTemplate, mainline/mainmenubarbagbuttontemplates.xml):
-- AnimIcon, OVERLAY, whole button; FlyIn: 1 s, scale 0.125 to 1, alpha 0 to 1, SMOOTH
-- path through (-15, 30) and (-75, 60). KeyRingMixin plays it reversed (FlyIn:Play(true)).
-- 3.3.5 has neither reverse play nor a start scale, so it runs frame by frame. It
-- replaces the 3.3.5 3D animation (KeyRingButtonItemAnim, ForcedBackpackItem.mdx).
local FLIGHT = { duration = 1, scale = 0.125, points = { { 0, 0 }, { -15, 30 }, { -75, 60 } } }
-- Smooth path: Catmull-Rom through the points, end points doubled.
-- q: progress from 0 to 1; returns x, y
local function curve(q)
local p = FLIGHT.points
local n = #p - 1
local s = math.min(n - 1e-9, math.max(0, q * n))
local i = math.floor(s) + 1
local u = s - (i - 1)
local a, b, c, d = p[math.max(1, i - 1)], p[i], p[i + 1], p[math.min(#p, i + 2)]
local function axis(k)
return 0.5 * (2 * b[k] + (c[k] - a[k]) * u + (2 * a[k] - 5 * b[k] + 4 * c[k] - d[k]) * u * u
+ (3 * b[k] - a[k] - 3 * c[k] + d[k]) * u * u * u)
end
return axis(1), axis(2)
end
local flight = CreateFrame("Frame")
flight:Hide()
flight:SetScript("OnUpdate", function(self, elapsed)
self.t = self.t + (elapsed or 0)
local icon = self.icon
if self.t >= FLIGHT.duration then
icon:Hide()
self:Hide()
return
end
-- reversed: progress goes from 1 to 0
local q = 1 - self.t / FLIGHT.duration
local x, y = curve(q)
local k = FLIGHT.scale + (1 - FLIGHT.scale) * q
local b = KeyRingButton
icon:ClearAllPoints()
icon:SetPoint("CENTER", b, "CENTER", x, y)
icon:SetWidth(b:GetWidth() * k)
icon:SetHeight(b:GetHeight() * k)
icon:SetAlpha(q)
icon:Show()
end)
flight:RegisterEvent("ITEM_PUSH")
flight:SetScript("OnEvent", function(self, _, bag, texture)
local b = KeyRingButton
if not b or bag ~= b:GetID() then
return
end
if not self.icon then
self.icon = b:CreateTexture(nil, "OVERLAY")
self.icon:Hide()
end
self.icon:SetTexture(texture)
self.t = 0
self:Show()
end)
ForeverUI.KeyRingFly = flight
if KeyRingButtonItemAnim then
KeyRingButtonItemAnim:UnregisterEvent("ITEM_PUSH")
KeyRingButtonItemAnim:Hide()
end
local BAG_ORDER = {
"MainMenuBarBackpackButton",
"CharacterBag0Slot", "CharacterBag1Slot", "CharacterBag2Slot", "CharacterBag3Slot",
}
local cells = {}
local separators = {}
local function layoutBags()
local previous = nil
cells = {}
for _, name in ipairs(BAG_ORDER) do
local button = _G[name]
if button then
skinBag(button, "ui-hud-actionbar-iconframe-bags", BAG_FRAME_W)
button:ClearAllPoints()
if previous then
button:SetPoint("RIGHT", previous, "LEFT", -BAG_PADDING, 0)
else
button:SetPoint("RIGHT", bags, "RIGHT", 0, 0)
end
previous = button
table.insert(cells, button)
end
end
skinKeyring()
if KeyRingButton and previous then
KeyRingButton:ClearAllPoints()
KeyRingButton:SetPoint("RIGHT", previous, "LEFT", -BAG_PADDING, 0)
table.insert(cells, KeyRingButton)
end
-- Divider between neighbouring cells: LEFT on the left cell's RIGHT, offset -5, so it
-- is centred on the 2 px gap.
for index = 2, #cells do
if not separators[index] then
local divider = ForeverUI.CreateDivider(bags, bags:GetFrameLevel() + 2)
divider:SetPoint("TOP", cells[index], "TOP", 0, 0)
divider:SetPoint("BOTTOM", cells[index], "BOTTOM", 0, 0)
divider:SetPoint("LEFT", cells[index], "RIGHT", -5, 0)
separators[index] = divider
end
end
end
-- The backpack uses camelot's icon, a separate file, not an atlas entry
-- (camelot/MainMenuBarBagButtons.xml: bagIcon).
local function setBackpackIcon()
local icon = MainMenuBarBackpackButtonIconTexture
if icon then
icon:SetTexture(BAG_ICON)
icon:SetTexCoord(0, 1, 0, 1)
end
end
-- ------------------------------------------------------------ Assembly
-- The row is a chain: the action bar and the bags sit on each side of the micro menu.
-- The chain is turned into screen positions so each element stays movable on its own.
local function applyDefaultPositions()
local half = micro:GetWidth() / 2
ForeverUI.Layout.SetDefaults("actionbar", "BOTTOMRIGHT", "BOTTOM",
MICRO_X - half + BAR_OFFSET_X, MICRO_Y + BAR_OFFSET_Y)
ForeverUI.Layout.SetDefaults("bags", "BOTTOMLEFT", "BOTTOM",
MICRO_X + half + BAGS_OFFSET_X, MICRO_Y + BAGS_OFFSET_Y)
end
-- Measures the row for what sits on top of it (status bars).
-- Like the default positions, x counts from the screen centre and y from the bottom.
local function measureRow()
local half = micro:GetWidth() / 2
local bar = ForeverUI.ActionBarHolder
local barRight = MICRO_X - half + BAR_OFFSET_X
local barLeft = barRight - (bar and bar:GetWidth() or 0)
local bagsLeft = MICRO_X + half + BAGS_OFFSET_X
-- Row top is the highest of the three frames: the action bar and bags frames overflow
-- by 6, the micro menu frame by 8.
local barTop = MICRO_Y + BAR_OFFSET_Y + BAG_SIZE + 6
local microTop = MICRO_Y + MICRO_H + 8
local bagsTop = MICRO_Y + BAGS_OFFSET_Y + BAG_SIZE + 6
ForeverUI.BottomRow = {
-- From the action bar's left edge to the bags bar's right edge. The end caps
-- overflow 30 px on each side, but what sits above aligns on the blocks, not the
-- griffins.
left = barLeft,
right = bagsLeft + bags:GetWidth(),
top = math.max(barTop, microTop, bagsTop),
}
end
-- Default positions of the end caps (gryphons), which frame the whole row: the left one on
-- the action bar's left edge, the right one on the bags bar's right edge, each overlapping its
-- bar and dropped like it (ActionBar.lua). A gryphon the player has moved stays where it is.
local function placeEndCaps()
local endCaps = ForeverUI.ActionBarEndCaps
local bar = ForeverUI.ActionBarHolder
if not endCaps or not bar then
return
end
local half = micro:GetWidth() / 2
local barLeft = MICRO_X - half + BAR_OFFSET_X - bar:GetWidth()
local bagsRight = MICRO_X + half + BAGS_OFFSET_X + bags:GetWidth()
ForeverUI.Layout.SetDefaults("leftgryphon", "BOTTOMRIGHT", "BOTTOM",
barLeft + endCaps.inset, MICRO_Y + BAR_OFFSET_Y + endCaps.drop)
ForeverUI.Layout.SetDefaults("rightgryphon", "BOTTOMLEFT", "BOTTOM",
bagsRight - endCaps.inset, MICRO_Y + BAGS_OFFSET_Y + endCaps.drop)
end
local function layoutAll()
layoutBags()
setBackpackIcon()
layoutMicro()
for _, entry in ipairs(microButtons) do
applyMicroState(entry)
end
end
ForeverUI.Layout.Register(micro, "micromenu", L.BOTTOMBAR_EDIT_LABEL_MICROMENU, "BOTTOM", "BOTTOM", MICRO_X, MICRO_Y)
ForeverUI.Layout.Register(bags, "bags", L.BOTTOMBAR_EDIT_LABEL_BAGS, "BOTTOMLEFT", "BOTTOM",
MICRO_X + micro:GetWidth() / 2 + BAGS_OFFSET_X, MICRO_Y + BAGS_OFFSET_Y)
applyDefaultPositions()
measureRow()
placeEndCaps()
layoutAll()
-- In a vehicle the client hides its bag buttons and moves its micro buttons into the vehicle
-- bar: the strip and the bags frame go too.
RegisterStateDriver(micro, "visibility", VEHICLE_HIDDEN)
RegisterStateDriver(bags, "visibility", VEHICLE_HIDDEN)
-- Micro button added by another addon, without ForeverUI knowing that addon.
-- ForeverUI.AddMicroButton(def), def fields:
-- name global button name; a button already in the strip is returned, never doubled
-- atlasSet camelot icon set (ui-hud-micromenu-<set>-<state>, c60 first)
-- after name of the button it follows; default: just before the game menu
-- tooltip text, or a function returning it (read on hover)
-- onClick click handler
-- ready optional, called with the button once placed
-- Returns the button, or nil while waiting for combat to end (the action bar holds
-- secure buttons and cannot move in combat). The strip widens and the action bar and bags
-- move apart (default positions only; a player-chosen position stays); end caps and
-- status bars follow. ForeverUI.UpdateMicro(name, isOpen) pushes or releases it.
local pendingMicro = CreateFrame("Frame")
local queued = {}
local function namedButton(name)
for _, entry in ipairs(microButtons) do
if entry.button:GetName() == name then return entry.button end
end
end
function ForeverUI.AddMicroButton(def)
if type(def) ~= "table" or type(def.name) ~= "string" then return nil end
local already = namedButton(def.name)
if already then return already end
if InCombatLockdown() then
queued[def.name] = def
pendingMicro:RegisterEvent("PLAYER_REGEN_ENABLED")
return nil
end
local definition = { name = def.name, atlasSet = def.atlasSet, create = def.name, tooltip = def.tooltip, onClick = def.onClick }
local rank, menu
for index, entry in ipairs(microButtons) do
local name = entry.button:GetName()
if def.after and name == def.after then rank = index + 1 end
if name == "MainMenuMicroButton" then menu = index end
end
rank = rank or menu or (#microButtons + 1)
local entry = skinMicro(definition, rank)
if not entry then return nil end
table.insert(microButtons, rank, entry)
local n = #microButtons
micro:SetWidth(n * MICRO_W + (n - 1) * MICRO_PADDING)
applyDefaultPositions()
measureRow()
placeEndCaps()
layoutAll()
if ForeverUI.StatusBarsLayout then ForeverUI.StatusBarsLayout() end
if type(def.ready) == "function" then def.ready(entry.button) end
return entry.button
end
pendingMicro:SetScript("OnEvent", function(self)
self:UnregisterEvent("PLAYER_REGEN_ENABLED")
local list = queued
queued = {}
for _, def in pairs(list) do ForeverUI.AddMicroButton(def) end
end)
if hooksecurefunc then
hooksecurefunc("UpdateMicroButtons", function()
for _, entry in ipairs(microButtons) do
applyMicroState(entry)
end
end)
end
local watcher = CreateFrame("Frame")
watcher:RegisterEvent("PLAYER_ENTERING_WORLD")
watcher:RegisterEvent("BAG_UPDATE")
watcher:RegisterEvent("CVAR_UPDATE")
watcher:SetScript("OnEvent", function()
layoutAll()
end)
-- /fui micro: prints each button's anchor, then for five seconds what the cursor hits.
-- Outside a vehicle (where they are anchored to VehicleMenuBar, scaled), an anchor other than
-- ForeverUIMicroMenu, or an offset that is not a multiple of the pitch, means something moved
-- the buttons (in 3.3.5 only VehicleMenuBar_MoveMicroButtons does, but another addon could).
-- GetMouseFocus shows which frame really gets the click.
function ForeverUI.MicroDebug()
local say = function(text)
DEFAULT_CHAT_FRAME:AddMessage("|cff66ccffForeverUI|r " .. text)
end
say(string.format(L.BOTTOMBAR_MICRO_DEBUG,
#microButtons, micro:GetWidth() or 0, micro:GetHeight() or 0, MICRO_PITCH))
for index, entry in ipairs(microButtons) do
local button = entry.button
local point, target, _, x, y = button:GetPoint(1)
DEFAULT_CHAT_FRAME:AddMessage(string.format(
" " .. L.BOTTOMBAR_MICRO_BUTTON,
index, button:GetName() or "?", tostring(point),
tostring(target and target.GetName and target:GetName()),
tostring(x), tostring(y), button:GetWidth() or 0, button:GetHeight() or 0,
(index - 1) * MICRO_PITCH,
tostring(button:IsShown()), tostring(button:IsEnabled()),
button:GetFrameLevel() or 0, button:GetNumPoints() or 0))
end
-- For five seconds, report what the cursor really hits.
local watcher = CreateFrame("Frame")
local rest, last = 5, nil
watcher:SetScript("OnUpdate", function(self, elapsed)
rest = rest - (elapsed or 0)
local sub = GetMouseFocus and GetMouseFocus()
local name = sub and sub.GetName and sub:GetName() or L.BOTTOMBAR_NOTHING
if name ~= last then
last = name
DEFAULT_CHAT_FRAME:AddMessage(" " .. L.BOTTOMBAR_UNDER_CURSOR .. name)
end
if rest <= 0 then
self:SetScript("OnUpdate", nil)
end
end)
say(L.BOTTOMBAR_HOVER_PROMPT)
end
-- Pure containers do not take the mouse. MainMenuBar is enableMouse=true and covers the
-- whole bottom of the screen, so it swallows clicks meant for the buttons. Its children
-- (action buttons, micro buttons) keep their mouse.
for _, name in ipairs({ "MainMenuBar", "MainMenuBarArtFrame" }) do
local frame = _G[name]
if frame and frame.EnableMouse then
frame:EnableMouse(false)
end
end
ForeverUI.MicroButtons = microButtons
ForeverUI.BagsCells = cells
ForeverUI.BagsDividers = separators
ForeverUI.BottomBarDebug = function()
local point, _, relativePoint, x, y = bags:GetPoint(1)
DEFAULT_CHAT_FRAME:AddMessage(string.format(
"|cff66ccffForeverUI|r " .. L.BOTTOMBAR_DEBUG,
micro:GetWidth(), micro:GetHeight(), #microButtons,
bags:GetWidth(), bags:GetHeight(),
tostring(point), tostring(relativePoint), x or 0, y or 0,
tostring(KeyRingButton and KeyRingButton:IsShown())))
end