8ba6b3c9ddb8dd1ace8aa849490d1f159f7e6c7b
[wowui.git] / libs / AceTimer-3.0 / AceTimer-3.0.lua
1 --- **AceTimer-3.0** provides a central facility for registering timers.
2 -- AceTimer supports one-shot timers and repeating timers. All timers are stored in an efficient
3 -- data structure that allows easy dispatching and fast rescheduling. Timers can be registered
4 -- or canceled at any time, even from within a running timer, without conflict or large overhead.\\
5 -- AceTimer is currently limited to firing timers at a frequency of 0.01s as this is what the WoW timer API
6 -- restricts us to.
7 --
8 -- All `:Schedule` functions will return a handle to the current timer, which you will need to store if you
9 -- need to cancel the timer you just registered.
10 --
11 -- **AceTimer-3.0** can be embeded into your addon, either explicitly by calling AceTimer:Embed(MyAddon) or by
12 -- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object
13 -- and can be accessed directly, without having to explicitly call AceTimer itself.\\
14 -- It is recommended to embed AceTimer, otherwise you'll have to specify a custom `self` on all calls you
15 -- make into AceTimer.
16 -- @class file
17 -- @name AceTimer-3.0
18 -- @release $Id: AceTimer-3.0.lua 1119 2014-10-14 17:23:29Z nevcairiel $
19
20 local MAJOR, MINOR = "AceTimer-3.0", 17 -- Bump minor on changes
21 local AceTimer, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
22
23 if not AceTimer then return end -- No upgrade needed
24 AceTimer.activeTimers = AceTimer.activeTimers or {} -- Active timer list
25 local activeTimers = AceTimer.activeTimers -- Upvalue our private data
26
27 -- Lua APIs
28 local type, unpack, next, error, select = type, unpack, next, error, select
29 -- WoW APIs
30 local GetTime, C_TimerAfter = GetTime, C_Timer.After
31
32 local function new(self, loop, func, delay, ...)
33         if delay < 0.01 then
34                 delay = 0.01 -- Restrict to the lowest time that the C_Timer API allows us
35         end
36
37         local timer = {...}
38         timer.object = self
39         timer.func = func
40         timer.looping = loop
41         timer.argsCount = select("#", ...)
42         timer.delay = delay
43         timer.ends = GetTime() + delay
44
45         activeTimers[timer] = timer
46
47         -- Create new timer closure to wrap the "timer" object
48         timer.callback = function() 
49                 if not timer.cancelled then
50                         if type(timer.func) == "string" then
51                                 -- We manually set the unpack count to prevent issues with an arg set that contains nil and ends with nil
52                                 -- e.g. local t = {1, 2, nil, 3, nil} print(#t) will result in 2, instead of 5. This fixes said issue.
53                                 timer.object[timer.func](timer.object, unpack(timer, 1, timer.argsCount))
54                         else
55                                 timer.func(unpack(timer, 1, timer.argsCount))
56                         end
57
58                         if timer.looping and not timer.cancelled then
59                                 -- Compensate delay to get a perfect average delay, even if individual times don't match up perfectly
60                                 -- due to fps differences
61                                 local time = GetTime()
62                                 local delay = timer.delay - (time - timer.ends)
63                                 -- Ensure the delay doesn't go below the threshold
64                                 if delay < 0.01 then delay = 0.01 end
65                                 C_TimerAfter(delay, timer.callback)
66                                 timer.ends = time + delay
67                         else
68                                 activeTimers[timer.handle or timer] = nil
69                         end
70                 end
71         end
72
73         C_TimerAfter(delay, timer.callback)
74         return timer
75 end
76
77 --- Schedule a new one-shot timer.
78 -- The timer will fire once in `delay` seconds, unless canceled before.
79 -- @param callback Callback function for the timer pulse (funcref or method name).
80 -- @param delay Delay for the timer, in seconds.
81 -- @param ... An optional, unlimited amount of arguments to pass to the callback function.
82 -- @usage
83 -- MyAddOn = LibStub("AceAddon-3.0"):NewAddon("MyAddOn", "AceTimer-3.0")
84 --
85 -- function MyAddOn:OnEnable()
86 --   self:ScheduleTimer("TimerFeedback", 5)
87 -- end
88 --
89 -- function MyAddOn:TimerFeedback()
90 --   print("5 seconds passed")
91 -- end
92 function AceTimer:ScheduleTimer(func, delay, ...)
93         if not func or not delay then
94                 error(MAJOR..": ScheduleTimer(callback, delay, args...): 'callback' and 'delay' must have set values.", 2)
95         end
96         if type(func) == "string" then
97                 if type(self) ~= "table" then
98                         error(MAJOR..": ScheduleTimer(callback, delay, args...): 'self' - must be a table.", 2)
99                 elseif not self[func] then
100                         error(MAJOR..": ScheduleTimer(callback, delay, args...): Tried to register '"..func.."' as the callback, but it doesn't exist in the module.", 2)
101                 end
102         end
103         return new(self, nil, func, delay, ...)
104 end
105
106 --- Schedule a repeating timer.
107 -- The timer will fire every `delay` seconds, until canceled.
108 -- @param callback Callback function for the timer pulse (funcref or method name).
109 -- @param delay Delay for the timer, in seconds.
110 -- @param ... An optional, unlimited amount of arguments to pass to the callback function.
111 -- @usage
112 -- MyAddOn = LibStub("AceAddon-3.0"):NewAddon("MyAddOn", "AceTimer-3.0")
113 --
114 -- function MyAddOn:OnEnable()
115 --   self.timerCount = 0
116 --   self.testTimer = self:ScheduleRepeatingTimer("TimerFeedback", 5)
117 -- end
118 --
119 -- function MyAddOn:TimerFeedback()
120 --   self.timerCount = self.timerCount + 1
121 --   print(("%d seconds passed"):format(5 * self.timerCount))
122 --   -- run 30 seconds in total
123 --   if self.timerCount == 6 then
124 --     self:CancelTimer(self.testTimer)
125 --   end
126 -- end
127 function AceTimer:ScheduleRepeatingTimer(func, delay, ...)
128         if not func or not delay then
129                 error(MAJOR..": ScheduleRepeatingTimer(callback, delay, args...): 'callback' and 'delay' must have set values.", 2)
130         end
131         if type(func) == "string" then
132                 if type(self) ~= "table" then
133                         error(MAJOR..": ScheduleRepeatingTimer(callback, delay, args...): 'self' - must be a table.", 2)
134                 elseif not self[func] then
135                         error(MAJOR..": ScheduleRepeatingTimer(callback, delay, args...): Tried to register '"..func.."' as the callback, but it doesn't exist in the module.", 2)
136                 end
137         end
138         return new(self, true, func, delay, ...)
139 end
140
141 --- Cancels a timer with the given id, registered by the same addon object as used for `:ScheduleTimer`
142 -- Both one-shot and repeating timers can be canceled with this function, as long as the `id` is valid
143 -- and the timer has not fired yet or was canceled before.
144 -- @param id The id of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer`
145 function AceTimer:CancelTimer(id)
146         local timer = activeTimers[id]
147
148         if not timer then
149                 return false
150         else
151                 timer.cancelled = true
152                 activeTimers[id] = nil
153                 return true
154         end
155 end
156
157 --- Cancels all timers registered to the current addon object ('self')
158 function AceTimer:CancelAllTimers()
159         for k,v in pairs(activeTimers) do
160                 if v.object == self then
161                         AceTimer.CancelTimer(self, k)
162                 end
163         end
164 end
165
166 --- Returns the time left for a timer with the given id, registered by the current addon object ('self').
167 -- This function will return 0 when the id is invalid.
168 -- @param id The id of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer`
169 -- @return The time left on the timer.
170 function AceTimer:TimeLeft(id)
171         local timer = activeTimers[id]
172         if not timer then
173                 return 0
174         else
175                 return timer.ends - GetTime()
176         end
177 end
178
179
180 -- ---------------------------------------------------------------------
181 -- Upgrading
182
183 -- Upgrade from old hash-bucket based timers to C_Timer.After timers.
184 if oldminor and oldminor < 10 then
185         -- disable old timer logic
186         AceTimer.frame:SetScript("OnUpdate", nil)
187         AceTimer.frame:SetScript("OnEvent", nil)
188         AceTimer.frame:UnregisterAllEvents()
189         -- convert timers
190         for object,timers in pairs(AceTimer.selfs) do
191                 for handle,timer in pairs(timers) do
192                         if type(timer) == "table" and timer.callback then
193                                 local newTimer
194                                 if timer.delay then
195                                         newTimer = AceTimer.ScheduleRepeatingTimer(timer.object, timer.callback, timer.delay, timer.arg)
196                                 else
197                                         newTimer = AceTimer.ScheduleTimer(timer.object, timer.callback, timer.when - GetTime(), timer.arg)
198                                 end
199                                 -- Use the old handle for old timers
200                                 activeTimers[newTimer] = nil
201                                 activeTimers[handle] = newTimer
202                                 newTimer.handle = handle
203                         end
204                 end
205         end
206         AceTimer.selfs = nil
207         AceTimer.hash = nil
208         AceTimer.debug = nil
209 elseif oldminor and oldminor < 17 then
210         -- Upgrade from old animation based timers to C_Timer.After timers.
211         AceTimer.inactiveTimers = nil
212         AceTimer.frame = nil
213         local oldTimers = AceTimer.activeTimers
214         -- Clear old timer table and update upvalue
215         AceTimer.activeTimers = {}
216         activeTimers = AceTimer.activeTimers
217         for handle, timer in pairs(oldTimers) do
218                 local newTimer
219                 -- Stop the old timer animation
220                 local duration, elapsed = timer:GetDuration(), timer:GetElapsed()
221                 timer:GetParent():Stop()
222                 if timer.looping then
223                         newTimer = AceTimer.ScheduleRepeatingTimer(timer.object, timer.func, duration, unpack(timer.args, 1, timer.argsCount))
224                 else
225                         newTimer = AceTimer.ScheduleTimer(timer.object, timer.func, duration - elapsed, unpack(timer.args, 1, timer.argsCount))
226                 end
227                 -- Use the old handle for old timers
228                 activeTimers[newTimer] = nil
229                 activeTimers[handle] = newTimer
230                 newTimer.handle = handle
231         end
232
233         -- Migrate transitional handles
234         if oldminor < 13 and AceTimer.hashCompatTable then
235                 for handle, id in pairs(AceTimer.hashCompatTable) do
236                         local t = activeTimers[id]
237                         if t then
238                                 activeTimers[id] = nil
239                                 activeTimers[handle] = t
240                                 t.handle = handle
241                         end
242                 end
243                 AceTimer.hashCompatTable = nil
244         end
245 end
246
247 -- ---------------------------------------------------------------------
248 -- Embed handling
249
250 AceTimer.embeds = AceTimer.embeds or {}
251
252 local mixins = {
253         "ScheduleTimer", "ScheduleRepeatingTimer",
254         "CancelTimer", "CancelAllTimers",
255         "TimeLeft"
256 }
257
258 function AceTimer:Embed(target)
259         AceTimer.embeds[target] = true
260         for _,v in pairs(mixins) do
261                 target[v] = AceTimer[v]
262         end
263         return target
264 end
265
266 -- AceTimer:OnEmbedDisable(target)
267 -- target (object) - target object that AceTimer is embedded in.
268 --
269 -- cancel all timers registered for the object
270 function AceTimer:OnEmbedDisable(target)
271         target:CancelAllTimers()
272 end
273
274 for addon in pairs(AceTimer.embeds) do
275         AceTimer:Embed(addon)
276 end