MG 19 Feb 2018 - This code impliments simple drag-and-drop reordering for the Listbox widget. It works with listboxes with -selectmode set to either single or multiple (not browse or extending, which already have 'dragging' behaviour for altering selection).
MG 21/2/18 Added -command to ListboxDnD::enable
To use, create a listbox, then do
package require ListboxDnD
ListboxDnD::enable $listbox ?-command $command?
If a -command is specified, the command is run when the list order is successfully changed via dragging. May contain %W which is replaced with the widget path prior to execution.
The code:
namespace eval ::ListboxDnD {
variable data;
variable state;
}
bind ListboxDnD <1> [list ::ListboxDnD::start %W %x %y]
bind ListboxDnD <Escape> "[list ::ListboxDnD::cancel %W %x %y];break"
bind ListboxDnD <B1-Motion> [list ::ListboxDnD::drag %W %x %y]
bind ListboxDnD <ButtonRelease-1> [list ::ListboxDnD::stop %W %x %y]
# Enable listbox $lb to be re-ordered with drag and drop
# Possible args:
# -command $cmd - run $cmd after the list has been rearranged, replacing %W with the window path
proc ::ListboxDnD::enable {lb {args}} {
variable data;
if { ![winfo exists $lb] || [winfo class $lb] ne "Listbox" } {
return 0;
}
set bt [bindtags $lb]
if { [set bti [lsearch -exact $bt "Listbox"]] < 0 } {
return 0;
}
foreach {x y} $args {
if { $x eq "-command" } {
if { $y eq "" } {
unset -nocomplain data($lb,-command)
} else {
set data($lb,-command) $y
}
} else {
error "Invalid option $x: Must be -command"
}
}
if { "ListboxDnD" in $bt } {
return 1;# already done
}
bindtags $lb [linsert $bt $bti+1 "ListboxDnD"]
return 1;
};#enable
# Called on B1 press to set up for a drag
proc ::ListboxDnD::start {lb x y} {
variable state;
if { [$lb cget -state] ne "normal" || [$lb cget -selectmode] ni [list "single" "multiple"] } {
return;
}
set _listvar [$lb cget -listvariable]
if { $_listvar eq "" } {
return;
}
upvar #0 $_listvar listvar
if { ![info exists listvar] || [llength $listvar] < 2 } {
return;
}
set state($lb,list) $listvar
set state($lb,startindex) [$lb index @$x,$y]
set state($lb,currindex) $state($lb,startindex)
set state($lb,startsel) [$lb curselection]
return;
};# start
# Called on B1 motion, as an item is being dragged
proc ::ListboxDnD::drag {lb x y} {
variable state;
set _listvar [$lb cget -listvariable]
if { $_listvar eq "" || ![info exists state($lb,list)] } {
return;
}
upvar #0 $_listvar listvar
set newpos [$lb nearest $y]
set oldpos $state($lb,currindex)
if { $newpos == $oldpos } {
return;
}
set sel [$lb curselection]
if { $oldpos in $sel && $newpos ni $sel } {
$lb selection clear $oldpos
$lb selection set $newpos
} elseif { $oldpos ni $sel && $newpos in $sel } {
$lb selection clear $newpos
$lb selection set $oldpos
}
set newlist $listvar
set oldval [lindex $newlist $oldpos]
set newval [lindex $newlist $newpos]
set newlist [lreplace $newlist $oldpos $oldpos $newval]
set newlist [lreplace $newlist $newpos $newpos $oldval]
set listvar $newlist
set state($lb,currindex) $newpos
return;
};# drag
# Called when Escape is pressed; cancel a drag
proc ::ListboxDnD::cancel {lb x y} {
variable state;
if { ![info exists state($lb,list)] } {
return;
}
set _listvar [$lb cget -listvariable]
if { $_listvar eq "" } {
return;
}
upvar #0 $_listvar listvar
set listvar $state($lb,list)
$lb selection clear 0 end
foreach x $state($lb,startsel) {
$lb selection set $x
}
array unset state $lb,*
return;
};# cancel
# Called on B1 release; finalise a drag
proc ::ListboxDnD::stop {lb x y} {
variable state;
variable data;
if { ![info exists state($lb,startindex)] } {
return; # drag was cancelled
}
set start $state($lb,startindex)
set curr $state($lb,currindex)
array unset state $lb,*
if { $start == $curr } {
return;# Wasn't dragged anyway
}
if { [info exists data($lb,-command)] } {
catch {uplevel #0 [string map [list %W $lb] $data($lb,-command)]}
}
return;
};# stop
package provide ListboxDnD 1.0
Example:
set list [list foo bar baz boing sprocket]
pack [listbox .lb -listvariable list -selectmode single]
ListboxDnD::enable .lb