source: trunk/gui/scripts/controlOwner.tcl @ 2753

Last change on this file since 2753 was 2753, checked in by gah, 13 years ago
File size: 18.9 KB
Line 
1# ----------------------------------------------------------------------
2#  COMPONENT: owner - manages Rappture controls
3#
4#  This object represents an entity managing Rappture controls.
5#  It is typically a Tool, a DeviceEditor, or some other large entity
6#  that manages a Rappture XML tree.  All controlling widgets are
7#  registered with an owner, and the owner propagates notifications
8#  out to clients who have an interest in a particular control.
9# ======================================================================
10#  AUTHOR:  Michael McLennan, Purdue University
11#  Copyright (c) 2004-2005  Purdue Research Foundation
12#
13#  See the file "license.terms" for information on usage and
14#  redistribution of this file, and for a DISCLAIMER OF ALL WARRANTIES.
15# ======================================================================
16package require Itcl
17
18itcl::class Rappture::ControlOwner {
19    constructor {owner} { # defined below }
20
21    public method xml {args}
22
23    public method load {newobj}
24    public method widgetfor {path args}
25    public method valuefor {path args}
26    public method dependenciesfor {path args}
27    public method ownerfor {path {skip ""}}
28    public method changed {path}
29    public method regularize {path}
30    public method notify {option owner args}
31    public method sync {}
32    public method tool {}
33
34    protected method _slave {option args}
35
36    protected variable _owner ""     ;# ControlOwner containing this one
37    protected variable _path ""      ;# paths within are relative to this
38    protected variable _slaves ""    ;# this owner has these slaves
39    protected variable _xmlobj ""    ;# Rappture XML description
40    private variable _path2widget    ;# maps path => widget on this page
41    private variable _owner2paths    ;# for notify: maps owner => interests
42    private variable _type2curpath   ;# maps type(path) => path's current value
43    private variable _callbacks      ;# for notify: maps owner/path => callback
44    private variable _dependencies   ;# maps path => other paths dep on this
45}
46
47# ----------------------------------------------------------------------
48# CONSTRUCTOR
49# ----------------------------------------------------------------------
50itcl::body Rappture::ControlOwner::constructor {owner} {
51    if {"" != $owner} {
52        set parts [split $owner @]
53        set _owner [lindex $parts 0]
54        set _path [lindex $parts 1]
55        $_owner _slave add $this
56    }
57
58    # we are adding this so notes can be used
59    # in coordination with loaders inside the load function
60    array set _type2curpath {
61        choice current
62        boolean current
63        image current
64        integer current
65        loader current
66        note contents
67        number current
68        periodicelement current
69        string current
70    }
71}
72
73# ----------------------------------------------------------------------
74# USAGE: xml <subcommand> ?<arg> <arg> ...?
75# USAGE: xml object
76#
77# Used by clients to manipulate the underlying XML data for this
78# tool.  The <subcommand> can be any operation supported by a
79# Rappture::library object.  Clients can also request the XML object
80# directly by using the "object" subcommand.
81# ----------------------------------------------------------------------
82itcl::body Rappture::ControlOwner::xml {args} {
83    if {"object" == $args} {
84        return $_xmlobj
85    }
86    return [eval $_xmlobj $args]
87}
88
89# ----------------------------------------------------------------------
90# USAGE: widgetfor <path> ?<widget>?
91#
92# Used by embedded widgets such as a Controls panel to register the
93# various controls associated with this page.  That way, this
94# ControlOwner knows what widgets to look at when syncing itself
95# to the underlying XML data.
96# ----------------------------------------------------------------------
97itcl::body Rappture::ControlOwner::widgetfor {path args} {
98    # if this is a query operation, then look for the path
99    if {[llength $args] == 0} {
100        set owner [ownerfor $path]
101        if {$owner != $this && $owner != ""} {
102            return [$owner widgetfor $path]
103        }
104        if {[info exists _path2widget($path)]} {
105            return $_path2widget($path)
106        }
107        return ""
108    }
109
110    # otherwise, associate the path with the given widget
111    set widget [lindex $args 0]
112    if {"" != $widget} {
113        if {[info exists _path2widget($path)]} {
114            error "$path already associated with widget $_path2widget($path)"
115        }
116        set _path2widget($path) $widget
117    } else {
118        catch {unset _path2widget($path)}
119    }
120}
121
122# ----------------------------------------------------------------------
123# USAGE: valuefor <path> ?<newValue>?
124#
125# Used by embedded widgets such as a Loader to query or set the
126# value of another control.  With no extra args, it returns the
127# value of the widget at the <path> in the XML.  Otherwise, it
128# sets the value of the widget to <newValue>.
129# ----------------------------------------------------------------------
130itcl::body Rappture::ControlOwner::valuefor {path args} {
131    set owner [ownerfor $path]
132
133    # if this is a query operation, then look for the path
134    if {[llength $args] == 0} {
135        if {$owner != $this && $owner != ""} {
136            return [$owner valuefor $path]
137        }
138        if {[info exists _path2widget($path)]} {
139            return [$_path2widget($path) value]
140        }
141        # can't find the path? try removing the prefix for this owner
142        set plen [string length $_path]
143        if {[string equal -length $plen $_path $path]} {
144            set relpath [string range $path [expr {$plen+1}] end]
145            if {[info exists _path2widget($relpath)]} {
146                return [$_path2widget($relpath) value]
147            }
148        }
149        return ""
150    }
151
152    # otherwise, set the value
153    if {$owner != $this && $owner != ""} {
154        return [eval $owner valuefor $path $args]
155    }
156    if {[llength $args] > 1} {
157        error "wrong # args: should be \"valuefor path ?newValue?\""
158    }
159
160    if {[info exists _path2widget($path)]} {
161        $_path2widget($path) value [lindex $args 0]
162    } else {
163        error "bad path \"$path\": should be one of [join [lsort [array names _path2widget]] {, }]"
164    }
165}
166
167# ----------------------------------------------------------------------
168# USAGE: dependenciesfor <path> ?<path>...?
169#
170# Used by embedded widgets such as a Controls panel to register the
171# various controls that are dependent on another one.  If only one
172# path is specified, then this method returns all known dependencies
173# for the specified <path>.  Otherwise, the additional <path>'s are
174# noted as being dependent on the first <path>.
175# ----------------------------------------------------------------------
176itcl::body Rappture::ControlOwner::dependenciesfor {path args} {
177    if {"" != $_owner} {
178        #
179        # Keep all dependencies at the highest level.
180        # That way, a structure can come and go, but the
181        # dependencies remain fixed in the topmost tool.
182        #
183        set plen [string length $_path]
184        if {"" != $_path && ![string equal -length $plen $_path $path]} {
185            set path $_path.$path
186        }
187        return [eval $_owner dependenciesfor $path $args]
188    }
189
190    # if this is a query operation, then look for the path
191    if {[llength $args] == 0} {
192        if {[info exists _dependencies($path)]} {
193            return $_dependencies($path)
194        }
195        return ""
196    }
197
198    # add new dependencies
199    if {![info exists _dependencies($path)]} {
200        set _dependencies($path) ""
201    }
202    foreach dpath $args {
203        set i [lsearch -exact $_dependencies($path) $dpath]
204        if {$i < 0} {
205            lappend _dependencies($path) $dpath
206        }
207    }
208}
209
210# ----------------------------------------------------------------------
211# USAGE: ownerfor <path> ?<skip>?
212#
213# Returns the ControlOwner that directly controls the specified <path>.
214# ----------------------------------------------------------------------
215itcl::body Rappture::ControlOwner::ownerfor {path {skip ""}} {
216    if {[info exists _path2widget($path)]} {
217        return $this
218    }
219
220    # can't find the path? try removing the prefix for this owner
221    set plen [string length $_path]
222    if {[string equal -length $plen $_path $path]} {
223        set relpath [string range $path [expr {$plen+1}] end]
224        if {[info exists _path2widget($relpath)]} {
225            return $this
226        }
227    }
228
229    # couldn't find this path?  then check all subordinates
230    foreach slave $_slaves {
231        if {$slave == $skip} {
232            continue  ;# skip this slave if it's already been searched
233        }
234        set rval [$slave ownerfor $path $this]
235        if {"" != $rval} {
236            return $rval
237        }
238    }
239
240    # check the owner as a last resort
241    if {"" != $_owner && $_owner != $skip} {
242        set rval [$_owner ownerfor $path $this]
243        if {"" != $rval} {
244            return $rval
245        }
246    }
247
248    return ""
249}
250
251# ----------------------------------------------------------------------
252# USAGE: load <xmlobj>
253#
254# Loads the contents of a Rappture <xmlobj> into the controls
255# associated with this tool.
256# ----------------------------------------------------------------------
257itcl::body Rappture::ControlOwner::load {newobj} {
258    if {![Rappture::library isvalid $newobj]} {
259        error "\"$newobj\" is not a Rappture::library"
260    }
261
262    foreach path [array names _path2widget] {
263        # the following elements do not accept "current" tags, skip them
264        set type [[tool] xml element -as type $path]
265        if {[lsearch {group separator control} $type] >= 0} {
266            continue
267        }
268
269        set type [[tool] xml element -as type $path]
270        if {[info exists _type2curpath($type)]} {
271            set currentpath $path.$_type2curpath($type)
272        } else {
273            # default incase i forgot an input type in _type2curpath
274            set currentpath $path.current
275        }
276
277        # copy new value to the XML tree
278        # also copy to the widget associated with the tree
279        #
280        # we only copy the values if they existed in newobj
281        # so we don't overwrite values that were set in previous loads.
282        # this is needed for when the users specify copy.from and copy.to
283        # in a loader. in this case, _path2widget holds a list of all
284        # widgets. if there are two loaders loading two different widgets,
285        # and each loader uses the copy from/to functionality,
286        # the second load could wipe out the values set in the first load
287        # because on the second load, the copied paths from the first load no
288        # longer exist in newobj and blanks are copied to the paths
289        # in [tool] xml set by the first loader. the solution is to check
290        # newobj and see if the path exists. if the path exists, then we copy
291        # it over to [tool] xml, otherwise we ignore it.
292        if {"" != [$newobj element -as type $currentpath]} {
293            [tool] xml copy $currentpath from $newobj $currentpath
294            set val [$newobj get $currentpath]
295            if {[string length $val] > 0
296                  || [llength [$newobj children $currentpath]] == 0} {
297                $_path2widget($path) value $val
298            } else {
299                set obj [$newobj element -as object $currentpath]
300                $_path2widget($path) value $obj
301            }
302        }
303    }
304}
305
306# ----------------------------------------------------------------------
307# USAGE: changed <path>
308#
309# Invoked automatically by the various widgets associated with this
310# tool whenever their value changes.  Sends notifications to any
311# client that has registered an interest via "notify add".
312# ----------------------------------------------------------------------
313itcl::body Rappture::ControlOwner::changed {path} {
314    if {"" != $_owner} {
315        set plen [string length $_path]
316        if {"" != $_path && ![string equal -length $plen $_path $path]} {
317            set path $_path.$path
318        }
319        $_owner changed $path
320    } else {
321        # send out any callback notifications
322        foreach owner [array names _owner2paths] {
323            foreach pattern $_owner2paths($owner) {
324                if {[string match $pattern $path]} {
325                    uplevel #0 $_callbacks($owner/$pattern)
326                    break
327                }
328            }
329        }
330
331        # find the control panel for each dependency, and tell it
332        # to update its layout.
333        foreach cpath [dependenciesfor $path] {
334            set wv [widgetfor $cpath]
335            while {"" != $wv} {
336                set wv [winfo parent $wv]
337                if {[winfo class $wv] == "Controls"} {
338                    $wv refresh
339                    break
340                }
341            }
342        }
343    }
344}
345
346# ----------------------------------------------------------------------
347# USAGE: regularize <path>
348#
349# Clients use this to get a full, regularized path for the specified
350# <path>, which may be relative to the current owner.
351# ----------------------------------------------------------------------
352itcl::body Rappture::ControlOwner::regularize {path} {
353    set owner [ownerfor $path]
354    if {$owner != $this && $owner != ""} {
355        return [$owner regularize $path]
356    }
357    set rpath ""
358    if {"" != $_xmlobj} {
359        set rpath [$_xmlobj element -as path $path]
360
361        # can't find the path? try removing the prefix for this owner
362        if {"" == $rpath} {
363            set plen [string length $_path]
364            if {[string equal -length $plen $_path $path]} {
365                set relpath [string range $path [expr {$plen+2}] end]
366                set rpath [$_xmlobj element -as path $relpath]
367            }
368        }
369
370        if {"" != $rpath && "" != $_path} {
371            # return a full name for the path
372            set rpath "$_path.$rpath"
373        }
374    }
375    return $rpath
376}
377
378# ----------------------------------------------------------------------
379# USAGE: notify add <owner> <path> <callback>
380# USAGE: notify info ?<owner>? ?<path>?
381# USAGE: notify remove <owner> ?<path> ...?
382#
383# Clients use this to request notifications about changes to a
384# particular <path> for a control under this tool.  Whenever the
385# value associated with <path> changes, the client identified by
386# <owner> is sent a message by invoking its <callback> routine.
387#
388# Notifications can be silenced by calling the "notify remove"
389# function.
390# ----------------------------------------------------------------------
391itcl::body Rappture::ControlOwner::notify {option args} {
392    switch -- $option {
393        add {
394            if {[llength $args] != 3} {
395                error "wrong # args: should be \"notify add owner path callback\""
396            }
397            set owner [lindex $args 0]
398            set path [lindex $args 1]
399            set cb [lindex $args 2]
400
401            if {[info exists _owner2paths($owner)]} {
402                set plist $_owner2paths($owner)
403            } else {
404                set plist ""
405            }
406
407            set i [lsearch -exact $plist $path]
408            if {$i < 0} { lappend _owner2paths($owner) $path }
409            set _callbacks($owner/$path) $cb
410        }
411        info {
412            if {[llength $args] == 0} {
413                # no args? then return all owners
414                return [array names _owner2paths]
415            } else {
416                set owner [lindex $args 0]
417                if {[info exists _owner2paths($owner)]} {
418                    set plist $_owner2paths($owner)
419                } else {
420                    set plist ""
421                }
422                if {[llength $args] == 1} {
423                    # 1 arg? then return paths for this owner
424                    return $plist
425                } elseif {[llength $args] == 2} {
426                    # 2 args? then return callback for this path
427                    set path [lindex $args 1]
428                    if {[info exists _callbacks($owner/$path)]} {
429                        return $_callbacks($owner/$path)
430                    }
431                    return ""
432                } else {
433                    error "wrong # args: should be \"notify info ?owner? ?path?\""
434                }
435            }
436        }
437        remove {
438            if {[llength $args] < 1} {
439                error "wrong # args: should be \"notify remove owner ?path ...?\""
440            }
441            set owner [lindex $args 0]
442
443            if {[llength $args] == 1} {
444                # no args? then delete all paths for this owner
445                if {[info exists _owner2paths($owner)]} {
446                    set plist $_owner2paths($owner)
447                } else {
448                    set plist ""
449                }
450            } else {
451                set plist [lrange $args 1 end]
452            }
453
454            # forget about the callback for each path
455            foreach path $plist {
456                catch {unset _callbacks($owner/$path)}
457
458                if {[info exists _owner2paths($owner)]} {
459                    set i [lsearch -exact $_owner2paths($owner) $path]
460                    if {$i >= 0} {
461                        set _owner2paths($owner) \
462                            [lreplace $_owner2paths($owner) $i $i]
463                    }
464                }
465            }
466        }
467    }
468}
469
470# ----------------------------------------------------------------------
471# USAGE: sync
472#
473# Used by descendents such as a Controls panel to register the
474# various controls associated with this page.  That way, this
475# ControlOwner knows what widgets to look at when syncing itself
476# to the underlying XML data.
477# ----------------------------------------------------------------------
478itcl::body Rappture::ControlOwner::sync {} {
479    # sync all of the widgets under control of this owner
480    if {"" != $_xmlobj} {
481        foreach path [lsort [array names _path2widget]] {
482            set type [$_xmlobj element -as type $path]
483            if {[lsearch {group separator control note} $type] >= 0} {
484                continue
485            }
486            $_xmlobj put $path.current [$_path2widget($path) value]
487        }
488    }
489
490    # sync all subordinate slaves as well
491    foreach slave $_slaves {
492        $slave sync
493    }
494}
495
496# ----------------------------------------------------------------------
497# USAGE: tool
498#
499# Clients use this to figure out which tool is associated with
500# this object.  If there is no parent ControlOwner, then this
501# must be the top-level tool.
502# ----------------------------------------------------------------------
503itcl::body Rappture::ControlOwner::tool {} {
504    if {"" != $_owner} {
505        return [$_owner tool]
506    }
507    return $this
508}
509
510# ----------------------------------------------------------------------
511# USAGE: _slave add <newobj>...
512#
513# Used internally to register the parent-child relationship whenever
514# one ControlOwner is registered to another.  When the parent syncs,
515# it causes all of its children to sync.  When a name is being
516# resolved, it is resolved locally first, then up to the parent for
517# further resolution.
518# ----------------------------------------------------------------------
519itcl::body Rappture::ControlOwner::_slave {option args} {
520    switch -- $option {
521        add {
522            eval lappend _slaves $args
523        }
524        default {
525            error "bad option \"$option\": should be add"
526        }
527    }
528}
Note: See TracBrowser for help on using the repository browser.