/choir.lua (5ac0a5f77b4b6bb0af9dd3258e59010596a98408) (10167 bytes) (mode 100644) (type blob)

local function getDefaultUnitButtonBarColor()
	return 0, 1, 0
end

local function createLabel(ownerFrame, fontObject)
	assert (ownerFrame ~= nil)

	local t = ownerFrame:CreateFontString((ownerFrame:GetName() or '') .. 'Text', 'OVERLAY')
	fontObject = fontObject 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 createUnitButtonBar(self)
	assert (self ~= nil)

	local b = self:CreateTexture(self:GetName() .. 'Bar', 'OVERLAY')
	local padding = 4
	b:SetPoint('BOTTOMLEFT', padding, padding)
	b:SetPoint('TOPRIGHT', -padding, -padding)
	b:SetTexture(getDefaultUnitButtonBarColor(), 1)

	self.bar = b

	return b
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 getUnitHealthRatio(unitDesignation)
	assert (unitDesignation ~= nil)

	local m = math.abs(math.max(UnitHealthMax(unitDesignation) or 1, 1))
	local c = math.abs(UnitHealth(unitDesignation) or 1)

	return c / m
end

local function updateUnitButtonText(self)
	assert (self ~= nil)

	local label = self.text
	assert (label ~= nil)

	local unitDesignation = self:GetAttribute('unit')
	assert (unitDesignation ~= nil)

	local n = UnitName(unitDesignation) or unitDesignation

	local key = GetBindingKey('CLICK ' .. self:GetName() .. ':LeftButton')
	if not key then
		key = self:GetAttribute('choirBindingKey')
	end
	if key then
		n = n .. ' <' .. key .. '>'
	end

	local d = getUnitHealthDeficit(unitDesignation)
	local t
	if UnitIsDead(unitDesignation) then
		t = n .. '\n\r' .. '(Dead)'
	elseif UnitIsGhost(unitDesignation) then
		t = n .. '\n\r' .. '(Ghost)'
	elseif d < 0 then
		t = n .. '\n\r' .. tostring(math.floor(d))
	else
		t = n
	end

	label:SetText(t)
end

local function getClassColor(classDesignation)
	local t = RAID_CLASS_COLORS[classDesignation]
	if not t then
		return nil
	end
	return t['r'], t['g'], t['b']
end

local function updateUnitButtonBar(self)
	assert (self ~= nil)

	local bar = self.bar
	assert (bar ~= nil)

	local unitDesignation = self:GetAttribute('unit')
	assert (unitDesignation ~= nil)

	--[[ Apply bar color update ]]--
	local _, classDesignation = UnitClass(unitDesignation)
	local r, g, b = getClassColor(classDesignation)
	--[[ TODO Range indicator ]]--
	local a = 1
	if not r or not g or not b then
		r, g, b = getDefaultUnitButtonBarColor()
	end
	bar:SetTexture(r, g, b, a)

	--[[ Apply bar width update ]]--

	self:SetAttribute('choirUnitHealthRatio', getUnitHealthRatio(unitDesignation))
	local secureHandler = self.secureHandler
	secureHandler:Execute([=[
		local u = self:GetFrameRef('ChoirUnitButton')
		local unitDesignation = u:GetAttribute('unit')

		local ratio = 1
		if UnitExists(unitDesignation) then
			ratio = u:GetAttribute('choirUnitHealthRatio')
		end
		ratio = math.min(math.max(0, ratio), 1)

		local bar = self:GetFrameRef('ChoirBar')
		local padding = 4
		bar:SetPoint('BOTTOMLEFT', self, 'BOTTOMLEFT', padding, padding)
		bar:SetPoint('TOPRIGHT', self, 'TOPLEFT', ratio * u:GetWidth() - padding, -padding)
	]=])
end

local function unitButtonEventProcessor(self)
	updateUnitButtonText(self)
	updateUnitButtonBar(self)
end

