Files
libdogtag-3-0/LibDogTag-3.0.lua
ckknight 2b6f0a190d - Make fontstring updates somewhat lazy. Don't allow more than 1/300 of a second to be used per-frame.
In most situations, this will have absolutely no effect, but this will mostly affect situations where absolutely every fontstring issues an update request, at which point it may take multiple frames to update all your data, but it won't freeze things anymore (hopefully).
2008-06-16 21:28:13 +00:00

578 lines
18 KiB
Lua

--[[
Name: LibDogTag-3.0
Revision: $Rev$
Author: Cameron Kenneth Knight (ckknight@gmail.com)
Website: http://www.wowace.com/
Description: A library to provide a markup syntax
]]
local MAJOR_VERSION = "LibDogTag-3.0"
local MINOR_VERSION = tonumber(("$Revision$"):match("%d+")) or 0
if MINOR_VERSION > _G.DogTag_MINOR_VERSION then
_G.DogTag_MINOR_VERSION = MINOR_VERSION
end
-- #AUTODOC_NAMESPACE DogTag
DogTag_funcs[#DogTag_funcs+1] = function(DogTag)
local L = DogTag.L
local newList, newSet, del, deepCopy = DogTag.newList, DogTag.newSet, DogTag.del, DogTag.deepCopy
local select2 = DogTag.select2
local fixNamespaceList = DogTag.fixNamespaceList
local memoizeTable = DogTag.memoizeTable
local kwargsToKwargTypes = DogTag.kwargsToKwargTypes
local fsNeedUpdate, fsNeedQuickUpdate, codeToFunction, codeToEventList, eventData, clearCodes
local clearCodes = DogTag.clearCodes
DogTag_funcs[#DogTag_funcs+1] = function()
fsNeedUpdate = DogTag.fsNeedUpdate
fsNeedQuickUpdate = DogTag.fsNeedQuickUpdate
codeToFunction = DogTag.codeToFunction
codeToEventList = DogTag.codeToEventList
eventData = DogTag.eventData
end
local fsToFrame, fsToCode, fsToNSList, fsToKwargs, Tags, AddonFinders
if DogTag.oldLib then
local oldLib = DogTag.oldLib
fsToFrame = oldLib.fsToFrame
fsToCode = oldLib.fsToCode
fsToNSList = oldLib.fsToNSList
fsToKwargs = oldLib.fsToKwargs
local tmp = {}
for fs, kwargs in pairs(fsToKwargs) do
tmp[fs] = memoizeTable(deepCopy(kwargs))
fsToKwargs[fs] = nil
end
for fs, kwargs in pairs(tmp) do
fsToKwargs[fs] = kwargs
end
tmp = nil
Tags = oldLib.Tags
Tags["Base"] = {}
AddonFinders = oldLib.AddonFinders
AddonFinders["Base"] = {}
else
fsToFrame = {}
fsToCode = {}
fsToNSList = {}
fsToKwargs = {}
Tags = { ["Base"] = {} }
AddonFinders = { ["Base"] = {} }
end
DogTag.fsToFrame = fsToFrame
DogTag.fsToCode = fsToCode
DogTag.fsToNSList = fsToNSList
DogTag.fsToKwargs = fsToKwargs
DogTag.Tags = Tags
DogTag.AddonFinders = AddonFinders
local sortStringList = DogTag.sortStringList
DogTag.__colors = {
minHP = { 1, 0, 0 },
midHP = { 1, 1, 0 },
maxHP = { 0, 1, 0 },
unknown = { 0.8, 0.8, 0.8 },
hostile = { 226/255, 45/255, 75/255 },
neutral = { 1, 1, 34/255 },
friendly = { 0.2, 0.8, 0.15 },
civilian = { 48/255, 113/255, 191/255 },
dead = { 0.6, 0.6, 0.6 },
tapped = { 0.5, 0.5, 0.5 },
disconnected = { 0.7, 0.7, 0.7 },
petHappy = { 0, 1, 0 },
petNeutral = { 1, 1, 0 },
petAngry = { 1, 0, 0 },
rage = { 226/255, 45/255, 75/255 },
energy = { 1, 220/255, 25/255 },
focus = { 1, 210/255, 0 },
mana = { 48/255, 113/255, 191/255 },
}
for class, data in pairs(_G.RAID_CLASS_COLORS) do
DogTag.__colors[class] = { data.r, data.g, data.b, }
end
--[[
Notes:
Adds a tag to the specified namespace
Arguments:
string - namespace to add to
string - name of the tag
table - data of the tag
Example:
LibStub("LibDogTag-3.0"):AddTag("MyNamespace", "Square", {
code = function(number) -- actual function that will be called
return number * number
end,
arg = {
'number', 'number', '@req', -- name, types, default
},
ret = 'number', -- return value
events = "SOME_EVENT#$number", -- will update when SOME_EVENT with the argument `number` is dispatched
doc = "Return the square of number", -- the description
example = '[4:Square] => "16"; [5:Square] => "25"', -- show one or more examples in this format
category = "Category name",
})
]]
function DogTag:AddTag(namespace, tag, data)
if type(namespace) ~= "string" then
error(("Bad argument #2 to `AddTag'. Expected %q, got %q"):format("string", type(namespace)), 2)
end
if type(tag) ~= "string" then
error(("Bad argument #3 to `AddTag'. Expected %q, got %q"):format("string", type(tag)), 2)
end
if type(data) ~= "table" then
error(("Bad argument #4 to `AddTag'. Expected %q, got %q"):format("table", type(data)), 2)
end
if not Tags[namespace] then
Tags[namespace] = {}
end
if Tags["Base"][tag] or Tags[namespace][tag] then
error(("Bad argument #3 to `AddTag'. %q already registered"):format(tag), 2)
end
local tagData = newList()
Tags[namespace][tag] = tagData
local arg = data.arg
if arg then
if type(arg) ~= "table" then
error("arg must be a table", 2)
end
if #arg % 3 ~= 0 then
error("arg must be a table with a length a multiple of 3", 2)
end
for i = 1, #arg, 3 do
local key, types, default = arg[i], arg[i+1], arg[i+2]
if type(key) ~= "string" then
error("arg must have its keys as strings", 2)
end
if type(types) ~= "string" then
error("arg must have its types as strings", 2)
end
if types:match("^tuple%-") then
if key ~= "..." then
error("arg must have its key be ... if it is a tuple.", 2)
end
local tupleTypes = types:sub(7)
local t = newSet((';'):split(tupleTypes))
for k in pairs(t) do
if k ~= "nil" and k ~= "number" and k ~= "string" and k ~= "boolean" then
error("arg can only have tuples of nil, number, string, or boolean", 2)
end
end
if t["boolean"] and (next(t, "boolean") or next(t) ~= "boolean") then
error("arg cannot specify both boolean and something else", 2)
end
t = del(t)
arg[i+1] = "tuple-" .. sortStringList(tupleTypes)
else
local t = newSet((';'):split(types))
for k in pairs(t) do
if k ~= "nil" and k ~= "number" and k ~= "string" and k ~= "undef" and k ~= "boolean" then
error("arg must have nil, number, string, undef, boolean, or tuple", 2)
end
end
if not key:match("^[a-z]+$") then
error("arg must have its key be a string of lowercase letters.", 2)
end
if t["nil"] and t["undef"] then
error("arg cannot specify both nil and undef", 2)
end
if t["boolean"] and (next(t, "boolean") or next(t) ~= "boolean") then
error("arg cannot specify both boolean and something else", 2)
end
t = del(t)
arg[i+1] = sortStringList(types)
end
end
tagData.arg = arg
end
if data.alias then
if type(data.alias) == "string" then
tagData.alias = data.alias
else -- function
tagData.alias = data.alias()
tagData.aliasFunc = data.alias
end
else
local ret = data.ret
if type(ret) == "string" then
tagData.ret = sortStringList(ret)
if ret then
local rets = newSet((";"):split(ret))
for k in pairs(rets) do
if k ~= "nil" and k ~= "number" and k ~= "string" and k ~= "boolean" then
error("ret must have nil, number, string, or boolean", 2)
end
end
rets = del(rets)
end
elseif type(ret) == "function" then
tagData.ret = ret
else
error(("ret must be a string or a function which returns a string, got %s"):format(type(ret)), 2)
end
if data.events then
if type(data.events) == "string" then
tagData.events = sortStringList(data.events)
elseif type(data.events) == "function" then
tagData.events = data.events
else
error(("events must be a string, function, or nil, got %s"):format(type(data.events)), 2)
end
end
tagData.alias = data.fakeAlias
if type(data) == "function" then
tagData.static = data.static
else
tagData.static = data.static and true or nil
end
if tagData.static and tagData.events then
error("Cannot specify both static and events", 2)
end
if type(data.code) ~= "function" then
error(("code must be a function, got %s"):format(type(data.code)), 2)
end
tagData.code = data.code
tagData.dynamicCode = data.dynamicCode and true or nil
end
tagData.doc = data.doc
if data.doc and type(data.doc) ~= "string" then
error(("doc must be nil or a string, got %s"):format(type(data.doc)), 2)
end
tagData.example = data.example
if data.doc then
if type(data.example) ~= "string" then
error(("if doc is supplied, example must be a string, got %s"):format(type(data.example)), 2)
end
local examples = newList((";"):split(data.example))
for i, v in ipairs(examples) do
if not v:trim():match("^%[.*%] => \".*\"$") then
error(("example must be in the form of [Tag sequence] => \"Result\", %s is not in said form."):format(v:trim()))
end
end
else
if data.example then
error("if doc is not supplied, example must be nil", 2)
end
end
tagData.category = data.category
if data.doc then
if type(data.category) ~= "string" then
error(("if doc is supplied, category must be a string, got %s"):format(type(data.category)), 2)
end
else
if data.category then
error("if doc is not supplied, category must be nil", 2)
end
end
del(data)
clearCodes(namespace)
end
local call__func, call__kwargs, call__code, call__nsList
local function call()
return call__func(call__kwargs)
end
local function errorhandler(err)
local _, minor = LibStub(MAJOR_VERSION)
return geterrorhandler()(("%s.%d: Error with code %q (%s). %s"):format(MAJOR_VERSION, minor, call__code, call__nsList, err))
end
local toUpdate = {}
local GetTime = _G.GetTime
local function updateFontStrings()
local finish = GetTime() + 1/300
for fs in pairs(toUpdate) do
if GetTime() >= finish then
return
end
toUpdate[fs] = nil
local code = fsToCode[fs]
assert(code)
local nsList = fsToNSList[fs]
local kwargs = fsToKwargs[fs]
local kwargTypes = kwargsToKwargTypes[kwargs]
local func = codeToFunction[nsList][kwargTypes][code]
DogTag.__isMouseOver = DogTag.__lastMouseover == fsToFrame[fs]
call__func, call__kwargs, call__code, call__nsList = func, kwargs, code, nsList
local success, ret, alpha, outline = xpcall(call, errorhandler)
call__func, call__kwargs, call__code, call__nsList = nil, nil, nil, nil
if success then
fs:SetText(ret)
if alpha then
fs:SetAlpha(alpha)
end
local a, b = fs:GetFont()
fs:SetFont(a, b, outline or '')
end
end
end
DogTag.updateFontStrings = updateFontStrings
local function updateFontString(fs)
fsNeedUpdate[fs] = nil
fsNeedQuickUpdate[fs] = nil
toUpdate[fs] = true
end
DogTag.updateFontString = updateFontString
--[[
Notes:
Manually updates a FontString previously registered.
Arguments:
frame - the FontString previously registered
Example:
LibStub("LibDogTag-3.0"):UpdateFontString(fs)
]]
function DogTag:UpdateFontString(fs)
local code = fsToCode[fs]
if code then
updateFontString(fs)
end
end
--[[
Notes:
Manually updates all FontStrings on a specified frame.
Arguments:
frame - the frame which to update all FontStrings on
Example:
LibStub("LibDogTag-3.0"):UpdateAllForFrame(frame)
]]
function DogTag:UpdateAllForFrame(frame)
for fs, f in pairs(fsToFrame) do
if frame == f then
updateFontString(fs)
end
end
end
--[[
Notes:
Adds a FontString to LibDogTag-3.0's registry, which will be updated automatically.
You can add twice without removing. It will just overwrite the previous registration.
You can specify any number of namespaces. "Base" is always included as a namespace.
The kwargs table is optional and always goes on the end after the namespaces. You can recycle the table after registering.
Arguments:
frame - the FontString to register
frame - the Frame which holds the FontString
string - the tag sequence
[optional] string - a semicolon-separated list of namespaces. Base is implied
[optional] table - a dictionary of default kwargs for all tags in the code to receive
Example:
LibStub("LibDogTag-3.0"):AddFontString(fs, fs:GetParent(), "[Name]", "Unit", { unit = 'mouseover' })
LibStub("LibDogTag-3.0"):AddFontString(fs, fs:GetParent(), "[Tag]", "MyNamespace")
LibStub("LibDogTag-3.0"):AddFontString(fs, fs:GetParent(), "[Tag] [Name]", "MyNamespace;Unit", { value = 5, unit = 'player', }) -- two namespaces at once
]]
function DogTag:AddFontString(fs, frame, code, nsList, kwargs)
if type(fs) ~= "table" then
error(("Bad argument #2 to `AddFontString'. Expected %q, got %q."):format("table", type(fs)), 2)
elseif type(frame) ~= "table" then
error(("Bad argument #3 to `AddFontString'. Expected %q, got %q."):format("table", type(frame)), 2)
elseif type(code) ~= "string" then
error(("Bad argument #4 to `AddFontString'. Expected %q, got %q."):format("string", type(code)), 2)
elseif nsList and type(nsList) ~= "string" then
error(("Bad argument #5 to `AddFontString'. Expected %q, got %q."):format("string", type(nsList)), 2)
elseif kwargs and type(kwargs) ~= "table" then
error(("Bad argument #6 to `AddFontString'. Expected %q, got %q."):format("table", type(kwargs)), 2)
end
nsList = fixNamespaceList[nsList]
kwargs = memoizeTable(deepCopy(kwargs))
if fsToCode[fs] then
if fsToFrame[fs] == frame and fsToCode[fs] == code and fsToNSList[fs] == nsList and fsToKwargs[fs] == kwargs then
fsNeedUpdate[fs] = true
return
end
self:RemoveFontString(fs)
end
fsToFrame[fs] = frame
fsToCode[fs] = code
fsToNSList[fs] = nsList
fsToKwargs[fs] = kwargs
local kwargTypes = kwargsToKwargTypes[kwargs]
local codeToEventList_nsList_kwargTypes_code = codeToEventList[nsList][kwargTypes][code]
if codeToEventList_nsList_kwargTypes_code == nil then
local _ = codeToFunction[nsList][kwargTypes][code]
codeToEventList_nsList_kwargTypes_code = codeToEventList[nsList][kwargTypes][code]
if codeToEventList_nsList_kwargTypes_code == nil then
local _, minor = LibStub(MAJOR_VERSION)
error(("%s.%d: Error with code %q (%s). Event list not created. Please inform ckknight."):format(MAJOR_VERSION, minor, code, nsList))
end
end
if codeToEventList_nsList_kwargTypes_code then
for event, arg in pairs(codeToEventList_nsList_kwargTypes_code) do
eventData[event][fs] = arg
end
end
updateFontString(fs)
end
--[[
Notes:
Removes a registered FontString from LibDogTag-3.0's registry.
You can remove twice without issues.
Arguments:
frame - the FontString previously registered
Example:
LibStub("LibDogTag-3.0"):RemoveFontString(fs)
]]
function DogTag:RemoveFontString(fs)
if type(fs) ~= "table" then
error(("Bad argument #2 to `RemoveFontString'. Expected %q, got %q"):format("table", type(fs)), 2)
end
local code = fsToCode[fs]
if not code then
return
end
local frame = fsToFrame[fs]
local nsList = fsToNSList[fs]
local kwargs = fsToKwargs[fs]
fsToCode[fs], fsToFrame[fs], fsToNSList[fs], fsToKwargs[fs] = nil, nil, nil, nil
fsNeedUpdate[fs], fsNeedQuickUpdate[fs] = nil, nil
local kwargTypes = kwargsToKwargTypes[kwargs]
local codeToEventList_nsList_kwargTypes_code = codeToEventList[nsList][kwargTypes][code]
if codeToEventList_nsList_kwargTypes_code then
for event in pairs(codeToEventList_nsList_kwargTypes_code) do
eventData[event][fs] = nil
end
end
fs:SetText(nil)
local a, b = fs:GetFont()
fs:SetFont(a, b, "")
end
--[[
Notes:
Adds a handler to be called when an addon or library comes into being
This should only really be called by sublibraries or addons that register tags.
Arguments:
string - namespace the addon finder is associated with
string - "_G" for a value on the global table or "LibStub", "Rock", "AceLibrary" for a library of the specified type
string - name of the addon or library
function - function to be called when addon or library is found
Example:
LibStub("DogTag-3.0"):AddAddonFinder("MyNamespace", "LibStub", "LibMonkey-1.0", function(LibMonkey)
-- do something with LibMonkey now
end)
]]
function DogTag:AddAddonFinder(namespace, kind, name, func)
if type(namespace) ~= "string" then
error(("Bad argument #2 to `AddAddonFinder'. Expected %q, got %q"):format("string", type(namespace)), 2)
end
if type(kind) ~= "string" then
error(("Bad argument #3 to `AddAddonFinder'. Expected %q, got %q"):format("string", type(kind)), 2)
end
if kind ~= "_G" and kind ~= "LibStub" and kind ~= "Rock" and kind ~= "AceLibrary" then
error(("Bad argument #3 to `AddAddonFinder'. Expected %q, %q, %q or %q, got %q"):format("_G", "LibStub", "Rock", "AceLibrary", kind), 2)
end
if type(name) ~= "string" then
error(("Bad argument #4 to `AddAddonFinder'. Expected %q, got %q"):format("string", type(name)), 2)
end
if type(func) ~= "function" then
error(("Bad argument #5 to `AddAddonFinder'. Expected %q, got %q"):format("function", type(func)), 2)
end
if not AddonFinders[namespace] then
AddonFinders[namespace] = {}
end
AddonFinders[namespace][newList(kind, name, func)] = true
end
local inADDON_LOADED = false
local accessed_ADDON_LOADED = false
function DogTag:ADDON_LOADED()
if inADDON_LOADED then
accessed_ADDON_LOADED = true
return
end
inADDON_LOADED = true
accessed_ADDON_LOADED = false
for namespace, data in pairs(AddonFinders) do
local refresh = false
for k in pairs(data) do
local kind, name, func = k[1], k[2], k[3]
if kind == "_G" then
if _G[name] then
data[k] = nil
del(k)
func(_G[name])
refresh = true
end
elseif kind == "AceLibrary" then
if AceLibrary and AceLibrary:HasInstance(name) then
data[k] = nil
del(k)
func(AceLibrary(name))
refresh = true
end
elseif kind == "Rock" then
if Rock and Rock:HasLibrary(name) then
data[k] = nil
del(k)
func(Rock:GetLibrary(name))
refresh = true
end
elseif kind == "LibStub" then
if Rock then
Rock:HasLibrary(name) -- try to load
end
if AceLibrary then
AceLibrary:HasInstance(name) -- try to load
end
LoadAddOn(name)
if LibStub:GetLibrary(name, true) then
data[k] = nil
del(k)
func(LibStub:GetLibrary(name))
refresh = true
end
end
end
if refresh then
clearCodes(namespace)
end
end
inADDON_LOADED = false
if accessed_ADDON_LOADED then
self:ADDON_LOADED()
end
end
--[[
Notes:
Clears a namespace's tags and any other relevant data.
This should be called when a sublibrary is upgrading.
Arguments:
string - namespace that is to be cleared
Example:
LibStub("LibDogTag-3.0"):ClearNamespace("MyNamespace")
]]
function DogTag:ClearNamespace(namespace)
if type(namespace) ~= "string" then
error(("Bad argument #2 to `ClearNamespace'. Expected %q, got %q"):format("string", type(namespace)), 2)
end
Tags[namespace] = nil
AddonFinders[namespace] = nil
self.EventHandlers[namespace] = nil
self.TimerHandlers[namespace] = nil
clearCodes(namespace)
collectgarbage('collect')
end
end