#
# tkdnd_unix.tcl --
#
#    This file implements some utility procedures that are used by the TkDND
#    package.
#
# This software is copyrighted by:
# George Petasis, National Centre for Scientific Research "Demokritos",
# Aghia Paraskevi, Athens, Greece.
# e-mail: petasis@iit.demokritos.gr
#
# The following terms apply to all files associated
# with the software unless explicitly disclaimed in individual files.
#
# The authors hereby grant permission to use, copy, modify, distribute,
# and license this software and its documentation for any purpose, provided
# that existing copyright notices are retained in all copies and that this
# notice is included verbatim in any distributions. No written agreement,
# license, or royalty fee is required for any of the authorized uses.
# Modifications to this software may be copyrighted by their authors
# and need not follow the licensing terms described here, provided that
# the new terms are clearly indicated on the first page of each file where
# they apply.
#
# IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY
# FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
# ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY
# DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.  THIS SOFTWARE
# IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE
# NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR
# MODIFICATIONS.
#

namespace eval xdnd {
  variable _dragging 0

  proc debug { msg } {
    tkdnd::debug $msg
  };# debug

  proc initialise { } {
    ## Mapping from platform types to TkDND types...
    ::tkdnd::generic::initialise_platform_to_tkdnd_types [list \
       text/plain\;charset=utf-8     DND_Text  \
       UTF8_STRING                   DND_Text  \
       text/plain                    DND_Text  \
       STRING                        DND_Text  \
       TEXT                          DND_Text  \
       COMPOUND_TEXT                 DND_Text  \
       text/uri-list                 DND_Files \
       text/html\;charset=utf-8      DND_HTML  \
       text/html                     DND_HTML  \
       application/x-color           DND_Color \
    ]
  };# initialise

};# namespace xdnd

# ----------------------------------------------------------------------------
#  Command xdnd::HandleXdndEnter
# ----------------------------------------------------------------------------
proc xdnd::GetPressedKeys { drop_target } {
  #DBG debug "xdnd::GetPressedKeys: $drop_target"
  if {[catch {set dict [_keyboard_get_state $drop_target]}]} {
    return {}
  }
  set pressedkeys {}
  for {set b 1} {$b <= 5} {incr b} {
    if {[dict get $dict $b]} {lappend pressedkeys $b}
  }
  foreach {k l} {Alt alt Shift shift Control ctrl Lock caps_lock
                 Mod1 mod1 Mod2 mod2 Mod3 mod3 Mod4 mod4 Mod5 mod5} {
    if {[dict get $dict $k]} {lappend pressedkeys $l}
  }
  #DBG debug "xdnd::GetPressedKeys: $drop_target -> $pressedkeys"
  return $pressedkeys
};# xdnd::GetPressedKeys

# ----------------------------------------------------------------------------
#  Command xdnd::HandleXdndEnter
# ----------------------------------------------------------------------------
proc xdnd::HandleXdndEnter { drop_target drag_source typelist time
                             { data {} } } {
  variable _pressedkeys
  variable _actionlist
  variable _typelist
  set _pressedkeys [GetPressedKeys $drop_target]
  set _actionlist  { copy move link ask private }
  set _typelist    $typelist
  #DBG debug "xdnd::HandleXdndEnter: $time"
  ::tkdnd::generic::SetDroppedData $data
  ::tkdnd::generic::HandleEnter $drop_target $drag_source $typelist $typelist \
           $_actionlist $_pressedkeys
};# xdnd::HandleXdndEnter

# ----------------------------------------------------------------------------
#  Command xdnd::HandleXdndPosition
# ----------------------------------------------------------------------------
proc xdnd::HandleXdndPosition { drop_target rootX rootY time
                               { drag_source {} } { action default } } {
  variable _pressedkeys
  variable _typelist
  variable _last_mouse_root_x; set _last_mouse_root_x $rootX
  variable _last_mouse_root_y; set _last_mouse_root_y $rootY
  set _pressedkeys [GetPressedKeys $drop_target]
  #DBG debug "xdnd::HandleXdndPosition: $time"
  ## Get the dropped data...
  catch {
    ::tkdnd::generic::SetDroppedData [GetPositionData $drop_target $_typelist $time]
  }
  ::tkdnd::generic::HandlePosition $drop_target $drag_source \
                                   $_pressedkeys $rootX $rootY $action
};# xdnd::HandleXdndPosition

# ----------------------------------------------------------------------------
#  Command xdnd::HandleXdndLeave
# ----------------------------------------------------------------------------
proc xdnd::HandleXdndLeave { } {
  #DBG debug "xdnd::HandleXdndLeave"
  ::tkdnd::generic::HandleLeave
};# xdnd::HandleXdndLeave

