/choir.lua (27ff7289ca268ef0533ffe891f0384cee4feba24) (65959 bytes) (mode 100644) (type blob)

--[[ 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()


Mode Type Size Ref File
100644 blob 9 b72f9be2042aa06db9cb3a6f533f953b5ce29901 .gitignore
100644 blob 2008 a7e33607385f2bc07c59ccff1406362cc3810bb6 .luacheckrc
100644 blob 934 73158d32fbccb1838992b04c2901ba6348410e3d bindings.xml
100644 blob 65959 27ff7289ca268ef0533ffe891f0384cee4feba24 choir.lua
100644 blob 306 6a74063e7c1bf51bf12a1126fa423c637338e50e choir.toc
040000 tree - 96550a3edc946ae323b85f3d394ea4cc6b0718b3 share
Hints:
Before first commit, do not forget to setup your git environment:
git config --global user.name "your_name_here"
git config --global user.email "your@email_here"

Clone this repository using HTTP(S):
git clone https://rocketgit.com/user/vrtc/choir

Clone this repository using ssh (do not forget to upload a key first):
git clone ssh://rocketgit@ssh.rocketgit.com/user/vrtc/choir

Clone this repository using git:
git clone git://git.rocketgit.com/user/vrtc/choir

You are allowed to anonymously push to this repository.
This means that your pushed commits will automatically be transformed into a merge request:
... clone the repository ...
... make some changes and some commits ...
git push origin main