source: trunk/gui/scripts/deviceViewer1D.tcl @ 111

Last change on this file since 111 was 82, checked in by mmc, 19 years ago

Fixed the device viewer to work properly even with
<units>arbitrary</units>. Also, fixed Controls so
that you can use a <separator> to break up a group
of groups and have it render as a flat list of groups
rather than a series of tabs.

File size: 24.9 KB
Line 
1# ----------------------------------------------------------------------
2#  COMPONENT: deviceViewer1D - visualizer for 1D device geometries
3#
4#  This widget is a simple visualizer for 1D devices.  It takes the
5#  Rappture XML representation for a 1D device and draws various
6#  facets of the data.  Each facet shows the physical layout along
7#  with some other quantity.  The "Electrical" facet shows electrical
8#  contacts.  The "Doping" facet shows the doping profile, and so
9#  forth.
10# ======================================================================
11#  AUTHOR:  Michael McLennan, Purdue University
12#  Copyright (c) 2004-2005
13#  Purdue Research Foundation, West Lafayette, IN
14# ======================================================================
15package require Itk
16package require BLT
17
18option add *DeviceViewer1D.padding 4 widgetDefault
19option add *DeviceViewer1D.deviceSize 0.25i widgetDefault
20option add *DeviceViewer1D.deviceOutline black widgetDefault
21option add *DeviceViewer1D*graph.width 3i widgetDefault
22option add *DeviceViewer1D*graph.height 2i widgetDefault
23
24itcl::class Rappture::DeviceViewer1D {
25    inherit itk::Widget
26
27    itk_option define -device device Device ""
28
29    constructor {owner args} { # defined below }
30    destructor { # defined below }
31
32    public method controls {option args}
33                                                                               
34    protected method _loadDevice {}
35    protected method _changeTabs {}
36    protected method _fixSize {}
37    protected method _fixAxes {}
38    protected method _align {}
39
40    protected method _marker {option {name ""} {path ""}}
41
42    protected method _controlCreate {container libObj path}
43    protected method _controlSet {widget libObj path}
44
45    private variable _owner ""      ;# thing managing this control
46    private variable _device ""     ;# XML library with <structure>
47    private variable _tab2fields    ;# maps tab name => list of fields
48    private variable _field2parm    ;# maps field path => parameter name
49    private variable _units ""      ;# units for field being edited
50    private variable _restrict ""   ;# restriction expr for field being edited
51    private variable _marker        ;# marker currently being edited
52}
53                                                                               
54itk::usual DeviceViewer1D {
55}
56
57# ----------------------------------------------------------------------
58# CONSTRUCTOR
59# ----------------------------------------------------------------------
60itcl::body Rappture::DeviceViewer1D::constructor {owner args} {
61    set _owner $owner
62
63    pack propagate $itk_component(hull) no
64
65    itk_component add tabs {
66        blt::tabset $itk_interior.tabs -borderwidth 0 -relief flat \
67            -side bottom -tearoff 0 \
68            -selectcommand [itcl::code $this _changeTabs]
69    } {
70        keep -activebackground -activeforeground
71        keep -background -cursor -font
72        rename -highlightbackground -background background Background
73        keep -highlightcolor -highlightthickness
74        keep -tabbackground -tabforeground
75        rename -selectbackground -background background Background
76        rename -selectforeground -foreground foreground Foreground
77    }
78    pack $itk_component(tabs) -expand yes -fill both
79
80    itk_component add -protected inner {
81        frame $itk_component(tabs).inner
82    }
83
84    itk_component add top {
85        frame $itk_component(inner).top
86    }
87    pack $itk_component(top) -fill x
88
89    itk_component add layout {
90        Rappture::DeviceLayout1D $itk_component(inner).layout
91    }
92    pack $itk_component(layout) -side top -fill x -pady 4
93
94    itk_component add graph {
95        blt::graph $itk_component(inner).graph \
96            -highlightthickness 0 -plotpadx 0 -plotpady 0
97    } {
98        keep -background -foreground -cursor -font
99    }
100    pack $itk_component(graph) -expand yes -fill both
101    $itk_component(graph) legend configure -hide yes
102
103    bind $itk_component(graph) <Configure> "
104        after cancel [itcl::code $this _fixAxes]
105        after 100 [itcl::code $this _fixAxes]
106    "
107
108    itk_component add geditor {
109        Rappture::Editor $itk_component(graph).editor \
110            -activatecommand [itcl::code $this _marker activate] \
111            -validatecommand [itcl::code $this _marker validate] \
112            -applycommand [itcl::code $this _marker apply]
113    }
114
115    itk_component add devcntls {
116        Rappture::Notebook $itk_component(inner).devcntls
117    }
118    pack $itk_component(devcntls) -side bottom -fill x
119
120    eval itk_initialize $args
121
122    _fixSize
123}
124
125# ----------------------------------------------------------------------
126# DESTRUCTOR
127# ----------------------------------------------------------------------
128itcl::body Rappture::DeviceViewer1D::destructor {} {
129    set _device ""
130    foreach name [array names _tab2fields] {
131        eval itcl::delete object $_tab2fields($name)
132    }
133    after cancel [list catch [itcl::code $this _fixAxes]]
134    after cancel [list catch [itcl::code $this _align]]
135}
136
137# ----------------------------------------------------------------------
138# USAGE: controls insert <pos> <xmlobj> <path>
139#
140# Clients use this to add a control to the internal panels of this
141# widget.  Such controls are usually placed at the top of the widget,
142# but if possible, they are integrated directly onto the device
143# layout or the field area.
144# ----------------------------------------------------------------------
145itcl::body Rappture::DeviceViewer1D::controls {option args} {
146    switch -- $option {
147        insert {
148            if {[llength $args] != 3} {
149                error "wrong # args: should be \"controls insert pos xmlobj path\""
150            }
151            set pos [lindex $args 0]
152            set xmlobj [lindex $args 1]
153            set path [lindex $args 2]
154            if {[string match *structure.parameters* $path]} {
155            } elseif {[string match structure.components* $path]} {
156                $itk_component(layout) controls insert $pos $xmlobj $path
157            }
158        }
159        default {
160            error "bad option \"$option\": should be insert"
161        }
162    }
163}
164
165# ----------------------------------------------------------------------
166# USAGE: _loadDevice
167#
168# Used internally to search for fields and create corresponding
169# tabs whenever a device is installed into this viewer.
170# ----------------------------------------------------------------------
171itcl::body Rappture::DeviceViewer1D::_loadDevice {} {
172    #
173    # Release any info left over from the last device.
174    #
175    foreach name [array names _tab2fields] {
176        eval itcl::delete object $_tab2fields($name)
177    }
178    catch {unset _tab2fields}
179    catch {unset _field2parm}
180
181    if {[winfo exists $itk_component(top).cntls]} {
182        $itk_component(top).cntls delete 0 end
183    }
184
185    #
186    # Scan through the current device and extract the list of
187    # fields.  Create a tab for each field.
188    #
189    if {$_device != ""} {
190        foreach nn [$_device children fields] {
191            set name [$_device get fields.$nn.about.label]
192            if {$name == ""} {
193                set name $nn
194            }
195
196            set fobj [Rappture::Field ::#auto $_device fields.$nn]
197            lappend _tab2fields($name) $fobj
198        }
199    }
200    set tabs [lsort [array names _tab2fields]]
201
202    if {[$itk_component(tabs) size] > 0} {
203        $itk_component(tabs) delete 0 end
204    }
205
206    if {[llength $tabs] <= 0} {
207        #
208        # No fields?  Then we don't need to bother with tabs.
209        # Just pack the inner frame directly.  If there are no
210        # fields, get rid of the graph.
211        #
212        pack $itk_component(inner) -expand yes -fill both
213        if {[llength $tabs] > 0} {
214            pack $itk_component(graph) -expand yes -fill both
215        } else {
216            pack forget $itk_component(graph)
217            $itk_component(layout) configure -leftmargin 0 -rightmargin 0
218        }
219    } else {
220        #
221        # Two or more fields?  Then create a tab for each field
222        # and select the first one by default.  Make sure the
223        # graph is packed.
224        #
225        pack forget $itk_component(inner)
226        pack $itk_component(graph) -expand yes -fill both
227
228        foreach name $tabs {
229            $itk_component(tabs) insert end $name \
230                -activebackground $itk_option(-background)
231        }
232        $itk_component(tabs) select 0
233    }
234
235    #
236    # Scan through and look for any parameters in the <structure>.
237    # Register any parameters associated with fields, so we can
238    # add them as active controls whenever we install new fields.
239    # Create controls for any remaining parameters, so the user
240    # can see that there's something to adjust.
241    #
242    if {$_device != ""} {
243        foreach cname [$_device children parameters] {
244            set handled 0
245            if {[$_device element -as type parameters.$cname] == "number"} {
246                set name [$_device element -as id parameters.$cname]
247
248                # look for a field that uses this parameter
249                set found ""
250                foreach fname [$_device children fields] {
251                    foreach comp [$_device children fields.$fname] {
252                        set v [$_device get fields.$fname.$comp.constant]
253                        if {[string equal $v $name]} {
254                            set found "fields.$fname.$comp"
255                            break
256                        }
257                    }
258                    if {"" != $found} break
259                }
260
261                if {"" != $found} {
262                    set _field2parm($found) $name
263                    set handled 1
264                }
265            }
266
267            #
268            # Any parameter that was not handled above should be handled
269            # here, by adding it to a control panel above the device
270            # layout area.
271            #
272            if {!$handled} {
273                set t $itk_component(top)
274                if {![winfo exists $t.cntls]} {
275                    Rappture::Controls $t.cntls $_owner
276                    pack $t.cntls -expand yes -fill both
277                }
278                $t.cntls insert end parameters.$cname
279            }
280        }
281    }
282
283    #
284    # Install the first tab
285    #
286    _changeTabs
287
288    #
289    # Fix the right margin of the graph so that it has enough room
290    # to display the right-hand edge of the device.
291    #
292    $itk_component(graph) configure \
293        -rightmargin [$itk_component(layout) extents bar3D]
294
295    _fixSize
296}
297
298# ----------------------------------------------------------------------
299# USAGE: _changeTabs
300#
301# Used internally to change the field being displayed whenever a new
302# tab is selected.
303# ----------------------------------------------------------------------
304itcl::body Rappture::DeviceViewer1D::_changeTabs {} {
305    set graph $itk_component(graph)
306
307    #
308    # Figure out which tab is selected and make the inner frame
309    # visible in that tab.
310    #
311    set i [$itk_component(tabs) index select]
312    if {$i != ""} {
313        set name [$itk_component(tabs) get $i]
314        $itk_component(tabs) tab configure $name \
315            -window $itk_component(inner) -fill both
316    } else {
317        set name [lindex [array names _tab2fields] 0]
318    }
319
320    #
321    # Update the graph to show the current field.
322    #
323    eval $graph element delete [$graph element names]
324    eval $graph marker delete [$graph marker names]
325
326    foreach {zmin zmax} [$itk_component(layout) limits] { break }
327    if {$zmin != "" && $zmin < $zmax} {
328        $graph axis configure x -min $zmin -max $zmax
329    }
330
331    if {$_device != ""} {
332        set units [$_device get units]
333        if {$units != "arbitrary"} {
334            $graph axis configure x -hide no -title "Position ($units)"
335        } else {
336            $graph axis configure x -hide yes
337        }
338    } else {
339        $graph axis configure x -hide no -title "Position"
340    }
341
342    # turn on auto limits
343    $graph axis configure y -min "" -max ""
344
345    set flist ""
346    if {[info exists _tab2fields($name)]} {
347        set flist $_tab2fields($name)
348    }
349
350    set n 0
351    foreach fobj $flist {
352        catch {unset hints}
353        array set hints [$fobj hints]
354
355        if {[info exists hints(units)]} {
356            set _units $hints(units)
357            $graph axis configure y -title "$name ($hints(units))"
358        } else {
359            set _units ""
360            $graph axis configure y -title $name
361        }
362
363        if {[info exists hints(restrict)]} {
364            set _restrict $hints(restrict)
365        } else {
366            set _restrict ""
367        }
368
369        if {[info exists hints(scale)]
370              && [string match log* $hints(scale)]} {
371            $graph axis configure y -logscale yes
372        } else {
373            $graph axis configure y -logscale no
374        }
375
376        foreach comp [$fobj components] {
377            # can only handle 1D meshes here
378            if {[$fobj components -dimensions $comp] != "1D"} {
379                continue
380            }
381
382            set elem "elem[incr n]"
383            set xv [$fobj mesh $comp]
384            set yv [$fobj values $comp]
385
386            $graph element create $elem -x $xv -y $yv \
387                -color black -symbol "" -linewidth 2
388
389            if {[info exists hints(color)]} {
390                $graph element configure $elem -color $hints(color)
391            }
392
393            foreach {path x y val} [$fobj controls get $comp] {
394                if {$path != ""} {
395                    set id "control[incr n]"
396                    $graph marker create text -coords [list $x $y] \
397                        -text $val -anchor s -name $id -background ""
398                    $graph marker bind $id <Enter> \
399                        [itcl::code $this _marker enter $id]
400                    $graph marker bind $id <Leave> \
401                        [itcl::code $this _marker leave $id]
402                    $graph marker bind $id <ButtonPress> \
403                        [itcl::code $this _marker edit $id $fobj/$path]
404                }
405            }
406        }
407    }
408
409    # let the widget settle, then fix the axes to "nice" values
410    after cancel [itcl::code $this _fixAxes]
411    after 100 [itcl::code $this _fixAxes]
412}
413
414# ----------------------------------------------------------------------
415# USAGE: _fixSize
416#
417# Used internally to fix the overall size of this widget based on
418# the various parts inside.  Sets the requested width/height of the
419# widget so that it is big enough to display the device and its
420# fields.
421# ----------------------------------------------------------------------
422itcl::body Rappture::DeviceViewer1D::_fixSize {} {
423    update idletasks
424    set w [winfo reqwidth $itk_component(tabs)]
425    set h [winfo reqheight $itk_component(tabs)]
426    component hull configure -width $w -height $h
427}
428
429# ----------------------------------------------------------------------
430# USAGE: _fixAxes
431#
432# Used internally to adjust the y-axis limits of the graph to "nice"
433# values, so that any control marker associated with the value,
434# for example, remains on screen.
435# ----------------------------------------------------------------------
436itcl::body Rappture::DeviceViewer1D::_fixAxes {} {
437    set graph $itk_component(graph)
438    if {![winfo ismapped $graph]} {
439        after cancel [itcl::code $this _fixAxes]
440        after 100 [itcl::code $this _fixAxes]
441        return
442    }
443
444    #
445    # HACK ALERT!
446    # Use this code to fix up the y-axis limits for the BLT graph.
447    # The auto-limits don't always work well.  We want them to be
448    # set to a "nice" number slightly above or below the min/max
449    # limits.
450    #
451    set log [$graph axis cget y -logscale]
452    $graph axis configure y -min "" -max ""
453    foreach {min max} [$graph axis limits y] { break }
454
455    if {$log} {
456        set min [expr {0.9*$min}]
457        set max [expr {1.1*$max}]
458    } else {
459        if {$min > 0} {
460            set min [expr {0.95*$min}]
461        } else {
462            set min [expr {1.05*$min}]
463        }
464        if {$max > 0} {
465            set max [expr {1.05*$max}]
466        } else {
467            set max [expr {0.95*$max}]
468        }
469    }
470
471    # bump up the max so that it's big enough to show control markers
472    set fnt $itk_option(-font)
473    set h [expr {[font metrics $fnt -linespace] + 5}]
474    foreach mname [$graph marker names] {
475        set xy [$graph marker cget $mname -coord]
476        foreach {x y} [eval $graph transform $xy] { break }
477        set y [expr {$y-$h}]  ;# find top of text in pixels
478        foreach {x y} [eval $graph invtransform [list 0 $y]] { break }
479        if {$y > $max} { set max $y }
480    }
481
482    if {$log} {
483        set min [expr {pow(10.0,floor(log10($min)))}]
484        set max [expr {pow(10.0,ceil(log10($max)))}]
485    } else {
486        set min [expr {0.1*floor(10*$min)}]
487        set max [expr {0.1*ceil(10*$max)}]
488    }
489
490    $graph axis configure y -min $min -max $max
491
492    after cancel [list catch [itcl::code $this _align]]
493    after 100 [list catch [itcl::code $this _align]]
494}
495
496# ----------------------------------------------------------------------
497# USAGE: _align
498#
499# Used internally to align the margins of the device layout and the
500# graph, so that two areas line up.
501# ----------------------------------------------------------------------
502itcl::body Rappture::DeviceViewer1D::_align {} {
503    set graph $itk_component(graph)
504
505    #
506    # Set the left/right margins of the layout so that it aligns
507    # with the graph.  Set the right margin of the graph so it
508    # it is big enough to show the 3D edge of the device that
509    # hangs over on the right-hand side.
510    #
511    update
512    foreach {xmin xmax} [$graph axis limits x] { break }
513    set lm [$graph xaxis transform $xmin]
514    $itk_component(layout) configure -leftmargin $lm
515
516    set w [winfo width $graph]
517    set rm [expr {$w-[$graph xaxis transform $xmax]}]
518    $itk_component(layout) configure -rightmargin $rm
519}
520
521# ----------------------------------------------------------------------
522# USAGE: _marker enter <name>
523# USAGE: _marker leave <name>
524# USAGE: _marker edit <name> <path>
525# USAGE: _marker activate
526# USAGE: _marker validate <value>
527# USAGE: _marker apply <value>
528#
529# Used internally to manipulate the control markers draw on the
530# graph for a field.
531# ----------------------------------------------------------------------
532itcl::body Rappture::DeviceViewer1D::_marker {option {name ""} {path ""}} {
533    switch -- $option {
534        enter {
535            $itk_component(graph) marker configure $name -background #e5e5e5
536            #
537            # BE CAREFUL:  Need an update here to force the graph to
538            #   refresh itself or else a subsequent click on the
539            #   marker will ignore the text that was recently changed,
540            #   and fail to generate a <ButtonPress> event!
541            #
542            update idletasks
543        }
544        leave {
545            $itk_component(graph) marker configure $name -background ""
546            #
547            # BE CAREFUL:  Need an update here to force the graph to
548            #   refresh itself or else a subsequent click on the
549            #   marker will ignore the text that was recently changed,
550            #   and fail to generate a <ButtonPress> event!
551            #
552            update idletasks
553        }
554        edit {
555            set _marker(name) $name
556            set _marker(fobj) [lindex [split $path /] 0]
557            set _marker(path) [lindex [split $path /] 1]
558            $itk_component(geditor) activate
559        }
560        activate {
561            set g $itk_component(graph)
562            set val [$g marker cget $_marker(name) -text]
563            foreach {x y} [$g marker cget $_marker(name) -coords] { break }
564            foreach {x y} [$g transform $x $y] { break }
565            set x [expr {$x + [winfo rootx $g] - 4}]
566            set y [expr {$y + [winfo rooty $g] - 5}]
567
568            set fnt $itk_option(-font)
569            set h [expr {[font metrics $fnt -linespace] + 2}]
570            set w [expr {[font measure $fnt $val] + 5}]
571
572            return [list text $val \
573                x [expr {$x-$w/2}] \
574                y [expr {$y-$h}] \
575                w $w \
576                h $h]
577        }
578        validate {
579            if {$_units != ""} {
580                if {[catch {Rappture::Units::convert $name \
581                        -context $_units -to $_units} result] != 0} {
582                    if {[regexp {^bad.*: +(.)(.+)} $result match first tail]
583                          || [regexp {(.)(.+)} $result match first tail]} {
584                        set result "[string toupper $first]$tail"
585                    }
586                    bell
587                    Rappture::Tooltip::cue $itk_component(geditor) $result
588                    return 0
589                }
590                if {"" != $_restrict
591                      && [catch {Rappture::Units::convert $result \
592                        -context $_units -to $_units -units off} value] == 0} {
593
594                    set rexpr $_restrict
595                    regsub -all value $rexpr {$value} rexpr
596                    if {[catch {expr $rexpr} result] == 0 && !$result} {
597                        bell
598                        Rappture::Tooltip::cue $itk_component(geditor) "Should satisfy the condition: $_restrict"
599                        return 0
600                    }
601                }
602            }
603            return 1
604        }
605        apply {
606            if {$_units != ""} {
607                catch {Rappture::Units::convert $name \
608                    -context $_units -to $_units} value
609            } else {
610                set value $name
611            }
612
613            $_marker(fobj) controls put $_marker(path) $value
614            $_owner changed $_marker(path)
615            event generate $itk_component(hull) <<Edit>>
616
617            _changeTabs
618        }
619    }
620}
621
622# ----------------------------------------------------------------------
623# USAGE: _controlCreate <container> <libObj> <path>
624#
625# Used internally to create a gauge widget and pack it into the
626# given <container>.  When the gauge is set, it updates the value
627# for the <path> in the <libObj>.
628# ----------------------------------------------------------------------
629itcl::body Rappture::DeviceViewer1D::_controlCreate {container libObj path} {
630    set presets ""
631    foreach pre [$libObj children -type preset $path] {
632        lappend presets \
633            [$libObj get $path.$pre.value] \
634            [$libObj get $path.$pre.label]
635    }
636
637    set type Rappture::Gauge
638    set units [$libObj get $path.units]
639    if {$units != ""} {
640        set desc [Rappture::Units::description $units]
641        if {[string match temperature* $desc]} {
642            set type Rappture::TemperatureGauge
643        }
644    }
645
646    set counter 0
647    set w "$container.gauge[incr counter]"
648    while {[winfo exists $w]} {
649        set w "$container.gauge[incr counter]"
650    }
651
652    # create the widget
653    $type $w -units $units -presets $presets
654    pack $w -side top -anchor w
655    bind $w <<Value>> [itcl::code $this _controlSet $w $libObj $path]
656
657    set min [$libObj get $path.min]
658    if {"" != $min} { $w configure -minvalue $min }
659
660    set max [$libObj get $path.max]
661    if {"" != $max} { $w configure -maxvalue $max }
662
663    set str [$libObj get $path.default]
664    if {$str != ""} { $w value $str }
665
666    if {$type == "Rappture::Gauge" && "" != $min && "" != $max} {
667        set color [$libObj get $path.color]
668        if {$color == ""} {
669            set color blue
670        }
671        if {$units != ""} {
672            set min [Rappture::Units::convert $min -to $units -units off]
673            set max [Rappture::Units::convert $max -to $units -units off]
674        }
675        $w configure -spectrum [Rappture::Spectrum ::#auto [list \
676            $min white $max $color] -units $units]
677    }
678
679    set str [$libObj get $path.label]
680    if {$str != ""} {
681        set help [$libObj get $path.help]
682        if {"" != $help} {
683            append str "\n$help"
684        }
685        if {$units != ""} {
686            set desc [Rappture::Units::description $units]
687            append str "\n(units of $desc)"
688        }
689        Rappture::Tooltip::for $w $str
690    }
691
692    set str [$libObj get $path.icon]
693    if {$str != ""} {
694        $w configure -image [image create photo -data $str]
695    }
696}
697
698# ----------------------------------------------------------------------
699# USAGE: _controlSet <widget> <libObj> <path>
700#
701# Invoked automatically whenever an internal control changes value.
702# Queries the new value for the control and assigns the value to the
703# given <path> on the XML object <libObj>.
704# ----------------------------------------------------------------------
705itcl::body Rappture::DeviceViewer1D::_controlSet {widget libObj path} {
706    set newval [$widget value]
707    $libObj put $path.current $newval
708    event generate $itk_component(hull) <<Edit>>
709}
710
711# ----------------------------------------------------------------------
712# CONFIGURATION OPTION: -device
713#
714# Set to the Rappture::Library object representing the device being
715# displayed in the viewer.  If set to "", the viewer is cleared to
716# display nothing.
717# ----------------------------------------------------------------------
718itcl::configbody Rappture::DeviceViewer1D::device {
719    if {$itk_option(-device) != ""} {
720        if {![Rappture::library isvalid $itk_option(-device)]} {
721            error "bad value \"$itk_option(-device)\": should be Rappture::Library"
722        }
723    }
724    set _device $itk_option(-device)
725    _loadDevice
726}
Note: See TracBrowser for help on using the repository browser.