From 6db3535b65e74981225b38e59e9a58eb2c2f1cd2 Mon Sep 17 00:00:00 2001 From: Krathe Date: Wed, 24 Jun 2026 00:02:57 +0100 Subject: [PATCH] Pinned frames: anchor a set to the raid/party frames MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pinned sets could only be placed freely on the screen, so they drifted out of alignment whenever the raid/party frames moved, resized, or re-centred on a roster change. Add an "Anchor To Frames" option to the pinned position panel. - set.position.anchorTo (default SCREEN = existing UIParent placement). FRAMES_* values glue the set's growth corner to that corner of the mode's frames container, with X/Y as a fine offset; the static SetPoint means the set tracks the frames automatically (incl. in combat — no per-frame reposition). - PositionPinnedContainer resolves the raid/party container (test variant while test mode is active, matching what's on screen) and falls back to screen if it doesn't exist. anchorTo is preserved through both drag handlers and mirrored to _realRaidDB so raid-set auto-layout overlays keep it. - Position panel: a pinned-only "Anchor To Raid/Party Frames" dropdown (9 corners + Screen). Switching anchor mode resets the offset to 0 so the set lands at the chosen reference instead of being flung off-screen by an offset that meant something different in the other mode. Panel grows in pinned mode to give the dropdown its own band. --- CHANGELOG.md | 1 + Features/PinnedFrames.lua | 74 ++++++++++++++++++++++++---- Frames/Position.lua | 100 +++++++++++++++++++++++++++++++++++++- Locales/enUS.lua | 4 ++ 4 files changed, 169 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec4811e6..63be84d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### New Features * (Pet Frames) Added an optional **power bar**. Enable **Show Power Bar** to display the pet's power (mana / energy / focus / etc.) as a bar along the bottom of the frame, with an adjustable height and either power-type or a custom colour. Off by default. (by Krathe) +* (Pinned Frames) Pinned sets can now **anchor to your raid or party frames** instead of the screen. While positioning a set (unlock your frames and click its handle), the position panel gains an **Anchor To Raid Frames** / **Anchor To Party Frames** dropdown — pick a corner (Top Left, Center, Bottom Right, …) and the set pins to that corner of your frames, with the X / Y nudge becoming a fine offset from there. The set then tracks the frames as they move or resize — across roster changes and in combat — so a pinned group stays aligned with your raid instead of drifting out of place. Choose **Screen (Free)** (the default) to keep placing the set freely on screen as before. (by Krathe) ### Improvements diff --git a/Features/PinnedFrames.lua b/Features/PinnedFrames.lua index 770e370c..4e783b28 100644 --- a/Features/PinnedFrames.lua +++ b/Features/PinnedFrames.lua @@ -833,6 +833,35 @@ local function AnchorFractions(point) return fx, fy end +-- pos.anchorTo values that anchor a pinned set to the raid/party FRAMES container +-- (instead of the screen) -> the WoW relative-point corner of that container. +local FRAMES_ANCHOR_POINTS = { + FRAMES_TOPLEFT = "TOPLEFT", + FRAMES_TOP = "TOP", + FRAMES_TOPRIGHT = "TOPRIGHT", + FRAMES_LEFT = "LEFT", + FRAMES_CENTER = "CENTER", + FRAMES_RIGHT = "RIGHT", + FRAMES_BOTTOMLEFT = "BOTTOMLEFT", + FRAMES_BOTTOM = "BOTTOM", + FRAMES_BOTTOMRIGHT = "BOTTOMRIGHT", +} + +-- The main frames container a pinned set should anchor to: the raid or party +-- container, test variant while test mode is active (the live containers are +-- hidden then). Raid-vs-party is resolved the SAME way GetSetForPosition picks +-- the set (test → DF.raidTestMode, live → IsInRaid) so the anchor target always +-- matches the frames actually on screen. (PositionTargetIsRaid itself is declared +-- later in the file, so its logic is inlined here.) Returns nil if the container +-- doesn't exist yet, so callers fall back to screen anchoring. +local function ResolveFramesAnchorTarget() + if PinnedFrames.testModeActive then + local raid = DF.raidTestMode and true or false + return raid and DF.testRaidContainer or DF.testPartyContainer + end + return IsInRaid() and DF.raidContainer or DF.container +end + -- Position a pinned container so its FIRST FRAME lands at a screen spot that is -- INDEPENDENT of the container's size (frame count). Frames grow from the -- container's GROWTH corner (GetContainerAnchorPoint), so we anchor THAT corner @@ -843,13 +872,36 @@ end -- visible count) now place the first frame identically. Dragged sets, whose point -- already equals the growth corner, get a zero offset and render unchanged. -- frameW/frameH are the set's per-frame size in container-local units. +-- +-- When pos.anchorTo is a FRAMES_* value the set is glued to the raid/party +-- container instead of the screen: its growth corner anchors to the chosen +-- container corner with x/y as a fine offset, so the set tracks the frames as +-- they move/resize (incl. in combat — it's a static anchor, no reposition) and +-- pinned/raid alignment stays locked. Falls back to screen if the target +-- container doesn't exist yet. local function PositionPinnedContainer(container, set, pos, frameW, frameH) if not container then return end local growth = GetContainerAnchorPoint(set) + local s = container:GetScale() or 1 + + local relPoint = FRAMES_ANCHOR_POINTS[pos and pos.anchorTo or ""] + if relPoint then + local target = ResolveFramesAnchorTarget() + if target and target ~= container then + -- x/y are screen-space → container units. No half-frame offset: the + -- growth corner is already size-invariant, and the chosen container + -- corner is the reference the user picked. + local x = ((pos and pos.x) or 0) / s + local y = ((pos and pos.y) or 0) / s + container:ClearAllPoints() + container:SetPoint(growth, target, relPoint, x, y) + return + end + end + local ref = (pos and pos.point) or growth local gfx, gfy = AnchorFractions(growth) local rfx, rfy = AnchorFractions(ref) - local s = container:GetScale() or 1 -- pos.x/y are screen-space (÷scale → container units); the frame offset is -- already in container-local units, so it is NOT divided by scale. local x = ((pos and pos.x) or 0) / s + (gfx - rfx) * (frameW or 0) @@ -1372,7 +1424,7 @@ function PinnedFrames:CreateSetFrames(setIndex) -- Track starting mouse and container position (+ the drag's anchor reference -- and frame size, captured once so OnUpdate/OnDragStop stay consistent). - local startMouseX, startMouseY, startPosX, startPosY, dragRef, dragW, dragH + local startMouseX, startMouseY, startPosX, startPosY, dragRef, dragW, dragH, dragAnchorTo mover:SetScript("OnDragStart", function(self) -- Re-resolve the set EVERY drag: the closure's `set` upvalue is bound at @@ -1393,6 +1445,8 @@ function PinnedFrames:CreateSetFrames(setIndex) -- Keep the set's existing anchor reference (pos.point) so coords stay in -- the same space; PositionPinnedContainer pins the growth corner from it. dragRef = (liveSet.position and liveSet.position.point) or GetContainerAnchorPoint(liveSet) + -- Preserve the frames-anchor mode across the drag (rebuilt fresh below). + dragAnchorTo = liveSet.position and liveSet.position.anchorTo dragW, dragH = GetSetFrameSize(liveSet, GetPinnedModeDB()) -- Get starting mouse position in screen coordinates @@ -1425,7 +1479,7 @@ function PinnedFrames:CreateSetFrames(setIndex) end -- Track the live drag in the DB + panel so the X/Y readouts update. - liveSet.position = { point = dragRef, x = newX, y = newY } + liveSet.position = { point = dragRef, x = newX, y = newY, anchorTo = dragAnchorTo } PositionPinnedContainer(container, liveSet, liveSet.position, dragW, dragH) if DF.UpdatePositionPanel then DF:UpdatePositionPanel() end end) @@ -1460,7 +1514,7 @@ function PinnedFrames:CreateSetFrames(setIndex) end -- Save logical position (unscaled) - liveSet.position = { point = anchor, x = finalX, y = finalY } + liveSet.position = { point = anchor, x = finalX, y = finalY, anchorTo = dragAnchorTo } -- RAID ONLY: when an auto layout is active, GetSetDB() returns a deep copy -- of _realRaidDB.pinnedFrames, so the write above goes to that throwaway copy @@ -1473,7 +1527,7 @@ function PinnedFrames:CreateSetFrames(setIndex) and DF._realRaidDB.pinnedFrames.sets and DF._realRaidDB.pinnedFrames.sets[setIndex] if realSet then - realSet.position = { point = anchor, x = finalX, y = finalY } + realSet.position = { point = anchor, x = finalX, y = finalY, anchorTo = dragAnchorTo } end end @@ -2420,6 +2474,7 @@ function PinnedFrames:ApplySetPosition(setIndex) realSet.position.point = pos.point or GetContainerAnchorPoint(set) realSet.position.x = pos.x realSet.position.y = pos.y + realSet.position.anchorTo = pos.anchorTo end end end @@ -3337,7 +3392,7 @@ local function AttachTestMover(container, set, isRaidMode, setIndex) end end) - local startMouseX, startMouseY, startPosX, startPosY, dragRef, dragW, dragH + local startMouseX, startMouseY, startPosX, startPosY, dragRef, dragW, dragH, dragAnchorTo mover:SetScript("OnDragStart", function(self) local currentSet = self.dfSet @@ -3350,6 +3405,7 @@ local function AttachTestMover(container, set, isRaidMode, setIndex) -- Keep the set's existing anchor reference + capture frame size, so the -- helper pins the growth corner consistently (matches the live mover). dragRef = (currentSet.position and currentSet.position.point) or GetContainerAnchorPoint(currentSet) + dragAnchorTo = currentSet.position and currentSet.position.anchorTo local ddb = self.dfIsRaidMode and DF:GetRaidDB() or DF:GetDB() dragW, dragH = GetSetFrameSize(currentSet, ddb) local uiScale = UIParent:GetEffectiveScale() @@ -3372,7 +3428,7 @@ local function AttachTestMover(container, set, isRaidMode, setIndex) newX, newY = DF:SnapToGrid(newX, newY) end -- Track the live drag in the DB + panel so the X/Y readouts update. - currentSet.position = { point = dragRef, x = newX, y = newY } + currentSet.position = { point = dragRef, x = newX, y = newY, anchorTo = dragAnchorTo } PositionPinnedContainer(container, currentSet, currentSet.position, dragW, dragH) if DF.UpdatePositionPanel then DF:UpdatePositionPanel() end end) @@ -3395,7 +3451,7 @@ local function AttachTestMover(container, set, isRaidMode, setIndex) if sdb and sdb.pinnedSnapToGrid and DF.SnapToGrid then finalX, finalY = DF:SnapToGrid(finalX, finalY) end - currentSet.position = { point = anchor, x = finalX, y = finalY } + currentSet.position = { point = anchor, x = finalX, y = finalY, anchorTo = dragAnchorTo } PositionPinnedContainer(container, currentSet, currentSet.position, dragW, dragH) -- Persist raid-set drags through to _realRaidDB (survives overlay rebuilds; @@ -3404,7 +3460,7 @@ local function AttachTestMover(container, set, isRaidMode, setIndex) local realSet = DF._realRaidDB and DF._realRaidDB.pinnedFrames and DF._realRaidDB.pinnedFrames.sets and DF._realRaidDB.pinnedFrames.sets[self.dfSetIndex] if realSet then - realSet.position = { point = anchor, x = finalX, y = finalY } + realSet.position = { point = anchor, x = finalX, y = finalY, anchorTo = dragAnchorTo } end end diff --git a/Frames/Position.lua b/Frames/Position.lua index 9e6e3a7a..3519d826 100644 --- a/Frames/Position.lua +++ b/Frames/Position.lua @@ -57,6 +57,28 @@ local function ResolvePinnedSet() return DF.PinnedFrames:GetSetForPosition(DF.positionPanelPinnedSet or 1) end +-- "Anchor To" options for the pinned position panel. SCREEN = free placement +-- (default; anchored to UIParent). FRAMES_* glue the set's growth corner to the +-- raid/party frames container at that corner, with X/Y as a fine offset, so the +-- set tracks the frames and pinned↔frames alignment stays locked. The dropdown's +-- label (set in UpdatePositionPanel) names the mode ("Raid"/"Party Frames"); the +-- option text is just the corner. Stored in set.position.anchorTo (nil = SCREEN). +local PINNED_ANCHOR_OPTIONS = { + SCREEN = L["Screen (Free)"], + FRAMES_TOPLEFT = L["Top Left"], + FRAMES_TOP = L["Top"], + FRAMES_TOPRIGHT = L["Top Right"], + FRAMES_LEFT = L["Left"], + FRAMES_CENTER = L["Center"], + FRAMES_RIGHT = L["Right"], + FRAMES_BOTTOMLEFT = L["Bottom Left"], + FRAMES_BOTTOM = L["Bottom"], + FRAMES_BOTTOMRIGHT = L["Bottom Right"], + _order = { "SCREEN", "FRAMES_TOPLEFT", "FRAMES_TOP", "FRAMES_TOPRIGHT", + "FRAMES_LEFT", "FRAMES_CENTER", "FRAMES_RIGHT", + "FRAMES_BOTTOMLEFT", "FRAMES_BOTTOM", "FRAMES_BOTTOMRIGHT" }, +} + local POSITION_MODES = { party = { title = "Party Position", @@ -1525,6 +1547,11 @@ function DF:CreatePositionPanel() -- Main panel - matches main GUI style local panel = CreateFrame("Frame", "DandersFramesPositionPanel", UIParent, "BackdropTemplate") panel:SetSize(300, 294) + -- Base height for most modes; pinned mode adds room for the Anchor-To dropdown + -- (toggled in UpdatePositionPanel). The bottom Reset/Center/Lock row is anchored + -- to BOTTOM, so growing the height pushes it down and opens up the dropdown band. + panel.baseHeight = 294 + panel.pinnedHeight = 312 panel:SetPoint("TOP", UIParent, "TOP", 0, -50) panel:SetFrameStrata("FULLSCREEN_DIALOG") panel:SetFrameLevel(100) -- High level to ensure it's on top @@ -2027,7 +2054,51 @@ function DF:CreatePositionPanel() panel.gridSlider = slider panel.gridInput = gridInput - + + -- "Anchor To Frames" dropdown — PINNED MODE ONLY (hidden for every other + -- mode). Anchors a pinned set to the raid/party frames container at a chosen + -- corner so it tracks the frames; X/Y above become a fine offset from that + -- corner. Sits in the empty band below the grid slider; no panel resize. + local anchorDropdown = DF.GUI:CreateDropdown( + panel, + L["Anchor To Frames"], + PINNED_ANCHOR_OPTIONS, + nil, nil, + function() + -- Re-anchor the targeted set after the choice changes. + if DF.PinnedFrames and DF.PinnedFrames.ApplySetPosition then + DF.PinnedFrames:ApplySetPosition(DF.positionPanelPinnedSet or 1) + end + if DF.UpdatePositionPanel then DF:UpdatePositionPanel() end + end, + function() -- customGet: read the targeted set's anchor mode (nil = SCREEN) + local set = ResolvePinnedSet() + return (set and set.position and set.position.anchorTo) or "SCREEN" + end, + function(v) -- customSet: store on the targeted set (nil for SCREEN default) + local set = ResolvePinnedSet() + if not set then return end + set.position = set.position or { point = "CENTER", x = 0, y = 0 } + local newAnchor = (v ~= "SCREEN") and v or nil + -- X/Y mean different things per mode: a screen offset from UIParent vs a + -- fine offset from the frames-container corner. Carrying a large screen + -- offset into frames-anchor mode (or vice versa) would fling the set far + -- off the chosen reference — off-screen. So whenever the anchor mode + -- changes, reset the offset to 0 so the set lands exactly AT the new + -- reference (the chosen corner / screen point); the user nudges from there. + if set.position.anchorTo ~= newAnchor then + set.position.x = 0 + set.position.y = 0 + end + set.position.anchorTo = newAnchor + end + ) + anchorDropdown:ClearAllPoints() + anchorDropdown:SetPoint("TOPLEFT", 15, -204) + anchorDropdown:SetWidth(255) + anchorDropdown:Hide() + panel.anchorDropdown = anchorDropdown + -- Reset Position button local resetBtn = CreateFrame("Button", nil, panel, "BackdropTemplate") resetBtn:SetSize(85, 26) @@ -2122,6 +2193,33 @@ function DF:UpdatePositionPanel() DF.positionPanelMode == "pinned" and L["Hide Mover"] or L["Hide Drag Overlay"]) end + -- Pinned-only "Anchor To Frames" dropdown: show in pinned mode, name it for + -- the targeted set's mode (Raid/Party), and refresh the selected value. + if DF.positionPanel.anchorDropdown then + if DF.positionPanelMode == "pinned" then + -- Grow the panel so the dropdown sits in its own band between the grid + -- slider and the buttons (shrinks back for the other, shorter modes). + if DF.positionPanel.pinnedHeight then + DF.positionPanel:SetHeight(DF.positionPanel.pinnedHeight) + end + local raid = DF.PinnedFrames and DF.PinnedFrames.IsPositionTargetRaid + and DF.PinnedFrames:IsPositionTargetRaid() + if DF.positionPanel.anchorDropdown.label then + DF.positionPanel.anchorDropdown.label:SetText( + raid and L["Anchor To Raid Frames"] or L["Anchor To Party Frames"]) + end + if DF.positionPanel.anchorDropdown.UpdateText then + DF.positionPanel.anchorDropdown:UpdateText() + end + DF.positionPanel.anchorDropdown:Show() + else + DF.positionPanel.anchorDropdown:Hide() + if DF.positionPanel.baseHeight then + DF.positionPanel:SetHeight(DF.positionPanel.baseHeight) + end + end + end + -- Update position override indicator if editing profile if DF.positionPanel.UpdatePositionOverride then DF.positionPanel.UpdatePositionOverride() diff --git a/Locales/enUS.lua b/Locales/enUS.lua index e3c91249..ff39eb01 100644 --- a/Locales/enUS.lua +++ b/Locales/enUS.lua @@ -338,6 +338,10 @@ L["Anchor"] = true L["Anchor Point"] = true L["Anchor Position"] = true L["Anchor To"] = true +L["Anchor To Frames"] = true +L["Anchor To Party Frames"] = true +L["Anchor To Raid Frames"] = true +L["Screen (Free)"] = true L["Animated Border"] = true L["Any debuff that can be dispelled, regardless of whether you can dispel it."] = true L["Appearance"] = true