1# Copyright (c) 1998-2003, Bryan Oakley
2# All Rights Reservered
3#
4# Bryan Oakley
5# oakley@bardo.clearlight.com
6#
7# combobox v2.3 August 16, 2003
8#
9# a combobox / dropdown listbox (pick your favorite name) widget
10# written in pure tcl
11#
12# this code is freely distributable without restriction, but is
13# provided as-is with no warranty expressed or implied.
14#
15# thanks to the following people who provided beta test support or
16# patches to the code (in no particular order):
17#
18# Scott Beasley     Alexandre Ferrieux      Todd Helfter
19# Matt Gushee       Laurent Duperval        John Jackson
20# Fred Rapp         Christopher Nelson
21# Eric Galluzzo     Jean-Francois Moine	    Oliver Bienert
22#
23# A special thanks to Martin M. Hunt who provided several good ideas,
24# and always with a patch to implement them. Jean-Francois Moine,
25# Todd Helfter and John Jackson were also kind enough to send in some
26# code patches.
27#
28# ... and many others over the years.
29
30package require Tk 8.0
31package provide combobox 2.3
32
33namespace eval ::combobox {
34
35    # this is the public interface
36    namespace export combobox
37
38    # these contain references to available options
39    variable widgetOptions
40
41    # these contain references to available commands and subcommands
42    variable widgetCommands
43    variable scanCommands
44    variable listCommands
45}
46
47# ::combobox::combobox --
48#
49#     This is the command that gets exported. It creates a new
50#     combobox widget.
51#
52# Arguments:
53#
54#     w        path of new widget to create
55#     args     additional option/value pairs (eg: -background white, etc.)
56#
57# Results:
58#
59#     It creates the widget and sets up all of the default bindings
60#
61# Returns:
62#
63#     The name of the newly create widget
64
65proc ::combobox::combobox {w args} {
66    variable widgetOptions
67    variable widgetCommands
68    variable scanCommands
69    variable listCommands
70
71    # perform a one time initialization
72    if {![info exists widgetOptions]} {
73	Init
74    }
75
76    # build it...
77    eval Build $w $args
78
79    # set some bindings...
80    SetBindings $w
81
82    # and we are done!
83    return $w
84}
85
86
87# ::combobox::Init --
88#
89#     Initialize the namespace variables. This should only be called
90#     once, immediately prior to creating the first instance of the
91#     widget
92#
93# Arguments:
94#
95#    none
96#
97# Results:
98#
99#     All state variables are set to their default values; all of
100#     the option database entries will exist.
101#
102# Returns:
103#
104#     empty string
105
106proc ::combobox::Init {} {
107    variable widgetOptions
108    variable widgetCommands
109    variable scanCommands
110    variable listCommands
111    variable defaultEntryCursor
112
113    array set widgetOptions [list \
114	    -background          {background          Background} \
115	    -bd                  -borderwidth \
116	    -bg                  -background \
117	    -borderwidth         {borderWidth         BorderWidth} \
118	    -buttonbackground    {buttonBackground    Background} \
119	    -command             {command             Command} \
120	    -commandstate        {commandState        State} \
121	    -cursor              {cursor              Cursor} \
122	    -disabledbackground  {disabledBackground  DisabledBackground} \
123	    -disabledforeground  {disabledForeground  DisabledForeground} \
124            -dropdownwidth       {dropdownWidth       DropdownWidth} \
125	    -editable            {editable            Editable} \
126	    -elementborderwidth  {elementBorderWidth  BorderWidth} \
127	    -fg                  -foreground \
128	    -font                {font                Font} \
129	    -foreground          {foreground          Foreground} \
130	    -height              {height              Height} \
131	    -highlightbackground {highlightBackground HighlightBackground} \
132	    -highlightcolor      {highlightColor      HighlightColor} \
133	    -highlightthickness  {highlightThickness  HighlightThickness} \
134	    -image               {image               Image} \
135	    -listvar             {listVariable        Variable} \
136	    -maxheight           {maxHeight           Height} \
137	    -opencommand         {opencommand         Command} \
138	    -relief              {relief              Relief} \
139	    -selectbackground    {selectBackground    Foreground} \
140	    -selectborderwidth   {selectBorderWidth   BorderWidth} \
141	    -selectforeground    {selectForeground    Background} \
142	    -state               {state               State} \
143	    -takefocus           {takeFocus           TakeFocus} \
144	    -textvariable        {textVariable        Variable} \
145	    -value               {value               Value} \
146	    -width               {width               Width} \
147	    -xscrollcommand      {xScrollCommand      ScrollCommand} \
148    ]
149
150
151    set widgetCommands [list \
152	    bbox      cget     configure    curselection \
153	    delete    get      icursor      index        \
154	    insert    list     scan         selection    \
155	    xview     select   toggle       open         \
156            close    subwidget  \
157    ]
158
159    set listCommands [list \
160	    delete       get      \
161            index        insert       size \
162    ]
163
164    set scanCommands [list mark dragto]
165
166    # why check for the Tk package? This lets us be sourced into
167    # an interpreter that doesn't have Tk loaded, such as the slave
168    # interpreter used by pkg_mkIndex. In theory it should have no
169    # side effects when run
170    if {[lsearch -exact [package names] "Tk"] != -1} {
171
172	##################################################################
173	#- this initializes the option database. Kinda gross, but it works
174	#- (I think).
175	##################################################################
176
177	# the image used for the button...
178	if {$::tcl_platform(platform) == "windows"} {
179	    image create bitmap ::combobox::bimage -data {
180		#define down_arrow_width 12
181		#define down_arrow_height 12
182		static char down_arrow_bits[] = {
183		    0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
184		    0xfc,0xf1,0xf8,0xf0,0x70,0xf0,0x20,0xf0,
185		    0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00;
186		}
187	    }
188	} else {
189	    image create bitmap ::combobox::bimage -data  {
190		#define down_arrow_width 15
191		#define down_arrow_height 15
192		static char down_arrow_bits[] = {
193		    0x00,0x80,0x00,0x80,0x00,0x80,0x00,0x80,
194		    0x00,0x80,0xf8,0x8f,0xf0,0x87,0xe0,0x83,
195		    0xc0,0x81,0x80,0x80,0x00,0x80,0x00,0x80,
196		    0x00,0x80,0x00,0x80,0x00,0x80
197		}
198	    }
199	}
200
201	# compute a widget name we can use to create a temporary widget
202	set tmpWidget ".__tmp__"
203	set count 0
204	while {[winfo exists $tmpWidget] == 1} {
205	    set tmpWidget ".__tmp__$count"
206	    incr count
207	}
208
209	# get the scrollbar width. Because we try to be clever and draw our
210	# own button instead of using a tk widget, we need to know what size
211	# button to create. This little hack tells us the width of a scroll
212	# bar.
213	#
214	# NB: we need to be sure and pick a window  that doesn't already
215	# exist...
216	scrollbar $tmpWidget
217	set sb_width [winfo reqwidth $tmpWidget]
218	set bbg [$tmpWidget cget -background]
219	destroy $tmpWidget
220
221	# steal options from the entry widget
222	# we want darn near all options, so we'll go ahead and do
223	# them all. No harm done in adding the one or two that we
224	# don't use.
225	entry $tmpWidget
226	foreach foo [$tmpWidget configure] {
227	    # the cursor option is special, so we'll save it in
228	    # a special way
229	    if {[lindex $foo 0] == "-cursor"} {
230		set defaultEntryCursor [lindex $foo 4]
231	    }
232	    if {[llength $foo] == 5} {
233		set option [lindex $foo 1]
234		set value [lindex $foo 4]
235		option add *Combobox.$option $value widgetDefault
236
237		# these options also apply to the dropdown listbox
238		if {[string compare $option "foreground"] == 0 \
239			|| [string compare $option "background"] == 0 \
240			|| [string compare $option "font"] == 0} {
241		    option add *Combobox*ComboboxListbox.$option $value \
242			    widgetDefault
243		}
244	    }
245	}
246	destroy $tmpWidget
247
248	# these are unique to us...
249	option add *Combobox.elementBorderWidth  1	widgetDefault
250	option add *Combobox.buttonBackground    $bbg	widgetDefault
251	option add *Combobox.dropdownWidth       {}     widgetDefault
252	option add *Combobox.openCommand         {}     widgetDefault
253	option add *Combobox.cursor              {}     widgetDefault
254	option add *Combobox.commandState        normal widgetDefault
255	option add *Combobox.editable            1      widgetDefault
256	option add *Combobox.maxHeight           10     widgetDefault
257	option add *Combobox.height              0
258    }
259
260    # set class bindings
261    SetClassBindings
262}
263
264# ::combobox::SetClassBindings --
265#
266#    Sets up the default bindings for the widget class
267#
268#    this proc exists since it's The Right Thing To Do, but
269#    I haven't had the time to figure out how to do all the
270#    binding stuff on a class level. The main problem is that
271#    the entry widget must have focus for the insertion cursor
272#    to be visible. So, I either have to have the entry widget
273#    have the Combobox bindtag, or do some fancy juggling of
274#    events or some such. What a pain.
275#
276# Arguments:
277#
278#    none
279#
280# Returns:
281#
282#    empty string
283
284proc ::combobox::SetClassBindings {} {
285
286    # make sure we clean up after ourselves...
287    bind Combobox <Destroy> [list ::combobox::DestroyHandler %W]
288
289    # this will (hopefully) close (and lose the grab on) the
290    # listbox if the user clicks anywhere outside of it. Note
291    # that on Windows, you can click on some other app and
292    # the listbox will still be there, because tcl won't see
293    # that button click
294    set this {[::combobox::convert %W -W]}
295    bind Combobox <Any-ButtonPress>   "$this close"
296    bind Combobox <Any-ButtonRelease> "$this close"
297
298    # this helps (but doesn't fully solve) focus issues. The general
299    # idea is, whenever the frame gets focus it gets passed on to
300    # the entry widget
301    bind Combobox <FocusIn> {::combobox::tkTabToWindow \
302				 [::combobox::convert %W -W].entry}
303
304    # this closes the listbox if we get hidden
305    bind Combobox <Unmap> {[::combobox::convert %W -W] close}
306
307    return ""
308}
309
310# ::combobox::SetBindings --
311#
312#    here's where we do most of the binding foo. I think there's probably
313#    a few bindings I ought to add that I just haven't thought
314#    about...
315#
316#    I'm not convinced these are the proper bindings. Ideally all
317#    bindings should be on "Combobox", but because of my juggling of
318#    bindtags I'm not convinced thats what I want to do. But, it all
319#    seems to work, its just not as robust as it could be.
320#
321# Arguments:
322#
323#    w    widget pathname
324#
325# Returns:
326#
327#    empty string
328
329proc ::combobox::SetBindings {w} {
330    upvar ::combobox::${w}::widgets  widgets
331    upvar ::combobox::${w}::options  options
332
333    # juggle the bindtags. The basic idea here is to associate the
334    # widget name with the entry widget, so if a user does a bind
335    # on the combobox it will get handled properly since it is
336    # the entry widget that has keyboard focus.
337    bindtags $widgets(entry) \
338	    [concat $widgets(this) [bindtags $widgets(entry)]]
339
340    bindtags $widgets(button) \
341	    [concat $widgets(this) [bindtags $widgets(button)]]
342
343    # override the default bindings for tab and shift-tab. The
344    # focus procs take a widget as their only parameter and we
345    # want to make sure the right window gets used (for shift-
346    # tab we want it to appear as if the event was generated
347    # on the frame rather than the entry.
348    bind $widgets(entry) <Tab> \
349	    "::combobox::tkTabToWindow \[tk_focusNext $widgets(entry)\]; break"
350    bind $widgets(entry) <Shift-Tab> \
351	    "::combobox::tkTabToWindow \[tk_focusPrev $widgets(this)\]; break"
352
353    # this makes our "button" (which is actually a label)
354    # do the right thing
355    bind $widgets(button) <ButtonPress-1> [list $widgets(this) toggle]
356
357    # this lets the autoscan of the listbox work, even if they
358    # move the cursor over the entry widget.
359    bind $widgets(entry) <B1-Enter> "break"
360
361    bind $widgets(listbox) <ButtonRelease-1> \
362        "::combobox::Select [list $widgets(this)] \
363         \[$widgets(listbox) nearest %y\]; break"
364
365    bind $widgets(vsb) <ButtonPress-1>   {continue}
366    bind $widgets(vsb) <ButtonRelease-1> {continue}
367
368    bind $widgets(listbox) <Any-Motion> {
369	%W selection clear 0 end
370	%W activate @%x,%y
371	%W selection anchor @%x,%y
372	%W selection set @%x,%y @%x,%y
373	# need to do a yview if the cursor goes off the top
374	# or bottom of the window... (or do we?)
375    }
376
377    # these events need to be passed from the entry widget
378    # to the listbox, or otherwise need some sort of special
379    # handling.
380    foreach event [list <Up> <Down> <Tab> <Return> <Escape> \
381	    <Next> <Prior> <Double-1> <1> <Any-KeyPress> \
382	    <FocusIn> <FocusOut>] {
383	bind $widgets(entry) $event \
384            [list ::combobox::HandleEvent $widgets(this) $event]
385    }
386
387    # like the other events, <MouseWheel> needs to be passed from
388    # the entry widget to the listbox. However, in this case we
389    # need to add an additional parameter
390    catch {
391	bind $widgets(entry) <MouseWheel> \
392	    [list ::combobox::HandleEvent $widgets(this) <MouseWheel> %D]
393    }
394}
395
396# ::combobox::Build --
397#
398#    This does all of the work necessary to create the basic
399#    combobox.
400#
401# Arguments:
402#
403#    w        widget name
404#    args     additional option/value pairs
405#
406# Results:
407#
408#    Creates a new widget with the given name. Also creates a new
409#    namespace patterened after the widget name, as a child namespace
410#    to ::combobox
411#
412# Returns:
413#
414#    the name of the widget
415
416proc ::combobox::Build {w args } {
417    variable widgetOptions
418
419    if {[winfo exists $w]} {
420	error "window name \"$w\" already exists"
421    }
422
423    # create the namespace for this instance, and define a few
424    # variables
425    namespace eval ::combobox::$w {
426
427	variable ignoreTrace 0
428	variable oldFocus    {}
429	variable oldGrab     {}
430	variable oldValue    {}
431	variable options
432	variable this
433	variable widgets
434
435	set widgets(foo) foo  ;# coerce into an array
436	set options(foo) foo  ;# coerce into an array
437
438	unset widgets(foo)
439	unset options(foo)
440    }
441
442    # import the widgets and options arrays into this proc so
443    # we don't have to use fully qualified names, which is a
444    # pain.
445    upvar ::combobox::${w}::widgets widgets
446    upvar ::combobox::${w}::options options
447
448    # this is our widget -- a frame of class Combobox. Naturally,
449    # it will contain other widgets. We create it here because
450    # we need it in order to set some default options.
451    set widgets(this)   [frame  $w -class Combobox -takefocus 0]
452    set widgets(entry)  [entry  $w.entry -takefocus 1]
453    set widgets(button) [label  $w.button -takefocus 0]
454
455    # this defines all of the default options. We get the
456    # values from the option database. Note that if an array
457    # value is a list of length one it is an alias to another
458    # option, so we just ignore it
459    foreach name [array names widgetOptions] {
460	if {[llength $widgetOptions($name)] == 1} continue
461
462	set optName  [lindex $widgetOptions($name) 0]
463	set optClass [lindex $widgetOptions($name) 1]
464
465	set value [option get $w $optName $optClass]
466	set options($name) $value
467    }
468
469    # a couple options aren't available in earlier versions of
470    # tcl, so we'll set them to sane values. For that matter, if
471    # they exist but are empty, set them to sane values.
472    if {[string length $options(-disabledforeground)] == 0} {
473        set options(-disabledforeground) $options(-foreground)
474    }
475    if {[string length $options(-disabledbackground)] == 0} {
476        set options(-disabledbackground) $options(-background)
477    }
478
479    # if -value is set to null, we'll remove it from our
480    # local array. The assumption is, if the user sets it from
481    # the option database, they will set it to something other
482    # than null (since it's impossible to determine the difference
483    # between a null value and no value at all).
484    if {[info exists options(-value)] \
485	    && [string length $options(-value)] == 0} {
486	unset options(-value)
487    }
488
489    # we will later rename the frame's widget proc to be our
490    # own custom widget proc. We need to keep track of this
491    # new name, so we'll define and store it here...
492    set widgets(frame) ::combobox::${w}::$w
493
494    # gotta do this sooner or later. Might as well do it now
495    pack $widgets(button) -side right -fill y    -expand no
496    pack $widgets(entry)  -side left  -fill both -expand yes
497
498    # I should probably do this in a catch, but for now it's
499    # good enough... What it does, obviously, is put all of
500    # the option/values pairs into an array. Make them easier
501    # to handle later on...
502    array set options $args
503
504    # now, the dropdown list... the same renaming nonsense
505    # must go on here as well...
506    set widgets(dropdown)   [toplevel  $w.top]
507    set widgets(listbox) [listbox   $w.top.list]
508    set widgets(vsb)     [scrollbar $w.top.vsb]
509
510    pack $widgets(listbox) -side left -fill both -expand y
511
512    # fine tune the widgets based on the options (and a few
513    # arbitrary values...)
514
515    # NB: we are going to use the frame to handle the relief
516    # of the widget as a whole, so the entry widget will be
517    # flat. This makes the button which drops down the list
518    # to appear "inside" the entry widget.
519
520    $widgets(vsb) configure \
521	    -borderwidth 1 \
522	    -command "$widgets(listbox) yview" \
523	    -highlightthickness 0
524
525    $widgets(button) configure \
526	    -background $options(-buttonbackground) \
527	    -highlightthickness 0 \
528	    -borderwidth $options(-elementborderwidth) \
529	    -relief raised \
530	    -width [expr {[winfo reqwidth $widgets(vsb)] - 2}]
531
532    $widgets(entry) configure \
533	    -borderwidth 0 \
534	    -relief flat \
535	    -highlightthickness 0
536
537    $widgets(dropdown) configure \
538	    -borderwidth $options(-elementborderwidth) \
539	    -relief sunken
540
541    $widgets(listbox) configure \
542	    -selectmode browse \
543	    -background [$widgets(entry) cget -bg] \
544	    -yscrollcommand "$widgets(vsb) set" \
545	    -exportselection false \
546	    -borderwidth 0
547
548
549#    trace variable ::combobox::${w}::entryTextVariable w \
550#	    [list ::combobox::EntryTrace $w]
551
552    # do some window management foo on the dropdown window
553    wm overrideredirect $widgets(dropdown) 1
554    wm transient        $widgets(dropdown) [winfo toplevel $w]
555    wm group            $widgets(dropdown) [winfo parent $w]
556    wm resizable        $widgets(dropdown) 0 0
557    wm withdraw         $widgets(dropdown)
558
559    # this moves the original frame widget proc into our
560    # namespace and gives it a handy name
561    rename ::$w $widgets(frame)
562
563    # now, create our widget proc. Obviously (?) it goes in
564    # the global namespace. All combobox widgets will actually
565    # share the same widget proc to cut down on the amount of
566    # bloat.
567    proc ::$w {command args} \
568        "eval ::combobox::WidgetProc $w \$command \$args"
569
570
571    # ok, the thing exists... let's do a bit more configuration.
572    if {[catch "::combobox::Configure [list $widgets(this)] [array get options]" error]} {
573	catch {destroy $w}
574	error "internal error: $error"
575    }
576
577    return ""
578
579}
580
581# ::combobox::HandleEvent --
582#
583#    this proc handles events from the entry widget that we want
584#    handled specially (typically, to allow navigation of the list
585#    even though the focus is in the entry widget)
586#
587# Arguments:
588#
589#    w       widget pathname
590#    event   a string representing the event (not necessarily an
591#            actual event)
592#    args    additional arguments required by particular events
593
594proc ::combobox::HandleEvent {w event args} {
595    upvar ::combobox::${w}::widgets  widgets
596    upvar ::combobox::${w}::options  options
597    upvar ::combobox::${w}::oldValue oldValue
598
599    # for all of these events, if we have a special action we'll
600    # do that and do a "return -code break" to keep additional
601    # bindings from firing. Otherwise we'll let the event fall
602    # on through.
603    switch $event {
604
605        "<MouseWheel>" {
606	    if {[winfo ismapped $widgets(dropdown)]} {
607                set D [lindex $args 0]
608                # the '120' number in the following expression has
609                # it's genesis in the tk bind manpage, which suggests
610                # that the smallest value of %D for mousewheel events
611                # will be 120. The intent is to scroll one line at a time.
612                $widgets(listbox) yview scroll [expr {-($D/120)}] units
613            }
614        }
615
616	"<Any-KeyPress>" {
617	    # if the widget is editable, clear the selection.
618	    # this makes it more obvious what will happen if the
619	    # user presses <Return> (and helps our code know what
620	    # to do if the user presses return)
621	    if {$options(-editable)} {
622		$widgets(listbox) see 0
623		$widgets(listbox) selection clear 0 end
624		$widgets(listbox) selection anchor 0
625		$widgets(listbox) activate 0
626	    }
627	}
628
629	"<FocusIn>" {
630	    set oldValue [$widgets(entry) get]
631	}
632
633	"<FocusOut>" {
634	    if {![winfo ismapped $widgets(dropdown)]} {
635		# did the value change?
636		set newValue [$widgets(entry) get]
637		if {$oldValue != $newValue} {
638		    CallCommand $widgets(this) $newValue
639		}
640	    }
641	}
642
643	"<1>" {
644	    set editable [::combobox::GetBoolean $options(-editable)]
645	    if {!$editable} {
646		if {[winfo ismapped $widgets(dropdown)]} {
647		    $widgets(this) close
648		    return -code break;
649
650		} else {
651		    if {$options(-state) != "disabled"} {
652			$widgets(this) open
653			return -code break;
654		    }
655		}
656	    }
657	}
658
659	"<Double-1>" {
660	    if {$options(-state) != "disabled"} {
661		$widgets(this) toggle
662		return -code break;
663	    }
664	}
665
666	"<Tab>" {
667	    if {[winfo ismapped $widgets(dropdown)]} {
668		::combobox::Find $widgets(this) 0
669		return -code break;
670	    } else {
671		::combobox::SetValue $widgets(this) [$widgets(this) get]
672	    }
673	}
674
675	"<Escape>" {
676#	    $widgets(entry) delete 0 end
677#	    $widgets(entry) insert 0 $oldValue
678	    if {[winfo ismapped $widgets(dropdown)]} {
679		$widgets(this) close
680		return -code break;
681	    }
682	}
683
684	"<Return>" {
685	    # did the value change?
686	    set newValue [$widgets(entry) get]
687	    if {$oldValue != $newValue} {
688		CallCommand $widgets(this) $newValue
689	    }
690
691	    if {[winfo ismapped $widgets(dropdown)]} {
692		::combobox::Select $widgets(this) \
693			[$widgets(listbox) curselection]
694		return -code break;
695	    }
696
697	}
698
699	"<Next>" {
700	    $widgets(listbox) yview scroll 1 pages
701	    set index [$widgets(listbox) index @0,0]
702	    $widgets(listbox) see $index
703	    $widgets(listbox) activate $index
704	    $widgets(listbox) selection clear 0 end
705	    $widgets(listbox) selection anchor $index
706	    $widgets(listbox) selection set $index
707
708	}
709
710	"<Prior>" {
711	    $widgets(listbox) yview scroll -1 pages
712	    set index [$widgets(listbox) index @0,0]
713	    $widgets(listbox) activate $index
714	    $widgets(listbox) see $index
715	    $widgets(listbox) selection clear 0 end
716	    $widgets(listbox) selection anchor $index
717	    $widgets(listbox) selection set $index
718	}
719
720	"<Down>" {
721	    if {[winfo ismapped $widgets(dropdown)]} {
722		::combobox::tkListboxUpDown $widgets(listbox) 1
723		return -code break;
724
725	    } else {
726		if {$options(-state) != "disabled"} {
727		    $widgets(this) open
728		    return -code break;
729		}
730	    }
731	}
732	"<Up>" {
733	    if {[winfo ismapped $widgets(dropdown)]} {
734		::combobox::tkListboxUpDown $widgets(listbox) -1
735		return -code break;
736
737	    } else {
738		if {$options(-state) != "disabled"} {
739		    $widgets(this) open
740		    return -code break;
741		}
742	    }
743	}
744    }
745
746    return ""
747}
748
749# ::combobox::DestroyHandler {w} --
750#
751#    Cleans up after a combobox widget is destroyed
752#
753# Arguments:
754#
755#    w    widget pathname
756#
757# Results:
758#
759#    The namespace that was created for the widget is deleted,
760#    and the widget proc is removed.
761
762proc ::combobox::DestroyHandler {w} {
763
764    catch {
765	# if the widget actually being destroyed is of class Combobox,
766	# remove the namespace and associated proc.
767	if {[string compare [winfo class $w] "Combobox"] == 0} {
768	    # delete the namespace and the proc which represents
769	    # our widget
770	    namespace delete ::combobox::$w
771	    rename $w {}
772	}
773    }
774    return ""
775}
776
777# ::combobox::Find
778#
779#    finds something in the listbox that matches the pattern in the
780#    entry widget and selects it
781#
782#    N.B. I'm not convinced this is working the way it ought to. It
783#    works, but is the behavior what is expected? I've also got a gut
784#    feeling that there's a better way to do this, but I'm too lazy to
785#    figure it out...
786#
787# Arguments:
788#
789#    w      widget pathname
790#    exact  boolean; if true an exact match is desired
791#
792# Returns:
793#
794#    Empty string
795
796proc ::combobox::Find {w {exact 0}} {
797    upvar ::combobox::${w}::widgets widgets
798    upvar ::combobox::${w}::options options
799
800    ## *sigh* this logic is rather gross and convoluted. Surely
801    ## there is a more simple, straight-forward way to implement
802    ## all this. As the saying goes, I lack the time to make it
803    ## shorter...
804
805    # use what is already in the entry widget as a pattern
806    set pattern [$widgets(entry) get]
807
808    if {[string length $pattern] == 0} {
809	# clear the current selection
810	$widgets(listbox) see 0
811	$widgets(listbox) selection clear 0 end
812	$widgets(listbox) selection anchor 0
813	$widgets(listbox) activate 0
814	return
815    }
816
817    # we're going to be searching this list...
818    set list [$widgets(listbox) get 0 end]
819
820    # if we are doing an exact match, try to find,
821    # well, an exact match
822    set exactMatch -1
823    if {$exact} {
824	set exactMatch [lsearch -exact $list $pattern]
825    }
826
827    # search for it. We'll try to be clever and not only
828    # search for a match for what they typed, but a match for
829    # something close to what they typed. We'll keep removing one
830    # character at a time from the pattern until we find a match
831    # of some sort.
832    set index -1
833    while {$index == -1 && [string length $pattern]} {
834	set index [lsearch -glob $list "$pattern*"]
835	if {$index == -1} {
836	    regsub {.$} $pattern {} pattern
837	}
838    }
839
840    # this is the item that most closely matches...
841    set thisItem [lindex $list $index]
842
843    # did we find a match? If so, do some additional munging...
844    if {$index != -1} {
845
846	# we need to find the part of the first item that is
847	# unique WRT the second... I know there's probably a
848	# simpler way to do this...
849
850	set nextIndex [expr {$index + 1}]
851	set nextItem [lindex $list $nextIndex]
852
853	# we don't really need to do much if the next
854	# item doesn't match our pattern...
855	if {[string match $pattern* $nextItem]} {
856	    # ok, the next item matches our pattern, too
857	    # now the trick is to find the first character
858	    # where they *don't* match...
859	    set marker [string length $pattern]
860	    while {$marker <= [string length $pattern]} {
861		set a [string index $thisItem $marker]
862		set b [string index $nextItem $marker]
863		if {[string compare $a $b] == 0} {
864		    append pattern $a
865		    incr marker
866		} else {
867		    break
868		}
869	    }
870	} else {
871	    set marker [string length $pattern]
872	}
873
874    } else {
875	set marker end
876	set index 0
877    }
878
879    # ok, we know the pattern and what part is unique;
880    # update the entry widget and listbox appropriately
881    if {$exact && $exactMatch == -1} {
882	# this means we didn't find an exact match
883	$widgets(listbox) selection clear 0 end
884	$widgets(listbox) see $index
885
886    } elseif {!$exact}  {
887	# this means we found something, but it isn't an exact
888	# match. If we find something that *is* an exact match we
889	# don't need to do the following, since it would merely
890	# be replacing the data in the entry widget with itself
891	set oldstate [$widgets(entry) cget -state]
892	$widgets(entry) configure -state normal
893	$widgets(entry) delete 0 end
894	$widgets(entry) insert end $thisItem
895	$widgets(entry) selection clear
896	$widgets(entry) selection range $marker end
897	$widgets(listbox) activate $index
898	$widgets(listbox) selection clear 0 end
899	$widgets(listbox) selection anchor $index
900	$widgets(listbox) selection set $index
901	$widgets(listbox) see $index
902	$widgets(entry) configure -state $oldstate
903    }
904}
905
906# ::combobox::Select --
907#
908#    selects an item from the list and sets the value of the combobox
909#    to that value
910#
911# Arguments:
912#
913#    w      widget pathname
914#    index  listbox index of item to be selected
915#
916# Returns:
917#
918#    empty string
919
920proc ::combobox::Select {w index} {
921    upvar ::combobox::${w}::widgets widgets
922    upvar ::combobox::${w}::options options
923
924    # the catch is because I'm sloppy -- presumably, the only time
925    # an error will be caught is if there is no selection.
926    if {![catch {set data [$widgets(listbox) get [lindex $index 0]]}]} {
927	::combobox::SetValue $widgets(this) $data
928
929	$widgets(listbox) selection clear 0 end
930	$widgets(listbox) selection anchor $index
931	$widgets(listbox) selection set $index
932
933    }
934    $widgets(entry) selection range 0 end
935    $widgets(entry) icursor end
936
937    $widgets(this) close
938
939    return ""
940}
941
942# ::combobox::HandleScrollbar --
943#
944#    causes the scrollbar of the dropdown list to appear or disappear
945#    based on the contents of the dropdown listbox
946#
947# Arguments:
948#
949#    w       widget pathname
950#    action  the action to perform on the scrollbar
951#
952# Returns:
953#
954#    an empty string
955
956proc ::combobox::HandleScrollbar {w {action "unknown"}} {
957    upvar ::combobox::${w}::widgets widgets
958    upvar ::combobox::${w}::options options
959
960    if {$options(-height) == 0} {
961	set hlimit $options(-maxheight)
962    } else {
963	set hlimit $options(-height)
964    }
965
966    switch $action {
967	"grow" {
968	    if {$hlimit > 0 && [$widgets(listbox) size] > $hlimit} {
969		pack forget $widgets(listbox)
970		pack $widgets(vsb) -side right -fill y -expand n
971		pack $widgets(listbox) -side left -fill both -expand y
972	    }
973	}
974
975	"shrink" {
976	    if {$hlimit > 0 && [$widgets(listbox) size] <= $hlimit} {
977		pack forget $widgets(vsb)
978	    }
979	}
980
981	"crop" {
982	    # this means the window was cropped and we definitely
983	    # need a scrollbar no matter what the user wants
984	    pack forget $widgets(listbox)
985	    pack $widgets(vsb) -side right -fill y -expand n
986	    pack $widgets(listbox) -side left -fill both -expand y
987	}
988
989	default {
990	    if {$hlimit > 0 && [$widgets(listbox) size] > $hlimit} {
991		pack forget $widgets(listbox)
992		pack $widgets(vsb) -side right -fill y -expand n
993		pack $widgets(listbox) -side left -fill both -expand y
994	    } else {
995		pack forget $widgets(vsb)
996	    }
997	}
998    }
999
1000    return ""
1001}
1002
1003# ::combobox::ComputeGeometry --
1004#
1005#    computes the geometry of the dropdown list based on the size of the
1006#    combobox...
1007#
1008# Arguments:
1009#
1010#    w     widget pathname
1011#
1012# Returns:
1013#
1014#    the desired geometry of the listbox
1015
1016proc ::combobox::ComputeGeometry {w} {
1017    upvar ::combobox::${w}::widgets widgets
1018    upvar ::combobox::${w}::options options
1019
1020    if {$options(-height) == 0 && $options(-maxheight) != "0"} {
1021	# if this is the case, count the items and see if
1022	# it exceeds our maxheight. If so, set the listbox
1023	# size to maxheight...
1024	set nitems [$widgets(listbox) size]
1025	if {$nitems > $options(-maxheight)} {
1026	    # tweak the height of the listbox
1027	    $widgets(listbox) configure -height $options(-maxheight)
1028	} else {
1029	    # un-tweak the height of the listbox
1030	    $widgets(listbox) configure -height 0
1031	}
1032	update idletasks
1033    }
1034
1035    # compute height and width of the dropdown list
1036    set bd [$widgets(dropdown) cget -borderwidth]
1037    set height [expr {[winfo reqheight $widgets(dropdown)] + $bd + $bd}]
1038    if {[string length $options(-dropdownwidth)] == 0 ||
1039        $options(-dropdownwidth) == 0} {
1040        set width [winfo width $widgets(this)]
1041    } else {
1042        set m [font measure [$widgets(listbox) cget -font] "m"]
1043        set width [expr {$options(-dropdownwidth) * $m}]
1044    }
1045
1046    # figure out where to place it on the screen, trying to take into
1047    # account we may be running under some virtual window manager
1048    set screenWidth  [winfo screenwidth $widgets(this)]
1049    set screenHeight [winfo screenheight $widgets(this)]
1050    set rootx        [winfo rootx $widgets(this)]
1051    set rooty        [winfo rooty $widgets(this)]
1052    set vrootx       [winfo vrootx $widgets(this)]
1053    set vrooty       [winfo vrooty $widgets(this)]
1054
1055    # the x coordinate is simply the rootx of our widget, adjusted for
1056    # the virtual window. We won't worry about whether the window will
1057    # be offscreen to the left or right -- we want the illusion that it
1058    # is part of the entry widget, so if part of the entry widget is off-
1059    # screen, so will the list. If you want to change the behavior,
1060    # simply change the if statement... (and be sure to update this
1061    # comment!)
1062    set x  [expr {$rootx + $vrootx}]
1063    if {0} {
1064	set rightEdge [expr {$x + $width}]
1065	if {$rightEdge > $screenWidth} {
1066	    set x [expr {$screenWidth - $width}]
1067	}
1068	if {$x < 0} {set x 0}
1069    }
1070
1071    # the y coordinate is the rooty plus vrooty offset plus
1072    # the height of the static part of the widget plus 1 for a
1073    # tiny bit of visual separation...
1074    set y [expr {$rooty + $vrooty + [winfo reqheight $widgets(this)] + 1}]
1075    set bottomEdge [expr {$y + $height}]
1076
1077    if {$bottomEdge >= $screenHeight} {
1078	# ok. Fine. Pop it up above the entry widget isntead of
1079	# below.
1080	set y [expr {($rooty - $height - 1) + $vrooty}]
1081
1082	if {$y < 0} {
1083	    # this means it extends beyond our screen. How annoying.
1084	    # Now we'll try to be real clever and either pop it up or
1085	    # down, depending on which way gives us the biggest list.
1086	    # then, we'll trim the list to fit and force the use of
1087	    # a scrollbar
1088
1089	    # (sadly, for windows users this measurement doesn't
1090	    # take into consideration the height of the taskbar,
1091	    # but don't blame me -- there isn't any way to detect
1092	    # it or figure out its dimensions. The same probably
1093	    # applies to any window manager with some magic windows
1094	    # glued to the top or bottom of the screen)
1095
1096	    if {$rooty > [expr {$screenHeight / 2}]} {
1097		# we are in the lower half of the screen --
1098		# pop it up. Y is zero; that parts easy. The height
1099		# is simply the y coordinate of our widget, minus
1100		# a pixel for some visual separation. The y coordinate
1101		# will be the topof the screen.
1102		set y 1
1103		set height [expr {$rooty - 1 - $y}]
1104
1105	    } else {
1106		# we are in the upper half of the screen --
1107		# pop it down
1108		set y [expr {$rooty + $vrooty + \
1109			[winfo reqheight $widgets(this)] + 1}]
1110		set height [expr {$screenHeight - $y}]
1111
1112	    }
1113
1114	    # force a scrollbar
1115	    HandleScrollbar $widgets(this) crop
1116	}
1117    }
1118
1119    if {$y < 0} {
1120	# hmmm. Bummer.
1121	set y 0
1122	set height $screenheight
1123    }
1124
1125    set geometry [format "=%dx%d+%d+%d" $width $height $x $y]
1126
1127    return $geometry
1128}
1129
1130# ::combobox::DoInternalWidgetCommand --
1131#
1132#    perform an internal widget command, then mung any error results
1133#    to look like it came from our megawidget. A lot of work just to
1134#    give the illusion that our megawidget is an atomic widget
1135#
1136# Arguments:
1137#
1138#    w           widget pathname
1139#    subwidget   pathname of the subwidget
1140#    command     subwidget command to be executed
1141#    args        arguments to the command
1142#
1143# Returns:
1144#
1145#    The result of the subwidget command, or an error
1146
1147proc ::combobox::DoInternalWidgetCommand {w subwidget command args} {
1148    upvar ::combobox::${w}::widgets widgets
1149    upvar ::combobox::${w}::options options
1150
1151    set subcommand $command
1152    set command [concat $widgets($subwidget) $command $args]
1153    if {[catch $command result]} {
1154	# replace the subwidget name with the megawidget name
1155	regsub $widgets($subwidget) $result $widgets(this) result
1156
1157	# replace specific instances of the subwidget command
1158	# with our megawidget command
1159	switch $subwidget,$subcommand {
1160	    listbox,index  {regsub "index"  $result "list index"  result}
1161	    listbox,insert {regsub "insert" $result "list insert" result}
1162	    listbox,delete {regsub "delete" $result "list delete" result}
1163	    listbox,get    {regsub "get"    $result "list get"    result}
1164	    listbox,size   {regsub "size"   $result "list size"   result}
1165	}
1166	error $result
1167
1168    } else {
1169	return $result
1170    }
1171}
1172
1173
1174# ::combobox::WidgetProc --
1175#
1176#    This gets uses as the widgetproc for an combobox widget.
1177#    Notice where the widget is created and you'll see that the
1178#    actual widget proc merely evals this proc with all of the
1179#    arguments intact.
1180#
1181#    Note that some widget commands are defined "inline" (ie:
1182#    within this proc), and some do most of their work in
1183#    separate procs. This is merely because sometimes it was
1184#    easier to do it one way or the other.
1185#
1186# Arguments:
1187#
1188#    w         widget pathname
1189#    command   widget subcommand
1190#    args      additional arguments; varies with the subcommand
1191#
1192# Results:
1193#
1194#    Performs the requested widget command
1195
1196proc ::combobox::WidgetProc {w command args} {
1197    upvar ::combobox::${w}::widgets widgets
1198    upvar ::combobox::${w}::options options
1199    upvar ::combobox::${w}::oldFocus oldFocus
1200    upvar ::combobox::${w}::oldFocus oldGrab
1201
1202    set command [::combobox::Canonize $w command $command]
1203
1204    # this is just shorthand notation...
1205    set doWidgetCommand \
1206	    [list ::combobox::DoInternalWidgetCommand $widgets(this)]
1207
1208    if {$command == "list"} {
1209	# ok, the next argument is a list command; we'll
1210	# rip it from args and append it to command to
1211	# create a unique internal command
1212	#
1213	# NB: because of the sloppy way we are doing this,
1214	# we'll also let the user enter our secret command
1215	# directly (eg: listinsert, listdelete), but we
1216	# won't document that fact
1217	set command "list-[lindex $args 0]"
1218	set args [lrange $args 1 end]
1219    }
1220
1221    set result ""
1222
1223    # many of these commands are just synonyms for specific
1224    # commands in one of the subwidgets. We'll get them out
1225    # of the way first, then do the custom commands.
1226    switch $command {
1227	bbox -
1228	delete -
1229	get -
1230	icursor -
1231	index -
1232	insert -
1233	scan -
1234	selection -
1235	xview {
1236	    set result [eval $doWidgetCommand entry $command $args]
1237	}
1238	list-get 	{set result [eval $doWidgetCommand listbox get $args]}
1239	list-index 	{set result [eval $doWidgetCommand listbox index $args]}
1240	list-size 	{set result [eval $doWidgetCommand listbox size $args]}
1241
1242	select {
1243	    if {[llength $args] == 1} {
1244		set index [lindex $args 0]
1245		set result [Select $widgets(this) $index]
1246	    } else {
1247		error "usage: $w select index"
1248	    }
1249	}
1250
1251	subwidget {
1252	    set knownWidgets [list button entry listbox dropdown vsb]
1253	    if {[llength $args] == 0} {
1254		return $knownWidgets
1255	    }
1256
1257	    set name [lindex $args 0]
1258	    if {[lsearch $knownWidgets $name] != -1} {
1259		set result $widgets($name)
1260	    } else {
1261		error "unknown subwidget $name"
1262	    }
1263	}
1264
1265	curselection {
1266	    set result [eval $doWidgetCommand listbox curselection]
1267	}
1268
1269	list-insert {
1270	    eval $doWidgetCommand listbox insert $args
1271	    set result [HandleScrollbar $w "grow"]
1272	}
1273
1274	list-delete {
1275	    eval $doWidgetCommand listbox delete $args
1276	    set result [HandleScrollbar $w "shrink"]
1277	}
1278
1279	toggle {
1280	    # ignore this command if the widget is disabled...
1281	    if {$options(-state) == "disabled"} return
1282
1283	    # pops down the list if it is not, hides it
1284	    # if it is...
1285	    if {[winfo ismapped $widgets(dropdown)]} {
1286		set result [$widgets(this) close]
1287	    } else {
1288		set result [$widgets(this) open]
1289	    }
1290	}
1291
1292	open {
1293
1294	    # if this is an editable combobox, the focus should
1295	    # be set to the entry widget
1296	    if {$options(-editable)} {
1297		focus $widgets(entry)
1298		$widgets(entry) select range 0 end
1299		$widgets(entry) icursor end
1300	    }
1301
1302	    # if we are disabled, we won't allow this to happen
1303	    if {$options(-state) == "disabled"} {
1304		return 0
1305	    }
1306
1307	    # if there is a -opencommand, execute it now
1308	    if {[string length $options(-opencommand)] > 0} {
1309		# hmmm... should I do a catch, or just let the normal
1310		# error handling handle any errors? For now, the latter...
1311		uplevel \#0 $options(-opencommand)
1312	    }
1313
1314	    # compute the geometry of the window to pop up, and set
1315	    # it, and force the window manager to take notice
1316	    # (even if it is not presently visible).
1317	    #
1318	    # this isn't strictly necessary if the window is already
1319	    # mapped, but we'll go ahead and set the geometry here
1320	    # since its harmless and *may* actually reset the geometry
1321	    # to something better in some weird case.
1322	    set geometry [::combobox::ComputeGeometry $widgets(this)]
1323	    wm geometry $widgets(dropdown) $geometry
1324	    update idletasks
1325
1326	    # if we are already open, there's nothing else to do
1327	    if {[winfo ismapped $widgets(dropdown)]} {
1328		return 0
1329	    }
1330
1331	    # save the widget that currently has the focus; we'll restore
1332	    # the focus there when we're done
1333	    set oldFocus [focus]
1334
1335	    # ok, tweak the visual appearance of things and
1336	    # make the list pop up
1337	    $widgets(button) configure -relief sunken
1338	    wm deiconify $widgets(dropdown)
1339	    update idletasks
1340	    raise $widgets(dropdown)
1341
1342	    # force focus to the entry widget so we can handle keypress
1343	    # events for traversal
1344	    focus -force $widgets(entry)
1345
1346	    # select something by default, but only if its an
1347	    # exact match...
1348	    ::combobox::Find $widgets(this) 1
1349
1350	    # save the current grab state for the display containing
1351	    # this widget. We'll restore it when we close the dropdown
1352	    # list
1353	    set status "none"
1354	    set grab [grab current $widgets(this)]
1355	    if {$grab != ""} {set status [grab status $grab]}
1356	    set oldGrab [list $grab $status]
1357	    unset grab status
1358
1359	    # *gasp* do a global grab!!! Mom always told me not to
1360	    # do things like this, but sometimes a man's gotta do
1361	    # what a man's gotta do.
1362	    grab -global $widgets(this)
1363
1364	    # fake the listbox into thinking it has focus. This is
1365	    # necessary to get scanning initialized properly in the
1366	    # listbox.
1367	    event generate $widgets(listbox) <B1-Enter>
1368
1369	    return 1
1370	}
1371
1372	close {
1373	    # if we are already closed, don't do anything...
1374	    if {![winfo ismapped $widgets(dropdown)]} {
1375		return 0
1376	    }
1377
1378	    # restore the focus and grab, but ignore any errors...
1379	    # we're going to be paranoid and release the grab before
1380	    # trying to set any other grab because we really really
1381	    # really want to make sure the grab is released.
1382	    catch {focus $oldFocus} result
1383	    catch {grab release $widgets(this)}
1384	    catch {
1385		set status [lindex $oldGrab 1]
1386		if {$status == "global"} {
1387		    grab -global [lindex $oldGrab 0]
1388		} elseif {$status == "local"} {
1389		    grab [lindex $oldGrab 0]
1390		}
1391		unset status
1392	    }
1393
1394	    # hides the listbox
1395	    $widgets(button) configure -relief raised
1396	    wm withdraw $widgets(dropdown)
1397
1398	    # select the data in the entry widget. Not sure
1399	    # why, other than observation seems to suggest that's
1400	    # what windows widgets do.
1401	    set editable [::combobox::GetBoolean $options(-editable)]
1402	    if {$editable} {
1403		$widgets(entry) selection range 0 end
1404		$widgets(button) configure -relief raised
1405	    }
1406
1407
1408	    # magic tcl stuff (see tk.tcl in the distribution
1409	    # lib directory)
1410	    ::combobox::tkCancelRepeat
1411
1412	    return 1
1413	}
1414
1415	cget {
1416	    if {[llength $args] != 1} {
1417		error "wrong # args: should be $w cget option"
1418	    }
1419	    set opt [::combobox::Canonize $w option [lindex $args 0]]
1420
1421	    if {$opt == "-value"} {
1422		set result [$widgets(entry) get]
1423	    } else {
1424		set result $options($opt)
1425	    }
1426	}
1427
1428	configure {
1429	    set result [eval ::combobox::Configure {$w} $args]
1430	}
1431
1432	default {
1433	    error "bad option \"$command\""
1434	}
1435    }
1436
1437    return $result
1438}
1439
1440# ::combobox::Configure --
1441#
1442#    Implements the "configure" widget subcommand
1443#
1444# Arguments:
1445#
1446#    w      widget pathname
1447#    args   zero or more option/value pairs (or a single option)
1448#
1449# Results:
1450#
1451#    Performs typcial "configure" type requests on the widget
1452
1453proc ::combobox::Configure {w args} {
1454    variable widgetOptions
1455    variable defaultEntryCursor
1456
1457    upvar ::combobox::${w}::widgets widgets
1458    upvar ::combobox::${w}::options options
1459
1460    if {[llength $args] == 0} {
1461	# hmmm. User must be wanting all configuration information
1462	# note that if the value of an array element is of length
1463	# one it is an alias, which needs to be handled slightly
1464	# differently
1465	set results {}
1466	foreach opt [lsort [array names widgetOptions]] {
1467	    if {[llength $widgetOptions($opt)] == 1} {
1468		set alias $widgetOptions($opt)
1469		set optName $widgetOptions($alias)
1470		lappend results [list $opt $optName]
1471	    } else {
1472		set optName  [lindex $widgetOptions($opt) 0]
1473		set optClass [lindex $widgetOptions($opt) 1]
1474		set default [option get $w $optName $optClass]
1475		if {[info exists options($opt)]} {
1476		    lappend results [list $opt $optName $optClass \
1477			    $default $options($opt)]
1478		} else {
1479		    lappend results [list $opt $optName $optClass \
1480			    $default ""]
1481		}
1482	    }
1483	}
1484
1485	return $results
1486    }
1487
1488    # one argument means we are looking for configuration
1489    # information on a single option
1490    if {[llength $args] == 1} {
1491	set opt [::combobox::Canonize $w option [lindex $args 0]]
1492
1493	set optName  [lindex $widgetOptions($opt) 0]
1494	set optClass [lindex $widgetOptions($opt) 1]
1495	set default [option get $w $optName $optClass]
1496	set results [list $opt $optName $optClass \
1497		$default $options($opt)]
1498	return $results
1499    }
1500
1501    # if we have an odd number of values, bail.
1502    if {[expr {[llength $args]%2}] == 1} {
1503	# hmmm. An odd number of elements in args
1504	error "value for \"[lindex $args end]\" missing"
1505    }
1506
1507    # Great. An even number of options. Let's make sure they
1508    # are all valid before we do anything. Note that Canonize
1509    # will generate an error if it finds a bogus option; otherwise
1510    # it returns the canonical option name
1511    foreach {name value} $args {
1512	set name [::combobox::Canonize $w option $name]
1513	set opts($name) $value
1514    }
1515
1516    # process all of the configuration options
1517    # some (actually, most) options require us to
1518    # do something, like change the attributes of
1519    # a widget or two. Here's where we do that...
1520    #
1521    # note that the handling of disabledforeground and
1522    # disabledbackground is a little wonky. First, we have
1523    # to deal with backwards compatibility (ie: tk 8.3 and below
1524    # didn't have such options for the entry widget), and
1525    # we have to deal with the fact we might want to disable
1526    # the entry widget but use the normal foreground/background
1527    # for when the combobox is not disabled, but not editable either.
1528
1529    set updateVisual 0
1530    foreach option [array names opts] {
1531	set newValue $opts($option)
1532	if {[info exists options($option)]} {
1533	    set oldValue $options($option)
1534	}
1535
1536	switch -- $option {
1537	    -buttonbackground {
1538		$widgets(button) configure -background $newValue
1539	    }
1540	    -background {
1541		set updateVisual 1
1542		set options($option) $newValue
1543	    }
1544
1545	    -borderwidth {
1546		$widgets(frame) configure -borderwidth $newValue
1547		set options($option) $newValue
1548	    }
1549
1550	    -command {
1551		# nothing else to do...
1552		set options($option) $newValue
1553	    }
1554
1555	    -commandstate {
1556		# do some value checking...
1557		if {$newValue != "normal" && $newValue != "disabled"} {
1558		    set options($option) $oldValue
1559		    set message "bad state value \"$newValue\";"
1560		    append message " must be normal or disabled"
1561		    error $message
1562		}
1563		set options($option) $newValue
1564	    }
1565
1566	    -cursor {
1567		$widgets(frame) configure -cursor $newValue
1568		$widgets(entry) configure -cursor $newValue
1569		$widgets(listbox) configure -cursor $newValue
1570		set options($option) $newValue
1571	    }
1572
1573	    -disabledforeground {
1574		set updateVisual 1
1575		set options($option) $newValue
1576	    }
1577
1578	    -disabledbackground {
1579		set updateVisual 1
1580		set options($option) $newValue
1581	    }
1582
1583            -dropdownwidth {
1584                set options($option) $newValue
1585            }
1586
1587	    -editable {
1588		set updateVisual 1
1589 		if {$newValue} {
1590 		    # it's editable...
1591 		    $widgets(entry) configure \
1592 			    -state normal \
1593 			    -cursor $defaultEntryCursor
1594 		} else {
1595 		    $widgets(entry) configure \
1596 			    -state disabled \
1597 			    -cursor $options(-cursor)
1598 		}
1599		set options($option) $newValue
1600	    }
1601
1602	    -elementborderwidth {
1603		$widgets(button) configure -borderwidth $newValue
1604		$widgets(vsb) configure -borderwidth $newValue
1605		$widgets(dropdown) configure -borderwidth $newValue
1606		set options($option) $newValue
1607	    }
1608
1609	    -font {
1610		$widgets(entry) configure -font $newValue
1611		$widgets(listbox) configure -font $newValue
1612		set options($option) $newValue
1613	    }
1614
1615	    -foreground {
1616		set updateVisual 1
1617		set options($option) $newValue
1618	    }
1619
1620	    -height {
1621		$widgets(listbox) configure -height $newValue
1622		HandleScrollbar $w
1623		set options($option) $newValue
1624	    }
1625
1626	    -highlightbackground {
1627		$widgets(frame) configure -highlightbackground $newValue
1628		set options($option) $newValue
1629	    }
1630
1631	    -highlightcolor {
1632		$widgets(frame) configure -highlightcolor $newValue
1633		set options($option) $newValue
1634	    }
1635
1636	    -highlightthickness {
1637		$widgets(frame) configure -highlightthickness $newValue
1638		set options($option) $newValue
1639	    }
1640
1641	    -image {
1642		if {[string length $newValue] > 0} {
1643		    puts "old button width: [$widgets(button) cget -width]"
1644		    $widgets(button) configure \
1645			-image $newValue \
1646			-width [expr {[image width $newValue] + 2}]
1647		    puts "new button width: [$widgets(button) cget -width]"
1648
1649		} else {
1650		    $widgets(button) configure -image ::combobox::bimage
1651		}
1652		set options($option) $newValue
1653	    }
1654
1655	    -listvar {
1656		if {[catch {$widgets(listbox) cget -listvar}]} {
1657		    return -code error \
1658			"-listvar not supported with this version of tk"
1659		}
1660		$widgets(listbox) configure -listvar $newValue
1661		set options($option) $newValue
1662	    }
1663
1664	    -maxheight {
1665		# ComputeGeometry may dork with the actual height
1666		# of the listbox, so let's undork it
1667		$widgets(listbox) configure -height $options(-height)
1668		HandleScrollbar $w
1669		set options($option) $newValue
1670	    }
1671
1672	    -opencommand {
1673		# nothing else to do...
1674		set options($option) $newValue
1675	    }
1676
1677	    -relief {
1678		$widgets(frame) configure -relief $newValue
1679		set options($option) $newValue
1680	    }
1681
1682	    -selectbackground {
1683		$widgets(entry) configure -selectbackground $newValue
1684		$widgets(listbox) configure -selectbackground $newValue
1685		set options($option) $newValue
1686	    }
1687
1688	    -selectborderwidth {
1689		$widgets(entry) configure -selectborderwidth $newValue
1690		$widgets(listbox) configure -selectborderwidth $newValue
1691		set options($option) $newValue
1692	    }
1693
1694	    -selectforeground {
1695		$widgets(entry) configure -selectforeground $newValue
1696		$widgets(listbox) configure -selectforeground $newValue
1697		set options($option) $newValue
1698	    }
1699
1700	    -state {
1701		if {$newValue == "normal"} {
1702		    set updateVisual 1
1703		    # it's enabled
1704
1705		    set editable [::combobox::GetBoolean \
1706			    $options(-editable)]
1707		    if {$editable} {
1708			$widgets(entry) configure -state normal
1709			$widgets(entry) configure -takefocus 1
1710		    }
1711
1712                    # note that $widgets(button) is actually a label,
1713                    # not a button. And being able to disable labels
1714                    # wasn't possible until tk 8.3. (makes me wonder
1715		    # why I chose to use a label, but that answer is
1716		    # lost to antiquity)
1717                    if {[info patchlevel] >= 8.3} {
1718                        $widgets(button) configure -state normal
1719                    }
1720
1721		} elseif {$newValue == "disabled"}  {
1722		    set updateVisual 1
1723		    # it's disabled
1724		    $widgets(entry) configure -state disabled
1725		    $widgets(entry) configure -takefocus 0
1726                    # note that $widgets(button) is actually a label,
1727                    # not a button. And being able to disable labels
1728                    # wasn't possible until tk 8.3. (makes me wonder
1729		    # why I chose to use a label, but that answer is
1730		    # lost to antiquity)
1731                    if {$::tcl_version >= 8.3} {
1732                        $widgets(button) configure -state disabled
1733                    }
1734
1735		} else {
1736		    set options($option) $oldValue
1737		    set message "bad state value \"$newValue\";"
1738		    append message " must be normal or disabled"
1739		    error $message
1740		}
1741
1742		set options($option) $newValue
1743	    }
1744
1745	    -takefocus {
1746		$widgets(entry) configure -takefocus $newValue
1747		set options($option) $newValue
1748	    }
1749
1750	    -textvariable {
1751		$widgets(entry) configure -textvariable $newValue
1752		set options($option) $newValue
1753	    }
1754
1755	    -value {
1756		::combobox::SetValue $widgets(this) $newValue
1757		set options($option) $newValue
1758	    }
1759
1760	    -width {
1761		$widgets(entry) configure -width $newValue
1762		$widgets(listbox) configure -width $newValue
1763		set options($option) $newValue
1764	    }
1765
1766	    -xscrollcommand {
1767		$widgets(entry) configure -xscrollcommand $newValue
1768		set options($option) $newValue
1769	    }
1770	}
1771
1772	if {$updateVisual} {UpdateVisualAttributes $w}
1773    }
1774}
1775
1776# ::combobox::UpdateVisualAttributes --
1777#
1778# sets the visual attributes (foreground, background mostly)
1779# based on the current state of the widget (normal/disabled,
1780# editable/non-editable)
1781#
1782# why a proc for such a simple thing? Well, in addition to the
1783# various states of the widget, we also have to consider the
1784# version of tk being used -- versions from 8.4 and beyond have
1785# the notion of disabled foreground/background options for various
1786# widgets. All of the permutations can get nasty, so we encapsulate
1787# it all in one spot.
1788#
1789# note also that we don't handle all visual attributes here; just
1790# the ones that depend on the state of the widget. The rest are
1791# handled on a case by case basis
1792#
1793# Arguments:
1794#    w		widget pathname
1795#
1796# Returns:
1797#    empty string
1798
1799proc ::combobox::UpdateVisualAttributes {w} {
1800
1801    upvar ::combobox::${w}::widgets     widgets
1802    upvar ::combobox::${w}::options     options
1803
1804    if {$options(-state) == "normal"} {
1805
1806	set foreground $options(-foreground)
1807	set background $options(-background)
1808
1809    } elseif {$options(-state) == "disabled"} {
1810
1811	set foreground $options(-disabledforeground)
1812	set background $options(-disabledbackground)
1813    }
1814
1815    $widgets(entry)   configure -foreground $foreground -background $background
1816    $widgets(listbox) configure -foreground $foreground -background $background
1817    $widgets(button)  configure -foreground $foreground
1818    $widgets(vsb)     configure -background $background -troughcolor $background
1819    $widgets(frame)   configure -background $background
1820
1821    # we need to set the disabled colors in case our widget is disabled.
1822    # We could actually check for disabled-ness, but we also need to
1823    # check whether we're enabled but not editable, in which case the
1824    # entry widget is disabled but we still want the enabled colors. It's
1825    # easier just to set everything and be done with it.
1826
1827    if {$::tcl_version >= 8.4} {
1828	$widgets(entry) configure \
1829	    -disabledforeground $foreground \
1830	    -disabledbackground $background
1831	$widgets(button)  configure -disabledforeground $foreground
1832	$widgets(listbox) configure -disabledforeground $foreground
1833    }
1834}
1835
1836# ::combobox::SetValue --
1837#
1838#    sets the value of the combobox and calls the -command,
1839#    if defined
1840#
1841# Arguments:
1842#
1843#    w          widget pathname
1844#    newValue   the new value of the combobox
1845#
1846# Returns
1847#
1848#    Empty string
1849
1850proc ::combobox::SetValue {w newValue} {
1851
1852    upvar ::combobox::${w}::widgets     widgets
1853    upvar ::combobox::${w}::options     options
1854    upvar ::combobox::${w}::ignoreTrace ignoreTrace
1855    upvar ::combobox::${w}::oldValue    oldValue
1856
1857    if {[info exists options(-textvariable)] \
1858	    && [string length $options(-textvariable)] > 0} {
1859	set variable ::$options(-textvariable)
1860	set $variable $newValue
1861    } else {
1862	set oldstate [$widgets(entry) cget -state]
1863	$widgets(entry) configure -state normal
1864	$widgets(entry) delete 0 end
1865	$widgets(entry) insert 0 $newValue
1866	$widgets(entry) configure -state $oldstate
1867    }
1868
1869    # set our internal textvariable; this will cause any public
1870    # textvariable (ie: defined by the user) to be updated as
1871    # well
1872#    set ::combobox::${w}::entryTextVariable $newValue
1873
1874    # redefine our concept of the "old value". Do it before running
1875    # any associated command so we can be sure it happens even
1876    # if the command somehow fails.
1877    set oldValue $newValue
1878
1879
1880    # call the associated command. The proc will handle whether or
1881    # not to actually call it, and with what args
1882    CallCommand $w $newValue
1883
1884    return ""
1885}
1886
1887# ::combobox::CallCommand --
1888#
1889#   calls the associated command, if any, appending the new
1890#   value to the command to be called.
1891#
1892# Arguments:
1893#
1894#    w         widget pathname
1895#    newValue  the new value of the combobox
1896#
1897# Returns
1898#
1899#    empty string
1900
1901proc ::combobox::CallCommand {w newValue} {
1902    upvar ::combobox::${w}::widgets widgets
1903    upvar ::combobox::${w}::options options
1904
1905    # call the associated command, if defined and -commandstate is
1906    # set to "normal"
1907    if {$options(-commandstate) == "normal" && \
1908	    [string length $options(-command)] > 0} {
1909	set args [list $widgets(this) $newValue]
1910	uplevel \#0 $options(-command) $args
1911    }
1912}
1913
1914
1915# ::combobox::GetBoolean --
1916#
1917#     returns the value of a (presumably) boolean string (ie: it should
1918#     do the right thing if the string is "yes", "no", "true", 1, etc
1919#
1920# Arguments:
1921#
1922#     value       value to be converted
1923#     errorValue  a default value to be returned in case of an error
1924#
1925# Returns:
1926#
1927#     a 1 or zero, or the value of errorValue if the string isn't
1928#     a proper boolean value
1929
1930proc ::combobox::GetBoolean {value {errorValue 1}} {
1931    if {[catch {expr {([string trim $value])?1:0}} res]} {
1932	return $errorValue
1933    } else {
1934	return $res
1935    }
1936}
1937
1938# ::combobox::convert --
1939#
1940#     public routine to convert %x, %y and %W binding substitutions.
1941#     Given an x, y and or %W value relative to a given widget, this
1942#     routine will convert the values to be relative to the combobox
1943#     widget. For example, it could be used in a binding like this:
1944#
1945#     bind .combobox <blah> {doSomething [::combobox::convert %W -x %x]}
1946#
1947#     Note that this procedure is *not* exported, but is intended for
1948#     public use. It is not exported because the name could easily
1949#     clash with existing commands.
1950#
1951# Arguments:
1952#
1953#     w     a widget path; typically the actual result of a %W
1954#           substitution in a binding. It should be either a
1955#           combobox widget or one of its subwidgets
1956#
1957#     args  should one or more of the following arguments or
1958#           pairs of arguments:
1959#
1960#           -x <x>      will convert the value <x>; typically <x> will
1961#                       be the result of a %x substitution
1962#           -y <y>      will convert the value <y>; typically <y> will
1963#                       be the result of a %y substitution
1964#           -W (or -w)  will return the name of the combobox widget
1965#                       which is the parent of $w
1966#
1967# Returns:
1968#
1969#     a list of the requested values. For example, a single -w will
1970#     result in a list of one items, the name of the combobox widget.
1971#     Supplying "-x 10 -y 20 -W" (in any order) will return a list of
1972#     three values: the converted x and y values, and the name of
1973#     the combobox widget.
1974
1975proc ::combobox::convert {w args} {
1976    set result {}
1977    if {![winfo exists $w]} {
1978	error "window \"$w\" doesn't exist"
1979    }
1980
1981    while {[llength $args] > 0} {
1982	set option [lindex $args 0]
1983	set args [lrange $args 1 end]
1984
1985	switch -exact -- $option {
1986	    -x {
1987		set value [lindex $args 0]
1988		set args [lrange $args 1 end]
1989		set win $w
1990		while {[winfo class $win] != "Combobox"} {
1991		    incr value [winfo x $win]
1992		    set win [winfo parent $win]
1993		    if {$win == "."} break
1994		}
1995		lappend result $value
1996	    }
1997
1998	    -y {
1999		set value [lindex $args 0]
2000		set args [lrange $args 1 end]
2001		set win $w
2002		while {[winfo class $win] != "Combobox"} {
2003		    incr value [winfo y $win]
2004		    set win [winfo parent $win]
2005		    if {$win == "."} break
2006		}
2007		lappend result $value
2008	    }
2009
2010	    -w -
2011	    -W {
2012		set win $w
2013		while {[winfo class $win] != "Combobox"} {
2014		    set win [winfo parent $win]
2015		    if {$win == "."} break;
2016		}
2017		lappend result $win
2018	    }
2019	}
2020    }
2021    return $result
2022}
2023
2024# ::combobox::Canonize --
2025#
2026#    takes a (possibly abbreviated) option or command name and either
2027#    returns the canonical name or an error
2028#
2029# Arguments:
2030#
2031#    w        widget pathname
2032#    object   type of object to canonize; must be one of "command",
2033#             "option", "scan command" or "list command"
2034#    opt      the option (or command) to be canonized
2035#
2036# Returns:
2037#
2038#    Returns either the canonical form of an option or command,
2039#    or raises an error if the option or command is unknown or
2040#    ambiguous.
2041
2042proc ::combobox::Canonize {w object opt} {
2043    variable widgetOptions
2044    variable columnOptions
2045    variable widgetCommands
2046    variable listCommands
2047    variable scanCommands
2048
2049    switch $object {
2050	command {
2051	    if {[lsearch -exact $widgetCommands $opt] >= 0} {
2052		return $opt
2053	    }
2054
2055	    # command names aren't stored in an array, and there
2056	    # isn't a way to get all the matches in a list, so
2057	    # we'll stuff the commands in a temporary array so
2058	    # we can use [array names]
2059	    set list $widgetCommands
2060	    foreach element $list {
2061		set tmp($element) ""
2062	    }
2063	    set matches [array names tmp ${opt}*]
2064	}
2065
2066	{list command} {
2067	    if {[lsearch -exact $listCommands $opt] >= 0} {
2068		return $opt
2069	    }
2070
2071	    # command names aren't stored in an array, and there
2072	    # isn't a way to get all the matches in a list, so
2073	    # we'll stuff the commands in a temporary array so
2074	    # we can use [array names]
2075	    set list $listCommands
2076	    foreach element $list {
2077		set tmp($element) ""
2078	    }
2079	    set matches [array names tmp ${opt}*]
2080	}
2081
2082	{scan command} {
2083	    if {[lsearch -exact $scanCommands $opt] >= 0} {
2084		return $opt
2085	    }
2086
2087	    # command names aren't stored in an array, and there
2088	    # isn't a way to get all the matches in a list, so
2089	    # we'll stuff the commands in a temporary array so
2090	    # we can use [array names]
2091	    set list $scanCommands
2092	    foreach element $list {
2093		set tmp($element) ""
2094	    }
2095	    set matches [array names tmp ${opt}*]
2096	}
2097
2098	option {
2099	    if {[info exists widgetOptions($opt)] \
2100		    && [llength $widgetOptions($opt)] == 2} {
2101		return $opt
2102	    }
2103	    set list [array names widgetOptions]
2104	    set matches [array names widgetOptions ${opt}*]
2105	}
2106
2107    }
2108
2109    if {[llength $matches] == 0} {
2110	set choices [HumanizeList $list]
2111	error "unknown $object \"$opt\"; must be one of $choices"
2112
2113    } elseif {[llength $matches] == 1} {
2114	set opt [lindex $matches 0]
2115
2116	# deal with option aliases
2117	switch $object {
2118	    option {
2119		set opt [lindex $matches 0]
2120		if {[llength $widgetOptions($opt)] == 1} {
2121		    set opt $widgetOptions($opt)
2122		}
2123	    }
2124	}
2125
2126	return $opt
2127
2128    } else {
2129	set choices [HumanizeList $list]
2130	error "ambiguous $object \"$opt\"; must be one of $choices"
2131    }
2132}
2133
2134# ::combobox::HumanizeList --
2135#
2136#    Returns a human-readable form of a list by separating items
2137#    by columns, but separating the last two elements with "or"
2138#    (eg: foo, bar or baz)
2139#
2140# Arguments:
2141#
2142#    list    a valid tcl list
2143#
2144# Results:
2145#
2146#    A string which as all of the elements joined with ", " or
2147#    the word " or "
2148
2149proc ::combobox::HumanizeList {list} {
2150
2151    if {[llength $list] == 1} {
2152	return [lindex $list 0]
2153    } else {
2154	set list [lsort $list]
2155	set secondToLast [expr {[llength $list] -2}]
2156	set most [lrange $list 0 $secondToLast]
2157	set last [lindex $list end]
2158
2159	return "[join $most {, }] or $last"
2160    }
2161}
2162
2163# This is some backwards-compatibility code to handle TIP 44
2164# (http://purl.org/tcl/tip/44.html). For all private tk commands
2165# used by this widget, we'll make duplicates of the procs in the
2166# combobox namespace.
2167#
2168# I'm not entirely convinced this is the right thing to do. I probably
2169# shouldn't even be using the private commands. Then again, maybe the
2170# private commands really should be public. Oh well; it works so it
2171# must be OK...
2172foreach command {TabToWindow CancelRepeat ListboxUpDown} {
2173    if {[llength [info commands ::combobox::tk$command]] == 1} break;
2174
2175    set tmp [info commands tk$command]
2176    set proc ::combobox::tk$command
2177    if {[llength [info commands tk$command]] == 1} {
2178        set command [namespace which [lindex $tmp 0]]
2179        proc $proc {args} "uplevel $command \$args"
2180    } else {
2181        if {[llength [info commands ::tk::$command]] == 1} {
2182            proc $proc {args} "uplevel ::tk::$command \$args"
2183        }
2184    }
2185}
2186
2187# end of combobox.tcl
2188
2189