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

Last change on this file since 1277 was 1226, checked in by dkearney, 16 years ago

fixing bug where loader fails to show the correct label when using the zoo/loader example.
moving the xml copy from newobj to inside of a similar check to see if the path exists in newobj.
adding example of using the loader copy feature in tcl.

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