######### scrollbox -- utility scrollbar with default bindings ####### # scrollbox is used in the Path and Startup dialogs to edit lists of options package provide scrollbox 0.1 namespace eval scrollbox { # This variable keeps track of the last list element we clicked on, # used to implement drag-drop reordering of list items variable lastIdx 0 } array set ::scrollbox::entrytext {} proc ::scrollbox::get_curidx { mytoplevel } { set box $mytoplevel.listbox.box if { ! [winfo exists $box] } { return } set idx [$box index active] if {$idx < 0 || \ $idx == [$box index end]} { return [expr {[$box index end] + 1}] } return [expr $idx] } proc ::scrollbox::my_edit_cancel { mytoplevel popup initialvalue } { set ::scrollbox::entrytext($mytoplevel) $initialvalue destroy $popup } proc ::scrollbox::my_edit { mytoplevel {initialvalue {}} } { set lb $mytoplevel.listbox.box set popup $lb.popup if { ! [winfo exists $lb ] } { return $initialvalue } destroy $popup set ::scrollbox::entrytext($mytoplevel) $initialvalue set selected {} # calculate the position of the popup # if there's an original entry (edit), place it over that # if there's none (add), shift it slightly left+down foreach {x y w h} {0 0 0 0} {break} foreach selected [$lb curselection] {break} if { $selected == "" } { foreach {x y w h} [$lb bbox end] {break} set y [expr $y + $h ] } else { foreach {x y _ _} [$lb bbox $selected] {break} if { $initialvalue == "" } { # adding a new entry: shift entry widget slightly down set y1 $y foreach {_ y1 _ _} [$lb bbox [expr $selected + 1]] {break} if { $y == $y1 } { set x [expr $x + 10] set y [expr $y + 10] } else { set x [expr $x + ($y1 - $y) / 2] set y [expr ($y + $y1) / 2 ] } } } # create a new popup entry entry $popup -textvariable ::scrollbox::entrytext($mytoplevel) $popup selection from 0 $popup selection adjust end place $popup -x $x -y $y -relwidth 1.0 focus $popup # override the Return/ESC bindings # note the 'break' at the end that prevents uplevel widgets from receiving the events bind $popup "destroy $popup; break" bind $popup "::scrollbox::my_edit_cancel {$mytoplevel} {$popup} {$initialvalue}; break" bind $popup "::scrollbox::my_edit_cancel {$mytoplevel} {$popup} {$initialvalue}; break" # wait until the user hits or tkwait window $popup # and return the new value if {[catch {set value $::scrollbox::entrytext($mytoplevel)}]} { ## if the user double-clicked while editing, this proc is called multiple times in parallel ## if this happened, the latecomers are skipped return "" } array unset ::scrollbox::entrytext $mytoplevel return $value } proc ::scrollbox::insert_item { mytoplevel idx name } { if {$name != ""} { $mytoplevel.listbox.box insert $idx $name set activeIdx [expr {[$mytoplevel.listbox.box index active] + 1}] $mytoplevel.listbox.box see $activeIdx $mytoplevel.listbox.box activate $activeIdx $mytoplevel.listbox.box selection clear 0 end $mytoplevel.listbox.box selection set active focus $mytoplevel.listbox.box } } proc ::scrollbox::add_item { mytoplevel add_method } { if { $add_method == "" } { set dir [::scrollbox::my_edit $mytoplevel ] } else { set dir [$add_method] } set idx [get_curidx $mytoplevel] if { $idx ne {} } { insert_item $mytoplevel [expr {[get_curidx $mytoplevel] + 1}] $dir } } proc ::scrollbox::edit_item { mytoplevel edit_method } { set idx [get_curidx $mytoplevel] if { $idx eq {} } { return } set box $mytoplevel.listbox.box set initialValue [$box get $idx] if {$initialValue != ""} { if { $edit_method == "" } { set dir [::scrollbox::my_edit $mytoplevel $initialValue ] } else { set dir [$edit_method $initialValue] } if { ! [winfo exists $box ] } { return } if {$dir != ""} { $box delete $idx insert_item $mytoplevel $idx $dir } $box activate $idx $box selection clear 0 end $box selection set active focus $box } } proc ::scrollbox::delete_item { mytoplevel } { set cursel [$mytoplevel.listbox.box curselection] foreach idx $cursel { $mytoplevel.listbox.box delete $idx } $mytoplevel.listbox.box selection set active } # Double-clicking on the listbox should edit the current item, # or add a new one if there is no current proc ::scrollbox::dbl_click { mytoplevel edit_method add_method x y } { if { $x == "" || $y == "" } { return } foreach {left top width height} {"" "" "" ""} {break} # listbox bbox returns an array of 4 items in the order: # left, top, width, height foreach {left top width height} [$mytoplevel.listbox.box bbox @$x,$y] {break} if { $height == "" || $top == "" } { # If for some reason we didn't get valid bbox info, # we want to default to adding a new item set height 0 set top 0 set y 1 } set bottom [expr {$height + $top}] if {$y > $bottom} { add_item $mytoplevel $add_method } else { edit_item $mytoplevel $edit_method } } proc ::scrollbox::click { mytoplevel x y } { # record the index of the current element being # clicked on variable lastIdx [$mytoplevel.listbox.box index @$x,$y] focus $mytoplevel.listbox.box } # For drag-and-drop reordering, recall the last-clicked index # and move it to the position of the item currently under the mouse proc ::scrollbox::release { mytoplevel x y } { variable lastIdx set curIdx [$mytoplevel.listbox.box index @$x,$y] if { $curIdx != $lastIdx } { # clear any current selection $mytoplevel.listbox.box selection clear 0 end set oldIdx $lastIdx set newIdx [expr {$curIdx+1}] set selIdx $curIdx if { $curIdx < $lastIdx } { set oldIdx [expr {$lastIdx + 1}] set newIdx $curIdx set selIdx $newIdx } $mytoplevel.listbox.box insert $newIdx [$mytoplevel.listbox.box get $lastIdx] $mytoplevel.listbox.box delete $oldIdx $mytoplevel.listbox.box activate $newIdx $mytoplevel.listbox.box selection set $selIdx } } # Make a scrollbox widget in a given window and set of data. # # id - the parent window for the scrollbox # listdata - array of data to populate the scrollbox # add_method - method to be called when we add a new item # edit_method - method to be called when we edit an existing item proc ::scrollbox::make { mytoplevel listdata add_method edit_method {title ""}} { if { ${title} eq "" } { frame $mytoplevel.listbox } else { labelframe $mytoplevel.listbox -text ${title} -padx 5 -pady 5 } listbox $mytoplevel.listbox.box -relief raised -highlightthickness 0 \ -selectmode browse -activestyle dotbox \ -yscrollcommand [list "$mytoplevel.listbox.scrollbar" set] # Create a scrollbar and keep it in sync with the current # listbox view pack $mytoplevel.listbox.box [scrollbar "$mytoplevel.listbox.scrollbar" \ -command [list $mytoplevel.listbox.box yview]] \ -side left -fill y -anchor w # Populate the listbox widget foreach item $listdata { $mytoplevel.listbox.box insert end $item } # Standard listbox key/mouse bindings event add <> if { $::windowingsystem eq "aqua" } { event add <> } bind $mytoplevel.listbox.box "::scrollbox::click $mytoplevel %x %y" bind $mytoplevel.listbox.box "::scrollbox::dbl_click $mytoplevel {$edit_method} {$add_method} %x %y" bind $mytoplevel.listbox.box "::scrollbox::release $mytoplevel %x %y" bind $mytoplevel.listbox.box "::scrollbox::edit_item $mytoplevel {$edit_method}; break" bind $mytoplevel.listbox.box <> "::scrollbox::delete_item $mytoplevel" # is called when the user modifies the window # We use it to capture resize events, to make sure the # currently selected item in the listbox is always visible bind $mytoplevel "$mytoplevel.listbox.box see active" # The listbox should expand to fill its containing window # the "-fill" option specifies which direction (x, y or both) to fill, while # the "-expand" option (false by default) specifies whether the widget # should fill pack $mytoplevel.listbox.box -side left -fill both -expand 1 pack $mytoplevel.listbox -side top -pady 2m -padx 2m -fill both -expand 1 # All widget interactions can be performed without buttons, but # we still need a "New..." button since the currently visible window # might be full (even though the user can still expand it) frame $mytoplevel.actions pack $mytoplevel.actions -side top -padx 2m -fill x button $mytoplevel.actions.add_path -text [_ "New..." ] \ -command "::scrollbox::add_item $mytoplevel {$add_method}" button $mytoplevel.actions.edit_path -text [_ "Edit..." ] \ -command "::scrollbox::edit_item $mytoplevel {$edit_method}" button $mytoplevel.actions.delete_path -text [_ "Delete" ] \ -command "::scrollbox::delete_item $mytoplevel" pack $mytoplevel.actions.delete_path -side right -pady 2m -padx 5 -ipadx 10 pack $mytoplevel.actions.edit_path -side right -pady 2m -padx 5 -ipadx 10 pack $mytoplevel.actions.add_path -side right -pady 2m -padx 5 -ipadx 10 $mytoplevel.listbox.box activate end $mytoplevel.listbox.box selection set end focus $mytoplevel.listbox.box }