#
# Scrolledlistbox
# ----------------------------------------------------------------------
# Implements a scrolled listbox with additional options to manage
# horizontal and vertical scrollbars.  This includes options to control
# which scrollbars are displayed and the method, i.e. statically,
# dynamically, or none at all.  
#
# ----------------------------------------------------------------------
#  AUTHOR: Mark L. Ulferts             EMAIL: mulferts@austin.dsccc.com
#
#  @(#) $Id: scrolledlistbox.itk,v 1.9 2002/03/16 16:25:44 mgbacke Exp $
# ----------------------------------------------------------------------
#            Copyright (c) 1995 DSC Technologies Corporation
# ======================================================================
# Permission to use, copy, modify, distribute and license this software 
# and its documentation for any purpose, and without fee or written 
# agreement with DSC, is hereby granted, provided that the above copyright 
# notice appears in all copies and that both the copyright notice and 
# warranty disclaimer below appear in supporting documentation, and that 
# the names of DSC Technologies Corporation or DSC Communications 
# Corporation not be used in advertising or publicity pertaining to the 
# software without specific, written prior permission.
# 
# DSC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING 
# ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, AND NON-
# INFRINGEMENT. THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, AND THE
# AUTHORS AND DISTRIBUTORS HAVE NO OBLIGATION TO PROVIDE MAINTENANCE, 
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. IN NO EVENT SHALL 
# DSC BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR 
# ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, 
# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTUOUS ACTION,
# ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS 
# SOFTWARE.
# ======================================================================

#
# Usual options.
#
itk::usual Scrolledlistbox {
    keep -activebackground -activerelief -background -borderwidth -cursor \
     -elementborderwidth -foreground -highlightcolor -highlightthickness \
     -jump -labelfont -selectbackground -selectborderwidth \
     -selectforeground -textbackground -textfont -troughcolor 
}

# ------------------------------------------------------------------
#                          SCROLLEDLISTBOX
# ------------------------------------------------------------------
itcl::class iwidgets::Scrolledlistbox {
    inherit iwidgets::Scrolledwidget

    constructor {args} {}
    destructor {}

    itk_option define -dblclickcommand dblClickCommand Command {}
    itk_option define -selectioncommand selectionCommand Command {}
    itk_option define -width width Width 0
    itk_option define -height height Height 0
    itk_option define -visibleitems visibleItems VisibleItems 20x10
    itk_option define -state state State normal

    public method curselection {} 
    public method activate {index} 
    public method bbox {index} 
    public method clear {} 
    public method see {index} 
    public method index {index} 
    public method delete {first {last {}}} 
    public method get {first {last {}}} 
    public method getcurselection {} 
    public method insert {index args} 
    public method nearest {y} 
    public method scan {option args} 
    public method selection {option first {last {}}} 
    public method size {} 
    public method selecteditemcount {} 
    public method justify {direction} 
    public method sort {{mode ascending}} 
    public method xview {args} 
    public method yview {args} 
    public method itemconfigure {args}

    protected method _makeSelection {} 
    protected method _dblclick {} 
    protected method _fixIndex {index}

    #
    # List the event sequences that invoke single and double selection.
    # Should these change in the underlying Tk listbox, then they must
    # change here too.
    #
    common doubleSelectSeq { \
    <Double-1> 
    }

    common singleSelectSeq { \
        <Control-Key-backslash> \
    <Control-Key-slash> \
    <Key-Escape> \
    <Shift-Key-Select> \
    <Control-Shift-Key-space> \
    <Key-Select> \
    <Key-space> \
    <Control-Shift-Key-End> \
    <Control-Key-End> \
    <Control-Shift-Key-Home> \
    <Control-Key-Home> \
    <Key-Down> \
    <Key-Up> \
    <Shift-Key-Down> \
    <Shift-Key-Up> \
    <Control-Button-1> \
    <Shift-Button-1> \
    <ButtonRelease-1> \
    }
}

#
# Provide a lowercased access method for the Scrolledlistbox class.
# 
proc ::iwidgets::scrolledlistbox {pathName args} {
    uplevel ::iwidgets::Scrolledlistbox $pathName $args
}

#
# Use option database to override default resources of base classes.
#
option add *Scrolledlistbox.labelPos n widgetDefault

