forked from Dusk/ShaguTweaks-ClassicAPI
414 lines
16 KiB
Lua
414 lines
16 KiB
Lua
local _G = ShaguTweaks.GetGlobalEnv()
|
|
local T = ShaguTweaks.T
|
|
local API = ShaguTweaks.API
|
|
local hooksecurefunc = ShaguTweaks.hooksecurefunc
|
|
local S = { states = {} }
|
|
|
|
local function Unlocked()
|
|
return API.IsShiftKeyDown() and API.IsControlKeyDown() and API.IsAltKeyDown()
|
|
end
|
|
|
|
function S.IsMoved(name)
|
|
return S.positions and S.positions[name] ~= nil
|
|
end
|
|
|
|
local function Apply(state)
|
|
local position = S.positions[state.name]
|
|
if not position then return end
|
|
state.clear(state.frame)
|
|
state.point(state.frame, "TOPLEFT", UIParent, "BOTTOMLEFT", position[1], position[2])
|
|
end
|
|
|
|
local function Layout()
|
|
if _G.UIParent_ManageFramePositions then _G.UIParent_ManageFramePositions() end
|
|
if _G.ReputationWatchBar_Update then _G.ReputationWatchBar_Update() end
|
|
end
|
|
|
|
-- Detect anchor chains as well as parent chains. Pin attached, movable bars
|
|
-- before moving their owner so moving one does not drag the others with it.
|
|
local function DependsOn(frame, owner, depth)
|
|
if not frame or frame == UIParent or depth > 12 then return false end
|
|
if frame == owner then return true end
|
|
if frame.GetParent and DependsOn(frame:GetParent(), owner, depth + 1) then return true end
|
|
if frame.GetPoint then
|
|
local count = frame.GetNumPoints and frame:GetNumPoints() or 1
|
|
for i = 1, count do
|
|
local point, relative = frame:GetPoint(i)
|
|
if type(relative) == "string" then relative = _G[relative] end
|
|
if point and relative and DependsOn(relative, owner, depth + 1) then return true end
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function PinDependents(owner)
|
|
for _, state in ipairs(S.states) do
|
|
if state ~= owner and not S.positions[state.name] and DependsOn(state.frame, owner.frame, 0) then
|
|
local x, y = state.frame:GetLeft(), state.frame:GetTop()
|
|
if x and y then S.positions[state.name] = { x, y }; Apply(state) end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Capture native neighbour offsets before applying any saved positions.
|
|
-- Keeping the entire offset preserves intentional insets as well as gaps
|
|
-- (the pet/stance buttons do not start at the main frame's left edge).
|
|
local function CaptureSnapOffsets()
|
|
for _, state in ipairs(S.states) do
|
|
state.snapOffsets = {}
|
|
local frame = state.frame
|
|
local scale = frame:GetEffectiveScale() / UIParent:GetEffectiveScale()
|
|
local x, y = frame:GetLeft(), frame:GetTop()
|
|
if x and y then
|
|
x, y = x * scale, y * scale
|
|
for _, other in ipairs(S.states) do
|
|
if other ~= state then
|
|
local target = other.frame
|
|
local targetScale = target:GetEffectiveScale() / UIParent:GetEffectiveScale()
|
|
local tx, ty = target:GetLeft(), target:GetTop()
|
|
if tx and ty then
|
|
local dx, dy = x - tx * targetScale, y - ty * targetScale
|
|
local width = math.max(frame:GetWidth() * scale, target:GetWidth() * targetScale)
|
|
local height = math.max(frame:GetHeight() * scale, target:GetHeight() * targetScale)
|
|
if math.abs(dx) <= width + 20 and math.abs(dy) <= height + 20 then
|
|
table.insert(state.snapOffsets, { target = other, x = dx, y = dy })
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function SnapTargetVisible(state)
|
|
local frame = state.frame
|
|
if not frame:IsVisible() then return false end
|
|
local parent = frame
|
|
while parent do
|
|
if parent.GetAlpha and parent:GetAlpha() == 0 then return false end
|
|
parent = parent:GetParent()
|
|
end
|
|
if state.name == "MainMenuExpBar" then
|
|
-- The container can remain shown at the level cap without an XP bar.
|
|
if _G.UnitLevel and UnitLevel("player") >= (_G.MAX_PLAYER_LEVEL or 60) then return false end
|
|
elseif state.name == "ReputationWatchBar" then
|
|
if not _G.GetWatchedFactionInfo or not GetWatchedFactionInfo() then return false end
|
|
local status = _G.ReputationWatchStatusBar
|
|
if not status or not status:IsVisible() or (status.GetAlpha and status:GetAlpha() == 0) then return false end
|
|
end
|
|
return true
|
|
end
|
|
|
|
local function ButtonBounds(state)
|
|
local first, last = _G[state.first or ""], _G[state.last or ""]
|
|
if state.name == "MainMenuBar" and _G.BonusActionBarFrame and BonusActionBarFrame:IsVisible() then
|
|
first, last = _G.BonusActionButton1, _G.BonusActionButton12
|
|
end
|
|
if not first or not last then return end
|
|
local l, t, r, b = first:GetLeft(), first:GetTop(), last:GetRight(), last:GetBottom()
|
|
if not l or not t or not r or not b then return end
|
|
local a = first:GetEffectiveScale() / UIParent:GetEffectiveScale()
|
|
local z = last:GetEffectiveScale() / UIParent:GetEffectiveScale()
|
|
return l * a, t * a, r * z, b * z
|
|
end
|
|
|
|
local function MainActionPair(a, b)
|
|
return not a.progress and not b.progress
|
|
and (a.name == "MainMenuBar" or b.name == "MainMenuBar")
|
|
end
|
|
|
|
function S.Snap(state, x, y)
|
|
local scale = state.frame:GetEffectiveScale() / UIParent:GetEffectiveScale()
|
|
local px, py = x * scale, y * scale
|
|
local best, resultX, resultY = 289, x, y
|
|
for _, offset in ipairs(state.snapOffsets) do
|
|
local target = offset.target.frame
|
|
if not MainActionPair(state, offset.target) and SnapTargetVisible(offset.target) then
|
|
local targetScale = target:GetEffectiveScale() / UIParent:GetEffectiveScale()
|
|
local tx, ty = target:GetLeft(), target:GetTop()
|
|
if tx and ty then
|
|
tx, ty = tx * targetScale + offset.x, ty * targetScale + offset.y
|
|
local dx, dy = tx - px, ty - py
|
|
local distance = dx * dx + dy * dy
|
|
if math.abs(dx) <= 12 and math.abs(dy) <= 12 and distance < best then
|
|
best, resultX, resultY = distance, tx / scale, ty / scale
|
|
end
|
|
end
|
|
end
|
|
end
|
|
-- MainMenuBar's native rectangle contains unused artwork. Its old frame
|
|
-- offsets must never determine the visible gap between action-button rows.
|
|
local l, t, r, b = ButtonBounds(state)
|
|
if l then
|
|
local ox = l - state.frame:GetLeft() * scale
|
|
local oy = t - state.frame:GetTop() * scale
|
|
local height = t - b
|
|
local gap = 6 * scale
|
|
if _G.ActionButton1 and _G.ActionButton2 then
|
|
local right, left = ActionButton1:GetRight(), ActionButton2:GetLeft()
|
|
if right and left then
|
|
local measured = left * ActionButton2:GetEffectiveScale() / UIParent:GetEffectiveScale()
|
|
- right * ActionButton1:GetEffectiveScale() / UIParent:GetEffectiveScale()
|
|
if measured >= 0 and measured <= 20 then gap = measured end
|
|
end
|
|
end
|
|
for _, other in ipairs(S.states) do
|
|
if other ~= state and MainActionPair(state, other) and SnapTargetVisible(other) then
|
|
local tl, tt, tr, tb = ButtonBounds(other)
|
|
if tl then
|
|
for side = 1, 2 do
|
|
local tx = tl - ox
|
|
local ty = (side == 1 and tb - gap or tt + gap + height) - oy
|
|
local dx, dy = tx - px, ty - py
|
|
local distance = dx * dx + dy * dy
|
|
if math.abs(dx) <= 12 and math.abs(dy) <= 12 and distance < best then
|
|
best, resultX, resultY = distance, tx / scale, ty / scale
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return resultX, resultY
|
|
end
|
|
|
|
local function Finish(save)
|
|
local state = S.dragging
|
|
if not state then return end
|
|
S.dragging = nil
|
|
S.outline:StopMovingOrSizing()
|
|
local x, y = S.outline:GetLeft(), S.outline:GetTop()
|
|
S.outline:Hide()
|
|
if save and x and y then
|
|
x, y = S.Snap(state, x - state.dragOffsetX, y - state.dragOffsetY)
|
|
PinDependents(state)
|
|
S.positions[state.name] = { x, y }
|
|
Apply(state)
|
|
-- Positions are stored in addon settings; native bars need not be movable.
|
|
Layout()
|
|
end
|
|
end
|
|
|
|
local function Reset(state)
|
|
Finish(false)
|
|
S.positions[state.name] = nil
|
|
state.clear(state.frame)
|
|
for _, point in ipairs(state.defaults) do state.point(state.frame, unpack(point)) end
|
|
|
|
Layout()
|
|
end
|
|
|
|
local function Begin()
|
|
if not Unlocked() then return end
|
|
Finish(true)
|
|
local state = this.state
|
|
local x, y = state.frame:GetLeft(), state.frame:GetTop()
|
|
if not x or not y then return end
|
|
local left, top = x, y
|
|
local width, height = state.frame:GetWidth(), math.max(state.frame:GetHeight(), 14)
|
|
-- Main and pet frames include artwork beyond the visible button row.
|
|
-- Read button edges directly; anchored marker dimensions are not reliable
|
|
-- on every supported client.
|
|
if state.name == "MainMenuBar" or state.name == "PetActionBarFrame" then
|
|
local first, last = _G.ActionButton1, _G.ActionButton12
|
|
if state.name == "PetActionBarFrame" then
|
|
first, last = _G.PetActionButton1, _G.PetActionButton10
|
|
elseif _G.BonusActionBarFrame and BonusActionBarFrame:IsVisible() then
|
|
first, last = _G.BonusActionButton1, _G.BonusActionButton12
|
|
end
|
|
if first and last then
|
|
local scale = first:GetEffectiveScale() / state.frame:GetEffectiveScale()
|
|
local lastScale = last:GetEffectiveScale() / state.frame:GetEffectiveScale()
|
|
local l, t, r, b = first:GetLeft(), first:GetTop(), last:GetRight(), last:GetBottom()
|
|
if l and t and r and b then
|
|
left, top = l * scale - 2, t * scale + 2
|
|
width, height = r * lastScale + 2 - left, top - b * lastScale + 2
|
|
end
|
|
end
|
|
end
|
|
state.dragOffsetX, state.dragOffsetY = left - x, top - y
|
|
S.dragging = state
|
|
local outline = S.outline
|
|
outline:SetScale(state.frame:GetEffectiveScale() / UIParent:GetEffectiveScale())
|
|
outline:SetWidth(width)
|
|
outline:SetHeight(height)
|
|
outline:ClearAllPoints()
|
|
outline:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", left, top)
|
|
outline:Show()
|
|
outline:StartMoving()
|
|
end
|
|
|
|
local function MarkerClick()
|
|
if Unlocked() and arg1 == "RightButton" then Reset(this.state) end
|
|
end
|
|
|
|
local function UpdateMarkers()
|
|
local unlocked = Unlocked()
|
|
if not unlocked then Finish(true) end
|
|
for _, state in ipairs(S.states) do
|
|
local controller = state.frame.ShaguTweaksMouseoverController
|
|
local visible = state.progress or state.frame:IsVisible() or (controller and controller.enabled)
|
|
if unlocked and visible then
|
|
-- Follow actual button bounds; the main frame also includes gryphons
|
|
-- and XP artwork outside its clickable action-button row.
|
|
local first = state.first and _G[state.first]
|
|
local last = state.last and _G[state.last]
|
|
if state.name == "MainMenuBar" and _G.BonusActionBarFrame and BonusActionBarFrame:IsVisible() then
|
|
first, last = _G.BonusActionButton1, _G.BonusActionButton12
|
|
elseif state.name == "ShapeshiftBarFrame" and type(_G.GetNumShapeshiftForms) == "function" then
|
|
first, last = _G.ShapeshiftButton1, _G["ShapeshiftButton" .. GetNumShapeshiftForms()]
|
|
end
|
|
state.marker:ClearAllPoints()
|
|
if first and last then
|
|
state.marker:SetPoint("TOPLEFT", first, "TOPLEFT", -2, 2)
|
|
state.marker:SetPoint("BOTTOMRIGHT", last, "BOTTOMRIGHT", 2, -2)
|
|
elseif state.progress then
|
|
local anchor = state.name == "MainMenuExpBar" and "TOPLEFT" or "BOTTOMLEFT"
|
|
state.marker:SetPoint(anchor, state.frame, anchor, 0, 0)
|
|
state.marker:SetWidth(state.frame:GetWidth())
|
|
state.marker:SetHeight(14)
|
|
else
|
|
state.marker:SetAllPoints(state.frame)
|
|
end
|
|
state.marker:Show()
|
|
else
|
|
if S.dragging == state then Finish(true) end
|
|
state.marker:Hide()
|
|
end
|
|
end
|
|
end
|
|
|
|
local function MarkerStyle(marker)
|
|
marker:SetFrameStrata("DIALOG")
|
|
marker:SetFrameLevel(100)
|
|
marker:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
|
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 8,
|
|
edgeSize = 12, insets = { left = 3, right = 3, top = 3, bottom = 3 } })
|
|
marker:SetBackdropColor(0, 0, 0, .7)
|
|
local color = ShaguTweaks.UIBorder
|
|
marker:SetBackdropBorderColor(color and color.r or 1, color and color.g or .82, color and color.b or 0, 1)
|
|
end
|
|
|
|
local function Add(definition)
|
|
local frame = _G[definition[1]]
|
|
if not frame then return end
|
|
local state = { name = definition[1], frame = frame, defaults = {}, first = definition[3],
|
|
last = definition[4], progress = definition[5], point = frame.SetPoint, clear = frame.ClearAllPoints }
|
|
for i = 1, (frame.GetNumPoints and frame:GetNumPoints() or 1) do
|
|
local p, relative, rp, x, y = frame:GetPoint(i)
|
|
if p then table.insert(state.defaults, { p, relative or UIParent, rp or p, x or 0, y or 0 }) end
|
|
end
|
|
-- Guard BOTH operations: ignoring SetPoint alone would let a preceding
|
|
-- ClearAllPoints detach a manually positioned bar during native relayout.
|
|
frame.ClearAllPoints = function(self)
|
|
if S.positions[state.name] then return end
|
|
state.clear(self)
|
|
end
|
|
frame.SetPoint = function(self, a, b, c, d, e)
|
|
if S.positions[state.name] then return end
|
|
state.point(self, a, b, c, d, e)
|
|
end
|
|
local marker = CreateFrame("Button", "ShaguTweaksMover" .. state.name, UIParent)
|
|
MarkerStyle(marker)
|
|
marker:SetScale(frame:GetEffectiveScale() / UIParent:GetEffectiveScale())
|
|
marker:EnableMouse(true)
|
|
marker:RegisterForDrag("LeftButton")
|
|
marker:RegisterForClicks("RightButtonUp")
|
|
marker.state = state
|
|
marker:SetScript("OnDragStart", Begin)
|
|
marker:SetScript("OnDragStop", function() Finish(true) end)
|
|
marker:SetScript("OnClick", MarkerClick)
|
|
marker:SetScript("OnHide", function() if S.dragging == this.state then Finish(true) end end)
|
|
marker.text = marker:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
|
|
marker.text:SetPoint("CENTER", marker, "CENTER", 0, 0)
|
|
marker.text:SetText(T[definition[2]])
|
|
marker:Hide()
|
|
state.marker = marker
|
|
table.insert(S.states, state)
|
|
ShaguTweaks.HookScript(frame, "OnShow", UpdateMarkers)
|
|
ShaguTweaks.HookScript(frame, "OnHide", UpdateMarkers)
|
|
end
|
|
|
|
local function Setup()
|
|
if S.installed then return end
|
|
S.installed = true
|
|
ShaguTweaks_config.ActionBarPositions = ShaguTweaks_config.ActionBarPositions or {}
|
|
S.positions = ShaguTweaks_config.ActionBarPositions
|
|
ShaguTweaks.ActionBarMovers = S
|
|
S.outline = CreateFrame("Frame", "ShaguTweaksActionBarDragOutline", UIParent)
|
|
MarkerStyle(S.outline)
|
|
S.outline:SetMovable(true)
|
|
S.outline:SetClampedToScreen(true)
|
|
S.outline:SetScript("OnHide", function() if S.dragging then Finish(true) end end)
|
|
S.outline:Hide()
|
|
if S.actions then
|
|
local definitions = {
|
|
{ "MainMenuBar", "Main Action Bar", "ActionButton1", "ActionButton12" },
|
|
{ "MultiBarBottomLeft", "Bottom Left Bar", "MultiBarBottomLeftButton1", "MultiBarBottomLeftButton12" },
|
|
{ "MultiBarBottomRight", "Bottom Right Bar", "MultiBarBottomRightButton1", "MultiBarBottomRightButton12" },
|
|
{ "MultiBarRight", "Right Bar", "MultiBarRightButton1", "MultiBarRightButton12" },
|
|
{ "MultiBarLeft", "Right Bar 2", "MultiBarLeftButton1", "MultiBarLeftButton12" },
|
|
{ "PetActionBarFrame", "Pet Action Bar", "PetActionButton1", "PetActionButton10" },
|
|
{ "ShapeshiftBarFrame", "Stance Bar" },
|
|
}
|
|
for _, definition in ipairs(definitions) do Add(definition) end
|
|
end
|
|
if S.progress then
|
|
Add({ "MainMenuExpBar", "Experience Bar", nil, nil, true })
|
|
Add({ "ReputationWatchBar", "Reputation Bar", nil, nil, true })
|
|
end
|
|
CaptureSnapOffsets()
|
|
for _, state in ipairs(S.states) do Apply(state) end
|
|
S.events = CreateFrame("Frame")
|
|
S.events:SetScript("OnEvent", function()
|
|
if event == "PLAYER_ENTERING_WORLD" then
|
|
for _, state in ipairs(S.states) do Apply(state) end
|
|
end
|
|
UpdateMarkers()
|
|
end)
|
|
S.events:RegisterEvent("PLAYER_ENTERING_WORLD")
|
|
if API.modifierstate then
|
|
S.events:RegisterEvent("MODIFIER_STATE_CHANGED")
|
|
else
|
|
S.events.elapsed = 0
|
|
S.events:SetScript("OnUpdate", function()
|
|
this.elapsed = this.elapsed + (arg1 or 0)
|
|
if this.elapsed < .1 then return end
|
|
this.elapsed = 0
|
|
UpdateMarkers()
|
|
end)
|
|
end
|
|
UpdateMarkers()
|
|
end
|
|
|
|
local function Schedule()
|
|
if S.scheduled then return end
|
|
S.scheduled = true
|
|
local ready = CreateFrame("Frame")
|
|
ready:RegisterEvent("PLAYER_ENTERING_WORLD")
|
|
ready:SetScript("OnEvent", function()
|
|
this:UnregisterAllEvents()
|
|
this:SetScript("OnUpdate", function()
|
|
this:SetScript("OnUpdate", nil)
|
|
this:Hide()
|
|
if API.timer then API.Defer(Setup) else Setup() end
|
|
end)
|
|
end)
|
|
end
|
|
|
|
local function ReducedEnabled()
|
|
local enabled = ShaguTweaks_config[T["Reduced Actionbar Size"]]
|
|
if enabled == nil then
|
|
local reduced = ShaguTweaks.mods[T["Reduced Actionbar Size"]]
|
|
enabled = reduced and reduced.enabled and 1 or 0
|
|
end
|
|
return enabled == 1
|
|
end
|
|
|
|
ShaguTweaks.EnableActionBarMovers = function()
|
|
if not ReducedEnabled() then return end
|
|
S.actions = true
|
|
S.progress = true
|
|
Schedule()
|
|
end
|