Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
74 changes: 65 additions & 9 deletions Features/PinnedFrames.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand All @@ -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)
Expand All @@ -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;
Expand All @@ -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

Expand Down
100 changes: 99 additions & 1 deletion Frames/Position.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down
4 changes: 4 additions & 0 deletions Locales/enUS.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down