mirror of
https://github.com/fuffc/WeakestAuras.git
synced 2026-11-10 20:07:35 +00:00
ClassicAPI 1.18 is now the only requirement Requirements - Needs ClassicAPI 1.18.0 or newer. SuperWoW is no longer used, and Nampower is optional. - On an older ClassicAPI the addon stays off, and a popup links the download. - Custom triggers on SuperWoW or Nampower events only fire while that mod is installed. Triggers - New Combat Log trigger, with lists of spell IDs, spell names and NPC IDs. WeakAuras2 auras that use one now import. - Custom triggers can listen to the combat log with CLEU or CLEU:SUBEVENT. - Crowd Control matches a specific type (stun, fear, root, silence, disarm and more) or a school lockout, with the effect's real timer. - Saved Crowd Control auras: Root, Silence and Disarm filters carry over; CC and Snare become "any type", and snares are no longer detected. - Cast bars follow pushback and channel updates. Displays - Icon cooldown swipes are drawn by the client's own cooldown frame. - Sound actions play on a chosen channel, and a new Stop sound type ends a looping sound. Fixes - Cooldown auras no longer sweep on every wand shot. - The Global Cooldown trigger ignores wand and Auto Shot shots.
1700 lines
72 KiB
Lua
1700 lines
72 KiB
Lua
-- WeakestAuras -- the shared region base every region type mixes in at create
|
|
-- time. Mirrors WA2's RegionPrototype (§7).
|
|
-- Upstream section refs (§N) point at design/architecture/weakauras2-reference.md
|
|
|
|
if WeakestAuras.disabled then return end
|
|
|
|
local WA = WeakestAuras
|
|
WA.regionPrototype = {}
|
|
local proto = WA.regionPrototype
|
|
|
|
-- Parses an adjusted-min/max field: absolute ("12") or relative ("20%").
|
|
-- Returns exactly one of (absolute, relPercent) non-nil, or both nil for an
|
|
-- empty or unparseable value -- user input must never error here.
|
|
local function ParseAdjust(v)
|
|
if not v or v == "" then return nil, nil end
|
|
local index = string.find(v, "%% *$")
|
|
if index then
|
|
local percent = tonumber(string.sub(v, 1, index - 1))
|
|
if not percent then return nil, nil end
|
|
return nil, percent / 100
|
|
end
|
|
return tonumber(v), nil
|
|
end
|
|
|
|
-- A tiny subscribable object -- the per-region event bus subregions subscribe
|
|
-- to (§8). Port of the idea, not upstream's file. Notifies with up to three
|
|
-- payload args, which covers "Update"(state, states) and the parameterless
|
|
-- lifecycle events (PreShow/PreHide/FrameTick).
|
|
local function CreateSubscribers()
|
|
local obj = { subs = {} }
|
|
function obj:AddSubscriber(event, fn)
|
|
self.subs[event] = self.subs[event] or {}
|
|
table.insert(self.subs[event], fn)
|
|
end
|
|
function obj:Notify(event, a, b, c)
|
|
local list = self.subs[event]
|
|
if not list then return end
|
|
for i = 1, #list do list[i](a, b, c) end
|
|
end
|
|
-- Takes the same function object AddSubscriber was given. A region whose text
|
|
-- a condition can replace has to be able to leave the FrameTick bus again, not
|
|
-- only join it.
|
|
function obj:RemoveSubscriber(event, fn)
|
|
local list = self.subs[event]
|
|
if not list or not fn then return end
|
|
for i = #list, 1, -1 do
|
|
if list[i] == fn then table.remove(list, i) end
|
|
end
|
|
end
|
|
-- Dropped and rebuilt on every modifyFinish so a re-config's stale closures
|
|
-- (pointing at replaced sub-region instances) never keep firing.
|
|
function obj:Clear() self.subs = {} end
|
|
return obj
|
|
end
|
|
|
|
-- One shared OnUpdate driving the FrameTick bus (§7 FrameTick): only regions
|
|
-- with a %p text subscribe, and only while shown, so a display with no per-frame
|
|
-- text costs nothing. RegionPrototype owns the set; Expand/Collapse and
|
|
-- modifyFinish move regions in/out of it.
|
|
local frameTickRegions = {}
|
|
local tickFrame = CreateFrame("Frame")
|
|
tickFrame:SetScript("OnUpdate", function()
|
|
WA.ProfBegin("text frame-tick")
|
|
for region in pairs(frameTickRegions) do
|
|
region.subRegionEvents:Notify("FrameTick")
|
|
end
|
|
WA.ProfEnd("text frame-tick")
|
|
end)
|
|
-- Hidden while the set is empty -- hiding a frame is how an OnUpdate is
|
|
-- switched off on this client, same as the glow ticker and FRAME_UPDATE pulse.
|
|
tickFrame:Hide()
|
|
function proto.RegisterForFrameTick(region)
|
|
frameTickRegions[region] = true
|
|
tickFrame:Show()
|
|
end
|
|
function proto.UnregisterForFrameTick(region)
|
|
frameTickRegions[region] = nil
|
|
if not next(frameTickRegions) then tickFrame:Hide() end
|
|
end
|
|
function proto.CountFrameTick()
|
|
local n = 0
|
|
for _ in pairs(frameTickRegions) do n = n + 1 end
|
|
return n
|
|
end
|
|
|
|
-- Mixed into a region frame by each region type's create(), after its own
|
|
-- frames are built.
|
|
function proto.create(region)
|
|
region.xOffset, region.yOffset = 0, 0
|
|
region.xOffsetAnim, region.yOffsetAnim = 0, 0
|
|
region.xOffsetRelative, region.yOffsetRelative = 0, 0
|
|
region.xOffsetLayout, region.yOffsetLayout = 0, 0
|
|
region.xOffsetBox, region.yOffsetBox = 0, 0
|
|
region.selfPoint = "CENTER"
|
|
region.anchorFrame = UIParent
|
|
region.anchorPoint = "CENTER"
|
|
region.regionAlpha = 1
|
|
region.animAlpha = nil
|
|
region.animatingFinish = false
|
|
region.pendingRelease = false
|
|
region.toShow = false
|
|
region.limited = false
|
|
region.shown = false
|
|
region.state = nil
|
|
region.states = {}
|
|
region.subRegionEvents = CreateSubscribers()
|
|
if WA.AttachActionMethods then WA.AttachActionMethods(region) end
|
|
|
|
-- Effective position composes config + animation + relative(condition) +
|
|
-- layout + box offsets, so those five never fight over SetPoint (§7).
|
|
--
|
|
-- The layout slot belongs to a dynamic group's animated expand and collapse:
|
|
-- the config slot holds the child's *new* slot the moment the layout runs,
|
|
-- and the group decays this one from the old-minus-new delta to zero. It is
|
|
-- a slot of its own rather than a second user of the anim slot because the
|
|
-- animation registry holds one animation per frame -- a child that both
|
|
-- pulses and slides needs somewhere for the slide to live. (Upstream buys the
|
|
-- same separation with a per-child control-point frame.)
|
|
--
|
|
-- The box slot is a group's alone: its frame is sized to its children's
|
|
-- bounding box, and the box is rarely centred on the anchor the children were
|
|
-- measured from (a DOWN grow's hangs entirely below it). Carrying the box
|
|
-- centre here is what makes the frame's *rectangle* the content rectangle
|
|
-- while the anchor still pins the origin -- so GetLeft/GetTop on a group mean
|
|
-- what they say, and the border is a plain outset of the frame. It is a slot
|
|
-- rather than an addend on the config one because the mover writes that one
|
|
-- straight from a drag and would drop the correction.
|
|
function region:UpdatePosition()
|
|
local x = (self.xOffset or 0) + (self.xOffsetAnim or 0) + (self.xOffsetRelative or 0)
|
|
+ (self.xOffsetLayout or 0) + (self.xOffsetBox or 0)
|
|
local y = (self.yOffset or 0) + (self.yOffsetAnim or 0) + (self.yOffsetRelative or 0)
|
|
+ (self.yOffsetLayout or 0) + (self.yOffsetBox or 0)
|
|
self:ClearAllPoints()
|
|
self:SetPoint(self.selfPoint, self.anchorFrame or UIParent, self.anchorPoint, x, y)
|
|
end
|
|
|
|
function region:SetAnchor(selfPoint, anchorFrame, anchorPoint)
|
|
self.selfPoint = selfPoint or "CENTER"
|
|
self.anchorFrame = anchorFrame or UIParent
|
|
self.anchorPoint = anchorPoint or "CENTER"
|
|
self:UpdatePosition()
|
|
end
|
|
function region:SetOffset(x, y) self.xOffset, self.yOffset = x, y; self:UpdatePosition() end
|
|
function region:SetOffsetAnim(x, y) self.xOffsetAnim, self.yOffsetAnim = x, y; self:UpdatePosition() end
|
|
function region:SetOffsetLayout(x, y) self.xOffsetLayout, self.yOffsetLayout = x, y; self:UpdatePosition() end
|
|
function region:SetOffsetBox(x, y) self.xOffsetBox, self.yOffsetBox = x, y; self:UpdatePosition() end
|
|
function region:SetXOffsetRelative(x) self.xOffsetRelative = x; self:UpdatePosition() end
|
|
function region:SetYOffsetRelative(y) self.yOffsetRelative = y; self:UpdatePosition() end
|
|
|
|
function region:SetRegionAlpha(a)
|
|
a = a or 1
|
|
self.regionAlpha = a
|
|
self:SetAlpha(a * (self.animAlpha or 1))
|
|
end
|
|
function region:GetRegionAlpha() return self.regionAlpha or 1 end
|
|
function region:SetAnimAlpha(a)
|
|
self.animAlpha = a
|
|
self:SetAlpha((self.regionAlpha or 1) * (a or 1))
|
|
end
|
|
|
|
-- Deliberately named the same as data.adjustedMin/Max (a string on the data
|
|
-- table) -- upstream's convention.
|
|
function region:SetAdjustedMin(v)
|
|
self.adjustedMin, self.adjustedMinRelPercent = ParseAdjust(v)
|
|
proto.UpdateProgress(self)
|
|
end
|
|
function region:SetAdjustedMax(v)
|
|
self.adjustedMax, self.adjustedMaxRelPercent = ParseAdjust(v)
|
|
proto.UpdateProgress(self)
|
|
end
|
|
|
|
-- -1 automatic (region.state), 0 manual (region.progressSourceManualValue/
|
|
-- Total), N > 0 trigger N's state (region.states[N]).
|
|
function region:SetProgressSource(v)
|
|
self.progressSource = v
|
|
proto.UpdateProgress(self)
|
|
end
|
|
|
|
-- The value range applyStatic/applyTimed last clamped to (post adjusted-
|
|
-- min/max), not the raw state -- a threshold sub-region reads this rather
|
|
-- than re-deriving it from data. 0/0 before the first UpdateProgress.
|
|
function region:GetMinMaxProgress()
|
|
return self.minProgress or 0, self.maxProgress or 0
|
|
end
|
|
|
|
-- toShow guards keep repeated Expand/Collapse idempotent (the state machine
|
|
-- may re-apply a shown state many times). Actions/animations hook these
|
|
-- later without another refactor (§7).
|
|
--
|
|
-- toShow (state machine: has a state to render) and limited (owning dynamic
|
|
-- group: past its visible-clone cap) are independent flags; region.shown --
|
|
-- the actual Show/Hide -- is toShow AND NOT limited. setShown is the only
|
|
-- place that compares against it, so a limit flip and an Expand/Collapse both
|
|
-- go through the same idempotent gate instead of each guarding separately.
|
|
local function setShown(self, want)
|
|
if self.shown == want then return end
|
|
self.shown = want
|
|
if want then
|
|
self.subRegionEvents:Notify("PreShow")
|
|
self:Show()
|
|
if self._hasFrameTick then proto.RegisterForFrameTick(self) end
|
|
else
|
|
self.subRegionEvents:Notify("PreHide")
|
|
proto.UnregisterForFrameTick(self)
|
|
self:Hide()
|
|
end
|
|
end
|
|
|
|
function region:Expand()
|
|
if self.toShow then return end
|
|
if self.animatingFinish then
|
|
WA.CancelAnimation(self, true, true, true, true, true, false)
|
|
self.animatingFinish = false
|
|
end
|
|
self.toShow = true
|
|
setShown(self, not self.limited)
|
|
WA.PerformActions(WeakestAurasDB.displays[self.id], "start", self)
|
|
local data = WeakestAurasDB.displays[self.id]
|
|
local function startMainAnimation()
|
|
if not data then return end
|
|
WA.Animate("display", data.uid, "main", data.animation and data.animation.main,
|
|
self, false, nil, true, self.cloneId)
|
|
end
|
|
if not data or not WA.Animate("display", data.uid, "start", data.animation and data.animation.start,
|
|
self, true, startMainAnimation, false, self.cloneId) then
|
|
startMainAnimation()
|
|
end
|
|
end
|
|
|
|
function region:Collapse(onFinished)
|
|
if not self.toShow then return end
|
|
self.toShow = false
|
|
self.limited = false
|
|
if self.SoundRepeatStop then self:SoundRepeatStop() end
|
|
if self.StopExternalGlows then self:StopExternalGlows() end
|
|
WA.PerformActions(WeakestAurasDB.displays[self.id], "finish", self)
|
|
local data = WeakestAurasDB.displays[self.id]
|
|
local function hideRegion()
|
|
self.animatingFinish = false
|
|
WA.CancelAnimation(self, true, true, true, true, true, false)
|
|
setShown(self, false)
|
|
if onFinished then onFinished() end
|
|
end
|
|
self.animatingFinish = true
|
|
if not data or not WA.Animate("display", data.uid, "finish", data.animation and data.animation.finish,
|
|
self, false, hideRegion, false, self.cloneId) then
|
|
hideRegion()
|
|
end
|
|
end
|
|
|
|
-- The dynamic group's visible-clone cap. Toggling this alone (toShow already
|
|
-- true) can flip the actual Show/Hide without going through Expand/Collapse,
|
|
-- which is what lets a clone stay fully alive -- state, conditions, pooling
|
|
-- identity -- while only its paint is suppressed.
|
|
function region:SetLimited(limited)
|
|
limited = limited and true or false
|
|
if self.limited == limited then return end
|
|
self.limited = limited
|
|
if self.toShow then setShown(self, not limited) end
|
|
end
|
|
|
|
-- The two custom-text update modes are different *call counts*, not
|
|
-- "throttled or not" (WA2 Text.lua). `event` runs the function once per state
|
|
-- update and ignores the throttle entirely -- that is what `force` means
|
|
-- here. `update` runs it on FrameTick, and there the throttle is the
|
|
-- difference between a feature and a frame-rate bug.
|
|
--
|
|
-- Every region type's own Update must call this with force before it (or its
|
|
-- sub-regions) resolve a placeholder: StateMachine runs region:Update ahead
|
|
-- of the sub-region bus, so a refresh at the top of Update is what makes the
|
|
-- values every subtext then reads cost exactly one run.
|
|
function region:RefreshCustomText(force)
|
|
if not self.customTextFunc then return end
|
|
if not force then
|
|
local last = self.lastCustomTextUpdate
|
|
if last and last + (self.customTextThrottle or 0) >= GetTime() then return end
|
|
end
|
|
self.customValues = WA.RunCustomTextFunc(self, self.customTextFunc)
|
|
self.lastCustomTextUpdate = GetTime()
|
|
end
|
|
|
|
-- One stable function object, so modifyFinish's rebuild can put the same one
|
|
-- back rather than accumulating closures.
|
|
region.customTextTick = function() region:RefreshCustomText() end
|
|
end
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- %c custom text (§9)
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
-- The function belongs to the *aura*, not to whatever renders it: upstream
|
|
-- compiles data.customText once and every text of the display indexes the same
|
|
-- result array, so five subtexts sharing %c1..%c5 cost one run per state update
|
|
-- rather than five. It lives on the region for that reason -- an icon has no
|
|
-- text of its own, every icon text being a sub-region, so machinery kept on the
|
|
-- text region left %c unavailable everywhere it is most wanted.
|
|
|
|
-- The default function, seeded into a fresh field and restored by the editor's
|
|
-- Reset -- an emptied box is otherwise unrecoverable without remembering the
|
|
-- signature. Arguments are WA.RunCustomTextFunc's, and every return is a %c:
|
|
-- the first is %c or %c1, the second %c2, and so on.
|
|
local CUSTOM_TEXT_DEFAULT = [[function(expirationTime, duration, progress, dur, name, icon, stacks)
|
|
return ""
|
|
end]]
|
|
|
|
-- Upstream defaults the throttle to 0, which in `update` mode is the custom
|
|
-- function running on every frame. A default that costs frame rate is not a
|
|
-- default. Resolved here rather than seeded into each region type's `defaults`,
|
|
-- so the number is written once -- `or` catches only nil, so a user who
|
|
-- deliberately chooses 0 still gets 0.
|
|
local CUSTOM_TEXT_THROTTLE = 0.2
|
|
|
|
-- Every string this aura might render: the region's own displayText, each
|
|
-- subtext's text_text, and anything a condition swaps into either. The compile
|
|
-- and the options gate both ask this, so the two cannot disagree -- an aura
|
|
-- whose only %c arrives through a condition would otherwise compile a function
|
|
-- it offers no editor to write (WA2's hideCustomTextOption walks conditions for
|
|
-- the same reason).
|
|
function proto.TextStrings(data)
|
|
local texts = {}
|
|
if data.displayText then table.insert(texts, data.displayText) end
|
|
local subs = data.subRegions or {}
|
|
for i = 1, #subs do
|
|
local sub = subs[i]
|
|
if sub.type == "subtext" and sub.text_text then table.insert(texts, sub.text_text) end
|
|
end
|
|
local conditions = data.conditions or {}
|
|
for i = 1, #conditions do
|
|
local changes = conditions[i].changes or {}
|
|
for c = 1, #changes do
|
|
local change = changes[c]
|
|
-- "displayText" on the region itself, "sub.<n>.text_text" on a subtext.
|
|
if type(change.value) == "string" and change.property
|
|
and (change.property == "displayText"
|
|
or string.find(change.property, "%.text_text$")) then
|
|
table.insert(texts, change.value)
|
|
end
|
|
end
|
|
end
|
|
return texts
|
|
end
|
|
|
|
function proto.WantsCustomText(data)
|
|
return WA.ContainsCustomPlaceHolder(proto.TextStrings(data))
|
|
end
|
|
|
|
-- Compiles data.customText onto the region, memoized on the source it came
|
|
-- from. modify is not the cold path it looks like: a `range` field's `set` calls
|
|
-- WA.Add, NewSlider's onChange fires on every step of a drag, and modify runs
|
|
-- each time -- so dragging a slider on an aura using %c would otherwise be one
|
|
-- loadstring per frame. The key has to be the source rather than a dirty flag: a
|
|
-- `set` writing the same text back (which a drag does to every *other* field)
|
|
-- must compare equal. Memoizing the whole attempt, failure included, is also
|
|
-- what keeps a broken function from reporting once per frame of that drag.
|
|
local function applyCustomText(region, data)
|
|
region.customValues = nil
|
|
region.lastCustomTextUpdate = nil
|
|
region.customTextMode = data.customTextUpdate or "event"
|
|
region.customTextThrottle = data.customTextUpdateThrottle or CUSTOM_TEXT_THROTTLE
|
|
|
|
local source = data.customText
|
|
if source == "" then source = nil end
|
|
if not source or not proto.WantsCustomText(data) then
|
|
region.customTextFunc, region.customTextSource = nil, nil
|
|
elseif region.customTextSource ~= source then
|
|
region.customTextSource = source
|
|
region.customTextFunc = WA.LoadFunction(source, tostring(data.id) .. ": custom text")
|
|
-- The source changed, so whatever the old code left in aura_env belongs to
|
|
-- code that no longer exists.
|
|
WA.ClearAuraEnv(data.id)
|
|
end
|
|
end
|
|
|
|
-- The Custom Text block -- the code editor, the update mode, and the throttle
|
|
-- that only decides anything in per-frame mode. One block per *aura* rather than
|
|
-- one per text, since that is what the function is: rendered once on the Display
|
|
-- tab (OptionsFrame's appendDisplayEffectsOptions) rather than inside each
|
|
-- subtext's own section, which would put N identical editors on one page --
|
|
-- upstream shows one subtext at a time and can afford to repeat them.
|
|
-- Empty until something in the aura's text actually references %c, which is also
|
|
-- how the connection between %c and this block is taught.
|
|
function proto.CustomTextOptionFields(data)
|
|
if not proto.WantsCustomText(data) then return {} end
|
|
local fields = {
|
|
{ type = "header", name = "Custom Text" },
|
|
{
|
|
type = "code", name = "Custom Text Function", key = "customText", height = 160,
|
|
-- Raw, not `or ""`: nil is what tells the renderer this has never been
|
|
-- configured and should open at the default below.
|
|
get = function() return data.customText end,
|
|
set = function(v) data.customText = v; WA.Add(data, true) end,
|
|
default = CUSTOM_TEXT_DEFAULT,
|
|
-- Asked of the compiler for its wrapper rather than spelling one here:
|
|
-- two spellings drift, and the symptom is an error line number silently
|
|
-- off by one.
|
|
validate = function(txt)
|
|
return WA.Widgets.LuaSyntaxError(WA.WrapFunctionSource(txt), "custom text")
|
|
end,
|
|
},
|
|
{
|
|
type = "select", name = "Update on", key = "customTextUpdate",
|
|
values = { "event", "update" },
|
|
labels = { event = "Every state change", update = "Every frame" },
|
|
get = function() return data.customTextUpdate or "event" end,
|
|
set = function(v)
|
|
data.customTextUpdate = v
|
|
WA.Add(data, true)
|
|
-- Repaints the tab: the throttle below decides nothing outside the
|
|
-- per-frame mode.
|
|
WA.RefreshOptions()
|
|
end,
|
|
},
|
|
}
|
|
if data.customTextUpdate == "update" then
|
|
table.insert(fields, {
|
|
type = "range", name = "Throttle (seconds)", key = "customTextUpdateThrottle",
|
|
min = 0, max = 2, step = 0.05,
|
|
get = function() return data.customTextUpdateThrottle or CUSTOM_TEXT_THROTTLE end,
|
|
set = function(v) data.customTextUpdateThrottle = v; WA.Add(data, true) end,
|
|
})
|
|
end
|
|
return fields
|
|
end
|
|
|
|
-- Idle sub-region instances, per owning region and keyed by type. **Frames
|
|
-- cannot be destroyed on this client**, so an instance displaced from its slot
|
|
-- has to stay reachable: deleting one effect shifts every later one up a slot,
|
|
-- and each shift past a differently-typed neighbour would otherwise strand an
|
|
-- instance nothing can ever reach again.
|
|
--
|
|
-- The pool is per-region, not global, and must stay that way: a sub-region's
|
|
-- create closes over its parent (subtick goes further and builds its texture on
|
|
-- the parent's bar frame), so an instance is only ever reusable under the region
|
|
-- it was made for.
|
|
local function parkSubRegion(region, inst)
|
|
if inst.Hide then inst:Hide() end
|
|
local free = region.subRegionPool[inst.subType]
|
|
if not free then
|
|
free = {}
|
|
region.subRegionPool[inst.subType] = free
|
|
end
|
|
table.insert(free, inst)
|
|
end
|
|
|
|
local function takeSubRegion(region, subType)
|
|
local free = region.subRegionPool[subType]
|
|
if free and #free > 0 then return table.remove(free) end
|
|
return nil
|
|
end
|
|
|
|
-- Rebuilds a region's sub-region instances from data.subRegions and re-wires
|
|
-- their event subscriptions (§8). Called at the end of each region type's
|
|
-- modify, so config edits, a new state, and a regionType switch all funnel
|
|
-- through one place. Instances are reused in place by index+type across edits
|
|
-- so a slider drag doesn't leak a FontString per tick, and displaced ones go to
|
|
-- the pool above rather than being dropped. Clone pooling reuses the whole
|
|
-- owning region frame; it does not detach individual sub-regions.
|
|
function proto.modifyFinish(region, data)
|
|
local setWidth, setHeight = region.SetRegionWidth, region.SetRegionHeight
|
|
if setWidth and setHeight and not WA.IsGroup(data) then
|
|
-- `width`/`height` hold the *configured* size, never the scaled one, and
|
|
-- they are upstream's field names because user code reads them: a custom
|
|
-- grow is handed regionData.region and does arithmetic on them to lay a
|
|
-- row out. Scale lives in scaleX/scaleY and is applied to the frame at
|
|
-- write time, exactly as upstream's UpdateSize applies `scalex` -- storing
|
|
-- the scaled value here instead would compound it on every Scale.
|
|
--
|
|
-- One field, not a public `width` beside a private `configWidth`: the two
|
|
-- would carry the same number until something wrote one of them directly,
|
|
-- and then Scale would silently restore the stale one.
|
|
region.width = region.width or data.width
|
|
region.height = region.height or data.height
|
|
region.scaleX, region.scaleY = region.scaleX or 1, region.scaleY or 1
|
|
function region:SetRegionWidth(width)
|
|
self.width = width
|
|
setWidth(self, math.max(math.abs(width * (self.scaleX or 1)), 0.01))
|
|
end
|
|
function region:SetRegionHeight(height)
|
|
self.height = height
|
|
setHeight(self, math.max(math.abs(height * (self.scaleY or 1)), 0.01))
|
|
end
|
|
function region:Scale(x, y)
|
|
self.scaleX, self.scaleY = x or 1, y or 1
|
|
self:SetRegionWidth(self.width or data.width)
|
|
self:SetRegionHeight(self.height or data.height)
|
|
end
|
|
region:SetRegionWidth(data.width)
|
|
region:SetRegionHeight(data.height)
|
|
else
|
|
region.Scale = nil
|
|
end
|
|
|
|
local setColor = region.Color
|
|
if setColor then
|
|
local color = data.color or data.text_color or data.barColor or data.foregroundColor or { 1, 1, 1, 1 }
|
|
region.configColorR, region.configColorG = color[1], color[2]
|
|
region.configColorB, region.configColorA = color[3], color[4] or 1
|
|
function region:Color(r, g, b, a)
|
|
self.configColorR, self.configColorG = r, g
|
|
self.configColorB, self.configColorA = b, a or 1
|
|
setColor(self, self.colorAnimR or r, self.colorAnimG or g,
|
|
self.colorAnimB or b, self.colorAnimA or (a or 1))
|
|
end
|
|
function region:ColorAnim(r, g, b, a)
|
|
self.colorAnimR, self.colorAnimG = r, g
|
|
self.colorAnimB, self.colorAnimA = b, a
|
|
setColor(self, r or self.configColorR, g or self.configColorG,
|
|
b or self.configColorB, a or self.configColorA)
|
|
end
|
|
function region:GetColor()
|
|
return self.configColorR, self.configColorG, self.configColorB, self.configColorA
|
|
end
|
|
region:Color(color[1], color[2], color[3], color[4])
|
|
else
|
|
region.ColorAnim, region.GetColor = nil, nil
|
|
end
|
|
|
|
-- A clone parked behind a *former* parent's limit has nobody left to release
|
|
-- it once it's reconfigured under new ownership -- the dynamic group that owns
|
|
-- it now re-applies its own limit on the relayout that follows every WA.Add.
|
|
region:SetLimited(false)
|
|
region.subRegionEvents:Clear()
|
|
applyCustomText(region, data)
|
|
-- Ahead of every sub-region's own subscription, because subscribers fire in
|
|
-- the order they were added: a subtext resolving %c on a frame tick has to
|
|
-- read a value refreshed on *this* frame rather than the previous one.
|
|
if region.customTextFunc and region.customTextMode == "update" then
|
|
region.subRegionEvents:AddSubscriber("FrameTick", region.customTextTick)
|
|
end
|
|
region.subRegions = region.subRegions or {}
|
|
region.subRegionPool = region.subRegionPool or {}
|
|
region.baseFrameLevel = region.baseFrameLevel or region:GetFrameLevel()
|
|
local list = data.subRegions or {}
|
|
local n = #list
|
|
for i = 1, n do
|
|
local subData = list[i]
|
|
local spec = WA.subRegionTypes[subData.type]
|
|
-- A subregion whose type doesn't support this display's region type
|
|
-- (e.g. a glow left on a display switched icon->bar) is hidden rather
|
|
-- than built, so the instance survives a switch back without erroring.
|
|
if spec and (not spec.supports or spec.supports(data.regionType)) then
|
|
local inst = region.subRegions[i]
|
|
if not inst or inst.subType ~= subData.type then
|
|
if inst then parkSubRegion(region, inst) end
|
|
inst = takeSubRegion(region, subData.type)
|
|
if not inst then
|
|
inst = spec.create(region)
|
|
inst.subType = subData.type
|
|
end
|
|
region.subRegions[i] = inst
|
|
end
|
|
if inst.Show then inst:Show() end
|
|
spec.modify(region, inst, data, subData)
|
|
-- After modify, not before: a type that rebuilds or re-parents its
|
|
-- frames there would otherwise be told a level and then discard it.
|
|
-- Optional because not every type has a frame to put on one --
|
|
-- subtick draws on the parent's bar and rides with the spark.
|
|
if inst.SetFrameLevel then
|
|
inst:SetFrameLevel(proto.SubRegionLevel(region, i))
|
|
end
|
|
else
|
|
local inst = region.subRegions[i]
|
|
if inst and inst.Hide then inst:Hide() end
|
|
end
|
|
end
|
|
-- Park instances left over from a shorter config. Bounded by the high-water
|
|
-- mark rather than #: an unsupported type at index 1 never gets an
|
|
-- instance, and # over the resulting hole reports 0, which would skip the
|
|
-- sweep entirely and leave a stale instance drawn.
|
|
local high = region.subRegionHigh or 0
|
|
if n > high then high = n end
|
|
for i = n + 1, high do
|
|
local inst = region.subRegions[i]
|
|
if inst then
|
|
parkSubRegion(region, inst)
|
|
region.subRegions[i] = nil
|
|
end
|
|
end
|
|
region.subRegionHigh = n
|
|
|
|
-- After the sub-region pass, not inside the region type's own modify: a
|
|
-- subbackground row moves the region's level, and its internals have to
|
|
-- follow it or the icon comes out from under its own swipe.
|
|
if region.ApplyInternalFrameLevels then region:ApplyInternalFrameLevels() end
|
|
|
|
proto.RefreshFrameTick(region)
|
|
end
|
|
|
|
-- Re-derives whether anything on this region wants a per-frame repaint and moves
|
|
-- it in or out of the shared tick set. Separate from modifyFinish because a
|
|
-- condition can swap a region's whole text after the fact: a string that gains a
|
|
-- %p has to start ticking without a re-modify, and one that loses it has to stop.
|
|
function proto.RefreshFrameTick(region)
|
|
local ft = region.subRegionEvents.subs["FrameTick"]
|
|
region._hasFrameTick = (ft and #ft > 0) or false
|
|
if region:IsShown() then
|
|
if region._hasFrameTick then proto.RegisterForFrameTick(region)
|
|
else proto.UnregisterForFrameTick(region) end
|
|
end
|
|
end
|
|
|
|
-- The adjusted-min/max arithmetic (region:SetAdjustedMin/Max, both a raw
|
|
-- number or nil plus a relative-percent fallback), shared by every progress
|
|
-- source below -- automatic, manual and per-trigger all clamp the same way.
|
|
local function applyStatic(region, value, total)
|
|
region.progressType = "static"
|
|
value = value or 0
|
|
total = total or 0
|
|
local adjustMin
|
|
if region.adjustedMin then adjustMin = region.adjustedMin
|
|
elseif region.adjustedMinRelPercent then adjustMin = region.adjustedMinRelPercent * total
|
|
else adjustMin = 0 end
|
|
local max
|
|
if region.adjustedMax then max = region.adjustedMax
|
|
elseif region.adjustedMaxRelPercent then max = region.adjustedMaxRelPercent * total
|
|
else max = total end
|
|
region.minProgress, region.maxProgress = adjustMin, max
|
|
region.value = value - adjustMin
|
|
region.total = max - adjustMin
|
|
region.paused = false
|
|
region.remaining = nil
|
|
if region.UpdateValue then region:UpdateValue() end
|
|
end
|
|
|
|
-- paused/remaining ride along separately from duration/expirationTime rather
|
|
-- than through a re-anchored expirationTime (upstream's approach): the bar
|
|
-- and progress-texture regions animate off region.expirationTime in their own
|
|
-- per-frame OnUpdate, so re-anchoring once would still visibly drain between
|
|
-- applies. UpdateTime freezes explicitly instead of computing a fake
|
|
-- expirationTime that only holds still until the next frame.
|
|
local function applyTimed(region, duration, expirationTime, paused, remaining)
|
|
region.progressType = "timed"
|
|
duration = duration or 0
|
|
expirationTime = expirationTime or 0
|
|
local adjustMin
|
|
if region.adjustedMin then adjustMin = region.adjustedMin
|
|
elseif region.adjustedMinRelPercent then adjustMin = region.adjustedMinRelPercent * duration
|
|
else adjustMin = 0 end
|
|
local max
|
|
if duration == 0 then max = 0
|
|
elseif region.adjustedMax then max = region.adjustedMax
|
|
elseif region.adjustedMaxRelPercent then max = region.adjustedMaxRelPercent * duration
|
|
else max = duration end
|
|
region.minProgress, region.maxProgress = adjustMin, max
|
|
region.duration = max - adjustMin
|
|
region.expirationTime = expirationTime - adjustMin
|
|
region.paused = paused and true or false
|
|
region.remaining = (type(remaining) == "number" and remaining) or (paused and 0) or nil
|
|
if region.UpdateTime then region:UpdateTime() end
|
|
end
|
|
|
|
-- Shared progress resolver (§7 UpdateProgressFrom). region.progressSource
|
|
-- picks which table drives the fill: -1 automatic (region.state, the active
|
|
-- trigger), 0 manual (region.progressSourceManualValue/Total), N > 0 trigger
|
|
-- N's state (region.states[N], filled by every apply regardless of which
|
|
-- trigger is active -- StateMachine.lua's ApplyStatesToRegions).
|
|
function proto.UpdateProgress(region)
|
|
local source = region.progressSource or -1
|
|
if source == 0 then
|
|
local value = region.progressSourceManualValue
|
|
if type(value) ~= "number" then value = 0 end
|
|
local total = region.progressSourceManualTotal
|
|
if type(total) ~= "number" then total = 100 end
|
|
applyStatic(region, value, total)
|
|
return
|
|
end
|
|
local state
|
|
if source > 0 then state = region.states and region.states[source]
|
|
else state = region.state end
|
|
-- A region's *visibility* is driven by the active trigger, not by its
|
|
-- progress source -- when the chosen trigger has no state, the display
|
|
-- isn't shown at all, so there's nothing to clear here. Inventing a
|
|
-- cleared/zeroed fill for a hidden region would be dead work at best and
|
|
-- a flash of "0%" at worst if it's ever shown before this trigger fires.
|
|
if not state then return end
|
|
region.stateInverse = state.inverse and true or false
|
|
if state.progressType == "timed" then
|
|
applyTimed(region, state.duration, state.expirationTime, state.paused, state.remaining)
|
|
else
|
|
applyStatic(region, state.value, state.total)
|
|
end
|
|
end
|
|
|
|
-- Cooldown swipe (the radial spiral): ClassicAPI's native Cooldown frame,
|
|
-- which draws in the engine and so costs no Lua per frame. A child frame
|
|
-- covering the parent, one level above it. The bling is off because upstream
|
|
-- turns it off too. The countdown numbers are hidden, because the text
|
|
-- subregions carry the countdown.
|
|
function proto.CreateSwipe(parent)
|
|
local swipe = CreateFrame("Cooldown", nil, parent)
|
|
swipe:SetAllPoints(parent)
|
|
swipe:SetFrameLevel(parent:GetFrameLevel() + 1)
|
|
swipe:SetDrawBling(false)
|
|
swipe:SetHideCountdownNumbers(true)
|
|
return swipe
|
|
end
|
|
|
|
-- Where the subregion band starts, relative to a region's held base. A child
|
|
-- frame's draw layers all sit above its parent's, so anything a region type
|
|
-- builds as a child (a progress bar, the icon's cooldown swipe) would otherwise
|
|
-- cover text or a border created on the region -- the gap between the base and
|
|
-- here is what those internals get.
|
|
--
|
|
-- The region's own art holds a slot in the band too -- its subbackground row --
|
|
-- and its internals then sit inside that slot rather than down here. The gap is
|
|
-- what a region carrying no such row still needs: a group, or any region
|
|
-- modified with data that never reached MergeDefaults.
|
|
proto.SUB_LEVEL = 5
|
|
|
|
-- Levels reserved per subregion, so a type needing more than one frame of its
|
|
-- own has room without landing on its neighbour's. The widest occupant is what
|
|
-- sets it: the subbackground row stands for the region's own art, and a
|
|
-- progressbar spends three levels there (the region, its bar, its icon frame).
|
|
-- subglow spends two, its backdrop under its art. At a narrower step the next
|
|
-- row down the list ties with one of those, leaving the winner to creation
|
|
-- order rather than to what the user arranged.
|
|
proto.SUB_STEP = 3
|
|
|
|
-- The draw level of the i-th entry of data.subRegions, in list order: the first
|
|
-- effect sits lowest, the last on top. Type no longer decides -- moving a row in
|
|
-- the Display Effects list is what restacks it.
|
|
--
|
|
-- Off the held base, never off the live level: the subbackground row moves the
|
|
-- region's *own* level, so reading it back here would fold that offset into the
|
|
-- base and stack another SUB_LEVEL onto it every repaint.
|
|
function proto.SubRegionLevel(region, index)
|
|
return proto.BaseFrameLevel(region) + proto.SUB_LEVEL + (index - 1) * proto.SUB_STEP
|
|
end
|
|
|
|
-- The level this region would sit at carrying no subbackground row: what the
|
|
-- client hands a fresh child of its parent. ResetFrameLevel records it; the
|
|
-- fallback covers a region asked before one ever ran.
|
|
function proto.BaseFrameLevel(region)
|
|
return region.baseFrameLevel or region:GetFrameLevel()
|
|
end
|
|
|
|
-- Puts the region back on its natural level and records it. Asserted rather
|
|
-- than read back, because a subbackground row leaves the region raised and
|
|
-- SetParent to the parent it already had does not undo that -- reading would
|
|
-- take the raised value for the base and ratchet it on every pass.
|
|
function proto.ResetFrameLevel(region)
|
|
local parent = region:GetParent()
|
|
if parent and parent.GetFrameLevel then
|
|
region:SetFrameLevel(parent:GetFrameLevel() + 1)
|
|
end
|
|
region.baseFrameLevel = region:GetFrameLevel()
|
|
end
|
|
|
|
-- Area-anchors a subregion frame over the whole parent region (border/glow
|
|
-- cover the region rather than self-anchoring to one point the way subtext
|
|
-- does, §8 anchor_area). inset grows(+)/shrinks(-) the covered rect.
|
|
function proto.AnchorArea(region, parent, inset)
|
|
inset = inset or 0
|
|
region:ClearAllPoints()
|
|
region:SetPoint("TOPLEFT", parent, "TOPLEFT", -inset, inset)
|
|
region:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", inset, -inset)
|
|
end
|
|
|
|
local function subRegionAnchorValue(parentData, subData, key, fallback)
|
|
local value = subData[key]
|
|
if value ~= nil then return value end
|
|
return fallback
|
|
end
|
|
|
|
function proto.GetSubRegionAnchorTarget(parent, key)
|
|
if parent.GetSubAnchorTarget then
|
|
return parent:GetSubAnchorTarget(key)
|
|
end
|
|
return parent
|
|
end
|
|
|
|
function proto.GetSubRegionAnchorPoint(key, fallback)
|
|
if not key or key == "region" or key == "bar" or key == "icon" or key == "fg" or key == "bg" or key == "SPARK" then
|
|
return fallback or "CENTER"
|
|
end
|
|
local point = string.gsub(key, "^ICON_", "")
|
|
point = string.gsub(point, "^INNER_", "")
|
|
point = string.gsub(point, "^OUTER_", "")
|
|
return point
|
|
end
|
|
|
|
local POINTS = {
|
|
CENTER = true, TOP = true, BOTTOM = true, LEFT = true, RIGHT = true,
|
|
TOPLEFT = true, TOPRIGHT = true, BOTTOMLEFT = true, BOTTOMRIGHT = true,
|
|
}
|
|
|
|
function proto.IsAnchorPoint(value)
|
|
return POINTS[value] == true
|
|
end
|
|
|
|
-- SetPoint raises "Unknown region point" on anything outside those nine, and the
|
|
-- error escapes through whatever repaint it was in -- selection preview included,
|
|
-- which is how a bad anchor makes an aura unclickable rather than merely
|
|
-- misplaced. Saved data can hold a combined key ("OUTER_TOPLEFT", "SPARK") where
|
|
-- a bare point belongs, so nothing reaches SetPoint without passing through here.
|
|
function proto.ResolveAnchorPoint(value, fallback)
|
|
fallback = POINTS[fallback] and fallback or "CENTER"
|
|
if POINTS[value] then return value end
|
|
if type(value) ~= "string" then return fallback end
|
|
local point = proto.GetSubRegionAnchorPoint(value, fallback)
|
|
return POINTS[point] and point or fallback
|
|
end
|
|
|
|
local INVERSE_POINTS = {
|
|
TOPLEFT = "BOTTOMRIGHT", TOP = "BOTTOM", TOPRIGHT = "BOTTOMLEFT",
|
|
LEFT = "RIGHT", CENTER = "CENTER", RIGHT = "LEFT",
|
|
BOTTOMLEFT = "TOPRIGHT", BOTTOM = "TOP", BOTTOMRIGHT = "TOPLEFT",
|
|
}
|
|
|
|
-- The self point upstream's AUTO derives (WA2 SubText.lua): on an icon it reads
|
|
-- the anchored part -- inside keeps the point, outside inverts it so the text
|
|
-- sits clear of the edge -- a bar keeps the point, anything else inverts. Ours
|
|
-- stores a real self point instead of resolving AUTO at paint time, so this is
|
|
-- what fills it in when the anchor comes from upstream.
|
|
function proto.AutoSelfPoint(anchorKey, point, regionType)
|
|
point = proto.ResolveAnchorPoint(point, "CENTER")
|
|
if regionType == "icon" then
|
|
if type(anchorKey) == "string" then
|
|
if string.sub(anchorKey, 1, 6) == "INNER_" then return point end
|
|
if string.sub(anchorKey, 1, 6) == "OUTER_" then return INVERSE_POINTS[point] end
|
|
end
|
|
return "CENTER"
|
|
end
|
|
if regionType == "progressbar" then return point end
|
|
return INVERSE_POINTS[point]
|
|
end
|
|
|
|
function proto.GetSubRegionAnchors(parentData, mode)
|
|
local spec = WA.RegionSpecFor(parentData)
|
|
local anchors = spec and spec.getSubRegionAnchors and spec.getSubRegionAnchors(parentData) or {}
|
|
local copy = { region = { display = "Whole region", point = true, area = true } }
|
|
for key, anchor in pairs(anchors) do copy[key] = anchor end
|
|
anchors = copy
|
|
local values, labels = {}, {}
|
|
for key, anchor in pairs(anchors) do
|
|
if anchor[mode] then
|
|
table.insert(values, key)
|
|
labels[key] = anchor.display or key
|
|
end
|
|
end
|
|
table.sort(values)
|
|
return values, labels
|
|
end
|
|
|
|
function proto.AnchorSubRegion(frame, parent, subData, defaults)
|
|
defaults = defaults or {}
|
|
local mode = defaults.areaOnly and "area" or (subData.anchor_mode or defaults.mode or "point")
|
|
local targetKey
|
|
local targetPoint
|
|
local selfPoint
|
|
local xOffset
|
|
local yOffset
|
|
if mode == "area" then
|
|
targetKey = subRegionAnchorValue(parent, subData, "anchor_area", defaults.areaTarget or "region")
|
|
xOffset = subRegionAnchorValue(parent, subData, "anchorXOffset", subData.border_offset or defaults.x or 0)
|
|
yOffset = subRegionAnchorValue(parent, subData, "anchorYOffset", subData.border_offset or defaults.y or 0)
|
|
else
|
|
targetKey = subRegionAnchorValue(parent, subData, "anchor_target", defaults.target or "region")
|
|
-- Upstream stores the anchored part and the point in one value; ours splits
|
|
-- them across anchor_target and anchor_point. A combined value sitting in
|
|
-- anchor_point therefore names the part too, unless a target was picked
|
|
-- separately.
|
|
if not subData.anchor_target and type(subData.anchor_point) == "string"
|
|
and not proto.IsAnchorPoint(subData.anchor_point) then
|
|
targetKey = subData.anchor_point
|
|
end
|
|
if subData.anchor_point then
|
|
targetPoint = subData.anchor_point
|
|
elseif subData.anchor_target then
|
|
targetPoint = proto.GetSubRegionAnchorPoint(targetKey, defaults.anchorPoint)
|
|
else
|
|
targetPoint = subRegionAnchorValue(parent, subData, "anchor_point", defaults.anchorPoint or "CENTER")
|
|
end
|
|
selfPoint = subRegionAnchorValue(parent, subData, "self_point", defaults.selfPoint or targetPoint or "CENTER")
|
|
targetPoint = proto.ResolveAnchorPoint(targetPoint, "CENTER")
|
|
selfPoint = proto.ResolveAnchorPoint(selfPoint, targetPoint)
|
|
xOffset = subRegionAnchorValue(parent, subData, "anchorXOffset", defaults.x or 0)
|
|
yOffset = subRegionAnchorValue(parent, subData, "anchorYOffset", defaults.y or 0)
|
|
end
|
|
|
|
local target = proto.GetSubRegionAnchorTarget(parent, targetKey)
|
|
if not target then target = parent end
|
|
frame:ClearAllPoints()
|
|
if mode == "area" then
|
|
frame:SetPoint("TOPLEFT", target, "TOPLEFT", -xOffset, yOffset)
|
|
frame:SetPoint("BOTTOMRIGHT", target, "BOTTOMRIGHT", xOffset, -yOffset)
|
|
else
|
|
frame:SetPoint(selfPoint or "CENTER", target, targetPoint or "CENTER", xOffset, yOffset)
|
|
end
|
|
end
|
|
|
|
function proto.SubRegionAnchorFields(parentData, subData, defaults)
|
|
defaults = defaults or {}
|
|
local fields = {}
|
|
local modeField = {
|
|
type = "select", name = "Anchor mode", key = "anchor_mode",
|
|
values = { "point", "area" },
|
|
labels = { point = "Point", area = "Area" },
|
|
get = function() return subData.anchor_mode or defaults.mode or "point" end,
|
|
set = function(v)
|
|
subData.anchor_mode = v
|
|
WA.Add(parentData, true)
|
|
WA.RefreshOptions()
|
|
end,
|
|
}
|
|
|
|
local mode = defaults.areaOnly and "area" or (subData.anchor_mode or defaults.mode or "point")
|
|
if mode == "area" then
|
|
if not defaults.areaOnly then table.insert(fields, modeField) end
|
|
local values, labels = proto.GetSubRegionAnchors(parentData, "area")
|
|
table.insert(fields, {
|
|
type = "select", name = "Area", key = "anchor_area", values = values, labels = labels,
|
|
get = function() return subData.anchor_area or defaults.areaTarget or "region" end,
|
|
set = function(v) subData.anchor_area = v; WA.Add(parentData, true) end,
|
|
})
|
|
else
|
|
local values, labels = proto.GetSubRegionAnchors(parentData, "point")
|
|
local targetField = {
|
|
type = "select", name = "Target", key = "anchor_target", values = values, labels = labels,
|
|
get = function() return subData.anchor_target or defaults.target or "region" end,
|
|
set = function(v)
|
|
subData.anchor_target = v
|
|
local point = proto.GetSubRegionAnchorPoint(v)
|
|
subData.anchor_point, subData.self_point = point, point
|
|
WA.Add(parentData, true)
|
|
end,
|
|
}
|
|
table.insert(fields, {
|
|
type = "anchorlayout", grid = {
|
|
type = "anchorgrid", name = "Anchor", key = "self_point",
|
|
values = proto.anchorGridPoints, width = 100, height = 50,
|
|
get = function() return subData.self_point or defaults.selfPoint or "CENTER" end,
|
|
set = function(v)
|
|
subData.anchor_point, subData.self_point = v, v
|
|
WA.Add(parentData, true)
|
|
end,
|
|
},
|
|
sideFields = { modeField, targetField },
|
|
})
|
|
end
|
|
|
|
table.insert(fields, {
|
|
type = "range", name = mode == "area" and "Extra width" or "X", key = "anchorXOffset", half = true,
|
|
min = -200, max = 200, step = 1,
|
|
get = function() return subData.anchorXOffset or defaults.x or 0 end,
|
|
set = function(v) subData.anchorXOffset = v; WA.Add(parentData, true) end,
|
|
})
|
|
table.insert(fields, {
|
|
type = "range", name = mode == "area" and "Extra height" or "Y", key = "anchorYOffset", half = true,
|
|
min = -200, max = 200, step = 1, half = true,
|
|
get = function() return subData.anchorYOffset or defaults.y or 0 end,
|
|
set = function(v) subData.anchorYOffset = v; WA.Add(parentData, true) end,
|
|
})
|
|
return fields
|
|
end
|
|
|
|
-- Injects the universal conditionable properties into a region type's registry
|
|
-- (§7 AddProperties). Lives beside the setters it names so the two stay honest.
|
|
function proto.AddProperties(properties)
|
|
properties.sound = { display = "Sound", action = "SoundPlay", type = "sound" }
|
|
properties.chat = { display = "Chat Message", action = "SendChat", type = "chat" }
|
|
properties.customcode = { display = "Run Custom Code", action = "RunCode", type = "customcode" }
|
|
properties.glowexternal = { display = "Glow External Element", action = "GlowExternal", type = "glowexternal" }
|
|
properties.alpha = { display = "Alpha", setter = "SetRegionAlpha", type = "number", min = 0, max = 1, step = 0.05 }
|
|
-- These are *relative* deltas only conditions set (no data.<key> backing
|
|
-- them), so their restored base is an explicit 0, not data[key] (§7).
|
|
properties.xOffsetRelative = { display = "X Offset", setter = "SetXOffsetRelative", type = "number", min = -200, max = 200, step = 1, base = 0 }
|
|
properties.yOffsetRelative = { display = "Y Offset", setter = "SetYOffsetRelative", type = "number", min = -200, max = 200, step = 1, base = 0 }
|
|
return properties
|
|
end
|
|
|
|
-- Separate from AddProperties: only progress region types (icon, progressbar,
|
|
-- progresstexture) have adjusted min/max to condition on, so this isn't folded
|
|
-- into the universal set a future non-progress region type would also inherit.
|
|
function proto.AddProgressProperties(properties)
|
|
properties.adjustedMin = { display = "Minimum Progress", setter = "SetAdjustedMin", type = "string" }
|
|
properties.adjustedMax = { display = "Maximum Progress", setter = "SetAdjustedMax", type = "string" }
|
|
return properties
|
|
end
|
|
|
|
-- The nine anchor tokens every region's self/anchor point picks from.
|
|
proto.anchorPoints = { "CENTER", "TOP", "BOTTOM", "LEFT", "RIGHT",
|
|
"TOPLEFT", "TOPRIGHT", "BOTTOMLEFT", "BOTTOMRIGHT" }
|
|
proto.anchorGridPoints = { "TOPLEFT", "TOP", "TOPRIGHT", "LEFT", "CENTER", "RIGHT",
|
|
"BOTTOMLEFT", "BOTTOM", "BOTTOMRIGHT" }
|
|
|
|
-- Upstream's frame_strata_types (WA2 Types.lua): 1-based, where 1 is
|
|
-- "Inherited" rather than a real strata. Shared by the Frame strata select
|
|
-- (labels) and ApplyFrameStrata (lookup) so the two can't disagree.
|
|
local FRAME_STRATA_NAMES = {
|
|
[1] = "Inherited",
|
|
[2] = "BACKGROUND",
|
|
[3] = "LOW",
|
|
[4] = "MEDIUM",
|
|
[5] = "HIGH",
|
|
[6] = "DIALOG",
|
|
[7] = "FULLSCREEN",
|
|
[8] = "FULLSCREEN_DIALOG",
|
|
[9] = "TOOLTIP",
|
|
}
|
|
local FRAME_STRATA_VALUES = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }
|
|
|
|
-- The names SetFrameStrata will take, for checking one that came back off a
|
|
-- frame rather than out of the table above.
|
|
local REAL_STRATA = {}
|
|
for i = 2, #FRAME_STRATA_VALUES do REAL_STRATA[FRAME_STRATA_NAMES[i]] = true end
|
|
|
|
local ANCHOR_FRAME_TYPES = { "SCREEN", "UIPARENT", "SELECTFRAME", "MOUSE", "NAMEPLATE", "UNITFRAME", "CUSTOM" }
|
|
local ANCHOR_FRAME_LABELS = {
|
|
SCREEN = "Screen / group",
|
|
UIPARENT = "UIParent",
|
|
SELECTFRAME = "Selected frame",
|
|
MOUSE = "Mouse",
|
|
NAMEPLATE = "Nameplate",
|
|
UNITFRAME = "Unit frame",
|
|
CUSTOM = "Custom",
|
|
}
|
|
local ANCHOR_FRAME_VALUES = { "SCREEN", "UIPARENT", "SELECTFRAME", "MOUSE", "NAMEPLATE", "UNITFRAME", "CUSTOM" }
|
|
|
|
local hiddenFrames
|
|
local pendingAnchorRetries = {}
|
|
local anchorRetryScheduled
|
|
local mouseAnchorFrame
|
|
local optionsNameplateAnchorFrame
|
|
local mouseAnchorMarker
|
|
local nameplateAnchorMarker
|
|
local dynamicAnchorWatcher
|
|
local nameplateFor
|
|
|
|
local function moveMouseAnchor()
|
|
local x, y = GetCursorPosition()
|
|
local scale = UIParent.GetEffectiveScale and UIParent:GetEffectiveScale() or 1
|
|
if not scale or scale == 0 then scale = 1 end
|
|
mouseAnchorFrame:ClearAllPoints()
|
|
mouseAnchorFrame:SetPoint("CENTER", UIParent, "BOTTOMLEFT", x / scale, y / scale)
|
|
end
|
|
|
|
local function hiddenAnchorFrame()
|
|
if not hiddenFrames then
|
|
hiddenFrames = CreateFrame("Frame", "WeakestAurasHiddenFrames", UIParent)
|
|
hiddenFrames:SetWidth(1)
|
|
hiddenFrames:SetHeight(1)
|
|
hiddenFrames:Hide()
|
|
WA.HiddenFrames = hiddenFrames
|
|
end
|
|
return hiddenFrames
|
|
end
|
|
|
|
local function createMouseAnchorFrame()
|
|
if mouseAnchorFrame then return mouseAnchorFrame end
|
|
mouseAnchorFrame = CreateFrame("Frame", "WeakestAurasMouseAnchor", UIParent)
|
|
mouseAnchorFrame:SetWidth(1)
|
|
mouseAnchorFrame:SetHeight(1)
|
|
mouseAnchorFrame:Hide()
|
|
mouseAnchorMarker = CreateFrame("Frame", nil, mouseAnchorFrame)
|
|
mouseAnchorMarker:SetWidth(92)
|
|
mouseAnchorMarker:SetHeight(28)
|
|
mouseAnchorMarker:SetPoint("CENTER", mouseAnchorFrame, "CENTER")
|
|
mouseAnchorMarker:SetFrameStrata("TOOLTIP")
|
|
mouseAnchorMarker:SetBackdrop({
|
|
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
|
edgeSize = 10,
|
|
insets = { left = 0, right = 0, top = 0, bottom = 0 },
|
|
})
|
|
mouseAnchorMarker:SetBackdropBorderColor(1, 0.82, 0, 1)
|
|
local label = mouseAnchorMarker:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
|
|
label:SetPoint("TOP", mouseAnchorMarker, "BOTTOM", 0, -2)
|
|
label:SetJustifyH("CENTER")
|
|
label:SetText("Mouse Anchor")
|
|
if label.SetTextColor then label:SetTextColor(1, 0.82, 0, 1) end
|
|
mouseAnchorFrame.mouseAnchorLabel = label
|
|
mouseAnchorMarker:Hide()
|
|
mouseAnchorFrame:SetScript("OnUpdate", function()
|
|
if WA.optionsOpen then return end
|
|
moveMouseAnchor()
|
|
end)
|
|
mouseAnchorFrame:Show()
|
|
return mouseAnchorFrame
|
|
end
|
|
|
|
local function xPositionNextToOptions()
|
|
local panel = getglobal("WeakestAurasOptions")
|
|
local screenWidth = GetScreenWidth()
|
|
if not panel or not panel.GetLeft or not panel.GetRight then return screenWidth / 2 end
|
|
local left, right = panel:GetLeft(), panel:GetRight()
|
|
if not left or not right then return screenWidth / 2 end
|
|
local center = (left + right) / 2
|
|
if center > screenWidth / 2 then
|
|
if left > 400 then return left - 200 end
|
|
return left / 2
|
|
end
|
|
if screenWidth - right > 400 then return right + 200 end
|
|
return (screenWidth + right) / 2
|
|
end
|
|
|
|
local function positionMouseAnchor()
|
|
if not mouseAnchorFrame or not WA.optionsOpen then return end
|
|
local panel = getglobal("WeakestAurasOptions")
|
|
local top = panel and panel.GetTop and panel:GetTop()
|
|
local bottom = panel and panel.GetBottom and panel:GetBottom()
|
|
local y = (top and bottom and (top + bottom) / 2) or (GetScreenHeight() / 2)
|
|
local x = xPositionNextToOptions()
|
|
mouseAnchorFrame:ClearAllPoints()
|
|
mouseAnchorFrame:SetPoint("CENTER", UIParent, "BOTTOMLEFT", x, y)
|
|
end
|
|
|
|
local function createOptionsNameplateAnchorFrame()
|
|
if optionsNameplateAnchorFrame then return optionsNameplateAnchorFrame end
|
|
optionsNameplateAnchorFrame = CreateFrame("Frame", "WeakestAurasNameplateAnchor", UIParent)
|
|
optionsNameplateAnchorFrame:SetWidth(200)
|
|
optionsNameplateAnchorFrame:SetHeight(40)
|
|
optionsNameplateAnchorFrame:SetFrameStrata("TOOLTIP")
|
|
optionsNameplateAnchorFrame:SetBackdrop({
|
|
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
|
edgeSize = 12,
|
|
insets = { left = 0, right = 0, top = 0, bottom = 0 },
|
|
})
|
|
optionsNameplateAnchorFrame:SetBackdropBorderColor(0, 1, 0, 1)
|
|
nameplateAnchorMarker = optionsNameplateAnchorFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
|
|
nameplateAnchorMarker:SetPoint("CENTER", optionsNameplateAnchorFrame, "CENTER")
|
|
nameplateAnchorMarker:SetJustifyH("CENTER")
|
|
nameplateAnchorMarker:SetText("Nameplate Anchor")
|
|
if nameplateAnchorMarker.SetTextColor then nameplateAnchorMarker:SetTextColor(0, 1, 0, 1) end
|
|
optionsNameplateAnchorFrame:Hide()
|
|
return optionsNameplateAnchorFrame
|
|
end
|
|
|
|
local function positionNameplateAnchor()
|
|
if not optionsNameplateAnchorFrame or not WA.optionsOpen then return end
|
|
local panel = getglobal("WeakestAurasOptions")
|
|
local top = panel and panel.GetTop and panel:GetTop()
|
|
local bottom = panel and panel.GetBottom and panel:GetBottom()
|
|
local x = panel and panel.GetLeft and panel:GetLeft() or (GetScreenWidth() / 2)
|
|
local right = panel and panel.GetRight and panel:GetRight()
|
|
local width = optionsNameplateAnchorFrame:GetWidth() or 200
|
|
local height = optionsNameplateAnchorFrame:GetHeight() or 40
|
|
local y = (top and bottom and bottom - 24 - height / 2) or (GetScreenHeight() / 2)
|
|
if right and x then x = (x + right) / 2 end
|
|
optionsNameplateAnchorFrame:ClearAllPoints()
|
|
optionsNameplateAnchorFrame:SetPoint("CENTER", UIParent, "BOTTOMLEFT", x, y)
|
|
end
|
|
|
|
local function queueAnchorRetry(data)
|
|
if not data or not data.id then return end
|
|
pendingAnchorRetries[data.id] = true
|
|
if anchorRetryScheduled then return end
|
|
anchorRetryScheduled = true
|
|
C_Timer.After(1, function()
|
|
anchorRetryScheduled = nil
|
|
local retry = pendingAnchorRetries
|
|
pendingAnchorRetries = {}
|
|
for id in pairs(retry) do
|
|
local d = WeakestAurasDB.displays[id]
|
|
local region = d and WA.PeekRegion(id, "")
|
|
if d and region then proto.ApplyPosition(region, d) end
|
|
end
|
|
end)
|
|
end
|
|
|
|
-- How anchorFrameFrame names another aura rather than a global frame. Public
|
|
-- because both the frame chooser and the WeakAuras2 import build the string, and
|
|
-- three spellings of one prefix is a bug nothing would catch.
|
|
WA.ANCHOR_AURA_PREFIX = "WeakestAuras:"
|
|
|
|
function WA.GetAnchorAuraID(data)
|
|
if not data or data.anchorFrameType ~= "SELECTFRAME" then return nil end
|
|
local ref = data.anchorFrameFrame
|
|
if not ref or ref == "" then return nil end
|
|
local _, _, id = string.find(ref, "^WeakestAuras:(.+)$")
|
|
if id then return id end
|
|
if WeakestAurasDB.displays[ref] then return ref end
|
|
return nil
|
|
end
|
|
|
|
local function anchorReference(data)
|
|
local id = WA.GetAnchorAuraID(data)
|
|
if id then return WA.PeekRegion(id, "") end
|
|
local ref = data.anchorFrameFrame
|
|
if not ref or ref == "" then return nil end
|
|
return getglobal(ref)
|
|
end
|
|
|
|
local function unitForRegion(region)
|
|
local state = region and region.state
|
|
if not state then return nil, nil end
|
|
return state.unit or state.unitId, state.guid or state.guidUnit
|
|
end
|
|
|
|
local function looksLikeGuid(value)
|
|
return type(value) == "string" and string.find(value, "^0[xX]%x+$") ~= nil
|
|
end
|
|
|
|
-- A GUID standing in for a unit token is the ordinary shape here, not an edge
|
|
-- case: ClassicAPI makes every unit-taking function accept one, and a trigger
|
|
-- following units it has no token for -- anything keyed off a combat event --
|
|
-- has nothing else to store.
|
|
--
|
|
-- C_NamePlate does not follow suit, and fails in the one way that hides itself.
|
|
-- ClassicAPI's GetNamePlateForUnit hands the string straight to the engine's own
|
|
-- 1.12 token resolver (src/nameplate/Info.cpp, FUN_TOKEN_TO_GUID), which knows
|
|
-- nothing about GUIDs and answers with the *current target* rather than with
|
|
-- nothing -- so the unit path returns a real frame, the GUID path below is never
|
|
-- reached, and every plate-anchored clone silently stacks on the target's plate.
|
|
-- A GUID therefore has to bypass the unit path outright: falling back to it on
|
|
-- a nil result would not help, because it does not return nil.
|
|
nameplateFor = function(unit, guid)
|
|
if not guid and looksLikeGuid(unit) then guid = unit end
|
|
if unit and not looksLikeGuid(unit) then
|
|
local frame = C_NamePlate.GetNamePlateForUnit(unit)
|
|
if frame then return frame end
|
|
end
|
|
if not guid and unit then guid = UnitGUID(unit) end
|
|
if guid then return C_NamePlate.GetNamePlateForGUID(guid) end
|
|
return nil
|
|
end
|
|
|
|
-- The plate showing `unit`, for anything anchoring to one -- a display's own
|
|
-- NAMEPLATE anchor below, and a dynamic group anchoring its clones per unit.
|
|
function WA.GetUnitNameplate(unit, guid) return nameplateFor(unit, guid) end
|
|
|
|
-- Where a region goes when its anchor target is not there: a real frame, shown
|
|
-- to nothing, so the region keeps a valid anchor instead of an error.
|
|
function WA.HiddenAnchorFrame() return hiddenAnchorFrame() end
|
|
|
|
function WA.GetUnitFrame(unit)
|
|
if not unit then return nil end
|
|
local pf = _G and _G.pfUI
|
|
local pfuf = pf and pf.uf
|
|
local probes = {
|
|
player = function() return pfuf and pfuf.player or getglobal("PlayerFrame") end,
|
|
target = function() return pfuf and pfuf.target or getglobal("TargetFrame") end,
|
|
targettarget = function() return pfuf and pfuf.targettarget or getglobal("TargetTargetFrame") end,
|
|
focus = function() return pfuf and pfuf.focus or getglobal("FocusFrame") end,
|
|
focustarget = function() return pfuf and pfuf.focustarget or getglobal("FocusTargetFrame") end,
|
|
pet = function() return pfuf and pfuf.pet or getglobal("PetFrame") end,
|
|
pettarget = function() return pfuf and pfuf.pettarget or getglobal("PetTargetFrame") end,
|
|
}
|
|
local probe = probes[unit]
|
|
if probe then return probe() end
|
|
local _, _, n = string.find(unit, "^party(%d+)$")
|
|
if n then return pfuf and pfuf.group and pfuf.group[tonumber(n)]
|
|
or getglobal("PartyMemberFrame" .. tostring(n)) end
|
|
local _, _, raid = string.find(unit, "^raid(%d+)$")
|
|
if raid and pfuf and pfuf.raid then return pfuf.raid[tonumber(raid)] end
|
|
local _, _, nameplate = string.find(unit, "^nameplate(%d+)$")
|
|
if nameplate then return nameplateFor(unit) end
|
|
return nil
|
|
end
|
|
|
|
local function FrameStrataField(data)
|
|
return {
|
|
type = "select", name = "Frame strata", key = "frameStrata",
|
|
values = FRAME_STRATA_VALUES,
|
|
labels = FRAME_STRATA_NAMES,
|
|
get = function() return data.frameStrata or 1 end,
|
|
set = function(v) data.frameStrata = v; WA.Add(data, true) end,
|
|
}
|
|
end
|
|
|
|
-- Applies a region's saved strata (1 = inherit from parent, 2..9 = a real
|
|
-- FRAME_STRATA_NAMES entry). ApplyPosition calls this on every exit rather than
|
|
-- leaving it to the caller: its own SetParent resets the frame's strata, so a
|
|
-- re-anchor that forgot to follow up left the region on whatever the frame it
|
|
-- just joined happens to use. Four of eleven call sites had forgotten.
|
|
function proto.ApplyFrameStrata(region, data)
|
|
local strata = data.frameStrata
|
|
if strata and strata ~= 1 then
|
|
region:SetFrameStrata(FRAME_STRATA_NAMES[strata])
|
|
return
|
|
end
|
|
-- "Inherit" cannot inherit from a frame that has no strata to give. A vanilla
|
|
-- nameplate is a WorldFrame child and answers "UNKNOWN", which is not a name
|
|
-- SetFrameStrata takes -- a region anchored to one has to be put back into the
|
|
-- UI's strata system explicitly or it is outside the draw order entirely.
|
|
local parent = region:GetParent() or UIParent
|
|
local inherited = parent.GetFrameStrata and parent:GetFrameStrata()
|
|
region:SetFrameStrata(REAL_STRATA[inherited] and inherited or "MEDIUM")
|
|
end
|
|
|
|
-- Resolves a display's anchor frame and applies its saved anchor tuple. The one
|
|
-- place anchor-frame selection lives (§16). An unresolved target parks the
|
|
-- region on a hidden singleton and schedules one shared retry.
|
|
-- * Grouped child (data.parent is a group): SetParent + anchor to the group
|
|
-- frame's CENTER, so the group's scale cascades and moving/dragging the
|
|
-- group carries its children. xOffset/yOffset are measured from the group's
|
|
-- anchor and data.anchorPoint is ignored -- the group case is always
|
|
-- center-relative (WA2's Group.lua getRect reads xOffset/yOffset as such) --
|
|
-- with GroupChildOffset converting them into the frame's own coordinates.
|
|
-- * Top-level (SCREEN): anchor selfPoint to UIParent at data.anchorPoint.
|
|
-- Ensuring the group frame here (WA.GetRegion) means a child applied before its
|
|
-- group still resolves -- the group is created on demand.
|
|
function proto.ResolveAnchor(region, data)
|
|
local frameType = data.anchorFrameType or "SCREEN"
|
|
local parentId = data.parent
|
|
local pdata = parentId and WeakestAurasDB.displays[parentId]
|
|
if frameType == "SCREEN" and pdata and WA.IsGroup(pdata) then
|
|
local groupFrame = WA.GetRegion(parentId, "")
|
|
if groupFrame then return groupFrame, "CENTER", true end
|
|
end
|
|
if frameType == "SCREEN" or frameType == "UIPARENT" then
|
|
return UIParent, data.anchorPoint or "CENTER", true
|
|
elseif frameType == "SELECTFRAME" then
|
|
local frame = anchorReference(data)
|
|
if frame then return frame, data.anchorPoint or "CENTER", data.anchorFrameParent ~= false end
|
|
queueAnchorRetry(data)
|
|
return hiddenAnchorFrame(), data.anchorPoint or "CENTER", true
|
|
elseif frameType == "MOUSE" then
|
|
local frame = createMouseAnchorFrame()
|
|
if WA.optionsOpen then
|
|
positionMouseAnchor()
|
|
if mouseAnchorMarker then mouseAnchorMarker:Show() end
|
|
return frame, "CENTER", true
|
|
end
|
|
return frame, "CENTER", false
|
|
elseif frameType == "NAMEPLATE" then
|
|
if WA.optionsOpen then
|
|
local frame = createOptionsNameplateAnchorFrame()
|
|
positionNameplateAnchor()
|
|
frame:Show()
|
|
if nameplateAnchorMarker then nameplateAnchorMarker:Show() end
|
|
return frame, data.anchorPoint or "CENTER", true
|
|
end
|
|
local unit, guid = unitForRegion(region)
|
|
local frame = nameplateFor(unit, guid)
|
|
if frame then return frame, data.anchorPoint or "CENTER", data.anchorFrameParent ~= false end
|
|
queueAnchorRetry(data)
|
|
return hiddenAnchorFrame(), data.anchorPoint or "CENTER", true
|
|
elseif frameType == "UNITFRAME" then
|
|
local unit = unitForRegion(region)
|
|
local frame = WA.GetUnitFrame(unit)
|
|
if frame then return frame, data.anchorPoint or "CENTER", data.anchorFrameParent ~= false end
|
|
queueAnchorRetry(data)
|
|
if WA.optionsOpen then return UIParent, data.anchorPoint or "CENTER", true end
|
|
return hiddenAnchorFrame(), data.anchorPoint or "CENTER", true
|
|
elseif frameType == "CUSTOM" and data.customAnchor then
|
|
local fn = region.customAnchorFunc
|
|
if not fn then
|
|
fn = WA.LoadFunction(data.customAnchor, tostring(data.id) .. ": custom anchor")
|
|
region.customAnchorFunc = fn
|
|
end
|
|
if fn then
|
|
local ok, frame = WA.RunAuraFunc(data.id, data.id .. ": custom anchor", fn)
|
|
if ok and frame then return frame, data.anchorPoint or "CENTER", data.anchorFrameParent ~= false end
|
|
end
|
|
return hiddenAnchorFrame(), data.anchorPoint or "CENTER", true
|
|
end
|
|
return UIParent, data.anchorPoint or "CENTER", data.anchorFrameParent ~= false
|
|
end
|
|
|
|
function proto.ReanchorDynamic(frameType)
|
|
if not WA.ForEachRegion then return end
|
|
WA.ForEachRegion(function(region)
|
|
local data = WeakestAurasDB.displays[region.id]
|
|
if data and (not frameType or data.anchorFrameType == frameType) then
|
|
if data.anchorFrameType == "MOUSE" or data.anchorFrameType == "NAMEPLATE"
|
|
or data.anchorFrameType == "UNITFRAME" then
|
|
proto.ApplyPosition(region, data)
|
|
end
|
|
end
|
|
end)
|
|
proto.SyncOptionsAnchorMarkers()
|
|
end
|
|
|
|
function proto.SyncOptionsAnchorMarkers()
|
|
if not WA.optionsOpen then
|
|
if mouseAnchorMarker then mouseAnchorMarker:Hide() end
|
|
if nameplateAnchorMarker then nameplateAnchorMarker:Hide() end
|
|
if optionsNameplateAnchorFrame then optionsNameplateAnchorFrame:Hide() end
|
|
return
|
|
end
|
|
local mouseVisible, nameplateVisible = false, false
|
|
if WA.ForEachRegion then
|
|
WA.ForEachRegion(function(region)
|
|
local data = WeakestAurasDB.displays[region.id]
|
|
if data and region.IsShown and region:IsShown() then
|
|
if data.anchorFrameType == "MOUSE" then mouseVisible = true end
|
|
if data.anchorFrameType == "NAMEPLATE" then nameplateVisible = true end
|
|
end
|
|
end)
|
|
end
|
|
if mouseAnchorMarker then
|
|
if mouseVisible then mouseAnchorMarker:Show() else mouseAnchorMarker:Hide() end
|
|
end
|
|
if optionsNameplateAnchorFrame then
|
|
if nameplateVisible then optionsNameplateAnchorFrame:Show() else optionsNameplateAnchorFrame:Hide() end
|
|
end
|
|
if nameplateAnchorMarker then
|
|
if nameplateVisible then nameplateAnchorMarker:Show() else nameplateAnchorMarker:Hide() end
|
|
end
|
|
end
|
|
|
|
function proto.SetOptionsAnchors(open)
|
|
if open then
|
|
createMouseAnchorFrame()
|
|
createOptionsNameplateAnchorFrame()
|
|
positionMouseAnchor()
|
|
positionNameplateAnchor()
|
|
optionsNameplateAnchorFrame:Hide()
|
|
if mouseAnchorMarker then mouseAnchorMarker:Hide() end
|
|
proto.ReanchorDynamic("MOUSE")
|
|
proto.ReanchorDynamic("NAMEPLATE")
|
|
proto.SyncOptionsAnchorMarkers()
|
|
else
|
|
if mouseAnchorFrame then
|
|
mouseAnchorFrame:SetScript("OnUpdate", function()
|
|
moveMouseAnchor()
|
|
end)
|
|
end
|
|
if optionsNameplateAnchorFrame then optionsNameplateAnchorFrame:Hide() end
|
|
if mouseAnchorMarker then mouseAnchorMarker:Hide() end
|
|
if nameplateAnchorMarker then nameplateAnchorMarker:Hide() end
|
|
proto.ReanchorDynamic("MOUSE")
|
|
proto.ReanchorDynamic("NAMEPLATE")
|
|
end
|
|
end
|
|
|
|
dynamicAnchorWatcher = CreateFrame("Frame")
|
|
dynamicAnchorWatcher:RegisterEvent("NAME_PLATE_UNIT_ADDED")
|
|
dynamicAnchorWatcher:RegisterEvent("NAME_PLATE_UNIT_REMOVED")
|
|
dynamicAnchorWatcher:RegisterEvent("PLAYER_TARGET_CHANGED")
|
|
dynamicAnchorWatcher:RegisterEvent("PLAYER_FOCUS_CHANGED")
|
|
dynamicAnchorWatcher:RegisterEvent("PARTY_MEMBERS_CHANGED")
|
|
dynamicAnchorWatcher:RegisterEvent("PLAYER_ENTERING_WORLD")
|
|
dynamicAnchorWatcher:SetScript("OnEvent", function()
|
|
if event == "NAME_PLATE_UNIT_ADDED" or event == "NAME_PLATE_UNIT_REMOVED" then
|
|
proto.ReanchorDynamic("NAMEPLATE")
|
|
else
|
|
proto.ReanchorDynamic("UNITFRAME")
|
|
end
|
|
-- A dynamic group's children anchor per unit through the group's layout, not
|
|
-- through their own anchorFrameType, so ReanchorDynamic never reaches them.
|
|
if WA.RelayoutUnitAnchoredGroups then WA.RelayoutUnitAnchoredGroups() end
|
|
end)
|
|
|
|
-- SetParent does not redraw a region that was already shown when it moved: it
|
|
-- keeps IsShown() true, IsVisible() false, and the engine never paints it
|
|
-- (design/client/gotchas.md). That is why a plate-anchored aura stayed dark
|
|
-- until the target was deselected and reselected, which does by hand exactly
|
|
-- what this does -- Hide/Show forces the flag down the chain. A region still not
|
|
-- drawn asks for another pass, so the retry runs until the aura is really on
|
|
-- screen rather than stopping the moment the anchor resolved. Raw frame methods,
|
|
-- not setShown -- no sub-region teardown, no FrameTick churn -- and bounded to
|
|
-- the two anchor types whose frame can come and go under them.
|
|
local function settleDynamicAnchor(region, data)
|
|
local frameType = data.anchorFrameType
|
|
if frameType ~= "NAMEPLATE" and frameType ~= "UNITFRAME" then return end
|
|
if not region.shown or not region.IsVisible or region:IsVisible() then return end
|
|
local parent = region:GetParent()
|
|
if parent and parent.IsVisible and parent:IsVisible() then
|
|
region:Hide()
|
|
region:Show()
|
|
end
|
|
if not region:IsVisible() then queueAnchorRetry(data) end
|
|
end
|
|
|
|
-- Whether a group's child is positioned *by* that group, which is the same
|
|
-- question as whether it belongs in the group's bounding box. A child anchored
|
|
-- to anything but the screen -- a nameplate, a unit frame, another aura --
|
|
-- measures xOffset/yOffset from that frame instead, so it occupies no known
|
|
-- point in the group's coordinate space.
|
|
--
|
|
-- Three places have to agree on this or they corrupt each other: ApplyPosition
|
|
-- (which frame the child anchors to), the box union, and the post-fit offset
|
|
-- pass that hands every child GroupChildOffset. Reading a foreign-anchored
|
|
-- child's offsets as group-relative both stretches the box to a corner nothing
|
|
-- occupies and then subtracts that box's centre out of the child's real offset,
|
|
-- so the group mis-sizes itself *and* drags the child off its own anchor.
|
|
function proto.IsGroupAnchored(cdata)
|
|
return cdata ~= nil and cdata.anchorFrameType == "SCREEN"
|
|
end
|
|
|
|
-- A static group's child measures xOffset/yOffset from the group's anchor, but
|
|
-- the frame it anchors to is centred on the children's bounding box instead
|
|
-- (UpdatePosition's box slot). The difference is what the child owes back, and
|
|
-- both the reanchor here and the group's own relayout hand it the same numbers.
|
|
function proto.GroupChildOffset(groupFrame, cdata)
|
|
local cx = groupFrame and groupFrame.xOffsetBox or 0
|
|
local cy = groupFrame and groupFrame.yOffsetBox or 0
|
|
return (cdata.xOffset or 0) - cx, (cdata.yOffset or 0) - cy
|
|
end
|
|
|
|
function proto.ApplyPosition(region, data)
|
|
local anchorFrame, anchorPoint, setParent = proto.ResolveAnchor(region, data)
|
|
local parentId = data.parent
|
|
local pdata = parentId and WeakestAurasDB.displays[parentId]
|
|
if setParent then region:SetParent(anchorFrame) else region:SetParent(UIParent) end
|
|
-- Immediately after the reparent, which is where the frame's level is the
|
|
-- client's own again; modifyFinish raises it from here if the aura carries a
|
|
-- subbackground row.
|
|
proto.ResetFrameLevel(region)
|
|
if pdata and WA.IsGroup(pdata) and proto.IsGroupAnchored(data) then
|
|
if pdata.regionType == "dynamicgroup" then
|
|
region:SetAnchor("CENTER", anchorFrame, "CENTER")
|
|
region:SetOffset(0, 0)
|
|
else
|
|
region:SetAnchor(data.selfPoint or "CENTER", anchorFrame, anchorPoint)
|
|
region:SetOffset(proto.GroupChildOffset(anchorFrame, data))
|
|
end
|
|
else
|
|
if data.anchorFrameType == "MOUSE" then anchorPoint = "CENTER" end
|
|
region:SetAnchor(data.selfPoint or "CENTER", anchorFrame, anchorPoint)
|
|
region:SetOffset(data.xOffset or 0, data.yOffset or 0)
|
|
if WA.optionsOpen then proto.SyncOptionsAnchorMarkers() end
|
|
end
|
|
proto.ApplyFrameStrata(region, data)
|
|
settleDynamicAnchor(region, data)
|
|
end
|
|
|
|
-- The Display-tab Position section, shared by every non-group region type so a
|
|
-- new region kind gets drag-compatible positioning for free. Appended to a
|
|
-- type's options() field array (Regions.lua). Position edits are pure-visual,
|
|
-- so they ride the WA.Add(data, true) fast-path like the size sliders.
|
|
function proto.PositionOptions(data)
|
|
-- A dynamicgroup arranges its children automatically, so manual position is
|
|
-- meaningless for them -- show a note instead of dead sliders.
|
|
-- A dynamic group's child still has its own strata even though the group
|
|
-- owns its position (matches upstream: frameStrata carries no
|
|
-- IsParentDynamicGroup hidden check in CommonOptions.lua).
|
|
local pdata = data.parent and WeakestAurasDB.displays[data.parent]
|
|
if pdata and pdata.regionType == "dynamicgroup" then
|
|
return {
|
|
{ type = "header", name = "Position (set by dynamic group)" },
|
|
FrameStrataField(data),
|
|
}
|
|
end
|
|
local fields = {
|
|
{ type = "header", name = "Position" },
|
|
{
|
|
type = "select", name = "Anchored to", key = "anchorFrameType",
|
|
values = ANCHOR_FRAME_VALUES, labels = ANCHOR_FRAME_LABELS,
|
|
get = function() return data.anchorFrameType or "SCREEN" end,
|
|
set = function(v)
|
|
data.anchorFrameType = v
|
|
WA.Add(data, true)
|
|
WA.RefreshOptions()
|
|
end,
|
|
},
|
|
{
|
|
type = "anchorgrid", name = (data.anchorFrameType == "SELECTFRAME" and "To frame point")
|
|
or (data.anchorFrameType == "UIPARENT" and "To UIParent point")
|
|
or (data.anchorFrameType == "MOUSE" and "To mouse point")
|
|
or (data.anchorFrameType == "NAMEPLATE" and "To nameplate point")
|
|
or (data.anchorFrameType == "UNITFRAME" and "To unit frame point")
|
|
or "To screen point", key = "anchorPoint", half = true,
|
|
width = 100, height = 50,
|
|
values = proto.anchorGridPoints,
|
|
get = function() return data.anchorPoint end,
|
|
set = function(v) data.anchorPoint = v; WA.Add(data, true) end,
|
|
},
|
|
{
|
|
type = "range", name = "X", key = "xOffset", min = -400, max = 400, step = 1, half = true,
|
|
get = function() return data.xOffset end,
|
|
set = function(v) data.xOffset = v; WA.Add(data, true) end,
|
|
},
|
|
{
|
|
type = "range", name = "Y", key = "yOffset", min = -400, max = 400, step = 1, half = true,
|
|
get = function() return data.yOffset end,
|
|
set = function(v) data.yOffset = v; WA.Add(data, true) end,
|
|
},
|
|
FrameStrataField(data),
|
|
}
|
|
-- A group gets no self-anchor grid: its frame is sized to its children's
|
|
-- bounding box and pinned by the centre, so any other point would displace the
|
|
-- whole pack by half that box and drift it as children come and go.
|
|
-- groupModify writes the field back to CENTER, and WA2Import reports an
|
|
-- imported one as a drop rather than carrying it.
|
|
if not WA.IsGroup(data) then
|
|
table.insert(fields, 3, {
|
|
type = "anchorgrid", name = "Anchor", key = "selfPoint", half = true,
|
|
width = 100, height = 50,
|
|
values = proto.anchorGridPoints,
|
|
get = function() return data.selfPoint end,
|
|
set = function(v) data.selfPoint = v; WA.Add(data, true) end,
|
|
})
|
|
end
|
|
if data.anchorFrameType ~= "SCREEN" and data.anchorFrameType ~= "UIPARENT" then
|
|
table.insert(fields, 3, {
|
|
type = "toggle", name = "Set parent to anchor", key = "anchorFrameParent",
|
|
get = function() return data.anchorFrameParent ~= false end,
|
|
set = function(v) data.anchorFrameParent = v; WA.Add(data, true) end,
|
|
})
|
|
end
|
|
if data.anchorFrameType == "SELECTFRAME" then
|
|
table.insert(fields, 4, {
|
|
type = "input", name = "Frame or aura ID", key = "anchorFrameFrame",
|
|
get = function() return data.anchorFrameFrame end,
|
|
set = function(v) data.anchorFrameFrame = v; WA.Add(data, true) end,
|
|
})
|
|
table.insert(fields, 5, {
|
|
type = "button", name = "Choose frame", width = 140,
|
|
onClick = function()
|
|
WA.StartFrameChooser(data, function(value)
|
|
data.anchorFrameFrame = value
|
|
WA.Add(data, true)
|
|
WA.RefreshOptions()
|
|
end)
|
|
end,
|
|
})
|
|
end
|
|
return fields
|
|
end
|
|
|
|
-- Re-applies a region's progress source and adjusted min/max from data at
|
|
-- modify time. Passing "" for a disabled bound (rather than its last-set
|
|
-- string) keeps a toggled-off bound from leaking a stale clamp. The manual
|
|
-- value/total are plain fields, not setters -- nothing conditions on them
|
|
-- individually, so there's no repaint to drive.
|
|
function proto.ApplyProgressConfig(region, data)
|
|
region.progressSourceManualValue = data.progressSourceManualValue
|
|
region.progressSourceManualTotal = data.progressSourceManualTotal
|
|
region:SetProgressSource(data.progressSource or -1)
|
|
region:SetAdjustedMin(data.useAdjustededMin and data.adjustedMin or "")
|
|
region:SetAdjustedMax(data.useAdjustededMax and data.adjustedMax or "")
|
|
end
|
|
|
|
-- The Progress Settings section, shared by icon and progressbar (both drive
|
|
-- their fill/swipe through proto.UpdateProgress). Appended to a type's
|
|
-- options() field array (Regions.lua). Pure-visual, so it rides the
|
|
-- WA.Add(data, true) fast-path like Position/Size.
|
|
function proto.ProgressOptions(data)
|
|
local sourceValues = { -1 }
|
|
local sourceLabels = { [-1] = "Automatic (active trigger)" }
|
|
local triggerCount = data.triggers and #data.triggers or 0
|
|
for i = 1, triggerCount do
|
|
table.insert(sourceValues, i)
|
|
sourceLabels[i] = "Trigger " .. i
|
|
end
|
|
table.insert(sourceValues, 0)
|
|
sourceLabels[0] = "Manual"
|
|
|
|
local fields = {
|
|
{ type = "header", name = "Progress" },
|
|
{
|
|
type = "select", name = "Progress source", key = "progressSource",
|
|
values = sourceValues,
|
|
labels = sourceLabels,
|
|
get = function() return data.progressSource or -1 end,
|
|
set = function(v)
|
|
data.progressSource = v
|
|
WA.Add(data, true)
|
|
-- The manual value/total fields appear and disappear with this pick.
|
|
WA.RefreshOptions()
|
|
end,
|
|
},
|
|
}
|
|
if data.progressSource == 0 then
|
|
table.insert(fields, {
|
|
type = "input", name = "Value", key = "progressSourceManualValue",
|
|
get = function() return data.progressSourceManualValue end,
|
|
set = function(v)
|
|
data.progressSourceManualValue = tonumber(v) or data.progressSourceManualValue
|
|
WA.Add(data, true)
|
|
end,
|
|
})
|
|
table.insert(fields, {
|
|
type = "input", name = "Total", key = "progressSourceManualTotal",
|
|
get = function() return data.progressSourceManualTotal end,
|
|
set = function(v)
|
|
data.progressSourceManualTotal = tonumber(v) or data.progressSourceManualTotal
|
|
WA.Add(data, true)
|
|
end,
|
|
})
|
|
end
|
|
table.insert(fields, {
|
|
type = "toggle", name = "Set minimum progress", key = "useAdjustededMin",
|
|
get = function() return data.useAdjustededMin end,
|
|
set = function(v)
|
|
data.useAdjustededMin = v
|
|
if not v then data.adjustedMin = "" end
|
|
WA.Add(data, true)
|
|
WA.RefreshOptions()
|
|
end,
|
|
})
|
|
if data.useAdjustededMin then
|
|
table.insert(fields, {
|
|
type = "input", name = "Minimum", key = "adjustedMin",
|
|
get = function() return data.adjustedMin end,
|
|
set = function(v) data.adjustedMin = v; WA.Add(data, true) end,
|
|
})
|
|
end
|
|
table.insert(fields, {
|
|
type = "toggle", name = "Set maximum progress", key = "useAdjustededMax",
|
|
get = function() return data.useAdjustededMax end,
|
|
set = function(v)
|
|
data.useAdjustededMax = v
|
|
if not v then data.adjustedMax = "" end
|
|
WA.Add(data, true)
|
|
WA.RefreshOptions()
|
|
end,
|
|
})
|
|
if data.useAdjustededMax then
|
|
table.insert(fields, {
|
|
type = "input", name = "Maximum", key = "adjustedMax",
|
|
get = function() return data.adjustedMax end,
|
|
set = function(v) data.adjustedMax = v; WA.Add(data, true) end,
|
|
})
|
|
end
|
|
return fields
|
|
end
|