if not WeakAuras.IsCorrectVersion() then return end local AddonName, Private = ... local WeakAuras = WeakAuras; local L = WeakAuras.L; WeakAuras.regionPrototype = {}; local SubRegionEventSystem = { ClearSubscribers = function(self) self.events = {} end, AddSubscriber = function(self, event, subRegion) if not subRegion[event] then print("Can't register subregion for ", event, " ", subRegion.type) return end self.events[event] = self.events[event] or {} tinsert(self.events[event], subRegion) end, RemoveSubscriber = function(self, event, subRegion) tremove(self.events[event], tIndexOf(self.events[event], subRegion)) end, Notify = function(self, event, ...) if self.events[event] then for _, subRegion in ipairs(self.events[event]) do subRegion[event](subRegion, ...) end end end } local function CreateSubRegionEventSystem() local system = {} for f, func in pairs(SubRegionEventSystem) do system[f] = func system.events = {} end return system end -- Alpha function WeakAuras.regionPrototype.AddAlphaToDefault(default) default.alpha = 1.0; end -- Adjusted Duration function WeakAuras.regionPrototype.AddAdjustedDurationToDefault(default) default.useAdjustededMax = false; default.useAdjustededMin = false; end function WeakAuras.regionPrototype.AddAdjustedDurationOptions(options, data, order) options.useAdjustededMin = { type = "toggle", width = WeakAuras.normalWidth, name = L["Set Minimum Progress"], desc = L["Values/Remaining Time below this value are displayed as no progress."], order = order }; options.adjustedMin = { type = "input", validate = WeakAuras.ValidateNumericOrPercent, width = WeakAuras.normalWidth, order = order + 0.01, name = L["Minimum"], hidden = function() return not data.useAdjustededMin end, desc = L["Enter static or relative values with %"] }; options.useAdjustedMinSpacer = { type = "description", width = WeakAuras.normalWidth, name = "", order = order + 0.02, hidden = function() return not (not data.useAdjustededMin and data.useAdjustededMax) end, }; options.useAdjustededMax = { type = "toggle", width = WeakAuras.normalWidth, name = L["Set Maximum Progress"], desc = L["Values/Remaining Time above this value are displayed as full progress."], order = order + 0.03 }; options.adjustedMax = { type = "input", width = WeakAuras.normalWidth, validate = WeakAuras.ValidateNumericOrPercent, order = order + 0.04, name = L["Maximum"], hidden = function() return not data.useAdjustededMax end, desc = L["Enter static or relative values with %"] }; options.useAdjustedMaxSpacer = { type = "description", width = WeakAuras.normalWidth, name = "", order = order + 0.05, hidden = function() return not (data.useAdjustededMin and not data.useAdjustededMax) end, }; return options; end local screenWidth, screenHeight = math.ceil(GetScreenWidth() / 20) * 20, math.ceil(GetScreenHeight() / 20) * 20; function Private.GetAnchorsForData(parentData, type) local result if not parentData.controlledChildren then if not WeakAuras.regionOptions[parentData.regionType] or not WeakAuras.regionOptions[parentData.regionType].getAnchors then return end local anchors = WeakAuras.regionOptions[parentData.regionType].getAnchors(parentData) for anchorId, anchorData in pairs(anchors) do if anchorData.type == type then result = result or {} result[anchorId] = anchorData.display end end end return result end function WeakAuras.regionPrototype:AnchorSubRegion(subRegion, anchorType, selfPoint, anchorPoint, anchorXOffset, anchorYOffset) subRegion:ClearAllPoints() if anchorType == "point" then local xOffset = anchorXOffset or 0 local yOffset = anchorYOffset or 0 subRegion:SetPoint(Private.point_types[selfPoint] and selfPoint or "CENTER", self, Private.point_types[anchorPoint] and anchorPoint or "CENTER", xOffset, yOffset) else anchorXOffset = anchorXOffset or 0 anchorYOffset = anchorYOffset or 0 subRegion:SetPoint("bottomleft", self, "bottomleft", -anchorXOffset, -anchorYOffset) subRegion:SetPoint("topright", self, "topright", anchorXOffset, anchorYOffset) end end -- Sound / Chat Message / Custom Code function WeakAuras.regionPrototype.AddProperties(properties, defaultsForRegion) properties["sound"] = { display = L["Sound"], action = "SoundPlay", type = "sound", }; properties["chat"] = { display = L["Chat Message"], action = "SendChat", type = "chat", }; properties["customcode"] = { display = L["Run Custom Code"], action = "RunCode", type = "customcode" } properties["xOffsetRelative"] = { display = L["Relative X-Offset"], setter = "SetXOffsetRelative", type = "number", softMin = -screenWidth, softMax = screenWidth, bigStep = 1 } properties["yOffsetRelative"] = { display = L["Relative Y-Offset"], setter = "SetYOffsetRelative", type = "number", softMin = -screenHeight, softMax = screenHeight, bigStep = 1 } properties["glowexternal"] = { display = L["Glow External Element"], action = "GlowExternal", type = "glowexternal" } if (defaultsForRegion and defaultsForRegion.alpha) then properties["alpha"] = { display = L["Alpha"], setter = "SetRegionAlpha", type = "number", min = 0, max = 1, bigStep = 0.01, isPercent = true } end end local function SoundRepeatStop(self) Private.StartProfileSystem("sound"); if (self.soundRepeatTimer) then WeakAuras.timer:CancelTimer(self.soundRepeatTimer); self.soundRepeatTimer = nil; end Private.StopProfileSystem("sound"); end local function SoundPlayHelper(self) Private.StartProfileSystem("sound"); local options = self.soundOptions; if (not options or options.sound_type == "Stop") then Private.StopProfileSystem("sound"); return; end if (WeakAuras.IsOptionsOpen() or Private.SquelchingActions()) then Private.StopProfileSystem("sound"); return; end if (options.sound == " custom") then if (options.sound_path) then pcall(PlaySoundFile, options.sound_path, options.sound_channel or "Master"); end elseif (options.sound == " KitID") then if (options.sound_kit_id) then pcall(PlaySound, options.sound_kit_id, options.sound_channel or "Master"); end else pcall(PlaySoundFile, options.sound, options.sound_channel or "Master"); end Private.StopProfileSystem("sound"); end local function SoundPlay(self, options) if (not options or WeakAuras.IsOptionsOpen()) then return end Private.StartProfileSystem("sound"); self:SoundRepeatStop(); self.soundOptions = options; SoundPlayHelper(self); local loop = options.do_loop or options.sound_type == "Loop"; if (loop and options.sound_repeat) then self.soundRepeatTimer = WeakAuras.timer:ScheduleRepeatingTimer(SoundPlayHelper, options.sound_repeat, self); end Private.StopProfileSystem("sound"); end local function SendChat(self, options) if (not options or WeakAuras.IsOptionsOpen()) then return end Private.HandleChatAction(options.message_type, options.message, options.message_dest, options.message_channel, options.r, options.g, options.b, self, options.message_custom, nil, options.message_formaters); end local function RunCode(self, func) if func and not WeakAuras.IsOptionsOpen() then Private.ActivateAuraEnvironment(self.id, self.cloneId, self.state, self.states); xpcall(func, geterrorhandler()); Private.ActivateAuraEnvironment(nil); end end local function GlowExternal(self, options) if (not options or WeakAuras.IsOptionsOpen()) then return end Private.HandleGlowAction(options, self) end local function UpdatePosition(self) if (not self.anchorPoint or not self.relativeTo or not self.relativePoint) then return; end local xOffset = self.xOffset + (self.xOffsetAnim or 0) + (self.xOffsetRelative or 0) local yOffset = self.yOffset + (self.yOffsetAnim or 0) + (self.yOffsetRelative or 0) self:RealClearAllPoints(); local ok, ret = pcall(self.SetPoint, self, self.anchorPoint, self.relativeTo, self.relativePoint, xOffset, yOffset); if not ok then geterrorhandler()(ret) end end local function ResetPosition(self) self.anchorPoint = nil; self.relativeTo = nil; self.relativePoint = nil; end local function SetAnchor(self, anchorPoint, relativeTo, relativePoint) if self.anchorPoint == anchorPoint and self.relativeTo == relativeTo and self.relativePoint == relativePoint then return end self.anchorPoint = anchorPoint; self.relativeTo = relativeTo; self.relativePoint = relativePoint; UpdatePosition(self); end local function SetOffset(self, xOffset, yOffset) if (self.xOffset == xOffset and self.yOffset == yOffset) then return; end self.xOffset = xOffset; self.yOffset = yOffset; UpdatePosition(self); end local function SetXOffset(self, xOffset) self:SetOffset(xOffset, self:GetYOffset()); end local function SetYOffset(self, yOffset) self:SetOffset(self:GetXOffset(), yOffset); end local function GetXOffset(self) return self.xOffset; end local function GetYOffset(self) return self.yOffset; end local function SetOffsetRelative(self, xOffsetRelative, yOffsetRelative) if (self.xOffsetRelative == xOffsetRelative and self.yOffsetRelative == yOffsetRelative) then return end self.xOffsetRelative = xOffsetRelative self.yOffsetRelative = yOffsetRelative UpdatePosition(self) end local function SetXOffsetRelative(self, xOffsetRelative) self:SetOffsetRelative(xOffsetRelative, self:GetYOffsetRelative()) end local function SetYOffsetRelative(self, yOffsetRelative) self:SetOffsetRelative(self:GetXOffsetRelative(), yOffsetRelative) end local function GetXOffsetRelative(self) return self.xOffsetRelative end local function GetYOffsetRelative(self) return self.yOffsetRelative end local function SetOffsetAnim(self, xOffset, yOffset) if (self.xOffsetAnim == xOffset and self.yOffsetAnim == yOffset) then return; end self.xOffsetAnim = xOffset; self.yOffsetAnim = yOffset; UpdatePosition(self); end local function SetRegionAlpha(self, alpha) if (self.alpha == alpha) then return; end self.alpha = alpha; self:SetAlpha(self.animAlpha or self.alpha or 1); self.subRegionEvents:Notify("AlphaChanged") end local function GetRegionAlpha(self) return self.animAlpha or self.alpha or 1; end local function SetAnimAlpha(self, alpha) if (self.animAlpha == alpha) then return; end self.animAlpha = alpha; if (WeakAuras.IsOptionsOpen()) then self:SetAlpha(max(self.animAlpha or self.alpha or 1, 0.5)); else self:SetAlpha(self.animAlpha or self.alpha or 1); end self.subRegionEvents:Notify("AlphaChanged") end local function SetTriggerProvidesTimer(self, timerTick) self.triggerProvidesTimer = timerTick self:UpdateTimerTick() end local function UpdateRegionHasTimerTick(self) local hasTimerTick = false if self.TimerTick then hasTimerTick = true elseif (self.subRegions) then for index, subRegion in pairs(self.subRegions) do if subRegion.TimerTick then hasTimerTick = true break; end end end self.regionHasTimer = hasTimerTick self:UpdateTimerTick() end local function TimerTickForRegion(region) Private.StartProfileSystem("timer tick") Private.StartProfileAura(region.id); if region.TimerTick then region:TimerTick(); end region.subRegionEvents:Notify("TimerTick") Private.StopProfileAura(region.id); Private.StopProfileSystem("timer tick") end local function UpdateTimerTick(self) if self.triggerProvidesTimer and self.regionHasTimer then if not self:GetScript("OnUpdate") then self:SetScript("OnUpdate", function() TimerTickForRegion(self) end); end else if self:GetScript("OnUpdate") then self:SetScript("OnUpdate", nil); end end end function WeakAuras.regionPrototype.create(region) region.SoundPlay = SoundPlay; region.SoundRepeatStop = SoundRepeatStop; region.SendChat = SendChat; region.RunCode = RunCode; region.GlowExternal = GlowExternal; region.SetAnchor = SetAnchor; region.SetOffset = SetOffset; region.SetXOffset = SetXOffset; region.SetYOffset = SetYOffset; region.GetXOffset = GetXOffset; region.GetYOffset = GetYOffset; region.SetOffsetRelative = SetOffsetRelative region.SetXOffsetRelative = SetXOffsetRelative region.SetYOffsetRelative = SetYOffsetRelative region.GetXOffsetRelative = GetXOffsetRelative region.GetYOffsetRelative = GetYOffsetRelative region.SetOffsetAnim = SetOffsetAnim; region.ResetPosition = ResetPosition; region.RealClearAllPoints = region.ClearAllPoints; region.ClearAllPoints = function() region:RealClearAllPoints(); region:ResetPosition(); end region.SetRegionAlpha = SetRegionAlpha; region.GetRegionAlpha = GetRegionAlpha; region.SetAnimAlpha = SetAnimAlpha; region.SetTriggerProvidesTimer = SetTriggerProvidesTimer region.UpdateRegionHasTimerTick = UpdateRegionHasTimerTick region.UpdateTimerTick = UpdateTimerTick region.subRegionEvents = CreateSubRegionEventSystem() region:SetPoint("CENTER", UIParent, "CENTER") end -- SetDurationInfo function WeakAuras.regionPrototype.modify(parent, region, data) region.subRegionEvents:ClearSubscribers() local defaultsForRegion = WeakAuras.regionTypes[data.regionType] and WeakAuras.regionTypes[data.regionType].default; if (defaultsForRegion and defaultsForRegion.alpha) then region:SetRegionAlpha(data.alpha); end if region.SetRegionAlpha then region:SetRegionAlpha(data.alpha) end local hasAdjustedMin = defaultsForRegion and defaultsForRegion.useAdjustededMin ~= nil and data.useAdjustededMin and data.adjustedMin; local hasAdjustedMax = defaultsForRegion and defaultsForRegion.useAdjustededMax ~= nil and data.useAdjustededMax and data.adjustedMax; region.adjustedMin = nil region.adjustedMinRel = nil region.adjustedMinRelPercent = nil region.adjustedMax = nil region.adjustedMaxRel = nil region.adjustedMaxRelPercent = nil if (hasAdjustedMin) then local percent = string.match(data.adjustedMin, "(%d+)%%") if percent then region.adjustedMinRelPercent = tonumber(percent) / 100 else region.adjustedMin = tonumber(data.adjustedMin); end end if (hasAdjustedMax) then local percent = string.match(data.adjustedMax, "(%d+)%%") if percent then region.adjustedMaxRelPercent = tonumber(percent) / 100 else region.adjustedMax = tonumber(data.adjustedMax) end end region:SetOffset(data.xOffset or 0, data.yOffset or 0); region:SetOffsetRelative(0, 0) region:SetOffsetAnim(0, 0); if data.anchorFrameType == "CUSTOM" and data.customAnchor then region.customAnchorFunc = WeakAuras.LoadFunction("return " .. data.customAnchor, data.id, "custom anchor") else region.customAnchorFunc = nil end if not parent or parent.regionType ~= "dynamicgroup" then if -- Don't anchor single Auras that with custom anchoring, -- these will be anchored in expand not ( data.anchorFrameType == "CUSTOM" or data.anchorFrameType == "UNITFRAME" ) -- Group Auras that will never be expanded, so those need -- to be always anchored here or data.regionType == "dynamicgroup" or data.regionType == "group" then Private.AnchorFrame(data, region, parent); end end region.startFormatters = Private.CreateFormatters(data.actions.start.message, function(key, default) local fullKey = "message_format_" .. key if data.actions.start[fullKey] == nil then data.actions.start[fullKey] = default end return data.actions.start[fullKey] end) region.finishFormatters = Private.CreateFormatters(data.actions.finish.message, function(key, default) local fullKey = "message_format_" .. key if data.actions.finish[fullKey] == nil then data.actions.finish[fullKey] = default end return data.actions.finish[fullKey] end) end function WeakAuras.regionPrototype.modifyFinish(parent, region, data) -- Sync subRegions if region.subRegions then for index, subRegion in pairs(region.subRegions) do Private.subRegionTypes[subRegion.type].release(subRegion) end wipe(region.subRegions) end if data.subRegions then region.subRegions = region.subRegions or {} local subRegionTypes = {} for index, subRegionData in pairs(data.subRegions) do if Private.subRegionTypes[subRegionData.type] then local subRegion = Private.subRegionTypes[subRegionData.type].acquire() subRegion.type = subRegionData.type if subRegion then Private.subRegionTypes[subRegionData.type].modify(region, subRegion, data, subRegionData, not subRegionTypes[subRegionData.type]) subRegionTypes[subRegionData.type] = true end tinsert(region.subRegions, subRegion) end end end region:UpdateRegionHasTimerTick() Private.ApplyFrameLevel(region) end local function SetProgressValue(region, value, total) local adjustMin = region.adjustedMin or 0; local max = region.adjustedMax or total; region:SetValue(value - adjustMin, max - adjustMin); end local regionsForFrameTick = {} local frameForFrameTick = CreateFrame("FRAME"); WeakAuras.frames["Frame Tick Frame"] = frameForFrameTick local function FrameTick() if WeakAuras.IsOptionsOpen() then return end Private.StartProfileSystem("frame tick") for region in pairs(regionsForFrameTick) do Private.StartProfileAura(region.id); if region.FrameTick then region.FrameTick() end region.subRegionEvents:Notify("FrameTick") Private.StopProfileAura(region.id); end Private.StopProfileSystem("frame tick") end local function RegisterForFrameTick(region) -- Check for a Frame Tick function local hasFrameTick = region.FrameTick if not hasFrameTick then if (region.subRegions) then for index, subRegion in pairs(region.subRegions) do if subRegion.FrameTick then hasFrameTick = true break end end end end if not hasFrameTick then return end regionsForFrameTick[region] = true if not frameForFrameTick:GetScript("OnUpdate") then frameForFrameTick:SetScript("OnUpdate", FrameTick); end end local function UnRegisterForFrameTick(region) regionsForFrameTick[region] = nil if not next(regionsForFrameTick) then frameForFrameTick:SetScript("OnUpdate", nil) end end local function TimerTickForSetDuration(self) local duration = self.duration local adjustMin = self.adjustedMin or 0; local max if duration == 0 then max = 0 elseif self.adjustedMax then max = self.adjustedMax else max = duration end self:SetTime(max - adjustMin, self.expirationTime - adjustMin, self.inverse); end function WeakAuras.regionPrototype.AddSetDurationInfo(region) if (region.SetValue and region.SetTime) then region.generatedSetDurationInfo = true; -- WeakAuras no longer calls SetDurationInfo, but some people do that, -- In that case we also need to overwrite TimerTick region.SetDurationInfo = function(self, duration, expirationTime, customValue, inverse) self.duration = duration or 0 self.expirationTime = expirationTime; self.inverse = inverse; if customValue then SetProgressValue(region, duration, expirationTime); region.TimerTick = nil region:UpdateRegionHasTimerTick() else local adjustMin = region.adjustedMin or 0; region:SetTime((duration ~= 0 and region.adjustedMax or duration) - adjustMin, expirationTime - adjustMin, inverse); region.TimerTick = TimerTickForSetDuration region:UpdateRegionHasTimerTick() end end elseif (region.generatedSetDurationInfo) then region.generatedSetDurationInfo = nil; region.SetDurationInfo = nil; end end -- Expand/Collapse function function WeakAuras.regionPrototype.AddExpandFunction(data, region, cloneId, parent, parentRegionType) local uid = data.uid local id = data.id local inDynamicGroup = parentRegionType == "dynamicgroup"; local inGroup = parentRegionType == "group"; local startMainAnimation = function() Private.Animate("display", uid, "main", data.animation.main, region, false, nil, true, cloneId); end function region:OptionsClosed() region:EnableMouse(false) region:SetScript("OnMouseDown", nil) end function region:ClickToPick() region:EnableMouse(true) region:SetScript("OnMouseDown", function() WeakAuras.PickDisplay(region.id, nil, true) end) if region.GetFrameStrata and region:GetFrameStrata() == "TOOLTIP" then region:SetFrameStrata("HIGH") end end local hideRegion; if(inDynamicGroup) then hideRegion = function() if region.PreHide then region:PreHide() end Private.RunConditions(region, uid, true) region.subRegionEvents:Notify("PreHide") region:Hide(); if (cloneId) then Private.ReleaseClone(region.id, cloneId, data.regionType); parent:RemoveChild(id, cloneId) else parent:DeactivateChild(id, cloneId); end end else hideRegion = function() if region.PreHide then region:PreHide() end Private.RunConditions(region, uid, true) region.subRegionEvents:Notify("PreHide") region:Hide(); if (cloneId) then Private.ReleaseClone(region.id, cloneId, data.regionType); end end end if(inDynamicGroup) then function region:Collapse() if (not region.toShow) then return; end region.toShow = false; region:SetScript("OnUpdate", nil) Private.PerformActions(data, "finish", region); if (not Private.Animate("display", data.uid, "finish", data.animation.finish, region, false, hideRegion, nil, cloneId)) then hideRegion(); end if (region.SoundRepeatStop) then region:SoundRepeatStop(); end UnRegisterForFrameTick(region) end function region:Expand() if (region.toShow) then return; end region.toShow = true; if(region.PreShow) then region:PreShow(); end region.subRegionEvents:Notify("PreShow") region.justCreated = nil; Private.ApplyFrameLevel(region) region:Show(); Private.PerformActions(data, "start", region); if not(Private.Animate("display", data.uid, "start", data.animation.start, region, true, startMainAnimation, nil, cloneId)) then startMainAnimation(); end parent:ActivateChild(data.id, cloneId); RegisterForFrameTick(region) region:UpdateTimerTick() end elseif not(data.controlledChildren) then function region:Collapse() if (not region.toShow) then return; end region.toShow = false; region:SetScript("OnUpdate", nil) Private.PerformActions(data, "finish", region); if (not Private.Animate("display", data.uid, "finish", data.animation.finish, region, false, hideRegion, nil, cloneId)) then hideRegion(); end if inGroup then parent:UpdateBorder(region); end if (region.SoundRepeatStop) then region:SoundRepeatStop(); end UnRegisterForFrameTick(region) end function region:Expand() if data.anchorFrameType == "SELECTFRAME" or data.anchorFrameType == "CUSTOM" or data.anchorFrameType == "UNITFRAME" then Private.AnchorFrame(data, region, parent); end if (region.toShow) then return; end region.toShow = true; region.justCreated = nil; if(region.PreShow) then region:PreShow(); end region.subRegionEvents:Notify("PreShow") Private.ApplyFrameLevel(region) region:Show(); Private.PerformActions(data, "start", region); if not(Private.Animate("display", data.uid, "start", data.animation.start, region, true, startMainAnimation, nil, cloneId)) then startMainAnimation(); end if inGroup then parent:UpdateBorder(region); end RegisterForFrameTick(region) region:UpdateTimerTick() end end -- Stubs that allow for polymorphism if not region.Collapse then function region:Collapse() end end if not region.Expand then function region:Expand() end end end