# ------------------------------------------------------------------
#                        CONSTRUCTOR
# ------------------------------------------------------------------
itcl::body iwidgets::Scrolledlistbox::constructor {args} {
    #
    # Our -width and -height options are slightly different than
    # those implemented by our base class, so we're going to
    # remove them and redefine our own.
    #
    itk_option remove iwidgets::Scrolledwidget::width
    itk_option remove iwidgets::Scrolledwidget::height

    # 
    # Create the listbox.
    #
    itk_component add listbox {
    listbox $itk_interior.listbox \
        -width 1 -height 1 \
            -xscrollcommand \
        [itcl::code $this _scrollWidget $itk_interior.horizsb] \
        -yscrollcommand \
        [itcl::code $this _scrollWidget $itk_interior.vertsb]
    } {
    usual

    keep -borderwidth -exportselection -relief -selectmode
    keep -listvariable
    
    rename -font -textfont textFont Font
    rename -background -textbackground textBackground Background
    rename -highlightbackground -background background Background
    }
    grid $itk_component(listbox) -row 0 -column 0 -sticky nsew
    grid rowconfigure $_interior 0 -weight 1
    grid columnconfigure $_interior 0 -weight 1
    
    # 
    # Configure the command on the vertical scroll bar in the base class.
    #
    $itk_component(vertsb) configure \
    -command [itcl::code $itk_component(listbox) yview]

    #
    # Configure the command on the horizontal scroll bar in the base class.
    #
    $itk_component(horizsb) configure \
        -command [itcl::code $itk_component(listbox) xview]
    
    # 
    # Create a set of bindings for monitoring the selection and install
    # them on the listbox component.
    #
    foreach seq $singleSelectSeq {
        bind SLBSelect$this $seq [itcl::code $this _makeSelection]
    }

    foreach seq $doubleSelectSeq {
        bind SLBSelect$this $seq [itcl::code $this _dblclick]
    }

    bindtags $itk_component(listbox) \
            [linsert [bindtags $itk_component(listbox)] end SLBSelect$this]

    #
    # Also create a set of bindings for disabling the scrolledlistbox.
    # Since the command for it is "break", we can drop the $this since
    # they don't need to be unique to the object level.
    #
    if {[bind SLBDisabled] == {}} {
        foreach seq $singleSelectSeq {
            bind SLBDisabled $seq break
        }

        bind SLBDisabled <Button-1> break

        foreach seq $doubleSelectSeq {
            bind SLBDisabled $seq break
        }
    }

    #
    # Initialize the widget based on the command line options.
    #
    eval itk_initialize $args
}

# ------------------------------------------------------------------
#                           DESTURCTOR
# ------------------------------------------------------------------
itcl::body iwidgets::Scrolledlistbox::destructor {} {
}

# ------------------------------------------------------------------
#                             OPTIONS
# ------------------------------------------------------------------

# ------------------------------------------------------------------
# OPTION: -dblclickcommand
#
# Specify a command to be executed upon double click of a listbox 
# item.  Also, create a couple of bindings used for specific
# selection modes
# ------------------------------------------------------------------
itcl::configbody iwidgets::Scrolledlistbox::dblclickcommand {}

# ------------------------------------------------------------------
# OPTION: -selectioncommand
#
# Specifies a command to be executed upon selection of a listbox 
# item.  The command will be called upon each selection regardless 
# of selection mode..
# ------------------------------------------------------------------
itcl::configbody iwidgets::Scrolledlistbox::selectioncommand {}

# ------------------------------------------------------------------
# OPTION: -width
#
# Specifies the width of the scrolled list box as an entire unit.
# The value may be specified in any of the forms acceptable to 
# Tk_GetPixels.  Any additional space needed to display the other
# components such as margins and scrollbars force the listbox
# to be compressed.  A value of zero along with the same value for 
# the height causes the value given for the visibleitems option 
# to be applied which administers geometry constraints in a different
# manner.
# ------------------------------------------------------------------
itcl::configbody iwidgets::Scrolledlistbox::width {
    if {$itk_option(-width) != 0} {
        set shell [lindex [grid info $itk_component(listbox)] 1]

        #
        # Due to a bug in the tk4.2 grid, we have to check the 
        # propagation before setting it.  Setting it to the same
        # value it already is will cause it to toggle.
        #
        if {[grid propagate $shell]} {
            grid propagate $shell no
        }
    
        $itk_component(listbox) configure -width 1
        $shell configure \
                -width [winfo pixels $shell $itk_option(-width)] 
    } else {
        configure -visibleitems $itk_option(-visibleitems)
    }
}