# ----------------------------------------------------------------------------
#  Command xdnd::_HandleXdndDrop
# ----------------------------------------------------------------------------
proc xdnd::HandleXdndDrop { time } {
  variable _pressedkeys
  variable _last_mouse_root_x
  variable _last_mouse_root_y
  set _pressedkeys [GetPressedKeys [::tkdnd::generic::GetDropTarget]]
  #DBG debug "xdnd::HandleXdndDrop: $time"
  ## Snapshot the in-flight drop state before GetDroppedData below: it pumps
  ## the Tk event queue while waiting for the SelectionNotify round-trip, and
  ## a queued XdndLeave processed during that wait resets this state via
  ## ::tkdnd::generic::HandleLeave before HandleDrop gets to use it -- a race
  ## commonly hit under XWayland (issue #77). Restore the snapshot if that
  ## happened so the drop can still be delivered.
  set dropstate [::tkdnd::generic::GetDropState]
  ## Get the dropped data...
  ::tkdnd::generic::SetDroppedData [GetDroppedData \
    [::tkdnd::generic::GetDragSource] [::tkdnd::generic::GetDropTarget] \
    [::tkdnd::generic::GetDragSourceCommonTypes] $time]
  if {![string length [::tkdnd::generic::GetDropTarget]]} {
    ::tkdnd::generic::RestoreDropState $dropstate
  }
  ::tkdnd::generic::HandleDrop {} {} $_pressedkeys \
                               $_last_mouse_root_x $_last_mouse_root_y $time
};# xdnd::HandleXdndDrop

# ----------------------------------------------------------------------------
#  Command xdnd::GetPositionData
# ----------------------------------------------------------------------------
proc xdnd::GetPositionData { drop_target typelist time } {
  foreach {drop_target common_drag_source_types common_drop_target_types} \
    [::tkdnd::generic::FindWindowWithCommonTypes $drop_target $typelist] {break}
  GetDroppedData [::tkdnd::generic::GetDragSource] $drop_target \
    $common_drag_source_types $time
};# xdnd::GetPositionData

# ----------------------------------------------------------------------------
#  Command xdnd::GetDroppedData
# ----------------------------------------------------------------------------
proc xdnd::GetDroppedData { _drag_source _drop_target _common_drag_source_types time } {
  if {![llength $_common_drag_source_types]} {
    return -code error "no common data types between the drag source and drop target widgets"
  }
  ## Is drag source in this application?
  if {[catch {winfo pathname -displayof $_drop_target $_drag_source} p]} {
    set _use_tk_selection 0
  } else {
    set _use_tk_selection 1
  }
  foreach type $_common_drag_source_types {
    #DBG debug "TYPE: $type ($_drop_target)"
    # _get_selection $_drop_target $time $type
    if {$_use_tk_selection} {
      if {![catch {
        selection get -displayof $_drop_target -selection XdndSelection \
                      -type $type
                                              } result options]} {
        return [normalise_data $type $result]
      }
    } else {
      #DBG debug "_selection_get -displayof $_drop_target -selection XdndSelection \
      #                 -type $type -time $time"
      #after 100 [list focus -force $_drop_target]
      #after 50 [list raise [winfo toplevel $_drop_target]]
      if {![catch {
        _selection_get -displayof $_drop_target -selection XdndSelection \
                      -type $type -time $time
                                              } result options]} {
        return [normalise_data $type $result]
      }
    }
  }
  return -options $options $result
};# xdnd::GetDroppedData

# ----------------------------------------------------------------------------
#  Command xdnd::platform_specific_types
# ----------------------------------------------------------------------------
proc xdnd::platform_specific_types { types } {
  ::tkdnd::generic::platform_specific_types $types
}; # xdnd::platform_specific_types

# ----------------------------------------------------------------------------
#  Command xdnd::platform_specific_type
# ----------------------------------------------------------------------------
proc xdnd::platform_specific_type { type } {
  ::tkdnd::generic::platform_specific_type $type
}; # xdnd::platform_specific_type

# ----------------------------------------------------------------------------
#  Command tkdnd::platform_independent_types
# ----------------------------------------------------------------------------
proc ::tkdnd::platform_independent_types { types } {
  ::tkdnd::generic::platform_independent_types $types
}; # tkdnd::platform_independent_types

# ----------------------------------------------------------------------------
#  Command xdnd::platform_independent_type
# ----------------------------------------------------------------------------
proc xdnd::platform_independent_type { type } {
  ::tkdnd::generic::platform_independent_type $type
}; # xdnd::platform_independent_type

