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

Last change on this file since 1555 was 1342, checked in by gah, 15 years ago

preliminary HQ output from molvisviewer; unexpand tabs; all jpeg generation at 100%

File size: 16.8 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.