# ------------------------------------------------------------------
# OPTION: -height
#
# Specifies the height of the scrolled list box as an entire unit.
# The value may be specified in any of the forms acceptable to 
# Tk_GetPixels.  Any additional space needed to display the other
# components such as margins and scrollbars force the listbox
# to be compressed.  A value of zero along with the same value for 
# the width causes the value given for the visibleitems option 
# to be applied which administers geometry constraints in a different
# manner.
# ------------------------------------------------------------------
itcl::configbody iwidgets::Scrolledlistbox::height {
    if {$itk_option(-height) != 0} {
        set shell [lindex [grid info $itk_component(listbox)] 1]

        #
        # Due to a bug in the tk4.2 grid, we have to check the 
        # propagation before setting it.  Setting it to the same
        # value it already is will cause it to toggle.
        #
        if {[grid propagate $shell]} {
            grid propagate $shell no
        }
    
        $itk_component(listbox) configure -height 1
        $shell configure \
                -height [winfo pixels $shell $itk_option(-height)] 
    } else {
        configure -visibleitems $itk_option(-visibleitems)
    }
}

# ------------------------------------------------------------------
# OPTION: -visibleitems
#
# Specified the widthxheight in characters and lines for the listbox.
# This option is only administered if the width and height options
# are both set to zero, otherwise they take precedence.  With the
# visibleitems option engaged, geometry constraints are maintained
# only on the listbox.  The size of the other components such as 
# labels, margins, and scrollbars, are additive and independent, 
# effecting the overall size of the scrolled list box.  In contrast,
# should the width and height options have non zero values, they
# are applied to the scrolled list box as a whole.  The listbox 
# is compressed or expanded to maintain the geometry constraints.
# ------------------------------------------------------------------
itcl::configbody iwidgets::Scrolledlistbox::visibleitems {
    if {[regexp {^[0-9]+x[0-9]+$} $itk_option(-visibleitems)]} {
        if {($itk_option(-width) == 0) && \
                ($itk_option(-height) == 0)} {
            set chars [lindex [split $itk_option(-visibleitems) x] 0]
            set lines [lindex [split $itk_option(-visibleitems) x] 1]
        
            set shell [lindex [grid info $itk_component(listbox)] 1]

            #
            # Due to a bug in the tk4.2 grid, we have to check the 
            # propagation before setting it.  Setting it to the same
            # value it already is will cause it to toggle.
            #
            if {! [grid propagate $shell]} {
                grid propagate $shell yes
            }
        
            $itk_component(listbox) configure -width $chars -height $lines
        }
    
    } else {
        error "bad visibleitems option\
                \"$itk_option(-visibleitems)\": should be\
        widthxheight"
    }
}

# ------------------------------------------------------------------
# OPTION: -state
#
# Specifies the state of the scrolledlistbox which may be either
# disabled or normal.  In a disabled state, the scrolledlistbox 
# does not accept user selection.  The default is normal.
# ------------------------------------------------------------------
itcl::configbody iwidgets::Scrolledlistbox::state {
    set tags [bindtags $itk_component(listbox)]

    #
    # If the state is normal, then we need to remove the disabled 
    # bindings if they exist.  If the state is disabled, then we need
    # to install the disabled bindings if they haven't been already.
    #
    switch -- $itk_option(-state) {
        normal {
                $itk_component(listbox) configure \
                    -foreground $itk_option(-foreground)
                $itk_component(listbox) configure \
                    -selectforeground $itk_option(-selectforeground)
            if {[set index [lsearch $tags SLBDisabled]] != -1} {
                bindtags $itk_component(listbox) \
                        [lreplace $tags $index $index]
            }
        }

        disabled {
                $itk_component(listbox) configure \
                    -foreground $itk_option(-disabledforeground)
                $itk_component(listbox) configure \
                    -selectforeground $itk_option(-disabledforeground)
            if {[set index [lsearch $tags SLBDisabled]] == -1} {
                bindtags $itk_component(listbox) \
                        [linsert $tags 1 SLBDisabled]
            }
        }
        default {
            error "bad state value \"$itk_option(-state)\":\
                    must be normal or disabled"
        }
    }
}

# ------------------------------------------------------------------
#                            METHODS
# ------------------------------------------------------------------

# ------------------------------------------------------------------
# METHOD: curselection 
#
# Returns a list containing the indices of all the elements in the 
# listbox that are currently selected.
# ------------------------------------------------------------------
itcl::body iwidgets::Scrolledlistbox::curselection {} {
    return [$itk_component(listbox) curselection]
}

# ------------------------------------------------------------------
# METHOD: activate index
#
# Sets the active element to the one indicated by index.
# ------------------------------------------------------------------
itcl::body iwidgets::Scrolledlistbox::activate {index} {
    return [$itk_component(listbox) activate [_fixIndex $index]]
}

# ------------------------------------------------------------------
# METHOD: bbox index
#
# Returns four element list describing the bounding box for the list
# item at index
# ------------------------------------------------------------------
itcl::body iwidgets::Scrolledlistbox::bbox {index} {
    return [$itk_component(listbox) bbox [_fixIndex $index]]
}

