source: branches/1.3/gui/scripts/resultselector.tcl @ 4531

Last change on this file since 4531 was 4215, checked in by mmc, 10 years ago

Fix for ticket #266315, which occurred when you simulate two results,
delete one, then simulate again. An old value from the deleted simulation
was hanging around and causing an internal error. This fix clears the
value so it works properly.

File size: 49.2 KB
Line 
1# -*- mode: tcl; indent-tabs-mode: nil -*-
2# ----------------------------------------------------------------------
3#  COMPONENT: ResultSelector - controls for a ResultSet
4#
5#  This widget displays a collection of results stored in a ResultSet
6#  object.  It manages the controls to select and visualize the data.
7# ======================================================================
8#  AUTHOR:  Michael McLennan, Purdue University
9#  Copyright (c) 2004-2012  HUBzero Foundation, LLC
10#
11#  See the file "license.terms" for information on usage and
12#  redistribution of this file, and for a DISCLAIMER OF ALL WARRANTIES.
13# ======================================================================
14package require Itk
15
16option add *ResultSelector.width 4i widgetDefault
17option add *ResultSelector.height 4i widgetDefault
18option add *ResultSelector.missingData skip widgetDefault
19option add *ResultSelector.controlbarBackground gray widgetDefault
20option add *ResultSelector.controlbarForeground white widgetDefault
21option add *ResultSelector.activeControlBackground #ffffcc widgetDefault
22option add *ResultSelector.activeControlForeground black widgetDefault
23option add *ResultSelector.controlActiveForeground blue widgetDefault
24option add *ResultSelector.toggleBackground gray widgetDefault
25option add *ResultSelector.toggleForeground white widgetDefault
26option add *ResultSelector.textFont \
27    -*-helvetica-medium-r-normal-*-12-* widgetDefault
28option add *ResultSelector.boldFont \
29    -*-helvetica-bold-r-normal-*-12-* widgetDefault
30
31itcl::class Rappture::ResultSelector {
32    inherit itk::Widget
33
34    itk_option define -resultset resultSet ResultSet ""
35    itk_option define -activecontrolbackground activeControlBackground Background ""
36    itk_option define -activecontrolforeground activeControlForeground Foreground ""
37    itk_option define -controlactiveforeground controlActiveForeground Foreground ""
38    itk_option define -togglebackground toggleBackground Background ""
39    itk_option define -toggleforeground toggleForeground Foreground ""
40    itk_option define -textfont textFont Font ""
41    itk_option define -boldfont boldFont Font ""
42    itk_option define -foreground foreground Foreground ""
43    itk_option define -settingscommand settingsCommand SettingsCommand ""
44
45    constructor {args} { # defined below }
46    destructor { # defined below }
47
48    public method activate {column}
49    public method size {{what -results}}
50
51    protected method _doClear {what}
52    protected method _doSettings {{cmd ""}}
53    protected method _control {option args}
54    protected method _fixControls {args}
55    protected method _fixLayout {args}
56    protected method _fixNumResults {}
57    protected method _fixSettings {args}
58    protected method _fixValue {column why}
59    protected method _drawValue {column widget wmax}
60    protected method _toggleAll {{column "current"}}
61    protected method _getValues {column {which "all"}}
62    protected method _getTooltip {role column}
63    protected method _getParamDesc {which {index "current"}}
64    protected method _log {col}
65
66    private variable _dispatcher ""  ;# dispatchers for !events
67    private variable _resultset ""   ;# object containing results
68    private variable _active ""      ;# name of active control
69    private variable _plotall 0      ;# non-zero => plot all active results
70    private variable _layout         ;# info used in _fixLayout
71    private variable _counter 0      ;# counter for unique control names
72    private variable _settings 0     ;# non-zero => _fixSettings in progress
73
74    private common _cntlInfo         ;# maps column name => control info
75}
76                                                                               
77itk::usual ResultSelector {
78    keep -background -foreground -cursor -font
79}
80
81# ----------------------------------------------------------------------
82# CONSTRUCTOR
83# ----------------------------------------------------------------------
84itcl::body Rappture::ResultSelector::constructor {args} {
85    option add hull.width hull.height
86    pack propagate $itk_component(hull) no
87
88    # create a dispatcher for events
89    Rappture::dispatcher _dispatcher
90
91    $_dispatcher register !layout
92    $_dispatcher dispatch $this !layout \
93        [itcl::code $this _fixLayout]
94
95    $_dispatcher register !settings
96    $_dispatcher dispatch $this !settings \
97        [itcl::code $this _fixSettings]
98
99    # initialize controls info
100    set _cntlInfo($this-all) ""
101
102    # initialize layout info
103    set _layout(mode) "usual"
104    set _layout(active) ""
105    set _layout(numcntls) 0
106
107    itk_component add cntls {
108        frame $itk_interior.cntls
109    } {
110        usual
111        rename -background -controlbarbackground controlbarBackground Background
112        rename -highlightbackground -controlbarbackground controlbarBackground Background
113    }
114    pack $itk_component(cntls) -fill x -pady {0 2}
115
116    itk_component add clearall {
117        button $itk_component(cntls).clearall -text "Clear" -state disabled \
118            -padx 1 -pady 1 \
119            -relief flat -overrelief raised \
120            -command [itcl::code $this _doClear all]
121    } {
122        usual
123        rename -background -controlbarbackground controlbarBackground Background
124        rename -foreground -controlbarforeground controlbarForeground Foreground
125        rename -highlightbackground -controlbarbackground controlbarBackground Background
126    }
127    pack $itk_component(clearall) -side right -padx 2 -pady 1
128    Rappture::Tooltip::for $itk_component(clearall) \
129        "Clears all results collected so far."
130
131    itk_component add clear {
132        button $itk_component(cntls).clear -text "Clear One" -state disabled \
133            -padx 1 -pady 1 \
134            -relief flat -overrelief raised \
135            -command [itcl::code $this _doClear current]
136    } {
137        usual
138        rename -background -controlbarbackground controlbarBackground Background
139        rename -foreground -controlbarforeground controlbarForeground Foreground
140        rename -highlightbackground -controlbarbackground controlbarBackground Background
141    }
142    pack $itk_component(clear) -side right -padx 2 -pady 1
143    Rappture::Tooltip::for $itk_component(clear) \
144        "Clears the result that is currently selected."
145
146    itk_component add status {
147        label $itk_component(cntls).status -anchor w \
148            -text "No results" -padx 0 -pady 0
149    } {
150        usual
151        rename -background -controlbarbackground controlbarBackground Background
152        rename -foreground -controlbarforeground controlbarForeground Foreground
153        rename -highlightbackground -controlbarbackground controlbarBackground Background
154    }
155    pack $itk_component(status) -side left -padx 2 -pady {2 0}
156
157    itk_component add dials {
158        frame $itk_interior.dials
159    }
160    pack $itk_component(dials) -expand yes -fill both
161    bind $itk_component(dials) <Configure> \
162        [list $_dispatcher event -after 10 !layout why resize]
163
164    # create the permanent controls in the "short list" area
165    set dials $itk_component(dials)
166    frame $dials.bg
167    Rappture::Radiodial $dials.dial -valuewidth 0
168    Rappture::Tooltip::for $dials.dial \
169        "@[itcl::code $this _getTooltip dial active]"
170
171    set fn [option get $itk_component(hull) textFont Font]
172    label $dials.all -text "All" -padx 8 \
173        -borderwidth 1 -relief raised -font $fn
174    Rappture::Tooltip::for $dials.all \
175        "@[itcl::code $this _getTooltip all active]"
176    bind $dials.all <ButtonRelease> [itcl::code $this _toggleAll]
177
178    frame $dials.labelmore
179    label $dials.labelmore.arrow -bitmap [Rappture::icon empty] -borderwidth 0
180    pack $dials.labelmore.arrow -side left -fill y
181    label $dials.labelmore.name -text "more parameters..." -font $fn \
182        -borderwidth 0 -padx 0 -pady 1
183    pack $dials.labelmore.name -side left
184    label $dials.labelmore.value
185    pack $dials.labelmore.value -side left
186
187    eval itk_initialize $args
188}
189
190# ----------------------------------------------------------------------
191# DESTRUCTOR
192# ----------------------------------------------------------------------
193itcl::body Rappture::ResultSelector::destructor {} {
194    # disconnect any existing resultset to stop notifications
195    configure -resultset ""
196}
197
198# ----------------------------------------------------------------------
199# USAGE: activate <column>
200#
201# Clients use this to activate a particular column in the set of
202# controls.  When a column is active, its label is bold and its
203# value has a radiodial in the "short list" area.
204# ----------------------------------------------------------------------
205itcl::body Rappture::ResultSelector::activate {column} {
206    if {$_resultset ne ""} {
207        set allowed [$_resultset diff names]
208        if {[lsearch $allowed $column] < 0} {
209            error "bad value \"$column\": should be one of [join $allowed {, }]"
210        }
211
212        # column is now active
213        set _active $column
214
215        # keep track of usage, so we know which controls are popular
216        incr _cntlInfo($this-$column-usage)
217
218        # fix controls at next idle point
219        $_dispatcher event -idle !layout why data
220        $_dispatcher event -idle !settings column $_active
221    }
222}
223
224# ----------------------------------------------------------------------
225# USAGE: size ?-results|-controls|-controlarea?
226#
227# Returns various measures for the size of this area:
228#   -results ....... number of results loaded
229#   -controls ...... number of distinct control parameters
230#   -controlarea ... minimum size of usable control area, in pixels
231# ----------------------------------------------------------------------
232itcl::body Rappture::ResultSelector::size {{what -results}} {
233    switch -- $what {
234        -results {
235            if {$_resultset eq ""} {
236                return 0
237            }
238            return [$_resultset size]
239        }
240        -controls {
241            return [expr {[llength [$_resultset diff names]]-1}]
242        }
243        -controlarea {
244            set ht [winfo reqheight $itk_component(cntls)]
245            incr ht 2  ;# padding below controls
246
247            set normalLine [font metrics $itk_option(-textfont) -linespace]
248            incr normalLine 2  ;# padding
249            set boldLine [font metrics $itk_option(-boldfont) -linespace]
250            incr boldLine 2  ;# padding
251
252            set numcntls [expr {[llength [$_resultset diff names]]-1}]
253            switch -- $numcntls {
254                0 - 1 {
255                    # 0 = no controls (no data at all)
256                    # 1 = run control, but only 1 run so far
257                    # add nothing
258                }
259                default {
260                    # non-active controls
261                    incr ht [expr {($numcntls-1)*$normalLine}]
262                    # active control
263                    incr ht $boldLine
264                    # dial for active control
265                    incr ht [winfo reqheight $itk_component(dials).dial]
266                    # padding around active control
267                    incr ht 4
268                }
269            }
270            return $ht
271        }
272        default {
273            error "bad option \"$what\": should be -results, -controls, or -controlarea"
274        }
275    }
276}
277
278# ----------------------------------------------------------------------
279# USAGE: _doClear all|current
280#
281# Invoked automatically when the user presses the "Clear One" or
282# "Clear All" buttons.  Clears results from the ResultSet and then
283# waits for an event to react to the change.
284# ----------------------------------------------------------------------
285itcl::body Rappture::ResultSelector::_doClear {what} {
286    if {$_resultset ne ""} {
287        switch -- $what {
288            current {
289                set simnum $_cntlInfo($this-simnum-value)
290                foreach xmlobj [$_resultset find simnum $simnum] {
291                    $_resultset clear $xmlobj
292                }
293                Rappture::Logger::log result -clear one
294            }
295            all {
296                $_resultset clear
297                Rappture::Logger::log result -clear all
298            }
299            default { error "bad option \"$what\": should be current or all" }
300        }
301    }
302}
303
304# ----------------------------------------------------------------------
305# USAGE: _doSettings ?<command>?
306#
307# Used internally whenever the result selection changes to invoke
308# the -settingscommand.  This will notify some external widget, which
309# with perform the plotting action specified in the <command>.
310# ----------------------------------------------------------------------
311itcl::body Rappture::ResultSelector::_doSettings {{cmd ""}} {
312    if {[string length $itk_option(-settingscommand)] > 0} {
313        uplevel #0 $itk_option(-settingscommand) $cmd
314    }
315}
316
317# ----------------------------------------------------------------------
318# USAGE: _control bind <widget> <column>
319# USAGE: _control hilite <state> <column> <panel>
320# USAGE: _control load <widget> <column>
321#
322# Used internally to manage the interactivity of controls.  The "bind"
323# operation sets up bindings on the label/value for each control, so
324# you can mouse over and click on a control to activate it.  The
325# "hilite" operation controls highlighting of the control.  The "load"
326# operation loads data into the specified radiodial <widget>.
327# ----------------------------------------------------------------------
328itcl::body Rappture::ResultSelector::_control {option args} {
329    switch -- $option {
330        bind {
331            if {[llength $args] != 2} {
332                error "wrong # args: should be _control bind widget column"
333            }
334            set widget [lindex $args 0]
335            set col [lindex $args 1]
336
337            set panel [winfo parent $widget]
338            if {[string match label* [winfo name $panel]]} {
339                set panel [winfo parent $panel]
340            }
341
342            bind $widget <Enter> \
343                [itcl::code $this _control hilite on $col $panel]
344            bind $widget <Leave> \
345                [itcl::code $this _control hilite off $col $panel]
346            bind $widget <ButtonRelease> "
347                [itcl::code $this activate $col]
348                [list Rappture::Logger::log result -active $col]
349            "
350        }
351        hilite {
352            if {[llength $args] != 3} {
353                error "wrong # args: should be _control hilite state column panel"
354            }
355            if {$_layout(mode) != "usual"} {
356                # abbreviated controls? then skip highlighting
357                return
358            }
359            set state [lindex $args 0]
360            set col [lindex $args 1]
361            set panel [lindex $args 2]
362
363            if {[string index $col 0] == "@"} {
364                # handle artificial names like "@more"
365                set id [string range $col 1 end]
366            } else {
367                # get id for ordinary columns
368                set id $_cntlInfo($this-$col-id)
369            }
370
371            # highlight any non-active entries
372            if {$col != $_active} {
373                if {$state} {
374                    set fg $itk_option(-controlactiveforeground)
375                    $panel.label$id.name configure -fg $fg
376                    $panel.label$id.value configure -fg $fg
377                    $panel.label$id.arrow configure -fg $fg \
378                        -bitmap [Rappture::icon rarrow2]
379                } else {
380                    set fg $itk_option(-foreground)
381                    $panel.label$id.name configure -fg $fg
382                    $panel.label$id.value configure -fg $fg
383                    $panel.label$id.arrow configure -fg $fg \
384                        -bitmap [Rappture::icon empty]
385                }
386            }
387        }
388        load {
389            if {[llength $args] != 2} {
390                error "wrong # args: should be _control load widget column"
391            }
392            set dial [lindex $args 0]
393            set col [lindex $args 1]
394
395            set shortlist $itk_component(dials)
396            $shortlist.dial configure -variable ""
397            $dial clear
398            foreach {label val} [_getValues $col all] {
399                $dial add $label $val
400            }
401            $shortlist.dial configure -variable \
402                "::Rappture::ResultSelector::_cntlInfo($this-$col-value)" \
403                -interactcommand [itcl::code $this _log $col]
404        }
405        default {
406            error "bad option \"$option\": should be bind, hilite, or load"
407        }
408    }
409}
410
411# ----------------------------------------------------------------------
412# USAGE: _fixControls ?<eventArgs...>?
413#
414# Called automatically at the idle point after one or more results
415# have been added to this result set.  Scans through all existing
416# data and updates controls used to select the data.
417# ----------------------------------------------------------------------
418itcl::body Rappture::ResultSelector::_fixControls {args} {
419    array set eventData $args
420    if {![info exists itk_component(dials)]} {
421        # no controls? then nothing to fix
422        # this happens when the widget is being destroyed
423        return
424    }
425    set shortlist $itk_component(dials)
426
427    # added or cleared something -- update number of results
428    _fixNumResults
429
430    if {$_resultset eq ""} {
431        return
432    }
433
434    # cleared something?
435    if {$eventData(op) eq "clear"} {
436        if {[$_resultset size] == 0} {
437            # cleared everything? then reset controls to initial state
438            array unset _cntlInfo $this-*
439            set _cntlInfo($this-all) ""
440            set _active ""
441            set _plotall 0
442            $itk_component(dials).all configure -relief raised \
443                -background $itk_option(-background) \
444                -foreground $itk_option(-foreground)
445
446            # clean up all of the label/value widgets
447            foreach w [winfo children $itk_component(dials)] {
448                if {[string match {label[0-9]*} [winfo name $w]]} {
449                    destroy $w
450                }
451            }
452            set _counter 0
453
454            $_dispatcher event -idle !layout why data
455
456            return
457        }
458
459        # cleared a single value? then move to another one
460        array set params $eventData(what)
461
462        # clear any currently highlighted result
463        _doSettings
464
465        # did the active control go away?  then switch to simnum
466        if {[lsearch -exact [$_resultset diff names] $_active] < 0} {
467            set _cntlInfo($this-$_active-value) ""
468            activate simnum
469        }
470
471        # figure out where we were in the active control, and
472        # what value we should display now that this was deleted
473        if {[info exists params($_active)]} {
474            set current $params($_active)
475            if {$current eq [$shortlist.dial get -format label current]} {
476                # result deleted is the current result
477                set vlist [$shortlist.dial get]
478                set i [lsearch -exact $vlist $current]
479                if {$i >= 0} {
480                    if {$i+1 < [llength $vlist]} {
481                        set newControlValue [lindex $vlist [expr {$i+1}]]
482                    } elseif {$i-1 >= 0} {
483                        set newControlValue [lindex $vlist [expr {$i-1}]]
484                    }
485                }
486            }
487        }
488
489        if {[info exists newControlValue]} {
490            # Set the control to a value we were able to find.
491            # Disconnect the current variable, then plug in the
492            # new (legal) value, then reload the controls.
493            # This will trigger !settings and other adjustments.
494            $shortlist.dial configure -variable ""
495            set _cntlInfo($this-$_active-value) $newControlValue
496            _control load $shortlist.dial $_active
497        } else {
498            # if all else fails, show solution #1
499            set xmlobj0 [lindex [$_resultset find * *] 0]
500            set simnum0 [$_resultset get simnum $xmlobj0]
501            set _cntlInfo($this-simnum-value) $simnum0
502            activate simnum
503        }
504
505        # if clearing this dataset changed the controls, then
506        # fix the layout
507        set numcntls [expr {[llength [$_resultset diff names]]-1}]
508        if {$numcntls != $_layout(numcntls)} {
509            $_dispatcher event -idle !layout why data
510        }
511        return
512    }
513
514    # must have added something...
515    set shortlist $itk_component(dials)
516    grid columnconfigure $shortlist 1 -weight 1
517
518    #
519    # Scan through all columns in the data and create any
520    # controls that just appeared.
521    #
522    set nadded 0
523    foreach col [$_resultset diff names] {
524        if {$col eq "xmlobj"} {
525            continue  ;# never create a control for this column
526        }
527
528        #
529        # If this column doesn't have a control yet, then
530        # create one.
531        #
532        if {![info exists _cntlInfo($this-$col-id)]} {
533            set tip ""
534            if {$col eq "simnum"} {
535                set quantity "Simulation"
536                set tip "List of all simulations that you have performed so far."
537            } else {
538                # search for the first XML object with this element defined
539                foreach xmlobj [$_resultset find * *] {
540                    set quantity [$xmlobj get $col.about.label]
541                    set tip [$xmlobj get $col.about.description]
542                    if {"" != $quantity} {
543                        break
544                    }
545                }
546                if {"" == $quantity && "" != $xmlobj} {
547                    set quantity [$xmlobj element -as id $col]
548                }
549            }
550
551            # Create the controls for the "short list" area.
552            set fn $itk_option(-textfont)
553            set w $shortlist.label$_counter
554            set row [lindex [grid size $shortlist] 1]
555            frame $w
556            grid $w -row $row -column 1 -sticky ew
557            label $w.arrow -bitmap [Rappture::icon empty] -borderwidth 0
558            pack $w.arrow -side left -fill y
559            _control bind $w.arrow $col
560
561            label $w.name -text $quantity -anchor w \
562                -borderwidth 0 -padx 0 -pady 1 -font $fn
563            pack $w.name -side left
564            bind $w.name <Configure> [itcl::code $this _fixValue $col resize]
565            _control bind $w.name $col
566
567            label $w.value -anchor w \
568                -borderwidth 0 -padx 0 -pady 1 -font $fn
569            pack $w.value -side left
570            bind $w.value <Configure> [itcl::code $this _fixValue $col resize]
571            _control bind $w.value $col
572
573            Rappture::Tooltip::for $w \
574                "@[itcl::code $this _getTooltip label $col]"
575
576            # create a record for this control
577            lappend _cntlInfo($this-all) $col
578            set _cntlInfo($this-$col-id) $_counter
579            set _cntlInfo($this-$col-label) $quantity
580            set _cntlInfo($this-$col-tip) $tip
581            set _cntlInfo($this-$col-value) ""
582            set _cntlInfo($this-$col-usage) 0
583            set _cntlInfo($this-$col) ""
584
585            trace add variable _cntlInfo($this-$col-value) write \
586                "[itcl::code $this _fixValue $col value]; list"
587
588            incr _counter
589
590            incr nadded
591        }
592
593        #
594        # Determine the unique values for this column and load
595        # them into the control.
596        #
597        set id $_cntlInfo($this-$col-id)
598
599        if {$col == $_layout(active)} {
600            _control load $shortlist.dial $col
601        }
602    }
603
604    #
605    # Activate the most recent control.  If a bunch of controls
606    # were just added, then activate the "Simulation" control,
607    # since that's the easiest way to page through results.
608    #
609    set numcntls [expr {[llength [$_resultset diff names]]-1}]
610    if {$nadded > 0 || $numcntls != $_layout(numcntls)} {
611        if {$numcntls == 2 || $nadded == 1} {
612            activate [lindex [$_resultset diff names] end]
613        } else {
614            activate simnum
615        }
616
617        # fix the shortlist layout to show as many controls as we can
618        $_dispatcher event -idle !layout why data
619    }
620
621    #
622    # Set all controls to the settings of the most recent addition.
623    # Setting the value slot will trigger the !settings event, which
624    # will then fix all other controls to match the one that changed.
625    #
626    if {[info exists eventData(what)]} {
627        set xmlobj $eventData(what)
628        set simnum [$_resultset get simnum $xmlobj]
629        set _cntlInfo($this-simnum-value) $simnum
630    }
631}
632
633# ----------------------------------------------------------------------
634# USAGE: _fixLayout ?<eventArgs...>?
635#
636# Called automatically at the idle point after the controls have
637# changed, or the size of the window has changed.  Fixes the layout
638# so that the active control is displayed, and other recent controls
639# are shown above and/or below.  At the very least, we must show the
640# "more options..." control.
641# ----------------------------------------------------------------------
642itcl::body Rappture::ResultSelector::_fixLayout {args} {
643    array set eventdata $args
644
645    set shortlist $itk_component(dials)
646
647    # clear out the short list area
648    foreach w [grid slaves $shortlist] {
649        grid forget $w
650    }
651
652    # reset all labels back to an ordinary font/background
653    set fn $itk_option(-textfont)
654    set bg $itk_option(-background)
655    set fg $itk_option(-foreground)
656    foreach col $_cntlInfo($this-all) {
657        set id $_cntlInfo($this-$col-id)
658        $shortlist.label$id configure -background $bg
659        $shortlist.label$id.arrow configure -background $bg \
660            -bitmap [Rappture::icon empty]
661        $shortlist.label$id.name configure -font $fn -background $bg
662        $shortlist.label$id.value configure -background $bg
663    }
664
665    # only 1 result? then we don't need any controls
666    if {$_resultset eq "" || [$_resultset size] < 2} {
667        set _layout(active) $_active
668
669        # let clients know that the layout has changed
670        # so they can fix the overall size accordingly
671        if {![info exists eventdata(why)] || $eventdata(why) ne "resize"} {
672            event generate $itk_component(hull) <<Layout>>
673        }
674
675        return
676    }
677
678    # compute the number of controls that will fit in the shortlist area
679    set dials $itk_component(dials)
680    set h [winfo height $dials]
681    set normalLine [font metrics $itk_option(-textfont) -linespace]
682    set boldLine [font metrics $itk_option(-boldfont) -linespace]
683    set active [expr {$boldLine+[winfo reqheight $dials.dial]+4}]
684
685    if {$h < $active+$normalLine} {
686        # active control kinda big? then show parameter values only
687        set _layout(mode) abbreviated
688        set ncntls [expr {int(floor(double($h)/$normalLine))}]
689    } else {
690        set _layout(mode) usual
691        set ncntls [expr {int(floor(double($h-$active)/$normalLine))+1}]
692    }
693
694    # find the controls with the most usage
695    set order ""
696    foreach col [lrange [$_resultset diff names] 1 end] {
697        lappend order [list $col $_cntlInfo($this-$col-usage)]
698    }
699    set order [lsort -integer -decreasing -index 1 $order]
700
701    set mostUsed ""
702    if {[llength $order] <= $ncntls} {
703        # plenty of space? then show all controls
704        foreach item $order {
705            lappend mostUsed [lindex $item 0]
706        }
707    } else {
708        # otherwise, limit to the most-used controls
709        foreach item [lrange $order 0 [expr {$ncntls-1}]] {
710            lappend mostUsed [lindex $item 0]
711        }
712
713        # make sure the active control is included
714        if {"" != $_active && [lsearch -exact $mostUsed $_active] < 0} {
715            set mostUsed [lreplace [linsert $mostUsed 0 $_active] end end]
716        }
717
718        # if there are more controls, add the "more parameters..." entry
719        if {$ncntls >= 2} {
720            set mostUsed [lreplace $mostUsed end end @more]
721            set rest [expr {[llength $order]-($ncntls-1)}]
722            if {$rest == 1} {
723                $dials.labelmore.name configure -text "1 more parameter..."
724            } else {
725                $dials.labelmore.name configure -text "$rest more parameters..."
726            }
727        }
728    }
729
730    # show controls associated with diffs and put up the radiodial
731    # for the "active" column
732    set row 0
733    foreach col [concat [lrange [$_resultset diff names] 1 end] @more] {
734        # this control not on the short list? then ignore it
735        if {[lsearch $mostUsed $col] < 0} {
736            continue
737        }
738
739        if {[string index $col 0] == "@"} {
740            set id [string range $col 1 end]
741        } else {
742            set id $_cntlInfo($this-$col-id)
743        }
744        grid $shortlist.label$id -row $row -column 1 -sticky ew -padx 4
745
746        if {$col == $_active} {
747            if {$_layout(mode) == "usual"} {
748                # put the background behind the active control in the shortlist
749                grid $shortlist.bg -row $row -rowspan 2 \
750                    -column 0 -columnspan 2 -sticky nsew
751                lower $shortlist.bg
752
753                # place the All and dial in the shortlist area
754                grid $shortlist.all -row $row -rowspan 2 -column 0 \
755                    -sticky nsew -padx 2 -pady 2
756                grid $shortlist.dial -row [expr {$row+1}] -column 1 \
757                    -sticky ew -padx 4
758                incr row
759
760                if {$_layout(active) != $_active} {
761                    $shortlist.dial configure -variable ""
762                    _control load $shortlist.dial $col
763                    $shortlist.dial configure -variable \
764                      "::Rappture::ResultSelector::_cntlInfo($this-$col-value)"
765                    set _layout(active) $_active
766                }
767            }
768        }
769        incr row
770    }
771
772    # highlight the active control
773    if {[info exists _cntlInfo($this-$_active-id)]} {
774        set id $_cntlInfo($this-$_active-id)
775        set bf $itk_option(-boldfont)
776        set fg $itk_option(-activecontrolforeground)
777        set bg $itk_option(-activecontrolbackground)
778
779        if {$_layout(mode) == "usual"} {
780            $shortlist.label$id configure -background $bg
781            $shortlist.label$id.arrow configure -foreground $fg \
782                -background $bg -bitmap [Rappture::icon rarrow]
783            $shortlist.label$id.name configure -foreground $fg \
784                -background $bg -font $bf
785            $shortlist.label$id.value configure -foreground $fg \
786                -background $bg
787            $shortlist.dial configure -background $bg
788            $shortlist.bg configure -background $bg
789
790            if {[$shortlist.all cget -relief] == "raised"} {
791                $shortlist.all configure -foreground $fg -background $bg
792            }
793        }
794    }
795
796    # let clients know that the layout has changed
797    # so they can fix the overall size accordingly
798    if {![info exists eventdata(why)] || $eventdata(why) ne "resize"} {
799        event generate $itk_component(hull) <<Layout>>
800    }
801}
802
803# ----------------------------------------------------------------------
804# USAGE: _fixNumResults
805#
806# Used internally to update the number of results displayed near the
807# top of this widget.  If there is only 1 result, then there is also
808# a single "Clear" button.  If there are no results, the clear button
809# is diabled.
810# ----------------------------------------------------------------------
811itcl::body Rappture::ResultSelector::_fixNumResults {} {
812    set size 0
813    if {$_resultset ne ""} {
814        set size [$_resultset size]
815    }
816
817    switch $size {
818        0 {
819            $itk_component(status) configure -text "No results"
820            $itk_component(clearall) configure -state disabled -text "Clear"
821            pack forget $itk_component(clear)
822        }
823        1 {
824            $itk_component(status) configure -text "1 result"
825            $itk_component(clearall) configure -state normal -text "Clear"
826            pack forget $itk_component(clear)
827        }
828        default {
829            $itk_component(status) configure -text "$size results"
830            $itk_component(clearall) configure -state normal -text "Clear All"
831            $itk_component(clear) configure -state normal
832            pack $itk_component(clear) -side right \
833                -after $itk_component(clearall) -padx {0 6}
834        }
835    }
836}
837
838# ----------------------------------------------------------------------
839# USAGE: _fixSettings ?<eventArgs...>?
840#
841# Called automatically at the idle point after a control has changed
842# to load new data into the plotting area at the top of this result
843# set.  Extracts the current tuple of control values from the control
844# area, then finds the corresponding data values.  Loads the data
845# by invoking a -settingscommand callback with parameters that
846# describe what data should be plotted.
847# ----------------------------------------------------------------------
848itcl::body Rappture::ResultSelector::_fixSettings {args} {
849    array set eventdata $args
850    if {[info exists eventdata(column)]} {
851        set changed $eventdata(column)
852    } else {
853        set changed ""
854    }
855
856    if {[info exists _cntlInfo($this-$_active-label)]} {
857        lappend params $_cntlInfo($this-$_active-label)
858    } else {
859        lappend params "???"
860    }
861    if {$_active == ""} {
862        return   ;# nothing active -- don't do anything
863    }
864    eval lappend params [_getValues $_active all]
865
866    if {$_resultset eq "" || [$_resultset size] == 0} {
867        # no data? then do nothing
868        return
869    } elseif {[$_resultset size] == 1} {
870        # only one data set? then plot it
871        set xmlobj [$_resultset find * *]
872        set simnum [lindex [$_resultset get simnum $xmlobj] 0]
873
874        _doSettings [list \
875            $simnum [list -width 2 \
876                    -param [_getValues $_active current] \
877                    -description [_getParamDesc all] \
878              ] \
879            params $params \
880        ]
881        return
882    }
883
884    #
885    # Find the selected run.  If the run setting changed, then
886    # look at its current value.  Otherwise, search the results
887    # for a tuple that matches the current settings.
888    #
889    if {$changed == "xmlobj" || $changed == "simnum"} {
890        set xmlobj [$_resultset find simnum $_cntlInfo($this-simnum-value)]
891    } else {
892        set format ""
893        set tuple ""
894        foreach col [lrange [$_resultset diff names] 2 end] {
895            lappend format $col
896            lappend tuple $_cntlInfo($this-$col-value)
897        }
898        set xmlobj [lindex [$_resultset find $format $tuple] 0]
899
900        if {$xmlobj eq "" && $changed ne ""} {
901            #
902            # No data for these settings.  Try leaving the next
903            # column open, then the next, and so forth, until
904            # we find some data.
905            #
906            # allcols:  foo bar baz qux
907            #               ^^^changed
908            #
909            # search:   baz qux foo
910            #
911            set val $_cntlInfo($this-$changed-value)
912            set allcols [lrange [$_resultset diff names] 2 end]
913            set i [lsearch -exact $allcols $changed]
914            set search [concat \
915                [lrange $allcols [expr {$i+1}] end] \
916                [lrange $allcols 0 [expr {$i-1}]] \
917            ]
918            set nsearch [llength $search]
919
920            for {set i 0} {$i < $nsearch} {incr i} {
921                set format $changed
922                set tuple [list $val]
923                for {set j [expr {$i+1}]} {$j < $nsearch} {incr j} {
924                    set col [lindex $search $j]
925                    lappend format $col
926                    lappend tuple $_cntlInfo($this-$col-value)
927                }
928                set xmlobj [lindex [$_resultset find $format $tuple] 0]
929                if {$xmlobj ne ""} {
930                    break
931                }
932            }
933        }
934    }
935
936    #
937    # If we found a particular run, then load its values into all
938    # controls.
939    #
940    if {$xmlobj ne ""} {
941        # stop reacting to value changes
942        set _settings 1
943
944        set format [lrange [$_resultset diff names] 2 end]
945        if {[llength $format] == 1} {
946            set data [list [$_resultset get $format $xmlobj]]
947        } else {
948            set data [$_resultset get $format $xmlobj]
949        }
950
951        foreach col $format val $data {
952            set _cntlInfo($this-$col-value) $val
953        }
954
955        set simnum [$_resultset get simnum $xmlobj]
956        set _cntlInfo($this-simnum-value) $simnum
957
958        # okay, react to value changes again
959        set _settings 0
960    }
961
962    #
963    # Search for tuples matching the current setting and
964    # plot them.
965    #
966    if {$_plotall && $_active eq "simnum"} {
967        set format ""
968    } else {
969        set format ""
970        set tuple ""
971        foreach col [lrange [$_resultset diff names] 2 end] {
972            if {!$_plotall || $col ne $_active} {
973                lappend format $col
974                lappend tuple $_cntlInfo($this-$col-value)
975            }
976        }
977    }
978
979    if {$format ne ""} {
980        set xolist [$_resultset find $format $tuple]
981    } else {
982        set xolist [$_resultset find * *]
983    }
984
985    if {[llength $xolist] > 0} {
986        # search for the result for these settings
987        set format ""
988        set tuple ""
989        foreach col [lrange [$_resultset diff names] 2 end] {
990            lappend format $col
991            lappend tuple $_cntlInfo($this-$col-value)
992        }
993        set curr [$_resultset find $format $tuple]
994
995        if {[llength $xolist] == 1} {
996            # single result -- always use active color
997            set xmlobj [lindex $xolist 0]
998            set simnum [$_resultset get simnum $xmlobj]
999            set plist [list \
1000                $simnum [list -width 2 \
1001                         -param [_getValues $_active $xmlobj] \
1002                         -description [_getParamDesc all $xmlobj] \
1003                   ] \
1004                params $params \
1005            ]
1006        } else {
1007            #
1008            # Get the color for all points according to
1009            # the color spectrum.
1010            #
1011            set plist [list params $params]
1012            foreach xmlobj $xolist {
1013                set simnum [$_resultset get simnum $xmlobj]
1014                if {$xmlobj eq $curr} {
1015                    lappend plist $simnum [list -width 3 -raise 1 \
1016                        -param [_getValues $_active $xmlobj] \
1017                        -description [_getParamDesc all $xmlobj]]
1018                } else {
1019                    lappend plist $simnum [list -brightness 0.7 -width 1 \
1020                        -param [_getValues $_active $xmlobj] \
1021                        -description [_getParamDesc all $xmlobj]]
1022                }
1023            }
1024        }
1025
1026        #
1027        # Load up the matching plots
1028        #
1029        _doSettings $plist
1030    }
1031}
1032
1033# ----------------------------------------------------------------------
1034# USAGE: _fixValue <columnName> <why>
1035#
1036# Called automatically whenver a value for a parameter dial changes.
1037# Updates the interface to display the new value.  The <why> is a
1038# reason for the change, which may be "resize" (draw old value in
1039# new size) or "value" (value changed).
1040# ----------------------------------------------------------------------
1041itcl::body Rappture::ResultSelector::_fixValue {col why} {
1042    if {[info exists _cntlInfo($this-$col-id)]} {
1043        set id $_cntlInfo($this-$col-id)
1044
1045        set widget $itk_component(dials).label$id
1046        set wmax [winfo width $itk_component(dials).dial]
1047        if {$wmax <= 1} {
1048            set wmax [expr {round(0.9*[winfo width $itk_component(cntls)])}]
1049        }
1050        _drawValue $col $widget $wmax
1051
1052        if {$why == "value" && !$_settings} {
1053            # keep track of usage, so we know which controls are popular
1054            incr _cntlInfo($this-$col-usage)
1055
1056            # adjust the settings according to the value in the column
1057            $_dispatcher event -idle !settings column $col
1058        }
1059    }
1060}
1061
1062# ----------------------------------------------------------------------
1063# USAGE: _drawValue <columnName> <widget> <widthMax>
1064#
1065# Used internally to fix the rendering of a "quantity = value" display.
1066# If the name/value in <widget> are smaller than <widthMax>, then the
1067# full "quantity = value" string is displayed.  Otherwise, an
1068# abbreviated form is displayed.
1069# ----------------------------------------------------------------------
1070itcl::body Rappture::ResultSelector::_drawValue {col widget wmax} {
1071    set quantity $_cntlInfo($this-$col-label)
1072    regsub -all {\n} $quantity " " quantity  ;# take out newlines
1073
1074    set newval $_cntlInfo($this-$col-value)
1075    regsub -all {\n} $newval " " newval  ;# take out newlines
1076
1077    set lfont [$widget.name cget -font]
1078    set vfont [$widget.value cget -font]
1079
1080    set wn [font measure $lfont $quantity]
1081    set wv [font measure $lfont " = $newval"]
1082    set w [expr {$wn + $wv}]
1083
1084    if {$w <= $wmax} {
1085        # if the text fits, then shown "quantity = value"
1086        $widget.name configure -text $quantity
1087        $widget.value configure -text " = $newval"
1088    } else {
1089        # Otherwise, we'll have to appreviate.
1090        # If the value is really long, then just show a little bit
1091        # of it.  Otherwise, show as much of the value as we can.
1092        if {[string length $newval] > 30} {
1093            set frac 0.8
1094        } else {
1095            set frac 0.2
1096        }
1097        set wNameSpace [expr {round($frac*$wmax)}]
1098        set wValueSpace [expr {$wmax-$wNameSpace}]
1099
1100        # fit as much of the "quantity" label in the space available
1101        if {$wn < $wNameSpace} {
1102            $widget.name configure -text $quantity
1103            set wValueSpace [expr {$wmax-$wn}]
1104        } else {
1105            set wDots [font measure $lfont "..."]
1106            set wchar [expr {double($wn)/[string length $quantity]}]
1107            while {1} {
1108                # figure out a good size for the abbreviated string
1109                set cmax [expr {round(($wNameSpace-$wDots)/$wchar)}]
1110                if {$cmax < 0} {set cmax 0}
1111                set str "[string range $quantity 0 $cmax]..."
1112                if {[font measure $lfont $str] <= $wNameSpace
1113                      || $wDots >= $wNameSpace} {
1114                    break
1115                }
1116                # we're measuring with average chars, so we may have
1117                # to shave a little off and do this again
1118                set wDots [expr {$wDots+2*$wchar}]
1119            }
1120            $widget.name configure -text $str
1121            set wValueSpace [expr {$wmax-[font measure $lfont $str]}]
1122        }
1123
1124        if {$wv < $wValueSpace} {
1125            $widget.value configure -text " = $newval"
1126        } else {
1127            set wDots [font measure $vfont "..."]
1128            set wEq [font measure $vfont " = "]
1129            set wchar [expr {double($wv)/[string length " = $newval"]}]
1130            while {1} {
1131                # figure out a good size for the abbreviated string
1132                set cmax [expr {round(($wValueSpace-$wDots-$wEq)/$wchar)}]
1133                if {$cmax < 0} {set cmax 0}
1134                set str " = [string range $newval 0 $cmax]..."
1135                if {[font measure $vfont $str] <= $wValueSpace
1136                      || $wDots >= $wValueSpace} {
1137                    break
1138                }
1139                # we're measuring with average chars, so we may have
1140                # to shave a little off and do this again
1141                set wDots [expr {$wDots+2*$wchar}]
1142            }
1143            $widget.value configure -text $str
1144        }
1145    }
1146}
1147
1148# ----------------------------------------------------------------------
1149# USAGE: _toggleAll ?<columnName>?
1150#
1151# Called automatically whenever the user clicks on an "All" button.
1152# Toggles the button between its on/off states.  In the "on" state,
1153# all results associated with the current control are sent to the
1154# result viewer.
1155# ----------------------------------------------------------------------
1156itcl::body Rappture::ResultSelector::_toggleAll {{col "current"}} {
1157    if {$col == "current"} {
1158        set col $_active
1159    }
1160    if {![info exists _cntlInfo($this-$col-id)]} {
1161        return
1162    }
1163    set id $_cntlInfo($this-$col-id)
1164    set sbutton $itk_component(dials).all
1165    set current [$sbutton cget -relief]
1166
1167    if {$current == "sunken"} {
1168        $sbutton configure -relief raised \
1169            -background $itk_option(-activecontrolbackground) \
1170            -foreground $itk_option(-activecontrolforeground)
1171        set _plotall 0
1172        Rappture::Logger::log result -all off
1173    } else {
1174        $sbutton configure -relief sunken \
1175            -background $itk_option(-togglebackground) \
1176            -foreground $itk_option(-toggleforeground)
1177        set _plotall 1
1178        Rappture::Logger::log result -all on
1179
1180        if {$col != $_active} {
1181            # clicked on an inactive "All" button? then activate that column
1182            activate $col
1183        }
1184    }
1185    $_dispatcher event -idle !settings
1186}
1187
1188# ----------------------------------------------------------------------
1189# USAGE: _getValues <column> ?<which>?
1190#
1191# Returns one or more value names associated with the given <column>.
1192# If the <which> parameter is "all", then it returns values for all
1193# results in the ResultSet.  Each value appears as two values in the
1194# flattened list: the normalized value and the value label.  If the
1195# <which> parameter is "current"
1196# ----------------------------------------------------------------------
1197itcl::body Rappture::ResultSelector::_getValues {col {which "all"}} {
1198    switch -- $which {
1199        current {
1200            set simnum $_cntlInfo($this-simnum-value)
1201            set xmlobj [$_resultset find simnum $simnum]
1202            if {$xmlobj ne ""} {
1203                return [$_resultset diff values $col $xmlobj]
1204            }
1205            return ""
1206        }
1207        all {
1208            return [$_resultset diff values $col all]
1209        }
1210        default {
1211            return [$_resultset diff values $col $which]
1212        }
1213    }
1214}
1215
1216# ----------------------------------------------------------------------
1217# USAGE: _getTooltip <role> <column>
1218#
1219# Called automatically whenever the user hovers on a control within
1220# this widget.  Returns the tooltip associated with the control.
1221# ----------------------------------------------------------------------
1222itcl::body Rappture::ResultSelector::_getTooltip {role column} {
1223    set label ""
1224    set tip ""
1225    if {$column eq "active"} {
1226        set column $_active
1227    }
1228    if {[info exists _cntlInfo($this-$column-label)]} {
1229        set label $_cntlInfo($this-$column-label)
1230    }
1231    if {[info exists _cntlInfo($this-$column-tip)]} {
1232        set tip $_cntlInfo($this-$column-tip)
1233    }
1234
1235    switch -- $role {
1236        label {
1237            if {$column ne $_active} {
1238                append tip "\n\nClick to activate this control."
1239            }
1240        }
1241        dial {
1242            append tip "\n\nClick to change the value of this parameter."
1243        }
1244        all {
1245            if {$label eq ""} {
1246                set tip "Plot all values for this quantity."
1247            } else {
1248                set tip "Plot all values for $label."
1249            }
1250            if {$_plotall} {
1251                set what "all values"
1252            } else {
1253                set what "one value"
1254            }
1255            append tip "\n\nCurrently, plotting $what.  Click to toggle."
1256        }
1257    }
1258    return [string trim $tip]
1259}
1260
1261# ----------------------------------------------------------------------
1262# USAGE: _getParamDesc <which> ?<xmlobj>?
1263#
1264# Used internally to build a descripton of parameters for the data
1265# tuple for the specified <xmlobj>.  This is passed on to the underlying
1266# results viewer, so it will know what data is being viewed.
1267# ----------------------------------------------------------------------
1268itcl::body Rappture::ResultSelector::_getParamDesc {which {xmlobj "current"}} {
1269    if {$_resultset eq ""} {
1270        return ""
1271    }
1272
1273    if {$xmlobj eq "current"} {
1274        # search for the result for these settings
1275        set format ""
1276        set tuple ""
1277        foreach col [lrange [$_resultset diff names] 2 end] {
1278            lappend format $col
1279            lappend tuple $_cntlInfo($this-$col-value)
1280        }
1281        set xmlobj [$_resultset find $format $tuple]
1282        if {$xmlobj eq ""} {
1283            return ""  ;# somethings wrong -- bail out!
1284        }
1285    }
1286
1287    switch -- $which {
1288        active {
1289            if {$_active eq ""} {
1290                return ""
1291            }
1292        }
1293        all {
1294            set desc ""
1295            foreach col [lrange [$_resultset diff names] 1 end] {
1296                set quantity $_cntlInfo($this-$col-label)
1297                set val [lindex [$_resultset get $col $xmlobj] 0]
1298                append desc "$quantity = $val\n"
1299            }
1300            return [string trim $desc]
1301        }
1302        default {
1303            error "bad value \"$which\": should be active or all"
1304        }
1305    }
1306}
1307
1308# ----------------------------------------------------------------------
1309# USAGE: _log <col>
1310#
1311# Used internally to log the event when a user switches to a different
1312# result in the result selector.
1313# ----------------------------------------------------------------------
1314itcl::body Rappture::ResultSelector::_log {col} {
1315    Rappture::Logger::log result -select $col $_cntlInfo($this-$col-value)
1316}
1317
1318# ----------------------------------------------------------------------
1319# OPTION: -resultset
1320# ----------------------------------------------------------------------
1321itcl::configbody Rappture::ResultSelector::resultset {
1322    set obj $itk_option(-resultset)
1323    if {$obj ne ""} {
1324        if {[catch {$obj isa ::Rappture::ResultSet} valid] || !$valid} {
1325            error "bad value \"$obj\": should be Rappture::ResultSet object"
1326        }
1327    }
1328
1329    # disconnect the existing ResultSet and install the new one
1330    if {$_resultset ne ""} {
1331        $_resultset notify remove $this
1332        _fixControls op clear
1333    }
1334    set _resultset $obj
1335
1336    if {$_resultset ne ""} {
1337        $_resultset notify add $this !change \
1338            [itcl::code $this _fixControls]
1339    }
1340
1341    _fixControls op add
1342    activate simnum
1343}
1344
1345# ----------------------------------------------------------------------
1346# OPTION: -activecontrolbackground
1347# ----------------------------------------------------------------------
1348itcl::configbody Rappture::ResultSelector::activecontrolbackground {
1349    $_dispatcher event -idle !layout
1350}
1351
1352# ----------------------------------------------------------------------
1353# OPTION: -activecontrolforeground
1354# ----------------------------------------------------------------------
1355itcl::configbody Rappture::ResultSelector::activecontrolforeground {
1356    $_dispatcher event -idle !layout
1357}
1358
Note: See TracBrowser for help on using the repository browser.