--[[--
`ChorusAuraButtonTemplate` handles individual aura pictograms.
Features:
* show formatted remaining aura duration;
* highlight auras that were applied by the player character;
* show remaining charge quantity when applicable;
* show aura artwork;
* show aura category (Magic, Poison, Disease, Curse);
* display tooltip on mouseover;
@submodule chorus
]]
local Chorus = Chorus
local GetTime = GetTime
local UnitAura = Chorus.test.UnitAura or UnitAura
local UnitExists = Chorus.test.UnitExists or UnitExists
local UnitIsConnected = Chorus.test.UnitIsConnected or UnitIsConnected
local UnitIsUnit = Chorus.test.UnitIsUnit or UnitIsUnit
local DebuffTypeColor = DebuffTypeColor
local GameTooltip = GameTooltip
local SecureButton_GetUnit = Chorus.test.SecureButton_GetUnit or SecureButton_GetUnit
--[[--
Render pictogram artwork for this aura button.
Fallback to a picture of a question mark if the artwork could not be loaded.
That is, when `artworkFile` is `nil`.
@function applyArtwork
@tparam frame auraButton this aura button
@tparam string artworkFile pathname in Windows format with escape characters;
]]
local function applyArtwork(auraButton, artworkFile)
assert(auraButton ~= nil)
if not artworkFile then
artworkFile = "Interface\\Icons\\INV_Misc_QuestionMark"
end
local artwork = auraButton.artwork
assert(artwork ~= nil)
artwork:SetTexture(artworkFile)
end
--[[--
Render sanitized and color coded border for this aura button.
@see FrameXML/BuffFrame.lua:DebuffTypeColor
@function applyOverlay
@tparam frame auraButton this aura button
@tparam string category key of `DebuffTypeColor` table
@tparam string owner unit designation of caster of the given aura, used for
color coding; in reality, either `player` or `nil`
@return nothing
]]
local function applyOverlay(auraButton, category, owner)
if not category then
--[[ Empty string is equivalent to 'none' by default for DebuffTypeColor. ]]--
category = ''
end
local r = 1
local g = 1
local b = 1
if category then
assert(category ~= nil)
assert('string' == type(category))
category = strtrim(category)
--[[ Empty string is permissible ]]--
assert(string.len(category) >= 0)
assert(string.len(category) <= 256)
local colorTuple = DebuffTypeColor[category]
r = colorTuple.r
g = colorTuple.g
b = colorTuple.b
end
local overlay = auraButton.overlay
assert(overlay ~= nil)
overlay:SetVertexColor(r, g, b)
local label = auraButton.label
assert(label ~= nil)
if owner and 'player' == owner then
label:SetTextColor(1, 225 / 255, 0)
else
label:SetTextColor(1, 1, 1)
end
overlay:SetAlpha(1)
end
--[[--
Format the given amount of seconds into a narrow human readable string.
This is inteded for aura effects. It may be used for any generic duration.
@function formatDuration
@tparam number durationSec positive number and not zero, the remaining seconds
of some effect
@treturn string remaining duration coerced into a string that is human
readeable and narrow for convenient rendering
]]
local function formatDuration(durationSec)
assert(durationSec ~= nil)
assert('number' == type(durationSec))
local t
local durationSecAbs = math.abs(durationSec)
if durationSecAbs < 60 then
t = string.format("%.0f", durationSec)
elseif durationSecAbs < 3600 then
t = string.format("%.0f m", durationSec / 60)
elseif durationSecAbs < 3600 * 24 then
t = string.format("%.0f h", durationSec / 60 / 60)
else
t = string.format("%.0f d", durationSec / 60 / 60 / 24)
end
return t
end
--[[--
Compute remaining aura duration for this aura button, given time instances,
then sanitize, format and render it.
@function applyDuration
@tparam frame auraButton this aura button
@tparam number now time instance, in the format of `function GetTime`, current
real time
@tparam number totalDurationSec positive number, the total duration in seconds
of the aura
@tparam number expirationInstance time instance, in the format of `function
GetTime`, the instance when the aura effect ends
@return nothing
]]
local function applyDuration(auraButton, now, totalDurationSec, expirationInstance)
assert(now ~= nil)
assert('number' == type(now))
assert(now >= 0)
assert(totalDurationSec ~= nil)
assert('number' == type(totalDurationSec))
assert(totalDurationSec >= 0)
assert(expirationInstance ~= nil)
assert('number' == type(expirationInstance))
assert(expirationInstance >= 0)
local label = auraButton.label
assert (label ~= nil)
local durationRemainingSec
if totalDurationSec and now < expirationInstance then
durationRemainingSec = expirationInstance - now
end
local t
if durationRemainingSec then
t = formatDuration(durationRemainingSec)
else
--[[ The aura button text is color coded to report if the owner
of the aura is the user or another player. Therefore, the
label should never be empty. ]]--
t = '∞'
end
label:SetText(t)
local artwork = auraButton.artwork
if artwork and totalDurationSec and durationRemainingSec and totalDurationSec >= 12 and durationRemainingSec <= 3 then
artwork:SetAlpha(0.6)
else
artwork:SetAlpha(1)
end
end
--[[--
Sanitize, format and render the charge quantity for this aura button.
@function applyChargeQuantity
@tparam frame auraButton this aura button
@tparam integer chargeQuantity positive integer and not zero, the remaining
charges (stacks) of the aura
@return nothing
]]
local function applyChargeQuantity(auraButton, chargeQuantity)
local label = auraButton.label2
assert (label ~= nil)
local t = nil
if chargeQuantity then
assert(chargeQuantity ~= nil)
assert('number' == type(chargeQuantity))
chargeQuantity = math.abs(math.floor(chargeQuantity))
if chargeQuantity < 2 then
t = nil
elseif chargeQuantity < 100 then
t = string.format('%d', chargeQuantity)
else
t = '>99'
end
end
label:SetText(t)
end
--[[--
Every frame, update the remaining duration and remaining stack quantity of the
aura, of this aura button.
Update scripts like this should be optimized for performance. Update scripts
also rely on the current real time and implicit game state.
Remaining duration of an aura cannot be queried. It must be computed, given
time instances that could be queried. This is the purpose of the update
processor function.
@see FrameXML/SecureTemplates.lua:function SecureButton_GetUnit
@function auraButtonUpdateProcessor
@tparam frame self aura button
@return nothing
]]
local function auraButtonUpdateProcessor(self)
--[[-- @warning This function is executed every frame for every unit
aura button. It must be optimized as much as possible.]]
--[[-- It is not throttled for the sake of accuracy. Maybe it should
be. ]]
local index = self.index
if not index then
return
end
local unitDesignation = SecureButton_GetUnit(self)
if not unitDesignation then
return
end
local filter = SecureButton_GetAttribute(self, 'filter')
if not filter then
return
end
local name, _, _, chargeQuantity, _, durationSec, expirationInstance = UnitAura(unitDesignation, index, filter)
if not name then
return
end
applyDuration(self, GetTime(), durationSec, expirationInstance)
applyChargeQuantity(self, chargeQuantity)
end
local function saneUnit(frame)
assert(frame ~= nil)
--[[-- @warning Insecure aura buttons access protected properties, like
"unit" via `SecureButton_GetUnit`. This may or may not cause issues,
like aura buttons not updating property. ]]
local u = frame.unit or SecureButton_GetUnit(frame) or 'none'
assert(u ~= nil)
assert('string' == type(u))
u = string.lower(strtrim(u))
assert(string.len(u) >= 1)
assert(string.len(u) <= 256)
return u
end
local function saneUnitAuraFilter(auraButton)
assert(auraButton ~= nil)
local filter = auraButton.filter or SecureButton_GetAttribute(auraButton, 'filter')
assert(filter ~= nil)
assert('string' == type(filter))
filter = string.upper(strtrim(filter))
assert(string.len(filter) >= 1)
assert(string.len(filter) <= 256)
return filter
end
local function saneUnitAuraIndex(auraButton)
assert(auraButton ~= nil)
local i = auraButton.index or 0
assert(i ~= nil)
assert('number' == type(i))
i = math.min(math.max(0, math.abs(math.floor(i))), 8192)
return i
end
local function saneEvent(eventCategory)
assert(eventCategory ~= nil)
assert('string' == type(eventCategory))
eventCategory = string.upper(strtrim(eventCategory))
assert(string.len(eventCategory) >= 1)
assert(string.len(eventCategory) <= 256)
return eventCategory
end
local function auraLargeButtonApply(auraLargeButton, ...)
assert(auraLargeButton ~= nil)
local artworkFile = select(3, ...)
local durationSec = select(6, ...)
local expirationInstance = select(7, ...)
local category = select(5, ...)
local owner = select(8, ...)
local chargeQuantity = select(4, ...)
applyArtwork(auraLargeButton, artworkFile)
applyDuration(auraLargeButton, GetTime(), durationSec, expirationInstance)
applyOverlay(auraLargeButton, category, owner)
applyChargeQuantity(auraLargeButton, chargeQuantity)
end
local function auraTinyButtonApply(auraTinyButton, ...)
assert(auraTinyButton ~= nil)
local artworkFile = select(3, ...)
applyArtwork(auraTinyButton, artworkFile)
end
--[[--
Change properties of given aura button according to given arguments.
Use values returned by `UnitAura` for arguments. Assumes sane arguments. Does
not check arguments for validity. May produce widget that does not accurately
represent game state. Sanity properties should be handled elsewhere.
The function may ignore some arguments and interpret others differently
depending on context.
@function auraButtonApply
@see auraButtonRefresh
@see auraButtonUnapply
]]
local function auraButtonApply(auraButton, ...)
assert(auraButton ~= nil)
local strategy = auraButton.strategy
if 'ChorusAuraLargeButtonTemplate' == strategy then
auraLargeButtonApply(auraButton, ...)
elseif 'ChorusAuraTinyButtonTemplate' == strategy then
auraTinyButtonApply(auraButton, ...)
else
error('ChorusAuraButtonTemplate.lua: invalid enum: ' ..
'unknown aura rendering strategy')
end
end
local function auraLargeButtonUnapply(auraLargeButton)
assert(auraLargeButton ~= nil)
auraLargeButton.index = nil
auraLargeButton.spell = nil
local artwork = auraLargeButton.artwork
assert(artwork ~= nil)
artwork:SetTexture(artwork)
artwork:SetAlpha(1)
--[[ Remaining duration ]]--
local label = auraLargeButton.label
assert (label ~= nil)
label:SetText(nil)
--[[ Remaining charge quantity ]]--
local label2 = auraLargeButton.label2
assert (label2 ~= nil)
label2:SetText(nil)
local overlay = auraLargeButton.overlay
assert(overlay ~= nil)
overlay:SetAlpha(0)
end
local function auraTinyButtonUnapply(auraTinyButton)
assert(auraTinyButton ~= nil)
auraTinyButton.index = nil
auraTinyButton.spell = nil
local artwork = auraTinyButton.artwork
if artwork then
artwork:SetTexture(nil)
end
end
--[[--
Obscure the aura button to the user's eye, without toggling frame visibility
mechanically.
Toggling visibility of a frame is a restricted action. That is, it can only be
done in combat by trusted or secure frames. The aura buttons, for the sake of
their core features, are not secure or trusted frames. Therefore, they must
never be toggled with `Show` or `Hide` frame methods.
This implementation must also leave to traces in the taint log.
(Logs/taint.log)
@see InCombatLockdown
@see issecure
@see auraButtonApply
@see auraButtonRefresh
@function auraButtonUnapply
@tparam frame auraButton this aura button
@return nothing, only side effects
]]
local function auraButtonUnapply(auraButton)
assert(auraButton ~= nil)
local strategy = auraButton.strategy
if 'ChorusAuraLargeButtonTemplate' == strategy then
auraLargeButtonUnapply(auraButton)
elseif 'ChorusAuraTinyButtonTemplate' == strategy then
auraTinyButtonUnapply(auraButton)
else
error('ChorusAuraButtonTemplate.lua: invalid enum: ' ..
'unknown aura rendering strategy')
end
end
--[[--
Query game state and attempt to update this aura button appropriately.
May toggle button visibility.
@see auraButtonApply
@see auraButtonUnapply
@function auraButtonRefresh
@tparam frame auraButton this aura button
@return nothing, produces side effects
]]
local function auraButtonRefresh(auraButton)
assert(auraButton ~= nil)
local u = saneUnit(auraButton)
local filter = saneUnitAuraFilter(auraButton)
local i = saneUnitAuraIndex(auraButton)
local auraName, rank, artworkFile, chargeQuantity, category,
durationSec, expirationInstance, owner = UnitAura(u, i, filter)
if UnitExists(u) and UnitIsConnected(u) and auraName then
auraButtonApply(auraButton, auraName, rank, artworkFile,
chargeQuantity, category, durationSec, expirationInstance,
owner)
else
auraButtonUnapply(auraButton)
end
end
--[[--
Process stream of events, filter events only relevant to the aura button.
When aura is added or removed from a unit, given the aura button is assigned to
watch that unit, then apply relevant changes to the aura button.
@function auraButtonEventProcessor
@see FrameXML/SecureTemplates.lua:function SecureButton_GetUnit
@see auraButtonRefresh
@tparam frame auraButton the aura button
@tparam string eventCategory event category designation of the given event
@param unitDesignation vararg, given `UNIT_AURA`, the relevant unit
@return nothing
]]
local function auraButtonEventProcessor(auraButton, eventCategory, ...)
assert(auraButton ~= nil)
local e = saneEvent(eventCategory)
--[[ Only refresh for the relevant unit. ]]--
if 'UNIT_AURA' == e then
local eventUnit = select(1, ...)
local thisUnit = saneUnit(auraButton)
if UnitIsUnit(thisUnit, eventUnit) then
auraButtonRefresh(auraButton)
end
else
auraButtonRefresh(auraButton)
end
end
--[[--
Process the state of given aura button, then apply the details to the native
`GameTooltip`.
@see FrameXML/GameTooltip.lua:GameTooltip
@see FrameXML/SecureTemplates.lua:function SecureButton_GetUnit
@function auraButtonGameTooltipShow
@tparam frame self the aura button
@return nothing
]]
function Chorus.auraButtonGameTooltipShow(self)
GameTooltip:SetOwner(self, "ANCHOR_BOTTOMLEFT");
GameTooltip:SetFrameLevel(self:GetFrameLevel() + 2);
local unitDesignation = SecureButton_GetUnit(self) or 'none'
local filter = SecureButton_GetAttribute(self, 'filter') or self.filter
local index = self.index
local spellName = SecureButton_GetAttribute(self, 'spell') or self.spell
if index then
GameTooltip:SetUnitAura(unitDesignation, index, filter)
elseif spellName then
local rank = nil
GameTooltip:SetUnitAura(unitDesignation, spellName, rank, filter)
end
end
--[[--
Hide the tooltip associated with the aura button, if necessary.
Effectively, simply hides the native `GameTooltip`.
@see FrameXML/GameTooltip.lua:GameTooltip
@function auraButtonGameTooltipHide
@return nothing
]]
function Chorus.auraButtonGameTooltipHide()
GameTooltip:Hide();
end
local function auraButtonTextScale(auraButton)
assert(auraButton ~= nil)
--[[ ShadowedUnitFrames recommend this approach for adjusting font
size. ]]--
--[[ Only do this at initialization to save on processing and prevent
visual flicker. ]]--
local label = auraButton.label
assert(label ~= nil)
local t = label:GetText()
if t ~= nil and string.len(t) >= 1 then
local fontSize = 12
auraButton:SetScale(label:GetStringHeight() / fontSize)
end
end
--[[--
Initialize the aura button frame with callbacks and children.
@function auraLargeButtonMain
@tparam frame self
@return nothing
]]
function Chorus.auraLargeButtonMain(self)
local n = self:GetName()
if n then
self.artwork = _G[n .. 'Artwork']
self.label = _G[n .. 'Text1']
self.label2 = _G[n .. 'Text2']
self.overlay = _G[n .. 'Overlay']
end
self.strategy = 'ChorusAuraLargeButtonTemplate'
self:SetScript('OnEvent', auraButtonEventProcessor)
self:SetScript('OnUpdate', auraButtonUpdateProcessor)
self:RegisterEvent('PLAYER_ENTERING_WORLD')
self:RegisterEvent('PLAYER_LOGIN')
self:RegisterEvent('UNIT_AURA')
auraButtonTextScale(self)
end
function Chorus.auraTinyButtonMain(auraTinyButton)
assert(auraTinyButton ~= nil)
local b = auraTinyButton
local n = b:GetName()
assert(n ~= nil)
assert('string' == type(n))
n = strtrim(n)
assert(string.len(n) >= 1)
assert(string.len(n) <= 256)
local artwork = _G[n .. 'Artwork']
assert(artwork ~= nil)
b.artwork = artwork
b.strategy = 'ChorusAuraTinyButtonTemplate'
b:RegisterEvent('UNIT_AURA')
b:RegisterEvent('PLAYER_ENTERING_WORLD')
b:SetScript('OnEvent', auraButtonEventProcessor)
end
function Chorus.auraButtonRefresh(...)
return auraButtonRefresh(...)
end
function Chorus.auraButtonEventProcessor(...)
return auraButtonEventProcessor(...)
end