package Tk::DragDrop;
require Tk::DragDrop::Common;
require Tk::Toplevel;
require Tk::Label;

use vars qw($VERSION);
$VERSION = sprintf '4.%03d', q$Revision: #14 $ =~ /\D(\d+)\s*$/;

use base  qw(Tk::DragDrop::Common Tk::Toplevel);

# This is a little tricky, ISA says 'Toplevel' but we
# define a Tk_cmd to actually build a 'Label', then
# use wmRelease in Populate to make it a toplevel.

my $useWmRelease = Tk::Wm->can('release'); # ($^O ne 'MSWin32');

sub Tk_cmd { ($useWmRelease) ? \&Tk::label : \&Tk::toplevel }

Construct Tk::Widget 'DragDrop';

use strict;
use vars qw(%type @types);
use Carp;


# There is a snag with having a token window and moving to
# exactly where cursor is - the cursor is "inside" the token
# window - hence it is not "inside" the dropsite window
# so we offset X,Y by OFFSET pixels.
sub OFFSET () {3}

sub ClassInit
{
 my ($class,$mw) = @_;
 $mw->bind($class,'<Map>','Mapped');
 $mw->bind($class,'<Any-KeyPress>','Done');
 $mw->bind($class,'<Any-ButtonRelease>','Drop');
 $mw->bind($class,'<Any-Motion>','Drag');
 return $class;
}

sub Populate
{
 my ($token,$args) = @_;
 my $parent = $token->parent;
 if ($useWmRelease)
  {
   $token->wmRelease;
   $token->ConfigSpecs(-text => ['SELF','text','Text',$parent->class]);
  }
 else
  {
   my $lab = $token->Label->pack(-expand => 1, -fill => 'both');
   bless $lab,ref($token);
   $lab->bindtags([ref($token), $lab, $token, 'all']);
   $token->ConfigSpecs(-text => [$lab,'text','Text',$parent->class],
                       DEFAULT => [$lab]);
  }
 $token->withdraw;
 $token->overrideredirect(1);
 $token->ConfigSpecs(-sitetypes       => ['METHOD','siteTypes','SiteTypes',undef],
                     -startcommand    => ['CALLBACK',undef,undef,undef],
                     -endcommand      => ['CALLBACK',undef,undef,undef],
                     -predropcommand  => ['CALLBACK',undef,undef,undef],
                     -postdropcommand => ['CALLBACK',undef,undef,undef],
                     -delta           => ['PASSIVE','delta','Delta',10],
                     -cursor          => ['SELF','cursor','Cursor','hand2'],
                     -handlers        => ['SETMETHOD','handlers','Handlers',[[[$token,'SendText']]]],
                     -selection       => ['SETMETHOD','selection','Selection','XdndSelection'],
                     -event           => ['SETMETHOD','event','Event','<B1-Motion>']
                    );
 $token->{InstallHandlers} = 0;
 $args->{-borderwidth} = 3;
 $args->{-relief} = 'flat';
 $args->{-takefocus} = 1;
}

sub sitetypes
{
 my ($w,$val) = @_;
 confess "Not a widget $w" unless (ref $w);
 my $var = \$w->{Configure}{'-sitetypes'};
 if (@_ > 1)
  {
   if (defined $val)
    {
     $val = [$val] unless (ref $val);
     my $type;
     foreach $type (@$val)
      {
       Tk::DragDrop->import($type);
      }
    }
   $$var = $val;
  }
 return (defined $$var) ? $$var : \@types;
}

sub SendText
{
 my ($w,$offset,$max) = @_;
 my $s = substr($w->cget('-text'),$offset);
 $s = substr($s,0,$max) if (length($s) > $max);
 return $s;
}

sub handlers
{
 my ($token,$opt,$value) = @_;
 $token->{InstallHandlers} = (defined($value) && @$value);
 $token->{'handlers'}  = $value;
}

sub selection
{
 my ($token,$opt,$value) = @_;
 my $handlers = $token->{'handlers'};
 $token->{InstallHandlers} = (defined($handlers) && @$handlers);
}

sub event
{
 my ($w,$opt,$value) = @_;
 # delete old bindings
 $w->parent->Tk::bind($value,[$w,'StartDrag']);
}

#

