diff --git a/CHANGELOG.md b/CHANGELOG.md index ec4811e..63be84d 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 770e370..4e783b2 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 9e6e3a7..3519d82 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 e3c9124..ff39eb0 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