1 #
2 # tk/timer.rb : methods for Tcl/Tk after command
3 #
4 # $Id: timer.rb 16020 2008-04-14 15:17:52Z nagai $
5 #
6 require 'tk'
7
8 class TkTimer
9 include TkCore
10 extend TkCore
11
12 TkCommandNames = ['after'.freeze].freeze
13
14 (Tk_CBID = ['a'.freeze, '00000'.taint]).instance_eval{
15 @mutex = Mutex.new
16 def mutex; @mutex; end
17 freeze
18 }
19
20 Tk_CBTBL = {}.taint
21
22 TkCore::INTERP.add_tk_procs('rb_after', 'id', <<-'EOL')
23 if {[set st [catch {eval {ruby_cmd TkTimer callback} $id} ret]] != 0} {
24 return -code $st $ret
25 } {
26 return $ret
27 }
28 EOL
29
30 DEFAULT_IGNORE_EXCEPTIONS = [ NameError, RuntimeError ].freeze
31
32 ###############################
33 # class methods
34 ###############################
35 def self.start(*args, &b)
36 self.new(*args, &b).start
37 end
38
39 def self.callback(obj_id)
40 ex_obj = Tk_CBTBL[obj_id]
41 return "" if ex_obj == nil; # canceled
42 ex_obj.cb_call
43 end
44
45 def self.info(obj = nil)
46 if obj
47 if obj.kind_of?(TkTimer)
48 if obj.after_id
49 inf = tk_split_list(tk_call_without_enc('after','info',obj.after_id))
50 [Tk_CBTBL[inf[0][1]], inf[1]]
51 else
52 nil
53 end
54 else
55 fail ArgumentError, "TkTimer object is expected"
56 end
57 else
58 tk_call_without_enc('after', 'info').split(' ').collect!{|id|
59 ret = Tk_CBTBL.find{|key,val| val.after_id == id}
60 (ret == nil)? id: ret[1]
61 }
62 end
63 end
64
65
66 ###############################
67 # instance methods
68 ###############################
69 def do_callback
70 @in_callback = true
71 @after_id = nil
72 begin
73 @return_value = @current_proc.call(self)
74 rescue SystemExit
75 exit(0)
76 rescue Interrupt
77 exit!(1)
78 rescue Exception => e
79 if @cancel_on_exception &&
80 @cancel_on_exception.find{|exc| e.kind_of?(exc)}
81 cancel
82 @return_value = e
83 @in_callback = false
84 return e
85 else
86 fail e
87 end
88 end
89 if @set_next
90 set_next_callback(@current_args)
91 else
92 @set_next = true
93 end
94 @in_callback = false
95 @return_value
96 end
97
98 def set_callback(sleep, args=nil)
99 if TkCore::INTERP.deleted?
100 self.cancel
101 return self
102 end
103 @after_script = "rb_after #{@id}"
104 @current_args = args
105 @current_script = [sleep, @after_script]
106 @after_id = tk_call_without_enc('after', sleep, @after_script)
107 self
108 end
109
110 def set_next_callback(args)
111 if @running == false || @proc_max == 0 || @do_loop == 0
112 Tk_CBTBL.delete(@id) ;# for GC
113 @running = false
114 # @wait_var.value = 0
115 __at_end__
116 return
117 end
118 if @current_pos >= @proc_max
119 if @do_loop < 0 || (@do_loop -= 1) > 0
120 @current_pos = 0
121 else
122 Tk_CBTBL.delete(@id) ;# for GC
123 @running = false
124 # @wait_var.value = 0
125 __at_end__
126 return
127 end
128 end
129
130 @current_args = args
131
132 # if @sleep_time.kind_of?(Proc)
133 if TkComm._callback_entry?(@sleep_time)
134 sleep = @sleep_time.call(self)
135 else
136 sleep = @sleep_time
137 end
138 @current_sleep = sleep
139
140 cmd, *cmd_args = @loop_proc[@current_pos]
141 @current_pos += 1
142 @current_proc = cmd
143
144 set_callback(sleep, cmd_args)
145 end
146
147 def initialize(*args, &b)
148 Tk_CBID.mutex.synchronize{
149 # @id = Tk_CBID.join('')
150 @id = Tk_CBID.join(TkCore::INTERP._ip_id_)
151 Tk_CBID[1].succ!
152 }
153
154 @wait_var = TkVariable.new(0)
155
156 @at_end_proc = nil
157
158 @cb_cmd = TkCore::INTERP.get_cb_entry(self.method(:do_callback))
159
160 @set_next = true
161
162 @init_sleep = 0
163 @init_proc = nil
164 @init_args = []
165
166 @current_script = []
167 @current_proc = nil
168 @current_args = nil
169 @return_value = nil
170
171 @sleep_time = 0
172 @current_sleep = 0
173 @loop_exec = 0
174 @do_loop = 0
175 @loop_proc = []
176 @proc_max = 0
177 @current_pos = 0
178
179 @after_id = nil
180 @after_script = nil
181
182 @cancel_on_exception = DEFAULT_IGNORE_EXCEPTIONS
183 # Unless @cancel_on_exception, Ruby/Tk shows an error dialog box when
184 # an excepsion is raised on TkTimer callback procedure.
185 # If @cancel_on_exception is an array of exception classes and the raised
186 # exception is included in the array, Ruby/Tk cancels executing TkTimer
187 # callback procedures silently (TkTimer#cancel is called and no dialog is
188 # shown).
189
190 if b
191 case args.size
192 when 0
193 add_procs(b)
194 when 1
195 args << -1 << b
196 else
197 args << b
198 end
199 end
200
201 set_procs(*args) if args != []
202
203 @running = false
204 @in_callback = false
205 end
206
207 attr :after_id
208 attr :after_script
209 attr :current_proc
210 attr :current_args
211 attr :current_sleep
212 alias :current_interval :current_sleep
213 attr :return_value
214
215 attr_accessor :loop_exec
216
217 def __at_end__
218 @at_end_proc.call(self) if @at_end_proc
219 @wait_var.value = 0 # for wait
220 end
221 private :__at_end__
222
223 def cb_call
224 @cb_cmd.call
225 end
226
227 def get_procs
228 [@init_sleep, @init_proc, @init_args, @sleep_time, @loop_exec, @loop_proc]
229 end
230
231 def current_status
232 [@running, @current_sleep, @current_proc, @current_args,
233 @do_loop, @cancel_on_exception]
234 end
235
236 def cancel_on_exception?
237 @cancel_on_exception
238 end
239
240 def cancel_on_exception=(mode)
241 if mode.kind_of?(Array)
242 @cancel_on_exception = mode
243 elsif mode
244 @cancel_on_exception = DEFAULT_IGNORE_EXCEPTIONS
245 else
246 @cancel_on_exception = false
247 end
248 #self
249 end
250
251 def running?
252 @running
253 end
254
255 def loop_rest
256 @do_loop
257 end
258
259 def loop_rest=(rest)
260 @do_loop = rest
261 #self
262 end
263
264 def set_interval(interval)
265 #if interval != 'idle' && interval != :idle \
266 # && !interval.kind_of?(Integer) && !interval.kind_of?(Proc)
267 if interval != 'idle' && interval != :idle \
268 && !interval.kind_of?(Integer) && !TkComm._callback_entry?(interval)
269 fail ArgumentError, "expect Integer or Proc"
270 end
271 @sleep_time = interval
272 end
273
274 def set_procs(interval, loop_exec, *procs)
275 #if interval != 'idle' && interval != :idle \
276 # && !interval.kind_of?(Integer) && !interval.kind_of?(Proc)
277 if interval != 'idle' && interval != :idle \
278 && !interval.kind_of?(Integer) && !TkComm._callback_entry?(interval)
279 fail ArgumentError, "expect Integer or Proc for 1st argument"
280 end
281 @sleep_time = interval
282
283 @loop_proc = []
284 procs.each{|e|
285 # if e.kind_of?(Proc)
286 if TkComm._callback_entry?(e)
287 @loop_proc.push([e])
288 else
289 @loop_proc.push(e)
290 end
291 }
292 @proc_max = @loop_proc.size
293 @current_pos = 0
294
295 if loop_exec.kind_of?(Integer) && loop_exec < 0
296 @loop_exec = -1
297 elsif loop_exec == true
298 @loop_exec = -1
299 elsif loop_exec == nil || loop_exec == false || loop_exec == 0
300 @loop_exec = 0
301 else
302 if not loop_exec.kind_of?(Integer)
303 fail ArgumentError, "expect Integer for 2nd argument"
304 end
305 @loop_exec = loop_exec
306 end
307 @do_loop = @loop_exec
308
309 self
310 end
311
312 def add_procs(*procs)
313 procs.each{|e|
314 # if e.kind_of?(Proc)
315 if TkComm._callback_entry?(e)
316 @loop_proc.push([e])
317 else
318 @loop_proc.push(e)
319 end
320 }
321 @proc_max = @loop_proc.size
322
323 self
324 end
325
326 def delete_procs(*procs)
327 procs.each{|e|
328 # if e.kind_of?(Proc)
329 if TkComm._callback_entry?(e)
330 @loop_proc.delete([e])
331 else
332 @loop_proc.delete(e)
333 end
334 }
335 @proc_max = @loop_proc.size
336
337 cancel if @proc_max == 0
338
339 self
340 end
341
342 def delete_at(n)
343 @loop_proc.delete_at(n)
344 @proc_max = @loop_proc.size
345 cancel if @proc_max == 0
346 self
347 end
348
349 def set_start_proc(sleep=nil, init_proc=nil, *init_args, &b)
350 # set parameters for 'restart'
351 sleep = @init_sleep unless sleep
352
353 if sleep != 'idle' && sleep != :idle && !sleep.kind_of?(Integer)
354 fail ArgumentError, "expect Integer or 'idle' for 1st argument"
355 end
356
357 @init_sleep = sleep
358 @init_proc = init_proc
359 @init_args = init_args
360
361 @init_proc = b if !@init_proc && b
362 @init_proc = proc{|*args| } if @init_sleep > 0 && !@init_proc
363
364 self
365 end
366
367 def start(*init_args, &b)
368 return nil if @running
369
370 Tk_CBTBL[@id] = self
371 @do_loop = @loop_exec
372 @current_pos = 0
373 @return_value = nil
374 @after_id = nil
375
376 @init_sleep = 0
377 @init_proc = nil
378 @init_args = nil
379
380 argc = init_args.size
381 if argc > 0
382 sleep = init_args.shift
383 if sleep != 'idle' && sleep != :idle && !sleep.kind_of?(Integer)
384 fail ArgumentError, "expect Integer or 'idle' for 1st argument"
385 end
386 @init_sleep = sleep
387 end
388 @init_proc = init_args.shift if argc > 1
389 @init_args = init_args if argc > 2
390
391 @init_proc = b if !@init_proc && b
392 @init_proc = proc{|*args| } if @init_sleep > 0 && !@init_proc
393
394 @current_sleep = @init_sleep
395 @running = true
396 if @init_proc
397 # if not @init_proc.kind_of?(Proc)
398 if !TkComm._callback_entry?(@init_proc)
399 fail ArgumentError, "Argument '#{@init_proc}' need to be Proc"
400 end
401 @current_proc = @init_proc
402 set_callback(@init_sleep, @init_args)
403 @set_next = false if @in_callback
404 else
405 set_next_callback(@init_args)
406 end
407
408 self
409 end
410
411 def reset(*reset_args)
412 restart() if @running
413
414 if @init_proc
415 @return_value = @init_proc.call(self)
416 else
417 @return_value = nil
418 end
419
420 @current_pos = 0
421 @current_args = @init_args
422 @current_script = []
423
424 @set_next = false if @in_callback
425
426 self
427 end
428
429 def restart(*restart_args, &b)
430 cancel if @running
431 if restart_args == [] && !b
432 start(@init_sleep, @init_proc, *@init_args)
433 else
434 start(*restart_args, &b)
435 end
436 end
437
438 def cancel
439 @running = false
440 # @wait_var.value = 0
441 __at_end__
442 tk_call 'after', 'cancel', @after_id if @after_id
443 @after_id = nil
444
445 Tk_CBTBL.delete(@id) ;# for GC
446 self
447 end
448 alias stop cancel
449
450 def continue(wait=nil)
451 fail RuntimeError, "is already running" if @running
452 return restart() if @current_script.empty?
453 sleep, cmd = @current_script
454 fail RuntimeError, "no procedure to continue" unless cmd
455 if wait
456 unless wait.kind_of?(Integer)
457 fail ArgumentError, "expect Integer for 1st argument"
458 end
459 sleep = wait
460 end
461 Tk_CBTBL[@id] = self
462 @running = true
463 @after_id = tk_call_without_enc('after', sleep, cmd)
464 self
465 end
466
467 def skip
468 fail RuntimeError, "is not running now" unless @running
469 cancel
470 Tk_CBTBL[@id] = self
471 @running = true
472 set_next_callback(@current_args)
473 self
474 end
475
476 def info
477 if @after_id
478 inf = tk_split_list(tk_call_without_enc('after', 'info', @after_id))
479 [Tk_CBTBL[inf[0][1]], inf[1]]
480 else
481 nil
482 end
483 end
484
485 def at_end(*arg, &b)
486 if arg.empty?
487 if b
488 @at_end_proc = b
489 else
490 # no proc
491 return @at_end_proc
492 end
493 else
494 fail ArgumentError, "wrong number of arguments" if arg.length != 1 || b
495 @at_end_proc = arg[0]
496 end
497 self
498 end
499
500 def wait(on_thread = true, check_root = false)
501 if $SAFE >= 4
502 fail SecurityError, "can't wait timer at $SAFE >= 4"
503 end
504
505 unless @running
506 if @return_value.kind_of?(Exception)
507 fail @return_value
508 else
509 return @return_value
510 end
511 end
512
513 @wait_var.wait(on_thread, check_root)
514 if @return_value.kind_of?(Exception)
515 fail @return_value
516 else
517 @return_value
518 end
519 end
520 def eventloop_wait(check_root = false)
521 wait(false, check_root)
522 end
523 def thread_wait(check_root = false)
524 wait(true, check_root)
525 end
526 def tkwait(on_thread = true)
527 wait(on_thread, true)
528 end
529 def eventloop_tkwait
530 wait(false, true)
531 end
532 def thread_tkwait
533 wait(true, true)
534 end
535 end
536
537 TkAfter = TkTimer
538
539
540 class TkRTTimer < TkTimer
541 DEFAULT_OFFSET_LIST_SIZE = 5
542
543 def initialize(*args, &b)
544 super(*args, &b)
545
546 @offset_list = Array.new(DEFAULT_OFFSET_LIST_SIZE){ [0, 0] }
547 @offset_s = 0
548 @offset_u = 0
549 @est_time = nil
550 end
551
552 def start(*args, &b)
553 return nil if @running
554 @est_time = nil
555 @cb_start_time = Time.now
556 super(*args, &b)
557 end
558
559 def cancel
560 super()
561 @est_time = nil
562 @cb_start_time = Time.now
563 self
564 end
565 alias stop cancel
566
567 def continue(wait=nil)
568 fail RuntimeError, "is already running" if @running
569 @cb_start_time = Time.now
570 super(wait)
571 end
572
573 def set_interval(interval)
574 super(interval)
575 @est_time = nil
576 end
577
578 def _offset_ave
579 size = 0
580 d_sec = 0; d_usec = 0
581 @offset_list.each_with_index{|offset, idx|
582 # weight = 1
583 weight = idx + 1
584 size += weight
585 d_sec += offset[0] * weight
586 d_usec += offset[1] * weight
587 }
588 offset_s, mod = d_sec.divmod(size)
589 offset_u = ((mod * 1000000 + d_usec) / size.to_f).round
590 [offset_s, offset_u]
591 end
592 private :_offset_ave
593
594 def set_next_callback(args)
595 if @running == false || @proc_max == 0 || @do_loop == 0
596 Tk_CBTBL.delete(@id) ;# for GC
597 @running = false
598 # @wait_var.value = 0
599 __at_end__
600 return
601 end
602 if @current_pos >= @proc_max
603 if @do_loop < 0 || (@do_loop -= 1) > 0
604 @current_pos = 0
605 else
606 Tk_CBTBL.delete(@id) ;# for GC
607 @running = false
608 # @wait_var.value = 0
609 __at_end__
610 return
611 end
612 end
613
614 @current_args = args
615
616 cmd, *cmd_args = @loop_proc[@current_pos]
617 @current_pos += 1
618 @current_proc = cmd
619
620 @offset_s, @offset_u = _offset_ave
621
622 if TkComm._callback_entry?(@sleep_time)
623 sleep = @sleep_time.call(self)
624 else
625 sleep = @sleep_time
626 end
627
628 if @est_time
629 @est_time = Time.at(@est_time.to_i, @est_time.usec + sleep*1000)
630 else
631 @est_time = Time.at(@cb_start_time.to_i,
632 @cb_start_time.usec + sleep*1000)
633 end
634
635 now = Time.now
636 real_sleep = ((@est_time.to_i - now.to_i + @offset_s)*1000.0 +
637 (@est_time.usec - now.usec + @offset_u)/1000.0).round
638 if real_sleep <= 0
639 real_sleep = 0
640 @offset_s = now.to_i
641 @offset_u = now.usec
642 end
643 @current_sleep = real_sleep
644
645 set_callback(real_sleep, cmd_args)
646 end
647
648 def cb_call
649 if @est_time
650 @offset_list.shift
651
652 @cb_start_time = Time.now
653
654 if @current_sleep == 0
655 @offset_list.push([
656 @offset_s - @cb_start_time.to_i,
657 @offset_u - @cb_start_time.usec
658 ])
659 else
660 @offset_list.push([
661 @offset_s + (@est_time.to_i - @cb_start_time.to_i),
662 @offset_u + (@est_time.usec - @cb_start_time.usec)
663 ])
664 end
665 end
666
667 @cb_cmd.call
668 end
669 end