修复拾取界面点击无效问题 修复宠物训练界面不显示训练点问题 新增天赋分享到聊天界面 修复飞行界面无法关闭问题 修复术士宠物的显示问题 为天赋界面添加默认数据库支持 框架现在也可自主选择是否启用 玩家框架和目标框架可取消显示3D头像以及透明度修改 背包和银行也添加透明度自定义支持 优化tooltip性能和背包物品显示方式 修复布局模式在ui缩放后不能正常定位的问题 添加硬核模式危险和死亡的工会通报 添加拾取和已拾取的框体 等等
999 lines
36 KiB
Lua
999 lines
36 KiB
Lua
--------------------------------------------------------------------------------
|
||
-- Nanami-UI: Layout Mode (Mover System)
|
||
--------------------------------------------------------------------------------
|
||
|
||
SFrames.Movers = {}
|
||
|
||
local M = SFrames.Movers
|
||
local registry = {}
|
||
local moverFrames = {}
|
||
local gridFrame = nil
|
||
local controlBar = nil
|
||
local overlayFrame = nil
|
||
local isLayoutMode = false
|
||
|
||
local GRID_SPACING = 50
|
||
local SNAP_THRESHOLD = 10
|
||
|
||
local MOVER_BACKDROP = {
|
||
bgFile = "Interface\\Buttons\\WHITE8x8",
|
||
edgeFile = "Interface\\Buttons\\WHITE8x8",
|
||
edgeSize = 1,
|
||
insets = { left = 1, right = 1, top = 1, bottom = 1 },
|
||
}
|
||
local MOVER_BACKDROP_2PX = {
|
||
bgFile = "Interface\\Buttons\\WHITE8x8",
|
||
edgeFile = "Interface\\Buttons\\WHITE8x8",
|
||
edgeSize = 2,
|
||
insets = { left = 2, right = 2, top = 2, bottom = 2 },
|
||
}
|
||
|
||
--------------------------------------------------------------------------------
|
||
-- Theme helper
|
||
--------------------------------------------------------------------------------
|
||
local function T()
|
||
return SFrames.ActiveTheme or {}
|
||
end
|
||
|
||
local function Font()
|
||
return (SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF"
|
||
end
|
||
|
||
--------------------------------------------------------------------------------
|
||
-- Config helpers
|
||
--------------------------------------------------------------------------------
|
||
local function GetLayoutCfg()
|
||
if not SFramesDB then SFramesDB = {} end
|
||
if type(SFramesDB.layoutMode) ~= "table" then
|
||
SFramesDB.layoutMode = {
|
||
snapEnabled = true,
|
||
snapThreshold = SNAP_THRESHOLD,
|
||
showGrid = true,
|
||
}
|
||
end
|
||
return SFramesDB.layoutMode
|
||
end
|
||
|
||
local function EnsurePositions()
|
||
if not SFramesDB then SFramesDB = {} end
|
||
if not SFramesDB.Positions then SFramesDB.Positions = {} end
|
||
return SFramesDB.Positions
|
||
end
|
||
|
||
--------------------------------------------------------------------------------
|
||
-- Snap logic
|
||
--------------------------------------------------------------------------------
|
||
local function GetFrameEdges(frame)
|
||
local l = frame:GetLeft() or 0
|
||
local r = frame:GetRight() or 0
|
||
local tp = frame:GetTop() or 0
|
||
local b = frame:GetBottom() or 0
|
||
return l, r, tp, b, (l + r) / 2, (tp + b) / 2
|
||
end
|
||
|
||
local function ApplySnap(mover)
|
||
local cfg = GetLayoutCfg()
|
||
if not cfg.snapEnabled then return end
|
||
if IsShiftKeyDown() then return end
|
||
|
||
local threshold = cfg.snapThreshold or SNAP_THRESHOLD
|
||
local ml, mr, mt, mb, mcx, mcy = GetFrameEdges(mover)
|
||
local mw, mh = mr - ml, mt - mb
|
||
|
||
local screenW = UIParent:GetRight() or UIParent:GetWidth()
|
||
local screenH = UIParent:GetTop() or UIParent:GetHeight()
|
||
local screenCX, screenCY = screenW / 2, screenH / 2
|
||
|
||
local snapX, snapY = nil, nil
|
||
local bestDX, bestDY = threshold + 1, threshold + 1
|
||
|
||
if math.abs(ml) < bestDX then bestDX = math.abs(ml); snapX = 0 end
|
||
if math.abs(mr - screenW) < bestDX then bestDX = math.abs(mr - screenW); snapX = screenW - mw end
|
||
if math.abs(mt - screenH) < bestDY then bestDY = math.abs(mt - screenH); snapY = screenH - mh end
|
||
if math.abs(mb) < bestDY then bestDY = math.abs(mb); snapY = 0 end
|
||
|
||
if math.abs(mcx - screenCX) < bestDX then bestDX = math.abs(mcx - screenCX); snapX = screenCX - mw / 2 end
|
||
if math.abs(mcy - screenCY) < bestDY then bestDY = math.abs(mcy - screenCY); snapY = screenCY - mh / 2 end
|
||
|
||
for name, _ in pairs(registry) do
|
||
local other = moverFrames[name]
|
||
if other and other ~= mover and other:IsShown() then
|
||
local ol, or2, ot, ob, ocx, ocy = GetFrameEdges(other)
|
||
local px = {
|
||
{ ml, or2, 0 }, { mr, ol, -mw }, { ml, ol, 0 },
|
||
{ mr, or2, -mw }, { mcx, ocx, -mw / 2 },
|
||
}
|
||
for _, p in ipairs(px) do
|
||
local d = math.abs(p[1] - p[2])
|
||
if d < bestDX then bestDX = d; snapX = p[2] + p[3] end
|
||
end
|
||
local py = {
|
||
{ math.abs(mb - ot), ot }, { math.abs(mt - ob), ob - mh },
|
||
{ math.abs(mt - ot), ot - mh }, { math.abs(mb - ob), ob },
|
||
{ math.abs(mcy - ocy), ocy - mh / 2 },
|
||
}
|
||
for _, p in ipairs(py) do
|
||
if p[1] < bestDY then bestDY = p[1]; snapY = p[2] end
|
||
end
|
||
end
|
||
end
|
||
|
||
if bestDX <= threshold and snapX then
|
||
mover:ClearAllPoints()
|
||
local curB = (bestDY <= threshold and snapY) or (mover:GetBottom() or 0)
|
||
mover:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", snapX, curB)
|
||
return
|
||
end
|
||
if bestDY <= threshold and snapY then
|
||
mover:ClearAllPoints()
|
||
mover:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", mover:GetLeft() or 0, snapY)
|
||
end
|
||
end
|
||
|
||
--------------------------------------------------------------------------------
|
||
-- Position save / load
|
||
--------------------------------------------------------------------------------
|
||
local function SaveMoverPosition(name, mover)
|
||
local positions = EnsurePositions()
|
||
local l = mover:GetLeft()
|
||
local b = mover:GetBottom()
|
||
local screenW = UIParent:GetRight() or UIParent:GetWidth()
|
||
local screenH = UIParent:GetTop() or UIParent:GetHeight()
|
||
if not l or not b then return end
|
||
|
||
local cx = l + (mover:GetWidth() or 0) / 2
|
||
local cy = b + (mover:GetHeight() or 0) / 2
|
||
|
||
local point, relPoint, xOfs, yOfs
|
||
if cx < screenW / 3 then
|
||
if cy > screenH * 2 / 3 then
|
||
point = "TOPLEFT"; relPoint = "TOPLEFT"
|
||
xOfs = l; yOfs = -(screenH - (b + (mover:GetHeight() or 0)))
|
||
elseif cy < screenH / 3 then
|
||
point = "BOTTOMLEFT"; relPoint = "BOTTOMLEFT"
|
||
xOfs = l; yOfs = b
|
||
else
|
||
point = "LEFT"; relPoint = "LEFT"
|
||
xOfs = l; yOfs = cy - screenH / 2
|
||
end
|
||
elseif cx > screenW * 2 / 3 then
|
||
local r = mover:GetRight() or 0
|
||
if cy > screenH * 2 / 3 then
|
||
point = "TOPRIGHT"; relPoint = "TOPRIGHT"
|
||
xOfs = r - screenW; yOfs = -(screenH - (b + (mover:GetHeight() or 0)))
|
||
elseif cy < screenH / 3 then
|
||
point = "BOTTOMRIGHT"; relPoint = "BOTTOMRIGHT"
|
||
xOfs = r - screenW; yOfs = b
|
||
else
|
||
point = "RIGHT"; relPoint = "RIGHT"
|
||
xOfs = r - screenW; yOfs = cy - screenH / 2
|
||
end
|
||
else
|
||
if cy > screenH * 2 / 3 then
|
||
point = "TOP"; relPoint = "TOP"
|
||
xOfs = cx - screenW / 2; yOfs = -(screenH - (b + (mover:GetHeight() or 0)))
|
||
elseif cy < screenH / 3 then
|
||
point = "BOTTOM"; relPoint = "BOTTOM"
|
||
xOfs = cx - screenW / 2; yOfs = b
|
||
else
|
||
point = "CENTER"; relPoint = "CENTER"
|
||
xOfs = cx - screenW / 2; yOfs = cy - screenH / 2
|
||
end
|
||
end
|
||
|
||
positions[name] = { point = point, relativePoint = relPoint, xOfs = xOfs, yOfs = yOfs }
|
||
end
|
||
|
||
--------------------------------------------------------------------------------
|
||
-- Sync mover <-> actual frame
|
||
--------------------------------------------------------------------------------
|
||
local function UpdateCoordText(mover)
|
||
if not mover or not mover._coordText then return end
|
||
local l = mover:GetLeft()
|
||
local b = mover:GetBottom()
|
||
if l and b then
|
||
mover._coordText:SetText(string.format("%.0f, %.0f", l, b))
|
||
end
|
||
end
|
||
|
||
local function SyncMoverToFrame(name)
|
||
local entry = registry[name]
|
||
local mover = moverFrames[name]
|
||
if not entry or not mover then return end
|
||
local frame = entry.frame
|
||
if not frame then return end
|
||
|
||
local scale = frame:GetEffectiveScale() / UIParent:GetEffectiveScale()
|
||
local w = (frame:GetWidth() or 100) * scale
|
||
local h = (frame:GetHeight() or 50) * scale
|
||
|
||
mover:SetWidth(math.max(w, 72))
|
||
mover:SetHeight(math.max(h, 40))
|
||
|
||
local l = frame:GetLeft()
|
||
local b = frame:GetBottom()
|
||
if l and b then
|
||
mover:ClearAllPoints()
|
||
mover:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", l * scale, b * scale)
|
||
else
|
||
local positions = EnsurePositions()
|
||
local pos = positions[name]
|
||
if pos and pos.point and pos.relativePoint then
|
||
local tempF = CreateFrame("Frame", nil, UIParent)
|
||
tempF:SetWidth(w)
|
||
tempF:SetHeight(h)
|
||
tempF:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)
|
||
local tl = tempF:GetLeft()
|
||
local tb = tempF:GetBottom()
|
||
if tl and tb then
|
||
mover:ClearAllPoints()
|
||
mover:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", tl, tb)
|
||
end
|
||
tempF:Hide()
|
||
else
|
||
mover:ClearAllPoints()
|
||
local relTo = _G[entry.defaultRelativeTo] or UIParent
|
||
mover:SetPoint(entry.defaultPoint, relTo, entry.defaultRelPoint,
|
||
entry.defaultX or 0, entry.defaultY or 0)
|
||
end
|
||
end
|
||
UpdateCoordText(mover)
|
||
end
|
||
|
||
local function SyncFrameToMover(name)
|
||
local entry = registry[name]
|
||
local mover = moverFrames[name]
|
||
if not entry or not mover then return end
|
||
local frame = entry.frame
|
||
if not frame then return end
|
||
|
||
local moverL = mover:GetLeft() or 0
|
||
local moverB = mover:GetBottom() or 0
|
||
|
||
SaveMoverPosition(name, mover)
|
||
|
||
local positions = EnsurePositions()
|
||
local pos = positions[name]
|
||
if pos then
|
||
frame:ClearAllPoints()
|
||
frame:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)
|
||
|
||
local scale = frame:GetEffectiveScale() / UIParent:GetEffectiveScale()
|
||
local newL = frame:GetLeft() or 0
|
||
local newB = frame:GetBottom() or 0
|
||
local dL = newL * scale - moverL
|
||
local dB = newB * scale - moverB
|
||
if math.abs(dL) > 2 or math.abs(dB) > 2 then
|
||
SFrames:Print(string.format(
|
||
"|cffff6666[位置偏差]|r |cffaaddff%s|r anchor=%s ofs=(%.1f,%.1f) dL=%.1f dB=%.1f scale=%.2f",
|
||
entry.label or name, pos.point, pos.xOfs or 0, pos.yOfs or 0, dL, dB, scale))
|
||
end
|
||
end
|
||
|
||
if entry.onMoved then entry.onMoved() end
|
||
UpdateCoordText(mover)
|
||
end
|
||
|
||
--------------------------------------------------------------------------------
|
||
-- Nudge helper
|
||
--------------------------------------------------------------------------------
|
||
local function NudgeMover(name, dx, dy)
|
||
local mover = moverFrames[name]
|
||
if not mover then return end
|
||
local l = mover:GetLeft()
|
||
local b = mover:GetBottom()
|
||
if not l or not b then return end
|
||
mover:ClearAllPoints()
|
||
mover:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", l + dx, b + dy)
|
||
SyncFrameToMover(name)
|
||
end
|
||
|
||
--------------------------------------------------------------------------------
|
||
-- Pixel-art triangle arrow helper
|
||
-- Draws a triangle using 4 horizontal/vertical strips for a clean geometric look
|
||
--------------------------------------------------------------------------------
|
||
local function CreateTriangle(parent, direction, r, g, b, a)
|
||
local strips = {}
|
||
|
||
if direction == "UP" then
|
||
local widths = { 2, 5, 8, 11 }
|
||
local heights = { 2, 2, 2, 2 }
|
||
local yOfs = { 3, 1, -1, -3 }
|
||
for i = 1, 4 do
|
||
local s = parent:CreateTexture(nil, "OVERLAY")
|
||
s:SetTexture("Interface\\Buttons\\WHITE8x8")
|
||
s:SetWidth(widths[i])
|
||
s:SetHeight(heights[i])
|
||
s:SetPoint("CENTER", parent, "CENTER", 0, yOfs[i])
|
||
s:SetVertexColor(r, g, b, a)
|
||
table.insert(strips, s)
|
||
end
|
||
elseif direction == "DOWN" then
|
||
local widths = { 11, 8, 5, 2 }
|
||
local heights = { 2, 2, 2, 2 }
|
||
local yOfs = { 3, 1, -1, -3 }
|
||
for i = 1, 4 do
|
||
local s = parent:CreateTexture(nil, "OVERLAY")
|
||
s:SetTexture("Interface\\Buttons\\WHITE8x8")
|
||
s:SetWidth(widths[i])
|
||
s:SetHeight(heights[i])
|
||
s:SetPoint("CENTER", parent, "CENTER", 0, yOfs[i])
|
||
s:SetVertexColor(r, g, b, a)
|
||
table.insert(strips, s)
|
||
end
|
||
elseif direction == "LEFT" then
|
||
local widths = { 2, 2, 2, 2 }
|
||
local heights = { 2, 5, 8, 11 }
|
||
local xOfs = { -3, -1, 1, 3 }
|
||
for i = 1, 4 do
|
||
local s = parent:CreateTexture(nil, "OVERLAY")
|
||
s:SetTexture("Interface\\Buttons\\WHITE8x8")
|
||
s:SetWidth(widths[i])
|
||
s:SetHeight(heights[i])
|
||
s:SetPoint("CENTER", parent, "CENTER", xOfs[i], 0)
|
||
s:SetVertexColor(r, g, b, a)
|
||
table.insert(strips, s)
|
||
end
|
||
elseif direction == "RIGHT" then
|
||
local widths = { 2, 2, 2, 2 }
|
||
local heights = { 11, 8, 5, 2 }
|
||
local xOfs = { -3, -1, 1, 3 }
|
||
for i = 1, 4 do
|
||
local s = parent:CreateTexture(nil, "OVERLAY")
|
||
s:SetTexture("Interface\\Buttons\\WHITE8x8")
|
||
s:SetWidth(widths[i])
|
||
s:SetHeight(heights[i])
|
||
s:SetPoint("CENTER", parent, "CENTER", xOfs[i], 0)
|
||
s:SetVertexColor(r, g, b, a)
|
||
table.insert(strips, s)
|
||
end
|
||
end
|
||
|
||
return strips
|
||
end
|
||
|
||
local function SetTriangleColor(strips, r, g, b, a)
|
||
for _, s in ipairs(strips) do
|
||
s:SetVertexColor(r, g, b, a)
|
||
end
|
||
end
|
||
|
||
--------------------------------------------------------------------------------
|
||
-- Arrow nudge button (triangle-based)
|
||
--------------------------------------------------------------------------------
|
||
local ARROW_SIZE = 16
|
||
local ARROW_REPEAT_DELAY = 0.45
|
||
local ARROW_REPEAT_RATE = 0.04
|
||
|
||
local function CreateArrowButton(parent, moverName, anchor, ofsX, ofsY, direction, dx, dy)
|
||
local btn = CreateFrame("Button", nil, parent)
|
||
btn:SetWidth(ARROW_SIZE)
|
||
btn:SetHeight(ARROW_SIZE)
|
||
btn:SetPoint(anchor, parent, anchor, ofsX, ofsY)
|
||
btn:SetFrameLevel(parent:GetFrameLevel() + 5)
|
||
btn:SetMovable(true)
|
||
btn:RegisterForDrag("LeftButton")
|
||
|
||
local th = T()
|
||
local dimC = th.dimText or { 0.5, 0.5, 0.55 }
|
||
local accentC = th.accent or { 1, 0.5, 0.8 }
|
||
|
||
local bgTex = btn:CreateTexture(nil, "BACKGROUND")
|
||
bgTex:SetTexture("Interface\\Buttons\\WHITE8x8")
|
||
bgTex:SetAllPoints(btn)
|
||
bgTex:SetVertexColor(0, 0, 0, 0)
|
||
btn._bg = bgTex
|
||
|
||
local tri = CreateTriangle(btn, direction, dimC[1], dimC[2], dimC[3], 0.55)
|
||
btn._tri = tri
|
||
|
||
btn._elapsed = 0
|
||
btn._held = false
|
||
btn._dragging = false
|
||
btn._delay = ARROW_REPEAT_DELAY
|
||
|
||
btn:SetScript("OnClick", function()
|
||
if not this._dragging then
|
||
NudgeMover(moverName, dx, dy)
|
||
end
|
||
this._dragging = false
|
||
end)
|
||
btn:SetScript("OnMouseDown", function()
|
||
this._held = true
|
||
this._dragging = false
|
||
this._elapsed = 0
|
||
this._delay = ARROW_REPEAT_DELAY
|
||
end)
|
||
btn:SetScript("OnMouseUp", function()
|
||
this._held = false
|
||
end)
|
||
btn:SetScript("OnDragStart", function()
|
||
this._held = false
|
||
this._dragging = true
|
||
parent:StartMoving()
|
||
end)
|
||
btn:SetScript("OnDragStop", function()
|
||
parent:StopMovingOrSizing()
|
||
ApplySnap(parent)
|
||
SyncFrameToMover(moverName)
|
||
this._dragging = false
|
||
end)
|
||
btn:SetScript("OnUpdate", function()
|
||
if not this._held then return end
|
||
this._elapsed = this._elapsed + arg1
|
||
if this._elapsed >= this._delay then
|
||
this._elapsed = 0
|
||
this._delay = ARROW_REPEAT_RATE
|
||
NudgeMover(moverName, dx, dy)
|
||
end
|
||
end)
|
||
btn:SetScript("OnEnter", function()
|
||
this._bg:SetVertexColor(accentC[1], accentC[2], accentC[3], 0.18)
|
||
SetTriangleColor(this._tri, 1, 1, 1, 1)
|
||
if parent._SetHover then parent:_SetHover(true) end
|
||
end)
|
||
btn:SetScript("OnLeave", function()
|
||
this._held = false
|
||
this._bg:SetVertexColor(0, 0, 0, 0)
|
||
SetTriangleColor(this._tri, dimC[1], dimC[2], dimC[3], 0.55)
|
||
if parent._SetHover then parent:_SetHover(false) end
|
||
end)
|
||
|
||
return btn
|
||
end
|
||
|
||
--------------------------------------------------------------------------------
|
||
-- Create individual mover frame
|
||
--------------------------------------------------------------------------------
|
||
local function CreateMoverFrame(name, entry)
|
||
if moverFrames[name] then return moverFrames[name] end
|
||
|
||
local mover = CreateFrame("Button", "SFramesMover_" .. name, UIParent)
|
||
mover:SetFrameStrata("FULLSCREEN")
|
||
mover:SetFrameLevel(100)
|
||
mover:SetWidth(100)
|
||
mover:SetHeight(40)
|
||
mover:SetClampedToScreen(true)
|
||
mover:SetMovable(true)
|
||
mover:EnableMouse(true)
|
||
mover:RegisterForDrag("LeftButton")
|
||
mover:RegisterForClicks("RightButtonUp")
|
||
|
||
mover:SetBackdrop(MOVER_BACKDROP_2PX)
|
||
|
||
local th = T()
|
||
local secBg = th.sectionBg or { 0.12, 0.10, 0.18, 0.82 }
|
||
local accent = th.accent or { 1, 0.5, 0.8, 0.98 }
|
||
local accentLine = th.accentLine or { 0.6, 0.9, 1, 0.9 }
|
||
|
||
mover:SetBackdropColor(secBg[1], secBg[2], secBg[3], secBg[4] or 0.82)
|
||
mover:SetBackdropBorderColor(accent[1], accent[2], accent[3], 0.6)
|
||
|
||
-- Top accent stripe
|
||
local stripe = mover:CreateTexture(nil, "ARTWORK")
|
||
stripe:SetTexture("Interface\\Buttons\\WHITE8x8")
|
||
stripe:SetHeight(2)
|
||
stripe:SetPoint("TOPLEFT", mover, "TOPLEFT", 2, -2)
|
||
stripe:SetPoint("TOPRIGHT", mover, "TOPRIGHT", -2, -2)
|
||
stripe:SetVertexColor(accentLine[1], accentLine[2], accentLine[3], accentLine[4] or 0.9)
|
||
mover._stripe = stripe
|
||
|
||
-- Label
|
||
local label = mover:CreateFontString(nil, "OVERLAY")
|
||
label:SetFont(Font(), 11, "OUTLINE")
|
||
label:SetPoint("CENTER", mover, "CENTER", 0, 5)
|
||
label:SetText(entry.label or name)
|
||
local titleC = th.title or { 1, 0.9, 1 }
|
||
label:SetTextColor(titleC[1], titleC[2], titleC[3], 1)
|
||
mover._label = label
|
||
|
||
-- Coordinate readout
|
||
local coord = mover:CreateFontString(nil, "OVERLAY")
|
||
coord:SetFont(Font(), 9, "OUTLINE")
|
||
coord:SetPoint("CENTER", mover, "CENTER", 0, -7)
|
||
local dimC = th.dimText or { 0.5, 0.5, 0.55 }
|
||
coord:SetTextColor(dimC[1], dimC[2], dimC[3], 0.9)
|
||
coord:SetText("")
|
||
mover._coordText = coord
|
||
|
||
-- Triangle arrow buttons
|
||
CreateArrowButton(mover, name, "TOP", 0, -1, "UP", 0, 1)
|
||
CreateArrowButton(mover, name, "BOTTOM", 0, 1, "DOWN", 0, -1)
|
||
CreateArrowButton(mover, name, "LEFT", 1, 0, "LEFT", -1, 0)
|
||
CreateArrowButton(mover, name, "RIGHT", -1, 0, "RIGHT", 1, 0)
|
||
|
||
-- Hover state manager (tracks child enter/leave)
|
||
local hoverCount = 0
|
||
function mover:_SetHover(isEnter)
|
||
if isEnter then
|
||
hoverCount = hoverCount + 1
|
||
else
|
||
hoverCount = hoverCount - 1
|
||
if hoverCount < 0 then hoverCount = 0 end
|
||
end
|
||
if hoverCount > 0 then
|
||
self:SetBackdropBorderColor(1, 1, 1, 0.95)
|
||
stripe:SetVertexColor(1, 1, 1, 1)
|
||
else
|
||
self:SetBackdropBorderColor(accent[1], accent[2], accent[3], 0.6)
|
||
stripe:SetVertexColor(accentLine[1], accentLine[2], accentLine[3], accentLine[4] or 0.9)
|
||
end
|
||
end
|
||
|
||
mover:SetScript("OnDragStart", function()
|
||
this:StartMoving()
|
||
end)
|
||
|
||
mover:SetScript("OnDragStop", function()
|
||
this:StopMovingOrSizing()
|
||
ApplySnap(this)
|
||
SyncFrameToMover(name)
|
||
end)
|
||
|
||
mover:SetScript("OnClick", function()
|
||
if arg1 == "RightButton" then
|
||
M:ResetMover(name)
|
||
end
|
||
end)
|
||
|
||
mover:SetScript("OnEnter", function()
|
||
this:_SetHover(true)
|
||
GameTooltip:SetOwner(this, "ANCHOR_TOP")
|
||
GameTooltip:ClearLines()
|
||
GameTooltip:AddLine(entry.label or name, 1, 0.84, 0.94)
|
||
GameTooltip:AddDoubleLine("拖拽", "移动位置", 0.7, 0.7, 0.7, 0.7, 0.7, 0.7)
|
||
GameTooltip:AddDoubleLine("箭头", "微调 1 像素", 0.7, 0.7, 0.7, 0.7, 0.7, 0.7)
|
||
GameTooltip:AddDoubleLine("右键", "重置位置", 0.7, 0.7, 0.7, 0.7, 0.7, 0.7)
|
||
GameTooltip:AddDoubleLine("Shift+拖拽", "禁用磁吸", 0.7, 0.7, 0.7, 0.7, 0.7, 0.7)
|
||
GameTooltip:Show()
|
||
end)
|
||
|
||
mover:SetScript("OnLeave", function()
|
||
this:_SetHover(false)
|
||
GameTooltip:Hide()
|
||
end)
|
||
|
||
mover:Hide()
|
||
moverFrames[name] = mover
|
||
return mover
|
||
end
|
||
|
||
--------------------------------------------------------------------------------
|
||
-- Dark overlay
|
||
--------------------------------------------------------------------------------
|
||
local function CreateOverlay()
|
||
if overlayFrame then return overlayFrame end
|
||
|
||
overlayFrame = CreateFrame("Frame", "SFramesLayoutOverlay", UIParent)
|
||
overlayFrame:SetFrameStrata("DIALOG")
|
||
overlayFrame:SetFrameLevel(0)
|
||
overlayFrame:SetAllPoints(UIParent)
|
||
overlayFrame:EnableMouse(false)
|
||
|
||
local bg = overlayFrame:CreateTexture(nil, "BACKGROUND")
|
||
bg:SetTexture("Interface\\Buttons\\WHITE8x8")
|
||
bg:SetAllPoints(overlayFrame)
|
||
bg:SetVertexColor(0, 0, 0, 0.45)
|
||
overlayFrame._bg = bg
|
||
|
||
overlayFrame:Hide()
|
||
return overlayFrame
|
||
end
|
||
|
||
--------------------------------------------------------------------------------
|
||
-- Grid overlay
|
||
--------------------------------------------------------------------------------
|
||
local function CreateGrid()
|
||
if gridFrame then return gridFrame end
|
||
|
||
gridFrame = CreateFrame("Frame", "SFramesLayoutGrid", UIParent)
|
||
gridFrame:SetFrameStrata("DIALOG")
|
||
gridFrame:SetFrameLevel(1)
|
||
gridFrame:SetAllPoints(UIParent)
|
||
gridFrame:EnableMouse(false)
|
||
gridFrame._lines = {}
|
||
|
||
local th = T()
|
||
local axisColor = th.accent or { 1, 0.5, 0.8 }
|
||
local lineColor = th.dimText or { 0.5, 0.5, 0.55 }
|
||
|
||
local function MakeLine(r, g, b, a)
|
||
local tex = gridFrame:CreateTexture(nil, "BACKGROUND")
|
||
tex:SetTexture("Interface\\Buttons\\WHITE8x8")
|
||
tex:SetVertexColor(r, g, b, a)
|
||
table.insert(gridFrame._lines, tex)
|
||
return tex
|
||
end
|
||
|
||
local screenW = UIParent:GetRight() or UIParent:GetWidth()
|
||
local screenH = UIParent:GetTop() or UIParent:GetHeight()
|
||
local halfW = math.floor(screenW / 2)
|
||
local halfH = math.floor(screenH / 2)
|
||
|
||
local cv = MakeLine(axisColor[1], axisColor[2], axisColor[3], 0.45)
|
||
cv:SetWidth(1)
|
||
cv:SetPoint("TOP", gridFrame, "TOP", 0, 0)
|
||
cv:SetPoint("BOTTOM", gridFrame, "BOTTOM", 0, 0)
|
||
|
||
local ch = MakeLine(axisColor[1], axisColor[2], axisColor[3], 0.45)
|
||
ch:SetHeight(1)
|
||
ch:SetPoint("LEFT", gridFrame, "LEFT", 0, 0)
|
||
ch:SetPoint("RIGHT", gridFrame, "RIGHT", 0, 0)
|
||
|
||
local i = GRID_SPACING
|
||
while i < halfW do
|
||
local vl = MakeLine(lineColor[1], lineColor[2], lineColor[3], 0.10)
|
||
vl:SetWidth(1)
|
||
vl:SetPoint("TOP", gridFrame, "TOP", -i, 0)
|
||
vl:SetPoint("BOTTOM", gridFrame, "BOTTOM", -i, 0)
|
||
|
||
local vr = MakeLine(lineColor[1], lineColor[2], lineColor[3], 0.10)
|
||
vr:SetWidth(1)
|
||
vr:SetPoint("TOP", gridFrame, "TOP", i, 0)
|
||
vr:SetPoint("BOTTOM", gridFrame, "BOTTOM", i, 0)
|
||
i = i + GRID_SPACING
|
||
end
|
||
|
||
i = GRID_SPACING
|
||
while i < halfH do
|
||
local ha = MakeLine(lineColor[1], lineColor[2], lineColor[3], 0.10)
|
||
ha:SetHeight(1)
|
||
ha:SetPoint("LEFT", gridFrame, "LEFT", 0, i)
|
||
ha:SetPoint("RIGHT", gridFrame, "RIGHT", 0, i)
|
||
|
||
local hb = MakeLine(lineColor[1], lineColor[2], lineColor[3], 0.10)
|
||
hb:SetHeight(1)
|
||
hb:SetPoint("LEFT", gridFrame, "LEFT", 0, -i)
|
||
hb:SetPoint("RIGHT", gridFrame, "RIGHT", 0, -i)
|
||
i = i + GRID_SPACING
|
||
end
|
||
|
||
gridFrame:Hide()
|
||
return gridFrame
|
||
end
|
||
|
||
--------------------------------------------------------------------------------
|
||
-- Rounded button helper (for control bar) – matches UI-wide rounded style
|
||
--------------------------------------------------------------------------------
|
||
local ROUND_BACKDROP = {
|
||
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
||
tile = true, tileSize = 16, edgeSize = 14,
|
||
insets = { left = 3, right = 3, top = 3, bottom = 3 },
|
||
}
|
||
|
||
local function MakeControlButton(parent, text, width, xOfs, onClick)
|
||
local th = T()
|
||
local btnBg = th.buttonBg or { 0.16, 0.12, 0.22, 0.94 }
|
||
local btnBd = th.buttonBorder or { 0.35, 0.30, 0.50, 0.90 }
|
||
local btnHBg = th.buttonHoverBg or { 0.22, 0.18, 0.30, 0.96 }
|
||
local btnDnBg = th.buttonDownBg or { 0.08, 0.04, 0.06, 0.95 }
|
||
local btnText = th.buttonText or { 0.85, 0.82, 0.92 }
|
||
local btnHTxt = th.buttonActiveText or { 1, 0.92, 0.96 }
|
||
|
||
local btn = CreateFrame("Button", nil, parent)
|
||
btn:SetWidth(width)
|
||
btn:SetHeight(26)
|
||
btn:SetPoint("LEFT", parent, "LEFT", xOfs, 0)
|
||
btn:SetBackdrop(ROUND_BACKDROP)
|
||
btn:SetBackdropColor(btnBg[1], btnBg[2], btnBg[3], btnBg[4] or 0.94)
|
||
btn:SetBackdropBorderColor(btnBd[1], btnBd[2], btnBd[3], btnBd[4] or 0.90)
|
||
|
||
local fs = btn:CreateFontString(nil, "OVERLAY")
|
||
fs:SetFont(Font(), 10, "OUTLINE")
|
||
fs:SetPoint("CENTER", 0, 0)
|
||
fs:SetText(text)
|
||
fs:SetTextColor(btnText[1], btnText[2], btnText[3], 1)
|
||
btn._text = fs
|
||
|
||
btn:SetScript("OnClick", onClick)
|
||
btn:SetScript("OnEnter", function()
|
||
this:SetBackdropColor(btnHBg[1], btnHBg[2], btnHBg[3], btnHBg[4] or 0.96)
|
||
local a = th.accent or { 1, 0.5, 0.8 }
|
||
this:SetBackdropBorderColor(a[1], a[2], a[3], 0.95)
|
||
this._text:SetTextColor(btnHTxt[1], btnHTxt[2], btnHTxt[3], 1)
|
||
end)
|
||
btn:SetScript("OnLeave", function()
|
||
this:SetBackdropColor(btnBg[1], btnBg[2], btnBg[3], btnBg[4] or 0.94)
|
||
this:SetBackdropBorderColor(btnBd[1], btnBd[2], btnBd[3], btnBd[4] or 0.90)
|
||
this._text:SetTextColor(btnText[1], btnText[2], btnText[3], 1)
|
||
end)
|
||
btn:SetScript("OnMouseDown", function()
|
||
this:SetBackdropColor(btnDnBg[1], btnDnBg[2], btnDnBg[3], btnDnBg[4] or 0.95)
|
||
end)
|
||
btn:SetScript("OnMouseUp", function()
|
||
this:SetBackdropColor(btnHBg[1], btnHBg[2], btnHBg[3], btnHBg[4] or 0.96)
|
||
end)
|
||
|
||
return btn
|
||
end
|
||
|
||
--------------------------------------------------------------------------------
|
||
-- Control bar
|
||
--------------------------------------------------------------------------------
|
||
local function CreateControlBar()
|
||
if controlBar then return controlBar end
|
||
|
||
local th = T()
|
||
local panelBg = th.headerBg or th.panelBg or { 0.08, 0.06, 0.12, 0.98 }
|
||
local panelBd = th.panelBorder or { 0.35, 0.30, 0.50, 0.90 }
|
||
local accent = th.accent or { 1, 0.5, 0.8, 0.98 }
|
||
local titleC = th.title or { 1, 0.88, 1 }
|
||
|
||
controlBar = CreateFrame("Frame", "SFramesLayoutControlBar", UIParent)
|
||
controlBar:SetFrameStrata("FULLSCREEN_DIALOG")
|
||
controlBar:SetFrameLevel(200)
|
||
controlBar:SetWidth(480)
|
||
controlBar:SetHeight(44)
|
||
controlBar:SetPoint("TOP", UIParent, "TOP", 0, -8)
|
||
controlBar:SetClampedToScreen(true)
|
||
controlBar:SetMovable(true)
|
||
controlBar:EnableMouse(true)
|
||
controlBar:RegisterForDrag("LeftButton")
|
||
controlBar:SetScript("OnDragStart", function() this:StartMoving() end)
|
||
controlBar:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
|
||
|
||
controlBar:SetBackdrop(ROUND_BACKDROP)
|
||
controlBar:SetBackdropColor(panelBg[1], panelBg[2], panelBg[3], panelBg[4] or 0.98)
|
||
controlBar:SetBackdropBorderColor(panelBd[1], panelBd[2], panelBd[3], panelBd[4] or 0.90)
|
||
|
||
local title = controlBar:CreateFontString(nil, "OVERLAY")
|
||
title:SetFont(Font(), 13, "OUTLINE")
|
||
title:SetPoint("LEFT", controlBar, "LEFT", 14, 0)
|
||
title:SetText("Nanami 布局")
|
||
title:SetTextColor(titleC[1], titleC[2], titleC[3], 1)
|
||
|
||
local sep = controlBar:CreateTexture(nil, "ARTWORK")
|
||
sep:SetTexture("Interface\\Buttons\\WHITE8x8")
|
||
sep:SetWidth(1)
|
||
sep:SetHeight(24)
|
||
sep:SetPoint("LEFT", controlBar, "LEFT", 118, 0)
|
||
sep:SetVertexColor(panelBd[1], panelBd[2], panelBd[3], 0.5)
|
||
|
||
local bx = 128
|
||
|
||
-- Snap toggle
|
||
local snapBtn = MakeControlButton(controlBar, "", 76, bx, function()
|
||
local cfg = GetLayoutCfg()
|
||
cfg.snapEnabled = not cfg.snapEnabled
|
||
if controlBar._updateSnap then controlBar._updateSnap() end
|
||
end)
|
||
controlBar.snapBtn = snapBtn
|
||
|
||
local function UpdateSnapBtnText()
|
||
local cfg = GetLayoutCfg()
|
||
local a2 = T().accent or { 1, 0.5, 0.8 }
|
||
if cfg.snapEnabled then
|
||
snapBtn._text:SetText("|cff66ee66开|r 磁吸")
|
||
snapBtn:SetBackdropBorderColor(a2[1], a2[2], a2[3], 0.7)
|
||
else
|
||
snapBtn._text:SetText("|cffee6666关|r 磁吸")
|
||
snapBtn:SetBackdropBorderColor(0.4, 0.2, 0.2, 0.7)
|
||
end
|
||
end
|
||
controlBar._updateSnap = UpdateSnapBtnText
|
||
|
||
-- Grid toggle
|
||
local gridBtn = MakeControlButton(controlBar, "", 76, bx + 84, function()
|
||
local cfg = GetLayoutCfg()
|
||
cfg.showGrid = not cfg.showGrid
|
||
if controlBar._updateGrid then controlBar._updateGrid() end
|
||
if gridFrame then
|
||
if cfg.showGrid then gridFrame:Show() else gridFrame:Hide() end
|
||
end
|
||
end)
|
||
controlBar.gridBtn = gridBtn
|
||
|
||
local function UpdateGridBtnText()
|
||
local cfg = GetLayoutCfg()
|
||
local a2 = T().accent or { 1, 0.5, 0.8 }
|
||
if cfg.showGrid then
|
||
gridBtn._text:SetText("|cff66ee66开|r 网格")
|
||
gridBtn:SetBackdropBorderColor(a2[1], a2[2], a2[3], 0.7)
|
||
else
|
||
gridBtn._text:SetText("|cffee6666关|r 网格")
|
||
gridBtn:SetBackdropBorderColor(0.4, 0.2, 0.2, 0.7)
|
||
end
|
||
end
|
||
controlBar._updateGrid = UpdateGridBtnText
|
||
|
||
-- Reset all
|
||
local resetBtn = MakeControlButton(controlBar, "全部重置", 76, bx + 176, function()
|
||
M:ResetAllMovers()
|
||
end)
|
||
local wbGold = th.wbGold or { 1, 0.88, 0.55 }
|
||
resetBtn._text:SetTextColor(wbGold[1], wbGold[2], wbGold[3], 1)
|
||
|
||
-- Close
|
||
local closeBtn = CreateFrame("Button", nil, controlBar)
|
||
closeBtn:SetWidth(60)
|
||
closeBtn:SetHeight(26)
|
||
closeBtn:SetPoint("RIGHT", controlBar, "RIGHT", -10, 0)
|
||
closeBtn:SetBackdrop(ROUND_BACKDROP)
|
||
closeBtn:SetBackdropColor(0.35, 0.08, 0.10, 0.95)
|
||
closeBtn:SetBackdropBorderColor(0.65, 0.20, 0.25, 0.90)
|
||
local closeText = closeBtn:CreateFontString(nil, "OVERLAY")
|
||
closeText:SetFont(Font(), 10, "OUTLINE")
|
||
closeText:SetPoint("CENTER", 0, 0)
|
||
closeText:SetText("关闭")
|
||
closeText:SetTextColor(1, 0.65, 0.65, 1)
|
||
|
||
closeBtn:SetScript("OnClick", function() M:ExitLayoutMode() end)
|
||
closeBtn:SetScript("OnEnter", function()
|
||
this:SetBackdropColor(0.50, 0.12, 0.15, 0.98)
|
||
this:SetBackdropBorderColor(1, 0.35, 0.40, 1)
|
||
closeText:SetTextColor(1, 1, 1, 1)
|
||
end)
|
||
closeBtn:SetScript("OnLeave", function()
|
||
this:SetBackdropColor(0.35, 0.08, 0.10, 0.95)
|
||
this:SetBackdropBorderColor(0.65, 0.20, 0.25, 0.90)
|
||
closeText:SetTextColor(1, 0.65, 0.65, 1)
|
||
end)
|
||
closeBtn:SetScript("OnMouseDown", function()
|
||
this:SetBackdropColor(0.25, 0.04, 0.06, 0.98)
|
||
end)
|
||
closeBtn:SetScript("OnMouseUp", function()
|
||
this:SetBackdropColor(0.50, 0.12, 0.15, 0.98)
|
||
end)
|
||
controlBar.closeBtn = closeBtn
|
||
|
||
controlBar:Hide()
|
||
return controlBar
|
||
end
|
||
|
||
--------------------------------------------------------------------------------
|
||
-- Register mover
|
||
--------------------------------------------------------------------------------
|
||
function M:RegisterMover(name, frame, label, defaultPoint, defaultRelativeTo, defaultRelPoint, defaultX, defaultY, onMoved)
|
||
if not name or not frame then return end
|
||
|
||
registry[name] = {
|
||
frame = frame,
|
||
label = label or name,
|
||
defaultPoint = defaultPoint or "CENTER",
|
||
defaultRelativeTo = defaultRelativeTo or "UIParent",
|
||
defaultRelPoint = defaultRelPoint or "CENTER",
|
||
defaultX = defaultX or 0,
|
||
defaultY = defaultY or 0,
|
||
onMoved = onMoved,
|
||
}
|
||
|
||
CreateMoverFrame(name, registry[name])
|
||
end
|
||
|
||
--------------------------------------------------------------------------------
|
||
-- Enter / Exit layout mode
|
||
--------------------------------------------------------------------------------
|
||
function M:EnterLayoutMode()
|
||
if isLayoutMode then return end
|
||
isLayoutMode = true
|
||
|
||
if SFrames.ConfigUI and SFramesConfigPanel and SFramesConfigPanel:IsShown() then
|
||
SFramesConfigPanel:Hide()
|
||
end
|
||
|
||
CreateOverlay()
|
||
CreateGrid()
|
||
CreateControlBar()
|
||
|
||
overlayFrame:Show()
|
||
|
||
local cfg = GetLayoutCfg()
|
||
if cfg.showGrid then gridFrame:Show() end
|
||
if controlBar._updateSnap then controlBar._updateSnap() end
|
||
if controlBar._updateGrid then controlBar._updateGrid() end
|
||
controlBar:Show()
|
||
|
||
SFrames:Print(string.format(
|
||
"|cff66eeff[布局]|r UIScale=%.2f screenW=%.0f screenH=%.0f (GetRight=%.0f GetTop=%.0f)",
|
||
UIParent:GetEffectiveScale(),
|
||
UIParent:GetWidth(), UIParent:GetHeight(),
|
||
UIParent:GetRight() or 0, UIParent:GetTop() or 0))
|
||
|
||
for name, _ in pairs(registry) do
|
||
SyncMoverToFrame(name)
|
||
local mover = moverFrames[name]
|
||
if mover then mover:Show() end
|
||
end
|
||
|
||
SFrames:Print("布局模式已开启 - 拖拽移动 | 箭头微调 | 右键重置 | Shift禁用磁吸")
|
||
end
|
||
|
||
function M:ExitLayoutMode()
|
||
if not isLayoutMode then return end
|
||
isLayoutMode = false
|
||
|
||
for name, _ in pairs(registry) do
|
||
SyncFrameToMover(name)
|
||
local mover = moverFrames[name]
|
||
if mover then mover:Hide() end
|
||
end
|
||
|
||
if gridFrame then gridFrame:Hide() end
|
||
if controlBar then controlBar:Hide() end
|
||
if overlayFrame then overlayFrame:Hide() end
|
||
|
||
SFrames:Print("布局模式已关闭 - 所有位置已保存")
|
||
end
|
||
|
||
function M:ToggleLayoutMode()
|
||
if isLayoutMode then self:ExitLayoutMode() else self:EnterLayoutMode() end
|
||
end
|
||
|
||
function M:IsLayoutMode()
|
||
return isLayoutMode
|
||
end
|
||
|
||
--------------------------------------------------------------------------------
|
||
-- Reset movers
|
||
--------------------------------------------------------------------------------
|
||
function M:ResetMover(name)
|
||
local entry = registry[name]
|
||
if not entry then return end
|
||
|
||
local positions = EnsurePositions()
|
||
positions[name] = nil
|
||
|
||
local frame = entry.frame
|
||
if frame then
|
||
frame:ClearAllPoints()
|
||
local relTo = _G[entry.defaultRelativeTo] or UIParent
|
||
frame:SetPoint(entry.defaultPoint, relTo, entry.defaultRelPoint,
|
||
entry.defaultX, entry.defaultY)
|
||
end
|
||
|
||
if entry.onMoved then entry.onMoved() end
|
||
if isLayoutMode then SyncMoverToFrame(name) end
|
||
|
||
SFrames:Print((entry.label or name) .. " 位置已重置")
|
||
end
|
||
|
||
function M:ResetAllMovers()
|
||
for name, _ in pairs(registry) do
|
||
local entry = registry[name]
|
||
local positions = EnsurePositions()
|
||
positions[name] = nil
|
||
|
||
local frame = entry.frame
|
||
if frame then
|
||
frame:ClearAllPoints()
|
||
local relTo = _G[entry.defaultRelativeTo] or UIParent
|
||
frame:SetPoint(entry.defaultPoint, relTo, entry.defaultRelPoint,
|
||
entry.defaultX, entry.defaultY)
|
||
end
|
||
if entry.onMoved then entry.onMoved() end
|
||
end
|
||
|
||
if isLayoutMode then
|
||
for name, _ in pairs(registry) do SyncMoverToFrame(name) end
|
||
end
|
||
|
||
SFrames:Print("所有位置已重置为默认")
|
||
end
|
||
|
||
--------------------------------------------------------------------------------
|
||
-- Apply saved position to a frame (for modules to call on init)
|
||
--------------------------------------------------------------------------------
|
||
function M:ApplyPosition(name, frame, defaultPoint, defaultRelTo, defaultRelPoint, defaultX, defaultY)
|
||
local positions = EnsurePositions()
|
||
local pos = positions[name]
|
||
if pos and pos.point and pos.relativePoint then
|
||
frame:ClearAllPoints()
|
||
frame:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)
|
||
return true
|
||
else
|
||
frame:ClearAllPoints()
|
||
local relFrame = (defaultRelTo and _G[defaultRelTo]) or UIParent
|
||
frame:SetPoint(defaultPoint or "CENTER", relFrame, defaultRelPoint or "CENTER",
|
||
defaultX or 0, defaultY or 0)
|
||
return false
|
||
end
|
||
end
|
||
|
||
--------------------------------------------------------------------------------
|
||
-- Utility
|
||
--------------------------------------------------------------------------------
|
||
function M:GetRegistry()
|
||
return registry
|
||
end
|