-- 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 fractions = Layout.Fractions -- 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