# ----------------------------------------------------------------------------
#  Command xdnd::_normalise_data
# ----------------------------------------------------------------------------
proc xdnd::normalise_data { type data } {
  # Tk knows how to interpret the following types:
  #    STRING, TEXT, COMPOUND_TEXT
  #    UTF8_STRING
  # Else, it returns a list of 8 or 32 bit numbers...
  switch -glob $type {
    STRING - UTF8_STRING - TEXT - COMPOUND_TEXT {return $data}
    text/html {
      if {[catch {
            ::tkdnd::from_encoding utf-16 $data
           } string]} {
        set string $data
      }
      return [string map {\r\n \n} $string]
    }
    text/html\;charset=utf-8  -
    text/plain\;charset=utf-8 -
    text/plain {
      if {[catch {
            ::tkdnd::from_encoding utf-8 [tkdnd::bytes_to_string $data]
           } string]} {
        set string $data
      }
      return [string map {\r\n \n} $string]
    }
    text/uri-list* {
      if {[catch {
            ::tkdnd::from_encoding utf-8 [tkdnd::bytes_to_string $data]
          } string]} {
        set string $data
      }
      ## Get rid of \r\n
      set string [string trim [string map {\r\n \n} $string]]
      set files {}
      foreach quoted_file [split $string] {
        set file [tkdnd::urn_unquote $quoted_file]
        switch -glob $file {
          \#*       {}
          file://*  {lappend files [string range $file 7 end]}
          ftp://*   -
          https://* -
          http://*  {lappend files $quoted_file}
          default   {lappend files $file}
        }
      }
      return $files
    }
    application/x-color {
      return $data
    }
    text/x-moz-url -
    application/q-iconlist -
    default    {return $data}
  }
}; # xdnd::normalise_data

#############################################################################
##
##  XDND drag implementation
##
#############################################################################

# ----------------------------------------------------------------------------
#  Command xdnd::_selection_ownership_lost
# ----------------------------------------------------------------------------
proc xdnd::_selection_ownership_lost {} {
  variable _dragging
  set _dragging 0
};# _selection_ownership_lost

