ca2b78e217
/fui keeps edit mode, reset and list. The thirty diagnostic and tuning functions behind the other subcommands (bar, bags, character, tabs, close, model, minimap, mouse, map, questlog, chat, chatlines, tabard...) go with them, with Panes.Report, the bags read-back and the 205 texts they alone used. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
418 lines
14 KiB
Lua
418 lines
14 KiB
Lua
-- ForeverUI: movable positions.
|
|
-- Every frame ForeverUI builds registers here with a default position; this table places it
|
|
-- on load and remembers where the player moves it. Everything is anchored to UIParent, never
|
|
-- to another ForeverUI frame, so moving one element never drags another.
|
|
|
|
ForeverUI = ForeverUI or {}
|
|
|
|
-- Disable a client frame. Hiding is not enough: its own code shows it again (page change,
|
|
-- vehicle exit, rune update). So we unregister its events, hide it, and hide it again on
|
|
-- OnShow.
|
|
function ForeverUI.Suppress(frame)
|
|
if not frame or frame.foreverSuppressed then
|
|
return
|
|
end
|
|
|
|
if frame.UnregisterAllEvents then
|
|
frame:UnregisterAllEvents()
|
|
end
|
|
frame:Hide()
|
|
|
|
if frame.HookScript then
|
|
frame:HookScript("OnShow", function(self)
|
|
self:Hide()
|
|
end)
|
|
end
|
|
|
|
frame.foreverSuppressed = true
|
|
end
|
|
|
|
local Layout = {}
|
|
ForeverUI.Layout = Layout
|
|
|
|
Layout.systems = {} -- id -> { frame, label, defaults, baseScale }
|
|
Layout.order = {} -- ids, in registration order
|
|
Layout.editing = false
|
|
Layout.work = nil -- working copy of the positions during a Customize UI session
|
|
Layout.onRegister = nil -- set by Customize UI: called with an id registered while editing
|
|
|
|
local PREFIX = "|cff66ccffForeverUI|r : "
|
|
local L = ForeverUI.L
|
|
|
|
local function say(message)
|
|
DEFAULT_CHAT_FRAME:AddMessage(PREFIX .. message)
|
|
end
|
|
Layout.Say = say
|
|
|
|
local function positions()
|
|
ForeverUIDB = ForeverUIDB or {}
|
|
ForeverUIDB.positions = ForeverUIDB.positions or {}
|
|
return ForeverUIDB.positions
|
|
end
|
|
|
|
-- Positions in effect: the working copy while editing, the saved ones otherwise.
|
|
local function current()
|
|
return Layout.work or positions()
|
|
end
|
|
|
|
-- A saved entry: an anchor (point, relativePoint, x, y), a size (scale), or both. Offsets are
|
|
-- in the frame's units at 100 %; an entry without anchor keeps the default one.
|
|
local function copy(p)
|
|
return { point = p.point, relativePoint = p.relativePoint, x = p.x, y = p.y, scale = p.scale }
|
|
end
|
|
|
|
-- Anchor in effect: the saved one, or the default one.
|
|
function Layout.Anchor(id)
|
|
local system = Layout.systems[id]
|
|
local p = current()[id]
|
|
if p and p.point then
|
|
return p
|
|
end
|
|
return system and system.defaults
|
|
end
|
|
|
|
-- True while the element keeps its default anchor (the player has not moved it).
|
|
function Layout.IsDefault(id)
|
|
local p = current()[id]
|
|
return not (p and p.point)
|
|
end
|
|
|
|
-- Size the element has at 100 %, set by its own code (an automatic fit); the player's size
|
|
-- applies on top of it.
|
|
function Layout.SetBaseScale(id, scale)
|
|
local system = Layout.systems[id]
|
|
if not system then
|
|
return false
|
|
end
|
|
system.baseScale = scale
|
|
Layout.Apply(id)
|
|
return true
|
|
end
|
|
|
|
-- Size in effect, 1 for 100 %.
|
|
function Layout.Scale(id)
|
|
local p = current()[id]
|
|
return p and p.scale or 1
|
|
end
|
|
|
|
-- Ratio of the frame's units at 100 % to UIParent's.
|
|
local function unit(system, scale)
|
|
return system.frame:GetEffectiveScale() / UIParent:GetEffectiveScale() / scale
|
|
end
|
|
|
|
-- Place the frame at its saved position and size, or at the default ones. The offsets shrink
|
|
-- with the size, so the anchor stays at the same place on screen.
|
|
function Layout.Apply(id)
|
|
local system = Layout.systems[id]
|
|
if not system then
|
|
return false
|
|
end
|
|
|
|
local p = Layout.Anchor(id)
|
|
local scale = Layout.Scale(id)
|
|
system.frame:SetScale(system.baseScale * scale)
|
|
system.frame:ClearAllPoints()
|
|
system.frame:SetPoint(p.point, UIParent, p.relativePoint, p.x / scale, p.y / scale)
|
|
return true
|
|
end
|
|
|
|
-- Records the frame's current anchor (in the working copy while editing).
|
|
function Layout.Save(id)
|
|
local system = Layout.systems[id]
|
|
if not system then
|
|
return false
|
|
end
|
|
|
|
local point, _, relativePoint, x, y = system.frame:GetPoint(1)
|
|
if not point then
|
|
return false
|
|
end
|
|
|
|
local scale = Layout.Scale(id)
|
|
current()[id] = {
|
|
point = point,
|
|
relativePoint = relativePoint or point,
|
|
x = (x or 0) * scale,
|
|
y = (y or 0) * scale,
|
|
scale = current()[id] and current()[id].scale,
|
|
}
|
|
return true
|
|
end
|
|
|
|
-- Moves the element. x, y: offsets in UIParent units; point: new anchor, the same point of
|
|
-- the element and of the screen (nil keeps the current anchor).
|
|
function Layout.SetPosition(id, x, y, point)
|
|
local system = Layout.systems[id]
|
|
if not system then
|
|
return false
|
|
end
|
|
local anchor = Layout.Anchor(id)
|
|
local k = unit(system, Layout.Scale(id))
|
|
local p = current()[id] or {}
|
|
p.point, p.relativePoint = point or anchor.point, point or anchor.relativePoint
|
|
p.x, p.y = x / k, y / k
|
|
current()[id] = p
|
|
Layout.Apply(id)
|
|
return true
|
|
end
|
|
|
|
-- Resizes the element around its anchor; scale: 1 for 100 %.
|
|
function Layout.SetScale(id, scale)
|
|
if not Layout.systems[id] then
|
|
return false
|
|
end
|
|
local p = current()[id] or {}
|
|
p.scale = (scale ~= 1) and scale or nil
|
|
current()[id] = next(p) and p or nil
|
|
Layout.Apply(id)
|
|
return true
|
|
end
|
|
|
|
-- Back to the default position: one element, or all of them when id is nil.
|
|
function Layout.Reset(id)
|
|
if id then
|
|
if not Layout.systems[id] then
|
|
return false
|
|
end
|
|
current()[id] = nil
|
|
Layout.Apply(id)
|
|
return true
|
|
end
|
|
|
|
for _, systemID in ipairs(Layout.order) do
|
|
current()[systemID] = nil
|
|
Layout.Apply(systemID)
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- Customize UI session: moves go to a working copy; Commit saves it, Revert drops it and
|
|
-- puts every element back where it was.
|
|
-- The working copy holds every entry, so an element registered during the session finds its
|
|
-- own; only the elements' entries go back (windows keep theirs, even moved meanwhile).
|
|
function Layout.BeginEdit()
|
|
local work = {}
|
|
for id, p in pairs(positions()) do
|
|
work[id] = copy(p)
|
|
end
|
|
Layout.work = work
|
|
Layout.editing = true
|
|
end
|
|
|
|
function Layout.Commit()
|
|
local work = Layout.work
|
|
if work then
|
|
local saved = positions()
|
|
for id in pairs(Layout.systems) do
|
|
saved[id] = work[id]
|
|
end
|
|
end
|
|
Layout.work = nil
|
|
Layout.editing = false
|
|
end
|
|
|
|
function Layout.Revert()
|
|
Layout.work = nil
|
|
Layout.editing = false
|
|
for _, id in ipairs(Layout.order) do
|
|
Layout.Apply(id)
|
|
end
|
|
end
|
|
|
|
-- Make the frame movable and place it.
|
|
-- id: saved-position key; label: edit mode label; point..y: default anchor on UIParent
|
|
function Layout.Register(frame, id, label, point, relativePoint, x, y)
|
|
Layout.systems[id] = {
|
|
frame = frame,
|
|
label = label,
|
|
defaults = { point = point, relativePoint = relativePoint, x = x, y = y },
|
|
baseScale = frame:GetScale(),
|
|
}
|
|
table.insert(Layout.order, id)
|
|
|
|
frame:SetMovable(true)
|
|
frame:SetClampedToScreen(true)
|
|
if frame:GetFrameStrata() ~= Layout.STRATA then frame:SetFrameStrata(Layout.STRATA) end
|
|
Layout.Apply(id)
|
|
|
|
if Layout.editing and Layout.onRegister then
|
|
Layout.onRegister(id)
|
|
end
|
|
return frame
|
|
end
|
|
|
|
-- Change the default position of a registered element. The bottom of the screen is a chain:
|
|
-- the action bar and the bags sit on each side of the micro menu, whose width is known only
|
|
-- once its buttons are counted. An element the player has moved keeps its position.
|
|
function Layout.SetDefaults(id, point, relativePoint, x, y)
|
|
local system = Layout.systems[id]
|
|
if not system then
|
|
return false
|
|
end
|
|
|
|
system.defaults = { point = point, relativePoint = relativePoint, x = x, y = y }
|
|
local p = current()[id]
|
|
if not (p and p.point) then
|
|
Layout.Apply(id)
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- Fractions of a box for an anchor point: x from the left, y from the bottom.
|
|
local function fractions(point)
|
|
local fx = (string.find(point, "LEFT") and 0) or (string.find(point, "RIGHT") and 1) or 0.5
|
|
local fy = (string.find(point, "BOTTOM") and 0) or (string.find(point, "TOP") and 1) or 0.5
|
|
return fx, fy
|
|
end
|
|
Layout.Fractions = fractions
|
|
|
|
-- The frame an anchor refers to; relativeTo: a frame or its name, as SetPoint takes it.
|
|
local function frameOf(relativeTo)
|
|
return type(relativeTo) == "string" and _G[relativeTo] or relativeTo
|
|
end
|
|
|
|
-- An anchor of the element on another frame, as the same place given from the screen: offsets
|
|
-- of point from the same point of UIParent, in the element's units at 100 %; nil while that
|
|
-- frame is not laid out. The other arguments as SetPoint's (no relativeTo: the parent).
|
|
local function onScreen(id, point, relativeTo, relativePoint, x, y)
|
|
local system = Layout.systems[id]
|
|
local other = frameOf(relativeTo) or system.frame:GetParent()
|
|
local left, bottom = other and other:GetLeft(), other and other:GetBottom()
|
|
if not (left and bottom) then
|
|
return nil
|
|
end
|
|
local scale = Layout.Scale(id)
|
|
local k = unit(system, scale) -- element's units at 100 %, in UIParent's
|
|
local ko = other:GetEffectiveScale() / UIParent:GetEffectiveScale()
|
|
local fx, fy = fractions(relativePoint or point)
|
|
local px = (left + other:GetWidth() * fx) * ko + (x or 0) * k * scale
|
|
local py = (bottom + other:GetHeight() * fy) * ko + (y or 0) * k * scale
|
|
local gx, gy = fractions(point)
|
|
return (px - gx * UIParent:GetWidth()) / k, (py - gy * UIParent:GetHeight()) / k
|
|
end
|
|
|
|
-- A client frame that the client itself keeps anchoring elsewhere (UIParent_ManageFramePositions),
|
|
-- made movable like ForeverUI's elements. Its default is the client's last anchor, given from the
|
|
-- screen, and follows the frame that anchor refers to when that one moves; once the player has
|
|
-- moved it, its own anchor comes back after each of the client's. Anchors on the screen (its own,
|
|
-- Customize UI's while dragging) are left alone. id, label: as Register's.
|
|
function Layout.RegisterClientFrame(frame, id, label)
|
|
local last = {} -- the client's last anchor
|
|
-- kept up to date even while moved, so that a reset finds the client's place of the moment
|
|
local function placeDefault()
|
|
local dx, dy = onScreen(id, last.point, last.relativeTo, last.relativePoint, last.x, last.y)
|
|
if dx then
|
|
Layout.SetDefaults(id, last.point, last.point, dx, dy)
|
|
end
|
|
end
|
|
-- point..y: an anchor as SetPoint takes it
|
|
local function follow(point, relativeTo, relativePoint, x, y)
|
|
if not point or relativeTo == UIParent or relativeTo == "UIParent" then
|
|
return
|
|
end
|
|
last = { point = point, relativeTo = relativeTo, relativePoint = relativePoint, x = x, y = y }
|
|
placeDefault()
|
|
if not Layout.IsDefault(id) then
|
|
Layout.Apply(id)
|
|
end
|
|
end
|
|
local point, relativeTo, relativePoint, x, y = frame:GetPoint(1)
|
|
Layout.Register(frame, id, label, "TOPRIGHT", "TOPRIGHT", 0, 0)
|
|
follow(point, relativeTo, relativePoint, x, y)
|
|
hooksecurefunc(frame, "SetPoint", function(_, ...) follow(...) end)
|
|
-- the frame the client's anchor refers to moved (the minimap, by the player)
|
|
hooksecurefunc(Layout, "Apply", function(other)
|
|
local moved = other ~= id and Layout.systems[other] and Layout.systems[other].frame
|
|
if moved and moved == frameOf(last.relativeTo) then
|
|
placeDefault()
|
|
end
|
|
end)
|
|
return frame
|
|
end
|
|
|
|
-- ---------- Draw order
|
|
-- camelot keeps the playing screen in LOW (unit frames, minimap, objectives) and MEDIUM (action
|
|
-- bars, menu, bags bar) and raises each window above the whole MEDIUM strata (toplevel, Raise).
|
|
-- 3.3.5 does not raise our windows above the bars, so every element of the playing screen
|
|
-- goes to LOW, keeping its levels, and MEDIUM holds only the windows and bags: any of them is
|
|
-- drawn over any element of the playing screen. The client's buttons we anchor elsewhere (main
|
|
-- bar, micro-menu, bag slots, bonus, stance, pet, totem and possess bars) are children of
|
|
-- MainMenuBar; the vehicle bar and the durability figure belong to the playing screen too.
|
|
-- The pet buttons carry their own MEDIUM and do not follow their bar.
|
|
Layout.STRATA = "LOW"
|
|
local CLIENT_FRAMES = { "MainMenuBar", "VehicleMenuBar", "DurabilityFrame" }
|
|
for i = 1, NUM_PET_ACTION_SLOTS or 10 do
|
|
CLIENT_FRAMES[#CLIENT_FRAMES + 1] = "PetActionButton" .. i
|
|
end
|
|
|
|
-- MainMenuBar holds secure buttons: out of combat only
|
|
local function lowerClientFrames()
|
|
if InCombatLockdown() then
|
|
return false
|
|
end
|
|
for _, name in ipairs(CLIENT_FRAMES) do
|
|
local f = _G[name]
|
|
if f and f:GetFrameStrata() ~= Layout.STRATA then f:SetFrameStrata(Layout.STRATA) end
|
|
end
|
|
return true
|
|
end
|
|
local lowered = lowerClientFrames()
|
|
|
|
-- The durability figure, stacked by the client under the minimap, moves like the elements. Its
|
|
-- weapon, shield and ranged pieces reach past its 60 x 65 frame (DurabilityFrame.xml): kept on
|
|
-- screen with it.
|
|
if DurabilityFrame then
|
|
Layout.RegisterClientFrame(DurabilityFrame, "durability", DURABILITY)
|
|
DurabilityFrame:SetClampRectInsets(-12, 19, 0, -11)
|
|
end
|
|
|
|
-- Places every element once saved variables are loaded.
|
|
local watcher = CreateFrame("Frame")
|
|
watcher:RegisterEvent("PLAYER_LOGIN")
|
|
watcher:RegisterEvent("PLAYER_REGEN_ENABLED")
|
|
watcher:SetScript("OnEvent", function(_, event)
|
|
if not lowered then lowered = lowerClientFrames() end
|
|
if event ~= "PLAYER_LOGIN" then return end
|
|
for _, id in ipairs(Layout.order) do
|
|
Layout.Apply(id)
|
|
end
|
|
end)
|
|
|
|
SLASH_FOREVERUI1 = "/fui"
|
|
SLASH_FOREVERUI2 = "/foreverui"
|
|
|
|
SlashCmdList["FOREVERUI"] = function(message)
|
|
local command, argument = string.match(message or "", "^(%S*)%s*(.*)$")
|
|
argument = argument or ""
|
|
command = string.lower(command or "")
|
|
|
|
if command == "" or command == "edit" then
|
|
ForeverUI.CustomizeUI.Toggle()
|
|
elseif command == "reset" then
|
|
if InCombatLockdown() then
|
|
-- secure frames (unit frames, action bars) cannot move in combat: the saved positions
|
|
-- would be erased and the frames left where they are
|
|
say(ERR_NOT_IN_COMBAT)
|
|
elseif argument ~= "" then
|
|
if Layout.Reset(argument) then
|
|
say(L.LAYOUT_RESET_ONE .. argument)
|
|
else
|
|
say(L.LAYOUT_UNKNOWN_ELEMENT .. argument)
|
|
end
|
|
else
|
|
Layout.Reset()
|
|
say(L.LAYOUT_RESET_ALL)
|
|
end
|
|
elseif command == "list" then
|
|
say(L.LAYOUT_LIST_HEADER)
|
|
for _, id in ipairs(Layout.order) do
|
|
local system = Layout.systems[id]
|
|
local saved = positions()[id]
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format(" %s (%s)%s", id, system.label,
|
|
saved and L.LAYOUT_LIST_MOVED or ""))
|
|
end
|
|
else
|
|
say(L.LAYOUT_HELP)
|
|
end
|
|
end
|