source: trunk/gui/scripts/coverflow.tcl @ 6235

Last change on this file since 6235 was 5659, checked in by ldelgass, 9 years ago

whitespace

File size: 21.5 KB
Line 
1# -*- mode: tcl; indent-tabs-mode: nil -*-
2# ----------------------------------------------------------------------
3#  COMPONENT: Coverflow - browse through a series of images
4#
5#  This widget works like the coverflow browser on iOS.  It presents
6#  a series of images with one in the center and others off to the
7#  sides.
8# ======================================================================
9#  AUTHOR:  Michael McLennan, Purdue University
10#  Copyright (c) 2004-2012  HUBzero Foundation, LLC
11#
12#  See the file "license.terms" for information on usage and
13#  redistribution of this file, and for a DISCLAIMER OF ALL WARRANTIES.
14# ======================================================================
15package require Itk
16package require BLT
17
18option add *Coverflow.width 5i widgetDefault
19option add *Coverflow.height 2i widgetDefault
20option add *Coverflow.background black widgetDefault
21option add *Coverflow.padding 20 widgetDefault
22option add *Coverflow.spacing 40 widgetDefault
23option add *Coverflow.font {Helvetica 10} widgetDefault
24option add *Coverflow.fontColor white widgetDefault
25
26itcl::class Rappture::Coverflow {
27    inherit itk::Widget
28
29    itk_option define -selectcommand selectCommand SelectCommand ""
30    itk_option define -padding padding Padding 0
31    itk_option define -spacing spacing Spacing 0
32    itk_option define -font font Font ""
33    itk_option define -fontcolor fontColor Foreground ""
34
35    constructor {args} { # defined below }
36    destructor { # defined below }
37
38    public method insert {where args}
39    public method itemconfigure {index args}
40    public method delete {args}
41    public method get {index args}
42    public method size {}
43    public method select {index args}
44
45    private method _itemForIndex {index}
46    private method _redraw {args}
47    private method _rescale {tid height}
48    private method _event {type x y}
49
50    private variable _dispatcher ""  ;# dispatcher for !events
51    private variable _tweener ""     ;# used for animations
52    private variable _items ""       ;# list of tag names for items
53    private variable _data           ;# data for coverflow items
54    private variable _counter 0      ;# for generating unique item IDs
55    private variable _current ""     ;# currently selected item
56    private variable _click ""       ;# click point from last _event call
57}
58
59itk::usual Rappture::Coverflow {
60    keep -background -cursor
61}
62
63# ----------------------------------------------------------------------
64# CONSTRUCTOR
65# ----------------------------------------------------------------------
66itcl::body Rappture::Coverflow::constructor {args} {
67    itk_option add hull.width hull.height
68    pack propagate $itk_component(hull) no
69
70    # create a dispatcher for events
71    Rappture::dispatcher _dispatcher
72    $_dispatcher register !redraw
73    $_dispatcher dispatch $this !redraw [itcl::code $this _redraw]
74
75    # create a tweener for animations
76    set _tweener [Rappture::Tweener ::#auto -duration 300 \
77        -command [list $_dispatcher event -idle !redraw -position %v] \
78        -finalize [list $_dispatcher event -idle !redraw]]
79
80    itk_component add area {
81        canvas $itk_interior.area
82    } {
83        keep -background -borderwidth -relief
84    }
85    pack $itk_component(area) -side left -expand yes -fill both
86
87    bind $itk_component(area) <Configure> \
88        [list $_dispatcher event -idle !redraw]
89
90    bind $itk_component(area) <ButtonPress> \
91        [itcl::code $this _event click %x %y]
92    bind $itk_component(area) <B1-Motion> \
93        [itcl::code $this _event drag %x %y]
94    bind $itk_component(area) <ButtonRelease> \
95        [itcl::code $this _event release %x %y]
96
97    eval itk_initialize $args
98}
99
100# ----------------------------------------------------------------------
101# DESTRUCTOR
102# ----------------------------------------------------------------------
103itcl::body Rappture::Coverflow::destructor {} {
104    itcl::delete object $_tweener
105}
106
107# ----------------------------------------------------------------------
108# USAGE: insert <index> <image> ?-option <value>...?
109#
110# Inserts a new image into the coverflow at the position <index>, which
111# is an integer starting from 0 at the left, or the keyword "end" for
112# the right.  The remaining arguments are configuration options:
113#
114#   -tag ....... unique tag name for this item
115#   -text ...... text displayed beneath the image when selected
116#   -data ...... data sent to the callback command
117# ----------------------------------------------------------------------
118itcl::body Rappture::Coverflow::insert {where imh args} {
119    array set params $args
120    if {[info exists params(-tag)]} {
121        set tid $params(-tag)
122        unset params(-tag)
123    } else {
124        set tid "item[incr _counter]"
125    }
126
127    if {[info exists _data($tid-text)]} {
128        error "item \"$tid\" already exists -- tags must be unique"
129    }
130    set _items [linsert $_items $where $tid]
131    set _data($tid-image) $imh
132    set _data($tid-scaled) [image create photo]
133    set _data($tid-lastupdate) ""
134    set _data($tid-text) ""
135    set _data($tid-data) ""
136
137    eval itemconfigure $tid [array get params]
138
139    if {$_current eq ""} {
140        select $tid
141    }
142
143    # return the tag name for this new item
144    return $tid
145}
146
147# ----------------------------------------------------------------------
148# USAGE: itemconfigure <indexOrTag> -option <value> ...
149#
150# Used to modify an existing item.  The <indexOrTag> can be an integer
151# index for the item or a -tag name given when the item was created.
152# The remaining -option/value pairs are used to modify the values
153# associated with the item.
154# ----------------------------------------------------------------------
155itcl::body Rappture::Coverflow::itemconfigure {index args} {
156    set n [_itemForIndex $index]
157    if {$n eq ""} {
158        error "bad index \"$index\": should be integer or -tag name"
159    }
160    set tid [lindex $_items $n]
161
162    # store the new data and then redraw
163    foreach {key val} $args {
164        if {[info exists _data($tid$key)]} {
165            set _data($tid$key) $val
166
167            # make sure we redraw the flow at some point
168            $_dispatcher event -idle !redraw
169        } else {
170            error "bad option \"$key\": should be -text, -data"
171        }
172    }
173}
174
175# ----------------------------------------------------------------------
176# USAGE: delete ?<indexOrTag> <indexOrTag>...?
177# USAGE: delete all
178#
179# Deletes one or more entries from this listbox.  With the "all"
180# keyword, it deletes all entries in the listbox.  Otherwise, it
181# deletes one or more entries as specified.
182# ----------------------------------------------------------------------
183itcl::body Rappture::Coverflow::delete {args} {
184    if {$args eq "all"} {
185        set args $_items
186    }
187    foreach index $args {
188        set n [_itemForIndex $index]
189        if {$n ne ""} {
190            set tid [lindex $_items $n]
191            set _items [lreplace $_items $n $n]
192
193            image delete $_data($tid-scaled)
194            unset _data($tid-image)
195            unset _data($tid-scaled)
196            unset _data($tid-lastupdate)
197            unset _data($tid-text)
198            unset _data($tid-data)
199
200            # if this item was selected, then clear that
201            if {$tid eq $_current} {
202                if {$n < [llength $_items]} {
203                    set _current [lindex $_items $n]
204                } else {
205                    set _current [lindex $_items end]
206                }
207            }
208
209            # make sure we redraw the list at some point
210            $_dispatcher event -idle !redraw
211        }
212    }
213}
214
215# ----------------------------------------------------------------------
216# USAGE: get <indexOrTag> ?-option -option...?
217#
218# Used to query information about an item.  With no extra args, it
219# returns the tag with an entry, or "" if the entry is not recognized.
220# If additional -option values are specified, then data values are
221# returned for the requested options.
222# ----------------------------------------------------------------------
223itcl::body Rappture::Coverflow::get {index args} {
224    set n [_itemForIndex $index]
225    if {$n eq ""} {
226        return ""
227    }
228    if {[llength $args] == 0} {
229        return [lindex $_items $n]
230    }
231
232    set vlist ""
233    foreach option $args {
234        if {[info exists _data($tid$option)]} {
235            lappend vlist $_data($tid$option)
236        } else {
237            error "invalid option \"$option\""
238        }
239    }
240    return $vlist
241}
242
243# ----------------------------------------------------------------------
244# USAGE: size
245#
246# Returns the number of items in the listbox.
247# ----------------------------------------------------------------------
248itcl::body Rappture::Coverflow::size {} {
249    return [llength $_items]
250}
251
252# ----------------------------------------------------------------------
253# USAGE: select <indexOrTag> ?-animate on|off?
254#
255# Makes the given item the "current" item in the coverflow.  This is
256# the item that is shown front-and-center.  If this was not already
257# the current item, then the -selectcommand is invoked with information
258# related to the selected item.
259# ----------------------------------------------------------------------
260itcl::body Rappture::Coverflow::select {index args} {
261    set n [_itemForIndex $index]
262    if {$n eq ""} {
263        error "bad index \"$index\": should be integer or -tag name"
264    }
265    set tid [lindex $_items $n]
266    if {$tid eq ""} {
267        error "bad index \"$index\": index out of range"
268    }
269
270    set animate on
271    array set params $args
272    if {[info exists params(-animate)]} {
273        set animate $params(-animate)
274    }
275
276    if {$_current ne $tid} {
277        set from [lsearch -exact $_items $_current]
278        set _current $tid
279
280        # invoke the callback to react to this selection
281        if {$itk_option(-selectcommand) ne ""} {
282            set arglist $_data($tid-image)
283            foreach opt {-text -data} {
284                lappend arglist $opt $_data($tid$opt)
285            }
286            uplevel #0 $itk_option(-selectcommand) $arglist
287        }
288
289        # animate the motion and redraw the current choice
290        if {$from >= 0 && $animate} {
291            $_tweener configure -from $from -to $n
292            $_tweener go -restart
293        } else {
294            $_dispatcher event -idle !redraw
295        }
296    }
297}
298
299# ----------------------------------------------------------------------
300# USAGE: _itemForIndex <indexOrTag>
301#
302# Converts the given <indexOrTag> to a valid entry name.  If the
303# <indexOrTag> is an integer, then it is interpreted as an index
304# in the range 0 to the size of the list.  Otherwise, it is treated
305# as a -tag name that may have been used when creating the item.
306#
307# Returns an integer index or "" if the item is not recognized.
308# ----------------------------------------------------------------------
309itcl::body Rappture::Coverflow::_itemForIndex {index} {
310    if {[string is integer -strict $index]} {
311        return $index
312    }
313    if {$index eq "end"} {
314        return [llength $_items]
315    }
316    set n [lsearch -exact $_items $index]
317    if {$n >= 0} {
318        return $n
319    }
320    return ""
321}
322
323# ----------------------------------------------------------------------
324# USAGE: _redraw ?-position <float>?
325#
326# Used internally to redraw all items in the list after it has changed.
327# With no args, it draws the current state with the selected image
328# shown in the middle.  The -position specifies an index, but perhaps
329# with a fractional part.  An index like 2.3 is drawn with the center
330# between images 2 and 3 -- about 1/3 of the way through the animation.
331# ----------------------------------------------------------------------
332itcl::body Rappture::Coverflow::_redraw {args} {
333    set c $itk_component(area)
334    set w [winfo width $c]
335    set h [winfo height $c]
336    $c delete all
337
338    if {$w == 1 && $h == 1} {
339        # just starting up -- forget it
340        return
341    }
342
343    # get the desired position from the args or the current position
344    set pos [lsearch $_items $_current]
345
346    array set params $args
347    if {[info exists params(-position)]} {
348        set pos $params(-position)
349    }
350
351    if {$pos < 0} { set pos 0 }
352    if {$pos >= [llength $_items]-1} { set pos [expr {[llength $_items]-1}] }
353
354    # does any image have text?  if so, make room for it along the bottom
355    set textht 0
356    foreach tid $_items {
357        if {$_data($tid-text) ne ""} {
358            set textht [font metrics $itk_option(-font) -linespace]
359            break
360        }
361    }
362
363    # figure out the height of the covers and their midpoint
364    set ih [expr {$h-2*$itk_option(-padding)-$textht}]
365    set y0 [expr {($h-$textht)/2}]
366
367    #
368    # If the position is an integer, then that image is centered
369    # in the current view.  Otherwise, we're between two images.
370    #
371    set xmid [expr {$w/2}]
372    set spacing $itk_option(-spacing)
373    set shadow 0.8
374    set shorten 0.8
375    set len 0.3
376
377    if {abs($pos-round($pos)) < 0.05} {
378        # one image right in the middle
379        set lpos [expr {round($pos)}]
380        set rpos $lpos
381
382        # find the x-coord for the left/right sides of that image
383        set tid [lindex $_items $lpos]
384        set origw [image width $_data($tid-image)]
385        set origh [image height $_data($tid-image)]
386        set iw [expr {$ih*$origw/$origh}]
387        set lx [expr {$xmid-$iw/2}]
388        set rx [expr {$xmid+$iw/2}]
389    } else {
390        # between images -- pos and pos+1
391        set lpos [expr {round(floor($pos))}]
392        set rpos [expr {$lpos+1}]
393
394        set llen [expr {$len + (1-$len)*($rpos-$pos)}]
395        set lside [expr {$shorten + (1-$shorten)*($rpos-$pos)}]
396        set lshadow [expr {$shadow*($pos-$lpos)}]
397        set rlen [expr {$len + (1-$len)*($pos-$lpos)}]
398        set rside [expr {$shorten + (1-$shorten)*($pos-$lpos)}]
399        set rshadow [expr {$shadow*($rpos-$pos)}]
400
401        # find the x-coord for the left/right sides of the two images
402        set tid [lindex $_items $lpos]
403        set origw [image width $_data($tid-image)]
404        set origh [image height $_data($tid-image)]
405        set iw0 [expr {$ih*$origw/$origh}]
406        set lx0 [expr {$xmid-$iw0/2}]
407        set rx0 [expr {$xmid+$iw0/2}]
408
409        set tid [lindex $_items $rpos]
410        set origw [image width $_data($tid-image)]
411        set origh [image height $_data($tid-image)]
412        set iw1 [expr {$ih*$origw/$origh}]
413        set lx1 [expr {$xmid-$iw1/2}]
414        set rx1 [expr {$xmid+$iw1/2}]
415
416        set lx [expr {($lx1-$lx0+$spacing)*($rpos-$pos) + $lx0-$spacing}]
417        set rx [expr {($rx1-$rx0-$spacing)*($pos-$lpos) + $rx0+$spacing}]
418    }
419
420    # scan through all images and update their scaling
421    # use -lastupdate to avoid scaling if we're in the same state
422    for {set i 0} {$i < [llength $_items]} {incr i} {
423        set tid [lindex $_items $i]
424        if {$i < $lpos} {
425            set state "left $ih"
426            if {$_data($tid-lastupdate) ne $state} {
427                _rescale $tid $ih
428                squeezer -side right -shadow $shadow -shorten $len \
429                    $_data($tid-scaled) $_data($tid-scaled)
430                set _data($tid-lastupdate) $state
431            }
432        } elseif {$i > $rpos} {
433            set state "right $ih"
434            if {$_data($tid-lastupdate) ne $state} {
435                _rescale $tid $ih
436                squeezer -side left -shadow $shadow -shorten $len \
437                    $_data($tid-scaled) $_data($tid-scaled)
438                set _data($tid-lastupdate) $state
439            }
440        } elseif {$i == $lpos && $i == $rpos} {
441            set state "center $ih"
442            if {$_data($tid-lastupdate) ne $state} {
443                _rescale $tid $ih
444                set _data($tid-lastupdate) $state
445            }
446        } elseif {$i == $lpos} {
447            set state "left $ih ~ $llen"
448            if {$_data($tid-lastupdate) ne $state} {
449                _rescale $tid $ih
450                squeezer -side right -shadow $lshadow -shorten $llen \
451                    -amount $lside $_data($tid-scaled) $_data($tid-scaled)
452                set _data($tid-lastupdate) $state
453            }
454        } elseif {$i == $rpos} {
455            set state "right $ih ~ $rlen"
456            if {$_data($tid-lastupdate) ne $state} {
457                _rescale $tid $ih
458                squeezer -side left -shadow $rshadow -shorten $rlen \
459                    -amount $rside $_data($tid-scaled) $_data($tid-scaled)
460                set _data($tid-lastupdate) $state
461            }
462        }
463    }
464
465    #
466    # Figure out the positions of the various images to the left,
467    # to the right, and in the middle.  Build up a list of items
468    # to draw.
469    #
470    set tiles ""
471
472    # start at left and lay down tiles toward the middle
473    set numOnLeft [expr {round(floor($lx/$spacing))+1}]
474    set i [expr {$lpos-$numOnLeft}]
475    if {$i < 0} { set i 0 }
476    set x0 [expr {$lx - ($lpos-$i)*$spacing}]
477    while {$i < $lpos} {
478        lappend tiles $i $x0 w
479        set x0 [expr {$x0+$spacing}]
480        incr i
481    }
482
483    # start at right and lay down tiles toward the middle
484    set numOnRight [expr {round(floor(($w-$rx)/$spacing))+1}]
485    set i [expr {$rpos+$numOnRight}]
486    if {$i >= [llength $_items]} { set i [expr {[llength $_items]-1}] }
487    set x0 [expr {$rx + ($i-$rpos)*$spacing}]
488    while {$i > $rpos} {
489        lappend tiles $i $x0 e
490        set x0 [expr {$x0-$spacing}]
491        incr i -1
492    }
493
494    # lay down the center image or images
495    if {$lpos == $rpos} {
496        # just one image in the middle
497        lappend tiles $lpos $xmid c
498    } else {
499        # two tiles in a transition
500        if {$llen > $rlen} {
501            lappend tiles $rpos $rx e $lpos $lx w
502        } else {
503            lappend tiles $lpos $lx w $rpos $rx e
504        }
505    }
506
507    # run through the list and draw all items
508    foreach {i x anchor} $tiles {
509        set tid [lindex $_items $i]
510        $c create image $x $y0 -anchor $anchor \
511            -image $_data($tid-scaled) -tags "item$i"
512    }
513
514    # if there is a single center image, show its text
515    if {$lpos == $rpos} {
516        set tid [lindex $_items $lpos]
517        if {$_data($tid-text) ne ""} {
518            $c create text $xmid [expr {$h-$itk_option(-padding)/2}] \
519                -anchor s -text $_data($tid-text) \
520                -font $itk_option(-font) -fill $itk_option(-fontcolor)
521        }
522    }
523}
524
525# ----------------------------------------------------------------------
526# USAGE: _rescale <tid> <height>
527#
528# Used internally to rescale a single image (associated with item ID
529# tid) to the specified <height>.
530# ----------------------------------------------------------------------
531itcl::body Rappture::Coverflow::_rescale {tid ht} {
532    set iw [image width $_data($tid-image)]
533    set ih [image height $_data($tid-image)]
534    set wd [expr {$ht*$iw/$ih}]
535    $_data($tid-scaled) configure -width $wd -height $ht
536    blt::winop resample $_data($tid-image) $_data($tid-scaled) box
537}
538
539# ----------------------------------------------------------------------
540# USAGE: _event <type> <x> <y>
541#
542# Called whenever the user clicks or interacts with the coverflow.
543# The event <type> is something like "click" or "drag".
544# ----------------------------------------------------------------------
545itcl::body Rappture::Coverflow::_event {type x y} {
546    set c $itk_component(area)
547    switch -- $type {
548        click {
549            set _click [list $x $y]
550        }
551        drag {
552            if {$_click ne "" && $_current ne ""} {
553                foreach {x0 y0} $_click break
554                set delta [expr {double($x-$x0)/$itk_option(-spacing)}]
555                set pos [lsearch -exact $_items $_current]
556                set newpos [expr {$pos+$delta}]
557                $_dispatcher event -idle !redraw -position $newpos
558
559                if {![regexp {dragged} $_click]} {
560                    lappend _click dragged
561                }
562            }
563        }
564        release {
565            if {$_click ne ""} {
566                foreach {x0 y0} $_click break
567                if {abs($x-$x0) > 3 || abs($y-$y0) > 3} {
568                    # one last drag event
569                    _event drag $x $y
570                }
571
572                if {[regexp {dragged} $_click]} {
573                    # adjust the current selection to the current pos
574                    set delta [expr {double($x-$x0)/$itk_option(-spacing)}]
575                    set pos [lsearch -exact $_items $_current]
576                    set newpos [expr {round($pos+$delta)}]
577                    if {$newpos < 0} {set newpos 0}
578                    set max [expr {[llength $_items]-1}]
579                    if {$newpos > $max} {set newpos $max}
580
581                    select $newpos -animate off
582
583                    # select may not change, so do one last proper draw
584                    $_dispatcher event -idle !redraw -position $newpos
585
586                    set _click ""
587                    return
588                }
589            }
590
591            # click in place -- see what image we clicked on
592            set found ""
593            foreach id [$c find overlapping $x $y $x $y] {
594                if {[regexp {item([0-9]+)} [$c gettags $id] match index]} {
595                    set found $index
596                }
597                # keep searching for top-most item in stack
598            }
599            if {$found ne ""} {
600                select $found
601            }
602            set _click ""
603        }
604        default {
605            error "bad option \"$type\": should be click, drag, release"
606        }
607    }
608}
609
610# ----------------------------------------------------------------------
611# CONFIGURATION OPTIONS
612# ----------------------------------------------------------------------
613itcl::configbody Rappture::Coverflow::padding {
614    $_dispatcher event -idle !redraw
615}
616itcl::configbody Rappture::Coverflow::spacing {
617    $_dispatcher event -idle !redraw
618}
619itcl::configbody Rappture::Coverflow::font {
620    $_dispatcher event -idle !redraw
621}
622itcl::configbody Rappture::Coverflow::fontcolor {
623    $_dispatcher event -idle !redraw
624}
Note: See TracBrowser for help on using the repository browser.