

package Tk::DoubleClick;

our $VERSION = '1.000';

use strict;
use warnings;

require Exporter;

our @ISA    = qw(Exporter);
our @EXPORT = qw(bind_clicks);

#  Track last-clicked mouse number, widget, "after" event id and callback.
my $h_pend = { 'mn' => 0, 'wi' => 0, 'id' => 0, 'cb' => 0 };


sub bind_clicks {
	my ($widget, $a_single, $a_double, %args)  = @_;

	my $delay    = delete $args{-delay}  || 300;
	my $button   = delete $args{-button} || 'left';
	my $h_button = { left => 1, middle => 2, right => 3 };
	my $mousenum = $h_button->{$button} || $button;
	($mousenum  =~ /^[123]$/) or $mousenum = 1;

	my $c_single = $a_single;
	if (ref $a_single eq 'ARRAY') {
		my $c_cmd = shift @$a_single;
		$c_single = sub { $c_cmd->(@$a_single) };
	}

	my $c_double = $a_double;
	if (ref $a_double eq 'ARRAY') {
		my $c_cmd = shift @$a_double;
		$c_double = sub { $c_cmd->(@$a_double) };
	}

	my $button_name    = "<Button-$mousenum>";

	my $c_pending = sub {
		my ($mousenum, $widget, $id) = @_;
		$h_pend->{'mn'} = $mousenum;
		$h_pend->{'wi'} = $widget;
		$h_pend->{'id'} = $id;
		$h_pend->{'cb'} = $c_single;
	};

	my $c_cmd = sub {
		my $b_sched  = 0;    # Schedule new single-click?

		if (!$h_pend->{'id'}) {
			# No click is pending -- schedule a new one
			$b_sched = 1;
		} else {
			# Cancel pending single-click event
			$h_pend->{'wi'}->afterCancel($h_pend->{'id'});
			$h_pend->{'id'} = 0;

			if ($h_pend->{'mn'} == $mousenum and $h_pend->{'wi'} eq $widget) {
				# Invoke double-click callback and reset pending event
				$c_double->();
				$c_pending->(0, 0, 0);
			} else {
				# Invoke previous single-click, and schedule a new one
				$h_pend->{'cb'}->();
				$b_sched = 1;
			}
		}

		# Schedule new single-click subroutine when $delay expires
		if ($b_sched) {
			my $c_after = sub { $c_pending->(0, 0, 0); $c_single->() };
			my $id = $widget->after($delay => $c_after);
			$c_pending->($mousenum, $widget, $id);
		}
	};

	$widget->bind($button_name => $c_cmd);
}


1;


=head1 NAME

Tk::Doubleclick - Correctly handle single-click vs double-click events,
calling only the appropriate callback for the given event.

=head1 SYNOPSIS

    use Tk::Doubleclick;

    bind_clicks(
        $widget,
        [ \&single_callback, @args ],    # Single callback with args
        \&double_callback,               # Double callback without args
        -delay  => 500,
        -button => 'right',
    );


=head1 REQUIRED PARAMETERS

=over 5

=item $widget

Widget to bind to mousebuttons.  Typically a Tk::Button object, but could
actually be almost any widget.

=item [ \&single_click_callback, @single_click_args ],

The callback subroutine to invoke when the event is a single-click, along
with the arguments to pass.  When no arguments are passed, the brackets
can be omitted.

=item [ \&double_click_callback, @double_click_args ],

The callback subroutine to invoke when the event is a double-click, along
with the arguments to pass.  When no arguments are passed, the brackets
can be omitted.

=back

=head1 OPTIONS

=over 5

=item -delay

Maximum delay time detween clicks in milliseconds. Default is 300.
If the second click of a two proximate mouse clicks occurs within the given
delay time, the event is considered a double-click.  If not, the two clicks
are considered two separate (albeit nearly simultaneous) single-clicks.

=item -button

Mouse button to bind. Options are 1, 2, 3, or the corresponding synonyms
'left', 'middle', or 'right'.  The default is 1 ('left').

=back

=head1 EXAMPLE

    # Libraries
    use strict;
    use warnings;
    use Tk;
    use Tk::DoubleClick;

    # User-defined
    my $a_colors  = [
        [ '#8800FF', '#88FF88', '#88FFFF' ],
        [ '#FF0000', '#FF0088', '#FF00FF' ],
        [ '#FF8800', '#FF8888', '#FF88FF' ],
        [ '#FFFF00', '#FFFF88', '#FFFFFF' ],
    ];

    # Main program
    my $nsingle = my $ndouble = 0;
    my $mw      = new MainWindow(-title => "Double-click example");
    my $f1      = $mw->Frame->pack(-expand => 1, -fill => 'both');
    my @args    = qw( -width 12 -height 2 -relief groove -borderwidth 4 );
    my @pack    = qw( -side left -expand 1 -fill both );

    # Display single/double click counts
    my $lb1 = $f1->Label(-text    => "Single Clicks", @args);
    my $lb2 = $f1->Label(-textvar => \$nsingle,       @args);
    my $lb3 = $f1->Label(-text    => "Double Clicks", @args);
    my $lb4 = $f1->Label(-textvar => \$ndouble,       @args);
    $lb1->pack($lb2, $lb3, $lb4, @pack);

    # Create button for each color, and bind single/double clicks to it
    foreach my $a_color (@$a_colors) {
        my $fr = $mw->Frame->pack(-expand => 1, -fill => 'both');
        foreach my $bg (@$a_color) {
            my $b = $fr->Button(-bg => $bg, -text => $bg, @args);
            $b->pack(@pack);
            bind_clicks($b, [\&single, $lb2, $bg], [\&double, $lb4, $bg]);
        }
    }

    # Make 'Escape' quit the program
    $mw->bind("<Escape>" => sub { exit });

    MainLoop;


    # Callbacks
    sub single {
        my ($lbl, $color) = @_;
        $lbl->configure(-bg => $color);
        ++$nsingle;
    }

    sub double {
        my ($lbl, $color) = @_;
        $lbl->configure(-bg => $color);
        ++$ndouble;
    }


=head1 ACKNOWLEDGEMENTS

Thanks to Mark Freeman for numerous great suggestions and documentation help.

=head1 AUTHOR

John C. Norton        jchnorton@verizon.net

Copyright (c) 2009 John C. Norton. All rights reserved.
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.

=head1 VERSION

Version 1.000  (August 2009)

=head1 REQUIREMENTS

The Tk module is required.

=head1 SEE ALSO

perl(1)

=cut




