blob: c9facf406d765880e9fba43c837bf18f7ce4392f [file] [log] [blame]
# Copyright (c) 1998-2003, Bryan Oakley
# All Rights Reservered
#
# Bryan Oakley
# oakley@bardo.clearlight.com
#
# combobox v2.3 August 16, 2003
#
# a combobox / dropdown listbox (pick your favorite name) widget
# written in pure tcl
#
# this code is freely distributable without restriction, but is
# provided as-is with no warranty expressed or implied.
#
# thanks to the following people who provided beta test support or
# patches to the code (in no particular order):
#
# Scott Beasley Alexandre Ferrieux Todd Helfter
# Matt Gushee Laurent Duperval John Jackson
# Fred Rapp Christopher Nelson
# Eric Galluzzo Jean-Francois Moine Oliver Bienert
#
# A special thanks to Martin M. Hunt who provided several good ideas,
# and always with a patch to implement them. Jean-Francois Moine,
# Todd Helfter and John Jackson were also kind enough to send in some
# code patches.
#
# ... and many others over the years.
package require Tk 8.0
package provide combobox 2.3
namespace eval ::combobox {
# this is the public interface
namespace export combobox
# these contain references to available options
variable widgetOptions
# these contain references to available commands and subcommands
variable widgetCommands
variable scanCommands
variable listCommands
}
# ::combobox::combobox --
#
# This is the command that gets exported. It creates a new
# combobox widget.
#
# Arguments:
#
# w path of new widget to create
# args additional option/value pairs (eg: -background white, etc.)
#
# Results:
#
# It creates the widget and sets up all of the default bindings
#
# Returns:
#
# The name of the newly create widget
proc ::combobox::combobox {w args} {
variable widgetOptions
variable widgetCommands
variable scanCommands
variable listCommands
# perform a one time initialization
if {![info exists widgetOptions]} {
Init
}
# build it...
eval Build $w $args
# set some bindings...
SetBindings $w
# and we are done!
return $w
}
# ::combobox::Init --
#
# Initialize the namespace variables. This should only be called
# once, immediately prior to creating the first instance of the
# widget
#
# Arguments:
#
# none
#
# Results:
#
# All state variables are set to their default values; all of
# the option database entries will exist.
#
# Returns:
#
# empty string
proc ::combobox::Init {} {
variable widgetOptions
variable widgetCommands
variable scanCommands
variable listCommands
variable defaultEntryCursor
array set widgetOptions [list \
-background {background Background} \
-bd -borderwidth \
-bg -background \
-borderwidth {borderWidth BorderWidth} \
-buttonbackground {buttonBackground Background} \
-command {command Command} \
-commandstate {commandState State} \
-cursor {cursor Cursor} \
-disabledbackground {disabledBackground DisabledBackground} \
-disabledforeground {disabledForeground DisabledForeground} \
-dropdownwidth {dropdownWidth DropdownWidth} \
-editable {editable Editable} \
-elementborderwidth {elementBorderWidth BorderWidth} \
-fg -foreground \
-font {font Font} \
-foreground {foreground Foreground} \
-height {height Height} \
-highlightbackground {highlightBackground HighlightBackground} \
-highlightcolor {highlightColor HighlightColor} \
-highlightthickness {highlightThickness HighlightThickness} \
-image {image Image} \
-listvar {listVariable Variable} \
-maxheight {maxHeight Height} \
-opencommand {opencommand Command} \
-relief {relief Relief} \
-selectbackground {selectBackground Foreground} \
-selectborderwidth {selectBorderWidth BorderWidth} \
-selectforeground {selectForeground Background} \
-state {state State} \
-takefocus {takeFocus TakeFocus} \
-textvariable {textVariable Variable} \
-value {value Value} \
-width {width Width} \
-xscrollcommand {xScrollCommand ScrollCommand} \
]
set widgetCommands [list \
bbox cget configure curselection \
delete get icursor index \
insert list scan selection \
xview select toggle open \
close entryset subwidget \
]
set listCommands [list \
delete get \
index insert size \
]
set scanCommands [list mark dragto]
# why check for the Tk package? This lets us be sourced into
# an interpreter that doesn't have Tk loaded, such as the slave
# interpreter used by pkg_mkIndex. In theory it should have no
# side effects when run
if {[lsearch -exact [package names] "Tk"] != -1} {
##################################################################
#- this initializes the option database. Kinda gross, but it works
#- (I think).
##################################################################
# the image used for the button...
if {$::tcl_platform(platform) == "windows"} {
image create bitmap ::combobox::bimage -data {
#define down_arrow_width 12
#define down_arrow_height 12
static char down_arrow_bits[] = {
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0xfc,0xf1,0xf8,0xf0,0x70,0xf0,0x20,0xf0,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00;
}
}
} else {
image create bitmap ::combobox::bimage -data {
#define down_arrow_width 15
#define down_arrow_height 15
static char down_arrow_bits[] = {
0x00,0x80,0x00,0x80,0x00,0x80,0x00,0x80,
0x00,0x80,0xf8,0x8f,0xf0,0x87,0xe0,0x83,
0xc0,0x81,0x80,0x80,0x00,0x80,0x00,0x80,
0x00,0x80,0x00,0x80,0x00,0x80
}
}
}
# compute a widget name we can use to create a temporary widget
set tmpWidget ".__tmp__"
set count 0
while {[winfo exists $tmpWidget] == 1} {
set tmpWidget ".__tmp__$count"
incr count
}
# get the scrollbar width. Because we try to be clever and draw our
# own button instead of using a tk widget, we need to know what size
# button to create. This little hack tells us the width of a scroll
# bar.
#
# NB: we need to be sure and pick a window that doesn't already
# exist...
scrollbar $tmpWidget
set sb_width [winfo reqwidth $tmpWidget]
set bbg [$tmpWidget cget -background]
destroy $tmpWidget
# steal options from the entry widget
# we want darn near all options, so we'll go ahead and do
# them all. No harm done in adding the one or two that we
# don't use.
entry $tmpWidget
foreach foo [$tmpWidget configure] {
# the cursor option is special, so we'll save it in
# a special way
if {[lindex $foo 0] == "-cursor"} {
set defaultEntryCursor [lindex $foo 4]
}
if {[llength $foo] == 5} {
set option [lindex $foo 1]
set value [lindex $foo 4]
option add *Combobox.$option $value widgetDefault
# these options also apply to the dropdown listbox
if {[string compare $option "foreground"] == 0 \
|| [string compare $option "background"] == 0 \
|| [string compare $option "font"] == 0} {
option add *Combobox*ComboboxListbox.$option $value \
widgetDefault
}
}
}
destroy $tmpWidget
# these are unique to us...
option add *Combobox.elementBorderWidth 1 widgetDefault
option add *Combobox.buttonBackground $bbg widgetDefault
option add *Combobox.dropdownWidth {} widgetDefault
option add *Combobox.openCommand {} widgetDefault
option add *Combobox.cursor {} widgetDefault
option add *Combobox.commandState normal widgetDefault
option add *Combobox.editable 1 widgetDefault
option add *Combobox.maxHeight 10 widgetDefault
option add *Combobox.height 0
}
# set class bindings
SetClassBindings
}
# ::combobox::SetClassBindings --
#
# Sets up the default bindings for the widget class
#
# this proc exists since it's The Right Thing To Do, but
# I haven't had the time to figure out how to do all the
# binding stuff on a class level. The main problem is that
# the entry widget must have focus for the insertion cursor
# to be visible. So, I either have to have the entry widget
# have the Combobox bindtag, or do some fancy juggling of
# events or some such. What a pain.
#
# Arguments:
#
# none
#
# Returns:
#
# empty string
proc ::combobox::SetClassBindings {} {
# make sure we clean up after ourselves...
bind Combobox <Destroy> [list ::combobox::DestroyHandler %W]
# this will (hopefully) close (and lose the grab on) the
# listbox if the user clicks anywhere outside of it. Note
# that on Windows, you can click on some other app and
# the listbox will still be there, because tcl won't see
# that button click
set this {[::combobox::convert %W -W]}
bind Combobox <Any-ButtonPress> "$this close"
bind Combobox <Any-ButtonRelease> "$this close"
# this helps (but doesn't fully solve) focus issues. The general
# idea is, whenever the frame gets focus it gets passed on to
# the entry widget
bind Combobox <FocusIn> {::combobox::tkTabToWindow \
[::combobox::convert %W -W].entry}
# this closes the listbox if we get hidden
bind Combobox <Unmap> {[::combobox::convert %W -W] close}
return ""
}
# ::combobox::SetBindings --
#
# here's where we do most of the binding foo. I think there's probably
# a few bindings I ought to add that I just haven't thought
# about...
#
# I'm not convinced these are the proper bindings. Ideally all
# bindings should be on "Combobox", but because of my juggling of
# bindtags I'm not convinced thats what I want to do. But, it all
# seems to work, its just not as robust as it could be.
#
# Arguments:
#
# w widget pathname
#
# Returns:
#
# empty string
proc ::combobox::SetBindings {w} {
upvar ::combobox::${w}::widgets widgets
upvar ::combobox::${w}::options options
# juggle the bindtags. The basic idea here is to associate the
# widget name with the entry widget, so if a user does a bind
# on the combobox it will get handled properly since it is
# the entry widget that has keyboard focus.
bindtags $widgets(entry) \
[concat $widgets(this) [bindtags $widgets(entry)]]
bindtags $widgets(button) \
[concat $widgets(this) [bindtags $widgets(button)]]
# override the default bindings for tab and shift-tab. The
# focus procs take a widget as their only parameter and we
# want to make sure the right window gets used (for shift-
# tab we want it to appear as if the event was generated
# on the frame rather than the entry.
bind $widgets(entry) <Tab> \
"::combobox::tkTabToWindow \[tk_focusNext $widgets(entry)\]; break"
bind $widgets(entry) <Shift-Tab> \
"::combobox::tkTabToWindow \[tk_focusPrev $widgets(this)\]; break"
# this makes our "button" (which is actually a label)
# do the right thing
bind $widgets(button) <ButtonPress-1> [list $widgets(this) toggle]
# this lets the autoscan of the listbox work, even if they
# move the cursor over the entry widget.
bind $widgets(entry) <B1-Enter> "break"
bind $widgets(listbox) <ButtonRelease-1> \
"::combobox::Select [list $widgets(this)] \
\[$widgets(listbox) nearest %y\]; break"
bind $widgets(vsb) <ButtonPress-1> {continue}
bind $widgets(vsb) <ButtonRelease-1> {continue}
bind $widgets(listbox) <Any-Motion> {
%W selection clear 0 end
%W activate @%x,%y
%W selection anchor @%x,%y
%W selection set @%x,%y @%x,%y
# need to do a yview if the cursor goes off the top
# or bottom of the window... (or do we?)
}
# these events need to be passed from the entry widget
# to the listbox, or otherwise need some sort of special
# handling.
foreach event [list <Up> <Down> <Tab> <Return> <Escape> \
<Next> <Prior> <Double-1> <1> <Any-KeyPress> \
<FocusIn> <FocusOut>] {
bind $widgets(entry) $event \
[list ::combobox::HandleEvent $widgets(this) $event]
}
# like the other events, <MouseWheel> needs to be passed from
# the entry widget to the listbox. However, in this case we
# need to add an additional parameter
catch {
bind $widgets(entry) <MouseWheel> \
[list ::combobox::HandleEvent $widgets(this) <MouseWheel> %D]
}
}
# ::combobox::Build --
#
# This does all of the work necessary to create the basic
# combobox.
#
# Arguments:
#
# w widget name
# args additional option/value pairs
#
# Results:
#
# Creates a new widget with the given name. Also creates a new
# namespace patterened after the widget name, as a child namespace
# to ::combobox
#
# Returns:
#
# the name of the widget
proc ::combobox::Build {w args } {
variable widgetOptions
if {[winfo exists $w]} {
error "window name \"$w\" already exists"
}
# create the namespace for this instance, and define a few
# variables
namespace eval ::combobox::$w {
variable ignoreTrace 0
variable oldFocus {}
variable oldGrab {}
variable oldValue {}
variable options
variable this
variable widgets
set widgets(foo) foo ;# coerce into an array
set options(foo) foo ;# coerce into an array
unset widgets(foo)
unset options(foo)
}
# import the widgets and options arrays into this proc so
# we don't have to use fully qualified names, which is a
# pain.
upvar ::combobox::${w}::widgets widgets
upvar ::combobox::${w}::options options
# this is our widget -- a frame of class Combobox. Naturally,
# it will contain other widgets. We create it here because
# we need it in order to set some default options.
set widgets(this) [frame $w -class Combobox -takefocus 0]
set widgets(entry) [entry $w.entry -takefocus 1]
set widgets(button) [label $w.button -takefocus 0]
# this defines all of the default options. We get the
# values from the option database. Note that if an array
# value is a list of length one it is an alias to another
# option, so we just ignore it
foreach name [array names widgetOptions] {
if {[llength $widgetOptions($name)] == 1} continue
set optName [lindex $widgetOptions($name) 0]
set optClass [lindex $widgetOptions($name) 1]
set value [option get $w $optName $optClass]
set options($name) $value
}
# a couple options aren't available in earlier versions of
# tcl, so we'll set them to sane values. For that matter, if
# they exist but are empty, set them to sane values.
if {[string length $options(-disabledforeground)] == 0} {
set options(-disabledforeground) $options(-foreground)
}
if {[string length $options(-disabledbackground)] == 0} {
set options(-disabledbackground) $options(-background)
}
# if -value is set to null, we'll remove it from our
# local array. The assumption is, if the user sets it from
# the option database, they will set it to something other
# than null (since it's impossible to determine the difference
# between a null value and no value at all).
if {[info exists options(-value)] \
&& [string length $options(-value)] == 0} {
unset options(-value)
}
# we will later rename the frame's widget proc to be our
# own custom widget proc. We need to keep track of this
# new name, so we'll define and store it here...
set widgets(frame) ::combobox::${w}::$w
# gotta do this sooner or later. Might as well do it now
pack $widgets(button) -side right -fill y -expand no
pack $widgets(entry) -side left -fill both -expand yes
# I should probably do this in a catch, but for now it's
# good enough... What it does, obviously, is put all of
# the option/values pairs into an array. Make them easier
# to handle later on...
array set options $args
# now, the dropdown list... the same renaming nonsense
# must go on here as well...
set widgets(dropdown) [toplevel $w.top]
set widgets(listbox) [listbox $w.top.list]
set widgets(vsb) [scrollbar $w.top.vsb]
pack $widgets(listbox) -side left -fill both -expand y
# fine tune the widgets based on the options (and a few
# arbitrary values...)
# NB: we are going to use the frame to handle the relief
# of the widget as a whole, so the entry widget will be
# flat. This makes the button which drops down the list
# to appear "inside" the entry widget.
$widgets(vsb) configure \
-borderwidth 1 \
-command "$widgets(listbox) yview" \
-highlightthickness 0
$widgets(button) configure \
-background $options(-buttonbackground) \
-highlightthickness 0 \
-borderwidth $options(-elementborderwidth) \
-relief raised \
-width [expr {[winfo reqwidth $widgets(vsb)] - 2}]
$widgets(entry) configure \
-borderwidth 0 \
-relief flat \
-highlightthickness 0
$widgets(dropdown) configure \
-borderwidth $options(-elementborderwidth) \
-relief sunken
$widgets(listbox) configure \
-selectmode browse \
-background [$widgets(entry) cget -bg] \
-yscrollcommand "$widgets(vsb) set" \
-exportselection false \
-borderwidth 0
# trace variable ::combobox::${w}::entryTextVariable w \
# [list ::combobox::EntryTrace $w]
# do some window management foo on the dropdown window
wm overrideredirect $widgets(dropdown) 1
wm transient $widgets(dropdown) [winfo toplevel $w]
wm group $widgets(dropdown) [winfo parent $w]
wm resizable $widgets(dropdown) 0 0
wm withdraw $widgets(dropdown)
# this moves the original frame widget proc into our
# namespace and gives it a handy name
rename ::$w $widgets(frame)
# now, create our widget proc. Obviously (?) it goes in
# the global namespace. All combobox widgets will actually
# share the same widget proc to cut down on the amount of
# bloat.
proc ::$w {command args} \
"eval ::combobox::WidgetProc $w \$command \$args"
# ok, the thing exists... let's do a bit more configuration.
if {[catch "::combobox::Configure [list $widgets(this)] [array get options]" error]} {
catch {destroy $w}
error "internal error: $error"
}
return ""
}
# ::combobox::HandleEvent --
#
# this proc handles events from the entry widget that we want
# handled specially (typically, to allow navigation of the list
# even though the focus is in the entry widget)
#
# Arguments:
#
# w widget pathname
# event a string representing the event (not necessarily an
# actual event)
# args additional arguments required by particular events
proc ::combobox::HandleEvent {w event args} {
upvar ::combobox::${w}::widgets widgets
upvar ::combobox::${w}::options options
upvar ::combobox::${w}::oldValue oldValue
# for all of these events, if we have a special action we'll
# do that and do a "return -code break" to keep additional
# bindings from firing. Otherwise we'll let the event fall
# on through.
switch $event {
"<MouseWheel>" {
if {[winfo ismapped $widgets(dropdown)]} {
set D [lindex $args 0]
# the '120' number in the following expression has
# it's genesis in the tk bind manpage, which suggests
# that the smallest value of %D for mousewheel events
# will be 120. The intent is to scroll one line at a time.
$widgets(listbox) yview scroll [expr {-($D/120)}] units
}
}
"<Any-KeyPress>" {
# if the widget is editable, clear the selection.
# this makes it more obvious what will happen if the
# user presses <Return> (and helps our code know what
# to do if the user presses return)
if {$options(-editable)} {
$widgets(listbox) see 0
$widgets(listbox) selection clear 0 end
$widgets(listbox) selection anchor 0
$widgets(listbox) activate 0
}
}
"<FocusIn>" {
set oldValue [$widgets(entry) get]
}
"<FocusOut>" {
if {![winfo ismapped $widgets(dropdown)]} {
# did the value change?
set newValue [$widgets(entry) get]
if {$oldValue != $newValue} {
CallCommand $widgets(this) $newValue
}
}
}
"<1>" {
set editable [::combobox::GetBoolean $options(-editable)]
if {!$editable} {
if {[winfo ismapped $widgets(dropdown)]} {
$widgets(this) close
return -code break;
} else {
if {$options(-state) != "disabled"} {
$widgets(this) open
return -code break;
}
}
}
}
"<Double-1>" {
if {$options(-state) != "disabled"} {
$widgets(this) toggle
return -code break;
}
}
"<Tab>" {
if {[winfo ismapped $widgets(dropdown)]} {
::combobox::Find $widgets(this) 0
return -code break;
} else {
::combobox::SetValue $widgets(this) [$widgets(this) get]
}
}
"<Escape>" {
# $widgets(entry) delete 0 end
# $widgets(entry) insert 0 $oldValue
if {[winfo ismapped $widgets(dropdown)]} {
$widgets(this) close
return -code break;
}
}
"<Return>" {
# did the value change?
set newValue [$widgets(entry) get]
if {$oldValue != $newValue} {
CallCommand $widgets(this) $newValue
}
if {[winfo ismapped $widgets(dropdown)]} {
::combobox::Select $widgets(this) \
[$widgets(listbox) curselection]
return -code break;
}
}
"<Next>" {
$widgets(listbox) yview scroll 1 pages
set index [$widgets(listbox) index @0,0]
$widgets(listbox) see $index
$widgets(listbox) activate $index
$widgets(listbox) selection clear 0 end
$widgets(listbox) selection anchor $index
$widgets(listbox) selection set $index
}
"<Prior>" {
$widgets(listbox) yview scroll -1 pages
set index [$widgets(listbox) index @0,0]
$widgets(listbox) activate $index
$widgets(listbox) see $index
$widgets(listbox) selection clear 0 end
$widgets(listbox) selection anchor $index
$widgets(listbox) selection set $index
}
"<Down>" {
if {[winfo ismapped $widgets(dropdown)]} {
::combobox::tkListboxUpDown $widgets(listbox) 1
return -code break;
} else {
if {$options(-state) != "disabled"} {
$widgets(this) open
return -code break;
}
}
}
"<Up>" {
if {[winfo ismapped $widgets(dropdown)]} {
::combobox::tkListboxUpDown $widgets(listbox) -1
return -code break;
} else {
if {$options(-state) != "disabled"} {
$widgets(this) open
return -code break;
}
}
}
}
return ""
}
# ::combobox::DestroyHandler {w} --
#
# Cleans up after a combobox widget is destroyed
#
# Arguments:
#
# w widget pathname
#
# Results:
#
# The namespace that was created for the widget is deleted,
# and the widget proc is removed.
proc ::combobox::DestroyHandler {w} {
catch {
# if the widget actually being destroyed is of class Combobox,
# remove the namespace and associated proc.
if {[string compare [winfo class $w] "Combobox"] == 0} {
# delete the namespace and the proc which represents
# our widget
namespace delete ::combobox::$w
rename $w {}
}
}
return ""
}
# ::combobox::Find
#
# finds something in the listbox that matches the pattern in the
# entry widget and selects it
#
# N.B. I'm not convinced this is working the way it ought to. It
# works, but is the behavior what is expected? I've also got a gut
# feeling that there's a better way to do this, but I'm too lazy to
# figure it out...
#
# Arguments:
#
# w widget pathname
# exact boolean; if true an exact match is desired
#
# Returns:
#
# Empty string
proc ::combobox::Find {w {exact 0}} {
upvar ::combobox::${w}::widgets widgets
upvar ::combobox::${w}::options options
## *sigh* this logic is rather gross and convoluted. Surely
## there is a more simple, straight-forward way to implement
## all this. As the saying goes, I lack the time to make it
## shorter...
# use what is already in the entry widget as a pattern
set pattern [$widgets(entry) get]
if {[string length $pattern] == 0} {
# clear the current selection
$widgets(listbox) see 0
$widgets(listbox) selection clear 0 end
$widgets(listbox) selection anchor 0
$widgets(listbox) activate 0
return
}
# we're going to be searching this list...
set list [$widgets(listbox) get 0 end]
# if we are doing an exact match, try to find,
# well, an exact match
set exactMatch -1
if {$exact} {
set exactMatch [lsearch -exact $list $pattern]
}
# search for it. We'll try to be clever and not only
# search for a match for what they typed, but a match for
# something close to what they typed. We'll keep removing one
# character at a time from the pattern until we find a match
# of some sort.
set index -1
while {$index == -1 && [string length $pattern]} {
set index [lsearch -glob $list "$pattern*"]
if {$index == -1} {
regsub {.$} $pattern {} pattern
}
}
# this is the item that most closely matches...
set thisItem [lindex $list $index]
# did we find a match? If so, do some additional munging...
if {$index != -1} {
# we need to find the part of the first item that is
# unique WRT the second... I know there's probably a
# simpler way to do this...
set nextIndex [expr {$index + 1}]
set nextItem [lindex $list $nextIndex]
# we don't really need to do much if the next
# item doesn't match our pattern...
if {[string match $pattern* $nextItem]} {
# ok, the next item matches our pattern, too
# now the trick is to find the first character
# where they *don't* match...
set marker [string length $pattern]
while {$marker <= [string length $pattern]} {
set a [string index $thisItem $marker]
set b [string index $nextItem $marker]
if {[string compare $a $b] == 0} {
append pattern $a
incr marker
} else {
break
}
}
} else {
set marker [string length $pattern]
}
} else {
set marker end
set index 0
}
# ok, we know the pattern and what part is unique;
# update the entry widget and listbox appropriately
if {$exact && $exactMatch == -1} {
# this means we didn't find an exact match
$widgets(listbox) selection clear 0 end
$widgets(listbox) see $index
} elseif {!$exact} {
# this means we found something, but it isn't an exact
# match. If we find something that *is* an exact match we
# don't need to do the following, since it would merely
# be replacing the data in the entry widget with itself
set oldstate [$widgets(entry) cget -state]
$widgets(entry) configure -state normal
$widgets(entry) delete 0 end
$widgets(entry) insert end $thisItem
$widgets(entry) selection clear
$widgets(entry) selection range $marker end
$widgets(listbox) activate $index
$widgets(listbox) selection clear 0 end
$widgets(listbox) selection anchor $index
$widgets(listbox) selection set $index
$widgets(listbox) see $index
$widgets(entry) configure -state $oldstate
}
}
# ::combobox::Select --
#
# selects an item from the list and sets the value of the combobox
# to that value
#
# Arguments:
#
# w widget pathname
# index listbox index of item to be selected
#
# Returns:
#
# empty string
proc ::combobox::Select {w index} {
upvar ::combobox::${w}::widgets widgets
upvar ::combobox::${w}::options options
# the catch is because I'm sloppy -- presumably, the only time
# an error will be caught is if there is no selection.
if {![catch {set data [$widgets(listbox) get [lindex $index 0]]}]} {
::combobox::SetValue $widgets(this) $data
$widgets(listbox) selection clear 0 end
$widgets(listbox) selection anchor $index
$widgets(listbox) selection set $index
}
$widgets(entry) selection range 0 end
$widgets(entry) icursor end
$widgets(this) close
return ""
}
# ::combobox::HandleScrollbar --
#
# causes the scrollbar of the dropdown list to appear or disappear
# based on the contents of the dropdown listbox
#
# Arguments:
#
# w widget pathname
# action the action to perform on the scrollbar
#
# Returns:
#
# an empty string
proc ::combobox::HandleScrollbar {w {action "unknown"}} {
upvar ::combobox::${w}::widgets widgets
upvar ::combobox::${w}::options options
if {$options(-height) == 0} {
set hlimit $options(-maxheight)
} else {
set hlimit $options(-height)
}
switch $action {
"grow" {
if {$hlimit > 0 && [$widgets(listbox) size] > $hlimit} {
pack forget $widgets(listbox)
pack $widgets(vsb) -side right -fill y -expand n
pack $widgets(listbox) -side left -fill both -expand y
}
}
"shrink" {
if {$hlimit > 0 && [$widgets(listbox) size] <= $hlimit} {
pack forget $widgets(vsb)
}
}
"crop" {
# this means the window was cropped and we definitely
# need a scrollbar no matter what the user wants
pack forget $widgets(listbox)
pack $widgets(vsb) -side right -fill y -expand n
pack $widgets(listbox) -side left -fill both -expand y
}
default {
if {$hlimit > 0 && [$widgets(listbox) size] > $hlimit} {
pack forget $widgets(listbox)
pack $widgets(vsb) -side right -fill y -expand n
pack $widgets(listbox) -side left -fill both -expand y
} else {
pack forget $widgets(vsb)
}
}
}
return ""
}
# ::combobox::ComputeGeometry --
#
# computes the geometry of the dropdown list based on the size of the
# combobox...
#
# Arguments:
#
# w widget pathname
#
# Returns:
#
# the desired geometry of the listbox
proc ::combobox::ComputeGeometry {w} {
upvar ::combobox::${w}::widgets widgets
upvar ::combobox::${w}::options options
if {$options(-height) == 0 && $options(-maxheight) != "0"} {
# if this is the case, count the items and see if
# it exceeds our maxheight. If so, set the listbox
# size to maxheight...
set nitems [$widgets(listbox) size]
if {$nitems > $options(-maxheight)} {
# tweak the height of the listbox
$widgets(listbox) configure -height $options(-maxheight)
} else {
# un-tweak the height of the listbox
$widgets(listbox) configure -height 0
}
update idletasks
}
# compute height and width of the dropdown list
set bd [$widgets(dropdown) cget -borderwidth]
set height [expr {[winfo reqheight $widgets(dropdown)] + $bd + $bd}]
if {[string length $options(-dropdownwidth)] == 0 ||
$options(-dropdownwidth) == 0} {
set width [winfo width $widgets(this)]
} else {
set m [font measure [$widgets(listbox) cget -font] "m"]
set width [expr {$options(-dropdownwidth) * $m}]
}
# figure out where to place it on the screen, trying to take into
# account we may be running under some virtual window manager
set screenWidth [winfo screenwidth $widgets(this)]
set screenHeight [winfo screenheight $widgets(this)]
set rootx [winfo rootx $widgets(this)]
set rooty [winfo rooty $widgets(this)]
set vrootx [winfo vrootx $widgets(this)]
set vrooty [winfo vrooty $widgets(this)]
# the x coordinate is simply the rootx of our widget, adjusted for
# the virtual window. We won't worry about whether the window will
# be offscreen to the left or right -- we want the illusion that it
# is part of the entry widget, so if part of the entry widget is off-
# screen, so will the list. If you want to change the behavior,
# simply change the if statement... (and be sure to update this
# comment!)
set x [expr {$rootx + $vrootx}]
if {0} {
set rightEdge [expr {$x + $width}]
if {$rightEdge > $screenWidth} {
set x [expr {$screenWidth - $width}]
}
if {$x < 0} {set x 0}
}
# the y coordinate is the rooty plus vrooty offset plus
# the height of the static part of the widget plus 1 for a
# tiny bit of visual separation...
set y [expr {$rooty + $vrooty + [winfo reqheight $widgets(this)] + 1}]
set bottomEdge [expr {$y + $height}]
if {$bottomEdge >= $screenHeight} {
# ok. Fine. Pop it up above the entry widget isntead of
# below.
set y [expr {($rooty - $height - 1) + $vrooty}]
if {$y < 0} {
# this means it extends beyond our screen. How annoying.
# Now we'll try to be real clever and either pop it up or
# down, depending on which way gives us the biggest list.
# then, we'll trim the list to fit and force the use of
# a scrollbar
# (sadly, for windows users this measurement doesn't
# take into consideration the height of the taskbar,
# but don't blame me -- there isn't any way to detect
# it or figure out its dimensions. The same probably
# applies to any window manager with some magic windows
# glued to the top or bottom of the screen)
if {$rooty > [expr {$screenHeight / 2}]} {
# we are in the lower half of the screen --
# pop it up. Y is zero; that parts easy. The height
# is simply the y coordinate of our widget, minus
# a pixel for some visual separation. The y coordinate
# will be the topof the screen.
set y 1
set height [expr {$rooty - 1 - $y}]
} else {
# we are in the upper half of the screen --
# pop it down
set y [expr {$rooty + $vrooty + \
[winfo reqheight $widgets(this)] + 1}]
set height [expr {$screenHeight - $y}]
}
# force a scrollbar
HandleScrollbar $widgets(this) crop
}
}
if {$y < 0} {
# hmmm. Bummer.
set y 0
set height $screenheight
}
set geometry [format "=%dx%d+%d+%d" $width $height $x $y]
return $geometry
}
# ::combobox::DoInternalWidgetCommand --
#
# perform an internal widget command, then mung any error results
# to look like it came from our megawidget. A lot of work just to
# give the illusion that our megawidget is an atomic widget
#
# Arguments:
#
# w widget pathname
# subwidget pathname of the subwidget
# command subwidget command to be executed
# args arguments to the command
#
# Returns:
#
# The result of the subwidget command, or an error
proc ::combobox::DoInternalWidgetCommand {w subwidget command args} {
upvar ::combobox::${w}::widgets widgets
upvar ::combobox::${w}::options options
set subcommand $command
set command [concat $widgets($subwidget) $command $args]
if {[catch $command result]} {
# replace the subwidget name with the megawidget name
regsub $widgets($subwidget) $result $widgets(this) result
# replace specific instances of the subwidget command
# with our megawidget command
switch $subwidget,$subcommand {
listbox,index {regsub "index" $result "list index" result}
listbox,insert {regsub "insert" $result "list insert" result}
listbox,delete {regsub "delete" $result "list delete" result}
listbox,get {regsub "get" $result "list get" result}
listbox,size {regsub "size" $result "list size" result}
}
error $result
} else {
return $result
}
}
# ::combobox::WidgetProc --
#
# This gets uses as the widgetproc for an combobox widget.
# Notice where the widget is created and you'll see that the
# actual widget proc merely evals this proc with all of the
# arguments intact.
#
# Note that some widget commands are defined "inline" (ie:
# within this proc), and some do most of their work in
# separate procs. This is merely because sometimes it was
# easier to do it one way or the other.
#
# Arguments:
#
# w widget pathname
# command widget subcommand
# args additional arguments; varies with the subcommand
#
# Results:
#
# Performs the requested widget command
proc ::combobox::WidgetProc {w command args} {
upvar ::combobox::${w}::widgets widgets
upvar ::combobox::${w}::options options
upvar ::combobox::${w}::oldFocus oldFocus
upvar ::combobox::${w}::oldFocus oldGrab
set command [::combobox::Canonize $w command $command]
# this is just shorthand notation...
set doWidgetCommand \
[list ::combobox::DoInternalWidgetCommand $widgets(this)]
if {$command == "list"} {
# ok, the next argument is a list command; we'll
# rip it from args and append it to command to
# create a unique internal command
#
# NB: because of the sloppy way we are doing this,
# we'll also let the user enter our secret command
# directly (eg: listinsert, listdelete), but we
# won't document that fact
set command "list-[lindex $args 0]"
set args [lrange $args 1 end]
}
set result ""
# many of these commands are just synonyms for specific
# commands in one of the subwidgets. We'll get them out
# of the way first, then do the custom commands.
switch $command {
bbox -
delete -
get -
icursor -
index -
insert -
scan -
selection -
xview {
set result [eval $doWidgetCommand entry $command $args]
}
list-get {set result [eval $doWidgetCommand listbox get $args]}
list-index {set result [eval $doWidgetCommand listbox index $args]}
list-size {set result [eval $doWidgetCommand listbox size $args]}
entryset {
# update the entry field without invoking the command
::combobox::SetValue $widgets(this) [lindex $args 0] 0
}
select {
if {[llength $args] == 1} {
set index [lindex $args 0]
set result [Select $widgets(this) $index]
} else {
error "usage: $w select index"
}
}
subwidget {
set knownWidgets [list button entry listbox dropdown vsb]
if {[llength $args] == 0} {
return $knownWidgets
}
set name [lindex $args 0]
if {[lsearch $knownWidgets $name] != -1} {
set result $widgets($name)
} else {
error "unknown subwidget $name"
}
}
curselection {
set result [eval $doWidgetCommand listbox curselection]
}
list-insert {
eval $doWidgetCommand listbox insert $args
set result [HandleScrollbar $w "grow"]
}
list-delete {
eval $doWidgetCommand listbox delete $args
set result [HandleScrollbar $w "shrink"]
}
toggle {
# ignore this command if the widget is disabled...
if {$options(-state) == "disabled"} return
# pops down the list if it is not, hides it
# if it is...
if {[winfo ismapped $widgets(dropdown)]} {
set result [$widgets(this) close]
} else {
set result [$widgets(this) open]
}
}
open {
# if this is an editable combobox, the focus should
# be set to the entry widget
if {$options(-editable)} {
focus $widgets(entry)
$widgets(entry) select range 0 end
$widgets(entry) icursor end
}
# if we are disabled, we won't allow this to happen
if {$options(-state) == "disabled"} {
return 0
}
# if there is a -opencommand, execute it now
if {[string length $options(-opencommand)] > 0} {
# hmmm... should I do a catch, or just let the normal
# error handling handle any errors? For now, the latter...
uplevel \#0 $options(-opencommand)
}
# compute the geometry of the window to pop up, and set
# it, and force the window manager to take notice
# (even if it is not presently visible).
#
# this isn't strictly necessary if the window is already
# mapped, but we'll go ahead and set the geometry here
# since its harmless and *may* actually reset the geometry
# to something better in some weird case.
set geometry [::combobox::ComputeGeometry $widgets(this)]
wm geometry $widgets(dropdown) $geometry
update idletasks
# if we are already open, there's nothing else to do
if {[winfo ismapped $widgets(dropdown)]} {
return 0
}
# save the widget that currently has the focus; we'll restore
# the focus there when we're done
set oldFocus [focus]
# ok, tweak the visual appearance of things and
# make the list pop up
$widgets(button) configure -relief sunken
wm deiconify $widgets(dropdown)
update idletasks
raise $widgets(dropdown)
# force focus to the entry widget so we can handle keypress
# events for traversal
focus -force $widgets(entry)
# select something by default, but only if its an
# exact match...
::combobox::Find $widgets(this) 1
# save the current grab state for the display containing
# this widget. We'll restore it when we close the dropdown
# list
set status "none"
set grab [grab current $widgets(this)]
if {$grab != ""} {set status [grab status $grab]}
set oldGrab [list $grab $status]
unset grab status
# *gasp* do a global grab!!! Mom always told me not to
# do things like this, but sometimes a man's gotta do
# what a man's gotta do.
grab -global $widgets(this)
# fake the listbox into thinking it has focus. This is
# necessary to get scanning initialized properly in the
# listbox.
event generate $widgets(listbox) <B1-Enter>
return 1
}
close {
# if we are already closed, don't do anything...
if {![winfo ismapped $widgets(dropdown)]} {
return 0
}
# restore the focus and grab, but ignore any errors...
# we're going to be paranoid and release the grab before
# trying to set any other grab because we really really
# really want to make sure the grab is released.
catch {focus $oldFocus} result
catch {grab release $widgets(this)}
catch {
set status [lindex $oldGrab 1]
if {$status == "global"} {
grab -global [lindex $oldGrab 0]
} elseif {$status == "local"} {
grab [lindex $oldGrab 0]
}
unset status
}
# hides the listbox
$widgets(button) configure -relief raised
wm withdraw $widgets(dropdown)
# select the data in the entry widget. Not sure
# why, other than observation seems to suggest that's
# what windows widgets do.
set editable [::combobox::GetBoolean $options(-editable)]
if {$editable} {
$widgets(entry) selection range 0 end
$widgets(button) configure -relief raised
}
# magic tcl stuff (see tk.tcl in the distribution
# lib directory)
::combobox::tkCancelRepeat
return 1
}
cget {
if {[llength $args] != 1} {
error "wrong # args: should be $w cget option"
}
set opt [::combobox::Canonize $w option [lindex $args 0]]
if {$opt == "-value"} {
set result [$widgets(entry) get]
} else {
set result $options($opt)
}
}
configure {
set result [eval ::combobox::Configure {$w} $args]
}
default {
error "bad option \"$command\""
}
}
return $result
}
# ::combobox::Configure --
#
# Implements the "configure" widget subcommand
#
# Arguments:
#
# w widget pathname
# args zero or more option/value pairs (or a single option)
#
# Results:
#
# Performs typcial "configure" type requests on the widget
proc ::combobox::Configure {w args} {
variable widgetOptions
variable defaultEntryCursor
upvar ::combobox::${w}::widgets widgets
upvar ::combobox::${w}::options options
if {[llength $args] == 0} {
# hmmm. User must be wanting all configuration information
# note that if the value of an array element is of length
# one it is an alias, which needs to be handled slightly
# differently
set results {}
foreach opt [lsort [array names widgetOptions]] {
if {[llength $widgetOptions($opt)] == 1} {
set alias $widgetOptions($opt)
set optName $widgetOptions($alias)
lappend results [list $opt $optName]
} else {
set optName [lindex $widgetOptions($opt) 0]
set optClass [lindex $widgetOptions($opt) 1]
set default [option get $w $optName $optClass]
if {[info exists options($opt)]} {
lappend results [list $opt $optName $optClass \
$default $options($opt)]
} else {
lappend results [list $opt $optName $optClass \
$default ""]
}
}
}
return $results
}
# one argument means we are looking for configuration
# information on a single option
if {[llength $args] == 1} {
set opt [::combobox::Canonize $w option [lindex $args 0]]
set optName [lindex $widgetOptions($opt) 0]
set optClass [lindex $widgetOptions($opt) 1]
set default [option get $w $optName $optClass]
set results [list $opt $optName $optClass \
$default $options($opt)]
return $results
}
# if we have an odd number of values, bail.
if {[expr {[llength $args]%2}] == 1} {
# hmmm. An odd number of elements in args
error "value for \"[lindex $args end]\" missing"
}
# Great. An even number of options. Let's make sure they
# are all valid before we do anything. Note that Canonize
# will generate an error if it finds a bogus option; otherwise
# it returns the canonical option name
foreach {name value} $args {
set name [::combobox::Canonize $w option $name]
set opts($name) $value
}
# process all of the configuration options
# some (actually, most) options require us to
# do something, like change the attributes of
# a widget or two. Here's where we do that...
#
# note that the handling of disabledforeground and
# disabledbackground is a little wonky. First, we have
# to deal with backwards compatibility (ie: tk 8.3 and below
# didn't have such options for the entry widget), and
# we have to deal with the fact we might want to disable
# the entry widget but use the normal foreground/background
# for when the combobox is not disabled, but not editable either.
set updateVisual 0
foreach option [array names opts] {
set newValue $opts($option)
if {[info exists options($option)]} {
set oldValue $options($option)
}
switch -- $option {
-buttonbackground {
$widgets(button) configure -background $newValue
}
-background {
set updateVisual 1
set options($option) $newValue
}
-borderwidth {
$widgets(frame) configure -borderwidth $newValue
set options($option) $newValue
}
-command {
# nothing else to do...
set options($option) $newValue
}
-commandstate {
# do some value checking...
if {$newValue != "normal" && $newValue != "disabled"} {
set options($option) $oldValue
set message "bad state value \"$newValue\";"
append message " must be normal or disabled"
error $message
}
set options($option) $newValue
}
-cursor {
$widgets(frame) configure -cursor $newValue
$widgets(entry) configure -cursor $newValue
$widgets(listbox) configure -cursor $newValue
set options($option) $newValue
}
-disabledforeground {
set updateVisual 1
set options($option) $newValue
}
-disabledbackground {
set updateVisual 1
set options($option) $newValue
}
-dropdownwidth {
set options($option) $newValue
}
-editable {
set updateVisual 1
if {$newValue} {
# it's editable...
$widgets(entry) configure \
-state normal \
-cursor $defaultEntryCursor
} else {
$widgets(entry) configure \
-state disabled \
-cursor $options(-cursor)
}
set options($option) $newValue
}
-elementborderwidth {
$widgets(button) configure -borderwidth $newValue
$widgets(vsb) configure -borderwidth $newValue
$widgets(dropdown) configure -borderwidth $newValue
set options($option) $newValue
}
-font {
$widgets(entry) configure -font $newValue
$widgets(listbox) configure -font $newValue
set options($option) $newValue
}
-foreground {
set updateVisual 1
set options($option) $newValue
}
-height {
$widgets(listbox) configure -height $newValue
HandleScrollbar $w
set options($option) $newValue
}
-highlightbackground {
$widgets(frame) configure -highlightbackground $newValue
set options($option) $newValue
}
-highlightcolor {
$widgets(frame) configure -highlightcolor $newValue
set options($option) $newValue
}
-highlightthickness {
$widgets(frame) configure -highlightthickness $newValue
set options($option) $newValue
}
-image {
if {[string length $newValue] > 0} {
puts "old button width: [$widgets(button) cget -width]"
$widgets(button) configure \
-image $newValue \
-width [expr {[image width $newValue] + 2}]
puts "new button width: [$widgets(button) cget -width]"
} else {
$widgets(button) configure -image ::combobox::bimage
}
set options($option) $newValue
}
-listvar {
if {[catch {$widgets(listbox) cget -listvar}]} {
return -code error \
"-listvar not supported with this version of tk"
}
$widgets(listbox) configure -listvar $newValue
set options($option) $newValue
}
-maxheight {
# ComputeGeometry may dork with the actual height
# of the listbox, so let's undork it
$widgets(listbox) configure -height $options(-height)
HandleScrollbar $w
set options($option) $newValue
}
-opencommand {
# nothing else to do...
set options($option) $newValue
}
-relief {
$widgets(frame) configure -relief $newValue
set options($option) $newValue
}
-selectbackground {
$widgets(entry) configure -selectbackground $newValue
$widgets(listbox) configure -selectbackground $newValue
set options($option) $newValue
}
-selectborderwidth {
$widgets(entry) configure -selectborderwidth $newValue
$widgets(listbox) configure -selectborderwidth $newValue
set options($option) $newValue
}
-selectforeground {
$widgets(entry) configure -selectforeground $newValue
$widgets(listbox) configure -selectforeground $newValue
set options($option) $newValue
}
-state {
if {$newValue == "normal"} {
set updateVisual 1
# it's enabled
set editable [::combobox::GetBoolean \
$options(-editable)]
if {$editable} {
$widgets(entry) configure -state normal
$widgets(entry) configure -takefocus 1
}
# note that $widgets(button) is actually a label,
# not a button. And being able to disable labels
# wasn't possible until tk 8.3. (makes me wonder
# why I chose to use a label, but that answer is
# lost to antiquity)
if {[info patchlevel] >= 8.3} {
$widgets(button) configure -state normal
}
} elseif {$newValue == "disabled"} {
set updateVisual 1
# it's disabled
$widgets(entry) configure -state disabled
$widgets(entry) configure -takefocus 0
# note that $widgets(button) is actually a label,
# not a button. And being able to disable labels
# wasn't possible until tk 8.3. (makes me wonder
# why I chose to use a label, but that answer is
# lost to antiquity)
if {$::tcl_version >= 8.3} {
$widgets(button) configure -state disabled
}
} else {
set options($option) $oldValue
set message "bad state value \"$newValue\";"
append message " must be normal or disabled"
error $message
}
set options($option) $newValue
}
-takefocus {
$widgets(entry) configure -takefocus $newValue
set options($option) $newValue
}
-textvariable {
$widgets(entry) configure -textvariable $newValue
set options($option) $newValue
}
-value {
::combobox::SetValue $widgets(this) $newValue
set options($option) $newValue
}
-width {
$widgets(entry) configure -width $newValue
$widgets(listbox) configure -width $newValue
set options($option) $newValue
}
-xscrollcommand {
$widgets(entry) configure -xscrollcommand $newValue
set options($option) $newValue
}
}
if {$updateVisual} {UpdateVisualAttributes $w}
}
}
# ::combobox::UpdateVisualAttributes --
#
# sets the visual attributes (foreground, background mostly)
# based on the current state of the widget (normal/disabled,
# editable/non-editable)
#
# why a proc for such a simple thing? Well, in addition to the
# various states of the widget, we also have to consider the
# version of tk being used -- versions from 8.4 and beyond have
# the notion of disabled foreground/background options for various
# widgets. All of the permutations can get nasty, so we encapsulate
# it all in one spot.
#
# note also that we don't handle all visual attributes here; just
# the ones that depend on the state of the widget. The rest are
# handled on a case by case basis
#
# Arguments:
# w widget pathname
#
# Returns:
# empty string
proc ::combobox::UpdateVisualAttributes {w} {
upvar ::combobox::${w}::widgets widgets
upvar ::combobox::${w}::options options
if {$options(-state) == "normal"} {
set foreground $options(-foreground)
set background $options(-background)
} elseif {$options(-state) == "disabled"} {
set foreground $options(-disabledforeground)
set background $options(-disabledbackground)
}
$widgets(entry) configure -foreground $foreground -background $background
$widgets(listbox) configure -foreground $foreground -background $background
$widgets(button) configure -foreground $foreground
$widgets(vsb) configure -background $background -troughcolor $background
$widgets(frame) configure -background $background
# we need to set the disabled colors in case our widget is disabled.
# We could actually check for disabled-ness, but we also need to
# check whether we're enabled but not editable, in which case the
# entry widget is disabled but we still want the enabled colors. It's
# easier just to set everything and be done with it.
if {$::tcl_version >= 8.4} {
$widgets(entry) configure \
-disabledforeground $foreground \
-disabledbackground $background
$widgets(button) configure -disabledforeground $foreground
$widgets(listbox) configure -disabledforeground $foreground
}
}
# ::combobox::SetValue --
#
# sets the value of the combobox and calls the -command,
# if defined
#
# Arguments:
#
# w widget pathname
# newValue the new value of the combobox
#
# Returns
#
# Empty string
proc ::combobox::SetValue {w newValue {call 1}} {
upvar ::combobox::${w}::widgets widgets
upvar ::combobox::${w}::options options
upvar ::combobox::${w}::ignoreTrace ignoreTrace
upvar ::combobox::${w}::oldValue oldValue
if {[info exists options(-textvariable)] \
&& [string length $options(-textvariable)] > 0} {
set variable ::$options(-textvariable)
set $variable $newValue
} else {
set oldstate [$widgets(entry) cget -state]
$widgets(entry) configure -state normal
$widgets(entry) delete 0 end
$widgets(entry) insert 0 $newValue
$widgets(entry) configure -state $oldstate
}
# set our internal textvariable; this will cause any public
# textvariable (ie: defined by the user) to be updated as
# well
# set ::combobox::${w}::entryTextVariable $newValue
# redefine our concept of the "old value". Do it before running
# any associated command so we can be sure it happens even
# if the command somehow fails.
set oldValue $newValue
# call the associated command. The proc will handle whether or
# not to actually call it, and with what args
if {$call} {
CallCommand $w $newValue
}
return ""
}
# ::combobox::CallCommand --
#
# calls the associated command, if any, appending the new
# value to the command to be called.
#
# Arguments:
#
# w widget pathname
# newValue the new value of the combobox
#
# Returns
#
# empty string
proc ::combobox::CallCommand {w newValue} {
upvar ::combobox::${w}::widgets widgets
upvar ::combobox::${w}::options options
# call the associated command, if defined and -commandstate is
# set to "normal"
if {$options(-commandstate) == "normal" && \
[string length $options(-command)] > 0} {
set args [list $widgets(this) $newValue]
uplevel \#0 $options(-command) $args
}
}
# ::combobox::GetBoolean --
#
# returns the value of a (presumably) boolean string (ie: it should
# do the right thing if the string is "yes", "no", "true", 1, etc
#
# Arguments:
#
# value value to be converted
# errorValue a default value to be returned in case of an error
#
# Returns:
#
# a 1 or zero, or the value of errorValue if the string isn't
# a proper boolean value
proc ::combobox::GetBoolean {value {errorValue 1}} {
if {[catch {expr {([string trim $value])?1:0}} res]} {
return $errorValue
} else {
return $res
}
}
# ::combobox::convert --
#
# public routine to convert %x, %y and %W binding substitutions.
# Given an x, y and or %W value relative to a given widget, this
# routine will convert the values to be relative to the combobox
# widget. For example, it could be used in a binding like this:
#
# bind .combobox <blah> {doSomething [::combobox::convert %W -x %x]}
#
# Note that this procedure is *not* exported, but is intended for
# public use. It is not exported because the name could easily
# clash with existing commands.
#
# Arguments:
#
# w a widget path; typically the actual result of a %W
# substitution in a binding. It should be either a
# combobox widget or one of its subwidgets
#
# args should one or more of the following arguments or
# pairs of arguments:
#
# -x <x> will convert the value <x>; typically <x> will
# be the result of a %x substitution
# -y <y> will convert the value <y>; typically <y> will
# be the result of a %y substitution
# -W (or -w) will return the name of the combobox widget
# which is the parent of $w
#
# Returns:
#
# a list of the requested values. For example, a single -w will
# result in a list of one items, the name of the combobox widget.
# Supplying "-x 10 -y 20 -W" (in any order) will return a list of
# three values: the converted x and y values, and the name of
# the combobox widget.
proc ::combobox::convert {w args} {
set result {}
if {![winfo exists $w]} {
error "window \"$w\" doesn't exist"
}
while {[llength $args] > 0} {
set option [lindex $args 0]
set args [lrange $args 1 end]
switch -exact -- $option {
-x {
set value [lindex $args 0]
set args [lrange $args 1 end]
set win $w
while {[winfo class $win] != "Combobox"} {
incr value [winfo x $win]
set win [winfo parent $win]
if {$win == "."} break
}
lappend result $value
}
-y {
set value [lindex $args 0]
set args [lrange $args 1 end]
set win $w
while {[winfo class $win] != "Combobox"} {
incr value [winfo y $win]
set win [winfo parent $win]
if {$win == "."} break
}
lappend result $value
}
-w -
-W {
set win $w
while {[winfo class $win] != "Combobox"} {
set win [winfo parent $win]
if {$win == "."} break;
}
lappend result $win
}
}
}
return $result
}
# ::combobox::Canonize --
#
# takes a (possibly abbreviated) option or command name and either
# returns the canonical name or an error
#
# Arguments:
#
# w widget pathname
# object type of object to canonize; must be one of "command",
# "option", "scan command" or "list command"
# opt the option (or command) to be canonized
#
# Returns:
#
# Returns either the canonical form of an option or command,
# or raises an error if the option or command is unknown or
# ambiguous.
proc ::combobox::Canonize {w object opt} {
variable widgetOptions
variable columnOptions
variable widgetCommands
variable listCommands
variable scanCommands
switch $object {
command {
if {[lsearch -exact $widgetCommands $opt] >= 0} {
return $opt
}
# command names aren't stored in an array, and there
# isn't a way to get all the matches in a list, so
# we'll stuff the commands in a temporary array so
# we can use [array names]
set list $widgetCommands
foreach element $list {
set tmp($element) ""
}
set matches [array names tmp ${opt}*]
}
{list command} {
if {[lsearch -exact $listCommands $opt] >= 0} {
return $opt
}
# command names aren't stored in an array, and there
# isn't a way to get all the matches in a list, so
# we'll stuff the commands in a temporary array so
# we can use [array names]
set list $listCommands
foreach element $list {
set tmp($element) ""
}
set matches [array names tmp ${opt}*]
}
{scan command} {
if {[lsearch -exact $scanCommands $opt] >= 0} {
return $opt
}
# command names aren't stored in an array, and there
# isn't a way to get all the matches in a list, so
# we'll stuff the commands in a temporary array so
# we can use [array names]
set list $scanCommands
foreach element $list {
set tmp($element) ""
}
set matches [array names tmp ${opt}*]
}
option {
if {[info exists widgetOptions($opt)] \
&& [llength $widgetOptions($opt)] == 2} {
return $opt
}
set list [array names widgetOptions]
set matches [array names widgetOptions ${opt}*]
}
}
if {[llength $matches] == 0} {
set choices [HumanizeList $list]
error "unknown $object \"$opt\"; must be one of $choices"
} elseif {[llength $matches] == 1} {
set opt [lindex $matches 0]
# deal with option aliases
switch $object {
option {
set opt [lindex $matches 0]
if {[llength $widgetOptions($opt)] == 1} {
set opt $widgetOptions($opt)
}
}
}
return $opt
} else {
set choices [HumanizeList $list]
error "ambiguous $object \"$opt\"; must be one of $choices"
}
}
# ::combobox::HumanizeList --
#
# Returns a human-readable form of a list by separating items
# by columns, but separating the last two elements with "or"
# (eg: foo, bar or baz)
#
# Arguments:
#
# list a valid tcl list
#
# Results:
#
# A string which as all of the elements joined with ", " or
# the word " or "
proc ::combobox::HumanizeList {list} {
if {[llength $list] == 1} {
return [lindex $list 0]
} else {
set list [lsort $list]
set secondToLast [expr {[llength $list] -2}]
set most [lrange $list 0 $secondToLast]
set last [lindex $list end]
return "[join $most {, }] or $last"
}
}
# This is some backwards-compatibility code to handle TIP 44
# (http://purl.org/tcl/tip/44.html). For all private tk commands
# used by this widget, we'll make duplicates of the procs in the
# combobox namespace.
#
# I'm not entirely convinced this is the right thing to do. I probably
# shouldn't even be using the private commands. Then again, maybe the
# private commands really should be public. Oh well; it works so it
# must be OK...
foreach command {TabToWindow CancelRepeat ListboxUpDown} {
if {[llength [info commands ::combobox::tk$command]] == 1} break;
set tmp [info commands tk$command]
set proc ::combobox::tk$command
if {[llength [info commands tk$command]] == 1} {
set command [namespace which [lindex $tmp 0]]
proc $proc {args} "uplevel $command \$args"
} else {
if {[llength [info commands ::tk::$command]] == 1} {
proc $proc {args} "uplevel ::tk::$command \$args"
}
}
}
# end of combobox.tcl