# ----------------------------------------------------------------------------
#  Command xdnd::_dodragdrop
# ----------------------------------------------------------------------------
proc xdnd::_dodragdrop { source actions types data button { cursor_map {} } } {
  variable _dragging

  #DBG debug "xdnd::_dodragdrop: source: $source, actions: $actions, types: $types,\
  #DBG        data: \"$data\", button: $button"
  if {$_dragging} {
    ## We are in the middle of another drag operation...
    error "another drag operation in progress"
  }

  variable _dodragdrop_drag_source                $source
  variable _dodragdrop_drop_target                0
  variable _dodragdrop_drop_target_proxy          0
  variable _dodragdrop_actions                    $actions
  variable _dodragdrop_action_descriptions        $actions
  variable _dodragdrop_actions_len                [llength $actions]
  variable _dodragdrop_types                      $types
  variable _dodragdrop_types_len                  [llength $types]
  variable _dodragdrop_data                       $data
  variable _dodragdrop_transfer_data              {}
  variable _dodragdrop_button                     $button
  variable _dodragdrop_time                       0
  variable _dodragdrop_default_action             refuse_drop
  variable _dodragdrop_waiting_status             0
  variable _dodragdrop_drop_target_accepts_drop   0
  variable _dodragdrop_drop_target_accepts_action refuse_drop
  variable _dodragdrop_current_cursor             $_dodragdrop_default_action
  variable _dodragdrop_drop_occured               0
  variable _dodragdrop_selection_requestor        0
  variable _dodragdrop_cursor_map                 $cursor_map

  ##
  ## If we have more than 3 types, the property XdndTypeList must be set on
  ## the drag source widget...
  ##
  if {$_dodragdrop_types_len > 3} {
    _announce_type_list $_dodragdrop_drag_source $_dodragdrop_types
  }

  ##
  ## Announce the actions & their descriptions on the XdndActionList &
  ## XdndActionDescription properties...
  ##
  _announce_action_list $_dodragdrop_drag_source $_dodragdrop_actions \
                        $_dodragdrop_action_descriptions

  ##
  ## Arrange selection handlers for our drag source, and all the supported types
  ##
  #DBG debug "xdnd::_dodragdrop: registerSelectionHandler $source $types"
  registerSelectionHandler $source $types

  ##
  ## Step 1: When a drag begins, the source takes ownership of XdndSelection.
  ##
  #DBG debug "xdnd::_dodragdrop: selection own $source"
  selection own -command ::tkdnd::xdnd::_selection_ownership_lost \
                -selection XdndSelection $source
  set _dragging 1

  ## Grab the mouse pointer on a dedicated offscreen window rather than the
  ## source widget itself: X automatically drops a grab when its window
  ## becomes unviewable, which breaks the whole drag if the source widget
  ## (or an ancestor) gets unmapped mid-drag, e.g. by switching virtual
  ## desktops via a pager (issue #6).
  set _dodragdrop_grab_window [_create_grab_window]
  #DBG debug "xdnd::_dodragdrop: _grab_pointer $_dodragdrop_grab_window [_get_mapped_cursor $_dodragdrop_default_action]"
  _grab_pointer $_dodragdrop_grab_window [_get_mapped_cursor $_dodragdrop_default_action]

  ## If the source widget is destroyed mid-drag (e.g. its toplevel is closed
  ## with Alt+F4), make sure we don't hang forever on tkwait below.
  set _dodragdrop_source_destroy_binding [bind $source <Destroy>]
  bind $source <Destroy> {+ set ::tkdnd::xdnd::_dragging 0}

  ## Register our generic event handler...
  #  The generic event callback will report events by modifying variable
  #  ::xdnd::_dodragdrop_event: a dict with event information will be set as
  #  the value of the variable...
  #DBG debug "xdnd::_dodragdrop: _register_generic_event_handler"
  _register_generic_event_handler

  ## Set a timeout for debugging purposes...
  #  after 60000 {set ::tkdnd::xdnd::_dragging 0}

  #DBG debug "xdnd::_dodragdrop: waiting drag action to finish..."
  tkwait variable ::tkdnd::xdnd::_dragging
  #DBG debug "xdnd::_dodragdrop: drag action finished!"
  catch {_SendXdndLeave}

  set _dragging 0
  catch {bind $source <Destroy> $_dodragdrop_source_destroy_binding}
  #DBG debug "xdnd::_dodragdrop: _ungrab_pointer $_dodragdrop_grab_window"
  _ungrab_pointer $_dodragdrop_grab_window
  #DBG debug "xdnd::_dodragdrop: _unregister_generic_event_handler"
  _unregister_generic_event_handler
  catch {selection clear -selection XdndSelection}
  #DBG debug "xdnd::_dodragdrop: unregisterSelectionHandler $source $types"
  catch {unregisterSelectionHandler $source $types}
  return $_dodragdrop_drop_target_accepts_action
};# xdnd::_dodragdrop

# ----------------------------------------------------------------------------
#  Command xdnd::_create_grab_window
#
#  Returns a small, offscreen, override-redirect helper toplevel used solely
#  as the target of the pointer grab during a drag. Created lazily once and
#  reused for subsequent drags (only one drag can be in progress at a time).
# ----------------------------------------------------------------------------
proc xdnd::_create_grab_window { } {
  set w .___tkdnd___grab___window___
  if {![winfo exists $w]} {
    toplevel $w -class TkDndGrab
    wm overrideredirect $w 1
    wm geometry $w 1x1+-32000+-32000
    ## Force the toplevel to actually be mapped now: XGrabPointer requires
    ## a viewable grab window, and the map request otherwise happens lazily
    ## on the next idle pass.
    update idletasks
  }
  return $w
};# xdnd::_create_grab_window

