Add Class Resources and Debuff Highlighting modules; migrate settings to AceDB profiles
Move CoA settings from E.global/E.private into a proper AceDB-3.0 database (CoA.db, SavedVariables ElvUI_CoADB) with a Profiles options tab, since ElvUI doesn't ship AceDBOptions-3.0 (vendored under Libraries/). Add ClassResources.lua: hooks the custom-class resource frames (segment bar, orb, bar, multi-cast bar) into ElvUI's Toggle Anchors, with a per-frame hide checkbox in a new "Class Resources" options tab. Add DispelHighlight.lua: highlights dispellable debuffs on unitframes, with talent-aware filtering for custom classes. Fix mover/anchor reliability for Class Resources and the Instance (LayerPicker) button: - Movers were only retried until the target frame existed, not until the mover was actually created, so frames that start at zero size (e.g. a resource bar before that resource is ever active) permanently lost their mover. - Class resource frames re-anchor themselves on refresh, stomping the mover's anchor; hook SetPoint to snap back to the mover holder whenever something else repositions the frame. - Native click-drag on these frames ends with the engine calling SetPoint directly, severing the mover anchor entirely. Permanently block drag (script and RegisterForDrag) instead of clearing it once.
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
local E, L, V, P, G = unpack(ElvUI)
|
||||
local CoA = E:GetModule("CoA")
|
||||
|
||||
local FRAMES = {
|
||||
{name = "CoAResourceSegmentBar", moverText = "Resource Segment Bar", hideKey = "hideResourceSegmentBar"},
|
||||
{name = "CoAResourceOrb", moverText = "Resource Orb", hideKey = "hideResourceOrb"},
|
||||
{name = "CoAResourceBar", moverText = "Resource Bar", hideKey = "hideResourceBar"},
|
||||
{name = "CoAMultiCastActionBarFrame", moverText = "Multi Cast Action Bar", hideKey = "hideMultiCastActionBar"},
|
||||
}
|
||||
|
||||
-- A single nil-out isn't enough: the frame's own update logic re-attaches
|
||||
-- OnDragStart/OnDragStop (and re-registers drag buttons) on refresh, and
|
||||
-- native dragging ends with the engine calling SetPoint directly on the
|
||||
-- frame, severing the live anchor to our mover holder. So we don't just
|
||||
-- clear drag once, we permanently intercept any future attempt to turn it
|
||||
-- back on. The CoAClearing* guards stop our own corrective calls from
|
||||
-- re-triggering these same hooks.
|
||||
local function DisableDrag(frame)
|
||||
if frame.CoADragDisabled then return end
|
||||
frame.CoADragDisabled = true
|
||||
|
||||
frame:SetScript("OnDragStart", nil)
|
||||
frame:SetScript("OnDragStop", nil)
|
||||
frame:RegisterForDrag()
|
||||
|
||||
hooksecurefunc(frame, "SetScript", function(self, script, handler)
|
||||
if handler and (script == "OnDragStart" or script == "OnDragStop") and not self.CoAClearingDragScript then
|
||||
self.CoAClearingDragScript = true
|
||||
self:SetScript(script, nil)
|
||||
self.CoAClearingDragScript = false
|
||||
end
|
||||
end)
|
||||
|
||||
hooksecurefunc(frame, "RegisterForDrag", function(self, ...)
|
||||
if select("#", ...) > 0 and not self.CoAClearingDrag then
|
||||
self.CoAClearingDrag = true
|
||||
self:RegisterForDrag()
|
||||
self.CoAClearingDrag = false
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- Anchoring a frame while in combat lockdown can taint it, so anchors
|
||||
-- queued during combat are batched and applied together on the next
|
||||
-- PLAYER_REGEN_ENABLED instead of each frame registering its own handler
|
||||
-- (which would stomp on each other, since AceEvent keeps only the most
|
||||
-- recently registered callback per event for a given object).
|
||||
local pendingAnchors = {}
|
||||
local combatHandlerRegistered = false
|
||||
|
||||
local function QueueAnchor(fn)
|
||||
if not InCombatLockdown() then
|
||||
fn()
|
||||
return
|
||||
end
|
||||
|
||||
table.insert(pendingAnchors, fn)
|
||||
|
||||
if not combatHandlerRegistered then
|
||||
combatHandlerRegistered = true
|
||||
|
||||
CoA:RegisterEvent("PLAYER_REGEN_ENABLED", function()
|
||||
for i = 1, #pendingAnchors do
|
||||
pendingAnchors[i]()
|
||||
end
|
||||
|
||||
wipe(pendingAnchors)
|
||||
CoA:UnregisterEvent("PLAYER_REGEN_ENABLED")
|
||||
combatHandlerRegistered = false
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
local function AnchorToHolder(frame, holder)
|
||||
frame:ClearAllPoints()
|
||||
frame:SetPoint("CENTER", holder, "CENTER")
|
||||
end
|
||||
|
||||
-- Class resource frames re-anchor themselves (e.g. relative to the player/
|
||||
-- target frame) whenever they refresh, which stomps our mover anchor. Since
|
||||
-- there's no event that fires only for that self-repositioning, we hook
|
||||
-- SetPoint itself and snap back to the holder any time something else moves
|
||||
-- the frame. CoARepositioning guards against the corrective SetPoint call
|
||||
-- re-triggering this same hook.
|
||||
local function LockPosition(frame, holder)
|
||||
if frame.CoAPositionLocked then return end
|
||||
frame.CoAPositionLocked = true
|
||||
|
||||
hooksecurefunc(frame, "SetPoint", function(self)
|
||||
if self.CoARepositioning then return end
|
||||
self.CoARepositioning = true
|
||||
|
||||
QueueAnchor(function()
|
||||
AnchorToHolder(self, holder)
|
||||
self.CoARepositioning = false
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
local function SetupMover(frame, name, moverText)
|
||||
if frame.CoAMoverCreated then return true end
|
||||
|
||||
local width, height = frame:GetSize()
|
||||
if width == 0 or height == 0 then return false end
|
||||
|
||||
local left, bottom = frame:GetLeft(), frame:GetBottom()
|
||||
if not left or not bottom then return false end
|
||||
|
||||
frame.CoAMoverCreated = true
|
||||
|
||||
local holder = CreateFrame("Frame", "CoA_"..name.."Holder", E.UIParent)
|
||||
holder:Size(width, height)
|
||||
holder:Point("BOTTOMLEFT", E.UIParent, "BOTTOMLEFT", left, bottom)
|
||||
|
||||
E:CreateMover(holder, "CoA_"..name.."Mover", moverText, nil, nil, nil, nil, nil, "CoA,skin,classResources")
|
||||
holder:SetAllPoints(_G["CoA_"..name.."Mover"])
|
||||
|
||||
QueueAnchor(function()
|
||||
AnchorToHolder(frame, holder)
|
||||
LockPosition(frame, holder)
|
||||
end)
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
-- CoAForceHidden distinguishes frames we hid ourselves (via the options
|
||||
-- checkbox) from frames the game itself decided to hide, so unchecking the
|
||||
-- box only restores frames we were suppressing.
|
||||
local function ApplyVisibility(frame, hideKey)
|
||||
if CoA.db.profile[hideKey] then
|
||||
frame.CoAForceHidden = true
|
||||
frame:Hide()
|
||||
elseif frame.CoAForceHidden then
|
||||
frame.CoAForceHidden = false
|
||||
frame:Show()
|
||||
end
|
||||
end
|
||||
|
||||
local function SetupVisibility(frame, hideKey)
|
||||
if frame.CoAVisibilityHooked then return end
|
||||
frame.CoAVisibilityHooked = true
|
||||
|
||||
frame:HookScript("OnShow", function(self)
|
||||
ApplyVisibility(self, hideKey)
|
||||
end)
|
||||
|
||||
ApplyVisibility(frame, hideKey)
|
||||
end
|
||||
|
||||
function CoA:UpdateClassResourceVisibility()
|
||||
for _, def in ipairs(FRAMES) do
|
||||
local frame = _G[def.name]
|
||||
if frame then
|
||||
ApplyVisibility(frame, def.hideKey)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local hooked = {}
|
||||
|
||||
local function TryHookAll()
|
||||
local allHooked = true
|
||||
|
||||
for _, def in ipairs(FRAMES) do
|
||||
if not hooked[def.name] then
|
||||
local frame = _G[def.name]
|
||||
|
||||
if frame then
|
||||
DisableDrag(frame)
|
||||
SetupVisibility(frame, def.hideKey)
|
||||
|
||||
if SetupMover(frame, def.name, def.moverText) then
|
||||
hooked[def.name] = true
|
||||
else
|
||||
allHooked = false
|
||||
end
|
||||
else
|
||||
allHooked = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return allHooked
|
||||
end
|
||||
|
||||
function CoA:InitializeClassResources()
|
||||
if TryHookAll() then return end
|
||||
|
||||
self.classResourcesTimer = self:ScheduleRepeatingTimer(function()
|
||||
if TryHookAll() then
|
||||
self:CancelTimer(self.classResourcesTimer)
|
||||
self.classResourcesTimer = nil
|
||||
end
|
||||
end, 0.5)
|
||||
end
|
||||
@@ -0,0 +1,86 @@
|
||||
local E, L, V, P, G = unpack(ElvUI)
|
||||
local UF = E:GetModule("UnitFrames")
|
||||
local CoA = E:GetModule("CoA")
|
||||
|
||||
-- Base dispel types each custom class can remove, independent of talents.
|
||||
-- CHRONOMANCER is a special case: its dispel removes the last debuff applied
|
||||
-- to the target regardless of type, so it's never filtered out here -- we
|
||||
-- can only approximate this as "treat every typed debuff as dispellable",
|
||||
-- since the underlying oUF scan only ever surfaces typed debuffs anyway.
|
||||
local CLASS_DISPEL_TYPES = {
|
||||
CHRONOMANCER = true,
|
||||
SUNCLERIC = {Magic = true, Poison = true, Disease = true},
|
||||
STARCALLER = {Poison = true, Disease = true},
|
||||
PROPHET = {Poison = true},
|
||||
WITCHDOCTOR = {Curse = true},
|
||||
CULTIST = {Magic = true},
|
||||
PYROMANCER = {},
|
||||
}
|
||||
|
||||
-- Extra dispel types unlocked by a talent choice. There's no reliable way to
|
||||
-- auto-detect the talent on this server, so these are gated by a manual
|
||||
-- checkbox in the options panel instead.
|
||||
local TALENT_DISPEL_TYPES = {
|
||||
PROPHET = {flag = "hasBlightAntidote", types = {Curse = true}},
|
||||
CULTIST = {flag = "hasDevourCurse", types = {Curse = true}},
|
||||
PYROMANCER = {flag = "hasBurnImpurities", types = {Magic = true, Disease = true, Bleed = true}},
|
||||
}
|
||||
|
||||
function CoA:CanDispel(debuffType)
|
||||
local _, class = UnitClass("player")
|
||||
local baseTypes = CLASS_DISPEL_TYPES[class]
|
||||
|
||||
if baseTypes == true then return true end
|
||||
if baseTypes and baseTypes[debuffType] then return true end
|
||||
|
||||
local talent = TALENT_DISPEL_TYPES[class]
|
||||
if talent and CoA.db.profile[talent.flag] and talent.types[debuffType] then return true end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local function SuppressHighlight(object)
|
||||
if object.DebuffHighlightBackdrop and object.DBHGlow then
|
||||
object.DBHGlow:Hide()
|
||||
elseif object.DebuffHighlightUseTexture then
|
||||
object.DebuffHighlight:SetTexture(nil)
|
||||
else
|
||||
object.DebuffHighlight:SetVertexColor(0, 0, 0, 0)
|
||||
end
|
||||
end
|
||||
|
||||
local origPostUpdate = UF.PostUpdate_DebuffHighlight
|
||||
|
||||
local function DispelAwarePostUpdate(dbh, object, debuffType, texture, wasFiltered, style, color)
|
||||
origPostUpdate(dbh, object, debuffType, texture, wasFiltered, style, color)
|
||||
|
||||
if CoA.db.profile.dispelHighlightOnlyMine and debuffType and not wasFiltered and not CoA:CanDispel(debuffType) then
|
||||
SuppressHighlight(object)
|
||||
end
|
||||
end
|
||||
|
||||
UF.PostUpdate_DebuffHighlight = DispelAwarePostUpdate
|
||||
|
||||
hooksecurefunc(UF, "Configure_DebuffHighlight", function(_, frame)
|
||||
local dbh = frame.DebuffHighlight
|
||||
if dbh then
|
||||
dbh.PostUpdate = UF.PostUpdate_DebuffHighlight
|
||||
end
|
||||
end)
|
||||
|
||||
function CoA:UpdateDispelHighlight()
|
||||
UF:Update_AllFrames()
|
||||
end
|
||||
|
||||
-- On Ascension, UnitClass("player")'s second return only reliably reports the
|
||||
-- real custom class (CULTIST, PYROMANCER, ...) a short while after login --
|
||||
-- immediately at ADDON_LOADED/PLAYER_LOGIN it can still read back the generic
|
||||
-- "HERO" base class. If a debuff highlight gets evaluated before that data
|
||||
-- syncs, CoA:CanDispel wrongly returns false and the highlight stays wrongly
|
||||
-- suppressed until the next aura change. Force one extra refresh shortly
|
||||
-- after entering the world so the very first debuff isn't judged too early.
|
||||
function CoA:InitializeDispelHighlight()
|
||||
CoA:RegisterEvent("PLAYER_ENTERING_WORLD", function()
|
||||
CoA:ScheduleTimer("UpdateDispelHighlight", 2)
|
||||
end)
|
||||
end
|
||||
@@ -44,7 +44,7 @@ end
|
||||
local function UpdateSize(button)
|
||||
button = button or _G[BUTTON_NAME]
|
||||
if button then
|
||||
local size = E.global.CoA.extraActionButtonSize or 52
|
||||
local size = CoA.db.profile.extraActionButtonSize or 52
|
||||
button:SetSize(size, size)
|
||||
UpdateRimEdge(button, size)
|
||||
UpdateHotkeyPosition(button, size)
|
||||
@@ -124,7 +124,7 @@ local function SkinButton(button, container)
|
||||
rim:SetAllPoints(icon)
|
||||
rim:SetFrameLevel(button.backdrop:GetFrameLevel() + 10)
|
||||
button.CoARim = rim
|
||||
UpdateRimEdge(button, E.global.CoA.extraActionButtonSize or 52)
|
||||
UpdateRimEdge(button, CoA.db.profile.extraActionButtonSize or 52)
|
||||
|
||||
button:SetFrameLevel(rim:GetFrameLevel() + 1)
|
||||
end
|
||||
@@ -158,13 +158,13 @@ local function SkinButton(button, container)
|
||||
end
|
||||
|
||||
local function SetupMover(container, button)
|
||||
if CoA.extraActionBarMoverCreated then return end
|
||||
if CoA.extraActionBarMoverCreated then return true end
|
||||
|
||||
local width, height = button:GetSize()
|
||||
if width == 0 or height == 0 then return end
|
||||
if width == 0 or height == 0 then return false end
|
||||
|
||||
local left, bottom = button:GetLeft(), button:GetBottom()
|
||||
if not left or not bottom then return end
|
||||
if not left or not bottom then return false end
|
||||
|
||||
CoA.extraActionBarMoverCreated = true
|
||||
|
||||
@@ -191,6 +191,8 @@ local function SetupMover(container, button)
|
||||
else
|
||||
Anchor()
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local function TryHook()
|
||||
@@ -198,11 +200,11 @@ local function TryHook()
|
||||
local button = _G[BUTTON_NAME]
|
||||
|
||||
if container and button then
|
||||
SetupMover(container, button)
|
||||
SkinButton(button, container)
|
||||
return SetupMover(container, button)
|
||||
end
|
||||
|
||||
return container ~= nil and button ~= nil
|
||||
return false
|
||||
end
|
||||
|
||||
function CoA:InitializeExtraActionBar()
|
||||
|
||||
+30
-6
@@ -11,7 +11,7 @@ local function UpdateFont(button)
|
||||
local text = button and _G[button:GetName().."Text"]
|
||||
if not text then return end
|
||||
|
||||
text:FontTemplate(LSM:Fetch("font", E.global.CoA.instanceButtonFont), E.global.CoA.instanceButtonFontSize, E.global.CoA.instanceButtonFontOutline)
|
||||
text:FontTemplate(LSM:Fetch("font", CoA.db.profile.instanceButtonFont), CoA.db.profile.instanceButtonFontSize, CoA.db.profile.instanceButtonFontOutline)
|
||||
|
||||
button:SetSize(
|
||||
math.max(MIN_WIDTH, text:GetStringWidth() + PAD_X),
|
||||
@@ -34,22 +34,44 @@ do
|
||||
end
|
||||
end
|
||||
|
||||
-- A single nil-out isn't enough: native dragging ends with the engine
|
||||
-- calling SetPoint directly on the frame, severing the live anchor to our
|
||||
-- mover holder. So we don't just clear drag once, we permanently intercept
|
||||
-- any future attempt to turn it back on. The CoAClearing* guards stop our
|
||||
-- own corrective calls from re-triggering these same hooks.
|
||||
local function DisableDrag(button)
|
||||
if button.CoADragDisabled then return end
|
||||
button.CoADragDisabled = true
|
||||
|
||||
button:SetScript("OnDragStart", nil)
|
||||
button:SetScript("OnDragStop", nil)
|
||||
button:RegisterForDrag()
|
||||
|
||||
hooksecurefunc(button, "SetScript", function(self, script, handler)
|
||||
if handler and (script == "OnDragStart" or script == "OnDragStop") and not self.CoAClearingDragScript then
|
||||
self.CoAClearingDragScript = true
|
||||
self:SetScript(script, nil)
|
||||
self.CoAClearingDragScript = false
|
||||
end
|
||||
end)
|
||||
|
||||
hooksecurefunc(button, "RegisterForDrag", function(self, ...)
|
||||
if select("#", ...) > 0 and not self.CoAClearingDrag then
|
||||
self.CoAClearingDrag = true
|
||||
self:RegisterForDrag()
|
||||
self.CoAClearingDrag = false
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local function SetupMover(button)
|
||||
if CoA.layerPickerMoverCreated then return end
|
||||
if CoA.layerPickerMoverCreated then return true end
|
||||
|
||||
local width, height = button:GetSize()
|
||||
if width == 0 or height == 0 then return end
|
||||
if width == 0 or height == 0 then return false end
|
||||
|
||||
local left, bottom = button:GetLeft(), button:GetBottom()
|
||||
if not left or not bottom then return end
|
||||
if not left or not bottom then return false end
|
||||
|
||||
CoA.layerPickerMoverCreated = true
|
||||
|
||||
@@ -74,6 +96,8 @@ local function SetupMover(button)
|
||||
else
|
||||
Anchor()
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local function SkinButton(button)
|
||||
@@ -92,11 +116,11 @@ local function TryHook()
|
||||
|
||||
if button then
|
||||
DisableDrag(button)
|
||||
SetupMover(button)
|
||||
SkinButton(button)
|
||||
return SetupMover(button)
|
||||
end
|
||||
|
||||
return button ~= nil
|
||||
return false
|
||||
end
|
||||
|
||||
function CoA:InitializeLayerPicker()
|
||||
|
||||
Reference in New Issue
Block a user