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

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

Fixed the device viewer to resize itself properly based on its
contents. The device layout takes into account the size of any
icon for a material layer, so a picture of a device can be embedded
by creating a single layer with an icon.

Fixed the Gauge to resize its icon area properly when the -spectrum
is configured later on.

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
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        # == DEPRECATED FUNCTIONALITY ==
208        # (I like the look of the tab, even if there's only one)
209        #
210        # No fields or one field?  Then we don't need to bother
211        # with tabs.  Just pack the inner frame directly.  If
212        # there are no fields, get rid of the graph.
213        #
214        pack $itk_component(inner) -expand yes -fill both
215        if {[llength $tabs] > 0} {
216            pack $itk_component(graph) -expand yes -fill both
217        } else {
218            pack forget $itk_component(graph)
219            $itk_component(layout) configure -leftmargin 0 -rightmargin 0
220        }
221    } else {
222        #
223        # Two or more fields?  Then create a tab for each field
224        # and select the first one by default.  Make sure the
225        # graph is packed.
226        #
227        pack forget $itk_component(inner)
228        pack $itk_component(graph) -expand yes -fill both
229
230        foreach name $tabs {
231            $itk_component(tabs) insert end $name \
232                -activebackground $itk_option(-background)
233        }
234        $itk_component(tabs) select 0
235    }
236
237    #
238    # Scan through and look for any parameters in the <structure>.
239    # Register any parameters associated with fields, so we can
240    # add them as active controls whenever we install new fields.
241    # Create controls for any remaining parameters, so the user
242    # can see that there's something to adjust.
243    #
244    if {$_device != ""} {
245        foreach cname [$_device children parameters] {
246            set handled 0
247            if {[$_device element -as type parameters.$cname] == "number"} {
248                set name [$_device element -as id parameters.$cname]
249
250                # look for a field that uses this parameter
251                set found ""
252                foreach fname [$_device children fields] {
253                    foreach comp [$_device children fields.$fname] {
254                        set v [$_device get fields.$fname.$comp.constant]
255                        if {[string equal $v $name]} {
256                            set found "fields.$fname.$comp"
257                            break
258                        }
259                    }
260                    if {"" != $found} break
261                }
262
263                if {"" != $found} {
264                    set _field2parm($found) $name
265                    set handled 1
266                }
267            }
268
269            #
270            # Any parameter that was not handled above should be handled
271            # here, by adding it to a control panel above the device
272            # layout area.
273            #
274            if {!$handled} {
275                set t $itk_component(top)
276                if {![winfo exists $t.cntls]} {
277                    Rappture::Controls $t.cntls $_owner
278                    pack $t.cntls -expand yes -fill both
279                }
280                $t.cntls insert end parameters.$cname
281            }
282        }
283    }
284
285    #
286    # Install the first tab
287    #
288    _changeTabs
289
290    #
291    # Fix the right margin of the graph so that it has enough room
292    # to display the right-hand edge of the device.
293    #
294    $itk_component(graph) configure \
295        -rightmargin [$itk_component(layout) extents bar3D]
296
297    _fixSize
298}
299
300# ----------------------------------------------------------------------
301# USAGE: _changeTabs
302#
303# Used internally to change the field being displayed whenever a new
304# tab is selected.
305# ----------------------------------------------------------------------
306itcl::body Rappture::DeviceViewer1D::_changeTabs {} {
307    set graph $itk_component(graph)
308
309    #
310    # Figure out which tab is selected and make the inner frame
311    # visible in that tab.
312    #
313    set i [$itk_component(tabs) index select]
314    if {$i != ""} {
315        set name [$itk_component(tabs) get $i]
316        $itk_component(tabs) tab configure $name \
317            -window $itk_component(inner) -fill both
318    } else {
319        set name [lindex [array names _tab2fields] 0]
320    }
321
322    #
323    # Update the graph to show the current field.
324    #
325    eval $graph element delete [$graph element names]
326    eval $graph marker delete [$graph marker names]
327
328    foreach {zmin zmax} [$itk_component(layout) limits] { break }
329    if {$_device != ""} {
330        set units [$_device get units]
331        if {$units != "arbitrary" && $zmax > $zmin} {
332            $graph axis configure x -hide no -min $zmin -max $zmax \
333                -title "Position ($units)"
334        } else {
335            $graph axis configure x -hide yes
336        }
337    } else {
338        $graph axis configure x -hide no -min $zmin -max $zmax \
339            -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.