local function createUnitButton(parentFrame, frameName, unit)
	assert (parentFrame ~= nil)
	assert (frameName ~= nil)
	assert (unit ~= nil)

	local u = CreateFrame('BUTTON', frameName, parentFrame, 'SecureUnitButtonTemplate')
	u:SetAttribute('type', 'target')
	u:SetAttribute('unit', unit)

	u:SetSize(12 * 8, 12 * 4)

	u.text = createLabel(u)

	local b = createBackground(u)
	u.background = b

	local bar = createUnitButtonBar(u)
	u.bar = bar

	local secureHandler = CreateFrame('BUTTON', frameName .. 'SecureHeader', u, 'SecureHandlerBaseTemplate')
	secureHandler:SetAllPoints()
	secureHandler:SetFrameRef('ChoirUnitButton', u)
	secureHandler:SetFrameRef('ChoirBar', bar)
	u.secureHandler = secureHandler

	u:SetScript('OnEvent', unitButtonEventProcessor)
	u:RegisterEvent('PARTY_CONVERTED_TO_RAID')
	u:RegisterEvent('PARTY_MEMBERS_CHANGED')
	u:RegisterEvent('PLAYER_ALIVE')
	u:RegisterEvent('RAID_ROSTER_UPDATE')
	u:RegisterEvent('UPDATE_BATTLEFIELD_SCORE')
	u:RegisterEvent('ADDON_LOADED')
	--[[ UNIT_HEALTH event with the current implementation does produce unnecessary calls.
	--   Optimization is desireable. Code maintanence takes priority currently. ]]--
	u:RegisterEvent('UNIT_HEALTH')

	assert (u ~= nil)
	return u
end

local function createSpoiler(spoilerParent, spoilerDesignation)
	local spoiler = CreateFrame('BUTTON', spoilerDesignation, spoilerParent, 'SecureHandlerClickTemplate')
	spoiler:EnableMouse(true)
	--[[ WARNING To assign the temporary override bindings to the buttons under a spoiler correctly,
	--           the children table must be sorted and be iterated over in a specific order.
	--           #GetChildList method seems to return the children in order they were created,
	--           but that might not be the case for #GetChildren variant or not at all. ]]--
	spoiler:SetAttribute('_onclick', [=[
		--[[ Toggle sibling frames, which are other spoilers that contain unit buttons ]]--
		local parentFrame = self:GetParent()
		local siblingTable = parentFrame:GetChildList(newtable())
		local i = 0
		while (i < #siblingTable) do
			i = i + 1
			local sibling = siblingTable[i]
			sibling:ClearBindings()
			sibling:Hide()
		end

		--[[ Apply override bindings to children which are unit buttons of a specific raid group ]]--
		self:Show()
		local j = 0
		local childTable = self:GetChildList(newtable())
		while (j < #childTable) do
			j = j + 1
			local child = childTable[j]
			child:Show()

			local key = child:GetAttribute('choirBindingKey')
			if key then
				self:SetBindingClick(true, key, child)
			end
		end
	]=])

	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 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 spoiler = createSpoiler(rootFrame, 'ChoirSpoiler' .. tostring(groupNumber))
	spoiler:SetSize(12 * 6, 12 * 4)
	spoiler:SetPoint('CENTER', 12 * 6 * 6 / -2, 144)

	local i = 0
	local marginLeft = 0
	local padding = 4
	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)
		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 ]]--
			local spoilerFrame = self:GetParent()
			spoilerFrame:ClearBindings()
			spoilerFrame:Hide()
		]=])
		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)
	end

	_G['BINDING_NAME_CLICK ' .. spoiler:GetName() .. ':LeftButton'] = 'Group ' .. tostring(groupNumber)

	return spoiler
end

local function init(rootFrame)
	assert (rootFrame ~= nil)

	rootFrame:UnregisterAllEvents()

	rootFrame:SetSize(1024, 768)
	rootFrame:SetPoint('CENTER', 0, 0)

	createGroup(rootFrame, 1)
	createGroup(rootFrame, 2)
	createGroup(rootFrame, 3)
	createGroup(rootFrame, 4)
	createGroup(rootFrame, 5)
	createGroup(rootFrame, 6)
	createGroup(rootFrame, 7)
	createGroup(rootFrame, 8)

	--[[ TODO Add Esc key that closes the spoiler without clicking any of the children buttons ]]--
	--[[ 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(rootFrame, 9, {'player', 'party1', 'party2', 'party3', 'party4'})
	_G['BINDING_NAME_CLICK ' .. spoilerParty:GetName() .. ':LeftButton'] = 'Player party'

	BINDING_HEADER_CHOIR = 'Choir'

	print('[Choir]: 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 615 df7e370d7c6f345b30baf722337a5d6584614ae2 .luacheckrc
100644 blob 934 73158d32fbccb1838992b04c2901ba6348410e3d bindings.xml
100644 blob 10167 5ac0a5f77b4b6bb0af9dd3258e59010596a98408 choir.lua
100644 blob 110 db7ba665fc9db451e9b7057f3213ae2d515693d6 choir.toc
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