diff options
Diffstat (limited to 'modules/tclsci/tcl/utils')
-rwxr-xr-x | modules/tclsci/tcl/utils/Balloon.tcl | 87 | ||||
-rwxr-xr-x | modules/tclsci/tcl/utils/Combobox.tcl | 2203 | ||||
-rwxr-xr-x | modules/tclsci/tcl/utils/Notebook.tcl | 279 |
3 files changed, 2569 insertions, 0 deletions
diff --git a/modules/tclsci/tcl/utils/Balloon.tcl b/modules/tclsci/tcl/utils/Balloon.tcl new file mode 100755 index 000000000..c941f181d --- /dev/null +++ b/modules/tclsci/tcl/utils/Balloon.tcl @@ -0,0 +1,87 @@ +############################################################################## +# $Id: Balloon.tcl,v 1.1 2004/06/28 15:01:00 leray Exp $ +# +# balloon.tcl - procedures used by balloon help +# +# Copyright (C) 1996-1997 Stewart Allen +# +# This is part of vtcl source code Adapted for +# general purpose by Daniel Roche <dan@lectra.com> +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +############################################################################## +# + +bind Bulle <Enter> { + set Bulle(set) 0 + set Bulle(first) 1 + set Bulle(id) [after 500 {balloon %W $Bulle(%W) %X %Y}] +} + +bind Bulle <Button> { + set Bulle(first) 0 + kill_balloon +} + +bind Bulle <Leave> { + set Bulle(first) 0 + kill_balloon +} + +bind Bulle <Motion> { + if {$Bulle(set) == 0} { + after cancel $Bulle(id) + set Bulle(id) [after 500 {balloon %W $Bulle(%W) %X %Y}] + } +} + +proc set_balloon {target message} { + global Bulle + set Bulle($target) $message + bindtags $target "[bindtags $target] Bulle" +} + +proc kill_balloon {} { + global Bulle + after cancel $Bulle(id) + if {[winfo exists .balloon] == 1} { + destroy .balloon + } + set Bulle(set) 0 +} + +proc balloon {target message {cx 0} {cy 0} } { + global Bulle + if {$Bulle(first) == 1 } { + set Bulle(first) 2 + if { $cx == 0 && $cy == 0 } { + set x [expr [winfo rootx $target] + ([winfo width $target]/2)] + set y [expr [winfo rooty $target] + [winfo height $target] + 4] + } else { + set x [expr $cx + 4] + set y [expr $cy + 4] + } + toplevel .balloon -bg black + wm overrideredirect .balloon 1 + label .balloon.l \ + -text $message -relief flat \ + -bg #ffffaa -fg black -padx 2 -pady 0 -anchor w + pack .balloon.l -side left -padx 1 -pady 1 + wm geometry .balloon +${x}+${y} + set Bulle(set) 1 + } +} + diff --git a/modules/tclsci/tcl/utils/Combobox.tcl b/modules/tclsci/tcl/utils/Combobox.tcl new file mode 100755 index 000000000..cd52b9f7b --- /dev/null +++ b/modules/tclsci/tcl/utils/Combobox.tcl @@ -0,0 +1,2203 @@ +# 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 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> <KP_Enter> <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; + } + + } + + "<KP_Enter>" { + # 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]} + + 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} { + + 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 + 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 + diff --git a/modules/tclsci/tcl/utils/Notebook.tcl b/modules/tclsci/tcl/utils/Notebook.tcl new file mode 100755 index 000000000..72444b296 --- /dev/null +++ b/modules/tclsci/tcl/utils/Notebook.tcl @@ -0,0 +1,279 @@ +# A Notebook widget for Tcl/Tk +# +# Copyright (C) 1996,1997,1998 D. Richard Hipp +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Library General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Library General Public License for more details. +# +# You should have received a copy of the GNU Library General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# +# Author contact information: +# drh@acm.org +# http://www.hwaci.com/drh/ + +# +# jb Silvy. To know the current page +# + +proc Notebook:getCurrentPage { w } { + global Notebook + return $Notebook($w,top) +} + +# +# Create a new notebook widget +# + +package provide Notebook + +proc Notebook:create {w args} { + global Notebook + set Notebook($w,width) 400 + set Notebook($w,height) 300 + set Notebook($w,pages) {} + set Notebook($w,top) 0 + set Notebook($w,pad) 5 + set Notebook($w,fg,on) black + set Notebook($w,fg,off) grey50 + canvas $w -bd 0 -highlightthickness 0 -takefocus 0 + set Notebook($w,bg) [$w cget -bg] + bind $w <1> "Notebook:click $w %x %y" + bind $w <Configure> "Notebook:scheduleExpand $w" + eval Notebook:config $w $args +} + +# +# Change configuration options for the notebook widget +# +proc Notebook:config {w args} { + global Notebook + foreach {tag value} $args { + switch -- $tag { + -width { + set Notebook($w,width) $value + } + -height { + set Notebook($w,height) $value + } + -pages { + set Notebook($w,pages) $value + } + -pad { + set Notebook($w,pad) $value + } + -bg { + set Notebook($w,bg) $value + } + -fg { + set Notebook($w,fg,on) $value + } + -disabledforeground { + set Notebook($w,fg,off) $value + } + } + } + + # + # After getting new configuration values, reconstruct the widget + # + $w delete all + set Notebook($w,x1) $Notebook($w,pad) + set Notebook($w,x2) [expr $Notebook($w,x1)+2] + set Notebook($w,x3) [expr $Notebook($w,x2)+$Notebook($w,width)] + set Notebook($w,x4) [expr $Notebook($w,x3)+2] + set Notebook($w,y1) [expr $Notebook($w,pad)+2] + set Notebook($w,y2) [expr $Notebook($w,y1)+2] + set Notebook($w,y5) [expr $Notebook($w,y1)+30] + set Notebook($w,y6) [expr $Notebook($w,y5)+2] + set Notebook($w,y3) [expr $Notebook($w,y6)+$Notebook($w,height)] + set Notebook($w,y4) [expr $Notebook($w,y3)+2] + set x $Notebook($w,x1) + set cnt 0 + set y7 [expr $Notebook($w,y1)+10] + foreach p $Notebook($w,pages) { + set Notebook($w,p$cnt,x5) $x + set id [$w create text 0 0 -text $p -anchor nw -tags "p$cnt t$cnt" -font {Arial -15} -fill $Notebook($w,fg,off)] + set bbox [$w bbox $id] + set width [lindex $bbox 2] + $w move $id [expr $x+10] $y7 + $w create line \ + $x $Notebook($w,y5)\ + $x $Notebook($w,y2) \ + [expr $x+2] $Notebook($w,y1) \ + [expr $x+$width+16] $Notebook($w,y1) \ + -width 2 -fill white -tags p$cnt + $w create line \ + [expr $x+$width+16] $Notebook($w,y1) \ + [expr $x+$width+18] $Notebook($w,y2) \ + [expr $x+$width+18] $Notebook($w,y5) \ + -width 2 -fill black -tags p$cnt + set x [expr $x+$width+20] + set Notebook($w,p$cnt,x6) [expr $x-2] + if {![winfo exists $w.f$cnt]} { + frame $w.f$cnt -bd 0 + } + $w.f$cnt config -bg $Notebook($w,bg) + place $w.f$cnt -x $Notebook($w,x2) -y $Notebook($w,y6) \ + -width $Notebook($w,width) -height $Notebook($w,height) + incr cnt + } + $w create line \ + $Notebook($w,x1) [expr $Notebook($w,y5)-2] \ + $Notebook($w,x1) $Notebook($w,y3) \ + -width 2 -fill white + $w create line \ + $Notebook($w,x1) $Notebook($w,y3) \ + $Notebook($w,x2) $Notebook($w,y4) \ + $Notebook($w,x3) $Notebook($w,y4) \ + $Notebook($w,x4) $Notebook($w,y3) \ + $Notebook($w,x4) $Notebook($w,y6) \ + $Notebook($w,x3) $Notebook($w,y5) \ + -width 2 -fill black + $w config -width [expr $Notebook($w,x4)+$Notebook($w,pad)] \ + -height [expr $Notebook($w,y4)+$Notebook($w,pad)] \ + -bg $Notebook($w,bg) + set top $Notebook($w,top) + set Notebook($w,top) -1 + Notebook:raise.page $w $top +} + +# +# This routine is called whenever the mouse-button is pressed over +# the notebook. It determines if any page should be raised and raises +# that page. +# +proc Notebook:click {w x y} { + global Notebook + if {$y<$Notebook($w,y1) || $y>$Notebook($w,y6)} return + set N [llength $Notebook($w,pages)] + for {set i 0} {$i<$N} {incr i} { + $w itemconfigure t$i -fill $Notebook($w,fg,off) + if {$x>=$Notebook($w,p$i,x5) && $x<=$Notebook($w,p$i,x6)} { + Notebook:raise.page $w $i + #break + } + } +} + +# +# For internal use only. This procedure raised the n-th page of +# the notebook +# +proc Notebook:raise.page {w n} { + global Notebook + if {$n<0 || $n>=[llength $Notebook($w,pages)]} return + set top $Notebook($w,top) + if {$top>=0 && $top<[llength $Notebook($w,pages)]} { + $w move p$top 0 2 + + } + $w move p$n 0 -2 + #$w itemconfigure t$n -font {Arial 12} + $w itemconfigure t$n -fill $Notebook($w,fg,on) + $w delete topline + if {$n>0} { + $w create line \ + $Notebook($w,x1) $Notebook($w,y6) \ + $Notebook($w,x2) $Notebook($w,y5) \ + $Notebook($w,p$n,x5) $Notebook($w,y5) \ + $Notebook($w,p$n,x5) [expr $Notebook($w,y5)-2] \ + -width 2 -fill white -tags topline + } + $w create line \ + $Notebook($w,p$n,x6) [expr $Notebook($w,y5)-2] \ + $Notebook($w,p$n,x6) $Notebook($w,y5) \ + -width 2 -fill white -tags topline + $w create line \ + $Notebook($w,p$n,x6) $Notebook($w,y5) \ + $Notebook($w,x3) $Notebook($w,y5) \ + -width 2 -fill white -tags topline + set Notebook($w,top) $n + raise $w.f$n +} + +# +# Change the page-specific configuration options for the notebook +# +proc Notebook:pageconfig {w name args} { + global Notebook + set i [lsearch $Notebook($w,pages) $name] + if {$i<0} return + foreach {tag value} $args { + switch -- $tag { + -state { + if {"$value"=="disabled"} { + $w itemconfig t$i -fg $Notebook($w,fg,off) + } else { + $w itemconfig t$i -fg $Notebook($w,fg,on) + } + } + -onexit { + set Notebook($w,p$i,onexit) $value + } + } + } +} + +# +# This procedure raises a notebook page given its name. But first +# we check the "onexit" procedure for the current page (if any) and +# if it returns false, we don't allow the raise to proceed. +# +proc Notebook:raise {w name} { + global Notebook + set i [lsearch $Notebook($w,pages) $name] + if {$i<0} return + if {[info exists Notebook($w,p$i,onexit)]} { + set onexit $Notebook($w,p$i,onexit) + if {"$onexit"!="" && [eval uplevel #0 $onexit]!=0} { + Notebook:raise.page $w $i + } + } else { + Notebook:raise.page $w $i + } +} + +# +# Return the frame associated with a given page of the notebook. +# +proc Notebook:frame {w name} { + global Notebook + set i [lsearch $Notebook($w,pages) $name] + if {$i>=0} { + return $w.f$i + } else { + return {} + } +} + +# +# Try to resize the notebook to the next time we become idle. +# +proc Notebook:scheduleExpand w { + global Notebook + if {[info exists Notebook($w,expand)]} return + set Notebook($w,expand) 1 + after idle "Notebook:expand $w" +} + +# +# Resize the notebook to fit inside its containing widget. +# +proc Notebook:expand w { + global Notebook + set wi [expr [winfo width $w]-($Notebook($w,pad)*2+4)] + set hi [expr [winfo height $w]-($Notebook($w,pad)*2+36)] + Notebook:config $w -width $wi -height $hi + catch {unset Notebook($w,expand)} +} + |