# ----------------------------------------------------------------------------
#  Command xdnd::_process_drag_events
# ----------------------------------------------------------------------------
proc xdnd::_process_drag_events {event} {
  # The return value from proc is normally 0. A non-zero return value indicates
  # that the event is not to be handled further; that is, proc has done all
  # processing that is to be allowed for the event
  variable _dragging
  if {!$_dragging} {return 0}
  #DBG debug "xdnd::_process_drag_events: $event"

  variable _dodragdrop_time
  set time [dict get $event time]
  set type [dict get $event type]
  if {$time < $_dodragdrop_time && ![string equal $type SelectionRequest]} {
    #DBG debug "xdnd::_process_drag_events: return 0 (1)"
    return 0
  }
  set _dodragdrop_time $time

  variable _dodragdrop_drag_source
  variable _dodragdrop_drop_target
  variable _dodragdrop_drop_target_proxy
  variable _dodragdrop_default_action
  switch $type {
    MotionNotify {
      set rootx  [dict get $event x_root]
      set rooty  [dict get $event y_root]
      set window [_find_drop_target_window $_dodragdrop_drag_source \
                                           $rootx $rooty]
      if {[string length $window]} {
        ## Examine the modifiers to suggest an action...
        set _dodragdrop_default_action [_default_action $event]
        ## Is it a Tk widget?
        #DBG set path [winfo containing $rootx $rooty]
        #DBG debug "Window under mouse: $window ($path)"
        if {$_dodragdrop_drop_target != $window} {
          ## Send XdndLeave to $_dodragdrop_drop_target
          _SendXdndLeave
          ## Is there a proxy? If not, _find_drop_target_proxy returns the
          ## target window, so we always get a valid "proxy".
          set proxy [_find_drop_target_proxy $_dodragdrop_drag_source $window]
          ## Send XdndEnter to $window
          _SendXdndEnter $window $proxy
          ## Send XdndPosition to $_dodragdrop_drop_target
          _SendXdndPosition $rootx $rooty $_dodragdrop_default_action
        } else {
          ## Send XdndPosition to $_dodragdrop_drop_target
          _SendXdndPosition $rootx $rooty $_dodragdrop_default_action
        }
      } else {
        ## No window under the mouse. Send XdndLeave to $_dodragdrop_drop_target
        _SendXdndLeave
      }
    }
    ButtonPress {
    }
    ButtonRelease {
      variable _dodragdrop_button
      set button [dict get $event button]
      if {$button == $_dodragdrop_button} {
        ## The button that initiated the drag was released. Trigger drop...
        #DBG debug "xdnd::_process_drag_events: _SendXdndDrop"
        _SendXdndDrop
      }
      #DBG debug "xdnd::_process_drag_events: return 1 (2)"
      # return 1 ;# Returning non-zero is not a good idea...
      return 0
    }
    KeyPress {
    }
    KeyRelease {
      set keysym [dict get $event keysym]
      switch $keysym {
        Escape {
          ## The user has pressed escape. Abort...
          if {$_dragging} {set _dragging 0}
        }
      }
    }
    SelectionRequest {
      variable _dodragdrop_selection_requestor
      variable _dodragdrop_selection_property
      variable _dodragdrop_selection_selection
      variable _dodragdrop_selection_target
      variable _dodragdrop_selection_time
      set _dodragdrop_selection_requestor [dict get $event requestor]
      set _dodragdrop_selection_property  [dict get $event property]
      set _dodragdrop_selection_selection [dict get $event selection]
      set _dodragdrop_selection_target    [dict get $event target]
      set _dodragdrop_selection_time      $time
      #DBG debug "xdnd::_process_drag_events: return 0 (3)"
      return 0
    }
    default {
      #DBG debug "xdnd::_process_drag_events: return 0 (4)"
      return 0
    }
  }
  #DBG debug "xdnd::_process_drag_events: return 0 (5)"
  return 0
};# _process_drag_events

# ----------------------------------------------------------------------------
#  Command xdnd::_SendXdndEnter
# ----------------------------------------------------------------------------
proc xdnd::_SendXdndEnter {window proxy} {
  variable _dodragdrop_drag_source
  variable _dodragdrop_drop_target
  variable _dodragdrop_drop_target_proxy
  variable _dodragdrop_types
  variable _dodragdrop_waiting_status
  variable _dodragdrop_drop_occured
  if {$_dodragdrop_drop_target > 0} _SendXdndLeave
  if {$_dodragdrop_drop_occured} return
  set _dodragdrop_drop_target       $window
  set _dodragdrop_drop_target_proxy $proxy
  set _dodragdrop_waiting_status    0
  if {$_dodragdrop_drop_target < 1} return
  #DBG debug "XdndEnter: $_dodragdrop_drop_target $_dodragdrop_drop_target_proxy"
  _send_XdndEnter $_dodragdrop_drag_source $_dodragdrop_drop_target \
                  $_dodragdrop_drop_target_proxy $_dodragdrop_types
};# xdnd::_SendXdndEnter

# ----------------------------------------------------------------------------
#  Command xdnd::_SendXdndPosition
# ----------------------------------------------------------------------------
proc xdnd::_SendXdndPosition {rootx rooty action} {
  variable _dodragdrop_drag_source
  variable _dodragdrop_drop_target
  if {$_dodragdrop_drop_target < 1} return
  variable _dodragdrop_drop_occured
  if {$_dodragdrop_drop_occured} return
  variable _dodragdrop_drop_target_proxy
  variable _dodragdrop_waiting_status
  ## Arrange a new XdndPosition, to be send periodically...
  variable _dodragdrop_xdnd_position_heartbeat
  catch {after cancel $_dodragdrop_xdnd_position_heartbeat}
  set _dodragdrop_xdnd_position_heartbeat [after 200 \
    [list ::tkdnd::xdnd::_SendXdndPosition $rootx $rooty $action]]
  if {$_dodragdrop_waiting_status} {return}
  #DBG debug "XdndPosition: $_dodragdrop_drop_target $rootx $rooty $action"
  _send_XdndPosition $_dodragdrop_drag_source $_dodragdrop_drop_target \
                     $_dodragdrop_drop_target_proxy $rootx $rooty $action
  set _dodragdrop_waiting_status 1
};# xdnd::_SendXdndPosition

