/trunk
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ |
..\FrameXML\UI.xsd"> |
<Script file="AceSerializer-3.0.lua"/> |
</Ui> |
--- **AceSerializer-3.0** can serialize any variable (except functions or userdata) into a string format, |
-- that can be send over the addon comm channel. AceSerializer was designed to keep all data intact, especially |
-- very large numbers or floating point numbers, and table structures. The only caveat currently is, that multiple |
-- references to the same table will be send individually. |
-- |
-- **AceSerializer-3.0** can be embeded into your addon, either explicitly by calling AceSerializer:Embed(MyAddon) or by |
-- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object |
-- and can be accessed directly, without having to explicitly call AceSerializer itself.\\ |
-- It is recommended to embed AceSerializer, otherwise you'll have to specify a custom `self` on all calls you |
-- make into AceSerializer. |
-- @class file |
-- @name AceSerializer-3.0 |
-- @release $Id: AceSerializer-3.0.lua 877 2009-11-02 15:56:50Z nevcairiel $ |
local MAJOR,MINOR = "AceSerializer-3.0", 2 |
local AceSerializer, oldminor = LibStub:NewLibrary(MAJOR, MINOR) |
if not AceSerializer then return end |
-- Lua APIs |
local strbyte, strchar, gsub, gmatch, format = string.byte, string.char, string.gsub, string.gmatch, string.format |
local assert, error, pcall = assert, error, pcall |
local type, tostring, tonumber = type, tostring, tonumber |
local pairs, select, frexp = pairs, select, math.frexp |
local tconcat = table.concat |
-- quick copies of string representations of wonky numbers |
local serNaN = tostring(0/0) |
local serInf = tostring(1/0) |
local serNegInf = tostring(-1/0) |
-- Serialization functions |
local function SerializeStringHelper(ch) -- Used by SerializeValue for strings |
-- We use \126 ("~") as an escape character for all nonprints plus a few more |
local n = strbyte(ch) |
if n<=32 then -- nonprint + space |
return "\126"..strchar(n+64) |
elseif n==94 then -- value separator |
return "\126\125" |
elseif n==126 then -- our own escape character |
return "\126\124" |
elseif n==127 then -- nonprint (DEL) |
return "\126\123" |
else |
assert(false) -- can't be reached if caller uses a sane regex |
end |
end |
local function SerializeValue(v, res, nres) |
-- We use "^" as a value separator, followed by one byte for type indicator |
local t=type(v) |
if t=="string" then -- ^S = string (escaped to remove nonprints, "^"s, etc) |
res[nres+1] = "^S" |
res[nres+2] = gsub(v,"[%c \94\126\127]", SerializeStringHelper) |
nres=nres+2 |
elseif t=="number" then -- ^N = number (just tostring()ed) or ^F (float components) |
local str = tostring(v) |
if tonumber(str)==v or str==serNaN or str==serInf or str==serNegInf then |
-- translates just fine, transmit as-is |
res[nres+1] = "^N" |
res[nres+2] = str |
nres=nres+2 |
else |
local m,e = frexp(v) |
res[nres+1] = "^F" |
res[nres+2] = format("%.0f",m*2^53) -- force mantissa to become integer (it's originally 0.5--0.9999) |
res[nres+3] = "^f" |
res[nres+4] = tostring(e-53) -- adjust exponent to counteract mantissa manipulation |
nres=nres+4 |
end |
elseif t=="table" then -- ^T...^t = table (list of key,value pairs) |
nres=nres+1 |
res[nres] = "^T" |
for k,v in pairs(v) do |
nres = SerializeValue(k, res, nres) |
nres = SerializeValue(v, res, nres) |
end |
nres=nres+1 |
res[nres] = "^t" |
elseif t=="boolean" then -- ^B = true, ^b = false |
nres=nres+1 |
if v then |
res[nres] = "^B" -- true |
else |
res[nres] = "^b" -- false |
end |
elseif t=="nil" then -- ^Z = nil (zero, "N" was taken :P) |
nres=nres+1 |
res[nres] = "^Z" |
else |
error(MAJOR..": Cannot serialize a value of type '"..t.."'") -- can't produce error on right level, this is wildly recursive |
end |
return nres |
end |
local serializeTbl = { "^1" } -- "^1" = Hi, I'm data serialized by AceSerializer protocol rev 1 |
--- Serialize the data passed into the function. |
-- Takes a list of values (strings, numbers, booleans, nils, tables) |
-- and returns it in serialized form (a string).\\ |
-- May throw errors on invalid data types. |
-- @param ... List of values to serialize |
-- @return The data in its serialized form (string) |
function AceSerializer:Serialize(...) |
local nres = 1 |
for i=1,select("#", ...) do |
local v = select(i, ...) |
nres = SerializeValue(v, serializeTbl, nres) |
end |
serializeTbl[nres+1] = "^^" -- "^^" = End of serialized data |
return tconcat(serializeTbl, "", 1, nres+1) |
end |
-- Deserialization functions |
local function DeserializeStringHelper(escape) |
if escape<"~\123" then |
return strchar(strbyte(escape,2,2)-64) |
elseif escape=="~\123" then |
return "\127" |
elseif escape=="~\124" then |
return "\126" |
elseif escape=="~\125" then |
return "\94" |
end |
error("DeserializeStringHelper got called for '"..escape.."'?!?") -- can't be reached unless regex is screwed up |
end |
local function DeserializeNumberHelper(number) |
if number == serNaN then |
return 0/0 |
elseif number == serNegInf then |
return -1/0 |
elseif number == serInf then |
return 1/0 |
else |
return tonumber(number) |
end |
end |
-- DeserializeValue: worker function for :Deserialize() |
-- It works in two modes: |
-- Main (top-level) mode: Deserialize a list of values and return them all |
-- Recursive (table) mode: Deserialize only a single value (_may_ of course be another table with lots of subvalues in it) |
-- |
-- The function _always_ works recursively due to having to build a list of values to return |
-- |
-- Callers are expected to pcall(DeserializeValue) to trap errors |
local function DeserializeValue(iter,single,ctl,data) |
if not single then |
ctl,data = iter() |
end |
if not ctl then |
error("Supplied data misses AceSerializer terminator ('^^')") |
end |
if ctl=="^^" then |
-- ignore extraneous data |
return |
end |
local res |
if ctl=="^S" then |
res = gsub(data, "~.", DeserializeStringHelper) |
elseif ctl=="^N" then |
res = DeserializeNumberHelper(data) |
if not res then |
error("Invalid serialized number: '"..tostring(data).."'") |
end |
elseif ctl=="^F" then -- ^F<mantissa>^f<exponent> |
local ctl2,e = iter() |
if ctl2~="^f" then |
error("Invalid serialized floating-point number, expected '^f', not '"..tostring(ctl2).."'") |
end |
local m=tonumber(data) |
e=tonumber(e) |
if not (m and e) then |
error("Invalid serialized floating-point number, expected mantissa and exponent, got '"..tostring(m).."' and '"..tostring(e).."'") |
end |
res = m*(2^e) |
elseif ctl=="^B" then -- yeah yeah ignore data portion |
res = true |
elseif ctl=="^b" then -- yeah yeah ignore data portion |
res = false |
elseif ctl=="^Z" then -- yeah yeah ignore data portion |
res = nil |
elseif ctl=="^T" then |
-- ignore ^T's data, future extensibility? |
res = {} |
local k,v |
while true do |
ctl,data = iter() |
if ctl=="^t" then break end -- ignore ^t's data |
k = DeserializeValue(iter,true,ctl,data) |
if k==nil then |
error("Invalid AceSerializer table format (no table end marker)") |
end |
ctl,data = iter() |
v = DeserializeValue(iter,true,ctl,data) |
if v==nil then |
error("Invalid AceSerializer table format (no table end marker)") |
end |
res[k]=v |
end |
else |
error("Invalid AceSerializer control code '"..ctl.."'") |
end |
if not single then |
return res,DeserializeValue(iter) |
else |
return res |
end |
end |
--- Deserializes the data into its original values. |
-- Accepts serialized data, ignoring all control characters and whitespace. |
-- @param str The serialized data (from :Serialize) |
-- @return true followed by a list of values, OR false followed by an error message |
function AceSerializer:Deserialize(str) |
str = gsub(str, "[%c ]", "") -- ignore all control characters; nice for embedding in email and stuff |
local iter = gmatch(str, "(^.)([^^]*)") -- Any ^x followed by string of non-^ |
local ctl,data = iter() |
if not ctl or ctl~="^1" then |
-- we purposefully ignore the data portion of the start code, it can be used as an extension mechanism |
return false, "Supplied data is not AceSerializer data (rev 1)" |
end |
return pcall(DeserializeValue, iter) |
end |
---------------------------------------- |
-- Base library stuff |
---------------------------------------- |
AceSerializer.internals = { -- for test scripts |
SerializeValue = SerializeValue, |
SerializeStringHelper = SerializeStringHelper, |
} |
local mixins = { |
"Serialize", |
"Deserialize", |
} |
AceSerializer.embeds = AceSerializer.embeds or {} |
function AceSerializer:Embed(target) |
for k, v in pairs(mixins) do |
target[v] = self[v] |
end |
self.embeds[target] = true |
return target |
end |
-- Update embeds |
for target, v in pairs(AceSerializer.embeds) do |
AceSerializer:Embed(target) |
end |
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ |
..\FrameXML\UI.xsd"> |
<Script file="CallbackHandler-1.0.lua"/> |
</Ui> |
--[[ $Id: CallbackHandler-1.0.lua 895 2009-12-06 16:28:55Z nevcairiel $ ]] |
local MAJOR, MINOR = "CallbackHandler-1.0", 5 |
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} |
-- Lua APIs |
local tconcat = table.concat |
local assert, error, loadstring = assert, error, loadstring |
local setmetatable, rawset, rawget = setmetatable, rawset, rawget |
local next, select, pairs, type, tostring = next, select, pairs, type, tostring |
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded |
-- List them here for Mikk's FindGlobals script |
-- GLOBALS: geterrorhandler |
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", tconcat(OLD_ARGS, ", ")):gsub("ARGS", tconcat(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. |
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ |
..\FrameXML\UI.xsd"> |
<Script file="AceTimer-3.0.lua"/> |
</Ui> |
--- **AceTimer-3.0** provides a central facility for registering timers. |
-- AceTimer supports one-shot timers and repeating timers. All timers are stored in an efficient |
-- data structure that allows easy dispatching and fast rescheduling. Timers can be registered, rescheduled |
-- or canceled at any time, even from within a running timer, without conflict or large overhead.\\ |
-- AceTimer is currently limited to firing timers at a frequency of 0.1s. This constant may change |
-- in the future, but for now it seemed like a good compromise in efficiency and accuracy. |
-- |
-- All `:Schedule` functions will return a handle to the current timer, which you will need to store if you |
-- need to cancel or reschedule the timer you just registered. |
-- |
-- **AceTimer-3.0** can be embeded into your addon, either explicitly by calling AceTimer:Embed(MyAddon) or by |
-- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object |
-- and can be accessed directly, without having to explicitly call AceTimer itself.\\ |
-- It is recommended to embed AceTimer, otherwise you'll have to specify a custom `self` on all calls you |
-- make into AceTimer. |
-- @class file |
-- @name AceTimer-3.0 |
-- @release $Id: AceTimer-3.0.lua 895 2009-12-06 16:28:55Z nevcairiel $ |
--[[ |
Basic assumptions: |
* In a typical system, we do more re-scheduling per second than there are timer pulses per second |
* Regardless of timer implementation, we cannot guarantee timely delivery due to FPS restriction (may be as low as 10) |
This implementation: |
CON: The smallest timer interval is constrained by HZ (currently 1/10s). |
PRO: It will still correctly fire any timer slower than HZ over a length of time, e.g. 0.11s interval -> 90 times over 10 seconds |
PRO: In lag bursts, the system simly skips missed timer intervals to decrease load |
CON: Algorithms depending on a timer firing "N times per minute" will fail |
PRO: (Re-)scheduling is O(1) with a VERY small constant. It's a simple linked list insertion in a hash bucket. |
CAUTION: The BUCKETS constant constrains how many timers can be efficiently handled. With too many hash collisions, performance will decrease. |
Major assumptions upheld: |
- ALLOWS scheduling multiple timers with the same funcref/method |
- ALLOWS scheduling more timers during OnUpdate processing |
- ALLOWS unscheduling ANY timer (including the current running one) at any time, including during OnUpdate processing |
]] |
local MAJOR, MINOR = "AceTimer-3.0", 5 |
local AceTimer, oldminor = LibStub:NewLibrary(MAJOR, MINOR) |
if not AceTimer then return end -- No upgrade needed |
AceTimer.hash = AceTimer.hash or {} -- Array of [0..BUCKET-1] = linked list of timers (using .next member) |
-- Linked list gets around ACE-88 and ACE-90. |
AceTimer.selfs = AceTimer.selfs or {} -- Array of [self]={[handle]=timerobj, [handle2]=timerobj2, ...} |
AceTimer.frame = AceTimer.frame or CreateFrame("Frame", "AceTimer30Frame") |
-- Lua APIs |
local assert, error, loadstring = assert, error, loadstring |
local setmetatable, rawset, rawget = setmetatable, rawset, rawget |
local select, pairs, type, next, tostring = select, pairs, type, next, tostring |
local floor, max, min = math.floor, math.max, math.min |
local tconcat = table.concat |
-- WoW APIs |
local GetTime = GetTime |
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded |
-- List them here for Mikk's FindGlobals script |
-- GLOBALS: DEFAULT_CHAT_FRAME, geterrorhandler |
-- Simple ONE-SHOT timer cache. Much more efficient than a full compost for our purposes. |
local timerCache = nil |
--[[ |
Timers will not be fired more often than HZ-1 times per second. |
Keep at intended speed PLUS ONE or we get bitten by floating point rounding errors (n.5 + 0.1 can be n.599999) |
If this is ever LOWERED, all existing timers need to be enforced to have a delay >= 1/HZ on lib upgrade. |
If this number is ever changed, all entries need to be rehashed on lib upgrade. |
]] |
local HZ = 11 |
--[[ |
Prime for good distribution |
If this number is ever changed, all entries need to be rehashed on lib upgrade. |
]] |
local BUCKETS = 131 |
local hash = AceTimer.hash |
for i=1,BUCKETS do |
hash[i] = hash[i] or false -- make it an integer-indexed array; it's faster than hashes |
end |
--[[ |
xpcall safecall implementation |
]] |
local xpcall = xpcall |
local function errorhandler(err) |
return geterrorhandler()(err) |
end |
local function CreateDispatcher(argCount) |
local code = [[ |
local xpcall, eh = ... -- our arguments are received as unnamed values in "..." since we don't have a proper function declaration |
local method, ARGS |
local function call() return method(ARGS) end |
local function dispatch(func, ...) |
method = func |
if not method then return end |
ARGS = ... |
return xpcall(call, eh) |
end |
return dispatch |
]] |
local ARGS = {} |
for i = 1, argCount do ARGS[i] = "arg"..i end |
code = code:gsub("ARGS", tconcat(ARGS, ", ")) |
return assert(loadstring(code, "safecall Dispatcher["..argCount.."]"))(xpcall, errorhandler) |
end |
local Dispatchers = setmetatable({}, { |
__index=function(self, argCount) |
local dispatcher = CreateDispatcher(argCount) |
rawset(self, argCount, dispatcher) |
return dispatcher |
end |
}) |
Dispatchers[0] = function(func) |
return xpcall(func, errorhandler) |
end |
local function safecall(func, ...) |
return Dispatchers[select('#', ...)](func, ...) |
end |
local lastint = floor(GetTime() * HZ) |
-- -------------------------------------------------------------------- |
-- OnUpdate handler |
-- |
-- traverse buckets, always chasing "now", and fire timers that have expired |
local function OnUpdate() |
local now = GetTime() |
local nowint = floor(now * HZ) |
-- Have we passed into a new hash bucket? |
if nowint == lastint then return end |
local soon = now + 1 -- +1 is safe as long as 1 < HZ < BUCKETS/2 |
-- Pass through each bucket at most once |
-- Happens on e.g. instance loads, but COULD happen on high local load situations also |
for curint = (max(lastint, nowint - BUCKETS) + 1), nowint do -- loop until we catch up with "now", usually only 1 iteration |
local curbucket = (curint % BUCKETS)+1 |
-- Yank the list of timers out of the bucket and empty it. This allows reinsertion in the currently-processed bucket from callbacks. |
local nexttimer = hash[curbucket] |
hash[curbucket] = false -- false rather than nil to prevent the array from becoming a hash |
while nexttimer do |
local timer = nexttimer |
nexttimer = timer.next |
local when = timer.when |
if when < soon then |
-- Call the timer func, either as a method on given object, or a straight function ref |
local callback = timer.callback |
if type(callback) == "string" then |
safecall(timer.object[callback], timer.object, timer.arg) |
elseif callback then |
safecall(callback, timer.arg) |
else |
-- probably nilled out by CancelTimer |
timer.delay = nil -- don't reschedule it |
end |
local delay = timer.delay -- NOW make a local copy, can't do it earlier in case the timer cancelled itself in the callback |
if not delay then |
-- single-shot timer (or cancelled) |
AceTimer.selfs[timer.object][tostring(timer)] = nil |
timerCache = timer |
else |
-- repeating timer |
local newtime = when + delay |
if newtime < now then -- Keep lag from making us firing a timer unnecessarily. (Note that this still won't catch too-short-delay timers though.) |
newtime = now + delay |
end |
timer.when = newtime |
-- add next timer execution to the correct bucket |
local bucket = (floor(newtime * HZ) % BUCKETS) + 1 |
timer.next = hash[bucket] |
hash[bucket] = timer |
end |
else -- if when>=soon |
-- reinsert (yeah, somewhat expensive, but shouldn't be happening too often either due to hash distribution) |
timer.next = hash[curbucket] |
hash[curbucket] = timer |
end -- if when<soon ... else |
end -- while nexttimer do |
end -- for curint=lastint,nowint |
lastint = nowint |
end |
-- --------------------------------------------------------------------- |
-- Reg( callback, delay, arg, repeating ) |
-- |
-- callback( function or string ) - direct function ref or method name in our object for the callback |
-- delay(int) - delay for the timer |
-- arg(variant) - any argument to be passed to the callback function |
-- repeating(boolean) - repeating timer, or oneshot |
-- |
-- returns the handle of the timer for later processing (canceling etc) |
local function Reg(self, callback, delay, arg, repeating) |
if type(callback) ~= "string" and type(callback) ~= "function" then |
local error_origin = repeating and "ScheduleRepeatingTimer" or "ScheduleTimer" |
error(MAJOR..": " .. error_origin .. "(callback, delay, arg): 'callback' - function or method name expected.", 3) |
end |
if type(callback) == "string" then |
if type(self)~="table" then |
local error_origin = repeating and "ScheduleRepeatingTimer" or "ScheduleTimer" |
error(MAJOR..": " .. error_origin .. "(\"methodName\", delay, arg): 'self' - must be a table.", 3) |
end |
if type(self[callback]) ~= "function" then |
local error_origin = repeating and "ScheduleRepeatingTimer" or "ScheduleTimer" |
error(MAJOR..": " .. error_origin .. "(\"methodName\", delay, arg): 'methodName' - method not found on target object.", 3) |
end |
end |
if delay < (1 / (HZ - 1)) then |
delay = 1 / (HZ - 1) |
end |
-- Create and stuff timer in the correct hash bucket |
local now = GetTime() |
local timer = timerCache or {} -- Get new timer object (from cache if available) |
timerCache = nil |
timer.object = self |
timer.callback = callback |
timer.delay = (repeating and delay) |
timer.arg = arg |
timer.when = now + delay |
local bucket = (floor((now+delay)*HZ) % BUCKETS) + 1 |
timer.next = hash[bucket] |
hash[bucket] = timer |
-- Insert timer in our self->handle->timer registry |
local handle = tostring(timer) |
local selftimers = AceTimer.selfs[self] |
if not selftimers then |
selftimers = {} |
AceTimer.selfs[self] = selftimers |
end |
selftimers[handle] = timer |
selftimers.__ops = (selftimers.__ops or 0) + 1 |
return handle |
end |
--- Schedule a new one-shot timer. |
-- The timer will fire once in `delay` seconds, unless canceled before. |
-- @param callback Callback function for the timer pulse (funcref or method name). |
-- @param delay Delay for the timer, in seconds. |
-- @param arg An optional argument to be passed to the callback function. |
-- @usage |
-- MyAddon = LibStub("AceAddon-3.0"):NewAddon("TimerTest", "AceTimer-3.0") |
-- |
-- function MyAddon:OnEnable() |
-- self:ScheduleTimer("TimerFeedback", 5) |
-- end |
-- |
-- function MyAddon:TimerFeedback() |
-- print("5 seconds passed") |
-- end |
function AceTimer:ScheduleTimer(callback, delay, arg) |
return Reg(self, callback, delay, arg) |
end |
--- Schedule a repeating timer. |
-- The timer will fire every `delay` seconds, until canceled. |
-- @param callback Callback function for the timer pulse (funcref or method name). |
-- @param delay Delay for the timer, in seconds. |
-- @param arg An optional argument to be passed to the callback function. |
-- @usage |
-- MyAddon = LibStub("AceAddon-3.0"):NewAddon("TimerTest", "AceTimer-3.0") |
-- |
-- function MyAddon:OnEnable() |
-- self.timerCount = 0 |
-- self.testTimer = self:ScheduleRepeatingTimer("TimerFeedback", 5) |
-- end |
-- |
-- function MyAddon:TimerFeedback() |
-- self.timerCount = self.timerCount + 1 |
-- print(("%d seconds passed"):format(5 * self.timerCount)) |
-- -- run 30 seconds in total |
-- if self.timerCount == 6 then |
-- self:CancelTimer(self.testTimer) |
-- end |
-- end |
function AceTimer:ScheduleRepeatingTimer(callback, delay, arg) |
return Reg(self, callback, delay, arg, true) |
end |
--- Cancels a timer with the given handle, registered by the same addon object as used for `:ScheduleTimer` |
-- Both one-shot and repeating timers can be canceled with this function, as long as the `handle` is valid |
-- and the timer has not fired yet or was canceled before. |
-- @param handle The handle of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer` |
-- @param silent If true, no error is raised if the timer handle is invalid (expired or already canceled) |
-- @return True if the timer was successfully cancelled. |
function AceTimer:CancelTimer(handle, silent) |
if not handle then return end -- nil handle -> bail out without erroring |
if type(handle) ~= "string" then |
error(MAJOR..": CancelTimer(handle): 'handle' - expected a string", 2) -- for now, anyway |
end |
local selftimers = AceTimer.selfs[self] |
local timer = selftimers and selftimers[handle] |
if silent then |
if timer then |
timer.callback = nil -- don't run it again |
timer.delay = nil -- if this is the currently-executing one: don't even reschedule |
-- The timer object is removed in the OnUpdate loop |
end |
return not not timer -- might return "true" even if we double-cancel. we'll live. |
else |
if not timer then |
geterrorhandler()(MAJOR..": CancelTimer(handle[, silent]): '"..tostring(handle).."' - no such timer registered") |
return false |
end |
if not timer.callback then |
geterrorhandler()(MAJOR..": CancelTimer(handle[, silent]): '"..tostring(handle).."' - timer already cancelled or expired") |
return false |
end |
timer.callback = nil -- don't run it again |
timer.delay = nil -- if this is the currently-executing one: don't even reschedule |
return true |
end |
end |
--- Cancels all timers registered to the current addon object ('self') |
function AceTimer:CancelAllTimers() |
if not(type(self) == "string" or type(self) == "table") then |
error(MAJOR..": CancelAllTimers(): 'self' - must be a string or a table",2) |
end |
if self == AceTimer then |
error(MAJOR..": CancelAllTimers(): supply a meaningful 'self'", 2) |
end |
local selftimers = AceTimer.selfs[self] |
if selftimers then |
for handle,v in pairs(selftimers) do |
if type(v) == "table" then -- avoid __ops, etc |
AceTimer.CancelTimer(self, handle, true) |
end |
end |
end |
end |
--- Returns the time left for a timer with the given handle, registered by the current addon object ('self'). |
-- This function will raise a warning when the handle is invalid, but not stop execution. |
-- @param handle The handle of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer` |
-- @return The time left on the timer, or false if the handle is invalid. |
function AceTimer:TimeLeft(handle) |
if not handle then return end |
if type(handle) ~= "string" then |
error(MAJOR..": TimeLeft(handle): 'handle' - expected a string", 2) -- for now, anyway |
end |
local selftimers = AceTimer.selfs[self] |
local timer = selftimers and selftimers[handle] |
if not timer then |
geterrorhandler()(MAJOR..": TimeLeft(handle): '"..tostring(handle).."' - no such timer registered") |
return false |
end |
return timer.when - GetTime() |
end |
-- --------------------------------------------------------------------- |
-- PLAYER_REGEN_ENABLED: Run through our .selfs[] array step by step |
-- and clean it out - otherwise the table indices can grow indefinitely |
-- if an addon starts and stops a lot of timers. AceBucket does this! |
-- |
-- See ACE-94 and tests/AceTimer-3.0-ACE-94.lua |
local lastCleaned = nil |
local function OnEvent(this, event) |
if event~="PLAYER_REGEN_ENABLED" then |
return |
end |
-- Get the next 'self' to process |
local selfs = AceTimer.selfs |
local self = next(selfs, lastCleaned) |
if not self then |
self = next(selfs) |
end |
lastCleaned = self |
if not self then -- should only happen if .selfs[] is empty |
return |
end |
-- Time to clean it out? |
local list = selfs[self] |
if (list.__ops or 0) < 250 then -- 250 slosh indices = ~10KB wasted (max!). For one 'self'. |
return |
end |
-- Create a new table and copy all members over |
local newlist = {} |
local n=0 |
for k,v in pairs(list) do |
newlist[k] = v |
n=n+1 |
end |
newlist.__ops = 0 -- Reset operation count |
-- And since we now have a count of the number of live timers, check that it's reasonable. Emit a warning if not. |
if n>BUCKETS then |
DEFAULT_CHAT_FRAME:AddMessage(MAJOR..": Warning: The addon/module '"..tostring(self).."' has "..n.." live timers. Surely that's not intended?") |
end |
selfs[self] = newlist |
end |
-- --------------------------------------------------------------------- |
-- Embed handling |
AceTimer.embeds = AceTimer.embeds or {} |
local mixins = { |
"ScheduleTimer", "ScheduleRepeatingTimer", |
"CancelTimer", "CancelAllTimers", |
"TimeLeft" |
} |
function AceTimer:Embed(target) |
AceTimer.embeds[target] = true |
for _,v in pairs(mixins) do |
target[v] = AceTimer[v] |
end |
return target |
end |
-- AceTimer:OnEmbedDisable( target ) |
-- target (object) - target object that AceTimer is embedded in. |
-- |
-- cancel all timers registered for the object |
function AceTimer:OnEmbedDisable( target ) |
target:CancelAllTimers() |
end |
for addon in pairs(AceTimer.embeds) do |
AceTimer:Embed(addon) |
end |
-- --------------------------------------------------------------------- |
-- Debug tools (expose copies of internals to test suites) |
AceTimer.debug = AceTimer.debug or {} |
AceTimer.debug.HZ = HZ |
AceTimer.debug.BUCKETS = BUCKETS |
-- --------------------------------------------------------------------- |
-- Finishing touchups |
AceTimer.frame:SetScript("OnUpdate", OnUpdate) |
AceTimer.frame:SetScript("OnEvent", OnEvent) |
AceTimer.frame:RegisterEvent("PLAYER_REGEN_ENABLED") |
-- In theory, we should hide&show the frame based on there being timers or not. |
-- However, this job is fairly expensive, and the chance that there will |
-- actually be zero timers running is diminuitive to say the lest. |
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ |
..\FrameXML\UI.xsd"> |
<Script file="LibCooldownIcons-1.0.lua"/> |
</Ui> |
--[[ |
##AddOn : CooldownIcons R(evamped) |
##Subfile : LibCooldownIcons-1.0.lua |
##Author : Hati-EK |
--]] |
local MAJOR, MINOR = "LibCooldownIcons", "1" |
local lib = LibStub:NewLibrary(MAJOR, MINOR) |
local CIR = CooldownIconsR |
if not lib then return end |
local Icon_Nil = "Interface\\Icons\\INV_Misc_QuestionMark" |
-- i = LCI:tonumber <tonumber>, i<-R |
-- i = LCI:tonumber (extended tonumber), i<-R |
function lib:tonumber(s) |
if type(s)=='boolean' then |
if s then |
return 1 |
else |
return 0 |
end |
else |
return tonumber(s) |
end |
end |
-- i = LCI:Getn(table), i<-N |
function lib:Getn(t) |
local i=0 |
for k,v in pairs(t) do |
i=i+1 |
end |
return i |
end |
-- b,i = LCI:inTable(v)| b<-{false,true}, i<-{v[indexes]} |
function lib:inTable(value,tbl) |
local b=false |
local num=0 |
if type(tbl)~='table' and value==nil then |
error('Failure in arguments. Arguments:( value='..tostring(value)..', table='..tostring(tbl)..')') |
return false,nil |
end |
if type(tbl)=='table' then |
for i,v in pairs(tbl) do |
if v==value then |
b=true |
num=i |
end |
end |
end |
return b,num |
end |
function lib:indexInTable(value,tbl) |
local b=false |
local index=0 |
if type(tbl)~='table' or value==nil then |
error('Failure in arguments. Arguments:( value='..tostring(value)..', table='..tostring(tbl)..')') |
return false,nil |
end |
for i,v in pairs(tbl) do |
if i==value then |
b=true |
index=i |
end |
end |
return b,index |
end |
--##Texture Part |
-- i = LCI:GetTexture(name), i<-TexturePath |
function lib:GetTexture(name) |
return Icon_Nil |
end |
function lib:GetConditionalByCase(case) |
local r,e |
if case=='stance' then |
r=GetShapeshiftForm(true) |
e='UPDATE_SHAPESHIFT_FORM' |
elseif case=='talentgroup' then |
r=GetActiveTalentGroup(false,false) |
e='ACTIVE_TALENT_GROUP_CHANGED' |
--elseif case=='buff' then |
end |
return r,e |
end |
--adds a Frame to a Table an event runs-through on trigger and returns the "new" table |
function lib:RegisterFrameForEvent(frameid,event,t) |
--create event in t if not already existent |
if not t[event] then |
t[event]={} |
end |
if not self:inTable(frameid,t[event]) then |
tinsert(t[event],frameid) |
end |
return t |
end |
function lib:UnregisterFrameForEvent(frameid,event,t) |
--create event in t if not already existent |
if not t[event] then return end |
local inTable,num = self:inTable(frameid,t[event]) |
if inTable then tremove(t[event],num) end |
return t |
end |
function lib:UnregisterFrameForAllEvents(frameid,t) |
if #t>0 then |
for e in pairs(t) do |
self:UnregisterFrameForEvent(frameid,e,t) |
end |
end |
end |
function lib:GetCooldownByCmd(cmd,watch) |
if cmd=='spell' then |
return GetSpellCooldown(tonumber(watch) or watch) |
elseif cmd=='item' then |
return GetItemCooldown(tonumber(watch) or watch) |
elseif cmd=='flag' then |
return GetInventoryItemCooldown("player", watch) |
else |
error(tostring(cmd)..' is an invalid Command') |
end |
end |
function lib:GetRangeByCmd(cmd,input) |
if input then |
if cmd=='spell' then |
return IsSpellInRange(GetSpellInfo(input),"target") |
elseif cmd=='item' then |
return IsItemInRange(GetItemInfo(input),"target") |
elseif cmd=='flag' then |
local itemLink = GetInventoryItemLink("player", input) |
return IsItemInRange(itemLink, "target") |
else |
error(tostring(cmd)..' is an invalid Command') |
end |
end |
end |
--Function makes a copy of the table without the key |
--and return this new tbl |
function lib:DeleteFromTableByKey(tbl,value) |
local store={} |
for key,content in pairs(tbl) do |
if key~=value then |
store[key]=content |
end |
end |
return store |
end |
function lib:deepcopy(object) |
local lookup_table = {} |
local function _copy(object) |
if type(object) ~= "table" then |
return object |
elseif lookup_table[object] then |
return lookup_table[object] |
end |
local new_table = {} |
lookup_table[object] = new_table |
for index, value in pairs(object) do |
new_table[_copy(index)] = _copy(value) |
end |
return setmetatable(new_table, _copy(object)) |
end |
return _copy(object) |
end |
local fade_in = {} |
local fade_out= {} |
--improved UIFrameFadeIn(frame,time,startAlpha,endAlpha,type) |
function lib:UIFrameFadeIn(frame,t,startAlpha,endAlpha,typ) |
local now=GetTime() |
if typ=='in' then |
local IsInTable,num = self:inTable(frame.realname,fade_in) |
if not IsInTable then |
UIFrameFadeIn(frame,t,startAlpha,endAlpha) |
--tinsert(frame,{icon_name,endTime}) |
tinsert(fade_in,{frame.realname,GetTime()+t}) |
else |
if now>fade_in[num][2] then |
tremove(fade_in,num) |
end |
end |
elseif typ=='out' then |
local IsInTable,num = self:inTable(frame.realname,fade_out) |
if not IsInTable then |
UIFrameFadeIn(frame,t,startAlpha,endAlpha) |
tinsert(fade_out,{frame.realname,GetTime()+t}) |
else |
if now>fade_in[num][2] then |
tremove(fade_out,num) |
end |
end |
end |
end |
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ |
..\FrameXML\UI.xsd"> |
<Script file="AceLocale-3.0.lua"/> |
</Ui> |
--- **AceLocale-3.0** manages localization in addons, allowing for multiple locale to be registered with fallback to the base locale for untranslated strings. |
-- @class file |
-- @name AceLocale-3.0 |
-- @release $Id: AceLocale-3.0.lua 895 2009-12-06 16:28:55Z nevcairiel $ |
local MAJOR,MINOR = "AceLocale-3.0", 2 |
local AceLocale, oldminor = LibStub:NewLibrary(MAJOR, MINOR) |
if not AceLocale then return end -- no upgrade needed |
-- Lua APIs |
local assert, tostring, error = assert, tostring, error |
local setmetatable, rawset, rawget = setmetatable, rawset, rawget |
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded |
-- List them here for Mikk's FindGlobals script |
-- GLOBALS: GAME_LOCALE, geterrorhandler |
local gameLocale = GetLocale() |
if gameLocale == "enGB" then |
gameLocale = "enUS" |
end |
AceLocale.apps = AceLocale.apps or {} -- array of ["AppName"]=localetableref |
AceLocale.appnames = AceLocale.appnames or {} -- array of [localetableref]="AppName" |
-- This metatable is used on all tables returned from GetLocale |
local readmeta = { |
__index = function(self, key) -- requesting totally unknown entries: fire off a nonbreaking error and return key |
rawset(self, key, key) -- only need to see the warning once, really |
geterrorhandler()(MAJOR..": "..tostring(AceLocale.appnames[self])..": Missing entry for '"..tostring(key).."'") |
return key |
end |
} |
-- This metatable is used on all tables returned from GetLocale if the silent flag is true, it does not issue a warning on unknown keys |
local readmetasilent = { |
__index = function(self, key) -- requesting totally unknown entries: return key |
rawset(self, key, key) -- only need to invoke this function once |
return key |
end |
} |
-- Remember the locale table being registered right now (it gets set by :NewLocale()) |
-- NOTE: Do never try to register 2 locale tables at once and mix their definition. |
local registering |
-- local assert false function |
local assertfalse = function() assert(false) end |
-- This metatable proxy is used when registering nondefault locales |
local writeproxy = setmetatable({}, { |
__newindex = function(self, key, value) |
rawset(registering, key, value == true and key or value) -- assigning values: replace 'true' with key string |
end, |
__index = assertfalse |
}) |
-- This metatable proxy is used when registering the default locale. |
-- It refuses to overwrite existing values |
-- Reason 1: Allows loading locales in any order |
-- Reason 2: If 2 modules have the same string, but only the first one to be |
-- loaded has a translation for the current locale, the translation |
-- doesn't get overwritten. |
-- |
local writedefaultproxy = setmetatable({}, { |
__newindex = function(self, key, value) |
if not rawget(registering, key) then |
rawset(registering, key, value == true and key or value) |
end |
end, |
__index = assertfalse |
}) |
--- Register a new locale (or extend an existing one) for the specified application. |
-- :NewLocale will return a table you can fill your locale into, or nil if the locale isn't needed for the players |
-- game locale. |
-- @paramsig application, locale[, isDefault[, silent]] |
-- @param application Unique name of addon / module |
-- @param locale Name of the locale to register, e.g. "enUS", "deDE", etc. |
-- @param isDefault If this is the default locale being registered (your addon is written in this language, generally enUS) |
-- @param silent If true, the locale will not issue warnings for missing keys. Can only be set on the default locale. |
-- @usage |
-- -- enUS.lua |
-- local L = LibStub("AceLocale-3.0"):NewLocale("TestLocale", "enUS", true) |
-- L["string1"] = true |
-- |
-- -- deDE.lua |
-- local L = LibStub("AceLocale-3.0"):NewLocale("TestLocale", "deDE") |
-- if not L then return end |
-- L["string1"] = "Zeichenkette1" |
-- @return Locale Table to add localizations to, or nil if the current locale is not required. |
function AceLocale:NewLocale(application, locale, isDefault, silent) |
if silent and not isDefault then |
error("Usage: NewLocale(application, locale[, isDefault[, silent]]): 'silent' can only be specified for the default locale", 2) |
end |
-- GAME_LOCALE allows translators to test translations of addons without having that wow client installed |
-- Ammo: I still think this is a bad idea, for instance an addon that checks for some ingame string will fail, just because some other addon |
-- gives the user the illusion that they can run in a different locale? Ditch this whole thing or allow a setting per 'application'. I'm of the |
-- opinion to remove this. |
local gameLocale = GAME_LOCALE or gameLocale |
if locale ~= gameLocale and not isDefault then |
return -- nop, we don't need these translations |
end |
local app = AceLocale.apps[application] |
if not app then |
app = setmetatable({}, silent and readmetasilent or readmeta) |
AceLocale.apps[application] = app |
AceLocale.appnames[app] = application |
end |
registering = app -- remember globally for writeproxy and writedefaultproxy |
if isDefault then |
return writedefaultproxy |
end |
return writeproxy |
end |
--- Returns localizations for the current locale (or default locale if translations are missing). |
-- Errors if nothing is registered (spank developer, not just a missing translation) |
-- @param application Unique name of addon / module |
-- @param silent If true, the locale is optional, silently return nil if it's not found (defaults to false, optional) |
-- @return The locale table for the current language. |
function AceLocale:GetLocale(application, silent) |
if not silent and not AceLocale.apps[application] then |
error("Usage: GetLocale(application[, silent]): 'application' - No locales registered for '"..tostring(application).."'", 2) |
end |
return AceLocale.apps[application] |
end |
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ |
..\FrameXML\UI.xsd"> |
<Script file="AceHook-3.0.lua"/> |
</Ui> |
--- **AceHook-3.0** offers safe Hooking/Unhooking of functions, methods and frame scripts. |
-- Using AceHook-3.0 is recommended when you need to unhook your hooks again, so the hook chain isn't broken |
-- when you manually restore the original function. |
-- |
-- **AceHook-3.0** can be embeded into your addon, either explicitly by calling AceHook:Embed(MyAddon) or by |
-- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object |
-- and can be accessed directly, without having to explicitly call AceHook itself.\\ |
-- It is recommended to embed AceHook, otherwise you'll have to specify a custom `self` on all calls you |
-- make into AceHook. |
-- @class file |
-- @name AceHook-3.0 |
-- @release $Id: AceHook-3.0.lua 877 2009-11-02 15:56:50Z nevcairiel $ |
local ACEHOOK_MAJOR, ACEHOOK_MINOR = "AceHook-3.0", 5 |
local AceHook, oldminor = LibStub:NewLibrary(ACEHOOK_MAJOR, ACEHOOK_MINOR) |
if not AceHook then return end -- No upgrade needed |
AceHook.embeded = AceHook.embeded or {} |
AceHook.registry = AceHook.registry or setmetatable({}, {__index = function(tbl, key) tbl[key] = {} return tbl[key] end }) |
AceHook.handlers = AceHook.handlers or {} |
AceHook.actives = AceHook.actives or {} |
AceHook.scripts = AceHook.scripts or {} |
AceHook.onceSecure = AceHook.onceSecure or {} |
AceHook.hooks = AceHook.hooks or {} |
-- local upvalues |
local registry = AceHook.registry |
local handlers = AceHook.handlers |
local actives = AceHook.actives |
local scripts = AceHook.scripts |
local onceSecure = AceHook.onceSecure |
-- Lua APIs |
local pairs, next, type = pairs, next, type |
local format = string.format |
local assert, error = assert, error |
-- WoW APIs |
local issecurevariable, hooksecurefunc = issecurevariable, hooksecurefunc |
local _G = _G |
-- functions for later definition |
local donothing, createHook, hook |
local protectedScripts = { |
OnClick = true, |
} |
-- upgrading of embeded is done at the bottom of the file |
local mixins = { |
"Hook", "SecureHook", |
"HookScript", "SecureHookScript", |
"Unhook", "UnhookAll", |
"IsHooked", |
"RawHook", "RawHookScript" |
} |
-- AceHook:Embed( target ) |
-- target (object) - target object to embed AceHook in |
-- |
-- Embeds AceEevent into the target object making the functions from the mixins list available on target:.. |
function AceHook:Embed( target ) |
for k, v in pairs( mixins ) do |
target[v] = self[v] |
end |
self.embeded[target] = true |
-- inject the hooks table safely |
target.hooks = target.hooks or {} |
return target |
end |
-- AceHook:OnEmbedDisable( target ) |
-- target (object) - target object that is being disabled |
-- |
-- Unhooks all hooks when the target disables. |
-- this method should be called by the target manually or by an addon framework |
function AceHook:OnEmbedDisable( target ) |
target:UnhookAll() |
end |
function createHook(self, handler, orig, secure, failsafe) |
local uid |
local method = type(handler) == "string" |
if failsafe and not secure then |
-- failsafe hook creation |
uid = function(...) |
if actives[uid] then |
if method then |
self[handler](self, ...) |
else |
handler(...) |
end |
end |
return orig(...) |
end |
-- /failsafe hook |
else |
-- all other hooks |
uid = function(...) |
if actives[uid] then |
if method then |
return self[handler](self, ...) |
else |
return handler(...) |
end |
elseif not secure then -- backup on non secure |
return orig(...) |
end |
end |
-- /hook |
end |
return uid |
end |
function donothing() end |
function hook(self, obj, method, handler, script, secure, raw, forceSecure, usage) |
if not handler then handler = method end |
-- These asserts make sure AceHooks's devs play by the rules. |
assert(not script or type(script) == "boolean") |
assert(not secure or type(secure) == "boolean") |
assert(not raw or type(raw) == "boolean") |
assert(not forceSecure or type(forceSecure) == "boolean") |
assert(usage) |
-- Error checking Battery! |
if obj and type(obj) ~= "table" then |
error(format("%s: 'object' - nil or table expected got %s", usage, type(obj)), 3) |
end |
if type(method) ~= "string" then |
error(format("%s: 'method' - string expected got %s", usage, type(method)), 3) |
end |
if type(handler) ~= "string" and type(handler) ~= "function" then |
error(format("%s: 'handler' - nil, string, or function expected got %s", usage, type(handler)), 3) |
end |
if type(handler) == "string" and type(self[handler]) ~= "function" then |
error(format("%s: 'handler' - Handler specified does not exist at self[handler]", usage), 3) |
end |
if script then |
if not secure and obj:IsProtected() and protectedScripts[method] then |
error(format("Cannot hook secure script %q; Use SecureHookScript(obj, method, [handler]) instead.", method), 3) |
end |
if not obj or not obj.GetScript or not obj:HasScript(method) then |
error(format("%s: You can only hook a script on a frame object", usage), 3) |
end |
else |
local issecure |
if obj then |
issecure = onceSecure[obj] and onceSecure[obj][method] or issecurevariable(obj, method) |
else |
issecure = onceSecure[method] or issecurevariable(method) |
end |
if issecure then |
if forceSecure then |
if obj then |
onceSecure[obj] = onceSecure[obj] or {} |
onceSecure[obj][method] = true |
else |
onceSecure[method] = true |
end |
elseif not secure then |
error(format("%s: Attempt to hook secure function %s. Use `SecureHook' or add `true' to the argument list to override.", usage, method), 3) |
end |
end |
end |
local uid |
if obj then |
uid = registry[self][obj] and registry[self][obj][method] |
else |
uid = registry[self][method] |
end |
if uid then |
if actives[uid] then |
-- Only two sane choices exist here. We either a) error 100% of the time or b) always unhook and then hook |
-- choice b would likely lead to odd debuging conditions or other mysteries so we're going with a. |
error(format("Attempting to rehook already active hook %s.", method)) |
end |
if handlers[uid] == handler then -- turn on a decative hook, note enclosures break this ability, small memory leak |
actives[uid] = true |
return |
elseif obj then -- is there any reason not to call unhook instead of doing the following several lines? |
if self.hooks and self.hooks[obj] then |
self.hooks[obj][method] = nil |
end |
registry[self][obj][method] = nil |
else |
if self.hooks then |
self.hooks[method] = nil |
end |
registry[self][method] = nil |
end |
handlers[uid], actives[uid], scripts[uid] = nil, nil, nil |
uid = nil |
end |
local orig |
if script then |
orig = obj:GetScript(method) or donothing |
elseif obj then |
orig = obj[method] |
else |
orig = _G[method] |
end |
if not orig then |
error(format("%s: Attempting to hook a non existing target", usage), 3) |
end |
uid = createHook(self, handler, orig, secure, not (raw or secure)) |
if obj then |
self.hooks[obj] = self.hooks[obj] or {} |
registry[self][obj] = registry[self][obj] or {} |
registry[self][obj][method] = uid |
if not secure then |
self.hooks[obj][method] = orig |
end |
if script then |
-- If the script is empty before, HookScript will not work, so use SetScript instead |
-- This will make the hook insecure, but shouldnt matter, since it was empty before. |
-- It does not taint the full frame. |
if not secure or orig == donothing then |
obj:SetScript(method, uid) |
elseif secure then |
obj:HookScript(method, uid) |
end |
else |
if not secure then |
obj[method] = uid |
else |
hooksecurefunc(obj, method, uid) |
end |
end |
else |
registry[self][method] = uid |
if not secure then |
_G[method] = uid |
self.hooks[method] = orig |
else |
hooksecurefunc(method, uid) |
end |
end |
actives[uid], handlers[uid], scripts[uid] = true, handler, script and true or nil |
end |
--- Hook a function or a method on an object. |
-- The hook created will be a "safe hook", that means that your handler will be called |
-- before the hooked function ("Pre-Hook"), and you don't have to call the original function yourself, |
-- however you cannot stop the execution of the function, or modify any of the arguments/return values.\\ |
-- This type of hook is typically used if you need to know if some function got called, and don't want to modify it. |
-- @paramsig [object], method, [handler], [hookSecure] |
-- @param object The object to hook a method from |
-- @param method If object was specified, the name of the method, or the name of the function to hook. |
-- @param handler The handler for the hook, a funcref or a method name. (Defaults to the name of the hooked function) |
-- @param hookSecure If true, AceHook will allow hooking of secure functions. |
-- @usage |
-- -- create an addon with AceHook embeded |
-- MyAddon = LibStub("AceAddon-3.0"):NewAddon("HookDemo", "AceHook-3.0") |
-- |
-- function MyAddon:OnEnable() |
-- -- Hook ActionButton_UpdateHotkeys, overwriting the secure status |
-- self:Hook("ActionButton_UpdateHotkeys", true) |
-- end |
-- |
-- function MyAddon:ActionButton_UpdateHotkeys(button, type) |
-- print(button:GetName() .. " is updating its HotKey") |
-- end |
function AceHook:Hook(object, method, handler, hookSecure) |
if type(object) == "string" then |
method, handler, hookSecure, object = object, method, handler, nil |
end |
if handler == true then |
handler, hookSecure = nil, true |
end |
hook(self, object, method, handler, false, false, false, hookSecure or false, "Usage: Hook([object], method, [handler], [hookSecure])") |
end |
--- RawHook a function or a method on an object. |
-- The hook created will be a "raw hook", that means that your handler will completly replace |
-- the original function, and your handler has to call the original function (or not, depending on your intentions).\\ |
-- The original function will be stored in `self.hooks[object][method]` or `self.hooks[functionName]` respectively.\\ |
-- This type of hook can be used for all purposes, and is usually the most common case when you need to modify arguments |
-- or want to control execution of the original function. |
-- @paramsig [object], method, [handler], [hookSecure] |
-- @param object The object to hook a method from |
-- @param method If object was specified, the name of the method, or the name of the function to hook. |
-- @param handler The handler for the hook, a funcref or a method name. (Defaults to the name of the hooked function) |
-- @param hookSecure If true, AceHook will allow hooking of secure functions. |
-- @usage |
-- -- create an addon with AceHook embeded |
-- MyAddon = LibStub("AceAddon-3.0"):NewAddon("HookDemo", "AceHook-3.0") |
-- |
-- function MyAddon:OnEnable() |
-- -- Hook ActionButton_UpdateHotkeys, overwriting the secure status |
-- self:RawHook("ActionButton_UpdateHotkeys", true) |
-- end |
-- |
-- function MyAddon:ActionButton_UpdateHotkeys(button, type) |
-- if button:GetName() == "MyButton" then |
-- -- do stuff here |
-- else |
-- self.hooks.ActionButton_UpdateHotkeys(button, type) |
-- end |
-- end |
function AceHook:RawHook(object, method, handler, hookSecure) |
if type(object) == "string" then |
method, handler, hookSecure, object = object, method, handler, nil |
end |
if handler == true then |
handler, hookSecure = nil, true |
end |
hook(self, object, method, handler, false, false, true, hookSecure or false, "Usage: RawHook([object], method, [handler], [hookSecure])") |
end |
--- SecureHook a function or a method on an object. |
-- This function is a wrapper around the `hooksecurefunc` function in the WoW API. Using AceHook |
-- extends the functionality of secure hooks, and adds the ability to unhook once the hook isn't |
-- required anymore, or the addon is being disabled.\\ |
-- Secure Hooks should be used if the secure-status of the function is vital to its function, |
-- and taint would block execution. Secure Hooks are always called after the original function was called |
-- ("Post Hook"), and you cannot modify the arguments, return values or control the execution. |
-- @paramsig [object], method, [handler] |
-- @param object The object to hook a method from |
-- @param method If object was specified, the name of the method, or the name of the function to hook. |
-- @param handler The handler for the hook, a funcref or a method name. (Defaults to the name of the hooked function) |
function AceHook:SecureHook(object, method, handler) |
if type(object) == "string" then |
method, handler, object = object, method, nil |
end |
hook(self, object, method, handler, false, true, false, false, "Usage: SecureHook([object], method, [handler])") |
end |
--- Hook a script handler on a frame. |
-- The hook created will be a "safe hook", that means that your handler will be called |
-- before the hooked script ("Pre-Hook"), and you don't have to call the original function yourself, |
-- however you cannot stop the execution of the function, or modify any of the arguments/return values.\\ |
-- This is the frame script equivalent of the :Hook safe-hook. It would typically be used to be notified |
-- when a certain event happens to a frame. |
-- @paramsig frame, script, [handler] |
-- @param frame The Frame to hook the script on |
-- @param script The script to hook |
-- @param handler The handler for the hook, a funcref or a method name. (Defaults to the name of the hooked script) |
-- @usage |
-- -- create an addon with AceHook embeded |
-- MyAddon = LibStub("AceAddon-3.0"):NewAddon("HookDemo", "AceHook-3.0") |
-- |
-- function MyAddon:OnEnable() |
-- -- Hook the OnShow of FriendsFrame |
-- self:HookScript(FriendsFrame, "OnShow", "FriendsFrameOnShow") |
-- end |
-- |
-- function MyAddon:FriendsFrameOnShow(frame) |
-- print("The FriendsFrame was shown!") |
-- end |
function AceHook:HookScript(frame, script, handler) |
hook(self, frame, script, handler, true, false, false, false, "Usage: HookScript(object, method, [handler])") |
end |
--- RawHook a script handler on a frame. |
-- The hook created will be a "raw hook", that means that your handler will completly replace |
-- the original script, and your handler has to call the original script (or not, depending on your intentions).\\ |
-- The original script will be stored in `self.hooks[frame][script]`.\\ |
-- This type of hook can be used for all purposes, and is usually the most common case when you need to modify arguments |
-- or want to control execution of the original script. |
-- @paramsig frame, script, [handler] |
-- @param frame The Frame to hook the script on |
-- @param script The script to hook |
-- @param handler The handler for the hook, a funcref or a method name. (Defaults to the name of the hooked script) |
-- @usage |
-- -- create an addon with AceHook embeded |
-- MyAddon = LibStub("AceAddon-3.0"):NewAddon("HookDemo", "AceHook-3.0") |
-- |
-- function MyAddon:OnEnable() |
-- -- Hook the OnShow of FriendsFrame |
-- self:RawHookScript(FriendsFrame, "OnShow", "FriendsFrameOnShow") |
-- end |
-- |
-- function MyAddon:FriendsFrameOnShow(frame) |
-- -- Call the original function |
-- self.hooks[frame].OnShow(frame) |
-- -- Do our processing |
-- -- .. stuff |
-- end |
function AceHook:RawHookScript(frame, script, handler) |
hook(self, frame, script, handler, true, false, true, false, "Usage: RawHookScript(object, method, [handler])") |
end |
--- SecureHook a script handler on a frame. |
-- This function is a wrapper around the `frame:HookScript` function in the WoW API. Using AceHook |
-- extends the functionality of secure hooks, and adds the ability to unhook once the hook isn't |
-- required anymore, or the addon is being disabled.\\ |
-- Secure Hooks should be used if the secure-status of the function is vital to its function, |
-- and taint would block execution. Secure Hooks are always called after the original function was called |
-- ("Post Hook"), and you cannot modify the arguments, return values or control the execution. |
-- @paramsig frame, script, [handler] |
-- @param frame The Frame to hook the script on |
-- @param script The script to hook |
-- @param handler The handler for the hook, a funcref or a method name. (Defaults to the name of the hooked script) |
function AceHook:SecureHookScript(frame, script, handler) |
hook(self, frame, script, handler, true, true, false, false, "Usage: SecureHookScript(object, method, [handler])") |
end |
--- Unhook from the specified function, method or script. |
-- @paramsig [obj], method |
-- @param obj The object or frame to unhook from |
-- @param method The name of the method, function or script to unhook from. |
function AceHook:Unhook(obj, method) |
local usage = "Usage: Unhook([obj], method)" |
if type(obj) == "string" then |
method, obj = obj, nil |
end |
if obj and type(obj) ~= "table" then |
error(format("%s: 'obj' - expecting nil or table got %s", usage, type(obj)), 2) |
end |
if type(method) ~= "string" then |
error(format("%s: 'method' - expeting string got %s", usage, type(method)), 2) |
end |
local uid |
if obj then |
uid = registry[self][obj] and registry[self][obj][method] |
else |
uid = registry[self][method] |
end |
if not uid or not actives[uid] then |
-- Declining to error on an unneeded unhook since the end effect is the same and this would just be annoying. |
return false |
end |
actives[uid], handlers[uid] = nil, nil |
if obj then |
registry[self][obj][method] = nil |
registry[self][obj] = next(registry[self][obj]) and registry[self][obj] or nil |
-- if the hook reference doesnt exist, then its a secure hook, just bail out and dont do any unhooking |
if not self.hooks[obj] or not self.hooks[obj][method] then return true end |
if scripts[uid] and obj:GetScript(method) == uid then -- unhooks scripts |
obj:SetScript(method, self.hooks[obj][method] ~= donothing and self.hooks[obj][method] or nil) |
scripts[uid] = nil |
elseif obj and self.hooks[obj] and self.hooks[obj][method] and obj[method] == uid then -- unhooks methods |
obj[method] = self.hooks[obj][method] |
end |
self.hooks[obj][method] = nil |
self.hooks[obj] = next(self.hooks[obj]) and self.hooks[obj] or nil |
else |
registry[self][method] = nil |
-- if self.hooks[method] doesn't exist, then this is a SecureHook, just bail out |
if not self.hooks[method] then return true end |
if self.hooks[method] and _G[method] == uid then -- unhooks functions |
_G[method] = self.hooks[method] |
end |
self.hooks[method] = nil |
end |
return true |
end |
--- Unhook all existing hooks for this addon. |
function AceHook:UnhookAll() |
for key, value in pairs(registry[self]) do |
if type(key) == "table" then |
for method in pairs(value) do |
self:Unhook(key, method) |
end |
else |
self:Unhook(key) |
end |
end |
end |
--- Check if the specific function, method or script is already hooked. |
-- @paramsig [obj], method |
-- @param obj The object or frame to unhook from |
-- @param method The name of the method, function or script to unhook from. |
function AceHook:IsHooked(obj, method) |
-- we don't check if registry[self] exists, this is done by evil magicks in the metatable |
if type(obj) == "string" then |
if registry[self][obj] and actives[registry[self][obj]] then |
return true, handlers[registry[self][obj]] |
end |
else |
if registry[self][obj] and registry[self][obj][method] and actives[registry[self][obj][method]] then |
return true, handlers[registry[self][obj][method]] |
end |
end |
return false, nil |
end |
--- Upgrade our old embeded |
for target, v in pairs( AceHook.embeded ) do |
AceHook:Embed( target ) |
end |
--[[ |
LibDebugLog-1.0 - Library providing simple debug logging for WoW addons |
Copyright (C) 2008 Adirelle |
This program is free software; you can redistribute it and/or |
modify it under the terms of the GNU General Public License |
as published by the Free Software Foundation; either version 2 |
of the License, or (at your option) any later version. |
This program is distributed in the hope that it will be useful, |
but WITHOUT ANY WARRANTY; without even the implied warranty of |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
GNU General Public License for more details. |
You should have received a copy of the GNU General Public License |
along with this program; if not, write to the Free Software |
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
--]] |
local MAJOR, MINOR = "LibDebugLog-1.0", 100000 + tonumber(("$Revision: 20 $"):match("%d+")) |
assert(LibStub, MAJOR.." requires LibStub") |
local CBH = LibStub("CallbackHandler-1.0",false) |
assert(CBH, MAJOR.." requires CallbackHandler-1.0") |
local lib = LibStub:NewLibrary(MAJOR, MINOR) |
if not lib then return end |
lib.loggers = lib.loggers or {} |
lib.debugging = lib.debugging or {} |
lib.mixins = lib.mixins or {} |
lib.options = lib.options or {} |
lib.callbacks = lib.callbacks or CBH:New(lib) |
function lib.callbacks:OnUsed(_, event) |
if event == "MessageLogged" then |
lib.quiet = true |
end |
end |
function lib.callbacks:OnUnused(_, event) |
if event == "MessageLogged" then |
lib.quiet = false |
end |
end |
local loggers = lib.loggers |
local debugging = lib.debugging |
local mixins = lib.mixins |
local options = lib.options |
function mixins:ToggleDebugLog(value) |
value = not not value |
if value ~= debugging[self] then |
if not value then |
self:Debug("Debug disabled") |
end |
debugging[self] = value |
if value then |
self:Debug("Debug enabled") |
end |
lib.callbacks:Fire(value and 'DebugLogEnabled' or 'DebugLogDisabled', self, value) |
end |
end |
function mixins:IsDebugLogEnabled() |
return debugging[self] |
end |
do |
-- Based on AceDebug-2.0 |
local tmp = {} |
local function LogMessage(self, a1, ...) |
if lib.globalToggle == false or (lib.globalToggle == nil and not debugging[self]) then return end |
local message, now, n = "", GetTime(), select('#', ...) |
for i=1,n do tmp[i] = tostring(select(i, ...)) end |
a1 = tostring(a1) |
if a1:find("%%") and n >= 1 then |
message = a1:format(unpack(tmp)) |
else |
message = a1 .. " " .. table.concat(tmp, " ") |
end |
for i=1,n do tmp[i] = nil end |
if lib.quiet then |
lib.callbacks:Fire("MessageLogged", self, now, message) |
else |
DEFAULT_CHAT_FRAME:AddMessage( |
("|cff7fff7f(DEBUG) %s:[%s.%3d]|r: %s"):format( |
tostring(self), date("%H:%M:%S"), (now % 1) * 1000, message |
) |
) |
end |
end |
function mixins:Debug(...) |
local success, errorMsg = pcall(LogMessage, self, ...) |
if not success then |
geterrorhandler()(errorMsg) |
end |
end |
end |
function lib:SetGlobalToggle(value) |
if value ~= nil then |
value = not not value |
end |
if value ~= lib.globalToggle then |
lib.globalToggle = value |
lib.callbacks:Fire(value and 'GlobalDebugEnabled' or 'GlobalDebugDisabled', value) |
end |
end |
function lib:GetGlobalToggle() |
return lib.globalToggle |
end |
function lib:GetAce3OptionTable(logger, order) |
if not loggers[logger] then return end |
if not options[logger] then |
options[logger] = { |
name = 'Enable debug logs', |
type = 'toggle', |
get = function(info) return logger:IsDebugLogEnabled() end, |
set = function(info, value) return logger:ToggleDebugLog(value) end, |
disabled = function() return lib.globalToggle ~= nil end, |
order = order or 100, |
} |
end |
return options[logger] |
end |
do |
local function iter(onlyNamed, logger) |
while true do |
local name |
logger, name = next(loggers, logger) |
if not logger then |
return |
elseif not onlyNamed or name ~= true then |
return logger, name |
end |
end |
end |
function lib:IterateLoggers(onlyNamed) |
return iter, onlyNamed |
end |
end |
function lib:Embed(logger) |
assert(type(logger) == 'table') |
for name,func in pairs(mixins) do |
logger[name] = func |
end |
if not loggers[logger] then |
local name = rawget(logger, 'name') |
loggers[logger] = name or true |
self.callbacks:Fire('NewLogger', logger, name) |
self.callbacks:Fire('NewBroker', logger, name) -- Compat |
end |
end |
function lib:GetLoggerName(logger) |
local name = loggers[logger] |
return name ~= true and name or nil |
end |
lib.GetBrokerName = lib.GetLoggerName -- Compat |
lib.IterateBrokers = lib.IterateLoggers -- Compat |
for logger in pairs(loggers) do |
lib:Embed(logger) |
end |
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\FrameXML\UI.xsd"> |
<Script file="LibDebugLog-1.0.lua"/> |
</Ui> |
--- AceConfigTab-3.0 provides support for tab-completion to AceConfig tables. |
-- Note: This library is not yet finalized. |
-- @class file |
-- @name AceConfigTab-3.0 |
-- @release $Id: AceConfigTab-3.0.lua 769 2009-04-04 11:05:08Z nevcairiel $ |
local MAJOR, MINOR = "AceConfigTab-3.0", 1 |
local lib = LibStub:NewLibrary(MAJOR, MINOR) |
if not lib then return end |
local ac = LibStub("AceConsole-3.0") |
local function printf(...) |
DEFAULT_CHAT_FRAME:AddMessage(string.format(...)) |
end |
-- getChildren(opt, ...) |
-- |
-- Retrieve the next valid group args in an AceConfig table. |
-- |
-- opt - AceConfig options table |
-- ... - args following the slash command |
-- |
-- opt will need to be determined by the slash-command |
-- The args will be obtained using AceConsole:GetArgs() or something similar on the remainder of the line. |
-- |
-- Returns arg1, arg2, ... |
local function getLevel(opt, ...) |
-- Walk down the options tree to the last arg in the commandline, or return if it does not follow the tree. |
local path = "" |
local lastChild |
for i = 1, select('#', ...) do |
local arg = select(i, ...) |
if not arg or type(arg) == 'number' then break end |
if opt.plugins then |
for k in pairs(opt.plugins) do |
if string.lower(k) == string.lower(arg) then |
opt = opt.plugins[k] |
path = path..arg.." " |
lastChild = arg |
break |
end |
end |
elseif opt.args then |
for k in pairs(opt.args) do |
if string.lower(k) == string.lower(arg) then |
opt = opt.args[k] |
path = path..arg.." " |
lastChild = arg |
break |
end |
end |
else |
break |
end |
end |
return opt, path |
end |
local function getChildren(opt, ...) |
local lastChild, path |
opt, path, lastChild = getLevel(opt, ...) |
local args = {} |
for _, field in ipairs({"args", "plugins"}) do |
if type(opt[field]) == 'table' then |
for k in pairs(opt[field]) do |
if opt[field].type ~= 'header' then |
table.insert(args, k) |
end |
end |
end |
end |
return args, path |
end |
--LibStub("AceConfig-3.0"):RegisterOptionsTable("ag_UnitFrames", aUF.Options.table) |
local function createWordlist(t, cmdline, pos) |
local cmd = string.match(cmdline, "(/[^ \t\n]+)") |
local argslist = string.sub(cmdline, pos, this:GetCursorPosition()) |
local opt -- TODO: figure out options table using cmd |
opt = LibStub("AceConfigRegistry-3.0"):GetOptionsTable("ag_UnitFrames", "cmd", "AceTab-3.0") -- hardcoded temporarily for testing |
if not opt then return end |
local args, path = getChildren(opt, ac:GetArgs(argslist, #argslist/2)) -- largest # of args representable by a string of length #argslist, since they must be separated by spaces |
for _, v in ipairs(args) do |
table.insert(t, path..v) |
end |
end |
local function usage(t, matches, _, cmdline) |
local cmd = string.match(cmdline, "(/[^ \t\n]+)") |
local argslist = string.sub(cmdline, #cmd, this:GetCursorPosition()) |
local opt -- TODO: figure out options table using cmd |
opt = LibStub("AceConfigRegistry-3.0"):GetOptionsTable("ag_UnitFrames")("cmd", "AceTab-3.0") -- hardcoded temporarily for testing |
if not opt then return end |
local level = getLevel(opt, ac:GetArgs(argslist, #argslist/2)) -- largest # of args representable by a string of length #argslist, since they must be separated by spaces |
local option |
for _, m in pairs(matches) do |
local tail = string.match(m, "([^ \t\n]+)$") |
option = level.plugins and level.plugins[tail] or level.args and level.args[tail] |
printf("%s - %s", tail, option.desc) |
end |
end |
LibStub("AceTab-3.0"):RegisterTabCompletion("aguftest", "%/%w+ ", createWordlist, usage) |
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ |
..\FrameXML\UI.xsd"> |
<Script file="AceTab-3.0.lua"/> |
<Script file="AceConfigTab-3.0.lua"/> |
</Ui> |
--- AceTab-3.0 provides support for tab-completion. |
-- Note: This library is not yet finalized. |
-- @class file |
-- @name AceTab-3.0 |
-- @release $Id: AceTab-3.0.lua 905 2009-12-15 16:48:32Z nevcairiel $ |
local ACETAB_MAJOR, ACETAB_MINOR = 'AceTab-3.0', 5 |
local AceTab, oldminor = LibStub:NewLibrary(ACETAB_MAJOR, ACETAB_MINOR) |
if not AceTab then return end -- No upgrade needed |
AceTab.registry = AceTab.registry or {} |
-- local upvalues |
local _G = _G |
local pairs = pairs |
local ipairs = ipairs |
local type = type |
local registry = AceTab.registry |
local strfind = string.find |
local strsub = string.sub |
local strlower = string.lower |
local strformat = string.format |
local strmatch = string.match |
local function printf(...) |
DEFAULT_CHAT_FRAME:AddMessage(strformat(...)) |
end |
local function getTextBeforeCursor(this, start) |
return strsub(this:GetText(), start or 1, this:GetCursorPosition()) |
end |
-- Hook OnTabPressed and OnTextChanged for the frame, give it an empty matches table, and set its curMatch to 0, if we haven't done so already. |
local function hookFrame(f) |
if f.hookedByAceTab3 then return end |
f.hookedByAceTab3 = true |
if f == ChatFrameEditBox then |
local origCTP = ChatEdit_CustomTabPressed |
function ChatEdit_CustomTabPressed() |
if AceTab:OnTabPressed(f) then |
return origCTP() |
else |
return true |
end |
end |
else |
local origOTP = f:GetScript('OnTabPressed') |
if type(origOTP) ~= 'function' then |
origOTP = function() end |
end |
f:SetScript('OnTabPressed', function() |
if AceTab:OnTabPressed(f) then |
return origOTP() |
end |
end) |
end |
f.at3curMatch = 0 |
f.at3matches = {} |
end |
local firstPMLength |
local fallbacks, notfallbacks = {}, {} -- classifies completions into those which have preconditions and those which do not. Those without preconditions are only considered if no other completions have matches. |
local pmolengths = {} -- holds the number of characters to overwrite according to pmoverwrite and the current prematch |
-- ------------------------------------------------------------------------------ |
-- RegisterTabCompletion( descriptor, prematches, wordlist, usagefunc, listenframes, postfunc, pmoverwrite ) |
-- See http://www.wowace.com/wiki/AceTab-2.0 for detailed API documentation |
-- |
-- descriptor string Unique identifier for this tab completion set |
-- |
-- prematches string|table|nil String match(es) AFTER which this tab completion will apply. |
-- AceTab will ignore tabs NOT preceded by the string(s). |
-- If no value is passed, will check all tabs pressed in the specified editframe(s) UNLESS a more-specific tab complete applies. |
-- |
-- wordlist function|table Function that will be passed a table into which it will insert strings corresponding to all possible completions, or an equivalent table. |
-- The text in the editbox, the position of the start of the word to be completed, and the uncompleted partial word |
-- are passed as second, third, and fourth arguments, to facilitate pre-filtering or conditional formatting, if desired. |
-- |
-- usagefunc function|boolean|nil Usage statement function. Defaults to the wordlist, one per line. A boolean true squelches usage output. |
-- |
-- listenframes string|table|nil EditFrames to monitor. Defaults to ChatFrameEditBox. |
-- |
-- postfunc function|nil Post-processing function. If supplied, matches will be passed through this function after they've been identified as a match. |
-- |
-- pmoverwrite boolean|number|nil Offset the beginning of the completion string in the editbox when making a completion. Passing a boolean true indicates that we want to overwrite |
-- the entire prematch string, and passing a number will overwrite that many characters prior to the cursor. |
-- This is useful when you want to use the prematch as an indicator character, but ultimately do not want it as part of the text, itself. |
-- |
-- no return |
-- ------------------------------------------------------------------------------ |
function AceTab:RegisterTabCompletion(descriptor, prematches, wordlist, usagefunc, listenframes, postfunc, pmoverwrite) |
-- Arg checks |
if type(descriptor) ~= 'string' then error("Usage: RegisterTabCompletion(descriptor, prematches, wordlist, usagefunc, listenframes, postfunc, pmoverwrite): 'descriptor' - string expected.", 3) end |
if prematches and type(prematches) ~= 'string' and type(prematches) ~= 'table' then error("Usage: RegisterTabCompletion(descriptor, prematches, wordlist, usagefunc, listenframes, postfunc, pmoverwrite): 'prematches' - string, table, or nil expected.", 3) end |
if type(wordlist) ~= 'function' and type(wordlist) ~= 'table' then error("Usage: RegisterTabCompletion(descriptor, prematches, wordlist, usagefunc, listenframes, postfunc, pmoverwrite): 'wordlist' - function or table expected.", 3) end |
if usagefunc and type(usagefunc) ~= 'function' and type(usagefunc) ~= 'boolean' then error("Usage: RegisterTabCompletion(descriptor, prematches, wordlist, usagefunc, listenframes, postfunc, pmoverwrite): 'usagefunc' - function or boolean expected.", 3) end |
if listenframes and type(listenframes) ~= 'string' and type(listenframes) ~= 'table' then error("Usage: RegisterTabCompletion(descriptor, prematches, wordlist, usagefunc, listenframes, postfunc, pmoverwrite): 'listenframes' - string or table expected.", 3) end |
if postfunc and type(postfunc) ~= 'function' then error("Usage: RegisterTabCompletion(descriptor, prematches, wordlist, usagefunc, listenframes, postfunc, pmoverwrite): 'postfunc' - function expected.", 3) end |
if pmoverwrite and type(pmoverwrite) ~= 'boolean' and type(pmoverwrite) ~= 'number' then error("Usage: RegisterTabCompletion(descriptor, prematches, wordlist, usagefunc, listenframes, postfunc, pmoverwrite): 'pmoverwrite' - boolean or number expected.", 3) end |
local pmtable = type(prematches) == 'table' and prematches or {} |
-- Mark this group as a fallback group if no value was passed. |
if not prematches then |
pmtable[1] = "" |
fallbacks[descriptor] = true |
-- Make prematches into a one-element table if it was passed as a string. |
elseif type(prematches) == 'string' then |
pmtable[1] = prematches |
if prematches == "" then |
fallbacks[descriptor] = true |
else |
notfallbacks[descriptor] = true |
end |
end |
-- Make listenframes into a one-element table if it was not passed a table of frames. |
if not listenframes then -- default |
listenframes = { ChatFrameEditBox } |
elseif type(listenframes) ~= 'table' or type(listenframes[0]) == 'userdata' and type(listenframes.IsObjectType) == 'function' then -- single frame or framename |
listenframes = { listenframes } |
end |
-- Hook each registered listenframe and give it a matches table. |
for _, f in pairs(listenframes) do |
if type(f) == 'string' then |
f = _G[f] |
end |
if type(f) ~= 'table' or type(f[0]) ~= 'userdata' or type(f.IsObjectType) ~= 'function' then |
error(format(ACETAB_MAJOR..": Cannot register frame %q; it does not exist", f:GetName())) |
end |
if f then |
if f:GetObjectType() ~= 'EditBox' then |
error(format(ACETAB_MAJOR..": Cannot register frame %q; it is not an EditBox", f:GetName())) |
else |
hookFrame(f) |
end |
end |
end |
-- Everything checks out; register this completion. |
if not registry[descriptor] then |
registry[descriptor] = { prematches = pmtable, wordlist = wordlist, usagefunc = usagefunc, listenframes = listenframes, postfunc = postfunc, pmoverwrite = pmoverwrite } |
end |
end |
function AceTab:IsTabCompletionRegistered(descriptor) |
return registry and registry[descriptor] |
end |
function AceTab:UnregisterTabCompletion(descriptor) |
registry[descriptor] = nil |
pmolengths[descriptor] = nil |
fallbacks[descriptor] = nil |
notfallbacks[descriptor] = nil |
end |
-- ------------------------------------------------------------------------------ |
-- gcbs( s1, s2 ) |
-- |
-- s1 string First string to be compared |
-- |
-- s2 string Second string to be compared |
-- |
-- returns the greatest common substring beginning s1 and s2 |
-- ------------------------------------------------------------------------------ |
local function gcbs(s1, s2) |
if not s1 and not s2 then return end |
if not s1 then s1 = s2 end |
if not s2 then s2 = s1 end |
if #s2 < #s1 then |
s1, s2 = s2, s1 |
end |
if strfind(strlower(s2), "^"..strlower(s1)) then |
return s1 |
else |
return gcbs(strsub(s1, 1, -2), s2) |
end |
end |
local cursor -- Holds cursor position. Set in :OnTabPressed(). |
-- ------------------------------------------------------------------------------ |
-- cycleTab() |
-- For when a tab press has multiple possible completions, we need to allow the user to press tab repeatedly to cycle through them. |
-- If we have multiple possible completions, all tab presses after the first will call this function to cycle through and insert the different possible matches. |
-- This function will stop being called after OnTextChanged() is triggered by something other than AceTab (i.e. the user inputs a character). |
-- ------------------------------------------------------------------------------ |
local previousLength, cMatch, matched, postmatch |
local function cycleTab(this) |
cMatch = 0 -- Counter across all sets. The pseudo-index relevant to this value and corresponding to the current match is held in this.at3curMatch |
matched = false |
-- Check each completion group registered to this frame. |
for desc, compgrp in pairs(this.at3matches) do |
-- Loop through the valid completions for this set. |
for m, pm in pairs(compgrp) do |
cMatch = cMatch + 1 |
if cMatch == this.at3curMatch then -- we're back to where we left off last time through the combined list |
this.at3lastMatch = m |
this.at3lastWord = pm |
this.at3curMatch = cMatch + 1 -- save the new cMatch index |
matched = true |
break |
end |
end |
if matched then break end |
end |
-- If our index is beyond the end of the list, reset the original uncompleted substring and let the cycle start over next time tab is pressed. |
if not matched then |
this.at3lastMatch = this.at3origMatch |
this.at3lastWord = this.at3origWord |
this.at3curMatch = 1 |
end |
-- Insert the completion. |
this:HighlightText(this.at3matchStart-1, cursor) |
this:Insert(this.at3lastWord or '') |
this.at3_last_precursor = getTextBeforeCursor(this) or '' |
end |
local IsSecureCmd = IsSecureCmd |
local cands, candUsage = {}, {} |
local numMatches = 0 |
local firstMatch, hasNonFallback, allGCBS, setGCBS, usage |
local text_precursor, text_all, text_pmendToCursor |
local matches, usagefunc -- convenience locals |
-- Fill the this.at3matches[descriptor] tables with matching completion pairs for each entry, based on |
-- the partial string preceding the cursor position and using the corresponding registered wordlist. |
-- |
-- The entries of the matches tables are of the format raw_match = formatted_match, where raw_match is the plaintext completion and |
-- formatted_match is the match after being formatted/altered/processed by the registered postfunc. |
-- If no postfunc exists, then the formatted and raw matches are the same. |
local pms, pme, pmt, prematchStart, prematchEnd, text_prematch, entry |
local function fillMatches(this, desc, fallback) |
entry = registry[desc] |
-- See what frames are registered for this completion group. If the frame in which we pressed tab is one of them, then we start building matches. |
for _, f in ipairs(entry.listenframes) do |
if f == this then |
-- Try each precondition string registered for this completion group. |
for _, prematch in ipairs(entry.prematches) do |
-- Test if our prematch string is satisfied. |
-- If it is, then we find its last occurence prior to the cursor, calculate and store its pmoverwrite value (if applicable), and start considering completions. |
if fallback then prematch = "%s" end |
-- Find the last occurence of the prematch before the cursor. |
pms, pme, pmt = nil, 1, '' |
text_prematch, prematchEnd, prematchStart = nil, nil, nil |
while true do |
pms, pme, pmt = strfind(text_precursor, "("..prematch..")", pme) |
if pms then |
prematchStart, prematchEnd, text_prematch = pms, pme, pmt |
pme = pme + 1 |
else |
break |
end |
end |
if not prematchStart and fallback then |
prematchStart, prematchEnd, text_prematch = 0, 0, '' |
end |
if prematchStart then |
-- text_pmendToCursor should be the sub-word/phrase to be completed. |
text_pmendToCursor = strsub(text_precursor, prematchEnd + 1) |
-- How many characters should we eliminate before the completion before writing it in. |
pmolengths[desc] = entry.pmoverwrite == true and #text_prematch or entry.pmoverwrite or 0 |
-- This is where we will insert completions, taking the prematch overwrite into account. |
this.at3matchStart = prematchEnd + 1 - (pmolengths[desc] or 0) |
-- We're either a non-fallback set or all completions thus far have been fallback sets, and the precondition matches. |
-- Create cands from the registered wordlist, filling it with all potential (unfiltered) completion strings. |
local wordlist = entry.wordlist |
local cands = type(wordlist) == 'table' and wordlist or {} |
if type(wordlist) == 'function' then |
wordlist(cands, text_all, prematchEnd + 1, text_pmendToCursor) |
end |
if cands ~= false then |
matches = this.at3matches[desc] or {} |
for i in pairs(matches) do matches[i] = nil end |
-- Check each of the entries in cands to see if it completes the word before the cursor. |
-- Finally, increment our match count and set firstMatch, if appropriate. |
for _, m in ipairs(cands) do |
if strfind(strlower(m), strlower(text_pmendToCursor), 1, 1) == 1 then -- we have a matching completion! |
hasNonFallback = not fallback |
matches[m] = entry.postfunc and entry.postfunc(m, prematchEnd + 1, text_all) or m |
numMatches = numMatches + 1 |
if numMatches == 1 then |
firstMatch = matches[m] |
firstPMLength = pmolengths[desc] or 0 |
end |
end |
end |
this.at3matches[desc] = numMatches > 0 and matches or nil |
end |
end |
end |
end |
end |
end |
function AceTab:OnTabPressed(this) |
if this:GetText() == '' then return true end |
-- allow Blizzard to handle slash commands, themselves |
if this == ChatFrameEditBox then |
local command = this:GetText() |
if strfind(command, "^/[%a%d_]+$") then |
return true |
end |
local cmd = strmatch(command, "^/[%a%d_]+") |
if cmd and IsSecureCmd(cmd) then |
return true |
end |
end |
cursor = this:GetCursorPosition() |
text_all = this:GetText() |
text_precursor = getTextBeforeCursor(this) or '' |
-- If we've already found some matches and haven't done anything since the last tab press, then (continue) cycling matches. |
-- Otherwise, reset this frame's matches and proceed to creating our list of possible completions. |
this.at3lastMatch = this.at3curMatch > 0 and (this.at3lastMatch or this.at3origWord) |
-- Detects if we've made any edits since the last tab press. If not, continue cycling completions. |
if text_precursor == this.at3_last_precursor then |
return cycleTab(this) |
else |
for i in pairs(this.at3matches) do this.at3matches[i] = nil end |
this.at3curMatch = 0 |
this.at3origWord = nil |
this.at3origMatch = nil |
this.at3lastWord = nil |
this.at3lastMatch = nil |
this.at3_last_precursor = text_precursor |
end |
numMatches = 0 |
firstMatch = nil |
firstPMLength = 0 |
hasNonFallback = false |
for i in pairs(pmolengths) do pmolengths[i] = nil end |
for desc in pairs(notfallbacks) do |
fillMatches(this, desc) |
end |
if not hasNonFallback then |
for desc in pairs(fallbacks) do |
fillMatches(this, desc, true) |
end |
end |
if not firstMatch then |
this.at3_last_precursor = "\0" |
return true |
end |
-- We want to replace the entire word with our completion, so highlight it up to the cursor. |
-- If only one match exists, then stick it in there and append a space. |
if numMatches == 1 then |
-- HighlightText takes the value AFTER which the highlighting starts, so we have to subtract 1 to have it start before the first character. |
this:HighlightText(this.at3matchStart-1, cursor) |
this:Insert(firstMatch) |
this:Insert(" ") |
else |
-- Otherwise, we want to begin cycling through the valid completions. |
-- Beginning a cycle also causes the usage statement to be printed, if one exists. |
-- Print usage statements for each possible completion (and gather up the GCBS of all matches while we're walking the tables). |
allGCBS = nil |
for desc, matches in pairs(this.at3matches) do |
-- Don't print usage statements for fallback completion groups if we have 'real' completion groups with matches. |
if hasNonFallback and fallbacks[desc] then break end |
-- Use the group's description as a heading for its usage statements. |
DEFAULT_CHAT_FRAME:AddMessage(desc..":") |
usagefunc = registry[desc].usagefunc |
if not usagefunc then |
-- No special usage processing; just print a list of the (formatted) matches. |
for m, fm in pairs(matches) do |
DEFAULT_CHAT_FRAME:AddMessage(fm) |
allGCBS = gcbs(allGCBS, m) |
end |
else |
-- Print a usage statement based on the corresponding registered usagefunc. |
-- candUsage is the table passed to usagefunc to be filled with candidate = usage_statement pairs. |
if type(usagefunc) == 'function' then |
for i in pairs(candUsage) do candUsage[i] = nil end |
-- usagefunc takes the greatest common substring of valid matches as one of its args, so let's find that now. |
-- TODO: Make the GCBS function accept a vararg or table, after which we can just pass in the list of matches. |
setGCBS = nil |
for m in pairs(matches) do |
setGCBS = gcbs(setGCBS, m) |
end |
allGCBS = gcbs(allGCBS, setGCBS) |
usage = usagefunc(candUsage, matches, setGCBS, strsub(text_precursor, 1, prematchEnd)) |
-- If the usagefunc returns a string, then the entire usage statement has been taken care of by usagefunc, and we need only to print it... |
if type(usage) == 'string' then |
DEFAULT_CHAT_FRAME:AddMessage(usage) |
-- ...otherwise, it should have filled candUsage with candidate-usage statement pairs, and we need to print the matching ones. |
elseif next(candUsage) and numMatches > 0 then |
for m, fm in pairs(matches) do |
if candUsage[m] then DEFAULT_CHAT_FRAME:AddMessage(strformat("%s - %s", fm, candUsage[m])) end |
end |
end |
end |
end |
-- Replace the original string with the greatest common substring of all valid completions. |
this.at3curMatch = 1 |
this.at3origWord = strsub(text_precursor, this.at3matchStart, this.at3matchStart + pmolengths[desc] - 1) .. allGCBS or "" |
this.at3origMatch = allGCBS or "" |
this.at3lastWord = this.at3origWord |
this.at3lastMatch = this.at3origMatch |
this:HighlightText(this.at3matchStart-1, cursor) |
this:Insert(this.at3origWord) |
this.at3_last_precursor = getTextBeforeCursor(this) or '' |
end |
end |
end |