WoWInterface SVN Ara_Broker_Guild_Friends

Compare Revisions

  • This comparison shows the changes necessary to convert path
    /trunk
    from Rev 1 to Rev 2
    Reverse comparison

Rev 1 → Rev 2

guild.tga Cannot display: file marked as a binary type. svn:mime-type = application/octet-stream Property changes : Added: svn:mime-type + application/octet-stream
friends.tga Cannot display: file marked as a binary type. svn:mime-type = application/octet-stream Property changes : Added: svn:mime-type + application/octet-stream
Ara_Broker_Guild_Friends.lua New file
0,0 → 1,377
-- ShowTablet line 3: possible source of bugs (glitch) when trying to solve: block/self -> incorrect setpoint
 
local BUTTON_HEIGHT, ICON_SIZE, GAP, TEXT_OFFSET, TIMEOUT =
15, 13, 10, 5, .05
 
local f = CreateFrame( "Frame", "AraBrokerGuildFriends" )
local extraFrame = CreateFrame"Frame"
local dropdown, dropdown_init = CreateFrame( "Frame", "AraBrokerGuildFriendsDDFrame", nil, "UIDropDownMenuTemplate" )
 
local block, isGuild, leaving
 
local L = setmetatable( {}, { __index = function(t,k) return k end } ) -- GetLocale()
 
local format, strfind, GetGuildRosterInfo, GetFriendInfo, RAID_CLASS_COLORS, CLASS_BUTTONS =
format, strfind, GetGuildRosterInfo, GetFriendInfo, RAID_CLASS_COLORS, CLASS_BUTTONS
 
---------------------------------[[ ]]--------------------------------
 
local tables = {}
 
local new = function( ... )
local table = tremove(tables) or {}
for i=1, select("#",...) do table[i] = select(i,...) end
return table
end
 
