Natural local drag&drop for tablelist mega widget

JMeh 20 Dec 2017 - tblNaturalDrag

Implementation of a natural way of local drag and drop with automatic scrolling if the mouse cursor is above the first or last row in the tablelist or tablelist_tile widget, no matter which selection mode is active. All previous selected lines are collected into one block and inserted at the drop position. No mouse movement is required while scrolling, instead the speed of scrolling accelerates the longer the mouse cursor lingers over the first or last visible line. The actual drop position is shown by a horizontal line using the tablelist showtargetmark mechanism. At the end of the drag process a virtual <<Drop>> event is generated.


  • Under Windows, it is not possible to bind Button-1 for dragging. The tablelist widget immediately activates single selection mode.
  • Under macOS, the command hidetargetmark doesn't work. Multiple tergetmark lines are shown, possible a refresh error.
package provide tblNaturalDrag 0.1

# @brief this installs the natural local drag & drop mechanism for the tablelist widget
# @param tbl tablelist widget
# @param args options: -button, -cycle, -accel, -color
# Install the natural local drag & drop mechanism for the tablelist or tablelist_tile 
# widget and configure it with the following options:
# - button specifies the button number (defaults to 1)
# - cycle specifies the time in milliseconds (defaults to 100) for calling the scroll procedure
# - accel sets the factor for acceleration (defaults to 1.0)
# - color sets the color of the target mark (defaults to green)
proc tblNaturalDrag {tbl args} {
  global dragInfo

  if {[info commands tablelist::tablelist] == {}} {
    error "package tablelist or tablelist_tile required"
  # init configurable drag infos
  set dragInfo(button) 1
  set dragInfo(cycle) 100
  set dragInfo(accel) 1.0
  set dragInfo(color) green

  # set drag parameters
  foreach {opt val} $args {
    set item [string range $opt 1 end]
    if {[info exists dragInfo($item)]} {
      set dragInfo($item) $val
    } else {
      error "unknown option: $opt"
  $tbl configure -targetcolor $dragInfo(color) -customdragsource true

  set dragInfo(start) 0
  set dragInfo(x) -1
  set dragInfo(y) -1
  set dragInfo(timer) {}

  # start with mouse button down event
  bind [$tbl bodytag] <ButtonPress-$dragInfo(button)> {_tblNaturalDragStart %W %x %y}

proc _tblNaturalDragStart {w x y} {
  global dragInfo

  # make window coordinates listbox relative
  foreach {tbl x y} [tablelist::convEventFields $w $x $y] {}
  # setup dragInfo array
  set dragInfo(start) [clock milliseconds]
  set dragInfo(x) $x
  set dragInfo(y) $y
  set dragInfo(row) -1
  # start check timer for scrolling
  set dragInfo(timer) [after $dragInfo(cycle) _tblNaturalDragCheck $tbl]
  # update coordinates on mouse movement
  bind [$tbl bodytag] <Motion> {_tblNaturalDragScan %W %x %y}
  # end with release of mouse button
  bind [$tbl bodytag] <ButtonRelease-$dragInfo(button)> {_tblNaturalDragDrop %W %x %y}

proc _tblNaturalDragCheck {tbl} {
  global dragInfo

  after cancel $dragInfo(timer)
  set x $dragInfo(x)
  set y $dragInfo(y)
  set dragInfo(row) [$tbl containing $y]
  $tbl showtargetmark before $dragInfo(row)
  set topRow [$tbl index top]
  set botRow [$tbl index bottom]
  # calc scroll amount per time slot
  set now [clock milliseconds]
  set scrollCount [expr {int($dragInfo(accel) * ($now - $dragInfo(start)) / 1000.0 + 0.5)}]
  if {$dragInfo(row) == $topRow} {
    # scroll back n lines
    $tbl yview scroll -$scrollCount units
  } elseif {$dragInfo(row) == $botRow} {
    # scroll forward n lines
    $tbl yview scroll +$scrollCount units
  } else {
    # slow down if not in first or last row
    set dragInfo(start) $now
  set dragInfo(timer) [after $dragInfo(cycle) _tblNaturalDragCheck $tbl]

proc _tblNaturalDragScan {w x y} {
  global dragInfo

  # make window coordinates listbox relative
  foreach {tbl x y} [tablelist::convEventFields $w $x $y] {}
  # update mouse coordinates
  set dragInfo(x) $x
  set dragInfo(y) $y

proc _tblNaturalDragDrop {w x y} {
  global dragInfo

  # stop scrolling immediately
  after cancel $dragInfo(timer)
  # make window coordinates listbox relative
  foreach {tbl x y} [tablelist::convEventFields $w $x $y] {}
  # remove mouse motion binding
  bind [$tbl bodytag] <Motion> {}
  # hide drop marker line
  $tbl hidetargetmark
  # quit if no drop destination 
  if {$dragInfo(row) == -1} return
  # get lines to move and quit if empty
  set sel [$tbl curselection]
  if {$sel == {}} return
  # count number of lines above drop line
  set upCnt 0
  foreach row $sel {
    if {$row < $dragInfo(row)} { incr upCnt } break
  # get selected rows
  set selData [$tbl get $sel]
  set selCnt [llength $sel]
  $tbl delete $sel
  # calculate destination range
  set insRow [expr {$dragInfo(row) - $upCnt}]
  set lastRow [expr {$insRow + $selCnt -1}]
  # reinsert data at this new position
  if {$selCnt > 1} {
    $tbl insertlist $insRow $selData
  } else {
    $tbl insert $insRow $selData
  # reselect inserted rows
  $tbl selection set $insRow $lastRow
  $tbl activate $insRow
  if {$insRow == $lastRow} {
    event generate $tbl <<Drag>> -data $insRow
  } else {
    event generate $tbl <<Drag>> -data [list $insRow $lastRow]

if {[info exists argv0] && [file tail $argv0] == "tblNaturalDrag.tcl"} {
  package require tablelist_tile
  set tbl .t
  set cols {
    0 Name left
    0 Value right
  grid [tablelist::tablelist $tbl -columns $cols -height 20 \
    -selectmode extended -yscrollcommand ".sby set"] -row 1 -column 0 -sticky nswe
  grid [ttk::scrollbar .sby -orient vertical -command "$tbl yview"] \
    -row 1 -column 1 -sticky ns
  grid rowconfigure . 1 -weight 1
  tblNaturalDrag $tbl -button 3 -cycle 50 -accel 2.5
  proc dropDebugger {args} {puts "dropDebugger $args"}
  bind $tbl <<Drag>> {dropDebugger %W %d}
  set data {}
  set n 1000
  for {set i 0} {$i < $n} {incr i} {
    lappend data [list "Test $i" $i]
  $tbl insertlist end $data