# ----------------------------------------------------------------------------
#  Command xdnd::_HandleXdndStatus
# ----------------------------------------------------------------------------
proc xdnd::_HandleXdndStatus {event} {
  variable _dodragdrop_drop_target
  variable _dodragdrop_waiting_status

  variable _dodragdrop_drop_target_accepts_drop
  variable _dodragdrop_drop_target_accepts_action
  set _dodragdrop_waiting_status 0
  foreach key {target accept want_position action x y w h} {
    set $key [dict get $event $key]
  }
  set _dodragdrop_drop_target_accepts_drop   $accept
  set _dodragdrop_drop_target_accepts_action $action
  if {$_dodragdrop_drop_target < 1} return
  variable _dodragdrop_drop_occured
  if {$_dodragdrop_drop_occured} return
  _update_cursor
  #DBG debug "XdndStatus: $event"
};# xdnd::_HandleXdndStatus

# ----------------------------------------------------------------------------
#  Command xdnd::_HandleXdndFinished
# ----------------------------------------------------------------------------
proc xdnd::_HandleXdndFinished {event} {
  variable _dodragdrop_xdnd_finished_event_after_id
  catch {after cancel $_dodragdrop_xdnd_finished_event_after_id}
  set _dodragdrop_xdnd_finished_event_after_id {}
  variable _dodragdrop_drop_target
  set _dodragdrop_drop_target 0
  variable _dragging
  if {$_dragging} {set _dragging 0}

  variable _dodragdrop_drop_target_accepts_drop
  variable _dodragdrop_drop_target_accepts_action
  if {[dict size $event]} {
    foreach key {target accept action} {
      set $key [dict get $event $key]
    }
    set _dodragdrop_drop_target_accepts_drop   $accept
    set _dodragdrop_drop_target_accepts_action $action
  } else {
    set _dodragdrop_drop_target_accepts_drop 0
  }
  if {!$_dodragdrop_drop_target_accepts_drop} {
    set _dodragdrop_drop_target_accepts_action refuse_drop
  }
  #DBG debug "XdndFinished: $event"
};# xdnd::_HandleXdndFinished

# ----------------------------------------------------------------------------
#  Command xdnd::_SendXdndLeave
# ----------------------------------------------------------------------------
proc xdnd::_SendXdndLeave {} {
  variable _dodragdrop_drag_source
  variable _dodragdrop_drop_target
  if {$_dodragdrop_drop_target < 1} return
  variable _dodragdrop_drop_target_proxy
  #DBG debug "XdndLeave: $_dodragdrop_drop_target"
  _send_XdndLeave $_dodragdrop_drag_source $_dodragdrop_drop_target \
                  $_dodragdrop_drop_target_proxy
  set _dodragdrop_drop_target 0
  variable _dodragdrop_drop_target_accepts_drop
  variable _dodragdrop_drop_target_accepts_action
  set _dodragdrop_drop_target_accepts_drop   0
  set _dodragdrop_drop_target_accepts_action refuse_drop
  variable _dodragdrop_drop_occured
  if {$_dodragdrop_drop_occured} return
  _update_cursor
};# xdnd::_SendXdndLeave

# ----------------------------------------------------------------------------
#  Command xdnd::_SendXdndDrop
# ----------------------------------------------------------------------------
proc xdnd::_SendXdndDrop {} {
  variable _dodragdrop_drag_source
  variable _dodragdrop_drop_target
  if {$_dodragdrop_drop_target < 1} {
    ## The mouse has been released over a widget that does not accept drops.
    _HandleXdndFinished {}
    return
  }
  variable _dodragdrop_drop_occured
  if {$_dodragdrop_drop_occured} {return}
  variable _dodragdrop_drop_target_proxy
  variable _dodragdrop_drop_target_accepts_drop
  variable _dodragdrop_drop_target_accepts_action

  set _dodragdrop_drop_occured 1
  _update_cursor clock

  if {!$_dodragdrop_drop_target_accepts_drop} {
    _SendXdndLeave
    _HandleXdndFinished {}
    return
  }
  #DBG debug "XdndDrop: $_dodragdrop_drop_target"
  variable _dodragdrop_drop_timestamp
  set _dodragdrop_drop_timestamp [_send_XdndDrop \
                 $_dodragdrop_drag_source $_dodragdrop_drop_target \
                 $_dodragdrop_drop_target_proxy]
  set _dodragdrop_drop_target 0
  #DBG debug "XdndDrop: $_dodragdrop_drop_target"
  ## Arrange a timeout for receiving XdndFinished...
  variable _dodragdrop_xdnd_finished_event_after_id
  set _dodragdrop_xdnd_finished_event_after_id \
    [after 10000 [list ::tkdnd::xdnd::_HandleXdndFinished {}]]
};# xdnd::_SendXdndDrop

