source: trunk/gui/scripts/dispatcher.tcl @ 4735

Last change on this file since 4735 was 3330, checked in by gah, 11 years ago

merge (by hand) with Rappture1.2 branch

File size: 12.1 KB
Line 
1# -*- mode: tcl; indent-tabs-mode: nil -*-
2# ----------------------------------------------------------------------
3#  COMPONENT: dispatcher - central notification mechanism
4#
5#  The DispatchObj is used within other objects to manage events that
6#  the object sends to itself or other clients.  Each event type
7#  must first be registered via the "register" method.  After that,
8#  an event can be dispatched by calling the "event" method.  Clients
9#  can bind to the various event types by registering a callback via
10#  the "dispatch" method.
11#
12#  The Dispatcher base class provides a foundation for objects that
13#  use the DispatchObj internally.
14# ======================================================================
15#  AUTHOR:  Michael McLennan, Purdue University
16#  Copyright (c) 2004-2012  HUBzero Foundation, LLC
17#
18#  See the file "license.terms" for information on usage and
19#  redistribution of this file, and for a DISCLAIMER OF ALL WARRANTIES.
20# ======================================================================
21package require Itcl
22
23namespace eval Rappture { # forward declaration }
24
25proc Rappture::dispatcher {varName} {
26    upvar $varName obj
27    set obj [Rappture::DispatchObj ::#auto]
28    trace variable obj u "[list rename $obj {}]; list"
29}
30
31itcl::class Rappture::DispatchObj {
32    constructor {} { # defined below }
33    destructor { # defined below }
34
35    public method register {args}
36    public method dispatch {option args}
37    public method event {name args}
38    public method cancel {args}
39    public method ispending {name}
40
41    protected method _send {event caller {arglist ""}}
42
43    private variable _event2clients  ;# maps !event => clients
44    private variable _event2datacb   ;# maps !event => data callback
45    private variable _dispatch       ;# maps !event/client => callback
46    private variable _extraargs      ;# extra args for dispatch calls
47}
48                                                                               
49# ----------------------------------------------------------------------
50# CONSTRUCTOR
51# ----------------------------------------------------------------------
52itcl::body Rappture::DispatchObj::constructor {} {
53    # nothing to do
54}
55                                                                               
56# ----------------------------------------------------------------------
57# DESTRUCTOR
58# ----------------------------------------------------------------------
59itcl::body Rappture::DispatchObj::destructor {} {
60    eval cancel [array names _event2clients]
61}
62
63# ----------------------------------------------------------------------
64# USAGE: register !event ?dataCallback?
65#
66# Clients use this to register new event types with this dispatcher.
67# Once registered, each !event name can be used in the "dispatch"
68# method to bind to the event, and in the "event" method to raise
69# the event.  If the optional dataCallback is supplied, then it is
70# invoked whenever the event is triggered to generate event data
71# that gets passed along to the various dispatch callbacks.
72# ----------------------------------------------------------------------
73itcl::body Rappture::DispatchObj::register {name {datacb ""}} {
74    if {![regexp {^![-_a-zA-Z0-9]} $name]} {
75        error "bad name \"$name\": should be !name"
76    }
77    if {[info exists _event2clients($name)]} {
78        error "event \"$name\" already exists"
79    }
80    set _event2clients($name) ""
81    set _event2datacb($name) $datacb
82}
83
84# ----------------------------------------------------------------------
85# USAGE: dispatch caller ?-now? ?!event ...? ?callback?
86#
87# Clients use this to query binding information, or to bind a callback
88# to one or more of the !events recognized by this object.
89#
90# With no args, this returns a list of all events that are currently
91# bound to the caller.
92#
93# With one argument, this returns the callback associated with the
94# specified !event for the given caller.
95#
96# Otherwise, it sets the callback for one or more events.  If the
97# -now flag is specified, then the callback is invoked immediately
98# to send data back to this caller.  This comes in handy when the
99# caller is set up to react to changes.  It provides a way of
100# initializing the client, as if an event had just occurred.
101# ----------------------------------------------------------------------
102itcl::body Rappture::DispatchObj::dispatch {caller args} {
103    if {[llength $args] == 0} {
104        # no args? then return a list of events for this caller
105        foreach key [array names _dispatch $caller-*] {
106            set name [lindex [split $key -] 1]
107            set events($name) 1
108        }
109        return [array names events]
110    } elseif {[llength $args] == 1} {
111        set name [lindex $args 0]
112        if {[info exists _dispatch($caller-$name)]} {
113            return $_dispatch($caller-$name)
114        }
115        return ""
116    }
117
118    # set a callback for one or more events
119    set now 0
120    set events ""
121    set callback [lindex $args end]
122    foreach str [lrange $args 0 end-1] {
123        if {$str == "-now"} {
124            set now 1
125        } elseif {[info exists _event2clients($str)]} {
126            lappend events $str
127        } else {
128            if {[string index $str 0] == "-"} {
129                error "bad option \"$str\": should be -now"
130            } else {
131                error "bad event \"$str\": should be [join [lsort [array names _event2clients]] {, }]"
132            }
133        }
134    }
135
136    #
137    # If the callback is "", then remove the callback for this
138    # caller.  Otherwise, set the callback for this caller.
139    #
140    foreach name $events {
141        cancel $name
142
143        if {"" == $callback} {
144            catch {unset _dispatch($caller-$name)}
145            if {"" == [array names _dispatch $caller-*]} {
146                set i [lsearch $_event2clients($name) $caller]
147                if {$i >= 0} {
148                    set _event2clients($name) [lreplace $_event2clients($name) $i $i]
149                }
150            }
151        } else {
152            set _dispatch($caller-$name) $callback
153            set i [lsearch $_event2clients($name) $caller]
154            if {$i < 0} { lappend _event2clients($name) $caller }
155
156            if {$now} {
157                _send $name $caller
158            }
159        }
160    }
161}
162
163# ----------------------------------------------------------------------
164# USAGE: event ?-now? ?-later? ?-idle? ?-after time? !event ?args...?
165#
166# Clients use this to dispatch an event to any callers who have
167# registered their interest.
168# ----------------------------------------------------------------------
169itcl::body Rappture::DispatchObj::event {args} {
170    set when "-now"
171    set first [lindex $args 0]
172    if {[string index $first 0] == "-"} {
173        switch -- $first {
174            -now   {
175                set when "-now"
176                set args [lrange $args 1 end]
177            }
178            -later {
179                set when 1
180                set args [lrange $args 1 end]
181            }
182            -idle  {
183                set when "-idle"
184                set args [lrange $args 1 end]
185            }
186            -after {
187                set when [lindex $args 1]
188                if {![string is int $when]} {
189                    error "bad value \"$when\": should be int (time in ms)"
190                }
191                set args [lrange $args 2 end]
192            }
193            default {
194                error "bad option \"$first\": should be -now, -later, -idle, or -after"
195            }
196        }
197    }
198
199    if {[llength $args] < 1} {
200        error "wrong # args: should be \"event ?switches? !event ?args...?\""
201    }
202    set event [lindex $args 0]
203    set args [lrange $args 1 end]
204    if {![info exists _event2clients($event)]} {
205        error "bad event \"$event\": should be [join [lsort [array names _event2clients]] {, }]"
206    }
207
208    switch -- $when {
209        -now {
210            _send $event all $args
211        }
212        -idle {
213            set _extraargs($event) $args
214            after cancel [itcl::code $this _send $event all @extra]
215            after idle [itcl::code $this _send $event all @extra]
216        }
217        default {
218            set _extraargs($event) $args
219            after cancel [itcl::code $this _send $event all @extra]
220            after $when [itcl::code $this _send $event all @extra]
221        }
222    }
223}
224
225# ----------------------------------------------------------------------
226# USAGE: cancel ?!event ...?
227#        cancel all
228#
229# Used to cancel any event notifications pending for the specified
230# list of events.  Notifications may be pending if a particular
231# event was raised with the -idle or -after flags.
232# ----------------------------------------------------------------------
233itcl::body Rappture::DispatchObj::cancel {args} {
234    if { $args == "all" } {
235        foreach event [array names _event2clients] {
236            after cancel [itcl::code $this _send $event all @extra]
237        }
238        return
239    }
240    foreach event $args {
241        if {![info exists _event2clients($event)]} {
242            error "bad event \"$event\": should be [join [lsort [array names _event2clients]] {, }]"
243        }
244        after cancel [itcl::code $this _send $event all @extra]
245    }
246}
247
248# ----------------------------------------------------------------------
249# USAGE: ispending !event
250#
251# Returns 1 if the specified !event is pending, and 0 otherwise.
252# Notifications may be pending if a particular event was raised
253# with the -idle or -after flags.
254# ----------------------------------------------------------------------
255itcl::body Rappture::DispatchObj::ispending {event} {
256    set cmd [itcl::code $this _send $event all @extra]
257    foreach id [after info] {
258        set cmd2 [lindex [after info $id] 0]
259        if {[string equal $cmd $cmd2]} {
260            return 1
261        }
262    }
263    return 0
264}
265
266# ----------------------------------------------------------------------
267# USAGE: _send !event caller ?arglist?
268#
269# Used internally to send an event to one or all callers.  The event
270# is sent by first invoking a callback to generate any data associated
271# with the event.  This data, along with any extra args passed in,
272# are added to the callback for the caller.
273# ----------------------------------------------------------------------
274itcl::body Rappture::DispatchObj::_send {event caller {arglist ""}} {
275    if {$caller == "all"} {
276        set caller $_event2clients($event)
277    }
278    if {$arglist == "@extra" && [info exists _extraargs($event)]} {
279        set arglist $_extraargs($event)
280    }
281
282    # if there are any clients for this event, get the arguments ready
283    set any 0
284    foreach cname $caller {
285        if {[info exists _dispatch($cname-$event)]} {
286            set any 1
287            break
288        }
289    }
290
291    set dargs ""
292    if {$any && [string length $_event2datacb($event)] > 0} {
293        # get the args from the data callback
294        set status [catch {
295            uplevel #0 $_event2datacb($event) $event
296        } dargs]
297
298        if {$status != 0} {
299            # anything go wrong? then throw a background error
300            bgerror "$dargs\n(while dispatching $event)"
301            set dargs ""
302        }
303    }
304    # add any arguments added from command line
305    eval lappend dargs $arglist
306
307    foreach cname $caller {
308        if {[info exists _dispatch($cname-$event)]} {
309            set status [catch {
310                uplevel #0 $_dispatch($cname-$event) event $event $dargs
311            } result]
312
313            if {$status != 0} {
314                # anything go wrong? then throw a background error
315                bgerror "$result\n(while dispatching $event to $cname)"
316            }
317        }
318    }
319}
320
321# ----------------------------------------------------------------------
322# BASE CLASS: Dispatcher
323# ----------------------------------------------------------------------
324itcl::class Rappture::Dispatcher {
325    constructor {args} {
326        Rappture::dispatcher _dispatcher
327        $_dispatcher register !destroy
328
329        eval configure $args
330    }
331
332    destructor {
333        event !destroy
334    }
335
336    public method dispatch {args} {
337        eval $_dispatcher dispatch $args
338    }
339
340    protected method register {args} {
341        eval $_dispatcher register $args
342    }
343    protected method event {args} {
344        eval $_dispatcher event $args object $this
345    }
346
347    private variable _dispatcher ""  ;# dispatcher for events
348}
Note: See TracBrowser for help on using the repository browser.