--[[ TODO Add pet target button ]]--
--[[ TODO Add new spoiler for enemy team in arena ]]--
--[[ TODO Add permanent unit buttons for player, target, focus and pet units ]]--
--[[ TODO Highlight raid frames that are focused or targeted or are player ]]--
--[[ TODO Add offline indicator for raid frames ]]--
--[[ TODO Add rest indicator for raid frames ]]--
--[[ TODO Add leader indicator for raid frames ]]--
--[[ TODO Add PvP status indicator for raid frames ]]--
--[[ FIXME Raid frame and spoiler overlap when raid roster updates ]]--
--[[ FIXME Range indicator sometimes does not update in time ]]--
local function trace(...)
print(date('%X'), '[|cFF5F87AFChoir|r]:', ...)
end
local function createBindingKeyHandler(button)
assert (button ~= nil)
local handler = CreateFrame('FRAME', button:GetName() .. 'ChoirBindingKeyHandler',
button, 'SecureHandlerShowHideTemplate')
handler:WrapScript(button, 'OnShow', [=[
local key = self:GetAttribute('choirBindingKey')
if key then
self:SetBindingClick(true, key, self)
end
]=])
handler:WrapScript(button, 'OnHide', [=[
self:ClearBindings()
]=])
return handler
end
local function createSpellShortcut(unitButton, frameDesignation, spellName)
assert (unitButton ~= nil)
assert (spellName ~= nil)
assert ('string' == type(spellName))
spellName = strtrim(spellName)
assert (string.len(spellName) >= 2)
assert (string.len(spellName) <= 256)
local unitDesignation = unitButton:GetAttribute('unit')
assert (unitDesignation ~= nil)
assert ('string' == type(unitDesignation))
unitDesignation = strtrim(unitDesignation)
assert (string.len(unitDesignation) >= 2)
assert (string.len(unitDesignation) <= 32)
local b = CreateFrame('BUTTON', frameDesignation, unitButton, 'SecureActionButtonTemplate')
b:SetAttribute('type', 'spell')
b:SetAttribute('unit', unitDesignation)
b:SetAttribute('spell', spellName)
createBindingKeyHandler(b)
assert (b ~= nil)
return b
end
local function createLabel(ownerFrame, fontObject)
assert (ownerFrame ~= nil)
local t = ownerFrame:CreateFontString((ownerFrame:GetName() or '') .. 'Text', 'OVERLAY')
fontObject = fontObject or ArimoRegular12 or CousineRegular12 or NumberFont_OutlineThick_Mono_Small
assert (fontObject ~= nil)
t:SetFontObject(fontObject)
t:SetAllPoints()
return t
end
local function createBackground(ownerFrame)
assert (ownerFrame ~= nil)
local background = ownerFrame:CreateTexture(ownerFrame:GetName() .. 'Background', 'BACKGROUND')
background:SetAllPoints()
background:SetTexture(0, 0, 0, 0.8)
return background
end
local function getRoleTexCoord(isTank, isHealer, isDamager)
local size = 64
if isTank then
return 0 / size, 19 / size, 22 / size, 41 / size
elseif isHealer then
return 20 / size, 39 / size, 1 / size, 20 / size
elseif isDamager then
return 20 / size, 39 / size, 22 / size, 41 / size
else
error('invalid argument')
end
end
local function roleWidgetEventProcessor(roleWidget)
assert (roleWidget ~= nil)
local unitButton = roleWidget:GetParent()
assert (unitButton ~= nil)
local unitDesignation = unitButton:GetAttribute('unit')
assert (unitDesignation ~= nil)
if not UnitExists(unitDesignation) then
return
end
local isTank, isHealer, isDamager = UnitGroupRolesAssigned(unitDesignation)
--[[ Corner-case for Interface >= 40000 ]]--
if 'string' == type(isTank) then
local roleDesignation = isTank
isTank = 'TANK' == roleDesignation
isHealer = 'HEALER' == roleDesignation
isDamager = 'DAMAGER' == roleDesignation
end
if isTank or isHealer or isDamager then
roleWidget:Show()
local artwork = roleWidget.artwork
assert (artwork ~= nil)
artwork:SetTexCoord(getRoleTexCoord(isTank, isHealer, isDamager))
else
roleWidget:Hide()
end
end
local function createRoleWidget(unitButton)
assert (unitButton ~= nil)
local n = unitButton:GetName() .. 'RoleWidgetFrame'
local widgetSize = 16
local roleWidget = CreateFrame('FRAME', n, unitButton)
roleWidget:SetPoint('TOPLEFT', 0, 0)
roleWidget:SetPoint('TOPRIGHT', 0, 0)
roleWidget:SetSize(unitButton:GetWidth(), widgetSize / 2)
local artwork = roleWidget:CreateTexture(roleWidget:GetName() .. 'Artwork', 'ARTWORK')
artwork:SetPoint('TOPRIGHT', roleWidget, 'TOPRIGHT', widgetSize * 0.0, widgetSize * 0.2)
artwork:SetPoint('BOTTOMLEFT', roleWidget, 'TOPRIGHT', -widgetSize * 1.0, -widgetSize * 0.8)
artwork:SetTexture("Interface\\LFGFrame\\UI-LFG-ICON-PORTRAITROLES.blp")
roleWidget.artwork = artwork
roleWidget:RegisterEvent('LFG_ROLE_UPDATE')
roleWidget:RegisterEvent('PARTY_MEMBERS_CHANGED')
roleWidget:RegisterEvent('PLAYER_ENTERING_BATTLEGROUND')
roleWidget:RegisterEvent('PLAYER_ENTERING_WORLD')
roleWidget:RegisterEvent('PLAYER_ROLES_ASSIGNED')
roleWidget:RegisterEvent('PLAYER_TALENT_UPDATE')
roleWidget:RegisterEvent('RAID_ROSTER_UPDATE')
roleWidget:RegisterEvent('ZONE_CHANGED_NEW_AREA')
roleWidget:SetScript('OnEvent', roleWidgetEventProcessor)
return roleWidget
end
local function getUnitHealthDeficit(unitDesignation)
assert (unitDesignation ~= nil)
local m = UnitHealthMax(unitDesignation) or 0
local c = UnitHealth(unitDesignation) or 0
return math.abs(c) - math.abs(m)
end
local function getClassColor(classDesignation)
assert (classDesignation ~= nil)
assert ('string' == type(classDesignation))
classDesignation = strtrim(classDesignation)
assert (string.len(classDesignation) >= 2)
assert (string.len(classDesignation) <= 64)
local t = RAID_CLASS_COLORS[classDesignation]
if not t then
return nil
end
return t['r'], t['g'], t['b']
end
local function formatNumber(n)
assert (nil ~= n)
assert ('number' == type(n))
local str
local absn = math.abs(n)
if absn < 1000 then
str = string.format('%d', n)
elseif absn < 1000000 then
str = string.format('%.2f K', n / 1000)
else
str = string.format('%.2f M', n / 1000000)
end
assert (nil ~= str)
assert ('string' == type(str))
return str
end
local function progressBarUpdateText(barFrame, label, unitDesignation, strategy)
assert (barFrame ~= nil)
assert (label ~= nil)
assert (unitDesignation ~= nil)
assert ('string' == type(unitDesignation))
assert (string.len(unitDesignation) >= 2)
assert (string.len(unitDesignation) <= 256)
strategy = strategy or barFrame.strategy
assert (nil ~= strategy)
assert ('UNIT_HEALTH' == strategy or 'UNIT_POWER' == strategy)
local str = nil
--[[ When rendering health, given the unit is a friendly, then show health deficit,
otherwise, given the unit is a hostile, render absolute amount of health remaining. ]]--
if 'UNIT_HEALTH' == strategy then
--[[ FIXME Find a better way to label units as dead, ghost or afk etc,
rather than tying this feature with the health bar. ]]--
if UnitIsCorpse(unitDesignation) then
str = '(Corpse)'
elseif UnitIsGhost(unitDesignation) then
str = '(Ghost)'
elseif UnitIsDead(unitDesignation) then
str = '(Dead)'
elseif UnitIsCharmed(unitDesignation) then
str = '(Charmed)'
elseif not UnitIsConnected(unitDesignation) then
str = '(Offline)'
elseif UnitIsAFK(unitDesignation) then --[[ FIXME AFK text label does not render at appropriate time ]]--
str = '(Away)'
elseif GetReadyCheckStatus(unitDesignation) then
str = GetReadyCheckStatus(unitDesignation)
elseif UnitIsFriend('player', unitDesignation) then
local n = getUnitHealthDeficit(unitDesignation) or 0
if n < 0 then
str = formatNumber(n)
else
str = nil
end
else
local n = UnitHealth(unitDesignation)
str = formatNumber(n)
end
elseif 'UNIT_POWER' == strategy then
local n = UnitPower(unitDesignation) or 0
if n < (UnitPowerMax(unitDesignation) or 0) then
str = formatNumber(n)
elseif UnitIsFriend('player', unitDesignation) and n >= (UnitPowerMax(unitDesignation)) then
str = nil
end
else
assert ('UNIT_HEALTH' == strategy or 'UNIT_POWER' == strategy)
end
label:SetText(str)
end
local function getUnitProgress(unitDesignation, eventCategory)
assert (unitDesignation ~= nil)
assert ('string' == type(unitDesignation))
assert (string.len(unitDesignation) >= 2)
if not UnitExists(unitDesignation) then
return
end
assert (eventCategory ~= nil)
assert ('string' == type(eventCategory))
assert (string.len(eventCategory) >= 2)
local progressRatio
if 'UNIT_HEALTH' == eventCategory then
local a = math.abs(UnitHealth(unitDesignation) or 0)
local b = math.abs(UnitHealthMax(unitDesignation) or 1)
progressRatio = a / b
elseif 'UNIT_MANA' == eventCategory then
local a = math.abs(UnitMana(unitDesignation) or 0)
local b = math.abs(UnitManaMax(unitDesignation) or 1)
progressRatio = a / b
else
local a = math.abs(UnitPower(unitDesignation) or 0)
local b = math.abs(UnitPowerMax(unitDesignation) or 1)
progressRatio = a / b
end
assert (progressRatio ~= nil)
return math.min(math.max(0, progressRatio), 1)
end
local function progressBarUpdateOverlay(barFrame, overlay, unitDesignation, strategy)
assert (barFrame~= nil)
assert (overlay ~= nil)
assert (unitDesignation ~= nil)
assert (strategy~= nil)
if not UnitExists(unitDesignation) then
return
end
local ratio = getUnitProgress(unitDesignation, strategy)
if not ratio then
ratio = 0
elseif 'number' ~= type(ratio) then
ratio = 0
else
ratio = math.min(math.max(0, ratio), 1)
end
--[[ Apply health bar width changes. ]]--
local overlayHeight = overlay:GetHeight()
local barHeight = barFrame:GetHeight()
local marginTop = (barHeight - overlayHeight) / 2
overlay:SetWidth(barFrame:GetWidth())
overlay:SetPoint('BOTTOMLEFT', barFrame, 'BOTTOMLEFT', 0, marginTop)
overlay:SetPoint('TOPRIGHT', barFrame, 'TOPRIGHT', (ratio - 1) * barFrame:GetWidth(), -marginTop)
--[[ FIXME When druid shapeshifts the power bar only assumes the color of the primary resource that is mana,
-- instead of changing depending on the current shape's resource.
-- The number that the text label shows remains correct. ]]--
if 'UNIT_POWER' == strategy then
local colorMap = PowerBarColor
assert (colorMap ~= nil)
assert ('table' == type(colorMap))
local unitPowerTypeIndex = UnitPowerType(unitDesignation)
local powerColor = colorMap[unitPowerTypeIndex]
if powerColor then
assert ('table' == type(powerColor))
local red = powerColor['r']
local green = powerColor['g']
local blue = powerColor['b']
overlay:SetVertexColor(red, green, blue)
end
elseif 'UNIT_HEALTH' == strategy then
local a = math.abs(UnitHealth(unitDesignation) or 0)
local b = math.abs(UnitHealthMax(unitDesignation) or 1)
local r = a / b
local red
local green
local blue
if r > (1 / 2) then
red = 0
green = 1
blue = 0
elseif r > (1 / 3) then
red = 1
green = 1
blue = 0
else
red = 1
green = 0
blue = 0
end
overlay:SetVertexColor(red, green, blue)
else
assert ('UNIT_HEALTH' == strategy or 'UNIT_POWER' == strategy)
end
end
local function progressBarEventProcessor(barFrame, eventCategory)
assert (barFrame ~= nil)
assert (eventCategory ~= nil)
assert ('string' == type(eventCategory))
local unitButton = barFrame:GetParent()
assert (unitButton ~= nil)
local unitDesignation = unitButton:GetAttribute('unit')
assert (unitDesignation ~= nil)
if 1 ~= UnitExists(unitDesignation) then
return
end
local overlay = barFrame.overlay
assert (overlay ~= nil)
local strategy = barFrame.strategy or 'UNIT_HEALTH'
assert (strategy ~= nil)
assert ('string' == type(strategy))
assert (string.len(strategy) >= 1)
barFrame:SetWidth(unitButton:GetWidth())
progressBarUpdateOverlay(barFrame, overlay, unitDesignation, strategy)
--[[ Apply health bar text content. ]]--
local label = barFrame.label
assert (label ~= nil)
progressBarUpdateText(barFrame, label, unitDesignation, strategy)
end
local function createProgressBar(n, unitButton, strategy, width, height, red, green, blue)
assert (n ~= nil)
assert (unitButton ~= nil)
assert ('UNIT_HEALTH' == strategy or 'UNIT_POWER' == strategy)
assert (width ~= nil)
assert ('number' == type(width))
assert (width >= 12)
assert (width <= 288)
assert (height ~= nil)
assert ('number' == type(height))
assert (height >= 12)
assert (height <= 288)
if not n then
n = (unitButton:GetName() or '') .. 'ProgressBarFrame'
end
local barFrame = _G[n]
if barFrame then
return barFrame
end
barFrame = CreateFrame('FRAME', n, unitButton)
barFrame:SetSize(width, height)
local b = barFrame:CreateTexture(n .. 'Overlay', 'OVERLAY')
b:SetWidth(width)
b:SetHeight(math.min(height, 9))
local marginBottom = (height - b:GetHeight()) / 2
b:SetPoint('BOTTOMLEFT', 0, marginBottom)
b:SetPoint('TOPRIGHT', 0, -marginBottom)
b:SetTexture("Interface\\AddOns\\choir\\share\\Minimalist.tga")
if not red then
red = 0
end
if not green then
green = 1
end
if not blue then
blue = 0
end
assert (red >= 0)
assert (red <= 1)
assert (green >= 0)
assert (green <= 1)
assert (blue >= 0)
assert (blue <= 1)
b:SetVertexColor(red, green, blue)
local t = createLabel(barFrame)
barFrame.strategy = strategy
barFrame.label = t
barFrame.overlay = b
barFrame:RegisterEvent('ADDON_LOADED')
barFrame:RegisterEvent('PARTY_CONVERTED_TO_RAID')
barFrame:RegisterEvent('READY_CHECK')
barFrame:RegisterEvent('PARTY_MEMBERS_CHANGED')
barFrame:RegisterEvent('PARTY_MEMBER_DISABLE')
barFrame:RegisterEvent('PARTY_MEMBER_ENABLE')
barFrame:RegisterEvent('PLAYER_ALIVE')
barFrame:RegisterEvent('PLAYER_LOGIN')
barFrame:RegisterEvent('RAID_ROSTER_UPDATE')
if 'UNIT_HEALTH' == strategy then
barFrame:RegisterEvent('UNIT_HEALTH')
elseif 'UNIT_POWER' == strategy then
barFrame:RegisterEvent('UNIT_ENERGY')
barFrame:RegisterEvent('UNIT_MANA')
barFrame:RegisterEvent('UNIT_RAGE')
barFrame:RegisterEvent('UNIT_RUNIC_POWER')
barFrame:RegisterEvent('UNIT_MAXPOWER')
barFrame:RegisterEvent('UNIT_POWER')
else
assert ('UNIT_POWER' == strategy or 'UNIT_HEALTH' == strategy);
end
barFrame:SetScript('OnEvent', progressBarEventProcessor)
progressBarEventProcessor(barFrame, strategy)
return barFrame, t, b
end
local function headerEventProcessor(headerFrame)
assert (headerFrame ~= nil)
local unitButton = headerFrame:GetParent()
assert (unitButton ~= nil)
local unitDesignation = unitButton:GetAttribute('unit')
assert (unitDesignation ~= nil)
if not UnitExists(unitDesignation) then
return
end
local n
local unitName = UnitName(unitDesignation)
if not unitName or string.len(unitName) < 2 then
n = unitDesignation
else
n = unitName
end
local fontWidth = 6
local m = math.floor(headerFrame:GetWidth() / fontWidth)
local sanitizedName = string.sub(n, 1, m)
if string.len(sanitizedName) < string.len(unitName) then
sanitizedName = sanitizedName .. '…'
end
local key = GetBindingKey('CLICK ' .. unitButton:GetName() .. ':LeftButton')
if not key then
key = unitButton:GetAttribute('choirBindingKey')
end
local y
if key then
y = ' <' .. key .. '>'
else
y = ''
end
sanitizedName = sanitizedName .. y
local label = headerFrame.label
assert (label ~= nil)
label:SetText(sanitizedName)
local _, classDesignation = UnitClass(unitDesignation)
local r = 1
local g = 1
local b = 1
if classDesignation then
r, g, b = getClassColor(classDesignation)
end
label:SetTextColor(r, g, b)
end
local function createHeader(unitButton, width, height)
assert (unitButton ~= nil)
assert (width ~= nil)
assert ('number' == type(width))
assert (width >= 12)
assert (width <= 288)
assert (height ~= nil)
assert ('number' == type(height))
assert (height >= 12)
assert (height <= 288)
local n = (unitButton:GetName() or '') .. 'HeaderFrame'
local headerFrame = CreateFrame('FRAME', n, unitButton)
headerFrame:SetSize(width, height)
local t = createLabel(headerFrame)
assert (t ~= nil)
headerFrame.label = t
headerFrame:RegisterEvent('PARTY_CONVERTED_TO_RAID')
headerFrame:RegisterEvent('PARTY_MEMBERS_CHANGED')
headerFrame:RegisterEvent('PLAYER_ENTERING_WORLD')
headerFrame:RegisterEvent('RAID_ROSTER_UPDATE')
headerFrame:SetScript('OnEvent', headerEventProcessor)
assert (headerFrame ~= nil)
return headerFrame
end
local function createInheritanceHandler(unitButton)
assert (unitButton ~= nil)
local n = (unitButton:GetName() or 'Choir') .. 'InheritanceHandler'
local inheritor = CreateFrame('FRAME', n, unitButton, 'SecureHandlerAttributeTemplate')
--[[ When a button's target unit changes, make sure that all children buttons of this button update,
-- to also target the same unit.
-- Spell shortcut feature applied by createSpellShortcut funciton depends on the inheritance handler. ]]--
inheritor:WrapScript(unitButton, 'OnAttributeChanged', [=[
local unitButton = self
local unitDesignation = unitButton:GetAttribute('unit')
if not unitDesignaiton then
return
end
local buttonChildList = unitButton:GetChildList(newtable())
local i = 0
while (i < #buttonChildList) do
i = i + 1
local child = buttonChildList[i]
child:SetAttribute('unit', unitDesignation)
end
]=])
return inheritor
end
local function createUnitButtonTooltip(unitButton)
assert (unitButton ~= nil)
unitButton:SetScript('OnEnter', function(self)
assert (self ~= nil)
local unitDesignation = self:GetAttribute('unit') or 'none'
if unitDesignation then
local tooltip = GameTooltip
tooltip:SetOwner(self, 'ANCHOR_BOTTOMRIGHT')
tooltip:SetUnit(unitDesignation)
end
end)
unitButton:SetScript('OnLeave', function()
GameTooltip:Hide()
end)
end
local function threatWidgetEventProcessor(threatWidget)
assert (threatWidget ~= nil)
local unitButton = threatWidget:GetParent()
assert (unitButton ~= nil)
local u = unitButton:GetAttribute('unit')
assert (u ~= nil)
if not UnitExists(u) then
return
end
local r = 0
local g = 0
local b = 0
local a = 0
local threatStatus = UnitThreatSituation(u)
if threatStatus then
r, g, b = GetThreatStatusColor(threatStatus)
a = 1
end
local artwork = threatWidget.artwork
assert (artwork ~= nil)
artwork:SetVertexColor(r, g, b, a)
end
local function createThreatWidget(unitButton, width, height)
local t = CreateFrame('FRAME', unitButton:GetName() .. 'ThreatFrame', unitButton)
t:SetSize(width, height)
local artwork = t:CreateTexture(t:GetName() .. 'Artwork', 'ARTWORK')
local artworkSize = math.min(width, height)
artwork:SetPoint('BOTTOMLEFT', t, 'BOTTOMLEFT', width / 2 - artworkSize / 2, 0)
artwork:SetPoint('TOPRIGHT', t, 'BOTTOMLEFT', width / 2 + artworkSize / 2, height)
artwork:SetTexture("Interface\\RaidFrame\\UI-RaidFrame-Threat")
t.artwork = artwork
t:RegisterEvent('UNIT_THREAT_SITUATION_UPDATE')
t:RegisterEvent('PLAYER_ENTERING_WORLD')
t:SetScript('OnEvent', threatWidgetEventProcessor)
return t
end
local function createClearcastingSubset(unitButton, targetFilter)
assert (unitButton ~= nil)
assert (targetFilter ~= nil)
assert ('string' == type(targetFilter))
local unitDesignation = unitButton:GetAttribute('unit')
assert (unitDesignation ~= nil)
targetFilter = string.upper(strtrim(targetFilter))
local n = unitButton:GetName() .. 'Clearcasting' .. targetFilter
local width = unitButton:GetWidth()
local buttonSize = 36
local columnQuantity = math.min(math.max(1, math.floor(width / buttonSize)), 36)
local rowQuantity = 1
assert (ClearcastingFrame ~= nil)
local createSubset = ClearcastingFrame.createSubset
assert (createSubset ~= nil)
return createSubset(unitButton, n,
unitDesignation, targetFilter,
columnQuantity, rowQuantity)
end
--[[--
Populate given menuFrame with menu buttons.
The menu buttons that will be added to the given contextual menu
are those that were defined by the baseline game client
for the TargetFrameDropDown, PlayerFrameDropDown and others,
declared in UnitPopupFrames global variable that is a table.
There is an important implicit parameter menuFrame.unit.
The property is set in contextualMenuOnShowCallback.
It is done this way to avoid creating redundant menu frames
for every unit button. Instead, this approach reuses the singleton
menu frame for every of 9*5 menu buttons that the add-on creates.
@function contenxtualMenuOnClickCallback
@arg menuFrame given menu frame, likely ChoirContextualMenu
]]
local function contextualMenuOnClickCallback(menuFrame)
assert (menuFrame ~= nil)
--[[ See https://github.com/Ennie/wow-ui-source/blob/master/FrameXML/UnitPopup.lua ]]--
--[[ See https://www.townlong-yak.com/framexml/live/TargetFrame.lua ]]--
local unitDesignation = menuFrame.unit or 'target'
local menuCategory
local name, id
if UnitIsUnit(unitDesignation, "player") then
menuCategory = "SELF"
elseif UnitIsUnit(unitDesignation, "vehicle") then
menuCategory = "VEHICLE"
elseif UnitIsUnit(unitDesignation, "pet") then
menuCategory = "PET"
elseif UnitIsPlayer(unitDesignation) then
id = UnitInRaid(unitDesignation)
if id then
menuCategory = "RAID_PLAYER"
elseif UnitInParty(unitDesignation) then
menuCategory = "PARTY"
else
if not UnitIsMercenary("player") then
if UnitCanCooperate("player", unitDesignation) then
menuCategory = "PLAYER";
else
menuCategory = "ENEMY_PLAYER"
end
else
if UnitCanAttack("player", unitDesignation) then
menuCategory = "ENEMY_PLAYER"
else
menuCategory = "PLAYER";
end
end
end
else
menuCategory = "TARGET"
name = RAID_TARGET_ICON
end
assert (UnitPopupMenus[menuCategory] ~= nil)
UnitPopup_ShowMenu(menuFrame, menuCategory, unitDesignation, name, id)
end
local function createContextualMenu()
--[[ See https://wowwiki-archive.fandom.com/wiki/UI_Object_UIDropDownMenu ]]--
local menuFrame = CreateFrame('FRAME', 'ChoirContextualMenu', ChoirFrame, 'UIDropDownMenuTemplate')
assert (menuFrame ~= nil)
UIDropDownMenu_Initialize(menuFrame, contextualMenuOnClickCallback, 'MENU')
return menuFrame
end
local function contextualMenuOnShowCallback(targetFrame, unitDesignation, mouseButtonDesignation)
assert (targetFrame ~= nil)
assert (unitDesignation ~= nil)
if mouseButtonDesignation ~= 'RightButton' then
return
end
local menuFrame = ChoirContextualMenu
assert (menuFrame ~= nil)
menuFrame.unit = unitDesignation
ToggleDropDownMenu(1, nil, menuFrame, targetFrame:GetName(), 144, 12)
end
local function rangeWidgetUpdateProcessor(self, elapsedDurationSec)
assert (self ~= nil)
assert (elapsedDurationSec ~= nil)
local updateFrequencyPerSecond = 0.5
local idleDurationSec = self.idleDurationSec or 0
if idleDurationSec > 0 then
self.idleDurationSec = idleDurationSec - elapsedDurationSec
else
self.idleDurationSec = math.abs(updateFrequencyPerSecond)
local unitButton = self:GetParent()
assert (unitButton ~= nil)
local unitDesignation = unitButton:GetAttribute('unit')
assert (unitDesignation ~= nil)
--[[ Change button transparency according to range ]]--
local a = 1
local rangeSpell = ChoirRangeSpellName
--[[ NOTE IsSpellInRange returns either 0, 1 or nil ]]--
if rangeSpell and 1 ~= IsSpellInRange(rangeSpell, unitDesignation) then
a = 0.5
end
unitButton:SetAlpha(a)
end
end
local function createRangeWidget(unitButton)
assert (unitButton ~= nil)
local n = (unitButton:GetName() or '') .. 'RangeWidget'
local w = CreateFrame('FRAME', n, unitButton)
w:SetScript('OnUpdate', rangeWidgetUpdateProcessor)
return w
end
local function createUnitButton(parentFrame, frameName, unit,
someFilterDescriptorFirst, someFilterDescriptorLast, width, height)
assert (parentFrame ~= nil)
assert (frameName ~= nil)
assert (unit ~= nil)
local u = CreateFrame('BUTTON', frameName, parentFrame, 'SecureUnitButtonTemplate')
createBindingKeyHandler(u)
createInheritanceHandler(u)
u:SetAttribute('type1', 'target')
if not width then
width = 16
end
assert (width >= 12 and width <= 288)
if not height then
height = 12
end
assert (height >= 12 and height <= 288)
u:SetSize(width, height)
u:SetAttribute('unit', unit)
RegisterUnitWatch(u)
createUnitButtonTooltip(u)
u:RegisterForClicks('AnyUp')
u:SetAttribute('*type2', 'menu')
SecureUnitButton_OnLoad(u, unit, contextualMenuOnShowCallback);
local roleWidget = createRoleWidget(u)
local headerFrame = createHeader(u, width, 12)
local progressBarMargin = 3
local progressBarWidth = width - (progressBarMargin * 2)
local progressBarHeight = 16
local healthBarFrame = createProgressBar(frameName .. 'HealthBarFrame', u, 'UNIT_HEALTH',
progressBarWidth, progressBarHeight)
local powerBarFrame = createProgressBar(frameName .. 'PowerBarFrame', u, 'UNIT_POWER',
progressBarWidth, progressBarHeight)
local threatWidget = createThreatWidget(u, width, 12)
local rangeWidget = createRangeWidget(u)
assert (rangeWidget ~= nil)
local buffRowFirst
local buffRowLast
if someFilterDescriptorFirst then
buffRowFirst = createClearcastingSubset(u, someFilterDescriptorFirst)
end
if someFilterDescriptorLast then
buffRowLast = createClearcastingSubset(u, someFilterDescriptorLast)
end
local sectionTable = {roleWidget, headerFrame, healthBarFrame, powerBarFrame, threatWidget, buffRowFirst, buffRowLast}
local i = 0
local j = 0
local marginLeft = 3
local marginRight = 3
local y = 0
while (i < #sectionTable) do
i = i + 1
local section = sectionTable[i]
if section then
j = j + 1
width = math.max(width, section:GetWidth())
section:SetPoint('TOPLEFT', marginLeft, y)
section:SetPoint('BOTTOMRIGHT', u, 'TOPRIGHT', -marginRight, y - section:GetHeight())
y = y - section:GetHeight()
end
end
local backdropInfo = {
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true,
tileEdge = true,
tileSize = 8,
edgeSize = 12,
insets = { left = 1, right = 1, top = 1, bottom = 1 },
}
u.backdropInfo = backdropInfo
u:SetBackdrop(backdropInfo)
u:SetBackdropColor(0, 0, 0, 0.8)
u:SetBackdropBorderColor(1, 1, 1)
assert (u ~= nil)
return u
end
local function createSpoilerPaginatorButton(buttonDesignation, spoiler, clickButton, labelText)
assert (buttonDesignation ~= nil)
assert (spoiler ~= nil)
assert (clickButton ~= nil)
assert (labelText ~= nil)
local paginatorButton = CreateFrame('BUTTON', buttonDesignation, spoiler, 'SecureActionButtonTemplate')
paginatorButton:SetSize(36, 24)
paginatorButton:SetPoint('TOPRIGHT', 0, 0)
local artwork = createBackground(paginatorButton)
paginatorButton:SetNormalTexture(artwork)
local label = createLabel(paginatorButton)
paginatorButton:SetFontString(label)
paginatorButton:SetText(labelText)
paginatorButton:SetAttribute('type', 'click')
paginatorButton:SetAttribute('clickbutton', clickButton)
createBindingKeyHandler(paginatorButton)
return paginatorButton
end
local function createSpoilerPaginator(spoiler, previousSpoiler, nextSpoiler)
assert (spoiler ~= nil)
assert (previousSpoiler ~= nil)
assert (nextSpoiler ~= nil)
local padding = 6
local n = spoiler:GetName()
local previousButton = createSpoilerPaginatorButton(n .. 'PreviousButton', spoiler, previousSpoiler, '←')
local nextButton = createSpoilerPaginatorButton(n .. 'NextButton', spoiler, nextSpoiler, '→')
local closeButton = createSpoilerPaginatorButton(n .. 'CloseButton', spoiler, spoiler, '⨯')
previousButton:SetAttribute('choirBindingKey', 'SHIFT-TAB')
nextButton:SetAttribute('choirBindingKey', 'TAB')
closeButton:SetAttribute('choirBindingKey', 'ESCAPE')
closeButton:SetPoint('TOPRIGHT', 0, -padding)
nextButton:SetPoint('TOPRIGHT', -padding - closeButton:GetWidth(), -padding)
previousButton:SetPoint('TOPRIGHT', -padding -nextButton:GetWidth() - padding - closeButton:GetWidth(), -padding)
return previousButton, nextButton, closeButton
end
local function createSpoiler(spoilerParent, spoilerDesignation)
local spoiler = CreateFrame('BUTTON', spoilerDesignation, spoilerParent, 'SecureHandlerClickTemplate')
spoiler:EnableMouse(true)
spoiler:SetAttribute('_onclick', [=[
--[[ Toggle the spoiler on repeated clicks. ]]--
if self:IsShown() then
self:Hide()
return
end
self:Show()
local childTable = self:GetChildList(newtable())
local i = 0
while (i < #childTable) do
i = i + 1
local child = childTable[i]
--[[ If the child is a unit button, take into account the unit's state ]]--
local unitDesignation = child:GetAttribute('unit')
if not unitDesignation then
child:Show()
elseif unitDesignation and UnitExists(unitDesignation) then
child:Show()
end
end
]=])
--[[ Toggle sibling frames, which are other spoilers that contain unit buttons ]]--
--[[ WARNING Make sure not to toggle this frame (self).
-- Otherwise wrap scripts might trigger unexpectedly. ]]--
spoiler:WrapScript(spoiler, 'OnShow', [=[
local parentFrame = self:GetParent()
local siblingTable = parentFrame:GetChildList(newtable())
local i = 0
while (i < #siblingTable) do
i = i + 1
local sibling = siblingTable[i]
if self ~= sibling then
sibling:Hide()
end
end
]=])
spoiler:WrapScript(spoiler, 'OnHide', [=[
self:ClearBindings()
]=])
createBindingKeyHandler(spoiler)
return spoiler
end
local function getButtonBindingKeyExplicit(buttonRef)
assert (buttonRef ~= nil)
local key = GetBindingKey('CLICK ' .. buttonRef:GetName() .. ':LeftButton')
return key
end
local function getButtonBindingKeyDefault(buttonNumber)
assert (buttonNumber ~= nil)
assert ('number' == type(buttonNumber))
buttonNumber = math.abs(math.floor(buttonNumber))
local key
local actionBarSize = 12
if buttonNumber >= 1 and buttonNumber <= actionBarSize then
key = GetBindingKey('ACTIONBUTTON' .. tostring(buttonNumber))
elseif buttonNumber > actionBarSize and buttonNumber <= actionBarSize * 6 then
local r = buttonNumber % actionBarSize
local n = buttonNumber - r * actionBarSize
key = GetBindingKey('MULTIACTIONBAR' .. r .. 'BUTTON' .. n)
else
key = nil
end
return key
end
local function getDefaultShortcutKeyBindingMap()
return {
--[[ Unit button 1 ]]--
{'Q', 'ALT-Q', 'ALT-A', 'SHIFT-A', '1',},
--[[ Unit button 2 ]]--
{'W', 'ALT-W', 'ALT-S', 'SHIFT-S', '2',},
--[[ Unit button 3 ]]--
{'E', 'ALT-E', 'ALT-D', 'SHIFT-D', '3',},
--[[ Unit button 4 ]]--
{'R', 'ALT-R', 'ALT-F', 'SHIFT-F', '4',},
--[[ Unit button 5 ]]--
{'T', 'ALT-T', 'ALT-G', 'SHIFT-G', '5',},
}
end
local function getShortcutBindingKeySuggestion(buttonNumber, spellNumber)
assert (buttonNumber ~= nil)
assert (buttonNumber >= 1 and buttonNumber <= 5)
assert (spellNumber ~= nil)
assert (spellNumber >= 1 and spellNumber <= 5)
local map = ChoirShortcutBindingKeyMap or getDefaultShortcutKeyBindingMap()
local bindingList = map[buttonNumber]
assert ('table' == type(bindingList) and 5 == #bindingList,
'"ChoirShortcutBindingKeyMap" value must be a map of five tables containing keys')
return bindingList[spellNumber]
end
local function getDefaultShortcutSpellNameList()
local _, key = UnitClass('player')
local map = {
['DRUID'] = {
'Abolish Poison',
'Remove Curse',
'Rejuvenation',
'Regrowth',
'Healing Touch',
},
['PALADIN'] = {
'Cleanse',
'Purify',
'Sacred Shield',
'Hand of Freedom',
'Flash of Light',
},
['PRIEST'] = {
'Dispel Magic',
'Abolish Disease',
'Renew',
'Power Word: Shield',
'Flash Heal',
},
['SHAMAN'] = {
'Purge',
'Cleanse Spirit',
'Healing Wave',
'Healing Wave',
'Healing Wave',
},
}
local spellNameList = map[key]
if not spellNameList then
trace('could not load default shortcut spell name list for ' .. tostring(key))
return nil
end
return spellNameList
end
local function getShortcutSpellNameList()
local spellNameList = ChoirShortcutSpellNameList or getDefaultShortcutSpellNameList()
if spellNameList then
assert ('table' == type(spellNameList) and 5 == #spellNameList,
'"ChoirShortcutSpellNameList" value must be a table of five spell names')
return spellNameList
else
trace('could not load default shortcut spell name list for the current character')
return nil
end
end
local function applyUnitButtonSpellShortcutIfPossible(shortcutButton, buttonNumber, spellNumber)
assert (shortcutButton ~= nil)
assert (buttonNumber ~= nil)
assert ('number' == type(buttonNumber))
assert (buttonNumber >= 1)
assert (spellNumber ~= nil)
assert ('number' == type(spellNumber))
assert (spellNumber >= 1)
local shortcutQuantity = 5
local spellSet = getShortcutSpellNameList()
if not spellSet then
trace('empty spell set')
return
end
assert (spellSet ~= nil)
assert ('table' == type(spellSet))
assert (shortcutQuantity == #spellSet)
local key = getShortcutBindingKeySuggestion(buttonNumber, spellNumber)
if not key then
trace('could not find binding key for', buttonNumber, spellNumber)
return
end
assert (key ~= nil)
assert ('string' == type(key))
key = strtrim(key)
assert (string.len(key) >= 1)
assert (string.len(key) <= 256)
shortcutButton:SetAttribute('choirBindingKey', key)
local spellName = spellSet[spellNumber]
if not spellName then
trace('could not find spell for', buttonNumber, spellNumber)
return
end
assert (spellName ~= nil)
assert ('string' == type(spellName))
spellName = strtrim(spellName)
assert (string.len(spellName) >= 2)
assert (string.len(spellName) <= 256)
shortcutButton:SetAttribute('spell', spellName)
end
local function createUnitButtonSpellShortcut(unitButton, buttonNumber)
assert (unitButton ~= nil)
local shortcutQuantity = 5
--[[ The default here is a meaningless placeholder that must be overriden
-- by @function applyUnitButtonSpellShortcutIfPossible.
-- The value exists only to satisfy argument validator
-- so that the button object will be allocated and reused. ]]--
local defaultSpellShortcut = 'Cleanse'
local spellNumber = 0
while (spellNumber < shortcutQuantity) do
spellNumber = spellNumber + 1
local n = unitButton:GetName() .. 'Shortcut' .. tostring(spellNumber)
local b = createSpellShortcut(unitButton, n, defaultSpellShortcut)
applyUnitButtonSpellShortcutIfPossible(b, buttonNumber, spellNumber)
end
end
local function reloadUnitButtonSpellShortcut()
local buttonQuantity = 9 * 5
local shortcutQuantityPerButton = 5
local i = 0
local k = 0
while (i < buttonQuantity) do
i = i + 1
k = k + 1
if k > 5 then
k = 1
end
local j = 0
while (j < shortcutQuantityPerButton) do
j = j + 1
local n = string.format('ChoirUnitButton%dShortcut%d', i, j)
local b = _G[n]
if b then
applyUnitButtonSpellShortcutIfPossible(b, k, j)
else
trace('button does not exist ' .. n)
end
end
end
end
local function createGroup(rootFrame, groupNumber, unitTable)
assert (rootFrame ~= nil)
assert (groupNumber ~= nil)
groupNumber = math.floor(math.abs(groupNumber))
assert ((groupNumber >= 1 and groupNumber <= 8) or unitTable ~= nil)
local groupSize = 5
local u
if unitTable then
u = unitTable
else
local q = 0
u = {}
while (q < groupSize) do
q = q + 1
u[q] = 'raid' .. tostring((groupNumber - 1) * groupSize + q)
end
end
assert (u ~= nil)
assert ('table' == type(u))
assert (#u == groupSize)
local nm = 'ChoirSpoiler'
local spoiler = createSpoiler(rootFrame, nm .. tostring(groupNumber))
local bgr = createBackground(spoiler)
bgr:SetTexture(0, 0, 0, 0.2)
local i = 0
local marginLeft = 0
local padding = 4
local buttonHeight = 12 * 12
local buttonWidth = buttonHeight * 1.62
while (i < #u) do
i = i + 1
local unitDesignation = u[i]
assert (unitDesignation ~= nil)
local memberNumber = (groupNumber - 1) * groupSize + i
local b = createUnitButton(spoiler, 'ChoirUnitButton' .. tostring(memberNumber), unitDesignation,
'HELPFUL', 'HARMFUL', buttonWidth, buttonHeight)
b:SetPoint('BOTTOMLEFT', marginLeft, 0)
marginLeft = marginLeft + b:GetWidth() + padding
spoiler:WrapScript(b, 'OnClick', [=[
--[[ Assume that parent is the spoiler frame
-- which was created with createSpoiler and
-- has required scripts hooked to it ]]--
if 'LeftButton' == button then
local spoilerFrame = self:GetParent()
spoilerFrame:Hide()
end
]=])
b:Hide()
local key = getButtonBindingKeyExplicit(b) or getButtonBindingKeyDefault(i)
if key then
b:SetAttribute('choirBindingKey', key)
end
_G['BINDING_NAME_CLICK ' .. b:GetName() .. ':LeftButton'] = 'Unit ' .. tostring(i)
createUnitButtonSpellShortcut(b, i)
end
spoiler:SetSize(marginLeft, 12 * 20)
spoiler:SetPoint('CENTER', 0, 12 * 6)
local title = createLabel(spoiler)
title:SetPoint('TOPRIGHT', 0, 0)
title:SetPoint('BOTTOMLEFT', spoiler, 'TOPLEFT', spoiler:GetWidth() / 2 - title:GetWidth() / 2, -24)
title:SetText('Group ' .. tostring(groupNumber))
spoiler.text = title
_G['BINDING_NAME_CLICK ' .. spoiler:GetName() .. ':LeftButton'] = 'Group ' .. tostring(groupNumber)
spoiler:Hide()
return spoiler
end
local function arrangeEveryRaidGroupFrame(raidFrame)
assert (raidFrame ~= nil)
local activeRaidGroupQuantity = 0
local maxRowQuantity = 4
local padding = 0
local row = 0
local column = 0
local i = 0
local t = {raidFrame:GetChildren()}
local x
local y
while (i < #t) do
i = i + 1
local raidGroupFrame = t[i]
if raidGroupFrame and raidGroupFrame:IsShown() then
x = column * (raidGroupFrame:GetWidth() + padding)
y = row * (raidGroupFrame:GetHeight() + padding)
raidGroupFrame:SetPoint('BOTTOMLEFT', raidFrame, 'BOTTOMLEFT', x, y)
row = row + 1
if row >= maxRowQuantity then
row = 0
column = column + 1
end
activeRaidGroupQuantity = activeRaidGroupQuantity + 1
end
end
end
local function raidGroupEventProcessor(groupFrame)
assert (groupFrame ~= nil)
local t = {groupFrame:GetChildren()}
local groupExists = false
local i = 0
while (i < #t) do
i = i + 1
local u = t[i]
if UnitExists(u:GetAttribute('unit') or 'none') then
groupExists = true
end
end
if groupExists then
groupFrame:Show()
else
groupFrame:Hide()
end
end
local function partyFrameEventProcessor(playerPartyFrame)
assert (playerPartyFrame ~= nil)
local t = {playerPartyFrame:GetChildren()}
local isRaid = 1 == UnitPlayerOrPetInRaid('player')
if isRaid then
playerPartyFrame:Hide()
else
playerPartyFrame:Show()
end
local i = 0
while (i < #t) do
i = i + 1
local p = t[i]
if isRaid then
UnregisterUnitWatch(p)
else
RegisterUnitWatch(p)
end
end
end
local function createRaidGroupLabel(groupFrame, groupNumber)
assert (groupFrame ~= nil)
assert (groupNumber >= 1 and groupNumber <= 8 + 1)
local labelWidth = 60
local groupLabel = groupFrame:CreateFontString(groupFrame:GetName() .. 'Label', 'OVERLAY')
local fontObject = ArimoRegular16 or CousineRegular16 or NumberFont_OutlineThick_Mono_Small
assert (fontObject ~= nil)
groupLabel:SetFontObject(fontObject)
groupLabel:SetPoint('BOTTOMLEFT', groupFrame, 'BOTTOMLEFT', 0, 0)
groupLabel:SetPoint('TOPRIGHT', groupFrame, 'TOPLEFT', labelWidth, 0)
local labelContent
--[[ FIXME Update hotkey label at runtime without needing UI /reload. ]]--
local hotkey = GetBindingKey('CLICK ChoirSpoiler' .. tostring(groupNumber) .. ':LeftButton')
if hotkey then
labelContent = _G['BINDING_NAME_CLICK ChoirSpoiler' .. tostring(groupNumber) .. ':LeftButton']
labelContent = labelContent .. ' <' .. tostring(hotkey) .. '>'
else
labelContent = tostring(groupNumber)
end
groupLabel:SetText(labelContent)
return groupLabel
end
local function createRaidGroupFrame(raidFrame, groupNumber, unitSetOverride)
assert (raidFrame ~= nil)
assert (groupNumber ~= nil)
groupNumber = math.floor(groupNumber)
assert ('number' == type(groupNumber))
assert (groupNumber >= 1 and groupNumber <= 8 + 1)
local maxPartySize = 5
if unitSetOverride ~= nil then
assert ('table' == type(unitSetOverride))
assert (#unitSetOverride == maxPartySize)
end
local buttonHeight = 12 * 9
local buttonWidth = 12 * 10
local padding = 6
local labelWidth = 60
local unitSet
if unitSetOverride ~= nil then
unitSet = unitSetOverride
else
unitSet = {}
local p = 0
while (p < maxPartySize) do
p = p + 1
unitSet[p] = 'raid' .. tostring((groupNumber - 1) * maxPartySize + p)
end
end
local groupFrame = CreateFrame('FRAME', 'ChoirRaidGroupFrame' .. tostring(groupNumber), raidFrame)
groupFrame:SetSize(labelWidth + maxPartySize * (buttonWidth + padding), buttonHeight + padding)
createRaidGroupLabel(groupFrame, groupNumber)
local i = 0
while (i < #unitSet) do
i = i + 1
local unitDesignation = unitSet[i]
assert (unitDesignation ~= nil)
local n = groupFrame:GetName() .. 'RaidUnitButton' .. tostring(i)
local b = createUnitButton(groupFrame, n, unitDesignation,
nil, 'HARMFUL', buttonWidth, buttonHeight)
b:SetPoint('BOTTOMLEFT', labelWidth + (i - 1) * (padding + buttonWidth), 0)
end
groupFrame:SetSize(labelWidth + maxPartySize * (buttonWidth + padding),
buttonHeight + padding)
groupFrame:RegisterEvent('PARTY_CONVERTED_TO_RAID')
groupFrame:RegisterEvent('PARTY_MEMBERS_CHANGED')
groupFrame:RegisterEvent('PLAYER_ALIVE')
groupFrame:RegisterEvent('RAID_ROSTER_UPDATE')
groupFrame:RegisterEvent('UPDATE_BATTLEFIELD_SCORE')
groupFrame:RegisterEvent('ADDON_LOADED')
groupFrame:SetScript('OnEvent', raidGroupEventProcessor)
raidGroupEventProcessor(groupFrame)
return groupFrame
end
local function raidFrameEventProcessor(raidFrame)
assert (raidFrame ~= nil)
arrangeEveryRaidGroupFrame(raidFrame)
end
local function raidFrameDisable(raidFrame)
assert (raidFrame ~= nil)
raidFrame:UnregisterAllEvents()
raidFrame:SetScript('OnEvent', nil)
raidFrame:Hide()
local partyFrameDisabler = _G['ChoirNativePartyFrameDisabler']
if partyFrameDisabler then
ChoirNativePartyFrameDisabler:Hide()
ShowPartyFrame()
PlayerFrame:Show()
end
end
local function raidFrameEnable(raidFrame)
assert (raidFrame ~= nil)
local x = ChoirConfRaidX or 0
x = math.min(math.max(0, x), UIParent:GetWidth())
local y = ChoirConfRaidY or 0
y = math.min(math.max(0, y), UIParent:GetHeight())
raidFrame:SetPoint('BOTTOMLEFT', x, y)
raidFrame:RegisterEvent('PARTY_CONVERTED_TO_RAID')
raidFrame:RegisterEvent('PARTY_MEMBERS_CHANGED')
raidFrame:RegisterEvent('RAID_ROSTER_UPDATE')
raidFrame:SetScript('OnEvent', raidFrameEventProcessor)
raidFrame:Show()
arrangeEveryRaidGroupFrame(raidFrame)
local partyFrameDisabler = _G['ChoirNativePartyFrameDisabler']
if partyFrameDisabler then
ChoirNativePartyFrameDisabler:Show()
HidePartyFrame()
PlayerFrame:Hide()
end
end
local function raidFrameToggle(raidFrame)
assert (raidFrame ~= nil)
if ChoirConfRaidFlag then
raidFrameEnable(raidFrame)
else
raidFrameDisable(raidFrame)
end
end
local function createRaidFrame(rootFrame, spoilerHolder)
assert (rootFrame ~= nil)
assert (spoilerHolder ~= nil)
--[[local maxPartySize = 5]]--
local maxSubgroupQuantity = 8
local groupWidth = 0
local groupHeight = 0
local raidFrame = CreateFrame('FRAME', 'ChoirRaidFrame', rootFrame)
local j = 0
while (j < maxSubgroupQuantity) do
j = j + 1
local g = createRaidGroupFrame(raidFrame, j)
groupWidth = math.max(groupWidth, g:GetWidth())
groupHeight = math.max(groupHeight, g:GetHeight())
end
--[[ NOTE Appearance of the party frame is conditional, only shown outside of raid.
-- Therefore corner case code is implemented. ]]--
local partyUnitSet = {'player', 'party1', 'party2', 'party3', 'party4'}
local playerPartyFrame = createRaidGroupFrame(raidFrame, maxSubgroupQuantity + 1, partyUnitSet)
playerPartyFrame:SetScript('OnEvent', partyFrameEventProcessor)
partyFrameEventProcessor(playerPartyFrame)
groupWidth = math.max(groupWidth, playerPartyFrame:GetWidth())
groupHeight = math.max(groupHeight, playerPartyFrame:GetHeight())
local maxColumnQuantity = 2
local maxRowQuantity = 4
assert (maxColumnQuantity * maxRowQuantity == maxSubgroupQuantity)
raidFrame:SetSize(groupWidth * maxColumnQuantity,
groupHeight * maxRowQuantity)
raidFrame:RegisterEvent('PARTY_CONVERTED_TO_RAID')
raidFrame:RegisterEvent('PARTY_MEMBERS_CHANGED')
raidFrame:RegisterEvent('RAID_ROSTER_UPDATE')
raidFrame:SetScript('OnEvent', raidFrameEventProcessor)
raidFrame:SetScript('OnShow', function(self)
--[[ NOTE Ensure that raid frame will always be hidden
-- given appropriate add-on configuration setting. ]]--
if not ChoirConfRaidFlag then
self:Hide()
end
end)
raidFrameToggle(raidFrame)
--[[ WARNING For some bizzare reason, possibly related to concurrency,
-- the raid toggling initialization __must__ be called here,
-- and not from another function for modularization.
-- This is probably an indication of a larger problem and
-- the lack of understanding of how exactly secure handlers work.
-- However, this approach does solve the problem that caused
-- the frames to behave unexpectedly and even crash the client.
]]--
--[[ Given any spoiler is shown, then hide the raid frame. ]]--
--[[ Given all spoilers are hidden, show the raid frame. ]]--
local spoilerList = {spoilerHolder:GetChildren()}
local p = 0
while (p < #spoilerList) do
p = p + 1
local spoiler = spoilerList[p]
local handler = CreateFrame('FRAME', 'ChoirRaidFrameToggleHandler' .. tostring(p),
raidFrame, 'SecureHandlerShowHideTemplate')
spoiler:SetFrameRef('ChoirRaidFrame', raidFrame)
handler:WrapScript(spoiler, 'OnShow', [=[
local raidFrame = self:GetFrameRef('ChoirRaidFrame')
if raidFrame then
raidFrame:Hide()
end
]=])
handler:WrapScript(spoiler, 'OnHide', [=[
local raidFrame = self:GetFrameRef('ChoirRaidFrame')
raidFrame:Show()
local spoilerHolder = self:GetParent()
local siblingList = spoilerHolder:GetChildList(newtable())
local i = 0
while (i < #siblingList) do
i = i + 1
local sibling = siblingList[i]
if sibling:IsShown() then
raidFrame:Hide()
break
end
end
]=])
end
return raidFrame
end
local function getRangeSpellNameSuggestion()
local _, classDesignation = UnitClass('player')
assert (classDesignation ~= nil)
assert ('string' == type(classDesignation))
classDesignation = strtrim(classDesignation)
assert (string.len(classDesignation) >= 2)
assert (string.len(classDesignation) <= 64)
local map = {
['DRUID'] = {'Cure Poison', 'Healing Touch'},
['PALADIN'] = {'Cleanse', 'Purify', 'Holy Light'},
['PRIEST'] = {'Dispel Magic', 'Cure Disease', 'Lesser Heal'},
['SHAMAN'] = {'Healing Wave'},
}
local t = map[classDesignation]
if not t then
return
end
assert (t ~= nil)
assert ('table' == type(t))
assert (#t >= 1)
local s = nil
local i = 0
while (i < #t) do
i = i + 1
local candidate = t[i]
assert (candidate ~= nil)
local spellName = GetSpellInfo(candidate)
if spellName then
s = spellName
break
end
end
return s
end
local function initRangeSpellName(rootFrame)
local f = CreateFrame('FRAME', rootFrame:GetName() .. 'RangeSpellFrame')
f:SetScript('OnEvent', function()
ChoirRangeSpellName = getRangeSpellNameSuggestion()
end)
f:RegisterEvent('SPELLS_CHANGED')
local s = ChoirRangeSpellName
if s then
assert (s ~= nil)
assert ('string' == type(s))
s = strtrim(s)
assert (string.len(s) >= 2)
assert (string.len(s) <= 256)
else
s = getRangeSpellNameSuggestion()
end
ChoirRangeSpellName = s
return s
end
local function initSpoiler(rootFrame, contextualMenu)
assert (rootFrame ~= nil)
--[[ NOTE Unit buttons require unit contextual menu to be initialized ]]--
assert (contextualMenu ~= nil)
--[[ WARNING All mutually exclusive frames must be placed under the same parent,
-- for the spoiler toggling to work correctly.
-- All siblings of spoiler frames will be toggled, which may not have been the intention. ]]--
local spoilerHolder = CreateFrame('FRAME', 'ChoirSpoilerRootFrame', rootFrame)
spoilerHolder:SetAllPoints()
local t = {}
local i = 0
while (i < 8) do
i = i + 1
t[i] = createGroup(spoilerHolder, i)
end
--[[ TODO Generalize the interface to be used with any kind of child frames,
-- especially nested spoilers and spell buttons. ]]--
--[[ NOTE To get a saved variables kind of table from a restricted frame snippet, use ```
-- local env = GetManagedEnvironment(headerRef)
-- local t = env['MySavedVariableTable']
-- ```
-- See http://www.iriel.org/wow/docs/SecureHeadersGuide-4.0-r1.pdf ]]--
local spoilerParty = createGroup(spoilerHolder, 9, {'player', 'party1', 'party2', 'party3', 'party4'})
spoilerParty.text:SetText('Party')
_G['BINDING_NAME_CLICK ' .. spoilerParty:GetName() .. ':LeftButton'] = 'Party'
t[i + 1] = spoilerParty
i = 0
while (i < #t) do
local p = i
if p < 1 then
p = #t
end
i = i + 1
local n = i + 1
if n > #t then
n = 1
end
createSpoilerPaginator(t[i], t[p], t[n])
end
BINDING_HEADER_CHOIR = 'Choir'
return spoilerHolder
end
local function initRaidFrame(rootFrame, spoilerHolder, contextualMenu)
--[[ NOTE Unit buttons require unit contextual menu to be initialized ]]--
assert (contextualMenu ~= nil)
return createRaidFrame(rootFrame, spoilerHolder)
end
local function readConfShortcutSpellNameList(spellEditBoxList)
local maxShortcutQuantityPerUnit = 5
assert (spellEditBoxList ~= nil)
assert ('table' == type(spellEditBoxList))
assert (maxShortcutQuantityPerUnit == #spellEditBoxList)
local spellNameList = {}
local w = 0
while (w < #spellEditBoxList) do
w = w + 1
local inputBox = spellEditBoxList[w]
assert (inputBox ~= nil)
local spellName = inputBox:GetText()
assert (spellName ~= nil)
assert ('string' == type(spellName))
spellName = strtrim(spellName)
assert (string.len(spellName) >= 2 and string.len(spellName) <= 144)
if not GetSpellInfo(spellName) then
trace('could not find spell by the name of "' .. spellName .. '"')
end
spellNameList[w] = spellName
end
assert (spellNameList ~= nil)
assert ('table' == type(spellNameList))
assert (maxShortcutQuantityPerUnit == #spellNameList)
return spellNameList
end
local function readConfShortcutBindingKeyMap(shortcutEditBoxMap)
local maxUnitQuantityPerSpoiler = 5
local maxShortcutQuantityPerUnit = 5
assert (shortcutEditBoxMap ~= nil)
assert ('table' == type(shortcutEditBoxMap))
assert (maxUnitQuantityPerSpoiler == #shortcutEditBoxMap)
local keyMap = {}
local p = 0
while (p < maxUnitQuantityPerSpoiler) do
p = p + 1
local editBoxList = shortcutEditBoxMap[p]
assert (editBoxList ~= nil)
assert ('table' == type(editBoxList))
assert (maxShortcutQuantityPerUnit == #editBoxList)
local bindingList = {}
local q = 0
while (q < maxShortcutQuantityPerUnit) do
q = q + 1
local editBox = editBoxList[q]
assert (editBox ~= nil)
local binding = editBox:GetText()
assert (binding ~= nil)
assert ('string' == type(binding))
binding = strtrim(binding)
assert (string.len(binding) >= 1 and string.len(binding) <= 144)
bindingList[q] = binding
end
keyMap[p] = bindingList
end
assert (keyMap ~= nil)
assert ('table' == type(keyMap))
assert (maxUnitQuantityPerSpoiler == #keyMap)
return keyMap
end
local function applyConfSpellShortcutFactory(shortcutEditBoxMap, spellEditBoxList)
local maxUnitQuantityPerSpoiler = 5
local maxShortcutQuantityPerUnit = 5
assert (shortcutEditBoxMap ~= nil)
assert ('table' == type(shortcutEditBoxMap))
assert (maxUnitQuantityPerSpoiler == #shortcutEditBoxMap)
assert (spellEditBoxList ~= nil)
assert ('table' == type(spellEditBoxList))
assert (maxShortcutQuantityPerUnit == #spellEditBoxList)
return function(bindingKeyFrame)
assert (bindingKeyFrame ~= nil)
local keyMap = readConfShortcutBindingKeyMap(shortcutEditBoxMap)
if keyMap then
assert (keyMap ~= nil)
assert ('table' == type(keyMap))
assert (maxUnitQuantityPerSpoiler == #keyMap)
ChoirShortcutBindingKeyMap = keyMap
end
local spellNameList = readConfShortcutSpellNameList(spellEditBoxList)
if spellNameList then
assert (spellNameList ~= nil)
assert ('table' == type(spellNameList))
assert (maxShortcutQuantityPerUnit == #spellNameList)
ChoirShortcutSpellNameList = spellNameList
else
trace('could not read shortcut spell name list from interface options')
end
--[[ NOTE Reload the GUI to force update override bindings on secure spell buttons. ]]--
--[[ReloadUI()]]--
reloadUnitButtonSpellShortcut()
end
end
local function cancelConfSpellShortcutFactory(shortcutEditBoxMap, spellEditBoxList)
local maxUnitQuantityPerSpoiler = 5
local maxShortcutQuantityPerUnit = 5
assert (shortcutEditBoxMap ~= nil)
assert ('table' == type(shortcutEditBoxMap))
assert (maxUnitQuantityPerSpoiler == #shortcutEditBoxMap)
assert (spellEditBoxList ~= nil)
assert ('table' == type(spellEditBoxList))
assert (maxShortcutQuantityPerUnit == #spellEditBoxList)
return function(bindingKeyFrame)
assert (bindingKeyFrame ~= nil)
local spellList = getShortcutSpellNameList()
assert (spellList ~= nil)
assert (maxShortcutQuantityPerUnit == #spellList)
local i = 0
while (i < maxUnitQuantityPerSpoiler) do
i = i + 1
local spellEditBox = spellEditBoxList[i]
assert (spellEditBox ~= nil)
spellEditBox:SetText(spellList[i])
spellEditBox:SetCursorPosition(0)
local j = 0
while (j < maxShortcutQuantityPerUnit) do
j = j + 1
local editBox = shortcutEditBoxMap[i][j]
assert (editBox ~= nil)
local binding = getShortcutBindingKeySuggestion(i, j)
editBox:SetText(binding)
--[[ WARNING Reset cursor position on every refresh to make sure updated text is visible. ]]--
editBox:SetCursorPosition(0)
end
end
end
end
local function initConfSpellShortcut(confFrame)
assert (confFrame ~= nil)
local maxUnitQuantityPerSpoiler = 5
local maxShortcutQuantityPerUnit = 5
if not (ChoirShortcutSpellNameList ~= nil and
'table' == type(ChoirShortcutSpellNameList) and
maxShortcutQuantityPerUnit == #ChoirShortcutSpellNameList) then
ChoirShortcutSpellNameList = getDefaultShortcutSpellNameList()
end
if not (ChoirShortcutBindingKeyMap ~= nil and
'table' == type(ChoirShortcutBindingKeyMap) and
maxUnitQuantityPerSpoiler == #ChoirShortcutBindingKeyMap) then
ChoirShortcutBindingKeyMap = getDefaultShortcutKeyBindingMap()
end
local bindingKeyFrame = CreateFrame('FRAME', 'ChoirConfSpellShortcutFrame', confFrame)
bindingKeyFrame.name = 'Spell shortcut'
bindingKeyFrame.parent = confFrame.name
local i = 0
local shortcutEditBoxMap = {}
local spellEditBoxList = {}
local marginBottom = 144
local padding = 8
local marginLeft = 24 * 2 + padding
while (i < maxUnitQuantityPerSpoiler) do
i = i + 1
local unitLabel = bindingKeyFrame:CreateFontString(bindingKeyFrame:GetName() .. 'UnitText' .. tostring(i))
unitLabel:SetFontObject(GameFontNormal)
unitLabel:SetSize(marginLeft - padding, 24)
unitLabel:SetText('Unit ' .. tostring(i))
unitLabel:SetPoint('BOTTOMLEFT', padding, marginBottom + (i - 1) * unitLabel:GetHeight())
local editBoxList = {}
local j = 0
while (j < maxShortcutQuantityPerUnit) do
j = j + 1
local n = 'ChoirConfShortcutUnit' .. tostring(i) .. 'Spell' .. tostring(j) .. 'EditBox'
local editBox = CreateFrame('EDITBOX', n, bindingKeyFrame, 'InputBoxTemplate')
editBox:SetSize(12 * 5, 24)
local editBoxX = marginLeft + (j - 1) * (editBox:GetWidth() + padding)
local editBoxY = marginBottom + (i - 1) * editBox:GetHeight()
editBox:SetPoint('BOTTOMLEFT', editBoxX, editBoxY)
editBox:SetAutoFocus(false)
editBox:SetCursorPosition(0)
editBoxList[j] = editBox
end
local m = 'ChoirConfSpellShortcutEditBox' .. tostring(i)
local spellEditBox = CreateFrame('EDITBOX', m, bindingKeyFrame, 'InputBoxTemplate')
spellEditBox:SetSize(12 * 5, 24)
local spellEditBoxX = marginLeft + (i - 1) * (spellEditBox:GetWidth() + padding)
local spellEditBoxY = marginBottom + 24 * 5
spellEditBox:SetPoint('BOTTOMLEFT', spellEditBoxX, spellEditBoxY)
spellEditBox:SetAutoFocus(false)
spellEditBox:SetCursorPosition(0)
shortcutEditBoxMap[i] = editBoxList
spellEditBoxList[i] = spellEditBox
end
local header = bindingKeyFrame:CreateFontString(bindingKeyFrame:GetName() .. 'HeaderText')
header:SetJustifyH('LEFT')
header:SetJustifyV('TOP')
header:SetFontObject(GameFontNormalLarge)
header:SetSize(144, 24)
header:SetPoint('TOPLEFT', 16, -16)
header:SetText(bindingKeyFrame.name)
local description = bindingKeyFrame:CreateFontString(bindingKeyFrame:GetName() .. 'ParagraphText')
description:SetJustifyH('LEFT')
description:SetJustifyV('TOP')
description:SetFontObject(GameFontWhite)
description:SetSize(386, 24 * 4)
description:SetPoint('TOPLEFT', 16, -16 - header:GetHeight())
description:SetText('Spell shortcut is a way to apply a specific spell on a unit ' ..
'with a hot key press, given that unit spoiler is present.\n\n' ..
'First row of edit boxes in the table below contain spell names ' ..
'that are available to be cast. ' ..
'Every row of edit boxes below the first contain key bindings ' ..
'with which a spell can be cast on a specific unit. ' ..
'Every row after the first corresponds to a single unit. ' ..
'Every column corresponds to a spell. ')
bindingKeyFrame.shortcutEditBoxMap = shortcutEditBoxMap
bindingKeyFrame.spellEditBoxList = spellEditBoxList
local applyConfSpellShortcut = applyConfSpellShortcutFactory(shortcutEditBoxMap, spellEditBoxList)
assert (applyConfSpellShortcut ~= nil)
local cancelConfSpellShortcut = cancelConfSpellShortcutFactory(shortcutEditBoxMap, spellEditBoxList)
assert (cancelConfSpellShortcut ~= nil)
bindingKeyFrame.okay = applyConfSpellShortcut
bindingKeyFrame.cancel = cancelConfSpellShortcut
bindingKeyFrame.refresh = function(self)
cancelConfSpellShortcut(self)
end
bindingKeyFrame.default = function()
ChoirShortcutSpellNameList = getDefaultShortcutSpellNameList()
ChoirShortcutBindingKeyMap = getDefaultShortcutKeyBindingMap()
--[[ReloadUI()]]--
reloadUnitButtonSpellShortcut()
--[[ NOTE Refresh callback is executed implicitly here. ]]--
end
return bindingKeyFrame
end
local function applyConfRaidFrameFactory(raidFrame, confRaidCheckButton, confRaidXEditBox, confRaidYEditBox)
assert (raidFrame ~= nil)
assert (confRaidCheckButton ~= nil)
assert (confRaidXEditBox ~= nil)
assert (confRaidYEditBox ~= nil)
return function()
local x = confRaidXEditBox:GetNumber() or 0
x = math.min(math.max(0, x), UIParent:GetWidth())
local y = confRaidYEditBox:GetNumber() or 0
y = math.min(math.max(0, y), UIParent:GetHeight())
local flag = false
if confRaidCheckButton:GetChecked() then
flag = true
end
ChoirConfRaidFlag = flag
ChoirConfRaidX = x
ChoirConfRaidY = y
raidFrame:SetPoint('BOTTOMLEFT', x, y)
raidFrameToggle(raidFrame)
end
end
local function cancelConfRaidFrameFactory(raidFrame, confRaidCheckButton, confRaidXEditBox, confRaidYEditBox)
assert (raidFrame ~= nil)
assert (confRaidCheckButton ~= nil)
assert (confRaidXEditBox ~= nil)
assert (confRaidYEditBox ~= nil)
return function()
local x = ChoirConfRaidX or 0
x = math.min(math.max(0, x), UIParent:GetWidth())
local y = ChoirConfRaidY or 0
y = math.min(math.max(0, y), UIParent:GetHeight())
local flag = false
if ChoirConfRaidFlag then
flag = true
end
ChoirConfRaidFlag = flag
ChoirConfRaidX = x
ChoirConfRaidY = y
if flag then
confRaidCheckButton:SetChecked(true)
else
confRaidCheckButton:SetChecked(false)
end
confRaidXEditBox:SetNumber(x)
confRaidXEditBox:SetCursorPosition(0)
confRaidYEditBox:SetNumber(y)
confRaidYEditBox:SetCursorPosition(0)
end
end
local function initConfRaidFrame(confFrame, raidFrame)
assert (confFrame ~= nil)
assert (raidFrame ~= nil)
local marginBottom = 144
local marginLeft = 16
local padding = 6
local confRaidFrame = CreateFrame('FRAME', 'ChoirConfRaidFrame', confFrame)
confRaidFrame:SetSize(144 * 3, 144)
confRaidFrame:SetPoint('BOTTOMLEFT', 0, 0)
local n0 = confRaidFrame:GetName() .. 'RaidFlagCheckButton'
local n0Width = 24 * 6
local confRaidCheckButton = CreateFrame('CHECKBUTTON', n0, confRaidFrame, 'ChatConfigCheckButtonTemplate');
confRaidCheckButton:SetPoint('BOTTOMLEFT', marginLeft, marginBottom)
local n0text = _G[n0 .. 'Text']
n0text:SetText('Raid Frame')
local n1 = confRaidFrame:GetName() .. 'RaidFrameXEditBox'
local confRaidXEditBox = CreateFrame('EDITBOX', n1, confRaidFrame, 'InputBoxTemplate')
confRaidXEditBox:SetSize(12 * 4, 24)
confRaidXEditBox:SetPoint('BOTTOMLEFT', marginLeft + n0Width + padding, marginBottom)
confRaidXEditBox:SetAutoFocus(false)
confRaidXEditBox:SetCursorPosition(0)
local n2 = confRaidFrame:GetName() .. 'RaidFrameYEditBox'
local confRaidYEditBox = CreateFrame('EDITBOX', n2, confRaidFrame, 'InputBoxTemplate')
confRaidYEditBox:SetSize(12 * 4, 24)
local n2x = marginLeft + n0Width + confRaidXEditBox:GetWidth() + padding * 2
local n2y = marginBottom
confRaidYEditBox:SetPoint('BOTTOMLEFT', n2x, n2y)
confRaidYEditBox:SetAutoFocus(false)
confRaidYEditBox:SetCursorPosition(0)
local okay = applyConfRaidFrameFactory(raidFrame, confRaidCheckButton, confRaidXEditBox, confRaidYEditBox)
local cancel = cancelConfRaidFrameFactory(raidFrame, confRaidCheckButton, confRaidXEditBox, confRaidYEditBox)
confFrame.okay = okay
confFrame.cancel = cancel
confFrame.refresh = cancel
local default = function()
ChoirConfRaidFlag = true
ChoirConfRaidX = 256
ChoirConfRaidY = 768 - 48 * 8 / 2
--[[ NOTE Refresh callback is executed implicitly here. ]]--
end
confFrame.default = default
if nil == ChoirConfRaidFlag and nil == ChoirConfRaidX and nil == ChoirConfRaidY then
default()
end
cancel()
okay()
return confRaidFrame
end
local function initConf(rootFrame, raidFrame)
assert (rootFrame ~= nil)
local confFrame = CreateFrame('FRAME', 'ChoirConfFrame', rootFrame)
confFrame.name = GetAddOnMetadata('Choir', 'Title') or 'Choir'
local h1 = confFrame:CreateFontString()
h1:SetJustifyH('LEFT')
h1:SetJustifyV('TOP')
h1:SetFontObject(GameFontNormalLarge)
h1:SetSize(144, 24)
h1:SetPoint('TOPLEFT', 16, -16)
h1:SetText(confFrame.name .. '-' .. (GetAddOnMetadata('Choir', 'Version') or '0'))
local p1 = confFrame:CreateFontString()
p1:SetJustifyH('LEFT')
p1:SetJustifyV('TOP')
p1:SetFontObject(GameFontWhite)
p1:SetSize(386, 24 * 12)
p1:SetPoint('TOPLEFT', 16, -16 - h1:GetHeight())
p1:SetText('Choir add-on enhances targeting, raid frames and spell buttons. ' ..
'The main purpose of the add-on is to allow the user to target units and ' ..
'to cast spells with a combination of key presses. ' ..
'It is expected to be used only by healers. ' ..
'It is intended as an alternative to mouseover macros.\n\n' ..
'For example, to target a unit with a combination of key presses do the following. ' ..
'First, bind a key to player party spoiler in the native key bindings menu. ' ..
'Then, join a party. Finally, press the hot key that was bound to the player party. ' ..
'A spoiler that is a contextual menu will open in the middle of the screen. ' ..
'Every button under the spoiler corresponds to a party member. ' ..
'To target a party member, press the key that is bound to the unit button. ' ..
'To learn what key is bound to the unit button, ' ..
'read the hint on the unit button itself, enclosed in the square brackets.')
InterfaceOptions_AddCategory(confFrame)
initConfRaidFrame(confFrame, raidFrame)
local bindingKeyFrame = initConfSpellShortcut(confFrame)
assert (bindingKeyFrame ~= nil)
InterfaceOptions_AddCategory(bindingKeyFrame)
return confFrame, bindingKeyFrame
end
local function initContextualMenu()
return createContextualMenu()
end
local function initNativePartyFrameDisabler()
local partyFrameDisabler = CreateFrame('FRAME', 'ChoirNativePartyFrameDisabler', nil, 'SecureHandlerShowHideTemplate')
--[[ Use secure handler feature to hide the native party frame.
This way the runtime configuration also works in combat. ]]--
--[[ In the script, "owner" variable refers to "ChoirNativePartyFrameDisabler" frame.
"self" variable refers to either "PlayerFrame" native unit frame,
or any of the party member frames.]]--
local script = [=[
if owner:IsShown() then
self:Hide()
end
]=]
local i = 0
while (i < MAX_PARTY_MEMBERS) do
i = i + 1
local p = _G['PartyMemberFrame' .. i];
assert (p ~= nil)
partyFrameDisabler:WrapScript(p, 'OnShow', script)
if ChoirConfRaidFlag then
p:Hide()
end
end
partyFrameDisabler:WrapScript(PlayerFrame, 'OnShow', script)
if ChoirConfRaidFlag then
PlayerFrame:Hide()
partyFrameDisabler:Show()
else
partyFrameDisabler:Hide()
end
end
local function init(rootFrame)
assert (rootFrame ~= nil)
local locale = GetLocale()
assert (locale == 'enGB' or locale == 'enUS', 'requires English localization')
rootFrame:UnregisterAllEvents()
rootFrame:SetAllPoints()
initRangeSpellName(rootFrame)
local contextualMenu = initContextualMenu()
local spoilerHolder = initSpoiler(rootFrame, contextualMenu)
local raidFrame = initRaidFrame(rootFrame, spoilerHolder, contextualMenu)
initConf(rootFrame, raidFrame)
initNativePartyFrameDisabler(rootFrame)
trace('init')
end
local function main()
local rootFrame = CreateFrame('FRAME', 'ChoirFrame', UIParent)
--[[ NOTE The add-on requires key bindings data.
-- Therefore trigger the add-on initialization after the key bindings were loaded. ]]--
rootFrame:RegisterEvent('PLAYER_LOGIN')
rootFrame:SetScript('OnEvent', init)
end
main()