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

Last change on this file since 26 was 26, checked in by mmc, 17 years ago

Fixed the rendering of groups, and groups within groups.
If groups are mixed in with other elements, then they are
drawn with a gray outline/heading, with the title taken
from the <group><about><label>. However, if a group
contains only other groups, then it is treated as a tabbed
notebook, and each group within is put on a separate page.

WARNING: There are many bad interactions between the
blt::tabset, the Rappture::Scroller, and the Rappture::Pager.
Pages shake violently when all are in play. The only way I
could get them to settle down was by putting the tabs above
the pages they control. Have to revisit this some time to
make it look better...

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