df9ae8fb07
Dans un véhicule à barre de véhicule, le client cache les barres de droite et finit le glissement de l'icône des sièges contre la minimap : sa place par défaut fait de même (recalculée à la montée et au retour). Seul un ancrage du client sur la minimap est remplacé ; un déplacement en mode Personnaliser n'est plus bloqué par un drapeau. Pour un élément trop grand pour tous les côtés, la fenêtre de taille va du côté le plus dégagé. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1013 lines
33 KiB
Lua
1013 lines
33 KiB
Lua
-- ForeverUI: Customize UI, the edit mode (camelot EditModeManagerFrame, blizzard_editmode).
|
|
-- A grid over the screen, every movable element under a blue veil (the edit mode highlight),
|
|
-- and a window for the grid (spacing, origin), snapping, and the session: Cancel restores the
|
|
-- layout found on opening, Reset goes back to the defaults, Validate saves.
|
|
-- A click selects an element (edit mode selected frame): nine handles on its box, each one
|
|
-- the reference while it is dragged, and a window beside it for its size in percent.
|
|
-- Opened from the Esc menu (after AddOns, as in camelot) or with /fui.
|
|
|
|
local ForeverUI = ForeverUI or {}
|
|
_G.ForeverUI = ForeverUI
|
|
|
|
local Layout = ForeverUI.Layout
|
|
local Tpl = ForeverUI.Templates
|
|
local L = ForeverUI.L
|
|
|
|
local C = {}
|
|
ForeverUI.CustomizeUI = C
|
|
|
|
-- Numbers kept in a table: Lua 5.1 allows at most 60 upvalues per function.
|
|
local N = {
|
|
width = 510, height = 262, top = -100, -- EditModeManagerFrame: fixedWidth 510, TOP (0, -100)
|
|
title = -15,
|
|
labelX = 25,
|
|
column = 170, columnGap = 30, -- controls column: after the widest label
|
|
rowY = -68, -- center of the grid spacing row
|
|
slider = { right = 440, height = 17, stepperGap = 4 },
|
|
valueGap = 25, -- MinimalSliderWithSteppers RightText
|
|
originTop = -90, box = { 108, 66 }, radio = 16, radioInset = 4, originNameGap = 12,
|
|
checkTop = -172, check = 32, checkLabel = 5, secondColumn = 255,
|
|
button = { 150, 28 }, buttonX = 15, buttonY = 16,
|
|
grid = { min = 20, max = 300, step = 10 }, -- Constants.EditModeConsts
|
|
line = { 1, 1, 1, 0.247 }, -- GlobalColor EDIT_MODE_GRID_LINE_COLOR
|
|
originLine = { 0.784, 0.271, 0.980, 0.498 }, -- EDIT_MODE_GRID_CENTER_LINE_COLOR
|
|
veilCorner = 8, -- EditModeSystemSelectionLayout: corners 8 outside
|
|
-- Veils above every element, the client's HIGH frames included (BonusActionBarFrame and
|
|
-- its buttons); the windows above the veils.
|
|
veilStrata = "DIALOG", windowStrata = "FULLSCREEN",
|
|
magnet = 8, -- attraction range, UIParent units
|
|
dragStart = 1, -- cursor travel before a click becomes a drag
|
|
handle = 16, handleLevel = 100,
|
|
size = { -- size window (EditModeSystemSettingsDialog look)
|
|
width = 260, height = 96, titleInset = 30, rowY = -58,
|
|
field = { 40, 20 }, fieldGap = 14, percentGap = 6,
|
|
gap = 12, -- from the element
|
|
min = 50, max = 200,
|
|
},
|
|
}
|
|
|
|
-- The nine grid origins: anchor, screen fractions (x from the left, y from the bottom), name.
|
|
local ORIGINS = {
|
|
{ "TOPLEFT", 0, 1, L.CUSTOMIZEUI_ORIGIN_TOPLEFT },
|
|
{ "TOP", 0.5, 1, L.CUSTOMIZEUI_ORIGIN_TOP },
|
|
{ "TOPRIGHT", 1, 1, L.CUSTOMIZEUI_ORIGIN_TOPRIGHT },
|
|
{ "LEFT", 0, 0.5, L.CUSTOMIZEUI_ORIGIN_LEFT },
|
|
{ "CENTER", 0.5, 0.5, L.CUSTOMIZEUI_ORIGIN_CENTER },
|
|
{ "RIGHT", 1, 0.5, L.CUSTOMIZEUI_ORIGIN_RIGHT },
|
|
{ "BOTTOMLEFT", 0, 0, L.CUSTOMIZEUI_ORIGIN_BOTTOMLEFT },
|
|
{ "BOTTOM", 0.5, 0, L.CUSTOMIZEUI_ORIGIN_BOTTOM },
|
|
{ "BOTTOMRIGHT", 1, 0, L.CUSTOMIZEUI_ORIGIN_BOTTOMRIGHT },
|
|
}
|
|
local ORIGIN_BY_KEY = {}
|
|
for _, o in ipairs(ORIGINS) do
|
|
ORIGIN_BY_KEY[o[1]] = o
|
|
end
|
|
|
|
local DEFAULTS = { spacing = 100, origin = "CENTER", snap = true, sticky = true }
|
|
|
|
-- Saved grid settings, completed with the defaults.
|
|
local function saved()
|
|
ForeverUIDB = ForeverUIDB or {}
|
|
if type(ForeverUIDB.customize) ~= "table" then
|
|
ForeverUIDB.customize = {}
|
|
end
|
|
local s = ForeverUIDB.customize
|
|
for k, v in pairs(DEFAULTS) do
|
|
if s[k] == nil then
|
|
s[k] = v
|
|
end
|
|
end
|
|
return s
|
|
end
|
|
|
|
-- The session's working copy of the grid settings (saved on Validate).
|
|
C.settings = {}
|
|
|
|
-- ------------------------------------------------------------ Grid
|
|
|
|
local grid = CreateFrame("Frame", "ForeverUICustomizeGrid", UIParent)
|
|
grid:SetAllPoints(UIParent)
|
|
grid:SetFrameStrata("BACKGROUND")
|
|
grid:Hide()
|
|
local lines = {}
|
|
|
|
-- Size of one screen pixel in UIParent units: the lines stay one pixel thick.
|
|
local function pixel()
|
|
local height = tonumber(string.match(GetCVar("gxResolution") or "", "%d+x(%d+)"))
|
|
if not height or height <= 0 then
|
|
return 1
|
|
end
|
|
return UIParent:GetHeight() / height
|
|
end
|
|
|
|
-- Draws a line through the origin at every spacing step, the origin lines in violet.
|
|
function C.DrawGrid()
|
|
local s = C.settings
|
|
local width, height = grid:GetWidth(), grid:GetHeight()
|
|
local o = ORIGIN_BY_KEY[s.origin] or ORIGIN_BY_KEY.CENTER
|
|
local ox, oy = width * o[2], height * o[3]
|
|
local px = pixel()
|
|
local n = 0
|
|
-- on whole pixels, or a line would blur over two
|
|
local function snap(v)
|
|
return math.floor(v / px + 0.5) * px
|
|
end
|
|
-- vertical: along x, else along y; position from the left or the bottom
|
|
local function line(vertical, position, isOrigin)
|
|
n = n + 1
|
|
local t = lines[n]
|
|
if not t then
|
|
t = grid:CreateTexture(nil, "BACKGROUND")
|
|
lines[n] = t
|
|
end
|
|
local c = isOrigin and N.originLine or N.line
|
|
t:SetTexture(c[1], c[2], c[3], c[4])
|
|
t:ClearAllPoints()
|
|
if vertical then
|
|
local x = snap(math.min(math.max(position - px / 2, 0), width - px))
|
|
t:SetPoint("TOPLEFT", grid, "TOPLEFT", x, 0)
|
|
t:SetPoint("BOTTOMLEFT", grid, "BOTTOMLEFT", x, 0)
|
|
t:SetWidth(px)
|
|
else
|
|
local y = snap(math.min(math.max(position - px / 2, 0), height - px))
|
|
t:SetPoint("BOTTOMLEFT", grid, "BOTTOMLEFT", 0, y)
|
|
t:SetPoint("BOTTOMRIGHT", grid, "BOTTOMRIGHT", 0, y)
|
|
t:SetHeight(px)
|
|
end
|
|
t:Show()
|
|
end
|
|
local step = s.spacing
|
|
for k = math.ceil(-ox / step), math.floor((width - ox) / step) do
|
|
line(true, ox + k * step, k == 0)
|
|
end
|
|
for k = math.ceil(-oy / step), math.floor((height - oy) / step) do
|
|
line(false, oy + k * step, k == 0)
|
|
end
|
|
for i = n + 1, #lines do
|
|
lines[i]:Hide()
|
|
end
|
|
end
|
|
|
|
grid:RegisterEvent("DISPLAY_SIZE_CHANGED")
|
|
grid:RegisterEvent("UI_SCALE_CHANGED")
|
|
grid:SetScript("OnEvent", function(self)
|
|
if self:IsShown() then
|
|
C.DrawGrid()
|
|
end
|
|
end)
|
|
C.grid = grid
|
|
|
|
-- ------------------------------------------------------------ Geometry
|
|
|
|
-- 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
|
|
|
|
-- Screen point of the third a position falls in (x from the left, y from the bottom, UIParent
|
|
-- units): TOPLEFT to BOTTOMRIGHT, CENTER in the middle one.
|
|
local function screenPoint(x, y)
|
|
local width, height = UIParent:GetWidth(), UIParent:GetHeight()
|
|
local h = (x < width / 3 and "LEFT") or (x > width * 2 / 3 and "RIGHT") or ""
|
|
local v = (y < height / 3 and "BOTTOM") or (y > height * 2 / 3 and "TOP") or ""
|
|
return (v .. h ~= "") and (v .. h) or "CENTER"
|
|
end
|
|
|
|
-- Box of a frame in UIParent units { left, bottom, width, height }; nil before layout.
|
|
local function box(frame)
|
|
local l, b = frame:GetLeft(), frame:GetBottom()
|
|
if not l or not b then
|
|
return nil
|
|
end
|
|
local k = frame:GetEffectiveScale() / UIParent:GetEffectiveScale()
|
|
return { l * k, b * k, frame:GetWidth() * k, frame:GetHeight() * k }
|
|
end
|
|
|
|
-- ------------------------------------------------------------ Veils
|
|
|
|
-- The edit mode selection (EditModeSystemSelectionLayout): one corner image mirrored four
|
|
-- times, centered on the element's corners, edges between them, and the center reaching the
|
|
-- corners' middle. Corners: point, side (x, y), flipX, flipY.
|
|
local CORNERS = {
|
|
{ "TOPLEFT", -1, 1, false, false }, { "TOPRIGHT", 1, 1, true, false },
|
|
{ "BOTTOMLEFT", -1, -1, false, true }, { "BOTTOMRIGHT", 1, -1, true, true },
|
|
}
|
|
|
|
-- Paints the pieces with a texture kit: "highlight" (the blue veil) or "selected".
|
|
local function paint(pieces, kit)
|
|
local base = "editmode-actionbar-" .. kit .. "-nineslice"
|
|
local e = ForeverUI.AtlasEntry(base .. "-corner")
|
|
for i, c in ipairs(CORNERS) do
|
|
local t = pieces.corners[i]
|
|
local l, r, top, bottom = e[2], e[3], e[4], e[5]
|
|
if c[4] then l, r = r, l end
|
|
if c[5] then top, bottom = bottom, top end
|
|
t:SetTexture(e[1])
|
|
t:SetTexCoord(l, r, top, bottom)
|
|
t:SetWidth(e[6])
|
|
t:SetHeight(e[7])
|
|
end
|
|
ForeverUI.SetAtlas(pieces.top, "_" .. base .. "-edgetop", true)
|
|
ForeverUI.SetAtlas(pieces.bottom, "_" .. base .. "-edgebottom", true)
|
|
ForeverUI.SetAtlas(pieces.left, "!" .. base .. "-edgeleft", true)
|
|
ForeverUI.SetAtlas(pieces.right, "!" .. base .. "-edgeright", true)
|
|
ForeverUI.SetAtlas(pieces.center, base .. "-center", true)
|
|
end
|
|
|
|
-- Builds the pieces on host, the frame they are drawn on.
|
|
local function selection(host)
|
|
local k = N.veilCorner
|
|
local p = { corners = {} }
|
|
for i, c in ipairs(CORNERS) do
|
|
local t = host:CreateTexture(nil, "BORDER")
|
|
t:SetPoint(c[1], host, c[1], c[2] * k, c[3] * k)
|
|
p.corners[i] = t
|
|
end
|
|
local tl, tr, bl, br = p.corners[1], p.corners[2], p.corners[3], p.corners[4]
|
|
-- from, to: the two corners the edge spans; the anchors pick the facing sides
|
|
local function edge(from, fromPoint, to, toPoint)
|
|
local t = host:CreateTexture(nil, "BORDER")
|
|
t:SetPoint("TOPLEFT", from, fromPoint)
|
|
t:SetPoint("BOTTOMRIGHT", to, toPoint)
|
|
return t
|
|
end
|
|
p.top = edge(tl, "TOPRIGHT", tr, "BOTTOMLEFT")
|
|
p.bottom = edge(bl, "TOPRIGHT", br, "BOTTOMLEFT")
|
|
p.left = edge(tl, "BOTTOMLEFT", bl, "TOPRIGHT")
|
|
p.right = edge(tr, "BOTTOMLEFT", br, "TOPRIGHT")
|
|
p.center = host:CreateTexture(nil, "BACKGROUND")
|
|
p.center:SetPoint("TOPLEFT", tl, "BOTTOMRIGHT", -k, k)
|
|
p.center:SetPoint("BOTTOMRIGHT", br, "TOPLEFT", k, -k)
|
|
return p
|
|
end
|
|
|
|
local veils = {}
|
|
|
|
-- The label fits the element: large font, smaller ones when it does not fit.
|
|
local function fitLabel(veil)
|
|
for _, font in ipairs({ GameFontHighlightLarge, GameFontHighlight, GameFontHighlightSmall }) do
|
|
veil.label:SetFontObject(font)
|
|
if veil.label:GetStringWidth() <= veil:GetWidth() then
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
-- The veil keeps the screen's scale whatever the element's size: its frame, label and
|
|
-- handles do not grow with the element.
|
|
local function unscale(veil)
|
|
veil:SetScale(UIParent:GetEffectiveScale() / veil:GetParent():GetEffectiveScale())
|
|
fitLabel(veil)
|
|
end
|
|
|
|
-- Veil over one element: a click selects it, a drag moves it.
|
|
local function buildVeil(id, system)
|
|
local veil = CreateFrame("Frame", nil, system.frame)
|
|
veil:SetAllPoints(system.frame)
|
|
veil:SetFrameStrata(N.veilStrata)
|
|
veil:EnableMouse(true)
|
|
veil.pieces = selection(veil)
|
|
paint(veil.pieces, "highlight")
|
|
veil.label = veil:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
|
|
veil.label:SetPoint("CENTER", veil, "CENTER", 0, 0)
|
|
veil.label:SetText(system.label)
|
|
veil:SetScript("OnMouseDown", function(_, button)
|
|
if button == "LeftButton" then
|
|
C.Select(id)
|
|
C.BeginDrag(id)
|
|
end
|
|
end)
|
|
veil:SetScript("OnMouseUp", function()
|
|
C.EndDrag()
|
|
end)
|
|
-- The element can hide while held (the cast bar when its cast ends, the target frame when the
|
|
-- target goes): a hidden veil gets no OnMouseUp, so the drag ends here
|
|
veil:SetScript("OnHide", function()
|
|
local d = C.drag and C.drag.state
|
|
if d and d.id == id then
|
|
C.EndDrag()
|
|
end
|
|
end)
|
|
veil:SetScript("OnSizeChanged", fitLabel)
|
|
veils[id] = veil
|
|
return veil
|
|
end
|
|
|
|
local function showVeil(id)
|
|
local system = Layout.systems[id]
|
|
if not system then
|
|
return
|
|
end
|
|
local veil = veils[id] or buildVeil(id, system)
|
|
veil:Show()
|
|
unscale(veil)
|
|
end
|
|
C.veils = veils
|
|
|
|
-- ------------------------------------------------------------ Moving
|
|
|
|
-- Drag in progress: { id, point (held point), fx, fy, box (at the start), cursorX, cursorY,
|
|
-- moved, left, bottom (where the box is now) }
|
|
local drag = CreateFrame("Frame")
|
|
drag:Hide()
|
|
C.drag = drag
|
|
|
|
-- The smallest shift within the magnet range. best: best shift so far, or nil; shift: candidate
|
|
local function closer(best, shift)
|
|
if math.abs(shift) <= N.magnet and (not best or math.abs(shift) < math.abs(best)) then
|
|
return shift
|
|
end
|
|
return best
|
|
end
|
|
|
|
-- Shifts along one axis that stick an edge or the middle of the moved box to another box.
|
|
-- from, size: the moved box on that axis; otherFrom, otherSize: the other box
|
|
local function stickShifts(from, size, otherFrom, otherSize)
|
|
local otherTo = otherFrom + otherSize
|
|
return {
|
|
otherFrom - from, otherTo - from, otherFrom - (from + size), otherTo - (from + size),
|
|
otherFrom + otherSize / 2 - (from + size / 2),
|
|
}
|
|
end
|
|
|
|
-- Where the box goes once attracted: Snap on Grid pulls the held point onto a grid line near
|
|
-- it, Sticky UI pulls an edge onto an edge of another element near it (facing it).
|
|
local function attract(d, left, bottom)
|
|
local s = C.settings
|
|
local w, h = d.box[3], d.box[4]
|
|
local sx, sy
|
|
if s.snap then
|
|
local o = ORIGIN_BY_KEY[s.origin]
|
|
local ox, oy = grid:GetWidth() * o[2], grid:GetHeight() * o[3]
|
|
local x, y = left + d.fx * w, bottom + d.fy * h
|
|
sx = closer(sx, ox + math.floor((x - ox) / s.spacing + 0.5) * s.spacing - x)
|
|
sy = closer(sy, oy + math.floor((y - oy) / s.spacing + 0.5) * s.spacing - y)
|
|
end
|
|
if s.sticky then
|
|
local m = N.magnet
|
|
for id, system in pairs(Layout.systems) do
|
|
local o = id ~= d.id and system.frame:IsVisible() and box(system.frame)
|
|
if o then
|
|
if bottom <= o[2] + o[4] + m and bottom + h >= o[2] - m then
|
|
for _, shift in ipairs(stickShifts(left, w, o[1], o[3])) do
|
|
sx = closer(sx, shift)
|
|
end
|
|
end
|
|
if left <= o[1] + o[3] + m and left + w >= o[1] - m then
|
|
for _, shift in ipairs(stickShifts(bottom, h, o[2], o[4])) do
|
|
sy = closer(sy, shift)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return left + (sx or 0), bottom + (sy or 0)
|
|
end
|
|
|
|
-- Puts the dragged box at left, bottom (UIParent units), attracted and kept on screen; the
|
|
-- held point anchors the element meanwhile.
|
|
local function place(d, left, bottom)
|
|
local w, h = d.box[3], d.box[4]
|
|
left, bottom = attract(d, left, bottom)
|
|
left = math.max(0, math.min(left, UIParent:GetWidth() - w))
|
|
bottom = math.max(0, math.min(bottom, UIParent:GetHeight() - h))
|
|
d.left, d.bottom = left, bottom
|
|
local frame = Layout.systems[d.id].frame
|
|
local k = frame:GetEffectiveScale() / UIParent:GetEffectiveScale()
|
|
frame:ClearAllPoints()
|
|
frame:SetPoint(d.point, UIParent, "BOTTOMLEFT", (left + d.fx * w) / k, (bottom + d.fy * h) / k)
|
|
end
|
|
|
|
drag:SetScript("OnUpdate", function(self)
|
|
local d = self.state
|
|
local ui = UIParent:GetEffectiveScale()
|
|
local cx, cy = GetCursorPosition()
|
|
local dx, dy = cx / ui - d.cursorX, cy / ui - d.cursorY
|
|
if not d.moved and math.abs(dx) + math.abs(dy) < N.dragStart then
|
|
return
|
|
end
|
|
d.moved = true
|
|
place(d, d.box[1] + dx, d.box[2] + dy)
|
|
-- the size window follows the element
|
|
C.PlaceSizeWindow({ d.left, d.bottom, d.box[3], d.box[4] })
|
|
end)
|
|
|
|
-- Starts moving an element. point: the held handle; nil for the body, whose reference is
|
|
-- the element's anchor.
|
|
function C.BeginDrag(id, point)
|
|
local system = Layout.systems[id]
|
|
local b = system and box(system.frame)
|
|
if not b then
|
|
return
|
|
end
|
|
point = point or Layout.Anchor(id).point
|
|
local fx, fy = fractions(point)
|
|
local ui = UIParent:GetEffectiveScale()
|
|
local cx, cy = GetCursorPosition()
|
|
drag.state = { id = id, point = point, fx = fx, fy = fy, box = b, cursorX = cx / ui, cursorY = cy / ui }
|
|
drag:Show()
|
|
end
|
|
|
|
-- Ends the move. Like camelot's edit mode, which keeps the anchor the client gives a moved
|
|
-- frame, the element is anchored to the screen point of the third its center ends in, so it
|
|
-- keeps its place from that edge when the screen size changes. A click without moving only
|
|
-- selects.
|
|
function C.EndDrag()
|
|
local d = drag.state
|
|
drag:Hide()
|
|
drag.state = nil
|
|
if not (d and d.moved) then
|
|
return
|
|
end
|
|
local w, h = d.box[3], d.box[4]
|
|
local point = screenPoint(d.left + w / 2, d.bottom + h / 2)
|
|
local fx, fy = fractions(point)
|
|
Layout.SetPosition(d.id, d.left + fx * w - fx * UIParent:GetWidth(),
|
|
d.bottom + fy * h - fy * UIParent:GetHeight(), point)
|
|
C.PlaceSizeWindow()
|
|
end
|
|
|
|
-- ------------------------------------------------------------ Selection
|
|
|
|
-- The nine handles, on the selected element only.
|
|
local HANDLES = { "TOPLEFT", "TOP", "TOPRIGHT", "LEFT", "CENTER", "RIGHT", "BOTTOMLEFT", "BOTTOM", "BOTTOMRIGHT" }
|
|
local handles = {}
|
|
C.handles = handles
|
|
|
|
-- Handle image: gold edged while hovered or held.
|
|
local function paintHandle(h)
|
|
local state = (h.hover or h.held) and "active" or "default"
|
|
ForeverUI.SetAtlas(h.texture, "housing-advancedmode-scale-handle-" .. state, true)
|
|
end
|
|
|
|
local function buildHandles()
|
|
for _, point in ipairs(HANDLES) do
|
|
local h = CreateFrame("Frame", nil, UIParent)
|
|
h:SetWidth(N.handle)
|
|
h:SetHeight(N.handle)
|
|
h:EnableMouse(true)
|
|
h:Hide()
|
|
h.texture = h:CreateTexture(nil, "OVERLAY")
|
|
h.texture:SetAllPoints(h)
|
|
paintHandle(h)
|
|
h:SetScript("OnEnter", function(self)
|
|
self.hover = true
|
|
paintHandle(self)
|
|
end)
|
|
h:SetScript("OnLeave", function(self)
|
|
self.hover = false
|
|
paintHandle(self)
|
|
end)
|
|
h:SetScript("OnMouseDown", function(self, button)
|
|
if button == "LeftButton" and C.selected then
|
|
self.held = true
|
|
paintHandle(self)
|
|
C.BeginDrag(C.selected, point)
|
|
end
|
|
end)
|
|
h:SetScript("OnMouseUp", function(self)
|
|
self.held = false
|
|
paintHandle(self)
|
|
C.EndDrag()
|
|
end)
|
|
handles[point] = h
|
|
end
|
|
end
|
|
|
|
local sizeWindow
|
|
|
|
-- Size in effect of the selected element, in percent.
|
|
local function selectedPercent()
|
|
return math.floor(Layout.Scale(C.selected) * 100 + 0.5)
|
|
end
|
|
|
|
-- Selects an element: selected frame, handles on its box, size window beside it.
|
|
function C.Select(id)
|
|
local veil = veils[id]
|
|
if C.selected == id or not veil then
|
|
return
|
|
end
|
|
C.Deselect()
|
|
C.selected = id
|
|
paint(veil.pieces, "selected")
|
|
for point, h in pairs(handles) do
|
|
h:SetParent(veil)
|
|
h:SetFrameStrata(N.veilStrata)
|
|
h:SetFrameLevel(N.handleLevel)
|
|
h:ClearAllPoints()
|
|
h:SetPoint("CENTER", veil, point, 0, 0)
|
|
h:Show()
|
|
end
|
|
sizeWindow.title:SetText(Layout.systems[id].label)
|
|
sizeWindow.field:SetText(tostring(selectedPercent()))
|
|
sizeWindow:Show()
|
|
C.PlaceSizeWindow()
|
|
end
|
|
|
|
function C.Deselect()
|
|
local veil = C.selected and veils[C.selected]
|
|
if veil then
|
|
paint(veil.pieces, "highlight")
|
|
end
|
|
for _, h in pairs(handles) do
|
|
h:Hide()
|
|
end
|
|
if sizeWindow then
|
|
sizeWindow.field:ClearFocus()
|
|
sizeWindow:Hide()
|
|
end
|
|
C.selected = nil
|
|
end
|
|
|
|
-- The size window along a side of the selected element, never over it and always within the
|
|
-- screen: on its right, else its left (level with its top), else under it, else above it (level
|
|
-- with its left edge), the first side where it fits. An element too big for any side gets the
|
|
-- window on the side with the most room, pushed back onto the screen. b: the element's box while
|
|
-- it is dragged ({ left, bottom, width, height }, UIParent units); nil reads it from the frame.
|
|
function C.PlaceSizeWindow(b)
|
|
local id = C.selected
|
|
if not (id and sizeWindow) then
|
|
return
|
|
end
|
|
b = b or box(Layout.systems[id].frame)
|
|
sizeWindow:ClearAllPoints()
|
|
if not b then
|
|
sizeWindow:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
|
|
return
|
|
end
|
|
local width, height = UIParent:GetWidth(), UIParent:GetHeight()
|
|
local w, h, gap = sizeWindow:GetWidth(), sizeWindow:GetHeight(), N.size.gap
|
|
local left, bottom, right, top = b[1], b[2], b[1] + b[3], b[2] + b[4]
|
|
-- each side: the window's top left corner, kept on screen along that side, and the room
|
|
-- left there for it (it fits when the room is not negative)
|
|
local level = math.max(h, math.min(height, top))
|
|
local along = math.max(0, math.min(left, width - w))
|
|
local sides = {
|
|
{ right + gap, level, width - (right + gap + w) },
|
|
{ left - gap - w, level, left - gap - w },
|
|
{ along, bottom - gap, bottom - gap - h },
|
|
{ along, top + gap + h, height - (top + gap + h) },
|
|
}
|
|
local spot
|
|
for _, side in ipairs(sides) do
|
|
if side[3] >= 0 then
|
|
spot = side
|
|
break
|
|
end
|
|
if not spot or side[3] > spot[3] then
|
|
spot = side
|
|
end
|
|
end
|
|
local x = math.max(0, math.min(spot[1], width - w))
|
|
local y = math.max(h, math.min(spot[2], height))
|
|
sizeWindow:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", x, y)
|
|
end
|
|
|
|
-- Resizes the selected element; percent: typed value, kept between 50 and 200.
|
|
-- Returns the size applied.
|
|
function C.SetSize(percent)
|
|
local id = C.selected
|
|
if not id then
|
|
return nil
|
|
end
|
|
percent = math.max(N.size.min, math.min(N.size.max, math.floor(percent + 0.5)))
|
|
Layout.SetScale(id, percent / 100)
|
|
unscale(veils[id])
|
|
C.PlaceSizeWindow()
|
|
return percent
|
|
end
|
|
|
|
-- ------------------------------------------------------------ Window
|
|
|
|
local window
|
|
local controls = {}
|
|
|
|
-- Row label; y: center of the row. Returns its width.
|
|
local function rowLabel(f, text, y)
|
|
local label = f:CreateFontString(nil, "ARTWORK", "GameFontHighlightMedium")
|
|
label:SetPoint("LEFT", f, "TOPLEFT", N.labelX, y)
|
|
label:SetText(text)
|
|
return label:GetStringWidth()
|
|
end
|
|
|
|
-- Slider with steppers (MinimalSliderWithSteppersTemplate) for the grid spacing.
|
|
-- column: left edge of the slider
|
|
local function buildSpacing(f, column)
|
|
local S = N.slider
|
|
local slider = CreateFrame("Slider", "ForeverUICustomizeSpacing", f)
|
|
slider:SetOrientation("HORIZONTAL")
|
|
slider:SetWidth(S.right - column)
|
|
slider:SetHeight(S.height)
|
|
slider:SetPoint("LEFT", f, "TOPLEFT", column, N.rowY)
|
|
slider:SetMinMaxValues(N.grid.min, N.grid.max)
|
|
slider:SetValueStep(N.grid.step)
|
|
slider:EnableMouse(true)
|
|
ForeverUI.Settings.Cursor(slider)
|
|
|
|
-- atlas: stepper image; point, relativePoint, x: placement beside the slider;
|
|
-- delta: value change per click
|
|
local function stepper(atlas, point, relativePoint, x, delta)
|
|
local b = CreateFrame("Button", nil, f)
|
|
local t = b:CreateTexture(nil, "ARTWORK")
|
|
ForeverUI.SetAtlas(t, atlas)
|
|
b:SetWidth(t:GetWidth())
|
|
b:SetHeight(t:GetHeight())
|
|
t:SetAllPoints(b)
|
|
b:SetPoint(point, slider, relativePoint, x, 0)
|
|
b:SetScript("OnClick", function()
|
|
slider:SetValue(slider:GetValue() + delta)
|
|
end)
|
|
end
|
|
stepper("minimal_sliderbar_button_left", "RIGHT", "LEFT", -S.stepperGap, -N.grid.step)
|
|
stepper("minimal_sliderbar_button_right", "LEFT", "RIGHT", S.stepperGap, N.grid.step)
|
|
|
|
local value = f:CreateFontString(nil, "ARTWORK", "GameFontNormal")
|
|
value:SetPoint("LEFT", slider, "RIGHT", N.valueGap, 0)
|
|
slider:SetScript("OnValueChanged", function(self, v)
|
|
local step = N.grid.step
|
|
v = math.floor(v / step + 0.5) * step
|
|
value:SetText(tostring(v))
|
|
if C.settings.spacing ~= v then
|
|
C.settings.spacing = v
|
|
C.DrawGrid()
|
|
end
|
|
end)
|
|
controls.spacing = slider
|
|
controls.spacingValue = value
|
|
end
|
|
|
|
-- Nine radio buttons placed like the screen: the grid origin. column: left edge of the box
|
|
local function buildOrigin(f, column)
|
|
local box = CreateFrame("Frame", nil, f)
|
|
box:SetWidth(N.box[1])
|
|
box:SetHeight(N.box[2])
|
|
box:SetPoint("TOPLEFT", f, "TOPLEFT", column, N.originTop)
|
|
Tpl.Inset(box)
|
|
|
|
local name = f:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
|
|
name:SetPoint("LEFT", box, "RIGHT", N.originNameGap, 0)
|
|
controls.originName = name
|
|
|
|
controls.origins = {}
|
|
for _, o in ipairs(ORIGINS) do
|
|
local key = o[1]
|
|
local r = CreateFrame("CheckButton", nil, box)
|
|
r:SetWidth(N.radio)
|
|
r:SetHeight(N.radio)
|
|
local dx = (o[2] == 0 and N.radioInset) or (o[2] == 1 and -N.radioInset) or 0
|
|
local dy = (o[3] == 0 and N.radioInset) or (o[3] == 1 and -N.radioInset) or 0
|
|
r:SetPoint(key, box, key, dx, dy)
|
|
-- set, get: the button's texture setter and getter; atlas: the image
|
|
local function state(set, get, atlas)
|
|
r[set](r, ForeverUI.AtlasEntry(atlas)[1])
|
|
local t = r[get](r)
|
|
ForeverUI.SetAtlas(t, atlas, true)
|
|
t:ClearAllPoints()
|
|
t:SetAllPoints(r)
|
|
end
|
|
state("SetNormalTexture", "GetNormalTexture", "common-radiobutton-circle")
|
|
state("SetCheckedTexture", "GetCheckedTexture", "common-radiobutton-dot")
|
|
r:SetScript("OnClick", function()
|
|
C.SetOrigin(key)
|
|
end)
|
|
r:SetScript("OnEnter", function(self)
|
|
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
|
|
GameTooltip:SetText(o[4])
|
|
GameTooltip:Show()
|
|
end)
|
|
r:SetScript("OnLeave", function()
|
|
GameTooltip:Hide()
|
|
end)
|
|
controls.origins[key] = r
|
|
end
|
|
end
|
|
|
|
-- EditModeCheckButtonTemplate: 32 x 32 UI-CheckBox, label 5 px right of it.
|
|
-- x: left position; text: label; key: setting it toggles
|
|
local function checkButton(f, x, text, key)
|
|
local SEP = string.char(92)
|
|
local b = CreateFrame("CheckButton", nil, f)
|
|
b:SetWidth(N.check)
|
|
b:SetHeight(N.check)
|
|
b:SetPoint("TOPLEFT", f, "TOPLEFT", x, N.checkTop)
|
|
local path = "Interface" .. SEP .. "Buttons" .. SEP .. "UI-CheckBox-"
|
|
b:SetNormalTexture(path .. "Up")
|
|
b:SetPushedTexture(path .. "Down")
|
|
b:SetHighlightTexture(path .. "Highlight", "ADD")
|
|
b:SetCheckedTexture(path .. "Check")
|
|
b:SetDisabledCheckedTexture(path .. "Check-Disabled")
|
|
local label = b:CreateFontString(nil, "ARTWORK", "GameFontHighlightMedium")
|
|
label:SetPoint("LEFT", b, "RIGHT", N.checkLabel, 0)
|
|
label:SetText(text)
|
|
b:SetScript("OnClick", function(self)
|
|
C.settings[key] = Tpl.Truthy(self:GetChecked())
|
|
end)
|
|
return b
|
|
end
|
|
|
|
local function buildButtons(f)
|
|
local B = N.button
|
|
local cancel = ForeverUI.CreatePanelButton(f, CANCEL, B[1], B[2], "ForeverUICustomizeCancel", "GameFontNormal")
|
|
cancel:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", N.buttonX, N.buttonY)
|
|
cancel:SetScript("OnClick", function() C.Cancel() end)
|
|
local reset = ForeverUI.CreatePanelButton(f, RESET, B[1], B[2], "ForeverUICustomizeReset", "GameFontNormal")
|
|
reset:SetPoint("BOTTOM", f, "BOTTOM", 0, N.buttonY)
|
|
reset:SetScript("OnClick", function() StaticPopup_Show("FOREVERUI_CUSTOMIZE_RESET") end)
|
|
local validate = ForeverUI.CreatePanelButton(f, L.CUSTOMIZEUI_VALIDATE, B[1], B[2],
|
|
"ForeverUICustomizeValidate", "GameFontNormal")
|
|
validate:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -N.buttonX, N.buttonY)
|
|
validate:SetScript("OnClick", function() C.Validate() end)
|
|
end
|
|
|
|
-- DialogBorderTranslucentTemplate: Dialog border, black 0.8 inside 7 px from the edge
|
|
local function dialogBackground(f)
|
|
local background = f:CreateTexture(nil, "BACKGROUND")
|
|
background:SetTexture(0, 0, 0, 0.8)
|
|
background:SetPoint("TOPLEFT", f, "TOPLEFT", 7, -7)
|
|
background:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -7, 7)
|
|
Tpl.NineSlice(f, "Dialog")
|
|
end
|
|
|
|
-- Numeric field in the InputBoxTemplate border (Common-Input-Border: 8 wide ends, stretched
|
|
-- middle, 20 high, the left end 5 out). Enter applies the typed size; leaving the field shows
|
|
-- the size in effect again.
|
|
local function sizeField(f)
|
|
local SEP = string.char(92)
|
|
local field = CreateFrame("EditBox", "ForeverUICustomizeSizeField", f)
|
|
field:SetWidth(N.size.field[1])
|
|
field:SetHeight(N.size.field[2])
|
|
field:SetAutoFocus(false)
|
|
field:SetNumeric(true)
|
|
field:SetMaxLetters(3)
|
|
field:SetFontObject(ChatFontNormal)
|
|
field:SetJustifyH("CENTER")
|
|
local border = "Interface" .. SEP .. "Common" .. SEP .. "Common-Input-Border"
|
|
-- u1, u2: part of the border image; point: where it holds on the field
|
|
local function piece(u1, u2, point, x)
|
|
local t = field:CreateTexture(nil, "BACKGROUND")
|
|
t:SetTexture(border)
|
|
t:SetTexCoord(u1, u2, 0, 0.625)
|
|
t:SetHeight(20)
|
|
if point then
|
|
t:SetWidth(8)
|
|
t:SetPoint(point, field, point, x, 0)
|
|
end
|
|
return t
|
|
end
|
|
local left = piece(0, 0.0625, "LEFT", -5)
|
|
local right = piece(0.9375, 1, "RIGHT", 0)
|
|
local middle = piece(0.0625, 0.9375)
|
|
middle:SetPoint("LEFT", left, "RIGHT", 0, 0)
|
|
middle:SetPoint("RIGHT", right, "LEFT", 0, 0)
|
|
field:SetScript("OnEnterPressed", function(self)
|
|
local percent = tonumber(self:GetText())
|
|
if percent then
|
|
C.SetSize(percent)
|
|
end
|
|
self:ClearFocus()
|
|
end)
|
|
field:SetScript("OnEscapePressed", function(self)
|
|
self:ClearFocus()
|
|
end)
|
|
field:SetScript("OnEditFocusLost", function(self)
|
|
if C.selected then
|
|
self:SetText(tostring(selectedPercent()))
|
|
end
|
|
end)
|
|
return field
|
|
end
|
|
|
|
-- The selected element's window: its name, and its size in percent.
|
|
local function buildSizeWindow()
|
|
local S = N.size
|
|
local f = CreateFrame("Frame", "ForeverUICustomizeSize", UIParent)
|
|
f:SetWidth(S.width)
|
|
f:SetHeight(S.height)
|
|
f:SetFrameStrata(N.windowStrata)
|
|
f:SetToplevel(true)
|
|
f:SetClampedToScreen(true)
|
|
f:EnableMouse(true)
|
|
f:Hide()
|
|
dialogBackground(f)
|
|
f.title = f:CreateFontString(nil, "ARTWORK", "GameFontHighlightLarge")
|
|
f.title:SetPoint("TOP", f, "TOP", 0, N.title)
|
|
f.title:SetWidth(S.width - 2 * S.titleInset)
|
|
local close = CreateFrame("Button", nil, f, "UIPanelCloseButton")
|
|
Tpl.CloseButton(close, f)
|
|
close:SetScript("OnClick", function() C.Deselect() end)
|
|
local label = f:CreateFontString(nil, "ARTWORK", "GameFontHighlightMedium")
|
|
label:SetPoint("LEFT", f, "TOPLEFT", N.labelX, S.rowY)
|
|
label:SetText(L.CUSTOMIZEUI_SIZE)
|
|
f.field = sizeField(f)
|
|
f.field:SetPoint("LEFT", label, "RIGHT", S.fieldGap, 0)
|
|
local percent = f:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
|
|
percent:SetPoint("LEFT", f.field, "RIGHT", S.percentGap, 0)
|
|
percent:SetText(L.CUSTOMIZEUI_PERCENT)
|
|
sizeWindow = f
|
|
C.sizeWindow = f
|
|
end
|
|
|
|
local function build()
|
|
if window then
|
|
return window
|
|
end
|
|
local f = CreateFrame("Frame", "ForeverUICustomizeUI", UIParent)
|
|
f:SetWidth(N.width)
|
|
f:SetHeight(N.height)
|
|
f:SetPoint("TOP", UIParent, "TOP", 0, N.top)
|
|
f:SetFrameStrata(N.windowStrata)
|
|
f:SetToplevel(true)
|
|
f:EnableMouse(true)
|
|
f:SetMovable(true)
|
|
f:SetClampedToScreen(true)
|
|
f:RegisterForDrag("LeftButton")
|
|
f:SetScript("OnDragStart", f.StartMoving)
|
|
f:SetScript("OnDragStop", f.StopMovingOrSizing)
|
|
f:Hide()
|
|
dialogBackground(f)
|
|
|
|
local title = f:CreateFontString(nil, "ARTWORK", "GameFontHighlightLarge")
|
|
title:SetPoint("TOP", f, "TOP", 0, N.title)
|
|
title:SetText(L.CUSTOMIZEUI_TITLE)
|
|
|
|
local close = CreateFrame("Button", nil, f, "UIPanelCloseButton")
|
|
Tpl.CloseButton(close, f)
|
|
close:SetScript("OnClick", function() C.Cancel() end)
|
|
|
|
-- the controls start after the widest label (French ones are longer)
|
|
local widest = math.max(rowLabel(f, L.CUSTOMIZEUI_GRID_SPACING, N.rowY),
|
|
rowLabel(f, L.CUSTOMIZEUI_GRID_ORIGIN, N.originTop - N.box[2] / 2))
|
|
local column = math.max(N.column, N.labelX + widest + N.columnGap)
|
|
buildSpacing(f, column)
|
|
buildOrigin(f, column)
|
|
controls.snap = checkButton(f, N.labelX - 5, L.CUSTOMIZEUI_SNAP, "snap")
|
|
controls.sticky = checkButton(f, N.secondColumn, L.CUSTOMIZEUI_STICKY, "sticky")
|
|
buildButtons(f)
|
|
|
|
-- Esc and any other hide without Validate cancel the session.
|
|
f:SetScript("OnHide", function()
|
|
if C.active then
|
|
C.Cancel()
|
|
end
|
|
end)
|
|
table.insert(UISpecialFrames, "ForeverUICustomizeUI")
|
|
buildSizeWindow()
|
|
buildHandles()
|
|
window = f
|
|
C.window = f
|
|
C.controls = controls
|
|
return f
|
|
end
|
|
|
|
-- Shows the working settings in the window.
|
|
local function refreshControls()
|
|
local s = C.settings
|
|
controls.spacing:SetValue(s.spacing)
|
|
controls.spacingValue:SetText(tostring(s.spacing))
|
|
for key, r in pairs(controls.origins) do
|
|
r:SetChecked(key == s.origin)
|
|
end
|
|
controls.originName:SetText(ORIGIN_BY_KEY[s.origin][4])
|
|
controls.snap:SetChecked(s.snap)
|
|
controls.sticky:SetChecked(s.sticky)
|
|
end
|
|
|
|
-- ------------------------------------------------------------ Session
|
|
|
|
function C.SetOrigin(key)
|
|
if not ORIGIN_BY_KEY[key] then
|
|
return
|
|
end
|
|
C.settings.origin = key
|
|
refreshControls()
|
|
C.DrawGrid()
|
|
end
|
|
|
|
function C.Open()
|
|
if C.active then
|
|
return
|
|
end
|
|
if InCombatLockdown() then
|
|
Layout.Say(L.LAYOUT_EDIT_MODE_COMBAT)
|
|
return
|
|
end
|
|
build()
|
|
local s = saved()
|
|
C.settings = { spacing = s.spacing, origin = s.origin, snap = s.snap, sticky = s.sticky }
|
|
Layout.BeginEdit()
|
|
C.active = true
|
|
grid:Show()
|
|
C.DrawGrid()
|
|
for _, id in ipairs(Layout.order) do
|
|
showVeil(id)
|
|
end
|
|
window:Show()
|
|
refreshControls()
|
|
end
|
|
|
|
local function close()
|
|
C.active = false
|
|
drag:Hide()
|
|
drag.state = nil
|
|
C.Deselect()
|
|
grid:Hide()
|
|
for _, veil in pairs(veils) do
|
|
veil:Hide()
|
|
end
|
|
if window then
|
|
window:Hide()
|
|
end
|
|
end
|
|
|
|
-- Saves the layout and the grid settings, then closes.
|
|
function C.Validate()
|
|
if not C.active then
|
|
return
|
|
end
|
|
local s = saved()
|
|
for k in pairs(DEFAULTS) do
|
|
s[k] = C.settings[k]
|
|
end
|
|
Layout.Commit()
|
|
close()
|
|
end
|
|
|
|
-- Puts every element back where it was on opening, then closes.
|
|
function C.Cancel()
|
|
if not C.active then
|
|
return
|
|
end
|
|
Layout.Revert()
|
|
close()
|
|
end
|
|
|
|
-- Every element back to its default position and size; the session stays open.
|
|
function C.Reset()
|
|
if not C.active then
|
|
return
|
|
end
|
|
Layout.Reset()
|
|
for _, veil in pairs(veils) do
|
|
if veil:IsShown() then
|
|
unscale(veil)
|
|
end
|
|
end
|
|
if C.selected then
|
|
sizeWindow.field:SetText(tostring(selectedPercent()))
|
|
C.PlaceSizeWindow()
|
|
end
|
|
end
|
|
|
|
function C.Toggle()
|
|
if C.active then
|
|
C.Cancel()
|
|
else
|
|
C.Open()
|
|
end
|
|
end
|
|
|
|
Layout.onRegister = showVeil
|
|
|
|
StaticPopupDialogs["FOREVERUI_CUSTOMIZE_RESET"] = {
|
|
text = L.CUSTOMIZEUI_RESET_CONFIRM,
|
|
button1 = OKAY,
|
|
button2 = CANCEL,
|
|
OnAccept = function() C.Reset() end,
|
|
-- the client's popups sit below our windows: raised above them while shown
|
|
OnShow = function(self)
|
|
C.popupStrata = self:GetFrameStrata()
|
|
self:SetFrameStrata("FULLSCREEN_DIALOG")
|
|
end,
|
|
OnHide = function(self)
|
|
self:SetFrameStrata(C.popupStrata or "DIALOG")
|
|
end,
|
|
timeout = 0,
|
|
whileDead = 1,
|
|
hideOnEscape = 1,
|
|
}
|
|
|
|
-- Secure frames cannot move in combat: entering combat cancels the session (the event comes
|
|
-- before the lockdown, so the elements can still go back).
|
|
local watcher = CreateFrame("Frame")
|
|
watcher:RegisterEvent("PLAYER_REGEN_DISABLED")
|
|
watcher:SetScript("OnEvent", function()
|
|
if C.active then
|
|
C.Cancel()
|
|
Layout.Say(L.LAYOUT_EDIT_MODE_COMBAT)
|
|
end
|
|
end)
|
|
|
|
-- Esc menu button, after AddOns (camelot GameMenuFrame: Edit Mode); GameMenu.lua places it.
|
|
if GameMenuFrame then
|
|
local b = CreateFrame("Button", "ForeverUIGameMenuButtonCustomize", GameMenuFrame, "GameMenuButtonTemplate")
|
|
b:SetText(L.CUSTOMIZEUI_MENU_BUTTON)
|
|
b:SetScript("OnClick", function()
|
|
PlaySound("igMainMenuOption")
|
|
HideUIPanel(GameMenuFrame)
|
|
C.Open()
|
|
end)
|
|
ForeverUI.GameMenu.Layout()
|
|
end
|