Files
coa-weakauras/WeakAuras/RegionTypes/DynamicGroup.lua
T
2025-01-06 12:53:01 +01:00

1278 lines
43 KiB
Lua

if not WeakAuras.IsCorrectVersion() or not WeakAuras.IsLibsOK() then return end
local AddonName, Private = ...
local WeakAuras = WeakAuras
local SharedMedia = LibStub("LibSharedMedia-3.0")
local default = {
controlledChildren = {},
border = false,
borderColor = {0, 0, 0, 1},
backdropColor = {1, 1, 1, 0.5},
borderEdge = "Square Full White",
borderOffset = 4,
borderInset = 1,
borderSize = 2,
borderBackdrop = "Blizzard Tooltip",
grow = "DOWN",
selfPoint = "TOP",
align = "CENTER",
space = 2,
stagger = 0,
sort = "none",
animate = false,
anchorPoint = "CENTER",
anchorFrameType = "SCREEN",
xOffset = 0,
yOffset = 0,
radius = 200,
rotation = 0,
fullCircle = true,
arcLength = 360,
constantFactor = "RADIUS",
frameStrata = 1,
scale = 1,
useLimit = false,
limit = 5,
gridType = "RD",
gridWidth = 5,
rowSpace = 1,
columnSpace = 1
}
local controlPointFunctions = {
["SetAnchorPoint"] = function(self, point, relativeFrame, relativePoint, offsetX, offsetY)
self:ClearAllPoints();
self.point, self.relativeFrame, self.relativePoint, self.offsetX, self.offsetY = point, relativeFrame, relativePoint, offsetX, offsetY
self.totalOffsetX = (self.animOffsetX or 0) + (self.offsetX or 0)
self.totalOffsetY = (self.animOffsetY or 0) + (self.offsetY or 0)
if self.relativeFrame and self.relativePoint then
self:SetPoint(self.point, self.relativeFrame, self.relativePoint, self.totalOffsetX, self.totalOffsetY)
else
self:SetPoint(self.point, self.totalOffsetX, self.totalOffsetY)
end
end,
["ClearAnchorPoint"] = function(self)
self.point, self.relativeFrame, self.relativePoint, self.offsetX, self.offsetY = nil, nil, nil, nil, nil
end,
["ReAnchor"] = function(self, frame)
self:ClearAllPoints()
self.relativeFrame = frame
if self.relativeFrame and self.relativePoint then
self:SetPoint(self.point, self.relativeFrame, self.relativePoint, self.totalOffsetX, self.totalOffsetY)
else
self:SetPoint(self.point, self.totalOffsetX, self.totalOffsetY)
end
end,
["SetOffsetAnim"] = function(self, x, y)
self.animOffsetX, self.animOffsetY = x, y
self.totalOffsetX = (self.animOffsetX or 0) + (self.offsetX or 0)
self.totalOffsetY = (self.animOffsetY or 0) + (self.offsetY or 0)
if not self.point then
-- Nothing to do
elseif self.relativeFrame and self.relativePoint then
self:SetPoint(self.point, self.relativeFrame, self.relativePoint, self.totalOffsetX, self.totalOffsetY)
else
self:SetPoint(self.point, self.totalOffsetX, self.totalOffsetY)
end
end
}
local function createControlPoint(self)
local controlPoint = CreateFrame("Frame", nil, self.parent)
WeakAuras.Mixin(controlPoint, controlPointFunctions)
controlPoint:SetWidth(16)
controlPoint:SetHeight(16)
controlPoint:Show()
controlPoint:SetAnchorPoint(self.parent.selfPoint)
return controlPoint
end
local function releaseControlPoint(self, controlPoint)
controlPoint:Hide()
controlPoint:ClearAnchorPoint()
local regionData = controlPoint.regionData
if regionData then
if self.parent.anchorPerUnit == "UNITFRAME" then
Private.dyngroup_unitframe_monitor[regionData] = nil
end
controlPoint.regionData = nil
regionData.controlPoint = nil
end
end
local function create(parent)
local region = CreateFrame("Frame", nil, parent)
region.regionType = "dynamicgroup"
region:SetSize(16, 16)
region:SetMovable(true)
region.sortedChildren = {}
region.controlledChildren = {}
region.updatedChildren = {}
local background = CreateFrame("Frame", nil, region)
region.background = background
region.selfPoint = "TOPLEFT"
region.controlPoints = CreateObjectPool(createControlPoint, releaseControlPoint)
region.controlPoints.parent = region
WeakAuras.regionPrototype.create(region)
region.suspended = 0
local oldSetFrameLevel = region.SetFrameLevel
region.SetFrameLevel = function(self, level)
oldSetFrameLevel(self, level)
self.background:SetFrameLevel(level)
end
return region
end
function WeakAuras.GetPolarCoordinates(x, y, originX, originY)
local dX, dY = x - originX, y - originY;
local r = math.sqrt(dX * dX + dY * dY);
local theta = atan2(dY, dX);
return r, theta;
end
function WeakAuras.InvertSort(sortFunc)
-- takes a comparator and returns the "inverse"
-- i.e. when sortFunc returns true/false, inverseSortFunc returns false/true
-- nils are preserved to ensure that inverseSortFunc composes well
if type(sortFunc) ~= "function" then
error("InvertSort requires a function to invert.")
else
return function(...)
local result = sortFunc(...)
if result == nil then return nil end
return not result
end
end
end
function WeakAuras.SortNilLast(a, b)
-- sorts nil values to the end
-- only returns nil if both values are non-nil
-- Useful as a high priority sorter in a composition,
-- to ensure that children with missing data
-- don't ever sit in the middle of a row
-- and interrupt the sorting algorithm
if a == nil and b == nil then
-- guarantee stability in the nil region
return false
elseif a == nil then
return false
elseif b == nil then
return true
else
return nil
end
end
local sortNilFirst = WeakAuras.InvertSort(WeakAuras.SortNilLast)
function WeakAuras.SortNilFirst(a, b)
if a == nil and b == nil then
-- we want SortNil to always prevent nils from propagating
-- as well as to sort nils onto one side
-- to maintain stability, we need SortNil(nil, nil) to always be false
-- hence this special case
return false
else
return sortNilFirst(a,b)
end
end
function WeakAuras.SortGreaterLast(a, b)
-- sorts values in ascending order
-- values of disparate types are sorted according to the value of type(value)
-- which is a bit weird but at least guarantees a stable sort
-- can only sort comparable values (i.e. numbers and strings)
-- no support currently for tables with __lt metamethods
if a == b then
return nil
end
if type(a) ~= type(b) then
return type(a) > type(b)
end
if type(a) == "number" then
if abs(b - a) < 0.001 then
return nil
else
return a < b
end
elseif type(a) == "string" then
return a < b
else
return nil
end
end
WeakAuras.SortGreaterFirst = WeakAuras.InvertSort(WeakAuras.SortGreaterLast)
function WeakAuras.SortRegionData(path, sortFunc)
-- takes an array-like table, and a function that takes 2 values and returns true/false/nil
-- creates function that accesses the value indicated by path, and compares using sortFunc
if type(path) ~= "table" then
path = {}
end
if type(sortFunc) ~= "function" then
-- if sortFunc not provided, compare by default as "<"
sortFunc = WeakAuras.SortGreaterLast
end
return function(a, b)
local aValue, bValue = a, b
for _, key in ipairs(path) do
if type(aValue) ~= "table" then return nil end
if type(bValue) ~= "table" then return nil end
aValue, bValue = aValue[key], bValue[key]
end
return sortFunc(aValue, bValue)
end
end
function WeakAuras.SortAscending(path)
return WeakAuras.SortRegionData(path, WeakAuras.ComposeSorts(WeakAuras.SortNilFirst, WeakAuras.SortGreaterLast))
end
function WeakAuras.SortDescending(path)
return WeakAuras.InvertSort(WeakAuras.SortAscending(path))
end
function WeakAuras.ComposeSorts(...)
-- accepts vararg of sort funcs
-- returns new sort func that combines the functions passed in
-- order of functions passed in determines their priority in new sort
-- returns nil if all functions return nil,
-- so that it can be composed or inverted without trouble
local sorts = {}
for i = 1, select("#", ...) do
local sortFunc = select(i, ...)
if type(sortFunc) == "function" then
tinsert(sorts, sortFunc)
end
end
return function(a, b)
for _, sortFunc in ipairs(sorts) do
local result = sortFunc(a, b)
if result ~= nil then
return result
end
end
return nil
end
end
local function noop() end
local sorters = {
none = function(data)
return WeakAuras.ComposeSorts(
WeakAuras.SortAscending({"dataIndex"}),
WeakAuras.SortAscending({"region", "state", "index"})
)
end,
hybrid = function(data)
local sortHybridTable = data.sortHybridTable or {}
local hybridSortAscending = data.hybridSortMode == "ascending"
local hybridFirst = data.hybridPosition == "hybridFirst"
local function sortHybridStatus(a, b)
if not b then return true end
if not a then return false end
local aIsHybrid = sortHybridTable[a.id]
local bIsHybrid = sortHybridTable[b.id]
if aIsHybrid and not bIsHybrid then
return hybridFirst
elseif bIsHybrid and not aIsHybrid then
return not hybridFirst
else
return nil
end
end
local sortExpirationTime
if hybridSortAscending then
sortExpirationTime = WeakAuras.SortAscending({"region", "state", "expirationTime"})
else
sortExpirationTime = WeakAuras.SortDescending({"region", "state", "expirationTime"})
end
return WeakAuras.ComposeSorts(
sortHybridStatus,
sortExpirationTime,
WeakAuras.SortAscending({"dataIndex"})
)
end,
ascending = function(data)
return WeakAuras.ComposeSorts(
WeakAuras.SortAscending({"region", "state", "expirationTime"}),
WeakAuras.SortAscending({"dataIndex"})
)
end,
descending = function(data)
return WeakAuras.ComposeSorts(
WeakAuras.SortDescending({"region", "state", "expirationTime"}),
WeakAuras.SortAscending({"dataIndex"})
)
end,
custom = function(data)
local sortStr = data.customSort or ""
local sortFunc = WeakAuras.LoadFunction("return " .. sortStr, data.id, "custom sort") or noop
return function(a, b)
Private.ActivateAuraEnvironment(data.id)
local ok, result = pcall(sortFunc, a, b)
Private.ActivateAuraEnvironment()
if not ok then
geterrorhandler()(result)
else
return result
end
end
end
}
WeakAuras.SortFunctions = sorters
local function createSortFunc(data)
local sorter = sorters[data.sort] or sorters.none
return sorter(data)
end
local function polarToRect(r, theta)
return r * math.cos(theta), r * math.sin(theta)
end
local function staggerCoefficient(alignment, stagger)
if alignment == "LEFT" then
if stagger < 0 then
return 1
else
return 0
end
elseif alignment == "RIGHT" then
if stagger > 0 then
return 1
else
return 0
end
else
return 0.5
end
end
local anchorers = {
["NAMEPLATE"] = function(data)
return function(frames, activeRegions)
for _, regionData in ipairs(activeRegions) do
local unit = regionData.region.state and regionData.region.state.unit
local found
if unit then
local frame = WeakAuras.GetNamePlateForUnit(unit)
if frame then
frames[frame] = frames[frame] or {}
tinsert(frames[frame], regionData)
found = true
end
end
if not found and WeakAuras.IsOptionsOpen() and regionData.region.state then
Private.ensurePRDFrame()
Private.personalRessourceDisplayFrame:anchorFrame(regionData.region.state.id, "NAMEPLATE")
frames[Private.personalRessourceDisplayFrame] = frames[Private.personalRessourceDisplayFrame] or {}
tinsert(frames[Private.personalRessourceDisplayFrame], regionData)
end
end
end
end,
["UNITFRAME"] = function(data)
return function(frames, activeRegions)
for _, regionData in ipairs(activeRegions) do
local unit = regionData.region.state and regionData.region.state.unit
if unit then
local frame = WeakAuras.GetUnitFrame(unit) or WeakAuras.HiddenFrames
if frame then
frames[frame] = frames[frame] or {}
tinsert(frames[frame], regionData)
end
end
end
end
end,
["CUSTOM"] = function(data)
local anchorStr = data.customAnchorPerUnit or ""
local anchorFunc = WeakAuras.LoadFunction("return " .. anchorStr, data.id, "custom frame anchor") or noop
return function(frames, activeRegions)
Private.ActivateAuraEnvironment(data.id)
local ok, ret = pcall(anchorFunc, frames, activeRegions)
if not ok then
geterrorhandler()(ret)
end
Private.ActivateAuraEnvironment()
end
end
}
local function createAnchorPerUnitFunc(data)
local anchorer = anchorers[data.anchorPerUnit] or anchorers.NAMEPLATE or anchorers.UNITFRAME
return anchorer(data)
end
local function getDimension(regionData, dim)
return regionData.dimensions[dim]
end
local growers = {
LEFT = function(data)
local stagger = -(data.stagger or 0)
local space = data.space or 0
local limit = data.useLimit and data.limit or math.huge
local startX, startY = 0, 0
local coeff = staggerCoefficient(data.align, data.stagger)
local anchorPerUnitFunc = data.useAnchorPerUnit and createAnchorPerUnitFunc(data)
return function(newPositions, activeRegions)
local frames = {}
if anchorPerUnitFunc then
anchorPerUnitFunc(frames, activeRegions)
else
frames[""] = activeRegions
end
for frame, regionDatas in pairs(frames) do
local numVisible = min(limit, #regionDatas)
local x, y = startX, startY + (numVisible - 1) * stagger * coeff
newPositions[frame] = {}
for i, regionData in ipairs(regionDatas) do
if i <= numVisible then
newPositions[frame][regionData] = { x, y, true }
x = x - regionData.dimensions.width - space
y = y - stagger
end
end
end
end
end,
RIGHT = function(data)
local stagger = data.stagger or 0
local space = data.space or 0
local limit = data.useLimit and data.limit or math.huge
local startX, startY = 0, 0
local coeff = 1 - staggerCoefficient(data.align, stagger)
local anchorPerUnitFunc = data.useAnchorPerUnit and createAnchorPerUnitFunc(data)
return function(newPositions, activeRegions)
local frames = {}
if anchorPerUnitFunc then
anchorPerUnitFunc(frames, activeRegions)
else
frames[""] = activeRegions
end
for frame, regionDatas in pairs(frames) do
local numVisible = min(limit, #regionDatas)
local x, y = startX, startY - (numVisible - 1) * stagger * coeff
newPositions[frame] = {}
for i, regionData in ipairs(regionDatas) do
if i <= numVisible then
newPositions[frame][regionData] = { x, y, true }
x = x + (regionData.dimensions.width) + space
y = y + stagger
end
end
end
end
end,
UP = function(data)
local stagger = data.stagger or 0
local space = data.space or 0
local limit = data.useLimit and data.limit or math.huge
local startX, startY = 0, 0
local coeff = 1 - staggerCoefficient(data.align, stagger)
local anchorPerUnitFunc = data.useAnchorPerUnit and createAnchorPerUnitFunc(data)
return function(newPositions, activeRegions)
local frames = {}
if anchorPerUnitFunc then
anchorPerUnitFunc(frames, activeRegions)
else
frames[""] = activeRegions
end
for frame, regionDatas in pairs(frames) do
local numVisible = min(limit, #regionDatas)
local x, y = startX - (numVisible - 1) * stagger * coeff, startY
newPositions[frame] = {}
for i, regionData in ipairs(regionDatas) do
if i <= numVisible then
newPositions[frame][regionData] = { x, y, true }
x = x + stagger
y = y + (regionData.dimensions.height) + space
end
end
end
end
end,
DOWN = function(data)
local stagger = data.stagger or 0
local space = data.space or 0
local limit = data.useLimit and data.limit or math.huge
local startX, startY = 0, 0
local coeff = staggerCoefficient(data.align, stagger)
local anchorPerUnitFunc = data.useAnchorPerUnit and createAnchorPerUnitFunc(data)
return function(newPositions, activeRegions)
local frames = {}
if anchorPerUnitFunc then
anchorPerUnitFunc(frames, activeRegions)
else
frames[""] = activeRegions
end
for frame, regionDatas in pairs(frames) do
local numVisible = min(limit, #regionDatas)
local x, y = startX - (numVisible - 1) * stagger * coeff, startY
newPositions[frame] = {}
for i, regionData in ipairs(regionDatas) do
if i <= numVisible then
newPositions[frame][regionData] = { x, y, true }
x = x + stagger
y = y - (regionData.dimensions.height) - space
end
end
end
end
end,
HORIZONTAL = function(data)
local stagger = data.stagger or 0
local space = data.space or 0
local limit = data.useLimit and data.limit or math.huge
local midX, midY = 0, 0
local anchorPerUnitFunc = data.useAnchorPerUnit and createAnchorPerUnitFunc(data)
return function(newPositions, activeRegions)
local frames = {}
if anchorPerUnitFunc then
anchorPerUnitFunc(frames, activeRegions)
else
frames[""] = activeRegions
end
for frame, regionDatas in pairs(frames) do
local numVisible = min(limit, #regionDatas)
local totalWidth = (numVisible - 1) * space
for i = 1, numVisible do
local regionData = regionDatas[i]
totalWidth = totalWidth + (regionData.dimensions.width)
end
local x, y = midX - totalWidth/2, midY - (stagger * (numVisible - 1)/2)
newPositions[frame] = {}
for i, regionData in ipairs(regionDatas) do
if i <= numVisible then
x = x + (regionData.dimensions.width) / 2
newPositions[frame][regionData] = { x, y, true }
x = x + (regionData.dimensions.width) / 2 + space
y = y + stagger
end
end
end
end
end,
VERTICAL = function(data)
local stagger = -(data.stagger or 0)
local space = data.space or 0
local limit = data.useLimit and data.limit or math.huge
local midX, midY = 0, 0
local anchorPerUnitFunc = data.useAnchorPerUnit and createAnchorPerUnitFunc(data)
return function(newPositions, activeRegions)
local frames = {}
if anchorPerUnitFunc then
anchorPerUnitFunc(frames, activeRegions)
else
frames[""] = activeRegions
end
for frame, regionDatas in pairs(frames) do
local numVisible = min(limit, #regionDatas)
local totalHeight = (numVisible - 1) * space
for i = 1, numVisible do
local regionData = regionDatas[i]
totalHeight = totalHeight + (regionData.dimensions.height)
end
local x, y = midX - (stagger * (numVisible - 1)/2), midY - totalHeight/2
newPositions[frame] = {}
for i, regionData in ipairs(regionDatas) do
if i <= numVisible then
y = y + (regionData.dimensions.height) / 2
newPositions[frame][regionData] = { x, y, true }
x = x + stagger
y = y + (regionData.dimensions.height) / 2 + space
end
end
end
end
end,
CIRCLE = function(data)
local oX, oY = 0, 0
local constantFactor = data.constantFactor
local space = data.space or 0
local radius = data.radius or 0
local limit = data.useLimit and data.limit or math.huge
local sAngle = (data.rotation or 0) * math.pi / 180
local arc = (data.fullCircle and 360 or data.arcLength or 0) * math.pi / 180
local anchorPerUnitFunc = data.useAnchorPerUnit and createAnchorPerUnitFunc(data)
return function(newPositions, activeRegions)
local frames = {}
if anchorPerUnitFunc then
anchorPerUnitFunc(frames, activeRegions)
else
frames[""] = activeRegions
end
for frame, regionDatas in pairs(frames) do
local numVisible = min(limit, #regionDatas)
local r
if constantFactor == "RADIUS" then
r = radius
else
if numVisible <= 1 then
r = 0
else
r = (numVisible * space) / (2 * math.pi)
end
end
local theta = sAngle
local dAngle
if numVisible == 1 then
dAngle = 0
elseif not data.fullCircle then
dAngle = arc / (numVisible - 1)
else
dAngle = arc / numVisible
end
newPositions[frame] = {}
for i, regionData in ipairs(regionDatas) do
if i <= numVisible then
local x, y = polarToRect(r, theta)
newPositions[frame][regionData] = { x, y, true }
theta = theta + dAngle
end
end
end
end
end,
COUNTERCIRCLE = function(data)
local oX, oY = 0, 0
local constantFactor = data.constantFactor
local space = data.space or 0
local radius = data.radius or 0
local limit = data.useLimit and data.limit or math.huge
local sAngle = (data.rotation or 0) * math.pi / 180
local arc = (data.fullCircle and 360 or data.arcLength or 0) * math.pi / 180
local anchorPerUnitFunc = data.useAnchorPerUnit and createAnchorPerUnitFunc(data)
return function(newPositions, activeRegions)
local frames = {}
if anchorPerUnitFunc then
anchorPerUnitFunc(frames, activeRegions)
else
frames[""] = activeRegions
end
for frame, regionDatas in pairs(frames) do
local numVisible = min(limit, #regionDatas)
local r
if constantFactor == "RADIUS" then
r = radius
else
if numVisible <= 1 then
r = 0
else
r = (numVisible * space) / (2 * math.pi)
end
end
local theta = sAngle
local dAngle
if numVisible == 1 then
dAngle = 0
elseif not data.fullCircle then
dAngle = arc / (1 - numVisible)
else
dAngle = arc / -numVisible
end
newPositions[frame] = {}
for i, regionData in ipairs(regionDatas) do
if i <= numVisible then
local x, y = polarToRect(r, theta)
newPositions[frame][regionData] = { x, y, true }
theta = theta + dAngle
end
end
end
end
end,
GRID = function(data)
local gridType = data.gridType
local gridWidth = data.gridWidth
local rowSpace = data.rowSpace
local colSpace = data.columnSpace
local rowFirst = (gridType:find("^[RL]")) ~= nil
local limit = data.useLimit and data.limit or math.huge
local rowMul, colMul
if gridType:find("D") then
rowMul = -1
else
rowMul = 1
end
if gridType:find("L") then
colMul = -1
else
colMul = 1
end
local primary = {
-- x direction
dim = "width",
coord = 1,
mul = colMul,
space = colSpace,
current = 0
}
local secondary = {
-- y direction
dim = "height",
coord = 2,
mul = rowMul,
space = rowSpace,
current = 0
}
if not rowFirst then
primary, secondary = secondary, primary
end
local anchorPerUnitFunc = data.useAnchorPerUnit and createAnchorPerUnitFunc(data)
return function(newPositions, activeRegions)
local frames = {}
if anchorPerUnitFunc then
anchorPerUnitFunc(frames, activeRegions)
else
frames[""] = activeRegions
end
for frame, regionDatas in pairs(frames) do
local numVisible = min(limit, #regionDatas)
primary.current = 0
secondary.current = 0
secondary.max = 0
newPositions[frame] = {}
for i, regionData in ipairs(regionDatas) do
if i <= numVisible then
newPositions[frame][regionData] = { [primary.coord] = primary.current, [secondary.coord] = secondary.current, [3] = true }
secondary.max = max(secondary.max, getDimension(regionData, secondary.dim))
if i % gridWidth == 0 then
primary.current = 0
secondary.current = secondary.current + (secondary.space + secondary.max) * secondary.mul
secondary.max = 0
else
primary.current = primary.current + (primary.space + getDimension(regionData, primary.dim)) * primary.mul
end
end
end
end
end
end,
CUSTOM = function(data)
local growStr = data.customGrow or ""
local growFunc = WeakAuras.LoadFunction("return " .. growStr, data.id, "custom grow") or noop
return function(newPositions, activeRegions)
Private.ActivateAuraEnvironment(data.id)
local ok, ret = pcall(growFunc, newPositions, activeRegions)
Private.ActivateAuraEnvironment()
if not ok then
geterrorhandler()(ret)
wipe(newPositions)
end
end
end
}
WeakAuras.GrowFunctions = growers
local function createGrowFunc(data)
local grower = growers[data.grow] or growers.DOWN
return grower(data)
end
local function SafeGetPos(region, func)
local ok, value1, value2 = pcall(func, region)
if ok then
return value1, value2
end
end
local function modify(parent, region, data)
Private.FixGroupChildrenOrderForGroup(data)
region:SetScale(data.scale and data.scale > 0 and data.scale <= 10 and data.scale or 1)
WeakAuras.regionPrototype.modify(parent, region, data)
if data.border and (data.grow ~= "CUSTOM" and not data.useAnchorPerUnit) then
local background = region.background
background:SetBackdrop({
edgeFile = data.borderEdge ~= "None" and SharedMedia:Fetch("border", data.borderEdge) or "",
edgeSize = data.borderSize,
bgFile = data.borderBackdrop ~= "None" and SharedMedia:Fetch("background", data.borderBackdrop) or "",
insets = {
left = data.borderInset,
right = data.borderInset,
top = data.borderInset,
bottom = data.borderInset,
},
});
background:SetBackdropBorderColor(data.borderColor[1], data.borderColor[2], data.borderColor[3], data.borderColor[4]);
background:SetBackdropColor(data.backdropColor[1], data.backdropColor[2], data.backdropColor[3], data.backdropColor[4]);
background:ClearAllPoints();
background:SetPoint("bottomleft", region, "bottomleft", -1 * data.borderOffset, -1 * data.borderOffset)
background:SetPoint("topright", region, "topright", data.borderOffset, data.borderOffset)
background:Show();
else
region.background:Hide();
end
function region:IsSuspended()
return not WeakAuras.IsLoginFinished() or self.suspended > 0
end
function region:Suspend()
-- Stops group from repositioning and re-indexing children
-- Calls to Activate, Deactivate, and Re-index will cache the relevant children
-- Similarly, Sort, Position, and Resize will be stopped
-- to be called on the next Resume
-- for when the group is resumed
self.suspended = self.suspended + 1
end
function region:Resume()
-- Allows group to reindex and reposition.
-- TriggersSortUpdatedChildren and PositionChildren to happen
if self.suspended > 0 then
self.suspended = self.suspended - 1
end
region:RunDelayedActions()
end
function region:RunDelayedActions()
if not self:IsSuspended() then
if self.needToReload then
self:ReloadControlledChildren()
end
if self.needToSort then
self:SortUpdatedChildren()
end
if self.needToPosition then
self:PositionChildren()
end
if self.needToResize then
self:Resize()
end
end
end
local function createRegionData(childData, childRegion, childID, cloneID, dataIndex)
cloneID = cloneID or ""
local controlPoint = region.controlPoints:Acquire()
controlPoint:SetWidth(childRegion:GetWidth())
controlPoint:SetHeight(childRegion:GetHeight())
local regionData = {
data = childData,
region = childRegion,
id = childID,
cloneId = cloneID,
dataIndex = dataIndex,
controlPoint = controlPoint,
parent = region
}
if childData.regionType == "text" then
regionData.dimensions = childRegion
else
regionData.dimensions = childData
end
controlPoint.regionData = regionData
childRegion:SetParent(controlPoint)
region.controlledChildren[childID] = region.controlledChildren[childID] or {}
region.controlledChildren[childID][cloneID] = controlPoint
childRegion:SetAnchor(data.selfPoint, controlPoint, data.selfPoint)
if(childData.frameStrata == 1) then
local frameStrata = region:GetFrameStrata()
childRegion:SetFrameStrata(frameStrata ~= "UNKNOWN" and frameStrata or "BACKGROUND");
else
childRegion:SetFrameStrata(Private.frame_strata_types[childData.frameStrata]);
end
Private.ApplyFrameLevel(childRegion)
return regionData
end
local function getRegionData(childID, cloneID)
cloneID = cloneID or ""
local controlPoint
controlPoint = region.controlledChildren[childID] and region.controlledChildren[childID][cloneID]
if not controlPoint then return end
return controlPoint.regionData
end
local function releaseRegionData(regionData)
if region.controlledChildren[regionData.id] then
region.controlledChildren[regionData.id][regionData.cloneId] = nil
end
region.controlPoints:Release(regionData.controlPoint)
end
function region:ReloadControlledChildren()
-- 'forgets' about regions it controls and starts from scratch. Mostly useful when Add()ing the group
if not self:IsSuspended() then
Private.StartProfileSystem("dynamicgroup")
Private.StartProfileAura(data.id)
self.needToReload = false
self.sortedChildren = {}
self.controlledChildren = {}
self.updatedChildren = {}
self.controlPoints:ReleaseAll()
for dataIndex, childID in ipairs(data.controlledChildren) do
local childRegion, childData = WeakAuras.GetRegion(childID), WeakAuras.GetData(childID)
if childRegion and childData then
local regionData = createRegionData(childData, childRegion, childID, nil, dataIndex)
if childRegion.toShow then
tinsert(self.sortedChildren, regionData)
self.updatedChildren[regionData] = true
end
end
if childData and WeakAuras.clones[childID] then
for cloneID, cloneRegion in pairs(WeakAuras.clones[childID]) do
local regionData = createRegionData(childData, cloneRegion, childID, cloneID, dataIndex)
if cloneRegion.toShow then
tinsert(self.sortedChildren, regionData)
self.updatedChildren[regionData] = true
end
end
end
end
Private.StopProfileSystem("dynamicgroup")
Private.StopProfileAura(data.id)
self:SortUpdatedChildren()
else
self.needToReload = true
end
end
function region:AddChild(childID, cloneID)
-- adds regionData to the store.
-- this is useful mostly for when clones are created which we didn't know about last time Reload was called
cloneID = cloneID or ""
if self.controlledChildren[childID] and self.controlledChildren[childID][cloneID] then
return
end
local dataIndex = tIndexOf(data.controlledChildren, childID)
if not dataIndex then return end
local childData = WeakAuras.GetData(childID)
local childRegion = WeakAuras.GetRegion(childID, cloneID)
if not childData or not childRegion then return end
local regionData = createRegionData(childData, childRegion, childID, cloneID, dataIndex)
if childRegion.toShow then
tinsert(self.sortedChildren, regionData)
self.updatedChildren[regionData] = true
end
self:SortUpdatedChildren()
end
function region:ActivateChild(childID, cloneID)
-- Causes the group to start controlling its order and position
-- Called in the child's Expand() method
local regionData = getRegionData(childID, cloneID)
if not regionData then
return self:AddChild(childID, cloneID)
end
if not regionData.region.toShow then return end
-- it's possible that while paused, we might get Activate, Deactivate, Activate on the same child
-- so we need to check if this child has been updated since the last Sort
-- if it has been, then don't insert it again
if not regionData.active and self.updatedChildren[regionData] == nil then
tinsert(self.sortedChildren, regionData)
end
self.updatedChildren[regionData] = true
self:SortUpdatedChildren()
end
function region:RemoveChild(childID, cloneID)
-- removes something from the store. Mostly useful when a clone gets released
-- so that we don't step on our own feet.
local regionData = getRegionData(childID, cloneID)
if not regionData then return end
releaseRegionData(regionData)
self.updatedChildren[regionData] = false
self:SortUpdatedChildren()
end
function region:DeactivateChild(childID, cloneID)
-- Causes the group to stop controlling its order and position
-- Called in the child's Collapse() method
local regionData = getRegionData(childID, cloneID)
if regionData and not regionData.region.toShow then
self.updatedChildren[regionData] = false
end
self:SortUpdatedChildren()
end
region.sortFunc = createSortFunc(data)
function region:SortUpdatedChildren()
-- iterates through cache to insert all updated children in the right spot
-- Called when the Group is Resume()d
-- uses sort data to determine the correct spot
if not self:IsSuspended() then
Private.StartProfileSystem("dynamicgroup")
Private.StartProfileAura(data.id)
self.needToSort = false
local i = 1
while self.sortedChildren[i] do
local regionData = self.sortedChildren[i]
local active = self.updatedChildren[regionData]
if active ~= nil then
regionData.active = active
end
if active == false then
-- i now refers to what was i + 1, so don't increment
tremove(self.sortedChildren, i)
else
local j = i
while j > 1 do
local otherRegionData = self.sortedChildren[j - 1]
if not (active or self.updatedChildren[otherRegionData])
or not self.sortFunc(regionData, otherRegionData) then
break
else
self.sortedChildren[j] = otherRegionData
j = j - 1
self.sortedChildren[j] = regionData
end
end
i = i + 1
end
end
self.updatedChildren = {}
Private.StopProfileSystem("dynamicgroup")
Private.StopProfileAura(data.id)
self:PositionChildren()
else
self.needToSort = true
end
end
region.growFunc = createGrowFunc(data)
region.anchorPerUnit = data.useAnchorPerUnit and data.anchorPerUnit
local animate = data.animate
function region:PositionChildren()
-- Repositions active children according to their index
-- Positioning is based on grow information from the data
if not self:IsSuspended() then
self.needToPosition = false
if #self.sortedChildren > 0 then
if animate then
Private.RegisterGroupForPositioning(data.uid, self)
else
self:DoPositionChildren()
end
else
self:Resize()
end
else
self.needToPosition = true
end
end
function region:DoPositionChildrenPerFrame(frame, positions, handledRegionData)
for regionData, pos in pairs(positions) do
if type(regionData) ~= "table" then
break;
end
handledRegionData[regionData] = true
local x, y, show = type(pos[1]) == "number" and pos[1] or 0,
type(pos[2]) == "number" and pos[2] or 0,
type(pos[3]) ~= "boolean" and true or pos[3]
local controlPoint = regionData.controlPoint
controlPoint:ClearAnchorPoint()
controlPoint:SetAnchorPoint(
data.selfPoint,
frame == "" and self.relativeTo or frame,
data.anchorPoint,
x + data.xOffset, y + data.yOffset
)
if show and frame ~= WeakAuras.HiddenFrames then
controlPoint:Show()
else
controlPoint:Hide()
end
controlPoint:SetWidth(regionData.dimensions.width)
controlPoint:SetHeight(regionData.dimensions.height)
if self.anchorPerUnit == "UNITFRAME" then
Private.dyngroup_unitframe_monitor[regionData] = frame
end
if animate then
Private.CancelAnimation(regionData.controlPoint, true)
local xPrev = regionData.xOffset or x
local yPrev = regionData.yOffset or y
local xDelta = xPrev - x
local yDelta = yPrev - y
if show and (abs(xDelta) > 0.01 or abs(yDelta) > 0.01) then
local anim
if data.grow == "CIRCLE" or data.grow == "COUNTERCIRCLE" then
local originX, originY = 0,0
local radius1, previousAngle = WeakAuras.GetPolarCoordinates(xPrev, yPrev, originX, originY)
local radius2, newAngle = WeakAuras.GetPolarCoordinates(x, y, originX, originY)
local dAngle = newAngle - previousAngle
dAngle = ((dAngle > 180 and dAngle - 360) or (dAngle < -180 and dAngle + 360) or dAngle)
if(math.abs(radius1 - radius2) > 0.1) then
local translateFunc = [[
function(progress, _, _, previousAngle, dAngle)
local previousRadius, dRadius = %f, %f;
local targetX, targetY = %f, %f
local radius = previousRadius + (1 - progress) * dRadius;
local angle = previousAngle + (1 - progress) * dAngle;
return cos(angle) * radius - targetX, sin(angle) * radius - targetY;
end
]]
anim = {
type = "custom",
duration = 0.2,
use_translate = true,
translateType = "custom",
translateFunc = translateFunc:format(radius1, radius2 - radius1, x, y),
x = previousAngle,
y = dAngle,
selfPoint = data.selfPoint,
anchor = self,
anchorPoint = data.selfPoint,
}
else
local translateFunc = [[
function(progress, _, _, previousAngle, dAngle)
local radius = %f;
local targetX, targetY = %f, %f
local angle = previousAngle + (1 - progress) * dAngle;
return cos(angle) * radius - targetX, sin(angle) * radius - targetY;
end
]]
anim = {
type = "custom",
duration = 0.2,
use_translate = true,
translateType = "custom",
translateFunc = translateFunc:format(radius1, x, y),
x = previousAngle,
y = dAngle,
selfPoint = data.selfPoint,
anchor = self,
anchorPoint = data.selfPoint,
}
end
end
if not(anim) then
anim = {
type = "custom",
duration = 0.2,
use_translate = true,
x = xDelta,
y = yDelta,
selfPoint = data.selfPoint,
anchor = self,
anchorPoint = data.selfPoint,
}
end
-- update animated expand & collapse for this child
Private.Animate("controlPoint", data.uid, "controlPoint", anim, regionData.controlPoint, true)
end
end
regionData.xOffset = x
regionData.yOffset = y
regionData.shown = show
end
end
function region:DoPositionChildren()
Private.StartProfileSystem("dynamicgroup")
Private.StartProfileAura(data.id)
local handledRegionData = {}
local newPositions = {}
self.growFunc(newPositions, self.sortedChildren)
if #newPositions > 0 then
for index = 1, #newPositions do
if type(newPositions[index]) == "table" then
local data = self.sortedChildren[index]
if data then
newPositions[data] = newPositions[index]
else
geterrorhandler()(("Error in '%s', Grow function return position for an invalid region"):format(region.id))
end
newPositions[index] = nil
end
end
region:DoPositionChildrenPerFrame("", newPositions, handledRegionData)
else
for frame, positions in pairs(newPositions) do
region:DoPositionChildrenPerFrame(frame, positions, handledRegionData)
end
end
for index, child in ipairs(self.sortedChildren) do
if not handledRegionData[child] then
child.controlPoint:Hide()
end
end
Private.StopProfileSystem("dynamicgroup")
Private.StopProfileAura(data.id)
self:Resize()
end
function region:Resize()
-- Resizes the dynamic group, for background and border purposes
if not self:IsSuspended() then
self.needToResize = false
-- if self.dynamicAnchor then self:UpdateBorder(); return end
Private.StartProfileSystem("dynamicgroup")
Private.StartProfileAura(data.id)
local numVisible, minX, maxX, maxY, minY = 0
for active, regionData in ipairs(self.sortedChildren) do
if regionData.shown then
numVisible = numVisible + 1
local childRegion = regionData.region
local regionLeft, regionRight, regionTop, regionBottom
= SafeGetPos(childRegion, childRegion.GetLeft), SafeGetPos(childRegion, childRegion.GetRight),
SafeGetPos(childRegion, childRegion.GetTop), SafeGetPos(childRegion, childRegion.GetBottom)
if(regionLeft and regionRight and regionTop and regionBottom) then
minX = minX and min(regionLeft, minX) or regionLeft
maxX = maxX and max(regionRight, maxX) or regionRight
minY = minY and min(regionBottom, minY) or regionBottom
maxY = maxY and max(regionTop, maxY) or regionTop
end
end
end
if numVisible > 0 then
self:Show()
minX, maxX, minY, maxY = (minX or 0), (maxX or 0), (minY or 0), (maxY or 0)
local width, height = maxX - minX, maxY - minY
width = width > 0 and width or 16
height = height > 0 and height or 16
self:SetWidth(width)
self:SetHeight(height)
self.currentWidth = width
self.currentHeight = height
else
self:Hide()
end
if WeakAuras.IsOptionsOpen() then
WeakAuras.OptionsFrame().moversizer:ReAnchor()
end
Private.StopProfileSystem("dynamicgroup")
Private.StopProfileAura(data.id)
else
self.needToResize = true
end
end
region:ReloadControlledChildren()
WeakAuras.regionPrototype.modifyFinish(parent, region, data)
end
WeakAuras.RegisterRegionType("dynamicgroup", create, modify, default)