# ----------------------------------------------------------------------------
#  Command xdnd::_update_cursor
# ----------------------------------------------------------------------------
proc xdnd::_update_cursor { {cursor {}}} {
  #DBG debug "_update_cursor $cursor"
  variable _dodragdrop_current_cursor
  variable _dodragdrop_drag_source
  variable _dodragdrop_drop_target_accepts_drop
  variable _dodragdrop_drop_target_accepts_action

  if {![string length $cursor]} {
    set cursor refuse_drop
    if {$_dodragdrop_drop_target_accepts_drop} {
      set cursor $_dodragdrop_drop_target_accepts_action
    }
  }
  if {![string equal $cursor $_dodragdrop_current_cursor]} {
    _set_pointer_cursor $_dodragdrop_drag_source [_get_mapped_cursor $cursor]
    set _dodragdrop_current_cursor $cursor
  }
};# xdnd::_update_cursor

# ----------------------------------------------------------------------------
#  Command xdnd::_get_mapped_cursor
# ----------------------------------------------------------------------------
proc xdnd::_get_mapped_cursor { cursor } {
  variable _dodragdrop_cursor_map
  variable _dodragdrop_drag_source
  ## Is there a custom cursor map?
  if {[catch {dict get $_dodragdrop_cursor_map $cursor} mapped]} {
    ## Do not report the error, ignore the mapping.
    set mapped $cursor
  }
  ## Is there a cursor feedback command?
  set cmd [bind $_dodragdrop_drag_source <<DragCursorFeedback>>]
  if {$cmd ne ""} {
    set code [catch {uplevel \#0 $cmd \{$_dodragdrop_drag_source\} \{$cursor\} \{$mapped\}} info options]
    #DBG debug "CODE: $code ---- $info"
    switch -exact -- $code {
      0 {if {$info ne ""} {set mapped $info}}
      default {
        return -options $options $info
      }
    }
  }
  return $mapped
};# xdnd::_get_mapped_cursor

# ----------------------------------------------------------------------------
#  Command xdnd::_default_action
# ----------------------------------------------------------------------------
proc xdnd::_default_action {event} {
  variable _dodragdrop_actions
  variable _dodragdrop_actions_len
  if {$_dodragdrop_actions_len == 1} {return [lindex $_dodragdrop_actions 0]}

  set alt     [dict get $event Alt]
  set shift   [dict get $event Shift]
  set control [dict get $event Control]

  if {$shift && $control && [lsearch $_dodragdrop_actions link] != -1} {
    return link
  } elseif {$control && [lsearch $_dodragdrop_actions copy] != -1} {
    return copy
  } elseif {$shift && [lsearch $_dodragdrop_actions move] != -1} {
    return move
  } elseif {$alt && [lsearch $_dodragdrop_actions link] != -1} {
    return link
  }
  return default
};# xdnd::_default_action

# ----------------------------------------------------------------------------
#  Command xdnd::getFormatForType
# ----------------------------------------------------------------------------
proc xdnd::getFormatForType {type} {
  switch -glob [string tolower $type] {
    text/plain\;charset=utf-8 -
    text/html\;charset=utf-8  -
    utf8_string               {set format UTF8_STRING}
    text/html                 -
    text/plain                -
    string                    -
    text                      -
    compound_text             {set format STRING}
    application/x-color       {set format $type}
    default                   {set format $type}
  }
  return $format
};# xdnd::getFormatForType

# ----------------------------------------------------------------------------
#  Command xdnd::registerSelectionHandler
# ----------------------------------------------------------------------------
proc xdnd::registerSelectionHandler {source types} {
  foreach type $types {
    selection handle -selection XdndSelection \
                     -type $type \
                     -format [getFormatForType $type] \
                     $source [list ::tkdnd::xdnd::_SendData $type]
  }
};# xdnd::registerSelectionHandler

# ----------------------------------------------------------------------------
#  Command xdnd::unregisterSelectionHandler
# ----------------------------------------------------------------------------
proc xdnd::unregisterSelectionHandler {source types} {
  foreach type $types {
    catch {
      selection handle -selection XdndSelection \
                       -type $type \
                       -format [getFormatForType $type] \
                       $source {}
    }
  }
};# xdnd::unregisterSelectionHandler

# ----------------------------------------------------------------------------
#  Command xdnd::_convert_to_unsigned
# ----------------------------------------------------------------------------
proc xdnd::_convert_to_unsigned {data format} {
  switch $format {
    8  { set mask 0xff }
    16 { set mask 0xffff }
    32 { set mask 0xffffff }
    default {error "unsupported format $format"}
  }
  ## Convert signed integer into unsigned...
  set d [list]
  foreach num $data {
    lappend d [expr { $num & $mask }]
  }
  return $d
};# xdnd::_convert_to_unsigned

# ----------------------------------------------------------------------------
#  Command xdnd::_SendData
# ----------------------------------------------------------------------------
proc xdnd::_SendData {type offset bytes args} {
  variable _dodragdrop_drag_source
  variable _dodragdrop_types
  variable _dodragdrop_data
  variable _dodragdrop_transfer_data

  ## The variable _dodragdrop_data contains a list of data, one for each
  ## type in the _dodragdrop_types variable. We have to search types, and find
  ## the corresponding entry in the _dodragdrop_data list.
  set index [lsearch $_dodragdrop_types $type]
  if {$index < 0} {
    error "unable to locate data suitable for type \"$type\""
  }
  set typed_data [lindex $_dodragdrop_data $index]
  set format 8
  if {$offset == 0} {
    ## Prepare the data to be transferred...
    switch -glob $type {
      text/plain* - UTF8_STRING - STRING - TEXT - COMPOUND_TEXT {
        binary scan [::tkdnd::to_encoding utf-8 $typed_data] \
                    c* _dodragdrop_transfer_data
        set _dodragdrop_transfer_data \
           [_convert_to_unsigned $_dodragdrop_transfer_data $format]
      }
      text/uri-list* {
        set files [list]
        foreach file $typed_data {
          switch -glob $file {
            *://*     {lappend files $file}
            default   {lappend files file://$file}
          }
        }
        binary scan [::tkdnd::to_encoding utf-8 "[join $files \r\n]\r\n"] \
                    c* _dodragdrop_transfer_data
        set _dodragdrop_transfer_data \
           [_convert_to_unsigned $_dodragdrop_transfer_data $format]
      }
      application/x-color {
        set format 16
        ## Try to understand the provided data: we accept a standard Tk colour,
        ## or a list of 3 values (red green blue) or a list of 4 values
        ## (red green blue opacity).
        switch [llength $typed_data] {
          1 { set color [winfo rgb $_dodragdrop_drag_source $typed_data]
              lappend color 65535 }
          3 { set color $typed_data; lappend color 65535 }
          4 { set color $typed_data }
          default {error "unknown color data: \"$typed_data\""}
        }
        ## Convert the 4 elements into 16 bit values...
        set _dodragdrop_transfer_data [list]
        foreach c $color {
          lappend _dodragdrop_transfer_data [format 0x%04X $c]
        }
      }
      default {
        set format 32
        binary scan [encoding convertto utf-8 $typed_data] c* _dodragdrop_transfer_data
      }
    }
  }

  ##
  ## Data has been split into bytes. Count the bytes requested, and return them
  ##
  set data [lrange $_dodragdrop_transfer_data $offset [expr {$offset+$bytes-1}]]
  switch $format {
    8  {
      set data [::tkdnd::from_encoding utf-8 [binary format c* $data]]
    }
    16 {
      variable _dodragdrop_selection_requestor
      if {$_dodragdrop_selection_requestor} {
        ## Tk selection cannot process this format (only 8 & 32 supported).
        ## Call our XChangeProperty...
        set numItems [llength $data]
        variable _dodragdrop_selection_property
        variable _dodragdrop_selection_selection
        variable _dodragdrop_selection_target
        variable _dodragdrop_selection_time
        XChangeProperty $_dodragdrop_drag_source \
                        $_dodragdrop_selection_requestor \
                        $_dodragdrop_selection_property \
                        $_dodragdrop_selection_target \
                        $format \
                        $_dodragdrop_selection_time \
                        $data $numItems
        return -code break
      }
    }
    32 {
    }
    default {
      error "unsupported format $format"
    }
  }
  #DBG debug "SendData: $type $offset $bytes $args ($typed_data)"
  #DBG debug "          $data"
  return $data
};# xdnd::_SendData
