From 05d29bb798a6462e191a6e966e6eafff8f956404 Mon Sep 17 00:00:00 2001 From: Dusk-92 Date: Tue, 8 Sep 2026 09:56:50 +0200 Subject: [PATCH] Add reset support to Movable Unit Frames Add per-frame reset support to Movable Unit Frames and document the reset action in the module tooltip. --- mods/move-unitframes.lua | 266 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 247 insertions(+), 19 deletions(-) diff --git a/mods/move-unitframes.lua b/mods/move-unitframes.lua index dc87b92..bded875 100644 --- a/mods/move-unitframes.lua +++ b/mods/move-unitframes.lua @@ -8,7 +8,7 @@ local API = ShaguTweaks.API local module = ShaguTweaks:register({ title = T["Movable Unit Frames"], - description = T["Player, Target, Party, Minimap, Clock, Timer, Buffs, Weapon Buffs and Debuffs can be moved while and are pressed together."], + description = T["Player, Target, Party, Minimap, Clock, Timer, Buffs, Weapon Buffs and Debuffs can be moved while and are pressed together. Right-click a frame to reset its position."], expansions = { ["vanilla"] = true }, category = T["Unit Frames"], enabled = true, @@ -54,24 +54,73 @@ module.enable = function(self) local unlocked = false local states = {} + local defaultPoints = {} local grid -- Player/Target keep the original ShaguTweaks user-placed behavior. -- The formerly-Extended frames use ShaguTweaks_config so their positions - -- survive reload/relog consistently. + -- survive reload/relog consistently. defaultPoint is only a fallback for + -- frames whose live pre-move anchor cannot be recovered (for example a + -- layout-cache position restored before addons load). local targets = { - { name = "PlayerFrame", clamp = true, persist = false }, - { name = "TargetFrame", clamp = true, persist = false }, - { name = "PartyMemberFrame1", persist = true }, - { name = "PartyMemberFrame2", persist = true }, - { name = "PartyMemberFrame3", persist = true }, - { name = "PartyMemberFrame4", persist = true }, - { name = "Minimap", moveParent = true, persist = true, clamp = true }, - { name = "MinimapClock", persist = true, clamp = true, manualGroup = "clock", suppressMouseDown = true }, - { name = "MinimapTimer", persist = true, clamp = true, manualGroup = "timer" }, - { name = "BuffButton0", persist = true, manualGroup = "buffs", cursorDrag = true }, - { name = "BuffButton32", persist = true, manualGroup = "debuffs", cursorDrag = true }, - { name = "TempEnchant1", persist = true, manualGroup = "weapon", cursorDrag = true }, + { + name = "PlayerFrame", clamp = true, persist = false, + defaultPoint = { "TOPLEFT", "UIParent", "TOPLEFT", -19, -4 }, + defaultUnclamped = true, + }, + { + name = "TargetFrame", clamp = true, persist = false, + defaultPoint = { "TOPLEFT", "UIParent", "TOPLEFT", 250, -4 }, + defaultUnclamped = true, + }, + { + name = "PartyMemberFrame1", persist = true, + defaultPoint = { "TOPLEFT", "UIParent", "TOPLEFT", 10, -128 }, + }, + { + name = "PartyMemberFrame2", persist = true, + defaultPoint = { "TOPLEFT", "PartyMemberFrame1PetFrame", "BOTTOMLEFT", -23, -10 }, + }, + { + name = "PartyMemberFrame3", persist = true, + defaultPoint = { "TOPLEFT", "PartyMemberFrame2PetFrame", "BOTTOMLEFT", -23, -10 }, + }, + { + name = "PartyMemberFrame4", persist = true, + defaultPoint = { "TOPLEFT", "PartyMemberFrame3PetFrame", "BOTTOMLEFT", -23, -10 }, + }, + { + name = "Minimap", moveParent = true, persist = true, clamp = true, + defaultPoint = { "TOPRIGHT", "UIParent", "TOPRIGHT", 0, 0 }, + }, + { + name = "MinimapClock", persist = true, clamp = true, + manualGroup = "clock", suppressMouseDown = true, + defaultPoint = { "BOTTOM", "MinimapCluster", "BOTTOM", 8, 18 }, + }, + { + -- MiniMap Clock already owns Ctrl+Shift+RightClick for the timer and + -- resets both its value and its position, so keep that native handler. + name = "MinimapTimer", persist = true, clamp = true, + manualGroup = "timer", nativeReset = true, + defaultPoint = { "TOP", "MinimapClock", "BOTTOM", 0, 0 }, + }, + { + name = "BuffButton0", persist = true, manualGroup = "buffs", + cursorDrag = true, refreshAuras = true, + defaultPoint = { "TOPRIGHT", "BuffFrame", "TOPRIGHT", 0, 0 }, + }, + { + -- Turtle WoW dynamically positions the first debuff row, so let its + -- own BuffButtons_UpdatePositions() rebuild the natural relationship. + name = "BuffButton32", persist = true, manualGroup = "debuffs", + cursorDrag = true, auraRowReset = true, + }, + { + name = "TempEnchant1", persist = true, manualGroup = "weapon", + cursorDrag = true, refreshAuras = true, + defaultPoint = { "TOPRIGHT", "TemporaryEnchantFrame", "TOPRIGHT", 0, 0 }, + }, } local function Resolve(target) @@ -93,6 +142,65 @@ module.enable = function(self) return target.name end + local function CapturePoints(frame) + if not frame or not frame.GetPoint then return end + + local count = frame.GetNumPoints and frame:GetNumPoints() or 1 + local points = {} + + for i = 1, count do + local point, relativeTo, relativePoint, x, y = frame:GetPoint(i) + if point then + table.insert(points, { + point, + relativeTo, + relativePoint, + x or 0, + y or 0, + }) + end + end + + if table.getn(points) > 0 then return points end + end + + local function CaptureDefault(target) + local _, moveFrame = Resolve(target) + if not moveFrame then return end + + local key = PositionKey(target, moveFrame) + if target.persist and movedb[key] ~= nil then return end + + -- A user-placed frame may already have been restored by layout-cache before + -- addon initialization. Do not mistake that old custom position for default. + if moveFrame.IsUserPlaced and moveFrame:IsUserPlaced() then return end + + defaultPoints[target.name] = CapturePoints(moveFrame) + end + + local function ApplyCapturedPoints(frame, points) + if not frame or not points or table.getn(points) == 0 then return false end + + frame:ClearAllPoints() + for _, point in ipairs(points) do + frame:SetPoint(point[1], point[2], point[3], point[4], point[5]) + end + + return true + end + + local function ApplyNamedDefault(target, frame) + local point = target.defaultPoint + if not point or not frame then return false end + + local relativeTo = point[2] and _G[point[2]] or nil + if point[2] and not relativeTo then return false end + + frame:ClearAllPoints() + frame:SetPoint(point[1], relativeTo, point[3], point[4] or 0, point[5] or 0) + return true + end + local function SetDragging(target, value) if target.manualGroup then sharedState.dragging[target.manualGroup] = value and true or false @@ -105,6 +213,13 @@ module.enable = function(self) end end + local function ClearManual(target) + if target.manualGroup then + sharedState.manual[target.manualGroup] = false + sharedState.dragging[target.manualGroup] = false + end + end + local function SavePosition(target, moveFrame) if not target.persist then return end @@ -185,6 +300,57 @@ module.enable = function(self) cursorDrag.moveFrame = nil end + local function ResetTarget(state, target, moveFrame) + if not moveFrame then + local _, resolved = Resolve(target) + moveFrame = resolved + end + if not moveFrame then return end + + if target.cursorDrag and state then StopCursorDrag(state) end + + if target.persist then + movedb[PositionKey(target, moveFrame)] = nil + end + ClearManual(target) + + if not target.cursorDrag and moveFrame.SetUserPlaced then + moveFrame:SetUserPlaced(false) + end + + local restored = ApplyCapturedPoints(moveFrame, defaultPoints[target.name]) + + if not restored and target.auraRowReset then + moveFrame:ClearAllPoints() + if BuffButtons_UpdatePositions then + BuffButtons_UpdatePositions() + restored = true + end + end + + if not restored then + if target.defaultUnclamped and moveFrame.SetClampedToScreen then + moveFrame:SetClampedToScreen(false) + end + restored = ApplyNamedDefault(target, moveFrame) + end + + if target.refreshAuras and BuffButtons_UpdatePositions then + BuffButtons_UpdatePositions() + end + + if target.name == "Minimap" and ShaguTweaks.ScheduleMinimapClamp then + ShaguTweaks.ScheduleMinimapClamp() + end + + if state then + state.dragged = false + state.reset = true + state.suppressRightClick = true + state.userPlaced = false + end + end + cursorDrag:SetScript("OnUpdate", function() local state = this.state local target = this.target @@ -261,13 +427,19 @@ module.enable = function(self) state.active = true state.dragged = false + state.reset = false + state.suppressRightClick = false state.handle = handle state.moveFrame = moveFrame state.onDragStart = handle:GetScript("OnDragStart") state.onDragStop = handle:GetScript("OnDragStop") - if target.suppressMouseDown then - state.onMouseDown = handle:GetScript("OnMouseDown") - handle:SetScript("OnMouseDown", nil) + state.onMouseDown = handle:GetScript("OnMouseDown") + state.onMouseUp = handle:GetScript("OnMouseUp") + state.hasClick = handle.RegisterForClicks and true or false + if state.hasClick then + state.onClick = handle:GetScript("OnClick") + else + state.onClick = nil end if handle.IsMouseEnabled then @@ -300,8 +472,43 @@ module.enable = function(self) handle:EnableMouse(true) handle:RegisterForDrag("LeftButton") + -- While Ctrl+Shift mode owns the frame, Ctrl+Shift+RightClick resets only + -- that frame and swallows its normal right-click action (unit menu, buff + -- cancel, minimap zoom, etc.). MiniMap Timer keeps its own native reset. + if not target.nativeReset then + handle:SetScript("OnMouseDown", function() + if arg1 == "RightButton" then + state.suppressRightClick = true + ResetTarget(state, target, moveFrame) + return + end + + state.suppressRightClick = false + if not target.suppressMouseDown and state.onMouseDown then + state.onMouseDown() + end + end) + + handle:SetScript("OnMouseUp", function() + if arg1 == "RightButton" and state.suppressRightClick then return end + if state.onMouseUp then state.onMouseUp() end + end) + + if state.hasClick then + handle:SetScript("OnClick", function() + if arg1 == "RightButton" and state.suppressRightClick then + state.suppressRightClick = false + return + end + + if state.onClick then state.onClick() end + end) + end + end + handle:SetScript("OnDragStart", function() state.dragged = true + state.reset = false MarkManual(target) SetDragging(target, true) @@ -353,12 +560,24 @@ module.enable = function(self) SavePosition(target, moveFrame) end + -- MiniMap Timer has its own Ctrl+Shift reset path. Detect its + -- SetUserPlaced(false) so releasing the modifiers does not undo it. + if target.nativeReset + and not state.dragged + and state.userPlaced + and moveFrame.IsUserPlaced + and not moveFrame:IsUserPlaced() then + state.reset = true + state.userPlaced = false + end + -- Restore UserPlaced while the frame is still temporarily movable. -- Some Vanilla/Turtle frames (notably MinimapCluster) reject -- SetUserPlaced() after SetMovable(false), which caused repeated -- "not movable or resizable" errors when releasing Ctrl+Shift. if not target.cursorDrag and not state.dragged + and not state.reset and state.userPlaced ~= nil and moveFrame.SetUserPlaced then moveFrame:SetUserPlaced(state.userPlaced) @@ -372,8 +591,13 @@ module.enable = function(self) if handle then handle:SetScript("OnDragStart", state.onDragStart) handle:SetScript("OnDragStop", state.onDragStop) - if target.suppressMouseDown then + + if not target.nativeReset then handle:SetScript("OnMouseDown", state.onMouseDown) + handle:SetScript("OnMouseUp", state.onMouseUp) + if state.hasClick then + handle:SetScript("OnClick", state.onClick) + end end if state.mouseEnabled ~= nil then @@ -443,7 +667,11 @@ module.enable = function(self) end) end + -- Capture clean pre-move anchors before this module applies any saved + -- positions. This preserves compatible addon/Turtle layouts when possible. for _, target in ipairs(targets) do + CaptureDefault(target) + if target.clamp then local _, moveFrame = Resolve(target) if moveFrame and moveFrame.SetClampedToScreen then @@ -455,4 +683,4 @@ module.enable = function(self) end UpdateLockState() -end \ No newline at end of file +end