# ------------------------------------------------------------------
# METHOD clear 
#
# Clear the listbox area of all items.
# ------------------------------------------------------------------
itcl::body iwidgets::Scrolledlistbox::clear {} {
    delete 0 end
}

# ------------------------------------------------------------------
# METHOD: see index
#
# Adjusts the view such that the element given by index is visible.
# ------------------------------------------------------------------
itcl::body iwidgets::Scrolledlistbox::see {index} {
    $itk_component(listbox) see [_fixIndex $index]
}

# ------------------------------------------------------------------
# METHOD: index index
#
# Returns the decimal string giving the integer index corresponding 
# to index.  The index value may be a integer number, active,
# anchor, end, @x,y, or a pattern.
# ------------------------------------------------------------------
itcl::body iwidgets::Scrolledlistbox::index {index} {
    if {[regexp {(^[0-9]+$)|(^active$)|(^anchor$)|(^end$)|(^@-?[0-9]+,-?[0-9]+$)} $index]} {
    return [$itk_component(listbox) index $index]
    
    } else {
        set indexValue [lsearch -glob [get 0 end] $index]
        if {$indexValue == -1} {
            error "bad Scrolledlistbox index \"$index\": must be active,\
                    anchor, end, @x,y, number, or a pattern"
        }
        return $indexValue
    }
}

# ------------------------------------------------------------------
# METHOD: _fixIndex index
#
# Similar to the regular "index" method, but it only converts
# the index to a numerical value if it is a string pattern.  If
# the index is in the proper form to be used with the listbox,
# it is left alone.  This fixes problems associated with converting
# an index such as "end" to a numerical value.
# ------------------------------------------------------------------
itcl::body iwidgets::Scrolledlistbox::_fixIndex {index} {
    if {[regexp {(^[0-9]+$)|(^active$)|(^anchor$)|(^end$)|(^@[0-9]+,[0-9]+$)} \
            $index]} {
        return $index

    } else {
        set indexValue [lsearch -glob [get 0 end] $index]

        if {$indexValue == -1} {
            error "bad Scrolledlistbox index \"$index\": must be active,\
                    anchor, end, @x,y, number, or a pattern"
        }
        return $indexValue
    }
}

# ------------------------------------------------------------------
# METHOD: delete first ?last?
#
# Delete one or more elements from list box based on the first and 
# last index values.  Indexes may be a number, active, anchor, end,
# @x,y, or a pattern.
# ------------------------------------------------------------------
itcl::body iwidgets::Scrolledlistbox::delete {first {last {}}} {
    set first [_fixIndex $first]
    
    if {$last != {}} {
        set last [_fixIndex $last]
    } else {
        set last $first
    }
    
    eval $itk_component(listbox) delete $first $last
}

# ------------------------------------------------------------------
# METHOD: get first ?last?
#
# Returns the elements of the listbox indicated by the indexes. 
# Indexes may be a number, active, anchor, end, @x,y, ora pattern.
# ------------------------------------------------------------------
itcl::body iwidgets::Scrolledlistbox::get {first {last {}}} {
    set first [_fixIndex $first]
    
    if {$last != {}} {
        set last [_fixIndex $last]
    }
    
    if {$last == {}} {
        return [$itk_component(listbox) get $first]
    } else {
        return [$itk_component(listbox) get $first $last]
    }
}

# ------------------------------------------------------------------
# METHOD: getcurselection 
#
# Returns the contents of the listbox element indicated by the current 
# selection indexes.  Short cut version of get and curselection 
# command combination.
# ------------------------------------------------------------------
itcl::body iwidgets::Scrolledlistbox::getcurselection {} {
    set rlist {}

    if {[selecteditemcount] > 0} {
        set cursels [$itk_component(listbox) curselection]
    
        switch $itk_option(-selectmode) {
            single -
            browse {
                set rlist [$itk_component(listbox) get $cursels]
            }

            multiple -
            extended {
                foreach sel $cursels {
                    lappend rlist [$itk_component(listbox) get $sel]
                }
            }
        }
    }
    
    return $rlist
}

# ------------------------------------------------------------------
# METHOD: insert index string ?string ...?
#
# Insert zero or more elements in the list just before the element 
# given by index.
# ------------------------------------------------------------------
itcl::body iwidgets::Scrolledlistbox::insert {index args} {
    set index [_fixIndex $index]

    eval $itk_component(listbox) insert $index $args
}

