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

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

merge (by hand) with Rappture1.2 branch

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