local del = function( table )
tables[ #tables + 1 ] = wipe(table)
end
 
 
local mejoin = format( ERR_GUILD_JOIN_S, UnitName"player" )
local friendOnline, friendOffline = gsub(ERR_FRIEND_ONLINE_SS,"\124Hplayer:%%s\124h%[%%s%]\124h",""), gsub(ERR_FRIEND_OFFLINE_S,"%%s","")
function f:CHAT_MSG_SYSTEM( event, msg )
if msg == mejoin then self.updateGuild = true return end
if strfind( msg, friendOnline ) or strfind( msg, friendOffline ) then self.updateFriends, self.updateGuild = true, true end
end
 
local friendEntries = {}
 
function f:FRIENDLIST_UPDATE()
for k,v in next,friendEntries do del(v) friendEntries[k]=nil end
local total, online = GetNumFriends(), 0
for i = 1, total do
local name, level, class, zone, connected, status, note = GetFriendInfo(i)
if( connected )then
if( online == 0 )then
friendEntries[1] = new( nil, "Name", "", "Lv", "Zone", "Notes" )
end
online = online + 1
friendEntries[online+1] = new( name, status and format("%s %s", tostring(status), tostring(name)) or name, class:gsub(" ",""):upper(), level, zone or "Unknown", note and format("[%s]",note) or "-" )
end
end
f.FriendsBlock.text = total > 0 and format("%d/%d", online, total) or L["No friends."]
if( not isGuild and block )then f:FriendsOnEnter(block) end
end
 
function f:GUILD_ROSTER_UPDATE()
local numOnline = 0
for i=1, GetNumGuildMembers() do
if( select( 9, GetGuildRosterInfo(i) ) )then numOnline = numOnline + 1 end
end
f.GuildBlock.text = IsInGuild() and format( "%s - %d/%d", GetGuildInfo"player", numOnline, GetNumGuildMembers(true) ) or L["No Guild"]
if( isGuild and block )then f:GuildOnEnter(block) end
end
 
function f:PLAYER_GUILD_UPDATE(event, unit)
if( unit and unit ~= "player" )then return end
return IsInGuild() and GuildRoster()
end
 
local horde
local hordeZones = "Orgrimmar,Undercity,Thunder Bluff,Silvermoon City,Tirisfal Glades,Durotar,The Barrens,Silverpine Forest,Mulgore,The Sepulcher,Eversong Woods,Ghostlands,"
local allianceZones = "Ironforge,Stormwind City,Darnassus,Azuremyst Isle,Bloodmyst Isle,Darkshore,Dun Morogh,Loch Modan,Wetlands,Elwynn Forest,Redridge Mountains,Westfall,Teldrassil,Duskwood,Darshire,Thelsamar,Deeprun Tram,The Exodar,Auberdine,Menethil Harbor,Theramore Isle"
local function GetZoneColor(zone)
if( strfind( hordeZones , zone.."," ) )then if( horde )then return 0,1,0 else return 1,0,0 end end
if( strfind( allianceZones, zone.."," ) )then if( horde )then return 1,0,0 else return 0,1,0 end end
return 1,1,0
end
 
local function Menu_OnUpdate( frame, elapsed )
extraFrame.timer = extraFrame.timer + elapsed
if( extraFrame.timer <= TIMEOUT )then return end
extraFrame:SetScript( "OnUpdate", nil )
f:Hide()
block = nil
end
 
local highlight = f:CreateTexture(nil,"ARTWORK")
highlight:SetTexture"Interface\\QuestFrame\\UI-QuestTitleHighlight"
highlight:SetBlendMode"ADD"
highlight:SetAlpha(0)
 
local function Menu_OnEnter(b)
leaving = false
extraFrame:SetScript( "OnUpdate", nil )
if(b.index)then
highlight:SetAllPoints(b)
if( b.index > 1)then highlight:SetAlpha(1) end
end
end
 
local function Menu_OnLeave(b)
leaving = true
extraFrame.timer = 0
extraFrame:SetScript( "OnUpdate", Menu_OnUpdate )
highlight:ClearAllPoints()
if(b.index and b.index > 1)then highlight:SetAlpha(0) end
end
 
local font, _, flags = GameFontNormal:GetFont()
local function CreateFS( index, parent, justify, anchor )
local fs = parent:CreateFontString( nil, "OVERLAY" )
fs:SetFont( font, 12, flags )
fs:SetJustifyH( justify )
fs:SetTextColor( 1, index == 1 and .8 or 1, 0 )
fs:SetShadowColor( 0, 0, 0 ) -- ADDED
fs:SetShadowOffset( 1, -1 ) -- ADDED
if( anchor )then fs:SetPoint( "LEFT", anchor, "RIGHT", GAP, 0 ) end
return fs
end
 
local buttons = setmetatable( { }, { __index = function( table, key )
local button = CreateFrame( "Button", nil, f )
rawset( table, key, button )
button.index = key
button:RegisterForClicks"AnyUp"
button:SetHeight( BUTTON_HEIGHT )
button:SetScript( "OnEnter", Menu_OnEnter )
button:SetScript( "OnLeave", Menu_OnLeave )
 
button.fontName = CreateFS( key, button, "LEFT" )
if( key == 0 )then
button.fontName:SetJustifyV"TOP"
else
if( key > 1 )then
button.icon = button:CreateTexture( nil, "OVERLAY" )
button.icon:SetWidth( ICON_SIZE ) button.icon:SetHeight( ICON_SIZE )
button.icon:SetPoint( "LEFT", button, "LEFT" )
end
 
button.fontName:SetPoint( "LEFT", button, "LEFT", ICON_SIZE + TEXT_OFFSET, 0 )
button.fontLevel = CreateFS( key, button, "CENTER", button.fontName )
button.fontZone = CreateFS( key, button, "CENTER", button.fontLevel )
button.fontNotes = CreateFS( key, button, "CENTER", button.fontZone )
button.fontRank = CreateFS( key, button, "RIGHT", button.fontNotes )
end
return button
end } )
 
 
local function SetButtonData( index, name, class, level, zone, notes, rank )
local button = buttons[index]
button.fontName:SetText( name or "" )
button:Show()
if( class )then
if( index > 1 )then
button.icon:SetTexture"Interface\\Glues\\CharacterCreate\\UI-CharacterCreate-Classes"
local offset, left, right, bottom, top = 0.025, unpack( CLASS_BUTTONS[class] )
button.icon:SetTexCoord( left+offset, right-offset, bottom+offset, top-offset )
-- button.icon:SetTexCoord( unpack( CLASS_BUTTONS[class] ) )
local color = RAID_CLASS_COLORS[class]
button.fontName:SetTextColor( color.r, color.g, color.b )
color = GetDifficultyColor(level)
button.fontLevel:SetTextColor( color.r, color.g, color.b )
button.fontZone:SetTextColor( GetZoneColor(zone or "#") )
end
button.fontLevel:SetText( level or "" )
button.fontZone:SetText( zone or "" )
button.fontNotes:SetText( notes or "" )
button.fontRank:SetText( rank or "" )
return button, button.fontName:GetStringWidth(), button.fontLevel:GetStringWidth(), button.fontZone:GetStringWidth(), button.fontNotes:GetStringWidth(), button.fontRank:GetStringWidth()
end
return button, button.fontName:GetStringWidth()
end
 
 
------------------------------[[ data feed ]]----------------------------
local function EditMOTD()
f:Hide()
GuildMOTDEditButton:Click()
end
 
local colspace = { ICON_SIZE }
local cols = { "class", "name", "level", "zone", "note", "rank" }
 
local function OnGuildmateClick( self, button )
if( not( self and self.unit ) )then return end
if( isGuild and button ~= "LeftButton" )then -- sort columns
local prevDist = GetCursorPosition() / self:GetEffectiveScale() - self:GetLeft()
local colpos, dist = colspace[1] - prevDist
for i=2, #colspace do
colpos = colpos + colspace[i] + (i==2 and TEXT_OFFSET or GAP)
dist = abs( colpos - colspace[i]*.5 )
if( dist > prevDist )then dist, prevDist = i-1, -1 break else prevDist = dist end
end
SortGuildRoster( cols[ prevDist < 0 and dist or #colspace ] )
elseif( IsAltKeyDown() )then
InviteUnit( self.unit )
elseif( not isGuild and IsControlKeyDown() )then
FriendsFrame.NotesID = self.index - 1
StaticPopup_Show( "SET_FRIENDNOTE", self.unit )
else
SetItemRef("player:"..self.unit, format( "|Hplayer:%s|h[%s]|h", self.unit, self.unit ), "LeftButton")
end
end
 
local function ShowTablet( self, _isGuild, entries, onClickFunc )
isGuild = _isGuild
block = not leaving and block or self -- possible source of bugs when fast switching
if( InCombatLockdown() )then return end
if( isGuild and not IsInGuild() or #entries == 0 )then
GameTooltip:SetOwner(self)
GameTooltip:AddLine( isGuild and ERR_GUILD_PLAYER_NOT_IN_GUILD or L["No friends online."] )
return GameTooltip:Show()
end
Menu_OnEnter(self)
local offsetEntries, button = isGuild and 1 or 0
local nameC, levelC, zoneC, notesC, rankC = 0, 0, 0, 0, 0
local nameW, levelW, zoneW, notesW, rankW
 
local extraHeight, motd = 0
if( isGuild )then
motd, extraHeight = SetButtonData( 0, "|cffffffffMOTD:|r " .. GetGuildRosterMOTD() )
if( CanEditMOTD() )then motd:SetScript( "OnClick", EditMOTD ) end
motd.fontName:ClearAllPoints()
end
 
for i, entry in ipairs(entries) do
button, nameW, levelW, zoneW, notesW, rankW = SetButtonData( i, entry[2], entry[3], entry[4], entry[5], entry[6], entry[7] )
button:SetScript( "OnClick", onClickFunc )
button.unit = entry[1]
if( nameW > nameC )then nameC = nameW end
if( levelW > levelC )then levelC = levelW end
if( zoneW > zoneC )then zoneC = zoneW end
if( notesW > notesC )then notesC = notesW end
if( rankW > rankC )then rankC = rankW end
end
 
local maxWidth = ICON_SIZE + TEXT_OFFSET + nameC + levelC + zoneC + notesC + rankC + GAP * (isGuild and 4 or 3)
extraHeight = ceil( extraHeight / maxWidth )
 
if( isGuild )then
motd:SetWidth( maxWidth )
motd:SetPoint( "TOPLEFT", f, "TOPLEFT", GAP, -GAP )
motd:SetHeight( (extraHeight+1) * BUTTON_HEIGHT )
motd.fontName:SetAllPoints(motd)
buttons[1]:SetPoint( "TOPLEFT", motd, "BOTTOMLEFT" )
elseif( rawget( buttons, 0 ) )then
buttons[0]:Hide()
end
 
for i=1, #entries do
button = buttons[i]
button.fontName:SetWidth(nameC)
button.fontLevel:SetWidth(levelC)
button.fontZone:SetWidth(zoneC)
button.fontNotes:SetWidth(notesC)
button.fontRank:SetWidth(rankC)
button:SetWidth( maxWidth )
if( not isGuild or i>1 )then button:SetPoint( "TOPLEFT", f, "TOPLEFT", GAP, (1-i-extraHeight-offsetEntries) * BUTTON_HEIGHT - GAP ) end
end
colspace[2], colspace[3], colspace[4], colspace[5], colspace[6] = nameC, levelC, zoneC, notesC, rankC
 
f:SetWidth( maxWidth + GAP*2 )
f:SetHeight( BUTTON_HEIGHT * (#entries+offsetEntries+extraHeight) + GAP*2 )
 
local showBelow = select( 2, block:GetCenter() ) > ( UIParent:GetHeight() / 2 )
f:ClearAllPoints()
f:SetPoint( showBelow and "TOP" or "BOTTOM", block, showBelow and "BOTTOM" or "TOP" )
f:Show()
for i=#entries+1, #buttons do buttons[i]:Hide() end
end
 
 
function f:FriendsOnEnter()
if( InCombatLockdown() )then return end
return ShowTablet( self, false, friendEntries, OnGuildmateClick )
end
 
local guildEntries = {}
 
function f:GuildOnEnter()
if( InCombatLockdown() )then return end
for k, v in next, guildEntries do del(v) guildEntries[k]=nil end
for i=1, GetNumGuildMembers(true) do
local name, rank, _, level, _, zone, note, offnote, connected, status, class = GetGuildRosterInfo(i)
if( connected )then
if( #guildEntries == 0 )then -- first entry, so add header
guildEntries[1] = new( nil, "Name", "", "Lv", "Zone", "Notes", "Rank" )
end
local notes = note ~= "" and (offnote ~= "" and
format( "[%s] - [%s]", note, offnote ) or
format( "[%s]", note )) or (offnote ~= "" and
format( "[%s]", offnote ) or "-")
guildEntries[#guildEntries+1] = new( name, status == "" and name or format( "%s %s", status, name ), class, level, zone, notes, rank )
end
end
ShowTablet( self, true, guildEntries, OnGuildmateClick )
end
 
 
local ldb = LibStub("LibDataBroker-1.1")
 
f.GuildBlock = ldb:NewDataObject( "|cFFFFB366Ara|r Guild", {
text = GUILD,
icon = "Interface\\AddOns\\Ara_Broker_Guild&Friends\\guild.tga",
OnEnter = f.GuildOnEnter,
OnLeave = function(self) return IsInGuild() and Menu_OnLeave(self) or GameTooltip:Hide() end,
OnClick = function(self) ToggleFriendsFrame(3) end,
} )
 
f.FriendsBlock = ldb:NewDataObject( "|cFFFFB366Ara|r Friends", {
text = FRIENDS,
icon = "Interface\\AddOns\\Ara_Broker_Guild&Friends\\friends.tga",
OnEnter = f.FriendsOnEnter,
OnLeave = function(self) Menu_OnLeave(self) GameTooltip:Hide() end,
OnClick = function(self) ToggleFriendsFrame(1) end,
} )
 
local orgShowFriends = ShowFriends
ShowFriends = function(...)
f.friendsTimer, f.updateFriends = 0, false
return orgShowFriends(...)
end
 
local orgGuildRoster = GuildRoster
GuildRoster = function(...)
f.guildTimer, f.updateGuild = 0, false
return orgGuildRoster(...)
end
 
local function OnUpdate( self, elapsed )
self.guildTimer, self.friendsTimer = self.guildTimer + elapsed, self.friendsTimer + elapsed
if( self.updateGuild and self.guildTimer > 15 or self.guildTimer > 300 )then
self.guildTimer, self.updateGuild = 0, false
if IsInGuild() then GuildRoster() end
end
if( self.updateFriends and self.friendsTimer > 15 or self.friendsTimer > 300 )then
ShowFriends()
end
end
 
-------------------------------[[ events ]]------------------------------
 
f.PLAYER_LOGIN = function( self, event )
horde = UnitFactionGroup"player" == "Horde"
SortGuildRoster"class" -- "name", "rank", "note", "online", "zone", "level", "class"
 
self:SetBackdrop( { bgFile="Interface\\Buttons\\WHITE8X8", edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",
tile=true, tileSize=12, edgeSize=12, insets = { left=2, right=2, top=2, bottom=2 } } )
self:SetBackdropColor( 0, 0, 0, .75 )
self:SetScale( UIParent:GetScale() )
self:SetFrameStrata"DIALOG"
self:SetClampedToScreen(true)
self:EnableMouse( true )
self:Show() -- possible fix for menu width bug (or TODO: put in PLAYER_ENTERING_WORLD)
 
self.guildTimer, self.friendsTimer = 0, 0
self:SetScript( "OnUpdate", OnUpdate )
self:SetScript( "OnEnter", Menu_OnEnter )
self:SetScript( "OnLeave", Menu_OnLeave )
 
self:RegisterEvent"GUILD_ROSTER_UPDATE"
self:RegisterEvent"PLAYER_GUILD_UPDATE"
self:RegisterEvent"FRIENDLIST_UPDATE"
self:RegisterEvent"CHAT_MSG_SYSTEM"
 
if IsInGuild() then GuildRoster() end
ShowFriends()
 
self:UnregisterEvent"PLAYER_LOGIN"
self.PLAYER_LOGIN = nil
end
 
f:SetScript( "OnEvent", function(self, event, ...) return self[event](self, event, ...) end )
return IsLoggedIn() and f:PLAYER_LOGIN() or f:RegisterEvent"PLAYER_LOGIN"
\ No newline at end of file
Ara_Broker_Guild_Friends.toc New file
0,0 → 1,17
## Interface: 30000
## Title: |cFFFFB366Ara|r - Guild & Friends
## Version: r1
## Notes: A Data Broker plugin that provides guildmates & friends informations and interactions.
## Dependencies:
## OptionalDeps:
## SavedVariables:
## SavedVariablesPerCharacter:
 
## _LoadManagers: AddonLoader
## _X-LoadOn-Always: delayed
 
libs\LibStub.lua
libs\CallbackHandler-1.0.lua
libs\LibDataBroker-1.1.lua
 
Ara_Broker_Guild_Friends.lua
\ No newline at end of file
libs/LibDataBroker-1.1.lua New file
0,0 → 1,66
 
assert(LibStub, "LibDataBroker-1.1 requires LibStub")
assert(LibStub:GetLibrary("CallbackHandler-1.0", true), "LibDataBroker-1.1 requires CallbackHandler-1.0")
 
local lib, oldminor = LibStub:NewLibrary("LibDataBroker-1.1", 3)
if not lib then return end
oldminor = oldminor or 0
 
 
lib.callbacks = lib.callbacks or LibStub:GetLibrary("CallbackHandler-1.0"):New(lib)
lib.attributestorage, lib.namestorage, lib.proxystorage = lib.attributestorage or {}, lib.namestorage or {}, lib.proxystorage or {}
local attributestorage, namestorage, callbacks = lib.attributestorage, lib.namestorage, lib.callbacks
 
if oldminor < 2 then
lib.domt = {
__metatable = "access denied",
__index = function(self, key) return attributestorage[self] and attributestorage[self][key] end,
}
end
 
if oldminor < 3 then
lib.domt.__newindex = function(self, key, value)
if not attributestorage[self] then attributestorage[self] = {} end
if attributestorage[self][key] == value then return end
attributestorage[self][key] = value
local name = namestorage[self]
if not name then return end
callbacks:Fire("LibDataBroker_AttributeChanged", name, key, value, self)
callbacks:Fire("LibDataBroker_AttributeChanged_"..name, name, key, value, self)
callbacks:Fire("LibDataBroker_AttributeChanged_"..name.."_"..key, name, key, value, self)
callbacks:Fire("LibDataBroker_AttributeChanged__"..key, name, key, value, self)
end
end
 
if oldminor < 2 then
function lib:NewDataObject(name, dataobj)
if self.proxystorage[name] then return end
 
if dataobj then
assert(type(dataobj) == "table", "Invalid dataobj, must be nil or a table")
self.attributestorage[dataobj] = {}
for i,v in pairs(dataobj) do
self.attributestorage[dataobj][i] = v
dataobj[i] = nil
end
end
dataobj = setmetatable(dataobj or {}, self.domt)
self.proxystorage[name], self.namestorage[dataobj] = dataobj, name
self.callbacks:Fire("LibDataBroker_DataObjectCreated", name, dataobj)
return dataobj
end
end
 
if oldminor < 1 then
function lib:DataObjectIterator()
return pairs(self.proxystorage)
end
 
function lib:GetDataObjectByName(dataobjectname)
return self.proxystorage[dataobjectname]
end
 
function lib:GetNameByDataObject(dataobject)
return self.namestorage[dataobject]
end
end
libs/LibStub.lua New file
0,0 → 1,30
-- LibStub is a simple versioning stub meant for use in Libraries. http://www.wowace.com/wiki/LibStub for more info
-- LibStub is hereby placed in the Public Domain Credits: Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel, joshborke
local LIBSTUB_MAJOR, LIBSTUB_MINOR = "LibStub", 2 -- NEVER MAKE THIS AN SVN REVISION! IT NEEDS TO BE USABLE IN ALL REPOS!
local LibStub = _G[LIBSTUB_MAJOR]
 
if not LibStub or LibStub.minor < LIBSTUB_MINOR then
LibStub = LibStub or {libs = {}, minors = {} }
_G[LIBSTUB_MAJOR] = LibStub
LibStub.minor = LIBSTUB_MINOR
 
function LibStub:NewLibrary(major, minor)
assert(type(major) == "string", "Bad argument #2 to `NewLibrary' (string expected)")
minor = assert(tonumber(strmatch(minor, "%d+")), "Minor version must either be a number or contain a number.")
 
local oldminor = self.minors[major]
if oldminor and oldminor >= minor then return nil end
self.minors[major], self.libs[major] = minor, self.libs[major] or {}
return self.libs[major], oldminor
end
 
function LibStub:GetLibrary(major, silent)
if not self.libs[major] and not silent then
error(("Cannot find a library instance of %q."):format(tostring(major)), 2)
end
return self.libs[major], self.minors[major]
end
 
function LibStub:IterateLibraries() return pairs(self.libs) end
setmetatable(LibStub, { __call = LibStub.GetLibrary })
end
libs/CallbackHandler-1.0.lua New file
0,0 → 1,239
--[[ $Id: CallbackHandler-1.0.lua 60697 2008-02-09 16:51:20Z nevcairiel $ ]]
local MAJOR, MINOR = "CallbackHandler-1.0", 3
local CallbackHandler = LibStub:NewLibrary(MAJOR, MINOR)
 
if not CallbackHandler then return end -- No upgrade needed
 
local meta = {__index = function(tbl, key) tbl[key] = {} return tbl[key] end}
 
local type = type
local pcall = pcall
local pairs = pairs
local assert = assert
local concat = table.concat
local loadstring = loadstring
local next = next
local select = select
local type = type
local xpcall = xpcall
 
local function errorhandler(err)
return geterrorhandler()(err)
end
 
local function CreateDispatcher(argCount)
local code = [[
local next, xpcall, eh = ...
 
local method, ARGS
local function call() method(ARGS) end
 
local function dispatch(handlers, ...)
local index
index, method = next(handlers)
if not method then return end
local OLD_ARGS = ARGS
ARGS = ...
repeat
xpcall(call, eh)
index, method = next(handlers, index)
until not method
ARGS = OLD_ARGS
end
 
return dispatch
]]
 
local ARGS, OLD_ARGS = {}, {}
for i = 1, argCount do ARGS[i], OLD_ARGS[i] = "arg"..i, "old_arg"..i end
code = code:gsub("OLD_ARGS", concat(OLD_ARGS, ", ")):gsub("ARGS", concat(ARGS, ", "))
return assert(loadstring(code, "safecall Dispatcher["..argCount.."]"))(next, xpcall, errorhandler)
end
 
local Dispatchers = setmetatable({}, {__index=function(self, argCount)
local dispatcher = CreateDispatcher(argCount)
rawset(self, argCount, dispatcher)
return dispatcher
end})
 
--------------------------------------------------------------------------
-- CallbackHandler:New
--
-- target - target object to embed public APIs in
-- RegisterName - name of the callback registration API, default "RegisterCallback"
-- UnregisterName - name of the callback unregistration API, default "UnregisterCallback"
-- UnregisterAllName - name of the API to unregister all callbacks, default "UnregisterAllCallbacks". false == don't publish this API.
 
function CallbackHandler:New(target, RegisterName, UnregisterName, UnregisterAllName, OnUsed, OnUnused)
-- TODO: Remove this after beta has gone out
assert(not OnUsed and not OnUnused, "ACE-80: OnUsed/OnUnused are deprecated. Callbacks are now done to registry.OnUsed and registry.OnUnused")
 
RegisterName = RegisterName or "RegisterCallback"
UnregisterName = UnregisterName or "UnregisterCallback"
if UnregisterAllName==nil then -- false is used to indicate "don't want this method"
UnregisterAllName = "UnregisterAllCallbacks"
end
 
-- we declare all objects and exported APIs inside this closure to quickly gain access
-- to e.g. function names, the "target" parameter, etc
 
 
-- Create the registry object
local events = setmetatable({}, meta)
local registry = { recurse=0, events=events }
 
-- registry:Fire() - fires the given event/message into the registry
function registry:Fire(eventname, ...)
if not rawget(events, eventname) or not next(events[eventname]) then return end
local oldrecurse = registry.recurse
registry.recurse = oldrecurse + 1
 
Dispatchers[select('#', ...) + 1](events[eventname], eventname, ...)
 
registry.recurse = oldrecurse
 
if registry.insertQueue and oldrecurse==0 then
-- Something in one of our callbacks wanted to register more callbacks; they got queued
for eventname,callbacks in pairs(registry.insertQueue) do
local first = not rawget(events, eventname) or not next(events[eventname]) -- test for empty before. not test for one member after. that one member may have been overwritten.
for self,func in pairs(callbacks) do
events[eventname][self] = func
-- fire OnUsed callback?
if first and registry.OnUsed then
registry.OnUsed(registry, target, eventname)
first = nil
end
end
end
registry.insertQueue = nil
end
end
 
-- Registration of a callback, handles:
-- self["method"], leads to self["method"](self, ...)
-- self with function ref, leads to functionref(...)
-- "addonId" (instead of self) with function ref, leads to functionref(...)
-- all with an optional arg, which, if present, gets passed as first argument (after self if present)
target[RegisterName] = function(self, eventname, method, ... --[[actually just a single arg]])
if type(eventname) ~= "string" then
error("Usage: "..RegisterName.."(eventname, method[, arg]): 'eventname' - string expected.", 2)
end
 
method = method or eventname
 
local first = not rawget(events, eventname) or not next(events[eventname]) -- test for empty before. not test for one member after. that one member may have been overwritten.
 
if type(method) ~= "string" and type(method) ~= "function" then
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): 'methodname' - string or function expected.", 2)
end
 
local regfunc
 
if type(method) == "string" then
-- self["method"] calling style
if type(self) ~= "table" then
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): self was not a table?", 2)
elseif self==target then
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): do not use Library:"..RegisterName.."(), use your own 'self'", 2)
elseif type(self[method]) ~= "function" then
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): 'methodname' - method '"..tostring(method).."' not found on self.", 2)
end
 
if select("#",...)>=1 then -- this is not the same as testing for arg==nil!
local arg=select(1,...)
regfunc = function(...) self[method](self,arg,...) end
else
regfunc = function(...) self[method](self,...) end
end
else
-- function ref with self=object or self="addonId"
if type(self)~="table" and type(self)~="string" then
error("Usage: "..RegisterName.."(self or \"addonId\", eventname, method): 'self or addonId': table or string expected.", 2)
end
 
if select("#",...)>=1 then -- this is not the same as testing for arg==nil!
local arg=select(1,...)
regfunc = function(...) method(arg,...) end
else
regfunc = method
end
end
 
 
if events[eventname][self] or registry.recurse<1 then
-- if registry.recurse<1 then
-- we're overwriting an existing entry, or not currently recursing. just set it.
events[eventname][self] = regfunc
-- fire OnUsed callback?
if registry.OnUsed and first then
registry.OnUsed(registry, target, eventname)
end
else
-- we're currently processing a callback in this registry, so delay the registration of this new entry!
-- yes, we're a bit wasteful on garbage, but this is a fringe case, so we're picking low implementation overhead over garbage efficiency
registry.insertQueue = registry.insertQueue or setmetatable({},meta)
registry.insertQueue[eventname][self] = regfunc
end
end
 
-- Unregister a callback
target[UnregisterName] = function(self, eventname)
if not self or self==target then
error("Usage: "..UnregisterName.."(eventname): bad 'self'", 2)
end
if type(eventname) ~= "string" then
error("Usage: "..UnregisterName.."(eventname): 'eventname' - string expected.", 2)
end
if rawget(events, eventname) and events[eventname][self] then
events[eventname][self] = nil
-- Fire OnUnused callback?
if registry.OnUnused and not next(events[eventname]) then
registry.OnUnused(registry, target, eventname)
end
end
if registry.insertQueue and rawget(registry.insertQueue, eventname) and registry.insertQueue[eventname][self] then
registry.insertQueue[eventname][self] = nil
end
end
 
-- OPTIONAL: Unregister all callbacks for given selfs/addonIds
if UnregisterAllName then
target[UnregisterAllName] = function(...)
if select("#",...)<1 then
error("Usage: "..UnregisterAllName.."([whatFor]): missing 'self' or \"addonId\" to unregister events for.", 2)
end
if select("#",...)==1 and ...==target then
error("Usage: "..UnregisterAllName.."([whatFor]): supply a meaningful 'self' or \"addonId\"", 2)
end
 
 
for i=1,select("#",...) do
local self = select(i,...)
if registry.insertQueue then
for eventname, callbacks in pairs(registry.insertQueue) do
if callbacks[self] then
callbacks[self] = nil
end
end
end
for eventname, callbacks in pairs(events) do
if callbacks[self] then
callbacks[self] = nil
-- Fire OnUnused callback?
if registry.OnUnused and not next(callbacks) then
registry.OnUnused(registry, target, eventname)
end
end
end
end
end
end
 
return registry
end
 
 
-- CallbackHandler purposefully does NOT do explicit embedding. Nor does it
-- try to upgrade old implicit embeds since the system is selfcontained and
-- relies on closures to work.