sub FindSite
{
 my ($token,$X,$Y,$e) = @_;
 my $site;
 my $types = $token->sitetypes;
 if (defined $types && @$types)
  {
   foreach my $type (@$types)
    {
     my $class = $type{$type};
     last if (defined($class) && ($site = $class->FindSite($token,$X,$Y)));
    }
  }
 else
  {
   warn 'No sitetypes';
  }
 my $new = $site || 'undef';
 my $over = $token->{'Over'};
 if ($over)
  {
   if (!$over->Match($site))
    {
     $over->Leave($token,$e);
     delete $token->{'Over'};
    }
  }
 if ($site)
  {
   unless ($token->{'Over'})
    {
     $site->Enter($token,$e);
     $token->{'Over'} = $site;
    }
   $site->Motion($token,$e) if (defined $site)
  }
 return $site;
}

sub Mapped
{
 my ($token) = @_;
 my $e = $token->parent->XEvent;
 $token = $token->toplevel;
 $token->grabGlobal;
 $token->focus;
 if (defined $e)
  {
   my $X = $e->X;
   my $Y = $e->Y;
   $token->MoveToplevelWindow($X+OFFSET,$Y+OFFSET);
   $token->NewDrag;
   $token->FindSite($X,$Y,$e);
  }
}

sub NewDrag
{
 my ($token) = @_;
 my $types = $token->sitetypes;
 if (defined $types && @$types)
  {
   my $type;
   foreach $type (@$types)
    {
     my $class = $type{$type};
     if (defined $class)
      {
       $class->NewDrag($token);
      }
    }
  }
}

sub Drag
{
 my $token = shift;
 my $e = $token->XEvent;
 my $X  = $e->X;
 my $Y  = $e->Y;
 $token = $token->toplevel;
 $token->MoveToplevelWindow($X+OFFSET,$Y+OFFSET);
 $token->FindSite($X,$Y,$e);
}

sub Done
{
 my $token = shift;
 my $e     = $token->XEvent;
 $token    = $token->toplevel;
 my $over  = delete $token->{'Over'};
 $over->Leave($token,$e) if (defined $over);
 my $w     = $token->parent;
 eval {local $SIG{__DIE__}; $token->grabRelease };
 $token->withdraw;
 delete $w->{'Dragging'};
 $w->update;
}

sub AcceptDrop
{
 my ($token) = @_;
 $token->configure(-relief => 'sunken');
 $token->{'Accepted'} = 1;
}

sub RejectDrop
{
 my ($token) = @_;
 $token->configure(-relief => 'flat');
 $token->{'Accepted'} = 0;
}

sub HandleLoose
{
 my ($w,$seln) = @_;
 return '';
}

sub InstallHandlers
{
 my ($token,$seln) = @_;
 my $w = $token->parent;
 $token->configure('-selection' => $seln) if $seln;
 $seln = $token->cget('-selection');
 if ($token->{InstallHandlers})
  {
   foreach my $h (@{$token->cget('-handlers')})
    {
     $w->SelectionHandle('-selection' => $seln,@$h);
    }
   $token->{InstallHandlers} = 0;
  }
 if (!$w->IS($w->SelectionOwner('-selection'=>$seln)))
  {
   $w->SelectionOwn('-selection' => $seln, -command => [\&HandleLoose,$w,$seln]);
  }
}

sub Drop
{
 my $ewin  = shift;
 my $e     = $ewin->XEvent;
 my $token = $ewin->toplevel;
 my $site  = $token->FindSite($e->X,$e->Y,$e);
 Tk::catch { $token->grabRelease };
 if (defined $site)
  {
   my $seln = $token->cget('-selection');
   unless ($token->Callback(-predropcommand => $seln, $site))
    {
# XXX This is ugly if the user restarts a drag within the 2000 ms:
#     my $id = $token->after(2000,[$token,'Done']);
     my $w = $token->parent;
     $token->InstallHandlers;
     $site->Drop($token,$seln,$e);
     $token->Callback(-postdropcommand => $seln);
     $token->Done;
    }
  }
 else
  {
   $token->Done;
  }
 $token->Callback('-endcommand');
}

sub StartDrag
{
 my $token = shift;
 my $w     = $token->parent;
 unless ($w->{'Dragging'})
  {
   my $e = $w->XEvent;
   my $X = $e->X;
   my $Y = $e->Y;
   my $was = $token->{'XY'};
   if ($was)
    {
     my $dx = $was->[0] - $X;
     my $dy = $was->[1] - $Y;
     if (sqrt($dx*$dx+$dy*$dy) > $token->cget('-delta'))
      {
       unless ($token->Callback('-startcommand',$token,$e))
        {
         delete $token->{'XY'};
         $w->{'Dragging'} = $token;
         $token->MoveToplevelWindow($X+OFFSET,$Y+OFFSET);
         $token->raise;
         $token->deiconify;
         $token->FindSite($X,$Y,$e);
        }
      }
    }
   else
    {
     $token->{'XY'} = [$X,$Y];
    }
  }
}


1;