# ------------------------------------------------------------------
# METHOD: nearest y
#
# Given a y-coordinate within the listbox, this command returns the 
# index of the visible listbox element nearest to that y-coordinate.
# ------------------------------------------------------------------
itcl::body iwidgets::Scrolledlistbox::nearest {y} {
    $itk_component(listbox) nearest $y
}

# ------------------------------------------------------------------
# METHOD: scan option args 
#
# Implements scanning on listboxes.
# ------------------------------------------------------------------
itcl::body iwidgets::Scrolledlistbox::scan {option args} {
    eval $itk_component(listbox) scan $option $args
}

# ------------------------------------------------------------------
# METHOD: selection option first ?last?
#
# Adjusts the selection within the listbox.  The index value may be 
# a integer number, active, anchor, end, @x,y, or a pattern.
# ------------------------------------------------------------------
itcl::body iwidgets::Scrolledlistbox::selection {option first {last {}}} {
    set first [_fixIndex $first]
    
    if {$last != {}} {
        set last [_fixIndex $last]
        $itk_component(listbox) selection $option $first $last
    } else {
        $itk_component(listbox) selection $option $first 
    }
}

# ------------------------------------------------------------------
# METHOD: size 
#
# Returns a decimal string indicating the total number of elements 
# in the listbox.
# ------------------------------------------------------------------
itcl::body iwidgets::Scrolledlistbox::size {} {
    return [$itk_component(listbox) size]
}

# ------------------------------------------------------------------
# METHOD: selecteditemcount 
#
# Returns a decimal string indicating the total number of selected 
# elements in the listbox.
# ------------------------------------------------------------------
itcl::body iwidgets::Scrolledlistbox::selecteditemcount {} {
    return [llength [$itk_component(listbox) curselection]]
}

# ------------------------------------------------------------------
# METHOD: justify direction
#
# Justifies the list scrolled region in one of four directions: top,
# bottom, left, or right.
# ------------------------------------------------------------------
itcl::body iwidgets::Scrolledlistbox::justify {direction} {
    switch $direction {
        left { 
            $itk_component(listbox) xview moveto 0
        }
        right {
            $itk_component(listbox) xview moveto 1
        }
        top {
            $itk_component(listbox) yview moveto 0
        }
        bottom {
            $itk_component(listbox) yview moveto 1
        }
        default {
            error "bad justify argument \"$direction\": should\
                be left, right, top, or bottom"
        }
    }
}

# ------------------------------------------------------------------
# METHOD: sort mode
#
# Sort the current list. This can take any sort switch from
# the lsort command: ascii, integer, real, command, 
# increasing/ascending, decreasing/descending, etc.
#     
# ------------------------------------------------------------------
itcl::body iwidgets::Scrolledlistbox::sort {{mode ascending}} {

    set vals [$itk_component(listbox) get 0 end]
    if {[llength $vals] == 0} {return}

    switch $mode {
        ascending   {set mode increasing}
        descending  {set mode decreasing}
    }

    $itk_component(listbox) delete 0 end
    if {[catch {eval $itk_component(listbox) insert end \
            [lsort -${mode} $vals]} errorstring]} {
        error "bad sort argument \"$mode\": must be a valid argument to the\
                Tcl lsort command"
    }

    return
}

# ------------------------------------------------------------------
# METHOD: xview args
#
# Change or query the vertical position of the text in the list box.
# ------------------------------------------------------------------
itcl::body iwidgets::Scrolledlistbox::xview {args} {
    return [eval $itk_component(listbox) xview $args]
}

# ------------------------------------------------------------------
# METHOD: yview args
#
# Change or query the horizontal position of the text in the list box.
# ------------------------------------------------------------------
itcl::body iwidgets::Scrolledlistbox::yview {args} {
    return [eval $itk_component(listbox) yview $args]
}

# ------------------------------------------------------------------
# METHOD: itemconfigure args
#
# This is a wrapper method around the new tk8.3 itemconfigure command
# for the listbox.
# ------------------------------------------------------------------
itcl::body iwidgets::Scrolledlistbox::itemconfigure {args} {
    return [eval $itk_component(listbox) itemconfigure $args]
}

# ------------------------------------------------------------------
# PROTECTED METHOD: _makeSelection 
#
# Evaluate the selection command.
# ------------------------------------------------------------------
itcl::body iwidgets::Scrolledlistbox::_makeSelection {} {
    uplevel #0 $itk_option(-selectioncommand)
}

# ------------------------------------------------------------------
# PROTECTED METHOD: _dblclick 
#
# Evaluate the double click command option if not empty.
# ------------------------------------------------------------------
itcl::body iwidgets::Scrolledlistbox::_dblclick {} {
    uplevel #0 $itk_option(-dblclickcommand)
}   

